pax_global_header00006660000000000000000000000064145470203640014517gustar00rootroot0000000000000052 comment=e99036415ec0cf0f75c1d0b8d60fdd91af0d6c68 colmap-3.9.1/000077500000000000000000000000001454702036400130045ustar00rootroot00000000000000colmap-3.9.1/.azure-pipelines/000077500000000000000000000000001454702036400161765ustar00rootroot00000000000000colmap-3.9.1/.azure-pipelines/build-docker.yaml000066400000000000000000000023321454702036400214260ustar00rootroot00000000000000parameters: displayName: 'Docker' jobs: - job: docker_build displayName: '${{ parameters.displayName }}' pool: vmImage: 'ubuntu-latest' variables: DOCKER_BUILDKIT: 1 ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: CudaArchs: 50 ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: # TODO: With CMake 3.23, we can set "all-major" instead. CudaArchs: 50;60;70;75;90 steps: - bash: | echo "Building git commit: $(Build.SourceVersion)" displayName: "Log git commit" - task: Docker@2 displayName: Build inputs: command: build arguments: --build-arg COLMAP_GIT_COMMIT=$(Build.SourceVersion) --build-arg CUDA_ARCHITECTURES=$(CudaArchs) Dockerfile: docker/Dockerfile repository: colmap/colmap tags: | latest $(Build.BuildNumber) - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - task: Docker@2 displayName: Login to Docker Hub inputs: command: login containerRegistry: dockerhub-colmap - task: Docker@2 displayName: Push to Docker Hub inputs: command: push repository: colmap/colmap tags: | latest $(Build.BuildNumber) colmap-3.9.1/.azure-pipelines/build-mac.yaml000066400000000000000000000034501454702036400207210ustar00rootroot00000000000000parameters: displayName: Mac 10.15 macVersion: '10.15' jobs: - job: mac_build_${{ replace(parameters.macVersion, '.', '') }} displayName: ${{ parameters.displayName }} pool: vmImage: 'macOS-${{ parameters.macVersion }}' variables: COMPILER_CACHE_DIR: $(Pipeline.Workspace)/compiler-cache COMPILER_CACHE_VERSION: 2 CCACHE_DIR: $(COMPILER_CACHE_DIR)/ccache CCACHE_BASEDIR: $(Build.SourcesDirectory) steps: - script: | brew install \ cmake \ ninja \ boost \ eigen \ flann \ freeimage \ metis \ glog \ googletest \ ceres-solver \ qt5 \ glew \ cgal \ sqlite3 \ ccache displayName: Install dependencies - task: Cache@2 inputs: key: ccache | "$(COMPILER_CACHE_VERSION)" | "$(Agent.OS)" | "${{ parameters.displayName }}" | "$(Build.BuildNumber)" restoreKeys: ccache | "$(COMPILER_CACHE_VERSION)" | "$(Agent.OS)" | "${{ parameters.displayName }}" path: $(COMPILER_CACHE_DIR) displayName: Restore compiler cache - script: | export PATH="/usr/local/opt/qt@5/bin:$PATH" cmake --version mkdir build cd build cmake .. \ -GNinja \ -DTESTS_ENABLED=ON \ -DQt5_DIR=/usr/local/opt/qt/lib/cmake/Qt5 ninja displayName: Configure and build - script: | cd build ctest tests_pass=$? if [ $tests_pass -ne 0 ]; then echo "\n\n\nTests failed, rerunning with verbose output" ctest --rerun-failed --output-on-failure fi exit $tests_pass displayName: Run tests - script: | set -x ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose displayName: Cleanup compiler cache colmap-3.9.1/.azure-pipelines/build-ubuntu.yaml000066400000000000000000000201231454702036400214770ustar00rootroot00000000000000parameters: displayName: Ubuntu 20.04 ubuntuVersion: '20.04' cudaEnabled: false asanEnabled: false e2eTestsEnabled: false checkCodeFormat: false cmakeBuildType: Release guiEnabled: true jobs: - job: ubuntu_build_${{ replace(parameters.ubuntuVersion, '.', '') }}_cuda_${{ parameters.cudaEnabled }}_asan_${{ parameters.asanEnabled }}_${{ parameters.cmakeBuildType }}_e2e_${{ parameters.e2eTestsEnabled }} displayName: ${{ parameters.displayName }} timeoutInMinutes: 120 pool: vmImage: 'ubuntu-${{ parameters.ubuntuVersion }}' variables: COMPILER_CACHE_DIR: $(Pipeline.Workspace)/compiler-cache COMPILER_CACHE_VERSION: 2 CCACHE_DIR: $(COMPILER_CACHE_DIR)/ccache CCACHE_BASEDIR: $(Build.SourcesDirectory) CTCACHE_DIR: $(COMPILER_CACHE_DIR)/ctcache ${{ if and(eq(parameters.asanEnabled, false), eq(parameters.guiEnabled, true)) }}: ctestExclusions: "(mvs/colmap_mvs_gpu_mat_test)" ${{ if or(eq(parameters.asanEnabled, true), eq(parameters.guiEnabled, false)) }}: ctestExclusions: "(feature/colmap_feature_sift_test)|(util/colmap_util_opengl_utils_test)|(mvs/colmap_mvs_gpu_mat_test)" ${{ if eq(parameters.cmakeBuildType, 'ClangTidy') }}: # Do not abort compilation in ClangTidy mode, so we see issues # for all targets logged out to the console. numAllowedNinjaErrors: 1000 ${{ if ne(parameters.cmakeBuildType, 'ClangTidy') }}: numAllowedNinjaErrors: 1 steps: - ${{ if eq(parameters.checkCodeFormat, true) }}: - script: | set +x -euo pipefail sudo apt-get install -y clang-format-14 black ./scripts/format/clang_format.sh ./scripts/format/black.sh git diff --name-only | xargs -r printf "##vso[task.LogIssue type=error;sourcepath=%s;linenumber=1;columnnumber=1;]Invalid code format, run scripts/format/*.sh\n" git diff --exit-code || echo "##vso[task.complete result=Failed;]" displayName: Check code format - script: | sudo apt-get update && sudo apt-get install -y \ build-essential \ ninja-build \ libboost-program-options-dev \ libboost-filesystem-dev \ libboost-graph-dev \ libboost-system-dev \ libeigen3-dev \ libceres-dev \ libflann-dev \ libfreeimage-dev \ libmetis-dev \ libgoogle-glog-dev \ libgtest-dev \ libsqlite3-dev \ libglew-dev \ qtbase5-dev \ libqt5opengl5-dev \ libcgal-dev \ libcgal-qt5-dev \ libgl1-mesa-dri \ libunwind-dev \ xvfb displayName: Install dependencies - ${{ if eq(parameters.cudaEnabled, true) }}: - ${{ if eq(parameters.ubuntuVersion, '22.04') }}: - script: | sudo apt-get install -y \ nvidia-cuda-toolkit \ nvidia-cuda-toolkit-gcc \ gcc-10 g++-10 echo '##vso[task.setvariable variable=CC]/usr/bin/gcc-10' echo '##vso[task.setvariable variable=CXX]/usr/bin/g++-10' echo '##vso[task.setvariable variable=CUDAHOSTCXX]/usr/bin/g++-10' displayName: Install CUDA - ${{ if ne(parameters.ubuntuVersion, '22.04') }}: - script: | sudo apt-get install -y \ nvidia-cuda-toolkit \ nvidia-cuda-toolkit-gcc echo '##vso[task.setvariable variable=CC]/usr/bin/cuda-gcc' echo '##vso[task.setvariable variable=CXX]/usr/bin/cuda-g++' displayName: Install CUDA - ${{ if eq(parameters.asanEnabled, true) }}: - script: | sudo apt-get install -y clang-15 libomp-15-dev echo '##vso[task.setvariable variable=CC]/usr/bin/clang-15' echo '##vso[task.setvariable variable=CXX]/usr/bin/clang++-15' displayName: Install Clang - ${{ if eq(parameters.cmakeBuildType, 'ClangTidy') }}: - script: | sudo apt-get install -y clang-15 clang-tidy-15 libomp-15-dev echo '##vso[task.setvariable variable=CC]/usr/bin/clang-15' echo '##vso[task.setvariable variable=CXX]/usr/bin/clang++-15' displayName: Install Clang - task: Cache@2 inputs: key: ccache | "$(COMPILER_CACHE_VERSION)" | "$(Agent.OS)" | "${{ parameters.displayName }}" | "$(Build.BuildNumber)" restoreKeys: ccache | "$(COMPILER_CACHE_VERSION)" | "$(Agent.OS)" | "${{ parameters.displayName }}" path: $(COMPILER_CACHE_DIR) cacheHitVar: CCACHE_RESTORED displayName: Restore compiler cache - script: | 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 tar xfv ccache-4.8.2-linux-x86_64.tar.xz mkdir -p "$(COMPILER_CACHE_DIR)/bin" mv ./ccache-4.8.2-linux-x86_64/ccache "$(COMPILER_CACHE_DIR)/bin" ctcache_commit_id="66c3614175fc650591488519333c411b2eac15a3" wget https://github.com/matus-chochlik/ctcache/archive/${ctcache_commit_id}.zip echo "108b087f156a9fe7da0c796de1ef73f5855d2a33a27983769ea39061359a40fc ${ctcache_commit_id}.zip" | sha256sum --check unzip "${ctcache_commit_id}.zip" mv ctcache-${ctcache_commit_id}/clang-tidy* "$(COMPILER_CACHE_DIR)/bin" displayName: Install compiler cache condition: ne(variables.CCACHE_RESTORED, 'true') - script: | echo "##vso[task.prependpath]$(COMPILER_CACHE_DIR)/bin" mkdir -p "$(CCACHE_DIR)" "$(CTCACHE_DIR)" displayName: Setup compiler cache - script: | set -x echo "##vso[task.prependpath]$(COMPILER_CACHE_DIR)/bin" cmake --version mkdir build cd build cmake .. \ -GNinja \ -DCMAKE_BUILD_TYPE=${{ parameters.cmakeBuildType }} \ -DCMAKE_INSTALL_PREFIX=./install \ -DTESTS_ENABLED=ON \ -DCMAKE_CUDA_ARCHITECTURES=50 \ -DASAN_ENABLED=${{ parameters.asanEnabled }} \ -DGUI_ENABLED=${{ parameters.guiEnabled }} ninja -k $(numAllowedNinjaErrors) displayName: Configure and build colmap - ${{ if and(eq(parameters.cmakeBuildType, 'Release'), eq(parameters.asanEnabled, false)) }}: - script: | set -x cd build ninja install cd ../doc/sample-project mkdir build cd build colmap_DIR=$(Build.SourcesDirectory)/build/install/share/colmap cmake .. \ -GNinja \ -DCMAKE_CUDA_ARCHITECTURES=50 ninja ./hello_world --message "world" displayName: Install colmap and build sample - ${{ if ne(parameters.cmakeBuildType, 'ClangTidy') }}: - script: | export DISPLAY=":99.0" export QT_QPA_PLATFORM="offscreen" Xvfb :99 & sleep 3 cd build ctest -E "$(ctestExclusions)" tests_pass=$? if [ $tests_pass -ne 0 ]; then echo "Tests failed, rerunning with verbose output" ctest --rerun-failed --output-on-failure fi exit $tests_pass displayName: Run tests - ${{ if eq(parameters.e2eTestsEnabled, true) }}: - script: | export DISPLAY=":99.0" export QT_QPA_PLATFORM="offscreen" Xvfb :99 & sleep 3 sudo apt install 7zip mkdir eth3d_benchmark # Error thresholds in degrees and meters, # as the ETH3D groundtruth has metric scale. python ./scripts/python/benchmark_eth3d.py \ --workspace_path ./eth3d_benchmark \ --colmap_path ./build/src/colmap/exe/colmap \ --dataset_names boulders,door \ --max_rotation_error 1.0 \ --max_proj_center_error 0.05 displayName: Run E2E tests - script: | 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)" displayName: Cleanup compiler cache colmap-3.9.1/.azure-pipelines/build-windows-vcpkg.txt000066400000000000000000000004071454702036400226370ustar00rootroot00000000000000--triplet x64-windows boost-algorithm boost-filesystem boost-graph boost-heap boost-program-options boost-property-map boost-property-tree boost-regex boost-system ceres[lapack,suitesparse] cgal eigen3 flann freeimage metis gflags glew glog gtest qt5-base sqlite3colmap-3.9.1/.azure-pipelines/build-windows.yaml000066400000000000000000000066621454702036400216630ustar00rootroot00000000000000parameters: visualStudioVersion: '2019' jobs: - job: windows_build_${{ parameters.visualStudioVersion }} displayName: Windows VS${{ parameters.visualStudioVersion }} timeoutInMinutes: 360 pool: vmImage: 'windows-${{ parameters.visualStudioVersion }}' variables: vcpkgGitCommitId: 4c1734ba2a969b1e6daa0ac79bc46ead7324df90 VCPKG_BINARY_SOURCES: 'clear;nuget,https://pkgs.dev.azure.com/colmap/colmap/_packaging/vcpkg/nuget/v3/index.json,readwrite' COMPILER_CACHE_DIR: $(Pipeline.Workspace)/compiler-cache COMPILER_CACHE_VERSION: 2 CCACHE_DIR: $(COMPILER_CACHE_DIR)/ccache CCACHE_BASEDIR: $(Build.SourcesDirectory) steps: - task: NuGetAuthenticate@0 displayName: NuGet Authenticate - pwsh: | curl -L -o ` $(Build.BinariesDirectory)/ninja.zip ` https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-win.zip Expand-Archive -LiteralPath $(Build.BinariesDirectory)/ninja.zip -DestinationPath $(Build.BinariesDirectory) displayName: Prepare build - pwsh: | git clone https://github.com/microsoft/vcpkg cd vcpkg git reset --hard $(vcpkgGitCommitId) ./bootstrap-vcpkg.bat $(Build.SourcesDirectory)/scripts/shell/enter_vs_dev_shell.ps1 ./vcpkg.exe install --recurse @$(Build.SourcesDirectory)/.azure-pipelines/build-windows-vcpkg.txt --clean-after-build workingDirectory: $(Build.BinariesDirectory) displayName: Setup vcpkg - task: Cache@2 inputs: key: ccache | "$(COMPILER_CACHE_VERSION)" | "$(Agent.OS)" | "${{ parameters.visualStudioVersion }}" | "$(Build.BuildNumber)" restoreKeys: ccache | "$(COMPILER_CACHE_VERSION)" | "$(Agent.OS)" | "${{ parameters.visualStudioVersion }}" path: $(COMPILER_CACHE_DIR) cacheHitVar: CCACHE_RESTORED displayName: Restore compiler cache - pwsh: | $(Build.SourcesDirectory)/.azure-pipelines/install-ccache.ps1 -Destination "$(COMPILER_CACHE_DIR)/bin" displayName: Install compiler cache condition: ne(variables.CCACHE_RESTORED, 'true') - pwsh: | echo "##vso[task.prependpath]$(COMPILER_CACHE_DIR)/bin" New-Item -ItemType Directory -Force -Path "$(CCACHE_DIR)" displayName: Setup compiler cache - pwsh: | $(Build.SourcesDirectory)/scripts/shell/enter_vs_dev_shell.ps1 $(Build.BinariesDirectory)/vcpkg/vcpkg.exe integrate install mkdir build cd build cmake .. ` -GNinja ` -DCMAKE_MAKE_PROGRAM=$(Build.BinariesDirectory)/ninja.exe ` -DCMAKE_BUILD_TYPE=Release ` -DTESTS_ENABLED=ON ` -DCMAKE_TOOLCHAIN_FILE=$(Build.BinariesDirectory)/vcpkg/scripts/buildsystems/vcpkg.cmake ` -DVCPKG_TARGET_TRIPLET=x64-windows $(Build.BinariesDirectory)/ninja.exe workingDirectory: $(Build.SourcesDirectory) displayName: Configure and build - pwsh: | $(Build.BinariesDirectory)/vcpkg/vcpkg.exe integrate install ctest -E "(feature/colmap_feature_sift_test)|(util/colmap_util_opengl_utils_test)|(mvs/colmap_mvs_gpu_mat_test)" $tests_pass=$LastExitCode if ($tests_pass -ne 0) { echo "`n`n`nTests failed, rerunning with verbose output" ctest --rerun-failed --output-on-failure } exit $tests_pass workingDirectory: $(Build.SourcesDirectory)/build displayName: Run tests - pwsh: | ccache --show-stats --verbose ccache --evict-older-than 1d ccache --show-stats --verbose displayName: Cleanup compiler cache colmap-3.9.1/.azure-pipelines/build.yaml000066400000000000000000000025251454702036400201650ustar00rootroot00000000000000trigger: - main jobs: - template: build-windows.yaml parameters: visualStudioVersion: 2019 - template: build-windows.yaml parameters: visualStudioVersion: 2022 - template: build-ubuntu.yaml parameters: displayName: 'Ubuntu 20.04' ubuntuVersion: 20.04 - template: build-ubuntu.yaml parameters: displayName: 'Ubuntu 20.04 (CUDA)' ubuntuVersion: 20.04 cudaEnabled: true # Disable the GUI in one of the two CUDA builds to test this build path. # It is easy to break this configuration otherwise. guiEnabled: false - template: build-ubuntu.yaml parameters: displayName: 'Ubuntu 22.04' ubuntuVersion: 22.04 e2eTestsEnabled: true checkCodeFormat: true - template: build-ubuntu.yaml parameters: displayName: 'Ubuntu 22.04 (CUDA)' ubuntuVersion: 22.04 cudaEnabled: true - template: build-ubuntu.yaml parameters: displayName: 'Ubuntu 22.04 (ASan)' ubuntuVersion: 22.04 asanEnabled: true - template: build-ubuntu.yaml parameters: displayName: 'Ubuntu 22.04 (ClangTidy)' ubuntuVersion: 22.04 cmakeBuildType: ClangTidy - template: build-mac.yaml parameters: displayName: 'Mac 12' macVersion: 12 - template: build-docker.yaml parameters: displayName: 'Docker' colmap-3.9.1/.azure-pipelines/install-ccache.ps1000066400000000000000000000022531454702036400214770ustar00rootroot00000000000000[CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Destination ) $version = "4.8" $folder = "ccache-$version-windows-x86_64" $url = "https://github.com/ccache/ccache/releases/download/v$version/$folder.zip" $expectedSha256 = "A2B3BAB4BB8318FFC5B3E4074DC25636258BC7E4B51261F7D9BEF8127FDA8309" $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 } colmap-3.9.1/.clang-format000077500000000000000000000005131454702036400153610ustar00rootroot00000000000000BasedOnStyle: Google BinPackArguments: false BinPackParameters: false DerivePointerAlignment: false IncludeBlocks: Regroup IncludeCategories: - Regex: '^"colmap' Priority: 1 - Regex: '^"thirdparty' Priority: 2 - Regex: '^<[[:alnum:]_]+>' Priority: 3 - Regex: '.*' Priority: 4 SortIncludes: true colmap-3.9.1/.clang-tidy000066400000000000000000000010501454702036400150340ustar00rootroot00000000000000Checks: > performance-*, concurrency-*, bugprone-*, -bugprone-easily-swappable-parameters, -bugprone-exception-escape, -bugprone-implicit-widening-of-multiplication-result, -bugprone-narrowing-conversions, -bugprone-reserved-identifier, -bugprone-unchecked-optional-access, cppcoreguidelines-virtual-class-destructor, google-explicit-constructor, google-build-using-namespace, readability-avoid-const-params-in-decls, clang-analyzer-core*, clang-analyzer-cplusplus*, WarningsAsErrors: '*' FormatStyle: 'file' User: 'user' colmap-3.9.1/.github/000077500000000000000000000000001454702036400143445ustar00rootroot00000000000000colmap-3.9.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001454702036400165275ustar00rootroot00000000000000colmap-3.9.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011531454702036400212210ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. Input images and/or output reconstructions are usually helpful. **Environment:** - OS: [e.g. Windows 11] - COLMAP Version [e.g. 3.8 or git commit hash] - Capture Device [e.g. iPhone X] colmap-3.9.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000010601454702036400222510ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. colmap-3.9.1/.gitignore000077500000000000000000000006411454702036400150000ustar00rootroot00000000000000# Custom files LocalConfig.cmake src/colmap/util/version.cc # Custom directories .idea/ .vscode .vs .DS_Store build*/ install-debug/ install-release/ data/ doc/_build # Compiled Object files *.slo *.lo *.o *.obj *.pyc # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app colmap-3.9.1/.gitmodules000066400000000000000000000001721454702036400151610ustar00rootroot00000000000000[submodule "doc/_build/html"] path = doc/_build/html url = https://github.com/colmap/colmap.github.io.git ignore = all colmap-3.9.1/CHANGELOG.txt000066400000000000000000001252541454702036400150450ustar00rootroot00000000000000COLMAP 3.9.1 (01/08/2024) ------------------------- * Version 3.9 changelog by @ahojnnes in https://github.com/colmap/colmap/pull/2325 * Fully encapsulate freeimage in bitmap library (#2332) by @ahojnnes in https://github.com/colmap/colmap/pull/2334 COLMAP 3.9 (01/06/2024) ----------------------- * clang format all code and require clang-format-14 by @ahojnnes in https://github.com/colmap/colmap/pull/1785 * Fix compilation for vcpkg windows build by @ahojnnes in https://github.com/colmap/colmap/pull/1791 * Increment version number to 3.9 by @ahojnnes in https://github.com/colmap/colmap/pull/1794 * Remove unnecessary /arch:sse2 flag for MSVC by @ahojnnes in https://github.com/colmap/colmap/pull/1798 * Updated faq.rst by @CGCooke in https://github.com/colmap/colmap/pull/1801 * Fixed mistake in code comment for OpenCV Fisheye camera by @CGCooke in https://github.com/colmap/colmap/pull/1802 * Replace deprecated cudaThreadSynchronize with cudaDeviceSynchronize by @ahojnnes in https://github.com/colmap/colmap/pull/1806 * Replace deprecated Cuda texture references with texture objects by @ahojnnes in https://github.com/colmap/colmap/pull/1809 * Remove unused SIFT GPU cuda texture reference by @ahojnnes in https://github.com/colmap/colmap/pull/1823 * Upgrade SiftGPU to use CUDA texture objects by @ahojnnes in https://github.com/colmap/colmap/pull/1838 * Remove PBA as bundle adjustment backend to support CUDA 12+ by @ahojnnes in https://github.com/colmap/colmap/pull/1840 * Replace deprecated CUDA sature function call by @ahojnnes in https://github.com/colmap/colmap/pull/1841 * Avoid unnecessary mallocs during sampling by @ahojnnes in https://github.com/colmap/colmap/pull/1842 * Cleaned up docker readme and scripts by @ahojnnes in https://github.com/colmap/colmap/pull/1852 * add "Shared intrinsics per sub-folder" checkbox to automatic reconstruction window by @kenshi84 in https://github.com/colmap/colmap/pull/1853 * Update vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/1925 * Log the name of the file that causes Mat::Read() to checkfail by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/1923 * check Z_index correctly in ReadPly by @countywest in https://github.com/colmap/colmap/pull/1896 * Don't re-open files when reading and writing matrices by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/1926 * Update vcpkg to latest commit by @ahojnnes in https://github.com/colmap/colmap/pull/1948 * Remove unnecessary custom Eigen aligned allocator macros by @ahojnnes in https://github.com/colmap/colmap/pull/1947 * Prefix internal sources/includes with colmap by @ahojnnes in https://github.com/colmap/colmap/pull/1949 * Simplify clang-format config and sort includes by @ahojnnes in https://github.com/colmap/colmap/pull/1950 * Handle possible overflow in median function by @ahojnnes in https://github.com/colmap/colmap/pull/1951 * Run ASan pipeline under Ubuntu 22.04 by @ahojnnes in https://github.com/colmap/colmap/pull/1952 * Fix Ceres version test by @drkoller in https://github.com/colmap/colmap/pull/1954 * Fix deprecation warning for Qt font metrics width by @ahojnnes in https://github.com/colmap/colmap/pull/1958 * Setup clang-tidy and enable perf warnings by @ahojnnes in https://github.com/colmap/colmap/pull/1959 * VCPKG binary caching for windows CI by @ahojnnes in https://github.com/colmap/colmap/pull/1957 * Cosmetics for VS dev shell script by @ahojnnes in https://github.com/colmap/colmap/pull/1965 * Enable clang-tidy concurrency checks by @ahojnnes in https://github.com/colmap/colmap/pull/1967 * [Bug] fix finding shared points3D in FindLocalBundle by @wesleyliwei in https://github.com/colmap/colmap/pull/1963 * Enable compiler caching in CI by @ahojnnes in https://github.com/colmap/colmap/pull/1972 * Set number of features for different quality levels by @ahojnnes in https://github.com/colmap/colmap/pull/1975 * Specify parameter name using inline comment by @ahojnnes in https://github.com/colmap/colmap/pull/1976 * Fix Windows CCache by @ahojnnes in https://github.com/colmap/colmap/pull/1977 * Add e2e tests in CI pipeline using ETH3D datasets by @ahojnnes in https://github.com/colmap/colmap/pull/1397 * [feature] print verbose information for model analyzer by @wesleyliwei in https://github.com/colmap/colmap/pull/1978 * Add a missing include to compile with gcc13 by @EstebanDugueperoux2 in https://github.com/colmap/colmap/pull/1984 * Speed up snapshot construct in RigBundleAdjuster by @wesleyliwei in https://github.com/colmap/colmap/pull/1988 * Update outdated docker cuda image tag by @ahojnnes in https://github.com/colmap/colmap/pull/1992 * Add boulders ETH3D dataset to CI E2E tests by @ahojnnes in https://github.com/colmap/colmap/pull/1991 * Update executable paths in documentation by @ahojnnes in https://github.com/colmap/colmap/pull/1993 * Avoid unnecessary copy in ExtractTopScaleFeatures by @ahojnnes in https://github.com/colmap/colmap/pull/1994 * Move related code under new image library folder by @ahojnnes in https://github.com/colmap/colmap/pull/1995 * Move related code under new camera folder by @ahojnnes in https://github.com/colmap/colmap/pull/1996 * Added a virtual destructor to Sampler by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/2000 * Add a few more clang-tidy checks by @ahojnnes in https://github.com/colmap/colmap/pull/2001 * Move related code to new geometry module by @ahojnnes in https://github.com/colmap/colmap/pull/2006 * Use #pragma once as include guard by @ahojnnes in https://github.com/colmap/colmap/pull/2007 * Add bugprone-* clang-tidy checks by @ahojnnes in https://github.com/colmap/colmap/pull/2010 * Avoid const params in declarations by @ahojnnes in https://github.com/colmap/colmap/pull/2011 * Set and require C++14 by @ahojnnes in https://github.com/colmap/colmap/pull/2012 * Cleanup math functions that are now part of eigen/stdlib by @ahojnnes in https://github.com/colmap/colmap/pull/2013 * Add clang-analyzer checks by @ahojnnes in https://github.com/colmap/colmap/pull/2014 * Replace CMake provided find_package scripts and modern CMake targets by @ahojnnes in https://github.com/colmap/colmap/pull/2016 * Switch from Boost unit tests to Gtest by @ahojnnes in https://github.com/colmap/colmap/pull/2017 * Fix ccache restore keys in pipeline caching by @ahojnnes in https://github.com/colmap/colmap/pull/2018 * Add missing cacheHitVar to fix ccache by @ahojnnes in https://github.com/colmap/colmap/pull/2020 * Add missing Boost::graph import by @sarlinpe in https://github.com/colmap/colmap/pull/2021 * Compressed/flattened correspondence graph for faster triangulation / less memory by @ahojnnes in https://github.com/colmap/colmap/pull/2019 * Fix window ccache key by @ahojnnes in https://github.com/colmap/colmap/pull/2024 * Consistently use shared_ptr for shared pointers for SFM objects by @ahojnnes in https://github.com/colmap/colmap/pull/2023 * Remove check on Qt version by @sarlinpe in https://github.com/colmap/colmap/pull/2022 * Synthetics for E2E incremental mapper tests by @ahojnnes in https://github.com/colmap/colmap/pull/2025 * New math module by @ahojnnes in https://github.com/colmap/colmap/pull/2028 * Simplify similarity transform and more tests by @ahojnnes in https://github.com/colmap/colmap/pull/2030 * Extract reconstruction alignment functions into new file by @ahojnnes in https://github.com/colmap/colmap/pull/2032 * Add E2E hierarchical mapper tests by @ahojnnes in https://github.com/colmap/colmap/pull/2033 * Rename SimilarityTransform3 to Sim3d by @ahojnnes in https://github.com/colmap/colmap/pull/2034 * Add Rigid3d transform class by @ahojnnes in https://github.com/colmap/colmap/pull/2035 * Consolidate and simplify Rigid3d and Sim3d by @ahojnnes in https://github.com/colmap/colmap/pull/2037 * Some small improvements/cleanup for rigid3d/sim3d usage by @ahojnnes in https://github.com/colmap/colmap/pull/2041 * CamFromWorld replaces qvec/tvec by @ahojnnes in https://github.com/colmap/colmap/pull/2039 * Retry download of ETH3D datasets by @ahojnnes in https://github.com/colmap/colmap/pull/2043 * WorldToImage becomes CamToImg by @ahojnnes in https://github.com/colmap/colmap/pull/2044 * Camera models operate on camera rays by @ahojnnes in https://github.com/colmap/colmap/pull/2045 * Ignore directory .vs by @whuaegeanse in https://github.com/colmap/colmap/pull/2046 * Use the reference of Rigid3d to reduce memory consumption by @whuaegeanse in https://github.com/colmap/colmap/pull/2047 * Inline point to image projection by @ahojnnes in https://github.com/colmap/colmap/pull/2050 * Point2D becomes simpler pure data struct by @ahojnnes in https://github.com/colmap/colmap/pull/2051 * Use Eigen math for estimator utils by @ahojnnes in https://github.com/colmap/colmap/pull/2052 * Move cost functions under geometry module and rename by @ahojnnes in https://github.com/colmap/colmap/pull/2053 * Bundle adjuster is an estimator by @ahojnnes in https://github.com/colmap/colmap/pull/2054 * Remaining base targets move to new scene module by @ahojnnes in https://github.com/colmap/colmap/pull/2055 * Vote and verify improvements/speedup by @ahojnnes in https://github.com/colmap/colmap/pull/2056 * Generate version info in .cc file to reduce number of recompilations by @ahojnnes in https://github.com/colmap/colmap/pull/2057 * Option manager moves to controllers to disentangle circular deps by @ahojnnes in https://github.com/colmap/colmap/pull/2058 * Granular CMake modules and build targets by @ahojnnes in https://github.com/colmap/colmap/pull/2059 * Fix docker build by @ahojnnes in https://github.com/colmap/colmap/pull/2069 * Remove warnings about duplicated marco NOMINMAX by @whuaegeanse in https://github.com/colmap/colmap/pull/2067 * lib folder becomes thirdparty folder by @ahojnnes in https://github.com/colmap/colmap/pull/2068 * Remove unnecessary checks in image pair conversion by @ahojnnes in https://github.com/colmap/colmap/pull/2074 * Replace flaky ETH3D terrace with courtyard dataset by @ahojnnes in https://github.com/colmap/colmap/pull/2075 * Synthesize chained match graph for more mapper tests by @ahojnnes in https://github.com/colmap/colmap/pull/2076 * Introduce abstract feature extractor by @ahojnnes in https://github.com/colmap/colmap/pull/2077 * Avoid unnecessary data copies in feature conversion utils by @ahojnnes in https://github.com/colmap/colmap/pull/2078 * Abstract feature matcher by @ahojnnes in https://github.com/colmap/colmap/pull/2082 * Encapsulate feature matching controller/worker implementations by @ahojnnes in https://github.com/colmap/colmap/pull/2085 * Some cosmetics for util/feature types by @ahojnnes in https://github.com/colmap/colmap/pull/2084 * Use std:: when cmath included by @whuaegeanse in https://github.com/colmap/colmap/pull/2081 * Encapsulate feature extraction controller/worker implementations by @ahojnnes in https://github.com/colmap/colmap/pull/2086 * Reenable VS2022 CI pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1689 * Consistent transform convention for CenterAndNormalizeImagePoints by @ahojnnes in https://github.com/colmap/colmap/pull/2092 * Retire Mac 11 CI build by @ahojnnes in https://github.com/colmap/colmap/pull/2094 * Add ReprojErrorConstantPoint3DCostFunction to speed up the RefineAbsolutePose function by @whuaegeanse in https://github.com/colmap/colmap/pull/2089 * Numeric differentiation of camera model using partial piv LU by @ahojnnes in https://github.com/colmap/colmap/pull/2100 * cmake: add testing.cc to colmap_util only if TESTS_ENABLED=ON by @NeroBurner in https://github.com/colmap/colmap/pull/2102 * Set CUDA_STANDARD to 14 by @ahojnnes in https://github.com/colmap/colmap/pull/2108 * Transform back to existing images positions after mapper processing if set fixed by @ferreram in https://github.com/colmap/colmap/pull/2095 * Update documentation with new branch policy by @ahojnnes in https://github.com/colmap/colmap/pull/2110 * Update CMake find dependencies for vcpkg by @ahojnnes in https://github.com/colmap/colmap/pull/2116 * Decouple SIFT match from two view geometry options by @ahojnnes in https://github.com/colmap/colmap/pull/2118 * Fix docker build by @vnmsklnk in https://github.com/colmap/colmap/pull/2122 * Trigger build pipeline on main branch by @ahojnnes in https://github.com/colmap/colmap/pull/2123 * Update Linux install documentation with new branch policy by @joshuaoreilly in https://github.com/colmap/colmap/pull/2126 * Fix link in camera model documentation by @CFretter in https://github.com/colmap/colmap/pull/2152 * [Bugfix] Fix GUI_ENABLED=OFF and skip SiftGPU if no GUI and no CUDA by @sarlinpe in https://github.com/colmap/colmap/pull/2151 * [Bugfix] Properly handle CGAL_ENABLED by @sarlinpe in https://github.com/colmap/colmap/pull/2149 * Refinement of intrinsics in the point_triangulator by @tsattler in https://github.com/colmap/colmap/pull/2144 * Bugfix in handling COLMAP_GPU_ENABLED by @sarlinpe in https://github.com/colmap/colmap/pull/2163 * Expose exe as libs by @sarlinpe in https://github.com/colmap/colmap/pull/2165 * Add Sim3d::FromMatrix by @sarlinpe in https://github.com/colmap/colmap/pull/2147 * Check code format in CI by @ahojnnes in https://github.com/colmap/colmap/pull/2171 * Clean up dependencies by @sarlinpe in https://github.com/colmap/colmap/pull/2173 * Move tests into anonymous namespaces by @ahojnnes in https://github.com/colmap/colmap/pull/2175 * Fix glew/qopengl conflict warning by @ahojnnes in https://github.com/colmap/colmap/pull/2176 * Update documentation with new link to GitHub discussions by @ahojnnes in https://github.com/colmap/colmap/pull/2177 * Restore GLEW include by @sarlinpe in https://github.com/colmap/colmap/pull/2178 * Align reconstructions via shared 3D points by @sarlinpe in https://github.com/colmap/colmap/pull/2169 * Add clang-tidy-cachein CI by @ahojnnes in https://github.com/colmap/colmap/pull/2182 * Disable GUI build in one CI config by @ahojnnes in https://github.com/colmap/colmap/pull/2181 * Show verbose ccache stats by @ahojnnes in https://github.com/colmap/colmap/pull/2183 * Add EstimateGeneralizedAbsolutePose by @sarlinpe in https://github.com/colmap/colmap/pull/2174 * Fix bug in ReconstructionManagerWidget::Update by @whuaegeanse in https://github.com/colmap/colmap/pull/2186 * Fix missing retrieval dependency by @ahojnnes in https://github.com/colmap/colmap/pull/2189 * Removing clustering_options_ and mapper_options_ in Hierarchical Mapper Controller by @Serenitysmk in https://github.com/colmap/colmap/pull/2193 * Publish docker image to docker hub by @ahojnnes in https://github.com/colmap/colmap/pull/2195 * Fix Cuda architecture in docker build by @ahojnnes in https://github.com/colmap/colmap/pull/2196 * Fix all-major cuda arch missing in CMake < 3.23 by @ahojnnes in https://github.com/colmap/colmap/pull/2197 * Update triangulation.cc by @RayShark0605 in https://github.com/colmap/colmap/pull/2205 * Update author and acknowledgements by @ahojnnes in https://github.com/colmap/colmap/pull/2207 * Code formatting for Python by @ahojnnes in https://github.com/colmap/colmap/pull/2208 * Retire outdated build script by @ahojnnes in https://github.com/colmap/colmap/pull/2217 * Remove mention of deprecated build script by @sarlinpe in https://github.com/colmap/colmap/pull/2220 * Improve word spelling by @zchrissirhcz in https://github.com/colmap/colmap/pull/2235 * Stack allocate camera param idx arrays by @ahojnnes in https://github.com/colmap/colmap/pull/2234 * fix: typo in colmap/src/colmap/ui/project_widget.cc by @varundhand in https://github.com/colmap/colmap/pull/2241 * Update reconstruction.cc by @RayShark0605 in https://github.com/colmap/colmap/pull/2238 * Update to Docker CUDA 12.2.2 by @ahojnnes in https://github.com/colmap/colmap/pull/2244 * Stop setting C++ standard flags manually by @AdrianBunk in https://github.com/colmap/colmap/pull/2251 * Setting clear_points to true per default in point_triangulator by @tsattler in https://github.com/colmap/colmap/pull/2252 * Update cameras.rst to fix link to code by @tsattler in https://github.com/colmap/colmap/pull/2246 * Fix matching of imported features without descriptors by @ahojnnes in https://github.com/colmap/colmap/pull/2269 * Consistent versioning between documentation and code by @ahojnnes in https://github.com/colmap/colmap/pull/2275 * Reduce mallocs for RANSAC estimator models by @ahojnnes in https://github.com/colmap/colmap/pull/2283 * Migrate to glog logging by @ahojnnes in https://github.com/colmap/colmap/pull/2172 * Turn Point3D into simple data struct by @ahojnnes in https://github.com/colmap/colmap/pull/2285 * Camera becomes simple data struct by @ahojnnes in https://github.com/colmap/colmap/pull/2286 * Recover custom Eigen std::vector allocator for Eigen <3.4 support by @ahojnnes in https://github.com/colmap/colmap/pull/2293 * Replace result_of with invoke_result_t by @sarlinpe in https://github.com/colmap/colmap/pull/2300 * Allow getters FocalLength{X,Y} for isotropic models by @sarlinpe in https://github.com/colmap/colmap/pull/2301 * Add missing Boost targets and cleanup includes by @sarlinpe in https://github.com/colmap/colmap/pull/2304 * Expose IncrementalMapperOptions::{mapper,triangulation} by @sarlinpe in https://github.com/colmap/colmap/pull/2308 * Update install instructions for Mac by @Dawars in https://github.com/colmap/colmap/pull/2310 * Remove unused ceres reference in doc by @ahojnnes in https://github.com/colmap/colmap/pull/2315 * Fix typo by @whuaegeanse in https://github.com/colmap/colmap/pull/2317 * Stable version 3.9 release by @ahojnnes in https://github.com/colmap/colmap/pull/2319 COLMAP 3.8 (01/31/2023) ----------------------- * Updating geo-registration doc. by @ferreram in https://github.com/colmap/colmap/pull/1410 * Adding user-specified option for reconstructing purely planar scene. … by @ferreram in https://github.com/colmap/colmap/pull/1408 * Only apply sqlite vacuum command when elements are deleted from the database. by @ferreram in https://github.com/colmap/colmap/pull/1414 * Replace Graclus with Metis dependency by @ahojnnes in https://github.com/colmap/colmap/pull/1422 * Update ceres download URL in build script by @whuaegeanse in https://github.com/colmap/colmap/pull/1430 * Fix type errors when building colmap with build.py in windows by @whuaegeanse in https://github.com/colmap/colmap/pull/1440 * Fix bug in the computation of the statistics Global/Local BA by @whuaegeanse in https://github.com/colmap/colmap/pull/1449 * Add RefineGeneralizedAbsolutePose and covariance estimation by @Skydes in https://github.com/colmap/colmap/pull/1464 * Update docker image definition by @ahojnnes in https://github.com/colmap/colmap/pull/1478 * Upgrade deprecated ceres parameterizations to manifolds by @ahojnnes in https://github.com/colmap/colmap/pull/1477 * Use masks for stereo fusion on automatic reconstruction by @ibrarmalik in https://github.com/colmap/colmap/pull/1488 * fix random seed set failed from external interface by @WZG3661 in https://github.com/colmap/colmap/pull/1498 * Replace deprecated Eigen nonZeros() call for most recent Eigen versions. by @nackjaylor in https://github.com/colmap/colmap/pull/1494 * Fix ceres-solver folder name by @f-fl0 in https://github.com/colmap/colmap/pull/1501 * Improved convergence criterion for XYZ to ELL conversion by @ahojnnes in https://github.com/colmap/colmap/pull/1505 * Fix bug in the function SetPtr of Bitmap by @whuaegeanse in https://github.com/colmap/colmap/pull/1525 * Avoid the calling of copy constructor/assignment by @whuaegeanse in https://github.com/colmap/colmap/pull/1524 * Avoid calling copy constructors of FeatureKeypoints and FeatureDescriptors by @whuaegeanse in https://github.com/colmap/colmap/pull/1540 * Initialize freeimage if statically linked by @ahojnnes in https://github.com/colmap/colmap/pull/1549 * Avoid hard crash if Jacobian matrix is rank deficient by @mihaidusmanu in https://github.com/colmap/colmap/pull/1557 * visualize_model.py: added FULL_OPENCV model by @soeroesg in https://github.com/colmap/colmap/pull/1552 * Update vcpkg version to fix CI pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1568 * Replace deprecated Mac OS 10.15 with Mac OS 12 build in CI by @ahojnnes in https://github.com/colmap/colmap/pull/1569 * Fix inconsistent between the actual executed image reader option and the saved project.ini file by @XuChengHUST in https://github.com/colmap/colmap/pull/1564 * checkout the expected version of ceres solver by @scott-vsi in https://github.com/colmap/colmap/pull/1576 * use default qt5 brew install directory #1573 by @catapulta in https://github.com/colmap/colmap/pull/1574 * Fix image undistortion with nested image folders by @ahojnnes in https://github.com/colmap/colmap/pull/1606 * Fix source file permissions by @ahojnnes in https://github.com/colmap/colmap/pull/1607 * Fixed the collection of arguments in colmap.bat by @tdegraaff in https://github.com/colmap/colmap/pull/1121 * Add OpenMP to COLMAP_EXTERNAL_LIBRARIES if enabled by @logchan in https://github.com/colmap/colmap/pull/1632 * Fix output tile reconstructions are the same as the input reconstruction in `RunModelSplitter` (#1513) by @Serenitysmk in https://github.com/colmap/colmap/pull/1531 * add `libmetis-dev` to solve `METIS_INCLUDE_DIRS`. by @FavorMylikes in https://github.com/colmap/colmap/pull/1672 * Update install.rst by @tomer-grin in https://github.com/colmap/colmap/pull/1671 * Update freeimage links. by @Yulv-git in https://github.com/colmap/colmap/pull/1675 * fix small typo by @skal65535 in https://github.com/colmap/colmap/pull/1668 * Update build.py with new glew link by @aghand0ur in https://github.com/colmap/colmap/pull/1658 * Add use_cache in fusion options GUI by @hrflr in https://github.com/colmap/colmap/pull/1655 * Add CI pipeline for Ubuntu 22.04 by @ahojnnes in https://github.com/colmap/colmap/pull/1688 * Avoid unnecessary copies of data by @ahojnnes in https://github.com/colmap/colmap/pull/1691 * Reduce memory allocations in correspondence graph search by @ahojnnes in https://github.com/colmap/colmap/pull/1692 * Use FindCUDAToolkit when available. by @hanseuljun in https://github.com/colmap/colmap/pull/1693 * Fixed a crash due to inconsistent undistortion by @SomeAlphabetGuy in https://github.com/colmap/colmap/pull/1698 * Add CUDA Ubuntu 22.04 CI build by @ahojnnes in https://github.com/colmap/colmap/pull/1705 * Delete the redundancy install of libmetis-dev by @thomas-graphopti in https://github.com/colmap/colmap/pull/1721 * Fix broken loading of image masks on macOS by @buesma in https://github.com/colmap/colmap/pull/1639 * Update install instructions with latest hints and known issues by @ahojnnes in https://github.com/colmap/colmap/pull/1736 * Modernize smart pointer initialization, fix alloc/dealloc mismatch by @ahojnnes in https://github.com/colmap/colmap/pull/1737 * Fix typo in cli.rst by @ojhernandez in https://github.com/colmap/colmap/pull/1747 * Fix inconsistent image resizing between CPU/GPU implementations of SIFT by @Yzhbuaa in https://github.com/colmap/colmap/pull/1642 * Reduce number of SIFT test features to make tests run under WSL by @ahojnnes in https://github.com/colmap/colmap/pull/1748 * Tag documentation version with dev by @ahojnnes in https://github.com/colmap/colmap/pull/1749 * Update copyright to 2023 by @ahojnnes in https://github.com/colmap/colmap/pull/1750 * Fix max image dimension for positive first_octave by @ahojnnes in https://github.com/colmap/colmap/pull/1751 * Fix SIFT GPU match creation by @ahojnnes in https://github.com/colmap/colmap/pull/1757 * Fix SIFT tests for OpenGL by @ahojnnes in https://github.com/colmap/colmap/pull/1762 * Suppress CUDA stack size warning for ptxas by @ahojnnes in https://github.com/colmap/colmap/pull/1770 * Simplify CUDA CMake configuration by @ahojnnes in https://github.com/colmap/colmap/pull/1776 * Fixes for CUDA compilation by @ahojnnes in https://github.com/colmap/colmap/pull/1777 * Improvements to dockerfile and build pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1778 * Explicitly require CMAKE_CUDA_ARCHITECTURES to be defined by @ahojnnes in https://github.com/colmap/colmap/pull/1781 * Depend on system installed FLANN by @ahojnnes in https://github.com/colmap/colmap/pull/1782 * Option to store relative pose between two cameras in database by @yanxke in https://github.com/colmap/colmap/pull/1774 * Depend on system installed SQLite3 by @ahojnnes in https://github.com/colmap/colmap/pull/1783 COLMAP 3.7 (01/26/2022) ----------------------- * Allow to save fused point cloud in colmap format when using command line by @boitumeloruf in https://github.com/colmap/colmap/pull/799 * Fix typos in image.h by @Pascal-So in https://github.com/colmap/colmap/pull/936 * Fix for EPnP estimator by @vlarsson in https://github.com/colmap/colmap/pull/943 * Visualize models using Python in Open3D by @ahojnnes in https://github.com/colmap/colmap/pull/948 * Update tutorial.rst by @ignacio-rocco in https://github.com/colmap/colmap/pull/953 * 8 point algorithm internal contraint fix by @mihaidusmanu in https://github.com/colmap/colmap/pull/982 * Python script for writing depth/normal arrays by @SBCV in https://github.com/colmap/colmap/pull/957 * BuildImageModel: use std::vector instead of numbered arguments by @Pascal-So in https://github.com/colmap/colmap/pull/949 * Fix bugs of sift feature matching by @whuaegeanse in https://github.com/colmap/colmap/pull/985 * script for modifying fused results by @SBCV in https://github.com/colmap/colmap/pull/984 * fix camera model query by @Pascal-So in https://github.com/colmap/colmap/pull/997 * fixed small bug in visualize_model.py by @sniklaus in https://github.com/colmap/colmap/pull/1007 * Update .travis.yml by @srinivas32 in https://github.com/colmap/colmap/pull/989 * Ensure DecomposeHomographyMatrix() always returns rotations by @daithimaco in https://github.com/colmap/colmap/pull/1040 * Remove deprecated qt foreach by @UncleGene in https://github.com/colmap/colmap/pull/1039 * Fix AMD/Windows GUI visualization bug by @drkoller in https://github.com/colmap/colmap/pull/1079 * include colmap_cuda in COLMAP_LIBRARIES when compiled with cuda by @ClementPinard in https://github.com/colmap/colmap/pull/1084 * Fix runtime crash when sparsesuite is missing from ceres by @anmatako in https://github.com/colmap/colmap/pull/1115 * Store relative poses in two_view_geometry table by @Ahmed-Salama in https://github.com/colmap/colmap/pull/1103 * search src images for patch_match from all set, not only referenced subset by @DaniilSNikulin in https://github.com/colmap/colmap/pull/1038 * Replace Travis CI with Azure Pipelines for Linux/Mac builds by @ahojnnes in https://github.com/colmap/colmap/pull/1119 * Allow ReadPly to handle double precision files by @anmatako in https://github.com/colmap/colmap/pull/1131 * Update GPSTransform calculations to improve accuracy by @anmatako in https://github.com/colmap/colmap/pull/1132 * Add scale template flag in SimilarityTransform3::Estimate by @anmatako in https://github.com/colmap/colmap/pull/1133 * Add CopyFile utility that can copy or hard/soft-link files by @anmatako in https://github.com/colmap/colmap/pull/1134 * Expose BA options in IncrementalMapper by @anmatako in https://github.com/colmap/colmap/pull/1139 * Allow configurable paths for mvs::Model by @anmatako in https://github.com/colmap/colmap/pull/1141 * Change ReconstructionMaanger to write larger recons first by @anmatako in https://github.com/colmap/colmap/pull/1137 * Setup Azure pipelines for Windows build by @ahojnnes in https://github.com/colmap/colmap/pull/1150 * Add fixed extrinsics in rig config by @anmatako in https://github.com/colmap/colmap/pull/1144 * Allow custom config and missing dependencies for patch-match by @anmatako in https://github.com/colmap/colmap/pull/1142 * Update print statements for Python 3 compatibility by @UncleGene in https://github.com/colmap/colmap/pull/1126 * Allow cleanup of SQLite tables using new database_cleaner command by @anmatako in https://github.com/colmap/colmap/pull/1136 * Extend SceneClustering to support non-hierarchical (flat) clusters by @anmatako in https://github.com/colmap/colmap/pull/1140 * Support more formats in model_converter by @anmatako in https://github.com/colmap/colmap/pull/1147 * Fix Mac 10.15 build due to changed Qt5 path by @ahojnnes in https://github.com/colmap/colmap/pull/1157 * Fix bug in ReadCameraRigConfig when reading extrinsics by @anmatako in https://github.com/colmap/colmap/pull/1158 * Add utility to compare poses between two sparse models by @ahojnnes in https://github.com/colmap/colmap/pull/1159 * Modularize executable main functions into separate sources by @ahojnnes in https://github.com/colmap/colmap/pull/1160 * Fix unnecessary copies in for range loops by @ahojnnes in https://github.com/colmap/colmap/pull/1162 * Add script to clang-format all source code by @ahojnnes in https://github.com/colmap/colmap/pull/1163 * Add back new options and formats for model_converter by @anmatako in https://github.com/colmap/colmap/pull/1164 * ImageReder new option and bug fix in GPS priors by @anmatako in https://github.com/colmap/colmap/pull/1146 * Parallelize stereo fusion; needs pre-loading of entire workspace by @anmatako in https://github.com/colmap/colmap/pull/1148 * Refactoring and new functionality in Reconstruction class by @anmatako in https://github.com/colmap/colmap/pull/1169 * Add new functionality in image_undistorter by @anmatako in https://github.com/colmap/colmap/pull/1168 * Add new CMake option to disable GUI by @anmatako in https://github.com/colmap/colmap/pull/1165 * Fix the memory leak caused by not releasing the memory of the PRNG at the end of the thread by @whuaegeanse in https://github.com/colmap/colmap/pull/1170 * Fix fusion segfault bug by @anmatako in https://github.com/colmap/colmap/pull/1176 * Update SiftGPU to use floorf for floats by @anmatako in https://github.com/colmap/colmap/pull/1182 * fix typo in extraction.cc by @iuk in https://github.com/colmap/colmap/pull/1191 * Improvements to NVM, Cam, Recon3D, and Bundler exporters by @drkoller in https://github.com/colmap/colmap/pull/1187 * Update model_aligner functionality by @anmatako in https://github.com/colmap/colmap/pull/1177 * Add new model_cropper and model_splitter commands by @anmatako in https://github.com/colmap/colmap/pull/1179 * use type point2D_t instead of image_t by @iuk in https://github.com/colmap/colmap/pull/1199 * Fix radial distortion in Cam format exporter by @drkoller in https://github.com/colmap/colmap/pull/1196 * Add new model_transformer command by @anmatako in https://github.com/colmap/colmap/pull/1178 * Fix error of using urllib to download eigen from gitlab by @whuaegeanse in https://github.com/colmap/colmap/pull/1194 * Multi-line string fix in Python model script by @mihaidusmanu in https://github.com/colmap/colmap/pull/1217 * added visibility_sigma to CLI input options for delaunay_mesher. by @Matstah in https://github.com/colmap/colmap/pull/1236 * Backwards compatibility of model_aligner by @tsattler in https://github.com/colmap/colmap/pull/1240 * [update undistortion] update dumped commands by @hiakru in https://github.com/colmap/colmap/pull/1276 * Compute reprojection error in generalized absolute solver by @Skydes in https://github.com/colmap/colmap/pull/1257 * Modifying scripts/python/flickr_downloader.py to create files with correct extensions by @snavely in https://github.com/colmap/colmap/pull/1275 * revise Dockerfile and readme. by @MasahiroOgawa in https://github.com/colmap/colmap/pull/1281 * Update to latest vcpkg version by @ahojnnes in https://github.com/colmap/colmap/pull/1319 * Fix compiler warnings reported by GCC by @ahojnnes in https://github.com/colmap/colmap/pull/1317 * Auto-rotate JPEG images based on EXIF orientation by @ahojnnes in https://github.com/colmap/colmap/pull/1318 * Upgrade vcpkg to fix CI build issues by @ahojnnes in https://github.com/colmap/colmap/pull/1331 * Added descriptor normalization argument to feature_extractor. by @mihaidusmanu in https://github.com/colmap/colmap/pull/1332 * Fix memory leak in the function of StringAppendV by @whuaegeanse in https://github.com/colmap/colmap/pull/1337 * Add CUDA_SAFE_CALL to cudaGetDeviceCount. by @chpatrick in https://github.com/colmap/colmap/pull/1334 * Add missing include in case CUDA/GUI is not available by @ahojnnes in https://github.com/colmap/colmap/pull/1329 * Fix wrong WGS84 model and test cases in GPSTransform by @Freeverc in https://github.com/colmap/colmap/pull/1333 * Fixes bug in sprt.cc: num_inliers was not set. by @rmbrualla in https://github.com/colmap/colmap/pull/1360 * Prevent a divide by zero corner case. by @rmbrualla in https://github.com/colmap/colmap/pull/1361 * Adds missing header. by @rmbrualla in https://github.com/colmap/colmap/pull/1362 * Require Qt in COLMAPConfig only if GUI is enabled by @Skydes in https://github.com/colmap/colmap/pull/1365 * Keep precision in the process of storing in text. by @whuaegeanse in https://github.com/colmap/colmap/pull/1363 * Expose exe internals by @Skydes in https://github.com/colmap/colmap/pull/1366 * Fix inliers matches extraction in EstimateUncalibrated function. by @ferreram in https://github.com/colmap/colmap/pull/1369 * Expose exe internals - fix by @Skydes in https://github.com/colmap/colmap/pull/1368 * Remove deprecated Mac OSX 10.14 image in ADO pipeline by @ahojnnes in https://github.com/colmap/colmap/pull/1383 * Add Mac OSX 11 ADO pipeline job by @ahojnnes in https://github.com/colmap/colmap/pull/1384 * Fix warnings for latest compiler/libraries by @ahojnnes in https://github.com/colmap/colmap/pull/1382 * Fix clang compiler warnings by @ahojnnes in https://github.com/colmap/colmap/pull/1387 * Add Address Sanitizer options and fix reported issues by @ahojnnes in https://github.com/colmap/colmap/pull/1390 * User/joschonb/asan cleanup by @ahojnnes in https://github.com/colmap/colmap/pull/1391 * Add ADO pipeline for Visual Studio 2022 by @ahojnnes in https://github.com/colmap/colmap/pull/1392 * Add ccache option by @ahojnnes in https://github.com/colmap/colmap/pull/1395 * Update ModelAligner to handle GPS and custom coords. and more by @ferreram in https://github.com/colmap/colmap/pull/1371 COLMAP 3.6 (07/24/2020) ----------------------- - Improved robustness and faster incremental reconstruction process - Add ``image_deleter`` command to remove images from sparse model - Add ``image_filter`` command to filter bad registrations from sparse model - Add ``point_filtering`` command to filter sparse model point clouds - Add ``database_merger`` command to merge two databases, which is useful to parallelize matching across different machines - Add ``image_undistorter_standalone`` to enable undistorting images without a pre-existing full sparse model - Improved undistortion for fisheye cameras and FOV camera model - Support for masking input images in feature extraction stage - Improved HiDPI support in GUI for high-resolution monitors - Import sparse model when launching GUI from CLI - Faster CPU-based matching using approximate NN search - Support for bundle adjustment with fixed extrinsics - Support for fixing existing images when continuing reconstruction - Camera model colors in viewer can be customized - Support for latest GPU architectures in CUDA build - Support for writing sparse models in Python scripts - Scripts for building and running COLMAP in Docker - Many more bug fixes and improvements to code and documentation COLMAP 3.5 (08/22/2018) ----------------------- - COLMAP is now released under the BSD license instead of the GPL - COLMAP is now installed as a library, whose headers can be included and libraries linked against from other C/C++ code - Add hierarchical mapper for parallelized reconstruction or large scenes - Add sparse and dense Delaunay meshing algorithms, which reconstruct a watertight surface using a graph cut on the Delaunay triangulation of the reconstructed sparse or dense point cloud - Improved robustness when merging different models - Improved pre-trained vocabulary trees available for download - Add COLMAP as a software entry under Linux desktop systems - Add support to compile COLMAP on ARM platforms - Add example Python script to read/write COLMAP database - Add region of interest (ROI) cropping in image undistortion - Several import bug fixes for spatial verification in image retrieval - Add more extensive continuous integration across more compilation scenarios - Many more bug fixes and improvements to code and documentation COLMAP 3.4 (01/29/2018) ----------------------- - Unified command-line interface: The functionality of previous executables have been merged into the ``src/exe/colmap.cc`` executable. The GUI can now be started using the command ``colmap gui`` and other commands are available as ``colmap [command]``. For example, the feature extractor is now available as ``colmap feature_extractor [args]`` while all command-line arguments stay the same as before. This should result in much faster project compile times and smaller disk space usage of the program. More details about the new interface are documented at https://colmap.github.io/cli.html - More complete depth and normal maps with larger patch sizes - Faster dense stereo computation by skipping rows/columns in patch match, improved random sampling in patch match, and faster bilateral NCC - Better high DPI screen support for the graphical user interface - Improved model viewer under Windows, which now requires Qt 5.4 - Save computed two-view geometries in database - Images (keypoint/matches visualization, depth and normal maps) can now be saved from the graphical user interface - Support for PMVS format without sparse bundler file - Faster covariant feature detection - Many more bug fixes and improvements COLMAP 3.3 (11/21/2017) ----------------------- - Add DSP (Domain Size Pooling) SIFT implementation. DSP-SIFT outperforms standard SIFT in most cases, as shown in "Comparative Evaluation of Hand-Crafted and Learned Local Features", Schoenberger et al., CVPR 2017 - Improved parameters dense reconstruction of smaller models - Improved compile times due to various code optimizations - Add option to specify camera model in automatic reconstruction - Add new model orientation alignment based on upright image assumption - Improved numerical stability for generalized absolute pose solver - Support for image range specification in PMVS dense reconstruction format - Support for older Python versions in automatic build script - Fix OpenCV Fisheye camera model to exactly match OpenCV specifications COLMAP 3.2 (9/2/2017) ---------------------- - Fully automatic cross-platform build script (Windows, Mac, Linux) - Add multi-GPU feature extraction if multiple CUDA devices are available - Configurable dimension and data type for vocabulary tree implementation - Add new sequential matching mode for image sequences with high frame-rate - Add generalized relative pose solver for multi-camera systems - Add sparse least absolute deviation solver - Add CPU/GPU options to automatic reconstruction tool - Add continuous integration system under Windows, Mac, Linux through Github - Many more bug fixes and improvements COLMAP 3.1 (6/15/2017) ---------------------- - Add fast spatial verification to image retrieval module - Add binary file format for sparse models by default. Old text format still fully compatible and possible conversion in GUI and CLI - Add cross-platform little endian binary file reading and writing - Faster and less memory hungry stereo fusion by computing consistency on demand and possible limitation of image size in fusion - Simpler geometric stereo processing interface. Now geometric stereo output can be computed using a single pass - Faster and multi-architecture CUDA compilation - Add medium quality option in automatic reconstructor - Many more bug fixes and improvements COLMAP 3.0 (5/22/2017) ---------------------- - Add automatic end-to-end reconstruction tool that automatically performs sparse and dense reconstruction on a given set of images - Add multi-GPU dense stereo if multiple CUDA devices are available - Add multi-GPU feature matching if multiple CUDA devices are available - Add Manhattan-world / gravity alignment using line detection - Add CUDA-based feature extraction useful for usage on clusters - Add CPU-based feature matching for machines without GPU - Add new THIN_PRISM_FISHEYE camera model with tangential/radial correction - Add binary to triangulate existing/empty sparse reconstruction - Add binary to print summary statistics about sparse reconstruction - Add transitive feature matching to transitively complete match graph - Improved scalability of dense reconstruction by using caching - More stable GPU-based feature matching with informative warnings - Faster vocabulary tree matching using dynamic scheduling in FLANN - Faster spatial feature matching using linear index instead of kd-tree - More stable camera undistortion using numerical Newton iteration - Improved option parsing with some backwards incompatible option renaming - Faster compile times by optimizing includes and CUDA flags - More stable view selection for small baseline scenario in dense reconstruction - Many more bug fixes and improvements COLMAP 2.1 (12/7/2016) ---------------------- - Support to only index and match specific images in vocabulary tree matching - Support to perform image retrieval using vocabulary tree - Several bug fixes and improvements for multi-view stereo module - Improved Structure-from-Motion initialization strategy - Support to only reconstruct the scene using specific images in the database - Add support to merge two models using overlapping registered images - Add support to geo-register/align models using known camera locations - Support to only extract specific images in feature extraction module - Support for snapshot model export during reconstruction - Skip already undistorted images if they exist in output directory - Support to limit the number of features in image retrieval for improved speed - Miscellaneous bug fixes and improvements COLMAP 2.0 (9/8/2016) --------------------- - Implementation of dense reconstruction pipeline - Improved feature matching performance - New bundle adjuster for rigidly mounted multi-camera systems - New generalized absolute pose solver for multi-camera systems - New executable to extract colors from all images - Boost can now be linked in shared and static mode - Various bug fixes and performance improvements COLMAP 1.1 (5/19/2016) ---------------------- - Implementation of state-of-the-art image retrieval system using Hamming embedding for vocabulary tree matching. This should lead to much improved matching results as compared to the previous implementation. - Guided matching as an optional functionality. - New demo datasets for download. - Automatically switch to PBA if supported by the project. - Implementation of EPNP solver for local pose optimization in RANSAC. - Add option to extract upright SIFT features. - Saving JPEGs in superb quality by default in export. - Add option to clear matches and inlier matches in the project. - New fisheye camera models, including the FOV camera model used by Google Project Tango (Thomas Schoeps). - Extended documentation based on user feedback. - Fixed typo in documentation (Thomas Schoeps). COLMAP 1.0 (4/4/2016) --------------------- - Initial release of COLMAP. colmap-3.9.1/CMakeLists.txt000066400000000000000000000312741454702036400155530ustar00rootroot00000000000000# 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. cmake_minimum_required(VERSION 3.10) project(COLMAP LANGUAGES C CXX) set(COLMAP_VERSION "3.9.1") set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CUDA_STANDARD 14) set(CMAKE_CUDA_STANDARD_REQUIRED ON) set_property(GLOBAL PROPERTY GLOBAL_DEPENDS_NO_CYCLES ON) ################################################################################ # Include CMake dependencies ################################################################################ set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(CheckCXXCompilerFlag) # Include helper macros and commands, and allow the included file to override # the CMake policies in this file include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeHelper.cmake NO_POLICY_SCOPE) # Build position-independent code, so that shared libraries can link against # COLMAP's static libraries. set(CMAKE_POSITION_INDEPENDENT_CODE ON) ################################################################################ # Options ################################################################################ option(SIMD_ENABLED "Whether to enable SIMD optimizations" ON) option(OPENMP_ENABLED "Whether to enable OpenMP parallelization" ON) option(IPO_ENABLED "Whether to enable interprocedural optimization" ON) option(CUDA_ENABLED "Whether to enable CUDA, if available" ON) option(GUI_ENABLED "Whether to enable the graphical UI" ON) option(OPENGL_ENABLED "Whether to enable OpenGL, if available" ON) option(TESTS_ENABLED "Whether to build test binaries" OFF) option(ASAN_ENABLED "Whether to enable AddressSanitizer flags" OFF) option(PROFILING_ENABLED "Whether to enable google-perftools linker flags" OFF) option(CCACHE_ENABLED "Whether to enable compiler caching, if available" ON) option(CGAL_ENABLED "Whether to enable the CGAL library" ON) if(TESTS_ENABLED) enable_testing() endif() ################################################################################ # Dependency configuration ################################################################################ set(COLMAP_FIND_QUIETLY FALSE) include(cmake/FindDependencies.cmake) ################################################################################ # Compiler specific configuration ################################################################################ if(CMAKE_BUILD_TYPE) message(STATUS "Build type specified as ${CMAKE_BUILD_TYPE}") else() message(STATUS "Build type not specified, using Release") set(CMAKE_BUILD_TYPE Release) set(IS_DEBUG OFF) endif() if("${CMAKE_BUILD_TYPE}" STREQUAL "ClangTidy") find_program(CLANG_TIDY_EXE NAMES clang-tidy) if(NOT CLANG_TIDY_EXE) message(FATAL_ERROR "Could not find the clang-tidy executable, please set CLANG_TIDY_EXE") endif() else() unset(CLANG_TIDY_EXE) endif() if(IS_MSVC) # Some fixes for the Glog library. add_definitions("-DGLOG_NO_ABBREVIATED_SEVERITIES") add_definitions("-DGL_GLEXT_PROTOTYPES") add_definitions("-DNOMINMAX") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc") # Enable object level parallel builds in Visual Studio. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /bigobj") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") endif() endif() if(IS_GNU) if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) message(FATAL_ERROR "GCC version 4.8 or older not supported") endif() # Hide incorrect warnings for uninitialized Eigen variables under GCC. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-maybe-uninitialized") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-maybe-uninitialized") endif() if(IS_DEBUG) add_definitions("-DEIGEN_INITIALIZE_MATRICES_BY_NAN") endif() if(SIMD_ENABLED) message(STATUS "Enabling SIMD support") else() message(STATUS "Disabling SIMD support") endif() if(IPO_ENABLED AND NOT IS_DEBUG AND NOT IS_GNU) message(STATUS "Enabling interprocedural optimization") set_property(DIRECTORY PROPERTY INTERPROCEDURAL_OPTIMIZATION 1) else() message(STATUS "Disabling interprocedural optimization") endif() if(ASAN_ENABLED) message(STATUS "Enabling ASan support") if(IS_CLANG OR IS_GNU) add_compile_options(-fsanitize=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope) add_link_options(-fsanitize=address) else() message(FATAL_ERROR "Unsupported compiler for ASan mode") endif() endif() if(CCACHE_ENABLED) find_program(CCACHE ccache) if(CCACHE) message(STATUS "Enabling ccache support") set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE}) set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE}) else() message(STATUS "Disabling ccache support") endif() else() message(STATUS "Disabling ccache support") endif() if(PROFILING_ENABLED) message(STATUS "Enabling profiling support") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -lprofiler -ltcmalloc") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -lprofiler -ltcmalloc") else() message(STATUS "Disabling profiling support") endif() ################################################################################ # Add sources ################################################################################ # Generate source file with version definitions. include(GenerateVersionDefinitions) include_directories(src) link_directories(${COLMAP_LINK_DIRS}) add_subdirectory(src/colmap) add_subdirectory(src/thirdparty) ################################################################################ # Generate source groups for Visual Studio, XCode, etc. ################################################################################ COLMAP_ADD_SOURCE_DIR(src/colmap/controllers CONTROLLERS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/estimators ESTIMATORS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/exe EXE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/feature FEATURE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/geometry GEOMETRY_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/image IMAGE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/math MATH_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/mvs MVS_SRCS *.h *.cc *.cu) COLMAP_ADD_SOURCE_DIR(src/colmap/optim OPTIM_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/retrieval RETRIEVAL_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/scene SCENE_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/sensor SENSOR_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/sfm SFM_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/tools TOOLS_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/ui UI_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/colmap/util UTIL_SRCS *.h *.cc) COLMAP_ADD_SOURCE_DIR(src/thirdparty/LSD THIRDPARTY_LSD_SRCS *.h *.c) COLMAP_ADD_SOURCE_DIR(src/thirdparty/PoissonRecon THIRDPARTY_POISSON_RECON_SRCS *.h *.cpp *.inl) COLMAP_ADD_SOURCE_DIR(src/thirdparty/SiftGPU THIRDPARTY_SIFT_GPU_SRCS *.h *.cpp *.cu) COLMAP_ADD_SOURCE_DIR(src/thirdparty/VLFeat THIRDPARTY_VLFEAT_SRCS *.h *.c *.tc) # Add all of the source files to a regular library target, as using a custom # target does not allow us to set its C++ include directories (and thus # intellisense can't find any of the included files). add_library( ${COLMAP_SRC_ROOT_FOLDER} ${CONTROLLERS_SRCS} ${ESTIMATORS_SRCS} ${EXE_SRCS} ${FEATURE_SRCS} ${GEOMETRY_SRCS} ${IMAGE_SRCS} ${MATH_SRCS} ${MVS_SRCS} ${OPTIM_SRCS} ${RETRIEVAL_SRCS} ${SCENE_SRCS} ${SENSOR_SRCS} ${SFM_SRCS} ${TOOLS_SRCS} ${UI_SRCS} ${UTIL_SRCS} ${THIRDPARTY_LSD_SRCS} ${THIRDPARTY_POISSON_RECON_SRCS} ${THIRDPARTY_SIFT_GPU_SRCS} ${THIRDPARTY_VLFEAT_SRCS} ) # Prevent the library from being compiled automatically. set_target_properties( ${COLMAP_SRC_ROOT_FOLDER} PROPERTIES EXCLUDE_FROM_ALL 1 EXCLUDE_FROM_DEFAULT_BUILD 1) ################################################################################ # Install and uninstall scripts ################################################################################ # Install batch scripts under Windows. if(IS_MSVC) install(FILES "scripts/shell/COLMAP.bat" "scripts/shell/RUN_TESTS.bat" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION "/") endif() # Install application meny entry under Linux/Unix. if(UNIX AND NOT APPLE) install(FILES "doc/COLMAP.desktop" DESTINATION "share/applications") endif() # Configure the uninstallation script. configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeUninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/CMakeUninstall.cmake" IMMEDIATE @ONLY) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/CMakeUninstall.cmake) set_target_properties(uninstall PROPERTIES FOLDER ${CMAKE_TARGETS_ROOT_FOLDER}) set(COLMAP_EXPORT_LIBS # Internal. colmap_controllers colmap_estimators colmap_exe colmap_feature colmap_geometry colmap_image colmap_math colmap_mvs colmap_optim colmap_retrieval colmap_scene colmap_sensor colmap_sfm colmap_util # Third-party. colmap_lsd colmap_poisson_recon colmap_vlfeat ) if(GUI_ENABLED) list(APPEND COLMAP_EXPORT_LIBS colmap_ui ) endif() if(CUDA_ENABLED) list(APPEND COLMAP_EXPORT_LIBS colmap_util_cuda colmap_mvs_cuda ) endif() if(GPU_ENABLED) list(APPEND COLMAP_EXPORT_LIBS colmap_sift_gpu ) endif() # Add unified interface library target to export. add_library(colmap INTERFACE) target_link_libraries(colmap INTERFACE ${COLMAP_EXPORT_LIBS}) set(INSTALL_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/include") target_include_directories( colmap INTERFACE $) install( TARGETS colmap ${COLMAP_EXPORT_LIBS} EXPORT colmap-targets LIBRARY DESTINATION thirdparty/) # Generate config and version. include(CMakePackageConfigHelpers) set(PACKAGE_CONFIG_FILE "${CMAKE_CURRENT_BINARY_DIR}/colmap-config.cmake") set(INSTALL_CONFIG_DIR "share/colmap") configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/cmake/colmap-config.cmake.in ${PACKAGE_CONFIG_FILE} INSTALL_DESTINATION ${INSTALL_CONFIG_DIR}) install(FILES ${PACKAGE_CONFIG_FILE} DESTINATION ${INSTALL_CONFIG_DIR}) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/colmap-config-version.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/colmap-config-version.cmake" @ONLY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/colmap-config-version.cmake" DESTINATION "share/colmap") # Install targets. install( EXPORT colmap-targets FILE colmap-targets.cmake NAMESPACE colmap:: DESTINATION ${INSTALL_CONFIG_DIR}) # Install header files. install( DIRECTORY src/colmap DESTINATION include FILES_MATCHING PATTERN "*.h") install( DIRECTORY src/thirdparty DESTINATION include/colmap FILES_MATCHING REGEX ".*[.]h|.*[.]hpp|.*[.]inl") # Install find_package scripts for dependencies. install( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/cmake DESTINATION share/colmap FILES_MATCHING PATTERN "Find*.cmake") colmap-3.9.1/CONTRIBUTING.md000066400000000000000000000010721454702036400152350ustar00rootroot00000000000000Contributing ------------ Contributions (bug reports, bug fixes, improvements, etc.) are very welcome and should be submitted in the form of new issues and/or pull requests on GitHub. Please, adhere to the Google coding style guide: https://google.github.io/styleguide/cppguide.html by using the provided ".clang-format" file. Document code, functions, methods, classes, etc. Make sure to add unit tests for all newly added code and make sure that algorithmic "improvements" generalize and actually improve the results of the pipeline on a variety of datasets. colmap-3.9.1/COPYING.txt000066400000000000000000000035721454702036400146640ustar00rootroot00000000000000The COLMAP library is licensed under the new BSD license. Note that this text refers only to the license for COLMAP itself, independent of its dependencies, which are separately licensed. Building COLMAP with these dependencies may affect the resulting COLMAP license. 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. colmap-3.9.1/README.md000066400000000000000000000120001454702036400142540ustar00rootroot00000000000000COLMAP ====== About ----- COLMAP is a general-purpose Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline with a graphical and command-line interface. It offers a wide range of features for reconstruction of ordered and unordered image collections. The software is licensed under the new BSD license. If you use this project for your research, please cite: @inproceedings{schoenberger2016sfm, author={Sch\"{o}nberger, Johannes Lutz and Frahm, Jan-Michael}, title={Structure-from-Motion Revisited}, booktitle={Conference on Computer Vision and Pattern Recognition (CVPR)}, year={2016}, } @inproceedings{schoenberger2016mvs, author={Sch\"{o}nberger, Johannes Lutz and Zheng, Enliang and Pollefeys, Marc and Frahm, Jan-Michael}, title={Pixelwise View Selection for Unstructured Multi-View Stereo}, booktitle={European Conference on Computer Vision (ECCV)}, year={2016}, } If you use the image retrieval / vocabulary tree engine, please also cite: @inproceedings{schoenberger2016vote, author={Sch\"{o}nberger, Johannes Lutz and Price, True and Sattler, Torsten and Frahm, Jan-Michael and Pollefeys, Marc}, title={A Vote-and-Verify Strategy for Fast Spatial Verification in Image Retrieval}, booktitle={Asian Conference on Computer Vision (ACCV)}, year={2016}, } The latest source code is available at https://github.com/colmap/colmap. COLMAP builds on top of existing works and when using specific algorithms within COLMAP, please also cite the original authors, as specified in the source code. Download -------- Executables for Windows and Mac and other resources can be downloaded from https://demuc.de/colmap/. Executables for Linux/Unix/BSD are available at https://repology.org/metapackage/colmap/versions. To build COLMAP from source, please see https://colmap.github.io/install.html. Getting Started --------------- 1. Download the pre-built binaries from https://demuc.de/colmap/ or build the library manually as described in the documentation. 2. Download one of the provided datasets at https://demuc.de/colmap/datasets/ or use your own images. 3. Use the **automatic reconstruction** to easily build models with a single click or command. Documentation ------------- The documentation is available at https://colmap.github.io/. Support ------- Please, use GitHub Discussions at https://github.com/colmap/colmap/discussions for questions and the GitHub issue tracker at https://github.com/colmap/colmap for bug reports, feature requests/additions, etc. Acknowledgments --------------- The library was originally written by Johannes L. Schönberger (https://demuc.de/) with funding provided by his PhD advisors Jan-Michael Frahm and Marc Pollefeys. Since then the project has benefited from countless community contributions, including bug fixes, improvements, new features, third-party tooling, and community support. Contribution ------------ Contributions (bug reports, bug fixes, improvements, etc.) are very welcome and should be submitted in the form of new issues and/or pull requests on GitHub. License ------- The COLMAP library is licensed under the new BSD license. Note that this text refers only to the license for COLMAP itself, independent of its thirdparty dependencies, which are separately licensed. Building COLMAP with these dependencies may affect the resulting COLMAP license. 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. colmap-3.9.1/cmake/000077500000000000000000000000001454702036400140645ustar00rootroot00000000000000colmap-3.9.1/cmake/CMakeHelper.cmake000066400000000000000000000127751454702036400172220ustar00rootroot00000000000000if(POLICY CMP0043) cmake_policy(SET CMP0043 NEW) endif() if(POLICY CMP0054) cmake_policy(SET CMP0054 NEW) endif() # 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() # Determine project architecture. if(CMAKE_SYSTEM_PROCESSOR MATCHES "[ix].?86|amd64|AMD64") set(IS_X86 TRUE) endif() # Determine project operating system. string(REGEX MATCH "Linux" IS_LINUX ${CMAKE_SYSTEM_NAME}) string(REGEX MATCH "DragonFly|BSD" IS_BSD ${CMAKE_SYSTEM_NAME}) string(REGEX MATCH "SunOS" IS_SOLARIS ${CMAKE_SYSTEM_NAME}) if(WIN32) set(IS_WINDOWS TRUE BOOL INTERNAL) endif() if(APPLE) set(IS_MACOS TRUE BOOL INTERNAL) endif() string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) if(CMAKE_BUILD_TYPE_LOWER STREQUAL "debug" OR CMAKE_BUILD_TYPE_LOWER STREQUAL "relwithdebinfo") set(IS_DEBUG TRUE) endif() # Enable solution folders. set_property(GLOBAL PROPERTY USE_FOLDERS ON) set(CMAKE_TARGETS_ROOT_FOLDER "cmake") set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER ${CMAKE_TARGETS_ROOT_FOLDER}) set(COLMAP_TARGETS_ROOT_FOLDER "colmap_targets") set(COLMAP_SRC_ROOT_FOLDER "colmap_sources") # This macro will search for source files in a given directory, will add them # to a source group (folder within a project), and will then return paths to # each of the found files. The usage of the macro is as follows: # COLMAP_ADD_SOURCE_DIR( # # # ) macro(COLMAP_ADD_SOURCE_DIR SRC_DIR SRC_VAR) # Create the list of expressions to be used in the search. set(GLOB_EXPRESSIONS "") foreach(ARG ${ARGN}) list(APPEND GLOB_EXPRESSIONS ${SRC_DIR}/${ARG}) endforeach() # Perform the search for the source files. file(GLOB ${SRC_VAR} RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${GLOB_EXPRESSIONS}) # Create the source group. string(REPLACE "/" "\\" GROUP_NAME ${SRC_DIR}) source_group(${GROUP_NAME} FILES ${${SRC_VAR}}) # Clean-up. unset(GLOB_EXPRESSIONS) unset(ARG) unset(GROUP_NAME) endmacro(COLMAP_ADD_SOURCE_DIR) # Replacement for the normal add_library() command. The syntax remains the same # in that the first argument is the target name, and the following arguments # are the source files to use when building the target. macro(COLMAP_ADD_LIBRARY) set(options) set(oneValueArgs) set(multiValueArgs NAME SRCS PRIVATE_LINK_LIBS PUBLIC_LINK_LIBS) cmake_parse_arguments(COLMAP_ADD_LIBRARY "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) add_library(${COLMAP_ADD_LIBRARY_NAME} STATIC ${COLMAP_ADD_LIBRARY_SRCS}) set_target_properties(${COLMAP_ADD_LIBRARY_NAME} PROPERTIES FOLDER ${COLMAP_TARGETS_ROOT_FOLDER}/${FOLDER_NAME}) if(CLANG_TIDY_EXE) set_target_properties(${COLMAP_ADD_LIBRARY_NAME} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-header-filter=.*") endif() target_link_libraries(${COLMAP_ADD_LIBRARY_NAME} PRIVATE ${COLMAP_ADD_LIBRARY_PRIVATE_LINK_LIBS} PUBLIC ${COLMAP_ADD_LIBRARY_PUBLIC_LINK_LIBS}) endmacro(COLMAP_ADD_LIBRARY) # Replacement for the normal add_executable() command. The syntax remains the # same in that the first argument is the target name, and the following # arguments are the source files to use when building the target. macro(COLMAP_ADD_EXECUTABLE) set(options) set(oneValueArgs) set(multiValueArgs NAME SRCS LINK_LIBS) cmake_parse_arguments(COLMAP_ADD_EXECUTABLE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) add_executable(${COLMAP_ADD_EXECUTABLE_NAME} ${COLMAP_ADD_EXECUTABLE_SRCS}) set_target_properties(${COLMAP_ADD_EXECUTABLE_NAME} PROPERTIES FOLDER ${COLMAP_TARGETS_ROOT_FOLDER}/${FOLDER_NAME}) target_link_libraries(${COLMAP_ADD_EXECUTABLE_NAME} ${COLMAP_ADD_EXECUTABLE_LINK_LIBS}) if(VCPKG_BUILD) install(TARGETS ${COLMAP_ADD_EXECUTABLE_NAME} DESTINATION tools/) else() install(TARGETS ${COLMAP_ADD_EXECUTABLE_NAME} DESTINATION bin/) endif() if(CLANG_TIDY_EXE) set_target_properties(${COLMAP_ADD_EXECUTABLE_NAME} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-header-filter=.*") endif() endmacro(COLMAP_ADD_EXECUTABLE) # Wrapper for test executables. macro(COLMAP_ADD_TEST) set(options) set(oneValueArgs) set(multiValueArgs NAME SRCS LINK_LIBS) cmake_parse_arguments(COLMAP_ADD_TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(TESTS_ENABLED) # ${ARGN} will store the list of link libraries. set(COLMAP_ADD_TEST_NAME "colmap_${FOLDER_NAME}_${COLMAP_ADD_TEST_NAME}") add_executable(${COLMAP_ADD_TEST_NAME} ${COLMAP_ADD_TEST_SRCS}) set_target_properties(${COLMAP_ADD_TEST_NAME} PROPERTIES FOLDER ${COLMAP_TARGETS_ROOT_FOLDER}/${FOLDER_NAME}) if(CLANG_TIDY_EXE) set_target_properties(${COLMAP_ADD_TEST_NAME} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-header-filter=.*") endif() target_link_libraries(${COLMAP_ADD_TEST_NAME} ${COLMAP_ADD_TEST_LINK_LIBS} GTest::gtest GTest::gtest_main) add_test("${FOLDER_NAME}/${COLMAP_ADD_TEST_NAME}" ${COLMAP_ADD_TEST_NAME}) if(IS_MSVC) install(TARGETS ${COLMAP_ADD_TEST_NAME} DESTINATION bin/) endif() endif() endmacro(COLMAP_ADD_TEST) colmap-3.9.1/cmake/CMakeUninstall.cmake.in000066400000000000000000000015331454702036400203470ustar00rootroot00000000000000if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") message(FATAL_ERROR "Cannot find install manifest: @CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") endif() file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) string(REGEX REPLACE "\n" ";" files "${files}") foreach(file ${files}) message(STATUS "Uninstalling $ENV{DESTDIR}${file}") if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") exec_program("@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" OUTPUT_VARIABLE rm_out RETURN_VALUE rm_retval) if(NOT "${rm_retval}" STREQUAL 0) message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") endif() else() message(STATUS "File $ENV{DESTDIR}${file} does not exist.") endif() endforeach() colmap-3.9.1/cmake/FindDependencies.cmake000066400000000000000000000152651454702036400202660ustar00rootroot00000000000000if(COLMAP_FIND_QUIETLY) set(COLMAP_FIND_TYPE QUIET) else() set(COLMAP_FIND_TYPE REQUIRED) endif() find_package(Boost ${COLMAP_FIND_TYPE} COMPONENTS filesystem graph program_options system) find_package(Eigen3 ${COLMAP_FIND_TYPE}) find_package(FreeImage ${COLMAP_FIND_TYPE}) find_package(FLANN ${COLMAP_FIND_TYPE}) find_package(LZ4 ${COLMAP_FIND_TYPE}) find_package(Metis ${COLMAP_FIND_TYPE}) find_package(Glog ${COLMAP_FIND_TYPE}) find_package(SQLite3 ${COLMAP_FIND_TYPE}) set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL ${COLMAP_FIND_TYPE}) find_package(Glew ${COLMAP_FIND_TYPE}) find_package(Git) find_package(Ceres ${COLMAP_FIND_TYPE}) if(NOT TARGET Ceres::ceres) # Older Ceres versions don't come with an imported interface target. add_library(Ceres::ceres INTERFACE IMPORTED) target_include_directories( Ceres::ceres INTERFACE ${CERES_INCLUDE_DIRS}) target_link_libraries( Ceres::ceres INTERFACE ${CERES_LIBRARIES}) endif() if(TESTS_ENABLED) find_package(GTest ${COLMAP_FIND_TYPE}) endif() if(OPENMP_ENABLED) find_package(OpenMP QUIET) endif() if(OPENMP_ENABLED AND OPENMP_FOUND) message(STATUS "Enabling OpenMP support") add_definitions("-DCOLMAP_OPENMP_ENABLED") else() message(STATUS "Disabling OpenMP support") endif() if(CGAL_ENABLED) set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE TRUE) # We do not use CGAL data. This prevents an unnecessary warning by CMake. set(CGAL_DATA_DIR "unused") find_package(CGAL ${COLMAP_FIND_TYPE}) endif() if(CGAL_FOUND) add_definitions("-DCOLMAP_CGAL_ENABLED") list(APPEND CGAL_LIBRARY ${CGAL_LIBRARIES}) message(STATUS "Found CGAL") message(STATUS " Includes : ${CGAL_INCLUDE_DIRS}") message(STATUS " Libraries : ${CGAL_LIBRARY}") if(NOT TARGET CGAL) # Older CGAL versions don't come with an imported interface target. add_library(CGAL INTERFACE IMPORTED) target_include_directories( CGAL INTERFACE ${CGAL_INCLUDE_DIRS} ${GMP_INCLUDE_DIR}) target_link_libraries( CGAL INTERFACE ${CGAL_LIBRARY} ${GMP_LIBRARIES}) endif() list(APPEND COLMAP_LINK_DIRS ${CGAL_LIBRARIES_DIR}) endif() set(COLMAP_LINK_DIRS ${Boost_LIBRARY_DIRS}) 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") endif() else() find_package(CUDAToolkit QUIET) if(CUDAToolkit_FOUND) set(CUDA_FOUND ON) enable_language(CUDA) endif() endif() endif() if(CUDA_ENABLED AND CUDA_FOUND) if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) message( FATAL_ERROR "You must set CMAKE_CUDA_ARCHITECTURES to e.g. 'native', 'all-major', '70', etc. " "More information at https://cmake.org/cmake/help/latest/prop_tgt/CUDA_ARCHITECTURES.html") endif() add_definitions("-DCOLMAP_CUDA_ENABLED") # Fix for some combinations of CUDA and GCC (e.g. under Ubuntu 16.04). set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -D_FORCE_INLINES") # 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() if(GUI_ENABLED) find_package(Qt5 5.4 ${COLMAP_FIND_TYPE} COMPONENTS Core OpenGL Widgets) message(STATUS "Found Qt") message(STATUS " Module : ${Qt5Core_DIR}") message(STATUS " Module : ${Qt5OpenGL_DIR}") message(STATUS " Module : ${Qt5Widgets_DIR}") if(Qt5_FOUND) # Qt5 was built with -reduce-relocations. if(Qt5_POSITION_INDEPENDENT_CODE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Workaround for Qt5 CMake config bug under Ubuntu 20.04: https://gitlab.kitware.com/cmake/cmake/-/issues/16915 if(TARGET Qt5::Core) get_property(core_options TARGET Qt5::Core PROPERTY INTERFACE_COMPILE_OPTIONS) string(REPLACE "-fPIC" "" new_qt5_core_options "${core_options}") set_property(TARGET Qt5::Core PROPERTY INTERFACE_COMPILE_OPTIONS ${new_qt5_core_options}) set_property(TARGET Qt5::Core PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE "ON") if(NOT IS_MSVC) set(CMAKE_CXX_COMPILE_OPTIONS_PIE "-fPIC") endif() endif() endif() # Enable automatic compilation of Qt resource files. set(CMAKE_AUTORCC ON) endif() endif() if(GUI_ENABLED AND Qt5_FOUND) add_definitions("-DCOLMAP_GUI_ENABLED") message(STATUS "Enabling GUI support") else() set(GUI_ENABLED OFF) message(STATUS "Disabling GUI support") endif() if(OPENGL_ENABLED) if(NOT GUI_ENABLED) message(STATUS "Disabling GUI also disables OpenGL") set(OPENGL_ENABLED OFF) else() add_definitions("-DCOLMAP_OPENGL_ENABLED") message(STATUS "Enabling OpenGL support") endif() else() message(STATUS "Disabling OpenGL support") endif() set(GPU_ENABLED OFF) if(OPENGL_ENABLED OR CUDA_ENABLED) add_definitions("-DCOLMAP_GPU_ENABLED") message(STATUS "Enabling GPU support") set(GPU_ENABLED ON) endif() colmap-3.9.1/cmake/FindFLANN.cmake000066400000000000000000000063671454702036400165410ustar00rootroot00000000000000# 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 FLANN library. # # The following variables are set by this module: # # FLANN_FOUND: TRUE if FLANN is found. # flann: Imported target to link against. # # The following variables control the behavior of this module: # # FLANN_INCLUDE_DIR_HINTS: List of additional directories in which to # search for FLANN includes. # FLANN_LIBRARY_DIR_HINTS: List of additional directories in which to # search for FLANN libraries. set(FLANN_INCLUDE_DIR_HINTS "" CACHE PATH "FLANN include directory") set(FLANN_LIBRARY_DIR_HINTS "" CACHE PATH "FLANN library directory") unset(FLANN_FOUND) unset(FLANN_INCLUDE_DIRS) unset(FLANN_LIBRARIES) list(APPEND FLANN_CHECK_INCLUDE_DIRS ${FLANN_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /opt/include /opt/local/include ) list(APPEND FLANN_CHECK_LIBRARY_DIRS ${FLANN_LIBRARY_DIR_HINTS} /usr/lib /usr/local/lib /opt/lib /opt/local/lib ) find_path(FLANN_INCLUDE_DIRS NAMES flann/flann.hpp PATHS ${FLANN_CHECK_INCLUDE_DIRS}) find_library(FLANN_LIBRARIES NAMES flann PATHS ${FLANN_CHECK_LIBRARY_DIRS}) if(FLANN_INCLUDE_DIRS AND FLANN_LIBRARIES) set(FLANN_FOUND TRUE) endif() if(FLANN_FOUND) message(STATUS "Found FLANN") message(STATUS " Includes : ${FLANN_INCLUDE_DIRS}") message(STATUS " Libraries : ${FLANN_LIBRARIES}") else() if(FLANN_FIND_REQUIRED) message(FATAL_ERROR "Could not find FLANN") endif() endif() add_library(flann INTERFACE IMPORTED) target_include_directories( flann INTERFACE ${FLANN_INCLUDE_DIRS}) target_link_libraries( flann INTERFACE ${FLANN_LIBRARIES}) colmap-3.9.1/cmake/FindFreeImage.cmake000066400000000000000000000074261454702036400175240ustar00rootroot00000000000000# 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 FreeImage library. # # The following variables are set by this module: # # FREEIMAGE_FOUND: TRUE if FreeImage is found. # freeimage::FreeImage: Imported target to link against. # # The following variables control the behavior of this module: # # FREEIMAGE_INCLUDE_DIR_HINTS: List of additional directories in which to # search for FreeImage includes. # FREEIMAGE_LIBRARY_DIR_HINTS: List of additional directories in which to # search for FreeImage libraries. set(FREEIMAGE_INCLUDE_DIR_HINTS "" CACHE PATH "FreeImage include directory") set(FREEIMAGE_LIBRARY_DIR_HINTS "" CACHE PATH "FreeImage library directory") unset(FREEIMAGE_FOUND) find_package(FreeImage CONFIG QUIET) if(FreeImage_FOUND) if(TARGET freeimage::FreeImage) set(FREEIMAGE_FOUND TRUE) message(STATUS "Found FreeImage") message(STATUS " Target : freeimage::FreeImage") endif() else() list(APPEND FREEIMAGE_CHECK_INCLUDE_DIRS ${FREEIMAGE_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /opt/include /opt/local/include ) list(APPEND FREEIMAGE_CHECK_LIBRARY_DIRS ${FREEIMAGE_LIBRARY_DIR_HINTS} /usr/lib /usr/local/lib /opt/lib /opt/local/lib ) find_path(FREEIMAGE_INCLUDE_DIRS NAMES FreeImage.h PATHS ${FREEIMAGE_CHECK_INCLUDE_DIRS}) find_library(FREEIMAGE_LIBRARIES NAMES freeimage PATHS ${FREEIMAGE_CHECK_LIBRARY_DIRS}) if(FREEIMAGE_INCLUDE_DIRS AND FREEIMAGE_LIBRARIES) set(FREEIMAGE_FOUND TRUE) endif() if(FREEIMAGE_FOUND) message(STATUS "Found FreeImage") message(STATUS " Includes : ${FREEIMAGE_INCLUDE_DIRS}") message(STATUS " Libraries : ${FREEIMAGE_LIBRARIES}") endif() add_library(freeimage::FreeImage INTERFACE IMPORTED) target_include_directories( freeimage::FreeImage INTERFACE ${FREEIMAGE_INCLUDE_DIRS}) target_link_libraries( freeimage::FreeImage INTERFACE ${FREEIMAGE_LIBRARIES}) endif() if(NOT FREEIMAGE_FOUND AND FREEIMAGE_FIND_REQUIRED) message(FATAL_ERROR "Could not find FreeImage") endif() colmap-3.9.1/cmake/FindGlew.cmake000066400000000000000000000067671454702036400166050ustar00rootroot00000000000000# 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 Glew library. # # The following variables are set by this module: # # GLEW_FOUND: TRUE if Glew is found. # GLEW::GLEW: Imported target to link against. # # The following variables control the behavior of this module: # # GLEW_INCLUDE_DIR_HINTS: List of additional directories in which to # search for Glew includes. # GLEW_LIBRARY_DIR_HINTS: List of additional directories in which to # search for Glew libraries. set(GLEW_INCLUDE_DIR_HINTS "" CACHE PATH "Glew include directory") set(GLEW_LIBRARY_DIR_HINTS "" CACHE PATH "Glew library directory") unset(GLEW_FOUND) unset(GLEW_INCLUDE_DIRS) unset(GLEW_LIBRARIES) find_package(Glew CONFIG QUIET) if(TARGET GLEW::GLEW) set(GLEW_FOUND TRUE) message(STATUS "Found Glew") message(STATUS " Target : GLEW::GLEW") else() find_path(GLEW_INCLUDE_DIRS NAMES GL/glew.h PATHS ${GLEW_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /sw/include /opt/include /opt/local/include) find_library(GLEW_LIBRARIES NAMES GLEW Glew glew glew32 PATHS ${GLEW_LIBRARY_DIR_HINTS} /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib /sw/lib /opt/lib /opt/local/lib) if(GLEW_INCLUDE_DIRS AND GLEW_LIBRARIES) set(GLEW_FOUND TRUE) message(STATUS "Found Glew") message(STATUS " Includes : ${GLEW_INCLUDE_DIRS}") message(STATUS " Libraries : ${GLEW_LIBRARIES}") else() set(GLEW_FOUND FALSE) endif() add_library(GLEW::GLEW INTERFACE IMPORTED) target_include_directories( GLEW::GLEW INTERFACE ${GLEW_INCLUDE_DIRS}) target_link_libraries( GLEW::GLEW INTERFACE ${GLEW_LIBRARIES}) endif() if(NOT GLEW_FOUND AND GLEW_FIND_REQUIRED) message(FATAL_ERROR "Could not find Glew") endif() colmap-3.9.1/cmake/FindGlog.cmake000066400000000000000000000101331454702036400165550ustar00rootroot00000000000000# 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() colmap-3.9.1/cmake/FindLZ4.cmake000066400000000000000000000063731454702036400163110ustar00rootroot00000000000000# 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 LZ4 library. # # The following variables are set by this module: # # LZ4_FOUND: TRUE if LZ4 is found. # LZ4_INCLUDE_DIRS: Include directories for LZ4. # LZ4_LIBRARIES: Libraries required to link LZ4. # # The following variables control the behavior of this module: # # LZ4_INCLUDE_DIR_HINTS: List of additional directories in which to # search for LZ4 includes. # LZ4_LIBRARY_DIR_HINTS: List of additional directories in which to # search for LZ4 libraries. set(LZ4_INCLUDE_DIR_HINTS "" CACHE PATH "LZ4 include directory") set(LZ4_LIBRARY_DIR_HINTS "" CACHE PATH "LZ4 library directory") unset(LZ4_FOUND) unset(LZ4_INCLUDE_DIRS) unset(LZ4_LIBRARIES) list(APPEND LZ4_CHECK_INCLUDE_DIRS ${LZ4_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /opt/include /opt/local/include ) list(APPEND LZ4_CHECK_LIBRARY_DIRS ${LZ4_LIBRARY_DIR_HINTS} /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu /opt/lib /opt/local/lib ) find_path(LZ4_INCLUDE_DIRS NAMES lz4.h PATHS ${LZ4_CHECK_INCLUDE_DIRS}) find_library(LZ4_LIBRARIES NAMES lz4 PATHS ${LZ4_CHECK_LIBRARY_DIRS}) if(LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) set(LZ4_FOUND TRUE) endif() if(LZ4_FOUND) message(STATUS "Found LZ4") message(STATUS " Includes : ${LZ4_INCLUDE_DIRS}") message(STATUS " Libraries : ${LZ4_LIBRARIES}") else() if(LZ4_FIND_REQUIRED) message(FATAL_ERROR "Could not find LZ4") endif() endif() add_library(lz4 INTERFACE IMPORTED) target_include_directories( lz4 INTERFACE ${LZ4_INCLUDE_DIRS}) target_link_libraries( lz4 INTERFACE ${LZ4_LIBRARIES}) colmap-3.9.1/cmake/FindMetis.cmake000066400000000000000000000071041454702036400167520ustar00rootroot00000000000000# 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 Metis library. # # The following variables are set by this module: # # METIS_FOUND: TRUE if Metis is found. # metis: Imported target to link against. # # The following variables control the behavior of this module: # # METIS_INCLUDE_DIR_HINTS: List of additional directories in which to # search for Metis includes. # METIS_LIBRARY_DIR_HINTS: List of additional directories in which to # search for Metis libraries. set(METIS_INCLUDE_DIR_HINTS "" CACHE PATH "Metis include directory") set(METIS_LIBRARY_DIR_HINTS "" CACHE PATH "Metis library directory") unset(METIS_FOUND) find_package(metis CONFIG QUIET) if(TARGET metis) set(METIS_FOUND TRUE) message(STATUS "Found Metis") message(STATUS " Target : metis") else() list(APPEND METIS_CHECK_INCLUDE_DIRS ${METIS_INCLUDE_DIR_HINTS} /usr/include /usr/local/include /opt/include /opt/local/include ) list(APPEND METIS_CHECK_LIBRARY_DIRS ${METIS_LIBRARY_DIR_HINTS} /usr/lib /usr/local/lib /opt/lib /opt/local/lib ) find_path(METIS_INCLUDE_DIRS NAMES metis.h PATHS ${METIS_CHECK_INCLUDE_DIRS}) find_library(METIS_LIBRARIES NAMES metis PATHS ${METIS_CHECK_LIBRARY_DIRS}) find_library(GK_LIBRARIES NAMES GKlib PATHS ${METIS_CHECK_LIBRARY_DIRS}) if(METIS_INCLUDE_DIRS AND METIS_LIBRARIES) set(METIS_FOUND TRUE) message(STATUS "Found Metis") message(STATUS " Includes : ${METIS_INCLUDE_DIRS}") message(STATUS " Libraries : ${METIS_LIBRARIES}") endif() add_library(metis INTERFACE IMPORTED) target_include_directories( metis INTERFACE ${METIS_INCLUDE_DIRS}) target_link_libraries( metis INTERFACE ${METIS_LIBRARIES}) endif() if(NOT METIS_FOUND AND METIS_FIND_REQUIRED) message(FATAL_ERROR "Could not find Metis") endif() colmap-3.9.1/cmake/GenerateVersionDefinitions.cmake000066400000000000000000000047721454702036400223740ustar00rootroot00000000000000# 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. if (DEFINED GIT_COMMIT_ID OR DEFINED GIT_COMMIT_DATE) message(STATUS "Using custom-defined GIT_COMMIT_ID (${GIT_COMMIT_ID}) " "and GIT_COMMIT_DATE (${GIT_COMMIT_DATE})") elseif(Git_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT_VARIABLE GIT_COMMIT_ID ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%ad --date=short WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT_VARIABLE GIT_COMMIT_DATE ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) else() set(GIT_COMMIT_ID "Unknown") set(GIT_COMMIT_DATE "Unknown") endif() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/colmap/util/version.cc.in" "${CMAKE_CURRENT_SOURCE_DIR}/src/colmap/util/version.cc") colmap-3.9.1/cmake/SelectCudaComputeArch.cmake000066400000000000000000000315751454702036400212500ustar00rootroot00000000000000# CMake - Cross Platform Makefile Generator # Copyright 2000-2017 Kitware, Inc. and Contributors # 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 Kitware, Inc. nor the names of 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 # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Synopsis: # CUDA_SELECT_NVCC_ARCH_FLAGS(out_variable [target_CUDA_architectures]) # -- Selects GPU arch flags for nvcc based on target_CUDA_architectures # target_CUDA_architectures : Auto | Common | All | LIST(ARCH_AND_PTX ...) # - "Auto" detects local machine GPU compute arch at runtime. # - "Common" and "All" cover common and entire subsets of architectures # ARCH_AND_PTX : NAME | NUM.NUM | NUM.NUM(NUM.NUM) | NUM.NUM+PTX # NAME: Fermi Kepler Maxwell Kepler+Tegra Kepler+Tesla Maxwell+Tegra Pascal Volta Turing Ampere # NUM: Any number. Only those pairs are currently accepted by NVCC though: # 2.0 2.1 3.0 3.2 3.5 3.7 5.0 5.2 5.3 6.0 6.2 7.0 7.2 7.5 8.0 8.6 # Returns LIST of flags to be added to CUDA_NVCC_FLAGS in ${out_variable} # Additionally, sets ${out_variable}_readable to the resulting numeric list # Example: # CUDA_SELECT_NVCC_ARCH_FLAGS(ARCH_FLAGS 3.0 3.5+PTX 5.2(5.0) Maxwell) # LIST(APPEND CUDA_NVCC_FLAGS ${ARCH_FLAGS}) # # More info on CUDA architectures: https://en.wikipedia.org/wiki/CUDA # if(CMAKE_CUDA_COMPILER_LOADED) # CUDA as a language if(CMAKE_CUDA_COMPILER_ID STREQUAL "NVIDIA" AND CMAKE_CUDA_COMPILER_VERSION MATCHES "^([0-9]+\\.[0-9]+)") set(CUDA_VERSION "${CMAKE_MATCH_1}") endif() endif() # See: https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#gpu-feature-list # This list will be used for CUDA_ARCH_NAME = All option set(CUDA_KNOWN_GPU_ARCHITECTURES "Fermi" "Kepler" "Maxwell") # This list will be used for CUDA_ARCH_NAME = Common option (enabled by default) set(CUDA_COMMON_GPU_ARCHITECTURES "3.5" "5.0") # 3.0 is removed in CUDA 11, see: # https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html#deprecated-features if(CUDA_VERSION VERSION_LESS "11.0") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "3.0") endif() if(CUDA_VERSION VERSION_LESS "7.0") set(CUDA_LIMIT_GPU_ARCHITECTURE "5.2") endif() # This list is used to filter CUDA archs when autodetecting set(CUDA_ALL_GPU_ARCHITECTURES "3.0" "3.2" "3.5" "5.0") if(CUDA_VERSION VERSION_EQUAL "7.0" OR CUDA_VERSION VERSION_GREATER "7.0") list(APPEND CUDA_KNOWN_GPU_ARCHITECTURES "Kepler+Tegra" "Kepler+Tesla" "Maxwell+Tegra") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "5.2") if(CUDA_VERSION VERSION_LESS "8.0") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "5.2+PTX") set(CUDA_LIMIT_GPU_ARCHITECTURE "6.0") endif() endif() if(CUDA_VERSION VERSION_EQUAL "8.0" OR CUDA_VERSION VERSION_GREATER "8.0") list(APPEND CUDA_KNOWN_GPU_ARCHITECTURES "Pascal") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "6.0" "6.1") list(APPEND CUDA_ALL_GPU_ARCHITECTURES "6.0" "6.1" "6.2") if(CUDA_VERSION VERSION_LESS "9.0") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "6.2+PTX") set(CUDA_LIMIT_GPU_ARCHITECTURE "7.0") endif() endif () if(CUDA_VERSION VERSION_EQUAL "9.0" OR CUDA_VERSION VERSION_GREATER "9.0") list(APPEND CUDA_KNOWN_GPU_ARCHITECTURES "Volta") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "7.0") list(APPEND CUDA_ALL_GPU_ARCHITECTURES "7.0" "7.2") if(CUDA_VERSION VERSION_LESS "10.0") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "7.2+PTX") set(CUDA_LIMIT_GPU_ARCHITECTURE "8.0") endif() endif() if(CUDA_VERSION VERSION_EQUAL "10.0" OR CUDA_VERSION VERSION_GREATER "10.0") list(APPEND CUDA_KNOWN_GPU_ARCHITECTURES "Turing") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "7.5") list(APPEND CUDA_ALL_GPU_ARCHITECTURES "7.5") if(CUDA_VERSION VERSION_LESS "11.0") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "7.5+PTX") set(CUDA_LIMIT_GPU_ARCHITECTURE "8.0") endif() endif() if(CUDA_VERSION VERSION_EQUAL "11.0" OR CUDA_VERSION VERSION_GREATER "11.0") list(APPEND CUDA_KNOWN_GPU_ARCHITECTURES "Ampere") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "8.0") list(APPEND CUDA_ALL_GPU_ARCHITECTURES "8.0") if(CUDA_VERSION VERSION_LESS "11.1") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "8.0+PTX") set(CUDA_LIMIT_GPU_ARCHITECTURE "8.6") endif() endif() if(CUDA_VERSION VERSION_EQUAL "11.1" OR CUDA_VERSION VERSION_GREATER "11.1") list(APPEND CUDA_COMMON_GPU_ARCHITECTURES "8.6" "8.6+PTX") list(APPEND CUDA_ALL_GPU_ARCHITECTURES "8.6") if(CUDA_VERSION VERSION_LESS "12.0") set(CUDA_LIMIT_GPU_ARCHITECTURE "9.0") endif() endif() ################################################################################################ # A function for automatic detection of GPUs installed (if autodetection is enabled) # Usage: # CUDA_DETECT_INSTALLED_GPUS(OUT_VARIABLE) # function(CUDA_DETECT_INSTALLED_GPUS OUT_VARIABLE) if(NOT CUDA_GPU_DETECT_OUTPUT) if(CMAKE_CUDA_COMPILER_LOADED) # CUDA as a language set(file "${PROJECT_BINARY_DIR}/detect_cuda_compute_capabilities.cu") else() set(file "${PROJECT_BINARY_DIR}/detect_cuda_compute_capabilities.cpp") endif() file(WRITE ${file} "" "#include \n" "#include \n" "int main()\n" "{\n" " int count = 0;\n" " if (cudaSuccess != cudaGetDeviceCount(&count)) return -1;\n" " if (count == 0) return -1;\n" " for (int device = 0; device < count; ++device)\n" " {\n" " cudaDeviceProp prop;\n" " if (cudaSuccess == cudaGetDeviceProperties(&prop, device))\n" " std::printf(\"%d.%d \", prop.major, prop.minor);\n" " }\n" " return 0;\n" "}\n") if(CMAKE_CUDA_COMPILER_LOADED) # CUDA as a language try_run(run_result compile_result ${PROJECT_BINARY_DIR} ${file} RUN_OUTPUT_VARIABLE compute_capabilities) else() try_run(run_result compile_result ${PROJECT_BINARY_DIR} ${file} CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${CUDA_INCLUDE_DIRS}" LINK_LIBRARIES ${CUDA_LIBRARIES} RUN_OUTPUT_VARIABLE compute_capabilities) endif() # Filter unrelated content out of the output. string(REGEX MATCHALL "[0-9]+\\.[0-9]+" compute_capabilities "${compute_capabilities}") if(run_result EQUAL 0) string(REPLACE "2.1" "2.1(2.0)" compute_capabilities "${compute_capabilities}") set(CUDA_GPU_DETECT_OUTPUT ${compute_capabilities} CACHE INTERNAL "Returned GPU architectures from detect_gpus tool" FORCE) endif() endif() if(NOT CUDA_GPU_DETECT_OUTPUT) message(STATUS "Automatic GPU detection failed. Building for common architectures.") set(${OUT_VARIABLE} ${CUDA_COMMON_GPU_ARCHITECTURES} PARENT_SCOPE) else() # Filter based on CUDA version supported archs set(CUDA_GPU_DETECT_OUTPUT_FILTERED "") separate_arguments(CUDA_GPU_DETECT_OUTPUT) foreach(ITEM IN ITEMS ${CUDA_GPU_DETECT_OUTPUT}) if(CUDA_LIMIT_GPU_ARCHITECTURE AND (ITEM VERSION_EQUAL CUDA_LIMIT_GPU_ARCHITECTURE OR ITEM VERSION_GREATER CUDA_LIMIT_GPU_ARCHITECTURE)) list(GET CUDA_COMMON_GPU_ARCHITECTURES -1 NEWITEM) string(APPEND CUDA_GPU_DETECT_OUTPUT_FILTERED " ${NEWITEM}") else() string(APPEND CUDA_GPU_DETECT_OUTPUT_FILTERED " ${ITEM}") endif() endforeach() set(${OUT_VARIABLE} ${CUDA_GPU_DETECT_OUTPUT_FILTERED} PARENT_SCOPE) endif() endfunction() ################################################################################################ # Function for selecting GPU arch flags for nvcc based on CUDA architectures from parameter list # Usage: # SELECT_NVCC_ARCH_FLAGS(out_variable [list of CUDA compute archs]) function(CUDA_SELECT_NVCC_ARCH_FLAGS out_variable) set(CUDA_ARCH_LIST "${ARGN}") if("X${CUDA_ARCH_LIST}" STREQUAL "X" ) set(CUDA_ARCH_LIST "Auto") endif() set(cuda_arch_bin) set(cuda_arch_ptx) if("${CUDA_ARCH_LIST}" STREQUAL "All") set(CUDA_ARCH_LIST ${CUDA_KNOWN_GPU_ARCHITECTURES}) elseif("${CUDA_ARCH_LIST}" STREQUAL "Common") set(CUDA_ARCH_LIST ${CUDA_COMMON_GPU_ARCHITECTURES}) elseif("${CUDA_ARCH_LIST}" STREQUAL "Auto") CUDA_DETECT_INSTALLED_GPUS(CUDA_ARCH_LIST) message(STATUS "Autodetected CUDA architecture(s): ${CUDA_ARCH_LIST}") endif() # Now process the list and look for names string(REGEX REPLACE "[ \t]+" ";" CUDA_ARCH_LIST "${CUDA_ARCH_LIST}") list(REMOVE_DUPLICATES CUDA_ARCH_LIST) foreach(arch_name ${CUDA_ARCH_LIST}) set(arch_bin) set(arch_ptx) set(add_ptx FALSE) # Check to see if we are compiling PTX if(arch_name MATCHES "(.*)\\+PTX$") set(add_ptx TRUE) set(arch_name ${CMAKE_MATCH_1}) endif() if(arch_name MATCHES "^([0-9]\\.[0-9](\\([0-9]\\.[0-9]\\))?)$") set(arch_bin ${CMAKE_MATCH_1}) set(arch_ptx ${arch_bin}) else() # Look for it in our list of known architectures if(${arch_name} STREQUAL "Fermi") set(arch_bin 2.0 "2.1(2.0)") elseif(${arch_name} STREQUAL "Kepler+Tegra") set(arch_bin 3.2) elseif(${arch_name} STREQUAL "Kepler+Tesla") set(arch_bin 3.7) elseif(${arch_name} STREQUAL "Kepler") set(arch_bin 3.0 3.5) set(arch_ptx 3.5) elseif(${arch_name} STREQUAL "Maxwell+Tegra") set(arch_bin 5.3) elseif(${arch_name} STREQUAL "Maxwell") set(arch_bin 5.0 5.2) set(arch_ptx 5.2) elseif(${arch_name} STREQUAL "Pascal") set(arch_bin 6.0 6.1) set(arch_ptx 6.1) elseif(${arch_name} STREQUAL "Volta") set(arch_bin 7.0 7.0) set(arch_ptx 7.0) elseif(${arch_name} STREQUAL "Turing") set(arch_bin 7.5) set(arch_ptx 7.5) elseif(${arch_name} STREQUAL "Ampere") set(arch_bin 8.0) set(arch_ptx 8.0) else() message(SEND_ERROR "Unknown CUDA Architecture Name ${arch_name} in CUDA_SELECT_NVCC_ARCH_FLAGS") endif() endif() if(NOT arch_bin) message(SEND_ERROR "arch_bin wasn't set for some reason") endif() list(APPEND cuda_arch_bin ${arch_bin}) if(add_ptx) if (NOT arch_ptx) set(arch_ptx ${arch_bin}) endif() list(APPEND cuda_arch_ptx ${arch_ptx}) endif() endforeach() # remove dots and convert to lists string(REGEX REPLACE "\\." "" cuda_arch_bin "${cuda_arch_bin}") string(REGEX REPLACE "\\." "" cuda_arch_ptx "${cuda_arch_ptx}") string(REGEX MATCHALL "[0-9()]+" cuda_arch_bin "${cuda_arch_bin}") string(REGEX MATCHALL "[0-9]+" cuda_arch_ptx "${cuda_arch_ptx}") if(cuda_arch_bin) list(REMOVE_DUPLICATES cuda_arch_bin) endif() if(cuda_arch_ptx) list(REMOVE_DUPLICATES cuda_arch_ptx) endif() set(nvcc_flags "") set(nvcc_archs_readable "") # Tell NVCC to add binaries for the specified GPUs foreach(arch ${cuda_arch_bin}) if(arch MATCHES "([0-9]+)\\(([0-9]+)\\)") # User explicitly specified ARCH for the concrete CODE list(APPEND nvcc_flags -gencode arch=compute_${CMAKE_MATCH_2},code=sm_${CMAKE_MATCH_1}) list(APPEND nvcc_archs_readable sm_${CMAKE_MATCH_1}) else() # User didn't explicitly specify ARCH for the concrete CODE, we assume ARCH=CODE list(APPEND nvcc_flags -gencode arch=compute_${arch},code=sm_${arch}) list(APPEND nvcc_archs_readable sm_${arch}) endif() endforeach() # Tell NVCC to add PTX intermediate code for the specified architectures foreach(arch ${cuda_arch_ptx}) list(APPEND nvcc_flags -gencode arch=compute_${arch},code=compute_${arch}) list(APPEND nvcc_archs_readable compute_${arch}) endforeach() string(REPLACE ";" " " nvcc_archs_readable "${nvcc_archs_readable}") set(${out_variable} ${nvcc_flags} PARENT_SCOPE) set(${out_variable}_readable ${nvcc_archs_readable} PARENT_SCOPE) endfunction() colmap-3.9.1/cmake/colmap-config-version.cmake.in000066400000000000000000000034211454702036400216740ustar00rootroot00000000000000# 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. set(PACKAGE_VERSION "@COLMAP_VERSION@") if("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") set(PACKAGE_VERSION_COMPATIBLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE FALSE) endif() colmap-3.9.1/cmake/colmap-config.cmake.in000066400000000000000000000060031454702036400202100ustar00rootroot00000000000000# 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 config for COLMAP library. # # The following variables are set by this config: # # COLMAP_FOUND: TRUE if COLMAP is found. # COLMAP_VERSION: COLMAP version. # # The colmap::colmap imported interface target is defined. @PACKAGE_INIT@ set(COLMAP_FOUND FALSE) # Set hints for finding dependency packages. set(FLANN_INCLUDE_DIR_HINTS @FLANN_INCLUDE_DIR_HINTS@) set(FLANN_LIBRARY_DIR_HINTS @FLANN_LIBRARY_DIR_HINTS@) set(LZ4_INCLUDE_DIR_HINTS @LZ4_INCLUDE_DIR_HINTS@) set(LZ4_LIBRARY_DIR_HINTS @LZ4_LIBRARY_DIR_HINTS@) set(FREEIMAGE_INCLUDE_DIR_HINTS @FREEIMAGE_INCLUDE_DIR_HINTS@) set(FREEIMAGE_LIBRARY_DIR_HINTS @FREEIMAGE_LIBRARY_DIR_HINTS@) set(METIS_INCLUDE_DIR_HINTS @METIS_INCLUDE_DIR_HINTS@) set(METIS_LIBRARY_DIR_HINTS @METIS_LIBRARY_DIR_HINTS@) set(GLEW_INCLUDE_DIR_HINTS @GLEW_INCLUDE_DIR_HINTS@) set(GLEW_LIBRARY_DIR_HINTS @GLEW_LIBRARY_DIR_HINTS@) # Find dependency packages. set(TEMP_CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}) set(CMAKE_MODULE_PATH ${PACKAGE_PREFIX_DIR}/share/colmap/cmake) # Set the exported variables. set(COLMAP_FOUND TRUE) set(COLMAP_VERSION @COLMAP_VERSION@) set(OPENMP_ENABLED @OPENMP_ENABLED@) set(CUDA_ENABLED @CUDA_ENABLED@) set(CUDA_MIN_VERSION @CUDA_MIN_VERSION@) set(GUI_ENABLED @GUI_ENABLED@) set(CGAL_ENABLED @CGAL_ENABLED@) include(${PACKAGE_PREFIX_DIR}/share/colmap/colmap-targets.cmake) include(${PACKAGE_PREFIX_DIR}/share/colmap/cmake/FindDependencies.cmake) check_required_components(colmap) colmap-3.9.1/doc/000077500000000000000000000000001454702036400135515ustar00rootroot00000000000000colmap-3.9.1/doc/COLMAP.desktop000066400000000000000000000003601454702036400161160ustar00rootroot00000000000000[Desktop Entry] Name=COLMAP Comment=Structure-from-Motion and Multi-View Stereo Exec=colmap gui Icon=colmap Terminal=false Categories=Graphics;3DGraphics; Keywords=3d;reconstruction;structure-from-motion;multi-view-stereo; Type=Application colmap-3.9.1/doc/Makefile000077500000000000000000000151521454702036400152200ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/COLMAP.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/COLMAP.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/COLMAP" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/COLMAP" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." colmap-3.9.1/doc/_build/000077500000000000000000000000001454702036400150075ustar00rootroot00000000000000colmap-3.9.1/doc/_build/html/000077500000000000000000000000001454702036400157535ustar00rootroot00000000000000colmap-3.9.1/doc/bibliography.rst000077500000000000000000000034441454702036400167660ustar00rootroot00000000000000Bibliography ============ .. [schoenberger_thesis] Johannes L. Schönberger. "Robust Methods for Accurate and Efficient 3D Modeling from Unstructured Imagery." ETH Zürich, 2018. .. [furukawa10] Furukawa, Yasutaka, and Jean Ponce. "Accurate, dense, and robust multiview stereopsis." Transactions on Pattern Analysis and Machine Intelligence, 2010. .. [hofer16] Hofer, M., Maurer, M., and Bischof, H. Efficient 3D Scene Abstraction Using Line Segments, Computer Vision and Image Understanding, 2016. .. [jancosek11] Jancosek, Michal, and Tomás Pajdla. "Multi-view reconstruction preserving weakly-supported surfaces." Conference on Computer Vision and Pattern Recognition, 2011. .. [kazhdan2013] Kazhdan, Michael and Hoppe, Hugues "Screened poisson surface reconstruction." ACM Transactions on Graphics (TOG), 2013. .. [schoenberger16sfm] Schönberger, Johannes Lutz and Frahm, Jan-Michael. "Structure-from-Motion Revisited." Conference on Computer Vision and Pattern Recognition, 2016. .. [schoenberger16mvs] Schönberger, Johannes Lutz and Zheng, Enliang and Pollefeys, Marc and Frahm, Jan-Michael. "Pixelwise View Selection for Unstructured Multi-View Stereo." European Conference on Computer Vision, 2016. .. [schoenberger16vote] Schönberger, Johannes Lutz and Price, True and Sattler, Torsten and Frahm, Jan-Michael and Pollefeys, Marc "A Vote­-and­-Verify Strategy for Fast Spatial Verification in Image Retrieval." Asian Conference on Computer Vision, 2016. .. [lowe04] Lowe, David G. "Distinctive image features from scale-invariant keypoints". International journal of computer vision 60.2 (2004): 91-110. .. [wu13] Wu, Changchang. "Towards linear-time incremental structure from motion." International Conference 3D Vision, 2013. colmap-3.9.1/doc/cameras.rst000066400000000000000000000052251454702036400157220ustar00rootroot00000000000000Camera Models ============= COLMAP implements different camera models of varying complexity. If no intrinsic parameters are known a priori, it is generally best to use the simplest camera model that is complex enough to model the distortion effects: - ``SIMPLE_PINHOLE``, ``PINHOLE``: Use these camera models, if your images are undistorted a priori. These use one and two focal length parameters, respectively. Note that even in the case of undistorted images, COLMAP could try to improve the intrinsics with a more complex camera model. - ``SIMPLE_RADIAL``, ``RADIAL``: This should be the camera model of choice, if the intrinsics are unknown and every image has a different camera calibration, e.g., in the case of Internet photos. Both models are simplified versions of the ``OPENCV`` model only modeling radial distortion effects with one and two parameters, respectively. - ``OPENCV``, ``FULL_OPENCV``: Use these camera models, if you know the calibration parameters a priori. You can also try to let COLMAP estimate the parameters, if you share the intrinsics for multiple images. Note that the automatic estimation of parameters will most likely fail, if every image has a separate set of intrinsic parameters. - ``SIMPLE_RADIAL_FISHEYE``, ``RADIAL_FISHEYE``, ``OPENCV_FISHEYE``, ``FOV``, ``THIN_PRISM_FISHEYE``: Use these camera models for fisheye lenses and note that all other models are not really capable of modeling the distortion effects of fisheye lenses. The ``FOV`` model is used by Google Project Tango (make sure to not initialize `omega` to zero). You can inspect the estimated intrinsic parameters by double-clicking specific images in the model viewer or by exporting the model and opening the `cameras.txt` file. To achieve optimal reconstruction results, you might have to try different camera models for your problem. Generally, when the reconstruction fails and the estimated focal length values / distortion coefficients are grossly wrong, it is a sign of using a too complex camera model. Contrary, if COLMAP uses many iterative local and global bundle adjustments, it is a sign of using a too simple camera model that is not able to fully model the distortion effects. You can also share intrinsics between multiple images to obtain more reliable results (see :ref:`Share intrinsic camera parameters `) or you can fix the intrinsic parameters during the reconstruction (see :ref:`Fix intrinsic camera parameters `). Please, refer to the camera models header file for information on the parameters of the different camera models: https://github.com/colmap/colmap/blob/main/src/colmap/sensor/models.h colmap-3.9.1/doc/changelog.rst000066400000000000000000000001031454702036400162240ustar00rootroot00000000000000.. _changelog: Changelog ========= .. include:: ../CHANGELOG.txt colmap-3.9.1/doc/cli.rst000066400000000000000000000273441454702036400150640ustar00rootroot00000000000000.. _cli: Command-line Interface ====================== The command-line interface provides access to all of COLMAP's functionality for automated scripting. Each core functionality is implemented as a command to the ``colmap`` executable. Run ``colmap -h`` to list the available commands (or ``COLMAP.bat -h`` under Windows). Note that if you run COLMAP from the CMake build folder, the executable is located at ``./src/colmap/exe/colmap``. To start the graphical user interface, run ``colmap gui``. Example ------- Assuming you stored the images of your project in the following structure:: /path/to/project/... +── images │   +── image1.jpg │   +── image2.jpg │   +── ... │   +── imageN.jpg The command for the automatic reconstruction tool would be:: # The project folder must contain a folder "images" with all the images. $ DATASET_PATH=/path/to/project $ colmap automatic_reconstructor \ --workspace_path $DATASET_PATH \ --image_path $DATASET_PATH/images Note that any command lists all available options using the ``-h,--help`` command-line argument. In case you need more control over the individual parameters of the reconstruction process, you can execute the following sequence of commands as an alternative to the automatic reconstruction command:: # The project folder must contain a folder "images" with all the images. $ DATASET_PATH=/path/to/dataset $ colmap feature_extractor \ --database_path $DATASET_PATH/database.db \ --image_path $DATASET_PATH/images $ colmap exhaustive_matcher \ --database_path $DATASET_PATH/database.db $ mkdir $DATASET_PATH/sparse $ colmap mapper \ --database_path $DATASET_PATH/database.db \ --image_path $DATASET_PATH/images \ --output_path $DATASET_PATH/sparse $ mkdir $DATASET_PATH/dense $ colmap image_undistorter \ --image_path $DATASET_PATH/images \ --input_path $DATASET_PATH/sparse/0 \ --output_path $DATASET_PATH/dense \ --output_type COLMAP \ --max_image_size 2000 $ colmap patch_match_stereo \ --workspace_path $DATASET_PATH/dense \ --workspace_format COLMAP \ --PatchMatchStereo.geom_consistency true $ colmap stereo_fusion \ --workspace_path $DATASET_PATH/dense \ --workspace_format COLMAP \ --input_type geometric \ --output_path $DATASET_PATH/dense/fused.ply $ colmap poisson_mesher \ --input_path $DATASET_PATH/dense/fused.ply \ --output_path $DATASET_PATH/dense/meshed-poisson.ply $ colmap delaunay_mesher \ --input_path $DATASET_PATH/dense \ --output_path $DATASET_PATH/dense/meshed-delaunay.ply If you want to run COLMAP on a computer without an attached display (e.g., cluster or cloud service), COLMAP automatically switches to use CUDA if supported by your system. If no CUDA enabled device is available, you can manually select to use CPU-based feature extraction and matching by setting the ``--SiftExtraction.use_gpu 0`` and ``--SiftMatching.use_gpu 0`` options. Help ---- The available commands can be listed using the command:: $ colmap help Usage: colmap [command] [options] Documentation: https://colmap.github.io/ Example usage: colmap help [ -h, --help ] colmap gui colmap gui -h [ --help ] colmap automatic_reconstructor -h [ --help ] colmap automatic_reconstructor --image_path IMAGES --workspace_path WORKSPACE colmap feature_extractor --image_path IMAGES --database_path DATABASE colmap exhaustive_matcher --database_path DATABASE colmap mapper --image_path IMAGES --database_path DATABASE --output_path MODEL ... Available commands: help gui automatic_reconstructor bundle_adjuster color_extractor database_creator delaunay_mesher exhaustive_matcher feature_extractor feature_importer image_deleter image_rectifier image_registrator image_undistorter mapper matches_importer model_aligner model_analyzer model_converter model_merger model_orientation_aligner patch_match_stereo point_triangulator poisson_mesher rig_bundle_adjuster sequential_matcher spatial_matcher stereo_fusion transitive_matcher vocab_tree_builder vocab_tree_matcher vocab_tree_retriever And each command has a ``-h,--help`` command-line argument to show the usage and the available options, e.g.:: $ colmap feature_extractor -h Options can either be specified via command-line or by defining them in a .ini project file passed to `--project_path`. -h [ --help ] --project_path arg --database_path arg --image_path arg --image_list_path arg --ImageReader.camera_model arg (=SIMPLE_RADIAL) --ImageReader.single_camera arg (=0) --ImageReader.camera_params arg --ImageReader.default_focal_length_factor arg (=1.2) --SiftExtraction.num_threads arg (=-1) --SiftExtraction.use_gpu arg (=1) --SiftExtraction.gpu_index arg (=-1) --SiftExtraction.max_image_size arg (=3200) --SiftExtraction.max_num_features arg (=8192) --SiftExtraction.first_octave arg (=-1) --SiftExtraction.num_octaves arg (=4) --SiftExtraction.octave_resolution arg (=3) --SiftExtraction.peak_threshold arg (=0.0066666666666666671) --SiftExtraction.edge_threshold arg (=10) --SiftExtraction.estimate_affine_shape arg (=0) --SiftExtraction.max_num_orientations arg (=2) --SiftExtraction.upright arg (=0) --SiftExtraction.domain_size_pooling arg (=0) --SiftExtraction.dsp_min_scale arg (=0.16666666666666666) --SiftExtraction.dsp_max_scale arg (=3) --SiftExtraction.dsp_num_scales arg (=10) The available options can either be provided directly from the command-line or through a `.ini` file provided to ``--project_path``. Commands -------- The following list briefly documents the functionality of each command, that is available as ``colmap [command]``: - ``gui``: The graphical user interface, see :ref:`Graphical User Interface ` for more information. - ``automatic_reconstructor``: Automatically reconstruct sparse and dense model for a set of input images. - ``project_generator``: Generate project files at different quality settings. - ``feature_extractor``, ``feature_importer``: Perform feature extraction or import features for a set of images. - ``exhaustive_matcher``, ``vocab_tree_matcher``, ``sequential_matcher``, ``spatial_matcher``, ``transitive_matcher``, ``matches_importer``: Perform feature matching after performing feature extraction. - ``mapper``: Sparse 3D reconstruction / mapping of the dataset using SfM after performing feature extraction and matching. - ``hierarchical_mapper``: Sparse 3D reconstruction / mapping of the dataset using hierarchical SfM after performing feature extraction and matching. This parallelizes the reconstruction process by partitioning the scene into overlapping submodels and then reconstructing each submodel independently. Finally, the overlapping submodels are merged into a single reconstruction. It is recommended to run a few rounds of point triangulation and bundle adjustment after this step. - ``image_undistorter``: Undistort images and/or export them for MVS or to external dense reconstruction software, such as CMVS/PMVS. - ``image_rectifier``: Stereo rectify cameras and undistort images for stereo disparity estimation. - ``image_filterer``: Filter images from a sparse reconstruction. - ``image_deleter``: Delete specific images from a sparse reconstruction. - ``patch_match_stereo``: Dense 3D reconstruction / mapping using MVS after running the ``image_undistorter`` to initialize the workspace. - ``stereo_fusion``: Fusion of ``patch_match_stereo`` results into to a colored point cloud. - ``poisson_mesher``: Meshing of the fused point cloud using Poisson surface reconstruction. - ``delaunay_mesher``: Meshing of the reconstructed sparse or dense point cloud using a graph cut on the Delaunay triangulation and visibility voting. - ``image_registrator``: Register new images in the database against an existing model, e.g., when extracting features and matching newly added images in a database after running ``mapper``. Note that no bundle adjustment or triangulation is performed. - ``point_triangulator``: Triangulate all observations of registered images in an existing model using the feature matches in a database. - ``point_filtering``: Filter sparse points in model by enforcing criteria, such as minimum track length, maximum reprojection error, etc. - ``bundle_adjuster``: Run global bundle adjustment on a reconstructed scene, e.g., when a refinement of the intrinsics is needed or after running the ``image_registrator``. - ``database_creator``: Create an empty COLMAP SQLite database with the necessary database schema information. - ``database_merger``: Merge two databases into a new database. Note that the cameras will not be merged and that the unique camera and image identifiers might change during the merging process. - ``model_analyzer``: Print statistics about reconstructions. - ``model_aligner``: Align/geo-register model to coordinate system of given camera centers. - ``model_orientation_aligner``: Align the coordinate axis of a model using a Manhattan world assumption. - ``model_converter``: Convert the COLMAP export format to another format, such as PLY or NVM. - ``model_cropper``: Crop model to specific bounding box described in GPS or model coordinate system. - ``model_splitter``: Divide model in rectangular sub-models specified from file containing bounding box coordinates, or max extent of sub-model, or number of subdivisions in each dimension. - ``model_merger``: Attempt to merge two disconnected reconstructions, if they have common registered images. - ``color_extractor``: Extract mean colors for all 3D points of a model. - ``vocab_tree_builder``: Create a vocabulary tree from a database with extracted images. This is an offline procedure and can be run once, while the same vocabulary tree can be reused for other datasets. Note that, as a rule of thumb, you should use at least 10-100 times more features than visual words. Pre-trained trees can be downloaded from https://demuc.de/colmap/. This is useful if you want to build a custom tree with a different trade-off in terms of precision/recall vs. speed. - ``vocab_tree_retriever``: Perform vocabulary tree based image retrieval. Visualization ------------- If you want to quickly visualize the outputs of the sparse or dense reconstruction pipelines, COLMAP offers you the following possibilities: - The sparse point cloud obtained with the ``mapper`` can be visualized via the COLMAP GUI by importing the following files: choose ``File > Import Model`` and select the folder where the three files, ``cameras.txt``, ``images.txt``, and ``points3d.txt`` are located. - The dense point cloud obtained with the ``stereo_fusion`` can be visualized via the COLMAP GUI by importing ``fused.ply``: choose ``File > Import Model from...`` and then select the file ``fused.ply``. - The dense mesh model ``meshed-*.ply`` obtained with the ``poisson_mesher`` or the ``delaunay_mesher`` can currently not be visualized with COLMAP, instead you can use an external viewer, such as Meshlab. colmap-3.9.1/doc/colmap.1000066400000000000000000000021021454702036400151010ustar00rootroot00000000000000.TH colmap 1 "January 4 2018" .SH NAME colmap \- Structure-from-Motion and Multi-View Stereo .SH SYNOPSIS .B colmap .RI "[command] [options]" .SH DESCRIPTION This manual page documents briefly the .B colmap command. .PP COLMAP is a general-purpose Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline with a graphical and command-line interface. It offers a wide range of features for reconstruction of ordered and unordered image collections. .SH OPTIONS This program offers a graphical and command-line interface. Each command offers summary of all available options. .TP .B help [ \-h, \-\-help ] Show summary of all options. .TP .B gui Start graphical interface. .TP .B gui \-h [ \-\-help ] Show summary of graphical interface options. .TP .B feature_extractor \-h [ \-\-help ] Show summary of feature extractor options. .TP .B feature_extractor \-\-image_path IMAGES \-\-database_path DATABASE Extract features for images in the given folder and store them in the database. .br .TP .B ... .br .SH ONLINE DOCUMENTATION The program is documented at https://colmap.github.io/ colmap-3.9.1/doc/conf.py000077500000000000000000000203741454702036400150610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # COLMAP documentation build configuration file, created by # sphinx-quickstart on Wed Jan 28 09:31:25 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.mathjax", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = u"COLMAP" copyright = u"2023, Johannes L. Schoenberger" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "3.9.1" # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "COLMAPdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'a4paper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( "index", "COLMAP.tex", u"COLMAP Documentation", u"Johannes L. Schoenberger", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. latex_show_pagerefs = True # If true, show URL addresses after external links. latex_show_urls = "footnote" # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. latex_domain_indices = False # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "colmap", u"COLMAP Documentation", [u"Johannes L. Schoenberger"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "COLMAP", u"COLMAP Documentation", u"Johannes L. Schoenberger", "COLMAP", "Structure-from-Motion and Multi-View Stereo.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False colmap-3.9.1/doc/contribution.rst000066400000000000000000000016731454702036400170310ustar00rootroot00000000000000Contribution ============ Contributions (bug reports, bug fixes, improvements, etc.) are very welcome and should be submitted in the form of new issues and/or pull requests on GitHub. Please, adhere to the Google coding style guide:: https://google.github.io/styleguide/cppguide.html by using the provided ".clang-format" file. Document functions, methods, classes, etc. with inline documentation strings describing the API, using the following format:: // Short description. // // Longer description with a few sentences and multiple lines. // // @param parameter1 Description for parameter 1. // @param parameter2 Description for parameter 2. // // @return Description of optional return value. Add unit tests for all newly added code and make sure that algorithmic "improvements" generalize and actually improve the results of the pipeline on a variety of datasets. colmap-3.9.1/doc/database.rst000077500000000000000000000113261454702036400160550ustar00rootroot00000000000000.. _database-format: Database Format =============== COLMAP stores all extracted information in a single SQLite database file. The database can be accessed with the database management toolkit in the COLMAP GUI, the provided C++ database API (see ``src/colmap/scene/database.h``), or with a scripting language of your choice (see ``scripts/python/database.py``). The database contains the following tables: - cameras - images - keypoints - descriptors - matches - two_view_geometries To initialize an empty SQLite database file with the required schema, you can either create a new project in the GUI or execute `src/colmap/exe/database_create.cc`. Cameras and Images ------------------ The relation between cameras and images is 1-to-N. This has important implications for Structure-from-Motion, since one camera shares the same intrinsic parameters (focal length, principal point, distortion, etc.), while every image has separate extrinsic parameters (orientation and location). The intrinsic parameters of cameras are stored as contiguous binary blobs in `float64`, ordered as specified in ``src/colmap/camera/models.h``. COLMAP only uses cameras that are referenced by images, all other cameras are ignored. The ``name`` column in the images table is the unique relative path in the image folder. As such, the database file and image folder can be moved to different locations, as long as the relative folder structure is preserved. When manually inserting images and cameras into the database, make sure that all identifiers are positive and non-zero, i.e. ``image_id > 0`` and ``camera_id > 0``. Keypoints and Descriptors ------------------------- The detected keypoints are stored as row-major `float32` binary blobs, where the first two columns are the X and Y locations in the image, respectively. COLMAP uses the convention that the upper left image corner has coordinate `(0, 0)` and the center of the upper left most pixel has coordinate `(0.5, 0.5)`. If the keypoints have 4 columns, then the feature geometry is a similarity and the third column is the scale and the fourth column the orientation of the feature (according to SIFT conventions). If the keypoints have 6 columns, then the feature geometry is an affinity and the last 4 columns encode its affine shape (see ``src/feature/types.h`` for details). The extracted descriptors are stored as row-major `uint8` binary blobs, where each row describes the feature appearance of the corresponding entry in the keypoints table. Note that COLMAP only supports 128-D descriptors for now, i.e. the `cols` column must be 128. In both tables, the `rows` table specifies the number of detected features per image, while `rows=0` means that an image has no features. For feature matching and geometric verification, every image must have a corresponding keypoints and descriptors entry. Note that only vocabulary tree matching with fast spatial verification requires meaningful values for the local feature geometry, i.e., only X and Y must be provided and the other keypoint columns can be set to zero. The rest of the reconstruction pipeline only uses the keypoint locations. Matches ------- Feature matching stores its output in the `matches` table and geometric verification in the `two_view_geometries` table. COLMAP only uses the data in `two_view_geometries` for reconstruction. Every entry in the two tables stores the feature matches between two unique images, where the `pair_id` is the row-major, linear index in the upper-triangular match matrix, generated as follows:: def image_ids_to_pair_id(image_id1, image_id2): if image_id1 > image_id2: return 2147483647 * image_id2 + image_id1 else: return 2147483647 * image_id1 + image_id2 and image identifiers can be uniquely determined from the `pair_id` as:: def pair_id_to_image_ids(pair_id): image_id2 = pair_id % 2147483647 image_id1 = (pair_id - image_id2) / 2147483647 return image_id1, image_id2 The `pair_id` enables efficient database queries, as the matches tables may contain several hundred millions of entries. This scheme limits the maximum number of images in a database to 2147483647 (maximum value of signed 32-bit integers), i.e. `image_id` must be smaller than 2147483647. The binary blobs in the matches tables are row-major `uint32` matrices, where the left column are zero-based indices into the features of `image_id1` and the second column into the features of `image_id2`. The column `cols` must be 2 and the `rows` column specifies the number of feature matches. The F, E, H blobs in the `two_view_geometries` table are stored as 3x3 matrices in row-major `float64` format. The meaning of the `config` values are documented in the `src/estimators/two_view_geometry.h` source file. colmap-3.9.1/doc/datasets.rst000066400000000000000000000021461454702036400161160ustar00rootroot00000000000000.. _datasets: Datasets ======== A number of different datasets are available for download at: https://demuc.de/colmap/datasets/ - **Gerrard Hall**: 100 high-resolution images of the "Gerrard" hall at UNC Chapel Hill, which is the building right next to the "South" building. The images are taken with the same camera but different focus using a wide-angle lens. - **Graham Hall**: 1273 high-resolution images of the interior and exterior of "Graham" memorial hall at UNC Chapel Hill. The images are taken with the same camera but different focus using a wide-angle lens. - **Person Hall**: 330 high-resolution images of the "Person" hall at UNC Chapel Hill. The images are taken with the same camera using a wide-angle lens. - **South Building**: 128 images of the "South" building at UNC Chapel Hill. The images are taken with the same camera, kindly provided by Christopher Zach. A number of sample reconstructions produced by COLMAP can be viewed here: **Sparse reconstructions**: - https://youtu.be/PmXqdfBQxfQ - https://youtu.be/DIv1aGKqSIk **Dense reconstructions**: - https://youtu.be/11awtGWSqQU colmap-3.9.1/doc/faq.rst000066400000000000000000000663701454702036400150660ustar00rootroot00000000000000Frequently Asked Questions ========================== Adjusting the options for different reconstruction scenarios and output quality ------------------------------------------------------------------------------- COLMAP provides many options that can be tuned for different reconstruction scenarios and to trade off accuracy and completeness versus efficiency. The default options are set to for medium to high quality reconstruction of unstructured input data. There are several presets for different scenarios and quality levels, which can be set in the GUI as ``Extras > Set options for ...``. To use these presets from the command-line, you can save the current set of options as ``File > Save project`` after choosing the presets. The resulting project file can be opened with a text editor to view the different options. Alternatively, you can generate the project file also from the command-line by running ``colmap project_generator``. Extending COLMAP ---------------- If you need to simply analyze the produced sparse or dense reconstructions from COLMAP, you can load the sparse models in Python and Matlab using the provided scripts in ``scripts/python`` and ``scripts/matlab``. If you want to write a C/C++ executable that builds on top of COLMAP, there are two possible approaches. First, the COLMAP headers and library are installed to the ``CMAKE_INSTALL_PREFIX`` by default. Compiling against COLMAP as a library is described :ref:`here `. Alternatively, you can start from the ``src/tools/example.cc`` code template and implement the desired functionality directly as a new binary within COLMAP. .. _faq-share-intrinsics: Share intrinsics ---------------- COLMAP supports shared intrinsics for arbitrary groups of images and camera models. Images share the same intrinsics, if they refer to the same camera, as specified by the `camera_id` property in the database. You can add new cameras and set shared intrinsics in the database management tool. Please, refer to :ref:`Database Management ` for more information. .. _faq-fix-intrinsics: Fix intrinsics -------------- By default, COLMAP tries to refine the intrinsic camera parameters (except principal point) automatically during the reconstruction. Usually, if there are enough images in the dataset and you share the intrinsics between multiple images, the estimated intrinsic camera parameters in SfM should be better than parameters manually obtained with a calibration pattern. However, sometimes COLMAP's self-calibration routine might converge in degenerate parameters, especially in case of the more complex camera models with many distortion parameters. If you know the calibration parameters a priori, you can fix different parameter groups during the reconstruction. Choose ``Reconstruction > Reconstruction options > Bundle Adj. > refine_*`` and check which parameter group to refine or to keep constant. Even if you keep the parameters constant during the reconstruction, you can refine the parameters in a final global bundle adjustment by setting ``Reconstruction > Bundle adj. options > refine_*`` and then running ``Reconstruction > Bundle adjustment``. Principal point refinement -------------------------- By default, COLMAP keeps the principal point constant during the reconstruction, as principal point estimation is an ill-posed problem in general. Once all images are reconstructed, the problem is most often constrained enough that you can try to refine the principal point in global bundle adjustment, especially when sharing intrinsic parameters between multiple images. Please, refer to :ref:`Fix intrinsics ` for more information. Increase number of matches / sparse 3D points --------------------------------------------- To increase the number of matches, you should use the more discriminative DSP-SIFT features instead of plain SIFT and also estimate the affine feature shape using the options: ``--SiftExtraction.estimate_affine_shape=true`` and ``--SiftExtraction.domain_size_pooling=true``. In addition, you should enable guided feature matching using: ``--SiftMatching.guided_matching=true``. By default, COLMAP ignores two-view feature tracks in triangulation, resulting in fewer 3D points than possible. Triangulation of two-view tracks can in rare cases improve the stability of sparse image collections by providing additional constraints in bundle adjustment. To also triangulate two-view tracks, unselect the option ``Reconstruction > Reconstruction options > Triangulation > ignore_two_view_tracks``. If your images are taken from far distance with respect to the scene, you can try to reduce the minimum triangulation angle. Reconstruct sparse/dense model from known camera poses ------------------------------------------------------ If the camera poses are known and you want to reconstruct a sparse or dense model of the scene, you must first manually construct a sparse model by creating a ``cameras.txt``, ``points3D.txt``, and ``images.txt`` under a new folder:: +── path/to/manually/created/sparse/model │   +── cameras.txt │   +── images.txt │   +── points3D.txt The ``points3D.txt`` file should be empty while every other line in the ``images.txt`` should also be empty, since the sparse features are computed, as described below. You can refer to :ref:`this article ` for more information about the structure of a sparse model. Example of images.txt:: 1 0.695104 0.718385 -0.024566 0.012285 -0.046895 0.005253 -0.199664 1 image0001.png # Make sure every other line is left empty 2 0.696445 0.717090 -0.023185 0.014441 -0.041213 0.001928 -0.134851 2 image0002.png 3 0.697457 0.715925 -0.025383 0.018967 -0.054056 0.008579 -0.378221 1 image0003.png 4 0.698777 0.714625 -0.023996 0.021129 -0.048184 0.004529 -0.313427 2 image0004.png Each image above must have the same ``image_id`` (first column) as in the database (next step). This database can be inspected either in the GUI (under ``Database management > Processing``), or, one can create a reconstruction with colmap and later export it as text in order to see the images.txt file it creates. To reconstruct a sparse map, you first have to recompute features from the images of the known camera poses as follows:: colmap feature_extractor \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images If your known camera intrinsics have large distortion coefficients, you should now manually copy the parameters from your ``cameras.txt`` to the database, such that the matcher can leverage the intrinsics. Modifying the database is possible in many ways, but an easy option is to use the provided ``scripts/python/database.py`` script. Otherwise, you can skip this step and simply continue as follows:: colmap exhaustive_matcher \ # or alternatively any other matcher --database_path $PROJECT_PATH/database.db colmap point_triangulator \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images --input_path path/to/manually/created/sparse/model \ --output_path path/to/triangulated/sparse/model Note that the sparse reconstruction step is not necessary in order to compute a dense model from known camera poses. Assuming you computed a sparse model from the known camera poses, you can compute a dense model as follows:: colmap image_undistorter \ --image_path $PROJECT_PATH/images \ --input_path path/to/triangulated/sparse/model \ --output_path path/to/dense/workspace colmap patch_match_stereo \ --workspace_path path/to/dense/workspace colmap stereo_fusion \ --workspace_path path/to/dense/workspace \ --output_path path/to/dense/workspace/fused.ply Alternatively, you can also produce a dense model without a sparse model as:: colmap image_undistorter \ --image_path $PROJECT_PATH/images \ --input_path path/to/manually/created/sparse/model \ --output_path path/to/dense/workspace Since the sparse point cloud is used to automatically select neighboring images during the dense stereo stage, you have to manually specify the source images, as described :ref:`here `. The dense stereo stage now also requires a manual specification of the depth range:: colmap patch_match_stereo \ --workspace_path path/to/dense/workspace \ --PatchMatchStereo.depth_min $MIN_DEPTH \ --PatchMatchStereo.depth_max $MAX_DEPTH colmap stereo_fusion \ --workspace_path path/to/dense/workspace \ --output_path path/to/dense/workspace/fused.ply .. _faq-merge-models: Merge disconnected models ------------------------- Sometimes COLMAP fails to reconstruct all images into the same model and hence produces multiple sub-models. If those sub-models have common registered images, they can be merged into a single model as post-processing step:: colmap model_merger \ --input_path1 /path/to/sub-model1 \ --input_path2 /path/to/sub-model2 \ --output_path /path/to/merged-model To improve the quality of the alignment between the two sub-models, it is recommended to run another global bundle adjustment after the merge:: colmap bundle_adjuster \ --input_path /path/to/merged-model \ --output_path /path/to/refined-merged-model Geo-registration ---------------- Geo-registration of models is possible by providing the 3D locations for the camera centers of a subset or all registered images. The 3D similarity transformation between the reconstructed model and the target coordinate frame of the geo-registration is determined from these correspondences. The geo-registered 3D coordinates can either be extracted from the database (tvec_prior field) or from a user specified text file. For text-files, the geo-registered 3D coordinates of the camera centers for images must be specified with the following format:: image_name1.jpg X1 Y1 Z1 image_name2.jpg X2 Y2 Z2 image_name3.jpg X3 Y3 Z3 ... The coordinates can be either GPS-based (lat/lon/alt) or cartesian-based (x/y/z). In case of GPS coordinates, a conversion will be performed to turn those into cartesian coordinates. The conversion can be done from GPS to ECEF (Earth-Centered-Earth-Fixed) or to ENU (East-North-Up) coordinates. If ENU coordinates are used, the first image GPS coordinates will define the origin of the ENU frame. It is also possible to use ECEF coordinates for alignment and then rotate the aligned reconstruction into the ENU plane. Note that at least 3 images must be specified to estimate a 3D similarity transformation. Then, the model can be geo-registered using:: colmap model_aligner \ --input_path /path/to/model \ --output_path /path/to/geo-registered-model \ --ref_images_path /path/to/text-file (or --database_path /path/to/database.db) \ --ref_is_gps 1 \ --alignment_type ecef \ --robust_alignment 1 \ --robust_alignment_max_error 3.0 (where 3.0 is the error threshold to be used in RANSAC) By default, the robust_alignment flag is set to 1. If this flag is set, a 3D similarity transformation will be estimated with a RANSAC estimator to be robust to potential outliers in the data. In such case, it is required to provide the error threshold to be used in the RANSAC estimator. Manhattan world alignment ------------------------- COLMAP has functionality to align the coordinate axes of a reconstruction using a Manhattan world assumption, i.e. COLMAP can automatically determine the gravity axis and the major horizontal axis of the Manhattan world through vanishing point detection in the images. Please, refer to the ``model_orientation_aligner`` for more details. Mask image regions ------------------ COLMAP supports masking of keypoints during feature extraction two different ways: 1. Passing ``mask_path`` to a folder with image masks. For a given image, the corresponding mask must have the same sub-path below this root as the image has below ``image_path``. The filename must be equal, aside from the added extension ``.png``. For example, for an image ``image_path/abc/012.jpg``, the mask would be ``mask_path/abc/012.jpg.png``. 2. Passing ``camera_mask_path`` to a single mask image. This single mask is applied to all images. In both cases no features will be extracted in regions, where the mask image is black (pixel intensity value 0 in grayscale). Register/localize new images into an existing reconstruction ------------------------------------------------------------ If you have an existing reconstruction of images and want to register/localize new images within this reconstruction, you can follow these steps:: colmap feature_extractor \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images \ --image_list_path /path/to/image-list.txt colmap vocab_tree_matcher \ --database_path $PROJECT_PATH/database.db \ --VocabTreeMatching.vocab_tree_path /path/to/vocab-tree.bin \ --VocabTreeMatching.match_list_path /path/to/image-list.txt colmap image_registrator \ --database_path $PROJECT_PATH/database.db \ --input_path /path/to/existing-model \ --output_path /path/to/model-with-new-images colmap bundle_adjuster \ --input_path /path/to/model-with-new-images \ --output_path /path/to/model-with-new-images Note that this first extracts features for the new images, then matches them to the existing images in the database, and finally registers them into the model. The image list text file contains a list of images to extract and match, specified as one image file name per line. The bundle adjustment is optional. If you need a more accurate image registration with triangulation, then you should restart or continue the reconstruction process rather than just registering the images to the model. Instead of running the ``image_registrator``, you should run the ``mapper`` to continue the reconstruction process from the existing model:: colmap mapper \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images \ --input_path /path/to/existing-model \ --output_path /path/to/model-with-new-images Or, alternatively, you can start the reconstruction from scratch:: colmap mapper \ --database_path $PROJECT_PATH/database.db \ --image_path $PROJECT_PATH/images \ --output_path /path/to/model-with-new-images Note that dense reconstruction must be re-run from scratch after running the ``mapper`` or the ``bundle_adjuster``, as the coordinate frame of the model can change during these steps. Available functionality without GPU/CUDA ---------------------------------------- If you do not have a CUDA-enabled GPU but some other GPU, you can use all COLMAP functionality except the dense reconstruction part. However, you can use external dense reconstruction software as an alternative, as described in the :ref:`Tutorial `. If you have a GPU with low compute power or you want to execute COLMAP on a machine without an attached display and without CUDA support, you can run all steps on the CPU by specifying the appropriate options (e.g., ``--SiftExtraction.use_gpu=false`` for the feature extraction step). But note that this might result in a significant slow-down of the reconstruction pipeline. Please, also note that feature extraction on the CPU can consume excessive RAM for large images in the default settings, which might require manually reducing the maximum image size using ``--SiftExtraction.max_image_size`` and/or setting ``--SiftExtraction.first_octave 0`` or by manually limiting the number of threads using ``--SiftExtraction.num_threads``. Multi-GPU support in feature extraction/matching ------------------------------------------------ You can run feature extraction/matching on multiple GPUs by specifying multiple indices for CUDA-enabled GPUs, e.g., ``--SiftExtraction.gpu_index=0,1,2,3`` and ``--SiftMatching.gpu_index=0,1,2,3`` runs the feature extraction/matching on 4 GPUs in parallel. Note that you can only run one thread per GPU and this typically also gives the best performance. By default, COLMAP runs one feature extraction/matching thread per CUDA-enabled GPU and this usually gives the best performance as compared to running multiple threads on the same GPU. Feature matching fails due to illegal memory access --------------------------------------------------- If you encounter the following error message:: MultiplyDescriptor: an illegal memory access was encountered or the following: ERROR: Feature matching failed. This probably caused by insufficient GPU memory. Consider reducing the maximum number of features. during feature matching, your GPU runs out of memory. Try decreasing the option ``--SiftMatching.max_num_matches`` until the error disappears. Note that this might lead to inferior feature matching results, since the lower-scale input features will be clamped in order to fit them into GPU memory. Alternatively, you could change to CPU-based feature matching, but this can become very slow, or better you buy a GPU with more memory. The maximum required GPU memory can be approximately estimated using the following formula: ``4 * num_matches * num_matches + 4 * num_matches * 256``. For example, if you set ``--SiftMatching.max_num_matches 10000``, the maximum required GPU memory will be around 400MB, which are only allocated if one of your images actually has that many features. Trading off completeness and accuracy in dense reconstruction ------------------------------------------------------------- If the dense point cloud contains too many outliers and too much noise, try to increase the value of option ``--StereoFusion.min_num_pixels``. If the reconstructed dense surface mesh model using Poisson reconstruction contains no surface or there are too many outlier surfaces, you should reduce the value of option ``--PoissonMeshing.trim`` to decrease the surface are and vice versa to increase it. Also consider to try the reduce the outliers or increase the completeness in the fusion stage, as described above. If the reconstructed dense surface mesh model using Delaunay reconstruction contains too noisy or incomplete surfaces, you should increase the ``--DenaunayMeshing.quality_regularization`` parameter to obtain a smoother surface. If the resolution of the mesh is too coarse, you should reduce the ``--DelaunayMeshing.max_proj_dist`` option to a lower value. Improving dense reconstruction results for weakly textured surfaces ------------------------------------------------------------------- For scenes with weakly textured surfaces it can help to have a high resolution of the input images (``--PatchMatchStereo.max_image_size``) and a large patch window radius (``--PatchMatchStereo.window_radius``). You may also want to reduce the filtering threshold for the photometric consistency cost (``--PatchMatchStereo.filter_min_ncc``). Surface mesh reconstruction --------------------------- COLMAP supports two types of surface reconstruction algorithms. Poisson surface reconstruction [kazhdan2013]_ and graph-cut based surface extraction from a Delaunay triangulation. Poisson surface reconstruction typically requires an almost outlier-free input point cloud and it often produces bad surfaces in the presence of outliers or large holes in the input data. The Delaunay triangulation based meshing algorithm is more robust to outliers and in general more scalable to large datasets than the Poisson algorithm, but it usually produces less smooth surfaces. Furthermore, the Delaunay based meshing can be applied to sparse and dense reconstruction results. To increase the smoothness of the surface as a post-processing step, you could use Laplacian smoothing, as e.g. implemented in Meshlab. Note that the two algorithms can also be combined by first running the Delaunay meshing to robustly filter outliers from the sparse or dense point cloud and then, in the second step, performing Poisson surface reconstruction to obtain a smooth surface. Speedup dense reconstruction ---------------------------- The dense reconstruction can be speeded up in multiple ways: - Put more GPUs in your system as the dense reconstruction can make use of multiple GPUs during the stereo reconstruction step. Put more RAM into your system and increase the ``--PatchMatchStereo.cache_size``, ``--StereoFusion.cache_size`` to the largest possible value in order to speed up the dense fusion step. - Do not perform geometric dense stereo reconstruction ``--PatchMatchStereo.geom_consistency false``. Make sure to also enable ``--PatchMatchStereo.filter true`` in this case. - Reduce the ``--PatchMatchStereo.max_image_size``, ``--StereoFusion.max_image_size`` values to perform dense reconstruction on a maximum image resolution. - Reduce the number of source images per reference image to be considered, as described :ref:`here `. - Increase the patch windows step ``--PatchMatchStereo.window_step`` to 2. - Reduce the patch window radius ``--PatchMatchStereo.window_radius``. - Reduce the number of patch match iterations ``--PatchMatchStereo.num_iterations``. - Reduce the number of sampled views ``--PatchMatchStereo.num_samples``. - To speedup the dense stereo and fusion step for very large reconstructions, you can use CMVS to partition your scene into multiple clusters and to prune redundant images, as described :ref:`here `. Note that apart from upgrading your hardware, the proposed changes might degrade the quality of the dense reconstruction results. When canceling the stereo reconstruction process and restarting it later, the previous progress is not lost and any already processed views will be skipped. .. _faq-dense-memory: Reduce memory usage during dense reconstruction ----------------------------------------------- If you run out of GPU memory during patch match stereo, you can either reduce the maximum image size by setting the option ``--PatchMatchStereo.max_image_size`` or reduce the number of source images in the ``stereo/patch-match.cfg`` file from e.g. ``__auto__, 30`` to ``__auto__, 10``. Note that enabling the ``geom_consistency`` option increases the required GPU memory. If you run out of CPU memory during stereo or fusion, you can reduce the ``--PatchMatchStereo.cache_size`` or ``--StereoFusion.cache_size`` specified in gigabytes or you can reduce ``--PatchMatchStereo.max_image_size`` or ``--StereoFusion.max_image_size``. Note that a too low value might lead to very slow processing and heavy load on the hard disk. For large-scale reconstructions of several thousands of images, you should consider splitting your sparse reconstruction into more manageable clusters of images using e.g. CMVS [furukawa10]_. In addition, CMVS allows to prune redundant images observing the same scene elements. Note that, for this use case, COLMAP's dense reconstruction pipeline also supports the PMVS/CMVS folder structure when executed from the command-line. Please, refer to the workspace folder for example shell scripts. Note that the example shell scripts for PMVS/CMVS are only generated, if the output type is set to PMVS. Since CMVS produces highly overlapping clusters, it is recommended to increase the default value of 100 images per cluster to as high as possible according to your available system resources and speed requirements. To change the number of images using CMVS, you must modify the shell scripts accordingly. For example, ``cmvs pmvs/ 500`` to limit each cluster to 500 images. If you want to use CMVS to prune redundant images but not to cluster the scene, you can simply set this number to a very large value. .. _faq-dense-manual-source: Manual specification of source images during dense reconstruction ----------------------------------------------------------------- You can change the number of source images in the ``stereo/patch-match.cfg`` file from e.g. ``__auto__, 30`` to ``__auto__, 10``. This selects the images with the most visual overlap automatically as source images. You can also use all other images as source images, by specifying ``__all__``. Alternatively, you can manually specify images with their name, for example:: image1.jpg image2.jpg, image3.jpg image2.jpg image1.jpg, image3.jpg image3.jpg image1.jpg, image2.jpg Here, ``image2.jpg`` and ``image3.jpg`` are used as source images for ``image1.jpg``, etc. Multi-GPU support in dense reconstruction ----------------------------------------- You can run dense reconstruction on multiple GPUs by specifying multiple indices for CUDA-enabled GPUs, e.g., ``--PatchMatchStereo.gpu_index=0,1,2,3`` runs the dense reconstruction on 4 GPUs in parallel. You can also run multiple dense reconstruction threads on the same GPU by specifying the same GPU index twice, e.g., ``--PatchMatchStereo.gpu_index=0,0,1,1,2,3``. By default, COLMAP runs one dense reconstruction thread per CUDA-enabled GPU. .. _faq-dense-timeout: Fix GPU freezes and timeouts during dense reconstruction -------------------------------------------------------- The stereo reconstruction pipeline runs on the GPU using CUDA and puts the GPU under heavy load. You might experience a display freeze or even a program crash during the reconstruction. As a solution to this problem, you could use a secondary GPU in your system, that is not connected to your display by setting the GPU indices explicitly (usually index 0 corresponds to the card that the display is attached to). Alternatively, you can increase the GPU timeouts of your system, as detailed in the following. By default, the Windows operating system detects response problems from the GPU, and recovers to a functional desktop by resetting the card and aborting the stereo reconstruction process. The solution is to increase the so-called "Timeout Detection & Recovery" (TDR) delay to a larger value. Please, refer to the `NVIDIA Nsight documentation `_ or to the `Microsoft documentation `_ on how to increase the delay time under Windows. You can increase the delay using the following Windows Registry entries:: [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers] "TdrLevel"=dword:00000001 "TdrDelay"=dword:00000120 To set the registry entries, execute the following commands using administrator privileges (e.g., in ``cmd.exe`` or ``powershell.exe``):: reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers /v TdrLevel /t REG_DWORD /d 00000001 reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers /v TdrDelay /t REG_DWORD /d 00000120 and restart your machine afterwards to make the changes effective. The X window system under Linux/Unix has a similar feature and detects response problems of the GPU. The easiest solution to avoid timeout problems under the X window system is to shut it down and run the stereo reconstruction from the command-line. Under Ubuntu, you could first stop X using:: sudo service lightdm stop And then run the dense reconstruction code from the command-line:: colmap patch_match_stereo ... Finally, you can restart your desktop environment with the following command:: sudo service lightdm start If the dense reconstruction still crashes after these changes, the reason is probably insufficient GPU memory, as discussed in a separate item in this list. colmap-3.9.1/doc/format.rst000066400000000000000000000225051454702036400155770ustar00rootroot00000000000000.. _output-format: Output Format ============= ================== Binary File Format ================== Note that all binary data is stored using little endian byte ordering. All x86 processors are little endian and thus no special care has to be taken when reading COLMAP binary data on most platforms. ======================= Indices and Identifiers ======================= Any variable name ending with ``*_idx`` should be considered as an ordered, contiguous zero-based index. In general, any variable name ending with ``*_id`` should be considered as an unordered, non-contiguous identifier. For example, the unique identifiers of cameras (`CAMERA_ID`), images (`IMAGE_ID`), and 3D points (`POINT3D_ID`) are unordered and are most likely not contiguous. This also means that the maximum `POINT3D_ID` does not necessarily correspond to the number 3D points, since some `POINT3D_ID`'s are missing due to filtering during the reconstruction, etc. ===================== Sparse Reconstruction ===================== By default, COLMAP uses a binary file format (machine-readable, fast) for storing sparse models. In addition, COLMAP provides the option to store the sparse models as text files (human-readable, slow). In both cases, the information is split into three files for the information about `cameras`, `images`, and `points`. Any directory containing those three files constitutes a sparse model. The binary files have the file extension `.bin` and the text files the file extension `.txt`. Note that when loading a model from a directory which contains both binary and text files, COLMAP prefers the binary format. To export the currently selected model in the GUI, choose ``File > Export model``. To export all reconstructed models in the current dataset, choose ``File > Export all``. The selected folder then contains the three files, and for convenience, the current project configuration for importing the model to COLMAP. To import the exported models, e.g., for visualization or to resume the reconstruction, choose ``File > Import model`` and select the folder containing the `cameras`, `images`, and `points3D` files. To convert between the binary and text format in the GUI, you can load the model using ``File > Import model`` and then export the model in the desired output format using ``File > Export model`` (binary) or ``File > Export model as text`` (text). In addition, you can export sparse models to other formats, such as VisualSfM's NVM, Bundler files, PLY, VRML, etc., using ``File > Export as...``. To convert between various formats from the CLI, use the ``model_converter`` executable. There are two source files to conveniently read the sparse reconstructions using Python (``scripts/python/read_write_model.py`` supporting binary and text) and Matlab (``scripts/matlab/read_model.m`` supporting text). ----------- Text Format ----------- COLMAP exports the following three text files for every reconstructed model: `cameras.txt`, `images.txt`, and `points3D.txt`. Comments start with a leading "#" character and are ignored. The first comment lines briefly describe the format of the text files, as described in more detailed on this page. cameras.txt ----------- This file contains the intrinsic parameters of all reconstructed cameras in the dataset using one line per camera, e.g.:: # Camera list with one line of data per camera: # CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[] # Number of cameras: 3 1 SIMPLE_PINHOLE 3072 2304 2559.81 1536 1152 2 PINHOLE 3072 2304 2560.56 2560.56 1536 1152 3 SIMPLE_RADIAL 3072 2304 2559.69 1536 1152 -0.0218531 Here, the dataset contains 3 cameras based using different distortion models with the same sensor dimensions (width: 3072, height: 2304). The length of parameters is variable and depends on the camera model. For the first camera, there are 3 parameters with a single focal length of 2559.81 pixels and a principal point at pixel location `(1536, 1152)`. The intrinsic parameters of a camera can be shared by multiple images, which refer to cameras using the unique identifier `CAMERA_ID`. images.txt ---------- This file contains the pose and keypoints of all reconstructed images in the dataset using two lines per image, e.g.:: # Image list with two lines of data per image: # IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME # POINTS2D[] as (X, Y, POINT3D_ID) # Number of images: 2, mean observations per image: 2 1 0.851773 0.0165051 0.503764 -0.142941 -0.737434 1.02973 3.74354 1 P1180141.JPG 2362.39 248.498 58396 1784.7 268.254 59027 1784.7 268.254 -1 2 0.851773 0.0165051 0.503764 -0.142941 -0.737434 1.02973 3.74354 1 P1180142.JPG 1190.83 663.957 23056 1258.77 640.354 59070 Here, the first two lines define the information of the first image, and so on. The reconstructed pose of an image is specified as the projection from world to the camera coordinate system of an image using a quaternion `(QW, QX, QY, QZ)` and a translation vector `(TX, TY, TZ)`. The quaternion is defined using the Hamilton convention, which is, for example, also used by the Eigen library. The coordinates of the projection/camera center are given by ``-R^t * T``, where ``R^t`` is the inverse/transpose of the 3x3 rotation matrix composed from the quaternion and ``T`` is the translation vector. The local camera coordinate system of an image is defined in a way that the X axis points to the right, the Y axis to the bottom, and the Z axis to the front as seen from the image. Both images in the example above use the same camera model and share intrinsics (`CAMERA_ID = 1`). The image name is relative to the selected base image folder of the project. The first image has 3 keypoints and the second image has 2 keypoints, while the location of the keypoints is specified in pixel coordinates. Both images observe 2 3D points and note that the last keypoint of the first image does not observe a 3D point in the reconstruction as the 3D point identifier is -1. points3D.txt ------------ This file contains the information of all reconstructed 3D points in the dataset using one line per point, e.g.:: # 3D point list with one line of data per point: # POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX) # Number of points: 3, mean track length: 3.3334 63390 1.67241 0.292931 0.609726 115 121 122 1.33927 16 6542 15 7345 6 6714 14 7227 63376 2.01848 0.108877 -0.0260841 102 209 250 1.73449 16 6519 15 7322 14 7212 8 3991 63371 1.71102 0.28566 0.53475 245 251 249 0.612829 118 4140 117 4473 Here, there are three reconstructed 3D points, where `POINT2D_IDX` defines the zero-based index of the keypoint in the `images.txt` file. The error is given in pixels of reprojection error and is only updated after global bundle adjustment. ==================== Dense Reconstruction ==================== COLMAP uses the following workspace folder structure:: +── images │   +── image1.jpg │   +── image2.jpg │   +── ... +── sparse │   +── cameras.txt │   +── images.txt │   +── points3D.txt +── stereo │   +── consistency_graphs │   │   +── image1.jpg.photometric.bin │   │   +── image2.jpg.photometric.bin │   │   +── ... │   +── depth_maps │   │   +── image1.jpg.photometric.bin │   │   +── image2.jpg.photometric.bin │   │   +── ... │   +── normal_maps │   │   +── image1.jpg.photometric.bin │   │   +── image2.jpg.photometric.bin │   │   +── ... │   +── patch-match.cfg │   +── fusion.cfg +── fused.ply +── meshed-poisson.ply +── meshed-delaunay.ply +── run-colmap-geometric.sh +── run-colmap-photometric.sh Here, the `images` folder contains the undistorted images, the `sparse` folder contains the sparse reconstruction with undistorted cameras, the `stereo` folder contains the stereo reconstruction results, `point-cloud.ply` and `mesh.ply` are the results of the fusion and meshing procedure, and `run-colmap-geometric.sh` and `run-colmap-photometric.sh` contain example command-line usage to perform the dense reconstruction. --------------------- Depth and Normal Maps --------------------- The depth maps are stored as mixed text and binary files. The text header defines the dimensions of the image in the format ``with&height&channels&`` followed by row-major `float32` binary data. For depth maps ``channels=1`` and for normal maps ``channels=3``. The depth and normal maps can be conveniently read with Python using the functions in ``scripts/python/read_dense.py`` and with Matlab using the functions in ``scripts/matlab/read_depth_map.m`` and ``scripts/matlab/read_normal_map.m``. ------------------ Consistency Graphs ------------------ The consistency graph defines, for all pixels in an image, the source images a pixel is consistent with. The graph is stored as a mixed text and binary file, while the text part is equivalent to the depth and normal maps and the binary part is a continuous list of `int32` values in the format ``...``. Here, ``(row, col)`` defines the location of the pixel in the image followed by a list of ``N`` image indices. The indices are specified w.r.t. the ordering in the ``images.txt`` file. colmap-3.9.1/doc/gui.rst000066400000000000000000000060071454702036400150720ustar00rootroot00000000000000.. _gui: Graphical User Interface ======================== The graphical user interface of COLMAP provides access to most of the available functionality and visualizes the reconstruction process in "real-time". To start the GUI, you can run the pre-built packages (Windows: `COLMAP.bat`, Mac: `COLMAP.app`), execute ``colmap gui`` if you installed COLMAP or execute ``./src/colmap/exe/colmap gui`` from the CMake build folder. The GUI application requires an attached display with at least OpenGL 3.2 support. Registered images are visualized in red and reconstructed points in their average point color extracted from the images. The viewer can also visualize dense point clouds produced from Multi-View Stereo. Model Viewer Controls --------------------- - **Rotate model**: Left-click and drag. - **Shift model**: Right-click or -click (-click) and drag. - **Zoom model**: Scroll. - **Change point size**: -scroll (-scroll). - **Change camera size**: -scroll. - **Adjust clipping plane**: -scroll. - **Select point**: Double-left-click point (change point size if too small). The green lines visualize the projections into the images that see the point. The opening window shows the projected locations of the point in all images. - **Select camera**: Double-left-click camera (change camera size if too small). The purple lines visualize images that see at least one common point with the selected image. The opening window shows a few statistics of the image. - **Reset view**: To reset all viewing settings, choose ``Render > Reset view``. Render Options -------------- The model viewer allows you to render the model with different settings, projections, colormaps, etc. Please, choose ``Render > Render options``. Create Screenshots ------------------ To create screenshots of the current viewpoint (without coordinate axes), choose ``Extras > Grab image`` and save the image in the format of your choice. Create Screencast ----------------- To create a video screen capture of the reconstructed model, choose ``Extras > Grab movie``. This dialog allows you to set individual control viewpoints by choosing ``Add``. COLMAP generates a fixed number of frames per second between each control viewpoint by smoothly interpolating the linear trajectory, and to interpolate the configured point and the camera sizes at the time of clicking ``Add``. To change the number of frames between two viewpoints or to reorder individual viewpoints, modify the time of the viewpoint by double-clicking the respective cell in the table. Note that the video capture requires to set the perspective projection model in the render options. You can review the trajectory in the viewer, which is rendered in light blue. Choose ``Assemble movie``, if you are done creating the trajectory. The output directory then contains the individual frames of the video capture, which can be assembled to a movie using `FFMPEG `_ with the following command:: ffmpeg -i frame%06d.png -r 30 -vf scale=1680:1050 movie.mp4 colmap-3.9.1/doc/images/000077500000000000000000000000001454702036400150165ustar00rootroot00000000000000colmap-3.9.1/doc/images/dense.png000066400000000000000000034071551454702036400166410ustar00rootroot00000000000000PNG  IHDR#jgAMA a cHRMz&u0`:pQ< pHYs""*YiTXtXML:com.adobe.xmp 1 L'Y@IDATxWu7wP@PPDQjShG/zxx?Ŀ}vնF7Ւ%Cxr9'?ɖ"KUB9;| Z\|@@ \ rhIhD@ @@ @@ eE`>["ˋsѪe'ߵ+HmmcVﵬ<cآƻkhkW+wʒ;xRӮ%b#i7oȲٜE UY̌6gs&i"}hdsZ,Ocx>(nbӹј%i΋plƓ?΍s̊u]΍Yl8hcKsK&hb]ڽdt.-HY۷rbW-OXg0Ngӱe Br mY/ZqKS֘.it:J1o ιwhycGm4Y2pl#pXzƴo 9rf6m>y}3ʌG! ҳD2iiʎ،r)g22 6aVkϐ: 4Ĺ)+ 69}Glk1+ smll X?sۮEvֆ,dlڣ{-OΎ]\٠7Gڪl4؈6E_ԯ\>Msk5Z<>٩ Cmsyjժ5ջw`е/13+S|^bNN,qҬcO*S ;1e&|&ݵ3|b0Z4f7Fq|2漈S>;sdjz}'3vU+.s@X5A4ƌ֑&F>J9|ډXٲno`J6؈s}>1,k;1ssO쯾c;<9L:ae;ni2,_(xZRL3d#sZK^:kl8,<㜛X$7fpgN4?ދӎ(mךԏy\iXjIouZm2Sk"ΚOShԣ@EFsY$i<\gk.@w¯@@ @@ )" ;M_x@@ |"t[%E6ӵٮZpr@)R[ׂC^VK;+Hr 15dmnHk׃T-V!P5*63 =YR4 A q_"WQ!x,IQ(,e2lC>C2$FG"\?"7EF!'>j,jh|HY3!ҙ=rCʗWv_1uX']^eO 9Ne9?ҟYQDDJ=z qGm ,օj\i)qS"Ez'!rA0&cr6:gJ;z.d1 o\VCC{;[bH%Wq9;ER{-CA HEfJKQľ_C12+vẕ'`ץ QJ{ꖭol{mnv8?0:nF 15}<4jˎ%W 9QùC>D#"\!x"sRs4{?sD{8s^։YE  ÈB*U tՎR1!Рp NSIfk8Lsʔ?I)4s)qg2Omk{H߽aZٺ(xY.!48pn.sg?vymKm2xj9r|P3/@"HqкP{SC9H) )D4zKf؄r}cɔN52$&"g\ cC"g՟`G47y1"U@@  nr`Gs1U;?9BJ]NaA=8LD3G)NCD&yL[Tn`a$ռƒpP(nc0frJNrޘz4RRy8F|kmqYaڕB@8{៷P|y;:FlU=Dߒs rj'=@"iPP+džYQ\;Kk}*`15]xo[UiY[D` m':26/-@DuQ'ŵSǩ 'zC{ - 9bͳY祮 Kz!սBA)D2?Q9ppPDpwޗ39yo#7Y8݇! @@ <d額caGGWg;"qE>BHˆQ7;dl֪3ځlBAIaS8kRy",!jbqv յEaaPt_ GꇜҩmVRhu:8iy9> }Z`EȽ}tu>HpU_B2 Գdkqo>.TDlr~+4"d#q`eDI8C9o7-u==+yBM;OB8\rU4uJ]HV{ۀx߾rŎAC>W57f39Ka5\LtxWpL4Wi<\eƆe˱DĬb"#w{Ǝ@ʡC 9/U{["D|y<ؤsfΉ9N"ITϵsg͌*nAw4z["UBG!nͷ>7= 0DO$&SN3xznKY׸YT_ҁKuPTi͛#R!4'ܣi(uz9szD+҄ھX(Uo9ׁ@whï@@ @@ &[< 0d$&+ѫazOH}29!;/Z/\u_|^3ǻ2,Wz@@ <Zʴ{PZ:ۍв6DY |"{CHkϞ Lvp樐(v~Qظ!&rHVr% 2Wnn%*yﲛږBK"Dl)~K{rC*SgP`+nGe/Eߕ$]• =kVO}:Q*ϙ*w(Ҁ)&-" h^#֍By"=abJdaVI`F4?> G֡-b=Mn T|M+ i޻:gGu/e"Ck{헌ٟMr\PQ4v L; `c D`l`sZ ȁFu 3}#ksUZJ)E п0_a@@  9CNBsqKWT u]Φ_%d!HUCIPBDoC%+Qn˂x)C h=CU+*8')x!'z &G{FN:vε []28^tYN4mKh.+z&2?|'S 'Ehbmx9T :> "%A{rFмcs fyڬk]69% ~D+A&7R@@ @@ [" Ń,271Be ٌ>>'DF]w;g*A. u~aeO6z`:\2z]5PĹ4GB6w|Ӎ ,d)liމh3 y^~@@yC`/J*2 2\SD!eu!=:9xl0.<l r< #wV?"~ RŹ]($ud(zB]%96ZVxw){gF#B&0Ӝ)9NygRyW 9S}"ef> Zw"MH$חs$m[1\v|zFFαy? }̊?Cu_=> >9F4k}3?Fu EA{g~g/w'㓎g}oSosgI(!E^i?R[>I{ͬzi=TۉV_X2rd៤:>1u8iH>g\tz5-x2mEۅZuacՕ G! c"x(9 |}pW9{E<4]Ը)2*tUDh19©ˎL$2zK>}H N!TEV4F%-D:8y Qxꓵ5Ǯ@Fh)"C弇 ERYeꑿbyIى@? fldb]Wq~&Q%/MS Un0 ՝q&tU*+PMC"O."TJa HAu ܁ Tv!QCD B/Q*RFsp cd-H2iV@#TcoصW^G6u^cݳMB5/RtTRܕ\a8LM~VRqU-m^4Ug2x0,+~Ϛ PÇ)-_65/NW 58fn/,Qqzf He}Wogp+UˮWd8 X(謕]fWf>XWQpep(2w6WvFtd9̨.ؿʮ@޻g†"ذQs}mK ݽqo8e!!rt#;e+YF&_z08u(9DYK?z͝kW_@:3=5e]+c0p˄@1U `Y!Ç8#0^1 CNoIamh|!GU: zB&c]]{aWvQoz2N1E7..)K/\ju}d}k4Wɯ` 5>YNWluz5h 'v@.~RDt9 M &w@ x.\?hF@@ VX XZ cm?챥crg35꧘\>Wi[y۶BNTPlzE 'x>[.-e)B} =u*6%!!R%nCPeOsU'47 _*UwH ݝ[Aؠvq0]B$y֎p_*Cg~xbޠDI rg}|x A*U-οPWC,*aU*%m%ԦC@'j^Vuť>^JP‰wQ_k$s"-/T-@Ŭ|r(\)mw꼞toBC=KD&Q 5j<@yYr$'! *(tzpun>uyO gK#e:\9q) +sʮnLPnYRNޯA& c)B0 ;9= *Z+붺7XkpquOVskKeRyt.e0xH5/Qf<"D6FV:C+W ˽JꋣsKr>ҷַ+҇C08`Kv+ vpx*[^]C$J*(_ho;,[:g!ʅY{9 C^`@|{BܿcUwC+xa- J8sŗ_~{ J\ Ϟ[;ށ$׶{e6w_@|n]rˑDys镻7ligux燶3}slejkV?ﶱ{5-|ǧUJp4^4DG`S2% azzƲ) C{]kB@my61P2*xawΎ? pywiej@@ ߀nKU H}BèCqAWLjl v' 23BH7ȥ$PCI2^ƤHh飇=ǔ uF.A> ^:kX*[f0Ӿ!XhXC^5G &IC*|]!@@ |q`V=l.F8ŊVex T95s|  '[RRcYpVaUN'b\ ,̩P^uۨP!&!*vնc+:tqpfϦ1[ڶ ۽qRaڢ~R*ooRv:!7~EI 8Dw Bkpqʩ]ݽmCbCO[R#2߼IN6|ᕫ,Y{NHv>F8a3Ex(g'mˢ\nB[}dDŽ>'NwZoޣjbϏ0Ur0*9̓s!1;Ng2 `6IcT>v@AҔ!M УM+:}0Yfxl[WTz5c(3)r==?<<{{5 aAg˶ i6qY9qtȪ8gܽm_![>GzpCBCܦ F7LJ'v}iƣ{kk[6d1(ԑ]h} Ƕcز8* /qb>*gZ0=:dpuQbZ pxFԴ!!ND< UTjq?ʾI(1!s9'z9[ #)ͭ1,dn",R)9LPpHANG8o@w{msg[͍! l{NF`oS+_?yC{w?It|[C,lWZjm0ޝx'Ƶ.c >,'Q>" )u'&\h{7yGȐS룾3&:F//\C@ @@ "6{*DcC6Fڈhb.-O$F6vksbՙY\-z(K Z$(v"l)\4FػT 'd{wzA^U>%dH6N ]*W((n4/qɎE?= <@@ |xz/$D){R> ?M?7=QAOO|j|(J;zsoUnY#6dkq=Ӈ}SH縭FAAzi rEHOZǐtjDx*rw!yIz${kC-olc3{ǯvB'rCB% $@u 0̫yBG6^aM6Wʋ-8[sNRTO:kʉ@fK SD@ +`_60"}ɧ#{6xUژ#ʡ-[q0Ah* uQ-B`=6CDvNJPJw1N]ZF~qQS"$@w D55^߃o]r?E-$Jc]1蓬>Z1o `xɀCިwQ_=d}2@5&w 'yc $ĶhsɽX?`_*; rӓЎŰPo3PߗQ:*gGZr8Q'R1׸&V߼ϯRW@ <Ј@@ gSٍEWv,I&%c 𤝝}n*`?>'ƔO˖nR3Ξ?/ !(匏gq-l燇V-GZVޮ\'\jxv" v߲ pM%Cϲ'?R/j @@ y#9ST1Dģ#kۮJu] Cxl%K){!Q{CV**D,"ĜIj"OuyLKϾDL JpK!8_BR@ETBo':ix2ϙBo_q V'ܳJļ6=tx<5uhpJek_Bi;#Uk0I {Jh \TR@{;T:nCxL ҽcs6vw֭ÚJgхo"b@Da\1a! (_'|f<~ҴrEX[_;#W֦(icht/G=]+C2FzKW1减i!c?_&Z #BzX=VZŹ8()%>j;|0KӶ%H(5:>'3r<)8|/ޖF~6]|<޷S d 9@.远Q(ufuBk`Ii"y@:(M{Ox>6"V8vU۾qC0(6!#:l!p'*q迲˸@U؋7(A1Z͜)IΟ%sFM7# S><@@ )DܽȦ}htB=@=pcפ1MT7Zs~Hq Eu"_ NiH 9ĥULGZFh]mx@r.5wD3$Y ]9u~Em22Q\G (U 蟋LCM ?-0 !ܞ%ɇE_FU|hnٛ7-Ʊ?>':ژcB>#om~I{oQG[!=rSkǧ'r1:$NKЖ(S;Fz(RUŔ'[ğ"omAS)!SSW*cx(*u a )iDFАZۜ z'*Wx.ss:90Ard'Ve_B|rUu4Czė ݫ(֖!QSrDT]D[krzWE1@gʱ]C6 O/i wWi{QrZAy􅥚;Dq1 (1{"eՎ(X.1`Sx%~>G'95#3!8cDX["y&Gܘ2DS $=QZ Ey?KSPcr-(?zs01@s"Qxx=+9ϼp;Lוm)j\SQDBCj'!5+_6s1"2ﰎ66^@ A瘏Z* =DO`1|s"<] Ƙ%q"ȧκ@P_@@@`|%" ~v#+ JX!/V!,,Z»kNDT/e꼶nbP`>W3yG#^RhgD/wѻO#pa7˾Y7ȆV:WB!sC"nqȆ}G"܏}XO@@ Y!wDb xh#|RyZ"DLy"dCO""r'_ 5IgJ| QÁ5qJM BKUne% V|Qr=qIu+WHB/rxg*W($$%Դ@IDAT ڜsF̐CN$9Ex+0bFRX)CxmE'E$fΕm'8j/QiC7^T}|BFαSDJqcǔ`B=k阇Weϋ i>B&U_!s:)G9N'ESE-Z㨐 s>k$ѿ9Qm,JDcUIp3g 2u(/PAwf¯@@ | /MݐG@ 2ƱܰHw1" oل#̡޶5Gs6 x}bc]ޗGzflLĹޒ}g?^:_|rc)W>c?oAgbH+zʉ>g%ݶ{?[B!ذ9-Ez({8C66pO>Hp~d:uF7@@ L_LɠTn r.2ef-aqԆʅ: =cs_g@@!{`G=JԮ"~E)td-Ya'D` EZ@ D`JUIzSsYuL0"e^tsD]BeK-~0VmSZRR*NJ(LA)qu@d|\[C> HeE!ה#Y}+TrzKE?iGO*gǵ"S")IPt8?2zj|&Ji2w2Tf}|J;"=9H,v VAr1YI}oǂRs*GXG_?kT=0ٽ#7j(ZbI:k;װPW=c{<9sŒePԧŗ~[zջ8Pf^^"D:f#:sQa D\iV\؝;wl͝5B_$-fs/oڃ{vyZmU3V$}#m0#ڄ I穞P}>]z-9Lp2֘Ρ#,sT^Y" |k]{x_VAsO6t݄%X+S{DQ? SH*C>K>I@x]s8Y(Ig k~ԇ?g^Wp _ty@@ dMbKPCPDșaC&ZŠ~4)H_/W($ڜ=y':s VV1(X 6S=T率&zM<9Rp6oT tխ(L6#ڬRɋ2yCsY쳉VE7)f JБ4B@@#`m ?kP"=@GECwv՛7w!Ͼƀn>=sER`V$` C =Fy 1>_ݺw亹錔qXϮNU OvL2\qRJ9.UH`DE">OnȮBUD3#PDcEq&[ickJ. 3!S-հSV$l&ẗȳI -k1:=\eLdݲCrӷ"+E[&N(QJ빯i(BVzCNRjP:G6̀:P}֌:̯DCT"imggjKK(y۶QJSl ?|w8llۍk o/<]=}kllKV^~/}=yIt#oP1PK*r9oȆN75@s)إo3)A5L׊%ezCS܆ӚT@e-z\wx[eqA~:?|g9Ux{8UrkQ! |6!gPc` KsDng'!['9u26loѓ>W1=V+le9,+vm] "/B}xLn/ݾ Q_?;!B# @@ >7e:i./rl`ʺbG]6"lÛ͝~:q‡MU9/㟷1*Dlsʗɫ_ 9EvOD{b7lLA##V"i>Cv+:2)TEԍd *mhGnsX~žicX2UgDT|DT:s7S~J/"?N"<Jf$,HV9"ujl*]++lE/M[DKO1nڼSFϹeSa>@ ҫwŻ4=Kr.e߷wՊm. ^{K:8A֛Q (}7HiF4Hneliދ@~/< ˘sMY+S/7JTjCٷ^w^E߳{NZɖ/S{[+ps³=@^P8 ϯ44{i2Q斝-lZ7^kns7"̿vfr*%19Z3ZZ"['L=vctڕC'$5/nay b"h̘}^uLp('PhwUjc6)OP2y(C14(30rT(S=#"H1f^A6B;8|BY6L'_V`p?آR'ZB9%/jF{=n4{D@@ XWB[XIrx4O'y߉HG@ )(>4*T+vQu߇(ױBz=:&7pE++6+F #*}֫ǺclĪ~ݰ"5sߝ kCd_}? Y*ҸIxt]VaU/=4:8yycVsC&4;<=l*h[}ܤW}CcpLc3Oo~ž؝׈fF{,g-K̚۰/!!2!D6"22x}'1E!nXaഹ$8L6?'2bCƶvУb8vK^&&."87U'NM$MqMD@21N\!lߢDfPsKV!1d2f-ާ="\O )T 8?"3ժf/\# Oݷ^1d%!3,UFVGmthMtW|Ws ר׭bCq=Lr2U_ SPȗ{h\zZC+U>"˕D"5e.[F"ȤX"8ݗ Dr.HQb"m]>0ꐜDԍ E>10Ԧ kg!R(g+D!.Ԧr^WVn!͛lv.MO3uڸZWzNF"eXDJe ^oH Vy{D։ V[tn7穖F8 Pm1j3yq!鿜Er k9N6"w)˯P>dɩ$mdBGm/ޡ+77$c'V`})[owUW8W].R{6I4<t\j+х7}Jbϊ4!/`))nBkHg`ܖ,@Uﶡ#υgz}]?u{|s[Y.NOtJ7wn76}}[1Cv^ڠo/:ˡQk>,xT5wcQоWt ).*kBe0cyucZơ8<!y-SAnr3gs(K| \9mFB*v0y0@ad1:rCUT6g2y bm kڤ`sy}Pa0g|Aj mnȠUy.{2f6L02Z|!1tgwˠ_x(lhS_֚pr o'ѩ_דz'<@@ $+FH{{:ZG{UO=DT<Ц1[l~⡝O=vuŎ [WnbI8\# (n"?n"s+=1N¬=x(*FݸC@Ql(\5F QRCm!ؕ"W֝Q}!"{1y*}}OzZʺWOr^rKĢއӷ(m}vA#-\NZsm(Ɖ F9]a-;te1 j0c1לJe* W 7蝢i~cT~:wMLH꧷p\[$&ssWv]=;.xCCivPٹbU EZqX*sV\Z*xTGV} PpvqavWnж~dbͪ+CH3f/rVl6UT7gM<ٝ<ݲX-\ yeOp]`.5t>]w4&)/ڟ񿰛d5YKWw[ok[wnaϭūCXY!=s\v{Vm2~8s+zEb, )>M9W ɈݚiZrg!ZoR_r8\]4/|M{ih_Hi8BHv=a柊\ӼTrHuGNs)ԵW E}+ (qdGեzBXc|UrQza# 'œy0`?`6Iߑ眅ʀ)U>ވ(/^:;\iʧqA+$ĉa)WO[ ǀ2#fѧCSSeДe C+g?h{c7wqW5nΒso (WN9G0Y p|dP]\p @}OoR^[g ڹ '{w-@nr  TRz d@駵._*+L9 <=euͣ؃3~s'Y{ @8vi1,fJr[n$b- 1!CnLm\-HV(- 3Xl*1.=+&VM-_6ǐ%\I!&',%xL0UMd9\[IVFf5ՅLu db +tph b//sJrQd۴WfӋEg^C,_@,qH~I]1]i j*_@ּ(muj2IP6R|aa"%/Ϧ~?D:gtVm@J.-}qx@y-,,23Z||g糿*I',=Q&.),Z>O } Q Ȝ^y!G~IzƕtVWϕ7>~6?fͭW!'X> PmsN80׭2ʵ[[q @1=b^m䴎M#HǣZϼ<9G,0L(K(8߉-g {|B[ǝ?es̜P.#桻iC'wCNlL8mH9x`hsThme^X|or'i3y0m;G<%s|*}z%›5iA4)d9A7EoUYX/S 3A6ީ!Fd[ X{Pkܗ7'̶4[!mK|dЯxYf˲,:J %:>Q! ڬ~] '?ź] {$6MX޿f7Qaa PZK{+3 /)b;-!nJkE朱oknI29$-s>Fڌ1 t2v4x%q1qLrEl9N0tCR]k$SofFk'fsZe$1qLEe'8_.&_[ @nr |,%d~F k X.]GqPQYhPhg=T+&ʷb#ְ*;>gkRlXy YqO\KBF +밎u%͠X7+,!Z|S:ZZ,f`% r% l@>qAAo\+Yv4U#33zu>Z5>9$A=66/ΦNLw܍URE gm @ w u5ItaRf{8->yTTZ ;m% e p<`V1Q&л 6t:E$ŇP(gx޾w/^}d{Mg'PwV:9\s[N͸W i 9m(6AO"AfD{`{O|+$}h1un 6 ›Y5A45(G6Z Wcm0<&FDQW>EclC  =BiN|x7 P10!n$Lϒ} ~8d^nyߣ⃶1Dr $9lb <&  z{Iz*1 Ac'7CN^EK1:&lB~^" <3y7[h|Տ;yA 8}?$%}OkS^+-7JhJ5Gh >oYFh'knZ*uDP NM->}iB^`_QvuQԒmSLN Ema#w&F3+\tɞV*KƹR=OAee(yym3v?mc rFKE3@7Hn ,'-q/q}8spVU:b:Ȱ)x"XOOfX am@P.wZɹpy dcj: }8Ԃcv$>:؅FT8B0t/pg˽瓒wbe~/rkn/r ~gS_jkt0>>ڢߝtDvgӵ "Y\!{՟ulijvsNl7s:̷_:m~pH`P\bڵlUrL:]La6$xD\%˼B&ʛd+2gۄ65#>Yϟ@Bz*Fh9:_%Qe YΨl I5!O b i K`Ų|hWPjoo/?8kt9UjWċ7T=>!ͬVxIR?A~B։Ȅ*{~qb^T7 'j X[?Bؕ=9gds<(22n>tbs--?S2`ޏoD ɹϸ \#]OOBZɡ 1Gɠ0~?! ƚvS57XAR] \јۂ7o |1U/,kĥ).b3];}B&HKm`8zx׶#{m>xkd MLϦ52j8޾}?}5{A iT?:<<8'*ܾ}/p%׆1?9l2!vHdv-|Oi/>^9˟;pXU;g̲^Jจ9&F7`b<2^$!/i+K%*TDA "0v܋6mKU.qGUQ4QmJoEb@{?3CySCC%;%Zf:ۈb{ h,s&9RoU/PyS(c[w_{8#mi~Y-v f4xm56b$t+o$H0S,xkH_"h].&;j3svX]3qTh ~<|YBԬcm3, y"<6ȏ-EYe}f.ͧ|xaꙂh"lUkږbDV%1k&D##b:"j5 WߝlzU׀bYsCilsK1xOX$FtmA2g؋1>7n~F&t[@^Й-h>4G SI&hOhsv "qJ7=TZ,7LV d%Ova |9Je J'-%ӃBzϝ3)@]B6PC=4al5Xz9X#{(pYgnհϓݴ~ }/ȧ6^kvvvhKki3 ksӶ*OP{N;?-@n//Ikf-[ @n+PK=Yd 1 $o%\dJe:6md d}OGM"w Nܽ.>9-8y$%CWFu~F!xdҮ*u.8z}׌[(<[Y1I&dNg1e"$u_ThRG 4|^ pgt~d@6i [\9ե^ wlTx_lAd&m&{ {\(!m*8SV i+gY PHG^NLrnlc|{,t`?P6ݝ}\/!_'wy:5wQ3a:)Ldє1J8+2aMRϵ]N؂<or -[;i QyՅٹt:Q:V } SgCumdx)w9or <lƨa-'W{ےvb&G5X #l'DKSJ8;s<>Sn1ׄc.qoh,؟>gm N=hXM0JE"~9F >3%b 15U:%S9u=~u0 .Hh eͼm4رn><[pzo쳒}`+3`MIbqs$gg|>#ll{^_y g"Ûsg*xWfx&J|x;23]HNkkirn&Uwn6U 1h`s{;>x9ݴ09* b doaCZe1xoPf+g$x}m(Qd/(@,Wz*:0sxV*2YMҹ|:josedt$q?oPz>t;H{{$2P:lmJ Ot^%;$"br[ep'*{B]υei?̸.hA|IpvY{ 9Mnެ/b{zyx=ϱ/_5}:(H[D^%>o{d/#IAIv2̛dL 8o!tPRSzWz|;d2 mSv7C<FKۦ5̓ZSMS8,VVQcSX%H $6+Dm{gMjHA# ponV DyO{)-1q+n2x5d͹Yb kvtamجdߗ6[~IXwwKNwXk(ؠr>?է 6V`YŬ>35/70fW.0؋{m8 fyؐHzJ{fj#۔R5 {ݶx)t; >]z۳`14y?גp 5U@8B^t&'Vd6K;̱f\<࠰mʉnڤ |6^u?.'D3grv8q_d1˖\ZnYq<ܲ6 ]C: M>lNNկ~LdR'OCgۻ~q>mpW>x=a9<[cog1 9:?\}f yhM%@*ǜS_rrr߽{77G}={P܎Ą>j̡shm@=C`"CU`p{hc6{}k6vRf_ hD{vR %s\^LoryJ;q'W1"gtwQ#ԁ09c45``|lk}&_ĞoV$/O sioIe{,,v6yvτcK̏%Bay̔h66蹏ľYA~ goAf,iq9N\KL0 68pژ%>&j;>58g|BIgN:`Vӭqð ,HO-QLJ^"Nv8<$fs2}4Px xr1̰?!ArUB,hk_L3Π+; W9*>HtsV];9$#H_♝?nʜ۶kCkc7wo st|nZ&}+pf ^Lk`>o9 {nl?l3ҹ622%dxQm0l7k,r\s޿Q;Gfq+#Q6#E_K zQ%">̼*1m0;)4CO·%v*g.N cꁶ7 N&{;x_yq޾8#KyLZ!1(9|8;4w-[ Kb[ @nu-f0 V0I눘O+6 Jc Lok fB:^p4f6+~ u3W։bZoisDYnQ6tґA3vA YLGQt 7V"dj?.L!#ff.Rt ¼NtQs9j^BrAJ >J&Vtc*)Ax~n:=z8]Z\H/edG M?G:~rzr)HK >؏~_t 4nܸfd__^#~>n]#`Dt%uE}"k8._YL[SZP_i48x5FluZHGvp]~Etl\ Ņ)@V*? ái`6HwȂW%3]&/sMQJ{ G $vb6K\l6 N~bv ḱCO-Tq)! ffN<(wLUfoq/-[ @nWU:tڵ4*k{nGZkC ?UH7:zk23,]Tx|7K-u,qs|iC_$%sKӋq#J1D3dA-krR'[*b)}m`4Te% kN6VB/碄us&Mmoᮇ]ƃ!!%ZFLۓc'Q* ~SX|4%C-"|hlnAҖt ?OHC.=t=dJBb /9 Y Hg{cO ־ 868WSV7AQ. 4H`lbWۃ49dS*DI@~0TW|?m͝׮>o"8qdj#;~af&'| 9y b: /Iˇ0!o8[s Ɀ ^:nYD;9)IVB!='CCr] ^im^ -6UK8?Ivݥ2w m}U8I0V*ɑtKȀ>]_qKHݯs 1xg>Ƹ/j}>{2 JFw CKto5Ri'8"eې8L#< 9 gmU0It_ϸY lT8m.!^oBY" SxLg"hΛ+Q 2 6:_)t% ͨuDH*6Q\<ڭh:p9ܻ2ʮI+.>5Wo9 \R?c@@H Qʦ׫izz"Wá3` ܠ)v:$afۆ!/9gK6ցJ'itl$"N?=)-B߹G=v#Vħd_H u\"Pν;ick12!?hp-^'x3~]ix`(=y4FmGG8ƬZc!xftt^3bkk3.-Ef[8zq$㥳lhhX\\}MpصNح c2^{b<#g$QA7QdNh90܏ّr -[%@I5i|_.?I?~7]X(׿H0˕HМ}?d=͛- V"C:0\@r|u =6Yqp,pOٰ>>e0g@{k!#n`exifutH#~9=77v?$pr96#Ĉtk I$s#R8:DAYܬrۦC1}Uj/^Z!3~n> !u Zy,^Cd?x0t&lUбs!GSÞp, iM@zcM-y2kH0 შ'?W {E2@0^Z^YJi vkKc? C-;tb6  6p;e?jyv$߸7dl ݸ:}Gwːc`o$'ƆC!.kUap<1¹y/+UG>T%-%F G%؟x/Ey ?Wu (xo2ڨ(K{G[)^\26 pRTgN|%|>eW=Wc&k:>ksKŸcGڌ}[ B 1X'SB_+ePA_>o1Ir -[ @nr hG"j9󆔖_eΨ>r015ՂjfLp;oo{-p,x~ANlf%!iw g53Bi@wd"UFz qTJYolIn `gOgg7I+Ƞ_*G['A<{=o@+J>~'Y8KN‰a"A}8b #84gfpޭ|>` 2:8u;ntbgf[:h{ [="9 KݧLPG^$>@qD]pnꯛ,6Ci^6=M9kkB>}rƍd#3ixq:W㇏3iii9_qcdtH[uޱ>vAp:tuRY>A]㎰sT άcRp(jL'eO0ǵ(#*aLǶѠ㣧nM_0#yA'^ 7c)?m߶u~}w,)nϸC]_ew:JUvccR*$m5bD] Lz`caxbxYo'wįdR %1WxT`1Y h cWCw\N6TY5.O)A[%*(:`_3ו77[mPrdm^8ߩn7m3@UeE#TXG@5dϟI]GHX 1he'O8ۤҗc1$i37{JUƔ%-'bȌ]=};/ku,"m}46w"-:ԋj@K7Zi|X[LGk@Md}fl^q{7 }!ÇbѶ-dm،Z{;Þލ ~p(~~6Gy@k6f8%I^Äzp,]%b!{7s^{^1$3| .6O#P{JaV8;~F~}JՅ4O]B+ B{VS2'G``-ăfh P߾Z:A ;(½xҝ2(VwGlV*1%H 0&(n7eǁ}]P'_2@.UI0?sruMlт>!;ۜ;s( A@\{9cyưJiὴJ}qxIqQ;.R?׷_U"ܧ` |cڤOkR#Ld*cpEHkSrJL9\] 8GJӼ-NfqeRn $7nJmgHnok$l3keM 0 Tq]jY>  Gx!MdH3d9}mj"]Hn1~YjML4UKb{'ej`F2^C=lY ]ksYӾvSx}Cy#`">cO lyc_Ȝ^ ?9;ψ11r%gP44cX9eMOӓSiv zqj8~82GƆ)7G\o lN/1~*Yow`Dro (BK/A(* 6T~$d7m{ j^ho >ƎPV;DAl)g}$ׂ~APp3zoyx]h,- ,wS9n EP1yfl ݼ9E)jZz*a\KoLDr>o-@?׏4YCh!F2M8 CRοyT-[ @nr x,p5$Zb pй"z %IНD"Wڀ0ж]q/f?c1GAG+hԴC`4`ㄈ_?#hJ*b >/qXg:]nvq-. cֈY.9\rF$KZjcgm/B1lé "76:@#uǩ=>6=xfPo` 5wOAIlf#CQHtt`}mYsSN^2g~6;]{p@z?d:$+݃Pn5s>@V8@ .lb'!&5j~p2t1A=Q@=AA3T}!!ϵW3@[cY{H8K+sKBی~,;;O?㫴0Υp{ 7®nt t8HG3h)'OpX,>5'XKS{-ᕀAvBc;gaE6@zN)rd_M}zKt(Ƀ{i>3>kߌq$ͼC7pɇI ZaOFn]zt 9 捎 ?Qdz{39;}c7_9kusk#>kq@e!FaxuGo޲糀~GŜԱ;G}ј>xq=Ζy_'&Ryo'u]MXf> rt܀k@'$d߾Y%ou 7 dGYJ귣.seA,0kxv'8Gr5Yw XrK֜az_BV ~ =׀\bˬ"ckb4S]mBB'V9!+TBB1MeǓF8R"M, .~kk?cFv9:\_:jAwhٷY dH- ^82 %*lZkG%OO ŻS2o%6#Ex lenlG6qFi*x- jo%8. 8=!KS݀>Tc+N;NO!lQz335O $b!PzQH:0cYNm) Y.>F|4ķ;b~ So0r~8=4;'dz'ޣ1ϳfkkqa0S2̝7_L|p7- u!-G@\q~%k\? !ɠ&$֟F-]@` ,Opz?v' ȣ a#E߅lt[Ck[{!ȋ5Z@R>#<>Q9ssn;'NSL~x{s}@ 4??;gR*dsĽKR$ 83}50hKW2TAV[id)J.4i }HYdJz`K3'r -[ @n^Z RGpwBJ,r%u~12%3#Vnͭ42:A4+ =og?| ΢?ۤ#Zf03.P(6/|ʪ)M/>اEVDUâ BA+ I&A?E=%iek@UfY/Isg/dL$uCm8;P"Go$Zܼq>=]`,,mQA[zLx+v#/C␌ &3MyS.-k"WTh@F_֓nF"mG~i 1*d5je}t3m[?QgNԋ!`ͱYF=#8W P m2KZb\'49tJöz&ʷ0{]r܉aq8%SnuYY~Od?Zq1|pS<{MӼTfh.l+C]@YuW{G?In]M߾}b}tP5]t2!g udSq,8ôF\4"t|:19ϳ?;$\{7v}fӹtFfRmn2IwS5GwzG;Ix&}Sm{qVȯ[yzMb{ R _* OB01x ~|=}^ȭ6c8q46D6|~jF -'.q܊i|fEx0~Vl?I[Ry&ÿm(RӦDR Uaʙ`q讆AAxλn"7xn+1%, PVARw9o,l$ Q ͈#JmXSt%!U%I3vqyz #ˇGFrٵ2 O^6 ֖mr~84<ŵjhu,qt bL|}W ms*pQV:u촃q_ 8/;kU:h۬x&8|i V $^1j@QzJ^> N)յ p@|6F% Rc;`}N%#w;Oĵ@DϰS9ΐ>§cPk܏ǀ 9xnN\S=v 2kc^11621nb#TM5ĩ [vg)Uq.&糵%vmo\~|v>XׯͦO8@lKY֍{^E Dc<- =`NG%ψl`+͠`==6 P`\+  /m,Qs[T٭K0 f;7> tlor -[ @n/bYbٚKG­]$m*O.nFu:1h?V[]_M[釯o%9# Mfq{s 840ڶhO=.%gn٭HfN~LwEQ!-4XJ v]>i.kڧv k38 =iJoAbh~ǏRN'қ]KW./upG h%!S6#닋AnY~qa4O}7Y0cEw'OFҁ;K9V"_'$λ}Hcll R#1G넙.1od6 );|{H,29{l*7g=`T':tzʱ1]A?״.^oFV- k;f0ҪA `? pPb>O!}N,>?qqgHodL]!l,OR{ ެKC&}`h8=@?cԭAZ!akO*uq /1d/-0<V=.]ZL|I|iHw%@ɽw+W/,m52ݧ҃ jB`&(6 Ameqk`tJj3ZUKsP=Xjy(dy_0"S~jÑ 4* :,#}: [t"fgg7i/b?:$ے?oIu`s7m*^Ϸߤ>%=>((  kejBC~rry}wy@'O5%&SuHs?1 {(2C\ybޯnon 30v 9+dkoMc `QfߑE[uHuw WN[YKߵ@}G` {YDB1s]Tvm~|iwvތ&xEJS5Զ̸ ZKۑ>wY~ T4H %J{G&:VS3g? ZLs8^8=&:te|w6AA>|N_N:1YRO2/dn}E_|ƹX~o.Ifw*W3siآH`KVq9M B@IDATYi; n+n증13+/DlkWQ2˞ƎQ˞@-ƍ*b>љ`P`e 1˧-0Ա2r1 Spxmhv*gT^G>Y0Ft ۴~t+~FVxm|πgm]KGbxVx }^DBcfV817m:J[*OL@7Ȩ]> Kd1+v?$7thiXk:6O򙫲jv>;(W ߏj.xguG!z#n|`/#p}q$B٢$ٟ-ܱ^=p%}{-o.s郃t9jڇA<1Z 5r -[ @nr tTx5 4p ^LY }*h.:i\ 8 5C9ߦne$~? *Xˍ8˽:W>k<hx+[6CA;Ay Cv d;e֑2_PN !%Kǀ$V b>!9GJxK$XO>J 2|VlpMXHduS[Ea6`[8s%1Rd!_亂oi1d\_@sWY5]gYӕpflomZ"f4_^DSf\g1+369H9cX'ݶH&` P)?K9l} ^Сr G$8 `V:όqc@c o9?S>MM1LKҕKڕK?$m'MC?y"IxB:L,̟Q8Oӛ3T ҏj|PHOR}C2%#RusR]1ajsAu+k|BA5ZWf֌i7_@Q5[f ?NLO v萙AGa IP!!穵Թ5b:n>+|$s+ͳcy+mgJ>A +ȽsCdj_RgGߞB_ӱ<Ξ8xz2'5  [/Zv(slBe8 zpzd!Rي~7TOKd*n}P횟 jÔop,*W`0^qŖU\%,I?hwe>w,v #Zf~  Rcleop.syvF#c3XY`jO:J 4>%UC97`@{-xl=3,TVymVR| Ei.-[yA9ra֒\ӻt9:!}3r *v t4r -[ @n^. m0TX]~6VVKEj,Jr^:l,YnyN6!X`e ɜ_?OCzkLhw$?[1H@+;R^)3·slS`>e$ڼ~rZ-`FwaX0df}zQwf$$Ynf@Z<DtW#,#(m`:8$RሏfI["NF{Mvۦ>Nvrˀ[W 8f ' ]IK+= g!i$_zmU:DUdF|Wۈi:!,GTWBm$֭B0ҢQڃJבө#ifN]䗤HQkJܛMu(EgmH™]'%m:u ѯ%ޭ5yVy|1x)tnfC ]%|D7߇Wv|DoooS|Wҭ7CvX/}~n.}iZ\XH.~Wsz[pܼkʕPS[$0{ާwp @37|L<] C(2a`ysyZ$ جpL' ΰe::tsbj9[s\Ηc'dY~&h\>?}֢nLjI8u$G|Vo>̮xv9.`:~Sj;u/h_'k?I3;fyh1gVCKjW B_mHb($]R$CwYYFs(0Lns?83SnJ7cf.++gH YR 0#o7-}{t%{?Y³jޑ6_c;o^)QۯCh ,i7?}pIgdOݺ{Ag{;im=llhuz/N*(yZ^JLF)w[SEMO9+|WTb\]=p:e;>[caCՄa9h'5!H\@V< +X/w}Kpp:Gly [l`6T j %}KMRF-Q,/OAH-4;f},YW&Li,8,w:zؿھ>$_BV5 vm5{r6q 巕gηU={J*MU3}|txy0X{ɇi[X{mJ}6R+_߼MN3#_WJ- ifz:JTgV rka`:N4W+wֶ ;W;Rdžߣ߰gW~g%}`HXW "i䞫ȸIr6~\rq<Ɔ*/Niv :%JfT1׀)bLonz bheNxN ~nmnƻ`[fNSi4QNv|&ngqjm$x1ykӨ& vKW2k@%?A~v}Eg\tyNשX_UE eUId \Ɛ$ӒC}31}[@!*8>$; ߇4 $0낔iõ} KTs!} ORI hM+[<jΕvQ68=݈pA2]8VWA@ʵ!@q{Ur{(@=PT'끁 S˵;y0տ'{&p.BSEƨ/w8~GkttiuX&&'ҿݟQ:MI=ȃHe]Άzsq^9]q3:ql/:L{:S|o%l*U.:ZRMY۞;]&q08$ >&k[&H.j:bYK{-j%̥ptttt‡%PMF  .==Z"`_ 54H-j>{8`*~ ;2}t[{zcI'!!ɠm"kAY3=lE[1 <` ΰp0u\wdɬVc)5%ZWLyS/P1 H9W'Z"Nt0ut ]PFF` 2R6-s4 @0Ap? f0Ot$/>;mĖ=o6(έpj@)AZ96|8γJJ%{B,{k ZQTwO4c ⏌!8%b1]Cv?3c=cK/by[ ~{a9Xe ?\&⫓v ?۟"gV,F|}ٿ7EWpf<fW xUAk[tA">=3"$:nr%%G6Cp9oJ(NMmݬdAyCd.!} 6@U*{zvgFjxQ~ r~(6L[E.wAnh`R^S+{sda1ײylI|߼HO>0 NMAd @gy}7g>3#3Ӎ zAZb\l`dZKd8OfHV<{ 6 NYkaWbYs2R/ > $j}>0Fش]l%Ek=&wVh (p8;c}irM±3X| M^A@өE*%%{ gjdzfkckrm5tT/G!]kq5SbYfWC*P_6L_19gmohhocMgLϙOv熌`^ %wM5Í `Ź~)n8׭ۈ?PAq*Hdb7n(18?r\ Qdy;.! J:0T0h6s&p;s_}Q[a\9J<¸L /p`f%i Ihi,Pԣ{(<#P[RnA=PonOƤLCc?z^o?${`~ nldptttSfJ@M# [k}uZ:Rt68G' ;TmȄc:`.rql,&* N(-`LN {\2q qk]mllvj~;{= 'lbm 'S*`X[ksHd/Z Ot,oƹL]^GHf9Av a 6Oڎ&;lxDRcʿ;7[>Ln1'C GE;'%g_'\vEKqƝ˺f5!iJs`pPzAp> %s~_NϨ_HƁȸo:ەmn׭ی~k;C/(K<4Wr^`Mdb3s7; ͞!e)}W7=ǹ{&r u2GQ.hE@ib|RDwZ[2:2b=]\QgQlU@wRd tIŋ4h>J[>x&cJ433lkzyo|?ݘHȨW޺_|@"xZ^\०!ז4g\ͦgL`}dFKyXDBdjk yLpy`m$ŜV -2+0. ˙PCzX#=\? n 2 f,} Xr-C 2 Wg|$llYUnܞAN) 2p簩sF:3XsRrk±q qk7סF>#C.dcKh@uZ\W"p^!,2#i/WLKèvl٥;&G{)+u3=6v͛7a,Du2U3CϺCw+BbB | 6Xi}yN ׵ c3\*DDUš_-ɾ6d|ۛF~ HWw-@}%d 3VwS3kb)_C̬s*ꈿ?q:YЯVvb{{8=%bxqQEl%>Ԉ}]Jl qSb'2w8Roj2iR~](8E:y<5#Mtݓ u}{ثy?ρU2{G~*7 V ڍuKڣ`>4@29{Y)Jο(@#=qP~{(/z@'K$L5?}VmikNH8чi~n>ܹEf'qn]XpZKntxt /΃/ o?/ux'g0O[~Nal=cS2uRtܰѩ AFYG5ἊN*PK'tq8|K'"@Yzo8k:X|P2]GUoպr2P)G𫻦=JnVsmp(-ȖC/wINTQ;a0ۺ80_Afk?WP,x \x;=@"YČLZ"+++Q'Ѯ3`dlcWUXp:6+qe(D6["5 (OVnȲIc)暎 X٧d>Wlmz}eB=`Ӈ+.lI\XԹh{uz:Vz ceYlAgu'~.KApyE<Ӄ RG - 5dR-2Y/IɈ8;H=ȱ[\9X%Z4(!)Lo fq.KQ:~_P+fߜgc#d %ܸ9Y y =}!OXqF|faaP8xu!OW/_fu$GSd wFPE180n|OGDNtyJ{Yo޼I?&n+oboH~8mCoXfT!S z B6H(vx`D=>T)r jF!ܬ-q̇sMdf;HJ9c}r/)H1+U`YUG@ub0ǽǽS <׭^tϲB#\ z [`jd;9G0$ 劽0;(KKivvi[/! 0ɴ:n B)/N8 o{J'fu>PNOsiȫ'j5NhP*Wvf 1 $W42QR҆YX1cI?Sc"~7v7 a Lئs_UC]8$?Ht9-3N[ o !nj&lW(Hʹu&1؉˘SȫV7[@6xf\qdF}ȝ*aS0 pGJ6-ys.Qy& /gI6Jp>H1.%i{G5AȀ``6lv@llHfڟ:C`5Ry!02%3o&`z1IM ԾG/O .SϺt8[cbQ{wQ<[$Ej5}'p*īY#x^h+PD|U$_s+O3kdɵ4vg(؞CLP54,bCf3K*5Y妍1bo}J Ǽ|@+BLm.ggJ$O'ߘD\K#E\L'IkVzu:ZNh Aq_?~ӗ9%ҡ һ.::uws2CǸi_/jjG;]O] ā ƪxR=kcpȹ"VpAFu+LKߟ7jk;+"kGU~Vx>ϝNU$sOcWq/_>+WXY>CIa#e= 9BU-$!6gAW~z> GWoV_џnQ[f)-Z{UÇ!<˳=9wD&[g6ͱ,\CR~W}JvV1$`IcMY>GC ItCE% u\{V+X\1O dN-ܟ(U[ҹ+Z|u6q|vgq00 "}-K N1`&L*kK *-&%l6"uG+^7c[f Rcv Y֖\ 1sh̖qdjOcXhYj G,6t56{ɠ ?!`aVll'''*W8w-Jm,!ar|"#px3UP3䫭>1:XTxH_.o%9^ ,h'`u#.hNVGH8f7|6[Zft8f>#D CJ^ՂJrZOkLy-C^XizA)+<@èg#H8Ɯ[Q@ !gJm 7i|r@v'C+Yi||,g^M.䌡k$[7ǟ|X-s5j  otūT}@ u2 Vhtk %w]آ9{kqi~>Z*F'm@Y_+xD,a16HcM^\:%Iz2GϸKkLnӬ{>뫂r'PJ5g.x8nbsH& gׯo8zE>,Mj9mYu[_j^mt`tzgT{wLs}ڟڔMd_=i>˳G@d dF j ~5^5D8_ߍp$TKJdG* hh=5 eL6>eAPU`bEnVU3LXU^b_)5^5j.o4uư~mj`= QJFeT%1T <L؎gI+KMURUL`aQ9/$CwAC{(Ն튏=Fm6q rs J;ůrs|߱˰2yڔ|-_@QCN{DJOkz۟M?mqvB>دTr?dT[Ϳ ^z.4:Бn=WNNP/ϪO; }3bVW^CQn*E|chh Iguhwr8'ԱH @^+MW\{ta2\+ۡDXf7|FHSF=vm_#6+òT-]Brp|.yJҹ佶~!˧EP2j.% >{y-$ڳ. sKN`ԙpցփ۔.O&pkf[إY;i.ZbIt%`_s8 SX9f,5N"V}]^c6s"X CD 12Ĥg4L17/f;?en&~5$߸RW;U!=Pm1fXxb:~\бRB/>OoߩE]W(valn#S8EO' YqH{Uߐ$pB^sCl/熀q13\˯r{(@=Pr|gz@]'#f~f$_] Gogh3>XyI@dutk%m&U2p4:n/0[2<3`.YY>kFl>z+q*pk͂.88UP}Oriia9d\8ʊYo\KGEIt :0f\ ʞ{n0SJi1"Ζ]#hi{A?`<-Yƹ6\ k1TQ5ZuWYS ylAq8^@8; Zp\_ |)홛8~5Ծ^g+pS?ʷE6{%*T0ڹHo_t!KՂv>58K o"l@ !@]<2]g0yti6 ` }8CapzSw 7 ue;FL5,{h3fA1tCI w~lۛz@im4 0֨_fqtȣKhh$hp݀4Mzo% JW6]E뻎@!kk۞ R /@MdoB8(7sݻiph}3hf޼~ms!]'\¿|>|'?~,tL۟~(OG,!2@x%mOM?<-,.qdwwғOm2{!sJd1aРIUeA& ΣQ "m8Z DSEc ϔIb]mjb^4إqoK iZUK./0~M^h 1$kUByd19&XOf__#CBalT]Cku]YCA 53:)aPer"ƪG#/ ~w+*$دx77ˆ.SoI')CR DEumką!^QriA `̕e=zO=PH*=Prþ4s-P:!CtA8fbI5FzZ[Y j >2RFhmp*zu )6n*x[$J@Vc#T>EA;N_~E1QPlλcfi+ 3*a/)#z\`eaSbj\ffM3m{ mkݛ o:կS A> 6:f>88DMt0m@IDATG8CYuqkC\b7"쳒 4|U s16S!kee9dXʮI@$cvyoO`y72xsHo0%:MHw_J޺e__]|&#}h }g+`?.>r+o12_~ @_i Ejz)4K$ 3?yF֥vtkJΝi}nanG#Mߺ>GLMΑ_Nc[y?sc/R$7.IwclpPIADI:l  9w z1{=1/̋ 1٦b */5%M:7ݟ\2þ`;\~~*C҉۰ 2'ԩ<Ci$'̱Z]Z=e=ɩ{]zFW]:~/u!]F S*SW\d#( /(G0Asm$7Ș?d$@ϲL(.i:ޘWƄ&RO'όKl&WEh~^ʭTSH{#0rjxo[s'l:8-Sڤq(qU}6Knf %.Tq;?3e{e~ +k ;>w"{p U2jX =J}55d!X@kG>y0w HLewI̦[_fsI9RϮ_ FwiHLfW{M*1^vsn~)B^P&7hl+B}FJ}y|v&X_jo ~|ޑVҧ~EaOU ;^hU11k 50nsP36\4zm}Pv`wF+P"U89M1pvvhc*hT%Q5ic$ND}}oT?Ԑ.DvOXCmDD 3$mM \Wڇ͸x Wu_;?3)Kf_(@=Pr{{8lٸvt`pܾYE,+MlJ2Q%Em8YrR:L;:05u mVV=4cRuڃ]av`d38l|݇a~vac͹jD$* seK$Zڥ0e8f>}T VgYޯ~AJbARg8En;@'S6Nt 3W9XئV]& 7Rwiig[; v;6[ȳ(im rCdoLOh ֎Rm4gND;i]v16A܆^G0=5ta]X4|!TNV/rs]I0Q6̽ie0^~)mp#r|Vr<xw$6w\i.Vbe;!\iC"[q[A_ηKo@(eX!Χޡ‚dfb.$QxY@ c_ we{ %S@mH@0;ùiF׳) C pNzpgil &:@xH#N?I|L+k#R?zqg>v1~+DLǍ@؞ ѱAMUF! p>` C_X1DwKY賀ssd? 16L}v3/6@ \zޝ4qH&77wc5:|Ȓ1$=|aq/PɉʷzN9/;[woSE*B)g(LMCrW/ q {QxFnjR-N_ϦfjK6O`Ȁd>>C?rUF o)xg'Xq<_kG}|q?%oAӭÒXfUfY>K9"_/plPIY3eo 2;\s"Rc玱ռ'2sST)~g .b{t mEj3.`Mrʉ,.}( OJ<Օ}ه9)7ărPWd~[Or>ʤ4o*}U_G7*O-\Л~H$\mמT͵jB@/ 0I޻`肏uMر %$H ϯ~_о 1aμ2۟f3fucяlH 0#9O_"&B[Ÿ;q?t@-sX@׾Uޞ 7sHh_yf xx.dw`}>#k R54z)-.m[SAw;n;?zn 1nĘ)gģ2s~/BVх!. !ڬ|!r.ԙ1!KuA$lA/evV/!s>uC]e;)IVnz[6K㓓FG&,&Ex,7C 9!SJH+gv*Yꀩ##Y8]ԧ^l!"|@pTWq.=@mHnـIdYWA=@rpJu.YeN3elumL~| 6S*H0)"N'vQޝɉ3L^1W9e|k6K`C`J1N(٥?B4}uikLL~6CqgD6$WWVbun0LNuOLLO>u>KK ;x':ȸϟ}zg/w0pinvDsk&}nFj/B;<$KCG=~hznݙdZ1B˗KSUzw}'=˴~`%$Ѡ}ჟ ȐE&2OMUѱdO;P(8#;Ӆ  A `k| @-TνFt$c\D( W]f04gO͛(~4[c}mdG fvz)n!+_;1 ں xK2oEzn$e{ͪ`5>[";9 \#23!|UU8fd2=4 k5 $~6f<#l{ʯr{ ̀yu}G̨do5$v'G]{<VB9xҹgNwߤ7-NICŌU~}{ 7CvAGnvMZ`we(DdڀTsW[_۵mTZSy gxF bj/ism!obS/<8-yn&1U2x x<_@hoXgԍ穾,5GvX:45vA:B%C~XNlƐW~)ղ!~-]Sa_:R\w`OG:) o P &l&lm#mxT4)`;*=6îr*i 6l>"܌j@قzfx,5?k'*C. g8WM#vAd:Cka8i}/D8Jg>ĽnBAu>~!}}M>LF} [>7l[mlY0:UTYxx  uR [+[ӍS>]n ceT:qo* Bz 0ޓaUU1 KѮCK3^QSK9aY[fR!YzId/z=mg{\Ok7#TC0}=˩%k>Q|;%K#A8{ttLsg5u}=~95w#@lkt<7qaĸwcl}:O_n4:<@GkZ]ۉ1߆c@ >B}_ُ!Cw+]ʌ<. }b+) 6BIQՕC(ňQ\?ֵoiAu >"%Y~i}VߏeU!e9oX{|O*9˃BD-\uȺ\c8ŋWec8R^ = YE5oCC脽i=Pr{>ˎwa$m(@ 6YPL6l5F1`/,ID&sO{>Ys8(Zf|+Utu2KYH!HVְWz]vAU K?ue%cDMK8]:ehRY:42uRq3hY t ,< @*( ,0.p{cWjNAAkn"DGlo*s1-ڼ =AV8j骯?qkj{;~?@~l1`]dn{K] )XhPEoS584RgKkwR73 wr(F?]?0@f3w|6 ҚOVf2t !߈nfXJn*XU2ynk!a`_ |&d dH|hír~Yo['M+eWHȔӇJM<^,ٰ81o!f7$p#kEGN{fe3öQӜsd0|17C%v@5z5úvu~*;ʼg_MdilUYp\atޛo? FKE4$\ywҕ?P7>D }|L=yMَR d/1۹vS2s1x^QU{CA'mHU:JSf~ P`u͇C_~\n65#s=a+.SiK ^KbOM7oͤI(_Od%`u|.Yތ~Ƹ庙<H:L5D B/mpfbcʯr*{ѓһD:F; U7Q`)]4`RdkYc)=fIyB$3Z(~ /s"U;ؐϟ=FY`l=J(E@\hkk#ij(rt؍Dоϡ l@RxuHsA> KSmt!=JH `}!MC^ĆkVM&^KZx+[".Tdg 6.]1>ΐNrϾ5fM!OLՇ!LjRQg&6m?^k2WYyġ+16L$:S5_F~c~5E}3U|N!Cmnc0B9n\(w(4-2{ i9b=V^̥ڣI3!*{mI-`II$T6kQMW%>m5"Üdi`z$pp b1FqXrF?q\`Ud xU6~Cd#ؾюD}aN3x=קk5e|A0M"SŗĀ#39L;SU{g9X(R{뮅{Ur{ߍMєr+=Pr|c=]zɔUι޼y dɶuE`3خ;d{s~ (gK52)u5} b Ѻ2 upq+ T8IY@T;dd TVƫ@10R|/J"-aauFf #y 2ciҙ :mAY+;'S&Ϋ`SA=c)Sf``a tsZER:c4|=&CKm&^D`w=5WV5&Ggn%Pdvqgrihp9zۃ`dTH[C P\d8X/z}l)35oNOEmia`$aFrE|`Z@ nCWgQ7H 8;o9asYk jq0-8l褞KбsJvLL[{3!X1屮q/rD}_` Afq ,tu\#Ω{ ȇ4s: D9AzuJ`mwE4g?kS .nHWr I]U pu%xDMkBLեm;@,Dv?sœ¾100$(e\f`|'0:Y]]^bL!@Ejsz^؞ݫ7 N?{g-@1?zrO ䷦q2>I>ȚejA31dnzSCI2z2}?`g}{i9 x$8*]lŘ<"}5s 81{ɬpjX(Q:~VtD>,%XC fg%7$) [c!=2$e/x=1fPhlhF:l4}[Tc֒"`I]c\[ _{f(gy{;(TݫZJdI.=pI!6A BbyYޞv};ݿ}7i ՑK( "q R~{W:mJl[҇O0Ndb=n8}]8Kyqku , ;0~--F}=ClTOYEHNbAvr|FMUl"UNcj tm= ! V<^1?%"U +eث%bkXPH'w@Vq>hinU G 31c5rV diF&!3ʬ4mC#V1Jw/Ϗ eQ-/k(렢OR@a+6z]53'[O pxrãߕ׷'1$rG ݫ.̉~q+Yr=l q_cܾ1 Ar-@j\%vK@b+lQo os#63%u;ZFXCi~n.G}/X ĵ#X{dzY"s UcXh  rd<..=o d û SINfެ.QĀ)b װm6C{p}:>$Ʉ%=#!xa0Ilf- +챂=pqk$r̵+ NHN9TR,GWmR cǻ=d8ϾIU!}O[((t 0jI gC-Ey^^fPXp \_U3!ǔB\l:Y68Y3.K>II^ygc s&Cr9PO"S,+~ga zMD'P ,(mÆk%g:'& Z> '*vləчihgwsdv25R݈;>y Ng[ 6 6QFM(ǔKC7~3u1VLRg3W3/?Q_nϴCފuZ&V1b5;l#(Uc#񠕸q!:o1C恪)>WTf|O[>uP͒#N\/=kܳς*|+yA a LdD>~nm/6LeIj{}ixdl89=L5GiLj$> |yNm$zY43߹YܷtZ:q.aUUjƷPR\ܯ }|Y>d *+IӃcJb~؉$eH?kE@rn ZYaNpCI>!SZ˘xø#L\MsG'6j3{D$t|~\FKCTևX(@=Pr{ j e3wDHWSI TFK~pxnhh-oxl3Tc=2Xd E/ l e% luAf-PFJ6ue *pxø綔t:$KT53uLwwMע60E]C@y+vtl]iem@s@ f*YOMfcj>i4?8 g-%VV>J[wM33_.Q3]NK'UAKW7^~W_4%{EoEBCt+ҟ~̽z#0{ G$R,l_s]OO^IBc2Yy6#;:M@XӡfFx- aX zxLp(VGm1dA:m_N|`͍q}8m8x+-9K#OԠ$Ltt^E-c 0fz]qD3q2;B싮*krGuU:%|32|Z4==Y #fg`5\{&D 9\͝4{յ(ߠW:^B闿U ߾MX*'lnnzuW߯ĜyMF׿PgNMmuY xK L1?Ov=r뛷K鳟<} 0[p33KD6}NFea̋O^"kx*Ϳ|izn~;o?sj&ӟI2`f@`\߽}N'kk9n<"Qc';2x"aEc<;3@=FX{.Tmbln3`&P"]6S̫MUsUp'GƉk{AO9;={ @1y0Zo'{M6lY= (&VΤ gJuEqQ=ys}Dٛ)^AC!6Cg=F4_`G??S=T%@66j݇ ǰS ?JT5}=ό7K rk X!j,LFa[3$ٻw(ؒسȹ qGΦ~ȁC|>(aח0AilPGA55xƬwoMԪ"(/_ɐX!oxQw@>9]1eNx|8Z # h?=uς_ NśT|>vJ\FЉYEH3O*E0<צb<+H;̌vJ1h{n;3)u<::.7(Ze:~wC`m3[k̍{;$ۙ9H+RG ľmT7\̥lLZ ԚƷ ߭eiGm"|aVaK]3Cre;Uisƺ{`uI」n֯ L h^6q]~eԧ6`C-NnԌ 0[=W[R"v ,!V[lDUrTԏYHOOԥE=/rXbC2CwZ̦RMf*Vˌi|OҀ!Ӵle۩mrBA?@g!r/Kcޯ,mU!rGMz3}y rNPπ8Oޟ %=~= ޼}Ov4%5/s\$ DH(3Y9sxCy6nPVD 1J||5Na16H$dl#4a}Ɣd,:b } ze$d|96M <_؏5 w['mks(֢\qLxm܄4|zWG8>wϻN:Sj@=F4za{ qOP3ѩy]:NVnB)E݆saݧ.K`"3;Tqm}iq;pB4.dtI t8~JiP `=Dvhގlce+ש\g#f5O,lg g`uAq+7q ?Y v60sEy{:?i1i>zɽf+cf}vS3)lրR 2:]<+S%t]I㖧-*Ŧ-=J p:u mcAE/fW]L kp 6f@zӾ[޻#PY€'z[fM@::>gkwb{7k X%Lo3ջ oԁIƁyau=1=ÓeCa.o|GF 11SRR B/qwk2>$;{sO Eދ \zn)OȪB,]jq!I&yJ}vGjV6Ȁ/"[~:s32/Oۻ!]_&X^M15Dfx/U WfoFܿ_v8Gy;{3 4,ޓ?Ak%v"}d&7.$_-2ݭ/713n"5ӗKč@xPQ`5op{~[tFJW. 1㣫륉eh#` ̝<5x8VjG2? 2DDPTyS--d`YfR~$H)xrsI'% ]8*80A1*ƫ@@`ADeOhO,dG؜_EwW"lrHMP氥=%!V_U,wO3|5aB`wimwf* t3&[iD47՝ޞ*I/hf& }ʸߎo5Y:z a%ӑ6q^bokOFlktb[ [:D T>pdKyپ׈s}]W q~'#ne jؖbn ]><{bսۋxg_ a U9!8s7lG1kL a?֞%! SGd3Ɛl࠘0aI6|3sh % ,/T QE{3|_u]^Jm  pTM1\ ӁG=F]s_3alx5zh@=0 ƫIsY1~K nū/I[ڽĐ| ێFt`sڐ-m6@Nm:Ge$Q-d*.He/S7 yF⠕̎yM3IdaϾ\"'R:G/_,-Q*<0Xtlf :w 7 0i5@,f Ykvf:[Q`Q`Y~nMs2' Z͙\ Ȭg .EA^lؼ=d%Gv)x8Yz__wL^|Ceßq$p6aid;!F9fз&keJ }ȼtVnqw%|5]KFYdCL6% 9C1?trunu h|QAݧ#y 8xeY^SG[13!:nv1g ?AX`M=)i3>onSC+HIgqTg,Bve+!CZɧbSGwHsu:frf,r.SR_9;>> `5!Y0 hU \WRBE_cmA`Ȍ6spxl PLpÓSbqCH5/o2Aaڽ,m0s1Fv$_@7طri5M.2OC.=ng/G_mAZ知Y-36X_Ga-}U%21f{D:HCXY]皅 _z>-l_}y޼yr_/{iP6z]e p5˽'T_dOGfhl<8ۗ2I`O|-}2{: Թ#[S.=Q.g% [cݢ[d~ƸJZ9 A rӆ62(c#k ڐ,An|OhnqM)-lQ=JH3X~͚!(l ˍFp.h'7܀q!X E|Yo@)uAs ߥoߥ֎z Tl#CHOHD8; l=,9~J}#3A-_sv?8aV5+jf=FVXkN[Ϭ?wrO`Jn@2 %Om`=b p=K&=/V~]2sH_]~p3X78-C°ADa=C̸%aY=7skΖi6C쁙8Yrff`_Vu qptW`14M]1,|ş)Ϳ|Jp])qov-eA[\Cw$h"QWkbO?i@6Yl3<뀩6RYhg` s %/',IQli 5|~|8}wѬE]{@fV@k> # !~5>3皐~g@?êi#w-ް4nt .)HY:QdOچOBᦂz=\!ds@bJ?Q0F}+GEG@V`\\5ɠRа99,hf{vH l_bF|N17|TU!πJ SϪ$AumПk}<ӿ W[@ejUJzA<mH2bO$s[T`؂)%ܶV|G8sV]=39m`>B$bJ6 [ǹ3!QCh @^"aJV8-+ 7"żm eHc| e@ddlͬ>2Ffз37!x7.k<>syn]K#a>:d҆lݟ%9UzfwxE!%b(x=* v >)~ IuUl$sO/&[*o|_t|鋵 0o-4k 5ϢܘW;Dy У[i@=Fz@C?uün`i5^iX8@ޡ/IZNsԳ}T6y)]LJ872[,jޭȊ圞QpbY twt\@?dTp$7Y\"Zn^.ഄA{g" #~gswvvLd(-(?BswfN(ò?<8Id 8 L=$ݭ8?;q0)ێ6\ ''GWɯǓ֚W&3R`' g7[L_I GiW fH Yw_3ExI::>2G4 rڎWpl=' cffNrm:Ifn=/#͆QPKțEQ"̊!k^'v-ʦ+Q^Zȑ&7$m_p?4r瓵%pWݯ ˘6?2ɹD3;udo 8@~=ÕL&V>!c!}OVB%Qk3 t?ej֘QCI/.il@ odc[2f7sT7d26ftQmXwTw `β-16Ef>1_Tt ױbӻe=4sr[{ -q(906Y;ާY|AcvaT ~_9?3{,Q|=a> Z38T9=gI p1ꬒ^5<~z?[Et9h`S4ڡt[.yHi]k͛ RBoOaWh:%yP{8_%tNVa ?Jmdwwv(Lƛ*5.j&հyJ^`G(L^߰ePY;6k^sqp$~96Yζ5$!g=+]Lxs B;M>o-mRd 5ˇ89F =l1Wl{? I|mreP;/wJ c9q}w2=y⟦yaNf>c}{njF=ApY@Sb`lcNX$]qSl딬-/d$L2/vYU % WksqJ0*CsbwVͨUYZ9fQku-Pڐl¶crL[TgyU(d9|AKI1cYk>JgTz} E[c[bN´5BrO1K{+.bE[rY̖rDܻ"kFwHWG `w&)Ў}ߏtY\qGث)jN<;31x6J}gI.}¸CXm͙nm#As/K->>5YuiT4>4 "Hd2k^ߙ-y#1cch??k3mG>L3gŵAO7^h@=Fz@ClAkj 茴PNjO4^eV@|QOh8`"j#8'g_y Q}}BF۬wʑi}֋u{Z"{z#OkWlfN,qE1َ@ߔ=?`||Hɺ4@ !8@X;Wq0k 64Bݓp> U^* o"s 8?8I_H HAQu<&{vkff㻀 ss Hbۛ/=(l@]'` ١f;Jʐk_^H  Y_@PތR맯ujPd!.v7JgguH *ɮ*o7c]Њ{e0=V"lgIRA`Ax@s2SW*f$ O#%BՂ^Xw:igɥu͝>ei8kL8$D)vs}^ p 0#0qtt,֨IF;Y`NQ ,?+yiUw9oS_ٵ\k!d ѯ&FPp<+3-[ ~w~x(ON[6kg1y ,w#wY+'(E-p@yhd$z% 3Aأij|B<O5ڳ07;a`/T"* FKD\ߠ[ PorU@4ϒlGjn5xZf|3n)֗"Gf~KX~L4Oޯ鐀7c|a?IDZݹcFDR} oyi27o7 P睽j}oQTNwÛk[Q2mY>!lM NBÖֿP;PFml<䕹=iWo m}>?>gY؊UlKxsnw9l4;1=ގ}M6ЮMM=d=p2}ӌ]+z,Bz$^sle@݇8F9{٢c{,\Dg Ifm( dPja^>C&SWUN=T:uœ=@f[6G"q3b͑ڌ< /Ƿc/l;L}bWY \_ P.Rzl!`k6#c{[ ;`38}AfA<@2-g}{,5cp(=+ -R?7:e)c JyYEs5fA.6@@ć;A< 1muxAoI=؀fan>,dѱ?et?>~]c\<l`ka%KX缧B^"H uۙ7'M$kނJʃ 9,R 8=|f/k$w=([[o$S0d|ė+U>pmm$?!6@Ņ{;s#kyˌ@q[=g$1(;-`xX*:Ȁ8=1fuFms̖7@g `[ xZ7wRwԿ1>uT'@A)hiK9Gߧ׽}1Vf&q@|cz1 v(g>Ϻ_DY4?aM4{kܲD( (PL OSy݂,:7lw07s<n3 \@w1o )LŅ yqo ~_βidVjݠJoByP @w;woZE2Y@y!O*UX>'x# ;I 7_BPRֽ r H'S'8{sXbobL/ 3\G璓Fxf$+eҗ}@מ#"g>uݛrMuYqnDǽ=/u?={*t ͼ J|<{Ȗfδ2}3ȹ;㏵1?-͢z b Ax(nv\bm2*+Pe^\a {缗,pm׊Dȵ8k3i;~|M\ H8Ύ&sB{ {;߭M.9ƹj̥bm~'jcxyF=AgG7`1p]%"_t܊\35F4z?n=wDN#Q1a֘Gg6>#pܘǽ8ֽ]մ'˶bc\%y Ä b#b]0LJnQn5"A8wt&qe_& ,"KT=3$qqH*XzT5G CbbG'K-E04OU@P)|vV̔ӁCr.J ALs / Yŋ=Ȗ.ٙ707{hlw7Y;ۻ^ +=733PhkW=޳RH6C~`?mnoo0y R5QpNDse5\{ UfP@ ^{)<\[ ~׏(A(mc xx@9ך֨?`6G=H`feP=".87 %S< ,rSC<@YC?NӬwoAH3s3dʕ dϒQoRa? #ϽWSǧo.+MTPo `s7! -w mWF\  !,~2ŋf}}|!o:R/ΰIg?6v_YOd#M/. ~V'!ҿ|^ddt,d(.*[_jg 6 0QQ8>a^FP^i}%'%*Tܐ!Pݑ9rA05㹂uj'38PLAZ;JoA1FtfUƫ{c ,?+ݴU:A}ag$oCE;^?U뙣5sKP]CF5ǻd\W6S!O.Rϭ*gutcxzz߃`E>*{ÿ-{ [TTmhFӾR)ͬ"vg-e߶m!ѿͬ%V/3Pߋ`^D٤ؼ% {Y{4yc uW9cF0xMWxA ֜ WgglAE,Ɣ ^b3Ϋ҅|s;.j5ڮGg0>vSE=RzBu~J&Ozpo )Ka"@>lf)gg~}|NE_?9d̯x+;&~?S $>?P1=5ԍ-f;=,'f!hUOz'%3<-ޑ7 +Ts8l 7dz{}'/D?~ƾ>qVڧCBq36$g$zph,I:VT^򄞟ރrHPȲv_b }88.$֢kCNJ+9*.SYU+8LV@? mSeϲ<{ܘK6^h@=FDzCŬ@ KnMF{)mؚfӯ⟦W"`_"O~vN)m2V`${cElXeL[3Wrɢ'آptOP0ͫul3hT@6.f|;T1OVw V'K .묍^Yjel%5M SWgΌ 6>,@-mU/ !_|SΚjq0^o3ȉCwe`6,l3$ָΥA& o T:w!f-Б}H|*A#u׍㹆B8+x}j&}3@NMwk+]n@HVݺ+{ ]Jid7 O r"HHK2{+ izゕd÷GY'Ȝa@="  vl#slvnϲ P+|jj:g*!8bw2wenvy9ں6\֫g?Yzɫ >$9\;i11ymu38vG  |Air^8D5 4O`Noޑr]KSdʿ~*H6;[x}D?XtqѕynqyCQ-o{&c$'/  `KʩB1QG.Jn@ .$t(G%C2ц)N )oEBb-~S%8:kLBAÝdv뤖C9(̠J`FŻ7[~F?56k~& P\t= (NVI=G@f@F|#,j,83$=^_cʏBj3Гg[!UٿRc]yb#}EXl7؃fa ( We$B_d8YUvn+ rN*ag`/sǗi.K()En>03ll*׹fH%Z HFE5:*EDJ<|Uzk͘ v{![S[kolz{'',5njzLVw}KN/@50(ǃt? _>L#CiLB_i`$Ѻ Ax=Mk NƃUάmJqB [yS}i$y'h=6hi/K*\^EUk*vv ֜Nbw12UviܽƆ]%Hlk@Ż 0"h[ڬ^uYl4^{. *Tk{>T!G\fP#CBV`^| 16->P~z{=bc|2;=-9|2\_ Z֨W)IM_5+%%AWq)i ;~6{$nkK^|F :#|w):,s:)IGX d~j ;v2=@T+ Ǿ4žyw bZ<_.'#1YL_TW^ǿ%s  fFy -Ayc}|f;xgۏĤDM<(?ΟTxtsՁo!ir ౵WUJuuYl<$rG0Sв@IDAT  U Koۑşϰ6trhm]E|x_Y%sVRS~nVUΗ;&f, :9<װf{bx.db_f;N/[[ܛ/Qw!ZŽB\ W 1㻮/?9b=K"m-&ĩ{Φ K YVrkEή VAO9B)FfO-. Y0Po, Op]i`/dggfpZCiBHe4@z贶*fkA|: pu'fpnjY])TJVsZ` ދ€yf !'o >||?X2sܭ[_xL Tf2Œs$3Mm:UvT%Dmzޚ_fS1;C?n ^ +vNС !$yө7;dBvW t :_Tvԃdv0oz)Jt0n0 EzԌI6@i{^.:^瘌pN֣ .>i(*־6%H.pM qĹ0ے.x!ofR;'=n h:Gz((>u.7SdB6㿳WVWK\wfsa_C},cE Gd OgO-H,fzi`{> k[@LRVߥfG֔Λ&6}Ja%2_F~}ڂ2H69 !m( ě5}y\^cVR'kb@Zlٛ jϤ+̥ۺ!h_|1O_7.q߃巀dFbGF_ncc:dc2GQ{%ϛWU 2 wk+˧%{1>\HnB0@7|p]2^ّmثb"053cuuyRf8V`ium`0^5y q5(G%HĴ;ϨgI,~gvI7θ[ °ۑ{xGRP.dJud;{7ҳc m-Y@{и1T&dgVD_WBvx=r# ) \3sVž'm=4(~܃w9h}kG~ x j0(d"3ݏ}_MaMzVo;ܳKtArLo?asϞ0f:7Ӟ}Hfv{/-s#~s3[#!lK??gPȰOys8~ħ~xraQvgc&gH.T{[{m/(\-$ÑRQx8l p~qP!oro*3z6{$=pI-pܸ%b ).:,bLBEKtRrJN̵c5AA[]C&MaTU3x~^9==?JTD3~!.[E)&xUoSK,6ƫh@~`n=w@ 6Y|}'=w8,a|ct3$h/,Ҽ2[ 403H-ōesQn]uZqXu4[k:fNYtng޿qDu `Xw=>p>csƷ:rdJTn`)oV"qrz:0߾`e@J Ϛ-s Tf~/}@C W/`2*yw/qR9g*e}~,iԏN#V Y Gt>ϫk @AuCF,O9RZYy.%'X9YS뗟+g8Y%nY'H C;C32o^?:Vf0tS½Gi;c14}jeep3LYJ?Lv+X1Ü xx}$KD67-"ϜW:lyc_:d,7^EQh-u|W=ȴ1$NY>`ųGp,\A< k01#PkQ213ZAS~˾"C~楄%,D9Rf][\pY7l(^WC{;\\c [ q3ggW_}$"=' ]]Xd+xov0ReY{]PZsӱɸw=Oeuꬿz@۷TxO붭ym9U@ׯ2@gb/5޽w]S|}}ZfCB, kڒ s9}ELƳ"oOafv6fkc-Ƙӎ6i{uu YSAx?DR^Fci8^qX}zv"֚{! _ѯ'ז 2^24;̣l{BJ" b#[6(au-C=;vm2R{{NlVW׃tr^B!.lj%‹teN~*l`5[ tp l%1B.J3k+# Hʳw";304q;g[<3.3h ח.g$'KuBtQnH(k~_ %2wwWHOaC<'^lނ/IsülM*([A9HܳVw#?{iv/mS=Խm.FRp!7Tn ^-wKGڌ:UH*o1mFkI6.lo5W!eK ZwVQ(Ŷxf_3ZF:>܄P  ##9Woy fI%|y ¾ {cm6 9r%61۟2U9-A Cf!l|=Ą{|H?Q,8/`9?~RLW/ufGG1h}wGΞa 3 gn:lHoAIT~6ekȄ?1GȄl2Y&؃rF|bNW[e-dvzsM <~1vdds[Z)YP8gG }ɌXV<>e$ g%r)C̻V0ʢΔV&ZZ*O6:8<0[To.>> B!)M9;gm2X KyG1F L!a X1$k? ,Y_EmYg0j3};Yy4!|*bY: 'llo8Z 1=/7esNש2~_0y( U2d]D3mo(2 @}Z==ݏ}~H#ڼ#|9uÞfB;o|&Hxf4wOcC5|Rv+C<ߖZ.mkL+i-j>gxS!b5$?fk[{8yji\[g^xA'雯[_͉VwCf<hîY1|rI]ω8 fYq;YIt~5 2!cFG 9E,Sl7}زANd{:>Y_p]dG^lg[Dʘ9}: pf p Vm<Ȑ֩TZk}{ )UԺ66jf6l}gy0جsN֠9yo`Lh+]ԗ$9Gfy2}ݪ 8> ѺWu8w7Wc믾J+K_ k+f,'dv/7oYb ӹ2f~/QN?Uo$ٙX 7e[/=\fuWNj4L+k!n}tޯEEw"02}|ź5>5%_XdMf6#}mԉ}ǜ+ ?PMI3+As Tɨ]7Q:wzߤ&2}k3:S'iͿH=:MdP>?ςUԹZO3@C2Rj~Dʘ,@Zn$wO\_L)0rIJ4hIThf#ۚyrtʏ~B$k ;fL8o? @ܸDk?켲 r5,#;;bkErWZ^AKEʽR >Mf>Ym#n+[( <tObDO_l㕾Y-f57\@gO;$ df9W޻!}nx+dKoc?8G=}c:~O.P)]j68X Nv݃#S:|IeH2}գr ѷ:x97M.EA\e-CQpw$n,g{g{bE3 *<מ۞*H=e#P/0yK0D&%s[H}l7}4.+2^@U^I#;fq#'#q \%@[ Pa}2t^ f*~o@7J ׺xscm4 ֣GM kiU~LԳu x㈑GU$HqEt(:Zn!#0fk^҄`3?~k]KSd&0M 0x/HC/YamT0̙^^2 "6㛄nFܿCA;f tp:llX]CЕy1g.zN+K%\0^dY3D7:6ta? 4FZ^c;9FB`DJ|M<0L 0 Ape`avt:[쳧pˈ)a \Ì?Ǒ =Hptqo uZl&[X?i`]H>R a `f^6n]Ldu11`-FLF2 q֪s6[#gk'e~?^OK;Dsδд>6(Ye 󂨨p:H6{PB6zxq:=xHD.f;sΎ1xk~e+QwV~ak4=(;!Tƅ+RsiZORct¹KA@䴿(7#y|}R#2Ϭ%:a5G&rgWOYrX_-ңg$N3~KXUYRz'ǟw51v8ꕗqz9'N[ 65̴'?e½ދ:j6Eqtj 7 о77=ca]9HeD`6D١>,Jp曞 0TVAXA](}`(gfȵx7*HF XF}HgdZ #}?N+qZdC.̓c&cKׂo>'}9Fwӕ1uJEAFZ(E c 9֩o7}5eǏW8 ˲S}&OQ9Q>[f(Ya&96'|]Ό  ɞu/.k$ΔȘxⰮlt \ΐO"\MҶNBYl8`tN tQʐ_YY˅ 8X,xڦ~wx(Fm"a [gvfgů%O?l9,vm}:MUU5zQG/Iӄ-"t!h_@_ٲޫGDk@~o0 ^BժŢŌ!Ǣ\ Y|u1&B?7ik0(EؚRSHCP6chVǛKߔL*7\bδ+gVPO1Lz^GAZmi(w7*:=ԭ69gY&gYN᙮"+CP0TxրZ2OwPn0"v S~zE*"QQ`5P+o h5qk syiZiMSyϠ8lj6? RKWSV]c@' oԬQzO¡^mPP 8$K)Qs?FֈzZׁ)NUPP5.3I6&0@1zH#X?j(虜/m4&Q_Z O] ^,z_]Q #(*]2`4'z70 `h"#xi@R7׍Q k&34p~T.$k-d~o>,8|F:264d~,SuZNVLĮ t2xRJu]vXD]v93[`}u`i$ ,YPd% KKiaJMufG[͛\ݹ<ǽ+ /{.P*tr6ey쎥ۤN?WpHs֧5ƍV~'"}^LOpbOo&}|*K/q_z)+x=M͗n? G]al7c9C}o=x[Ȍp ӥv2{_<:1~&e:۫[ic2 8I`PcUF#  8AޭFG=1|ѣih|8!ihtv*6:{d}|O? D`g/t;Թ%t8+X'W "o<_#~j*ԓ\)S -"-6a7Ms`eMٷ0_S<5jqϔQΫs.s?Pv%Kp J8f6}b@ԫw8<XM/W$3~#(nw>1| `86SѝvTdϏ\gϰ~2[5`<:Yh r!W5 I Ha \KL./џ<|!@!]umҡҽ铻Dt!"uL #ЀOXHDiAmYKɴ}4bp'Gd(k {Ѐ~gv<2HFQs`>ٶH' 2Qzٛ;H=r wYmAARQyK kflZpqVkqOɜ#̷6u͚(#lq~UxдȜ5G:Zfih?Qse:>YJ=/l.dZ%" *^6Iko'/JHv 7c7ѳ,ETWl޳dV)b.s2-!W8s#s5@tFvQO@].}Qܫi.s&Z5K ?Ë1u\A"@gŁ!|wY MJDjW)]`jD3 u!0Q%#\Ged>o,`uCTrLn{82Q"J`Ѭ]ܧSy[l`FZF> [L: g:5myYcY^`v0ykR L.wt؈lJ[yWOY|gxfC֓W u}YL}J3s?m;7c ̂pYpPgpa|[D`]鴽Sϑ:N֕ot2@C6Dtz&1zl 1&5(}9}b>8{de&`t8(1㞛vp u/Z3XqNA/ssMfG_E5 % ]{frώ MpmPAZӞ3v3HA|C7H'QG%_b IéU?^OV_+>;_ WdCj=_TU~ƙw(||K7Q֞Љs z[mptRhG^ G-2gԇ7z> ?RQSnIL:oQ"0! &&^eHM 0`+2>/ NQ*Q[TobV:0r\Ћ*"YnUbh{ m zCͻ(]x D4ңO=@)Y[!jpKo}k߱PX. 2X#)G2 Kt>)0]pf:*<퍷^Iq/=xL-Iqn''j`g?Vqc!Ԛl|eJs} scē'Qr?ɻiuiԍ'?r2G|wi6ۡ4߹{7|4 aL̽5mǡ^G OF ?>=Fjb(<0x'#{Pfc~CbM=f<B ѵ4Uǀ·Deuñe#MR|?g J<]QyLf}} p;0~ADV?|OV7#g}(gU$8:<~Hqm _%-byHQ+;)=&=$i5ꓤ#*mg:l3@f510CZ\.6xҩ];-E Pv)+%18%nCTG.ƞ{X%)hDZT!tmҷc!mx8G(`vIU a t]$hx:WI{66|VB6wRB f,edrO hDjK|H;z++,_^Le|Rz7! Nfȼek}1=ĹZQ2k ީ_gۉ6CQw~Z~1:hϔ.RyY19bp3<>xgW>Vj~RrcMNI}&ԇ0W4yo(Ml!} /ʖZ2[V`?sRZUUF)QݱnCg d)5qgS+V #=8_D(##Uro()urAKee.=b|qD @Z;o7ƠȚ){{fzm`~,oPGrq `YV҂: uV~j99v @gmoD y"'zrC3uhyy{6]4:X.-!i&H}:;8}FVĸtV.w CἤƵ<|gg=~֗|R3΂0(t uq0ne1m~mDasqv>[(kXS^WtOU}Y k9{zkŠt3޽aAeּ?{{&*t=/tqN8op++`\>rjFײsv=Ӳ#9dӫ +O[RifәPKcs5@c9{ ¢D{UJ:hXdqZ/͝=b 8FPA{zQP@×`Ƨ8elq|zJIޫGDVfŨ d!=a_Wk0# ^H U0x E0ߕ@IDATyڍXɧNU1pzWOQnTGsϱB&ѦB:zwܩtU|rtƹ1҆8qIXmo6g{PT>T;<I**Ce(6'PTUvyǣC*$PqO?QhSغo*=*!HX9#QUM4I=3zTF#`| '+aʷwx6Jv( _}AM/0ye릁tlV TNvFӽ/>H #ͪJ#&PgajΥGҵ۷ӝ?]˗𻼼jqo m.d0o{#H7 l$Q9OCmbDP6g/-C?ίƺ4=xo޸Z9n\=B2iIf=AJ5_Fl [޽{ԫ'H/Rc7ER)@%iw~͕>#7^ƫ'O#uUXiJm 4VtH~ij1JX+eWp0,׳ _9Ž/L:,|t&7K_|EmoS} cߋn:Nӗ{o-?Mo.wx2;_>I}#{5>4x7ݽ|ҕowǏM?Ʃ΃黿mx=}BƼ@a>GqJ#xiYw/>^}Dko>$M(!ed>D{D`>7Lw</'G#$B_'~ WL#(?`mk,cР%.22 F;׌IaqHX]!O$.6"0rihlSRM47ux:!sno:3BDC3FEv$Rgq+EF80*l`_;ӸfG[%FXq㑦j\:>ime*Y';8b| ]W5 g`fjo>6,N`Whfpֶ]1~rdGAȆQH.D'/_% >5Ro[oQz12{wȭy:gfK̳J)B o]My9Qӏ{N 9уt ɬ+qF[K+ts)]{-)}^EN&dMca:F33˄TJ:ۈE:^U DH!/чYmC3 BWF96Rh>#VW{ @k x(e@zZ&ƵkȀ 2^އ|G$~T~oIrhQKuQ0:Ũ^GVU=- ܝHC /!M NTdGr#1ʿkY4͛[}m /f<1r(H+w{ o\y%]|]T#u!j YǔˋdvXNS|GіiW.s 'D +_[,8#`\P)~}bOk?I72 !^z)䉋. _#¢eoTmy=r*:63l"eՎ. Vx.30)>5;/r AvCջ3Om uu蓮q"Y)-?Q' hQ5xF?N<<FRZ brv]y@Ͳazyy5UnAykKr}g嘔qK|-Cg{c̶3cV7=={_ʂdC^֑%Q%g%FmƄK+KG ªgY2e Tg(`NC5k^!߬sD >qok9 ʇ !1J~f,W;tlq_Z#upsp8'\Z!yUF䎿yrw5U% ڬwyMOc.]3tic ,g+5=gM׵a?L%XoՎv)VɊN҂.+Bd> AGA%kXS3рQ~B& >Juݫ9a_> "@gݳxi'#l|yj zt`mSĺaیhͦ\5m_ ]ڵ)"^Y)$1^XO)~j+c./ҖvH:(6-5 L@zmEmp u߱lOGIe3hK?] 5i 'QG2|uRXWD}  _4:Pz# BsH;kjb;ƾ AH 4P @ ? UAxE?Ko]Z|/-Z3C<{|}ҏ6J @M¡eQv-n5VXe8m\%YQ1 k!KUh{pûkY@0׽0ҦJmJ g#$:8caD47{ 03?5<5h8]oeb_ys\y嫰x Tl<-B!R(PשaQ06A/IC#L g_dƇ6i4HPw>&Bv^sli2i)&:㤯̇+2GpkQhkpRh3^/u0Nj_T5'}*l9F)=/R΅W"iGqҒo{} \DjE GdWxv !_?V̶ X'Om~j&)åIfd6Ƈ1R<M q AMa7ij5tZ`Qye-se]*O+BM9ШB451@C~&?eN}=eO'}.cF1j( ]_\ t ϗOQv3.FS׹?}Yȳ#ǽx?Q^\g[܈Gן[&󯽚#5A_U uPe-A͸~ pS1@/~Nv?t )Gh\m_~B(@Q]d+&_ǘzsյEikӵW_0X ڞLCsb\ C{&&eJVH~e08U&b ;Szz{ 80 a|i2,h0铗wVǏ>%cbȊcu" dj%OxTGh*]ga&|u<"ޠ/xfj^5w(Cd:G^BNҩBvI23 4էp{!#O0BX7aɧ!< `VL1〠ǜ߂:V/;"S:|5̡2?Rg;5Jփ!ځ'sB<}9=\y>AQhQ^|Jt!Ne6b= \צ9}1O=L,P跦0G"M,\g)<|WPVyf/u_߼mqdC,g?F'/P6$ugڒOPnsRLFdh/&ki=pBI42w-xZ*"_<㻋icdp}.F160L:\O'ցcu.:#e#xDࣘkZ!b,'=y%,H^ԕ40zrrDʣmAr۷) AfCa]h?t~#[olo{K/o4\>B֧{Z!9?B`4ʛF%L^ lC@uJMW82(İvFb//C"_P5hr4g^ZF|+!!878D]I҄lE"wSztfx$`:1v1#Mz{ɗ񘘃Q-y_U&>'9'nhUOt7͸ k: 6|f0eK?19ש~o?{ꭖ5R\ l#ףu8uKR]g诌szw~&LR^5NG: k+~mxv2CP+se]>Ct. {hQ+ 2甠%9"22^%x8a( 2ço⯢926 Aӳ3<se q SC<#gzeAJ'QO[NG1xW欗dc5лO-OVW|ZF] ^NIQDnpaϛ}&g=Pe&ѤM޴ tm?[`c^= (УPЗXAD%A뽞ҭG$u)|qD.y)КJ!O:<>kE:G ʋJ/y=Uzͷ?$ HlLBzb!88g:Bq~[YodHhQ⌣U#Q|FFeB_[}(g< Kȗ&\70pJQ'96MJ`q\5B ^r= t4Pb.Хqx@YW\F7DRF0ڧbQ )4XJ½SSDQLqܠ3/5 dPӺwvbRK˜ό26U~{ɩ쓵7_ sίhTW<&zV)AA4z΁)xx?"5yF. ԟSSeΙ_OfXҷZwvA7gXix3>rv5׼g3(ٓ uyCf4:1GM"S}4HQw8 a<ч4"5#Vù s0Z0J nh#O]4#WF F#c?OSόvrN ]Sbǹ4r/ҠdQ⼥ g xmeMX4hq$oB'E>O=`[+yhs] M}=w)Wko:N1:i5LTA ֳ0o*߈Pd,*43#d5oߺu^eN1]g_~(Sr{.n5^z-휯;?2l_ ~gqG?;[x3ÔG{[CO_ ]@ @)Rh07]@! 0^8 gF0眏5:~ mJ#wx4Ly+8g p]Q{A>0h$s#;-9W:xgun<òF,CkFN O/2am@~p l48*q=0FL[0q}4}YX*iWo)q3YQ 5C^fkytirhNh< ˈ}k1K.|A:\^l}X>tN7eTGF4gAgzzt?,>HWV4K o&:i‡aڣlХ_Kz<^ ='+s~_!DQ3Ed.,Ϝ/{;8&7Y8ܢm#KDU2Y~8TйLk.4k@tM}wUSd_)L?NnvN^kҏpFfpD02odȪb>thf̀k\5zR [aO"zN\;*Y*'jP3hZxi9@=ls'q%C :Y ]֖$h:rvlǹ/<\qh28b,EFWbFu0V֢9H{Ʋ;ۛaAN&1ByBx(s8gxKml8XR}O y}Nt63ͅP,o2zQMYgY|c_d_B` w*lb4 fE=( , N6֨TSe3G)N8x74)PJM%h+ om% 4 6')lXzU/:=5(ئ}zr(Q(ק6m{>úD՞c HiY;aDq(e°ՐKPVIRHGB)2U&gBhP"cAӰfѺm̺8G[*z \$ ›r9R[Sڻu,%BT18\O_U ٜ 7רf@TOt6BY凇)q}7Jm.Oqʏ>*?{YN$3m׽kA.(Cq\tRP/=ڥ-j5΁#JNgevX+)ٹޚy- Ύ*JJOEѬlOhNF9tX6oSS\YDvF1HBV빋0Rh!9d7F?%L`>t1Z<)\3 g>0iB#@(m#t+R4i3uJs (bmtݦ u g  3q[P7_``p 5q}ӎYzR0$.scU up-[·Tg!iJ\cX5p@Q_%'][[:>=s32/SHR{T%XF4y>F/a `v-VΓ\.6Kv'yugʓ!dϵ':d^q-YPޤߗ_##gZAK}OFMFC^sK @nw!x\#R#+>F ɮ{\s}O0ms7CQرkd˜@1 w9g5gG8uW֗Gq?ŠMdZ_}cI1AW) u)GL,# >Ai:lёGg:Ҟ717qL.PH|q`,-3'A`enJsW8k8i#d>rبwrsZjx٥fF>Rd7-짽 0hLz6]URSZc``T\ ("3GГ[ `T$yr3i5E6;74Ȝ#JE aь 32p6)1AmR>GqX~'ι.5֪?.'߹P~a&q_jrJX|@*ä/Uqȏԗ_Ms҆t'7Dy0Pdc2aei'dW_ׇzN4+:MX ^u.4-usdMdF֌+2@?2! ynw!52170˂Gr:F\Or>w@YxJY~K:mw0 :+cq'"?sz$gd3}{ dFRG>Wtǻ/wpj֤{=AR3Xnpd%] }#YɆpf:<ߐW+6Yn[/̬ܭmBg\oNقY9Xm ]/]uR*;iddn+B3R3c'|2 K mw8'YM #)f:T|qSưf̷59K44:](uF2 H}uCS,62:k;ts32JDUhQUcuآdЖ3K=%38x&O3: m"fxӌhKk33cBaM'ENy]Ci69gpF;ytNԏG܊ 2L{3!H&BN >~n?f(svه%(<ѡp\;FWWtb, ez3>Zε@cǦ.|~ gMi4#gg:|Xsod hr| jc upzx~z! =LhޫGzx^ Ȭ4)0Yrz,߻RCBQ7Q@Y#Fb/""@ )PbR裏s5)xc~&K)-z1ŮnPfLߧiM1LAY%?9\#M5L6M) Z2QlT,ҶhBAy#4.@|2K,U0f%;T?C e>B|AA?u\iR!yn&J3tb rq1e^(߇q%Φ d `_4&]㚘g}Te<7sn ŹwKf.[:O m򝣢:y;|ڍ;.t9:tҀ%noD@k'FKQzQHF-ʹN['B|>;?+I\o3h@,izB˄-Rȏ|>>` NYķs@tЏ2gB@C8-- {/xN;# ײKaSým.ԇC4UOࠡe6t3p =ͳ/J1Rڱ9ʬ :HO&uӏ#'` 4:H+;16S OCZi~&qS^ȡev.gd9 bYVգ@= |I @zD,/ }i)u* [@u ~QO}KK! )^/Rg,oK^J @F[oQ&1^mY*Ʀ!0=pGVkXc e% B_-$3!dP`ܵ>f?`acq1Ǝ7JTbDYQU@R`%;C m h +[q9FgAE_# o6^?q*U9=TLޞL[in|)+|w6*U!!xa"@c^_˼b7L/igDcVΨaY皅tp| _1!BnDϕGH.-/!a4F; a\Q" }Cs#?%>_|i>" :;,Zp+t9>5E?i-lfN49)=?aʀ~FW:AY}ƘgBsmR#G9y>o&}>dkdJh0rnh=-0b{;԰adc38#Yшnh<؞QUz><',F\:KCAι*P&=QD lm$00uMi F*ZnrϾzr% `0r^c bݣ3tp:F:'wlZMI haBF+Fc>_~⺏LN];1Y*pjdgIHj{sm)4ggag_K{K>.>}@FEcF1bQf:[3kbf/9fv}!''/XNyncl҉ OcޫG+ח`~:yDZt Mo;^vt|JX 048x(iC X?c|H87S8牸7U˺ `'KwU+ !87A$0wRP-hDt]_DEy"y0uIM6"@x~ <:gM:ΌϦo٣>dd2 j{WW-(ji|.\zj@BRp1Bebl=Xզ&e|)L^>bw8qΏ7Fyuԫѭ\ mmxnACԚ:A"gv+Ȍ1'dAd3RqCnSHޥ ;x'MviGq0eڽ1^7ʶ9@9 epL,.p N"^GbPPuO8R+_꜒E|q[;%Oy{zdT^!4 2Dl3rKfA CfكflH&NSFӞ7ȘN"c71CCU:s@QC%pnʕ:ƾ9#ȉ:6}d.7"< ~ҙs OR[=`)?32LR L]7`]]s)]u^Z6h)&Tc2%/=#npv tờ˟ ?!&/)Șsg$% "XgvP0~C@2IlETC5s2N&)v+y֥q?T\Z¹4Fy+n_+m%N2kֲb- I]:lY]Jqi5paAcd"}Crp}ꭥ[iR3ռסu* fbQ>yS/Όva]i|g(G+|tc(*vF)?Hg zqvI`ޫlJ ^|Me4Y^lZC(AL4diya@h[pc1Yߦ "q5[C1C@ָLX<i2 pvvON9>I]m#`Ήs'_FnM:a;O_H & A>lrJE@IDAT97sy3sJ;+8hn-[8~}*;"{| 'sơl g m\F]sDqN#Q4 W.hjR. :㳼а`;?/^x}9kz1FqAgAa=j:A}-V#C%oZ#h\&F_VaΉ-I @mZ+"Ix{Bi ]gsq19 ȽS:,#3dgwA,9Ƥ<)*}''qP^ossww-wډc7ku16H;s):kiqO99TN"qM9;ԑ @Q^}f\܏OO@ͨ( p0BH"<R`3|'1cfAm7_~b#c1Seߐ${KF:^= |p,P:Kgi46s9obiǙiC 3ҌNմvܷA X-R_F|<;GC kCݤFGDul;EQ :Btr\'b7pڨ"ˌ Hyϙu}9}:#ib : [)kw&)Ms=>D5N$;cӌ«'¹Z$c7S2Ys$k@RGGt,Q>lR6…l5#_H׌.ƐOɦ]wS=`})3iaPiѵ4pFR_9 d4+X-Y-XȠu/KvQ$X-)ϱ[ȏ܋΂!f>:$K< -}-yqFH?:ьòMFM<0׈@/*ːáۍgז өCEZxuo  hf;`9#xkQ@ :g>-ahյD,i|:vU %P-l5SFwik'!wM7wfe5tb&t`#/edj{X^k nF3|ˋ[|ޯq;oϧ[W,E.s8 FB>Pԩ^'ar2`: gk ީ\#F^Ӷ/exitF[Ag8˰@ug J)1N}lu6gNN֝ʣqsik)|\'dʥNpD u|Q9XZl}A8yB< ;k8$v҃tQ)m}ϻyq<d>YEB >OcJlZs?cˌ<>Cx0t 4jIzi`1FZm"j=7bHOltqU4D!W~\7J(y^ͺ|>0vC8*HrvJđ8ig}KЕczz$hovޜg>#h{,1Ft3h#ss訣F,W ~\s1xb>Hf 0|YG,2 pcᕟ (7OA'k={|ɟ]vuP4B =H}/W.cuymsM}h h߱ؗK kE_?kTq8p+v3/U(e47 4Ȼx>0g8HEqCD[% 3<[hpIΉ5sBɳxO4h :V+, r FGXWyC?ҋy e#3քl!GZF#;":g|'t6(~yF3:k6]]WCi]{vc 5{оl |I##^Q'8̐J}cQ>FE]̉T6{ >oA{9RJ|f\뉮dA~{(۠M@lP)2gzeҴY<oY+^`tNrݑId+ CiH0RuˈSz%y5hDJ`I(寿y^-gn_y39n콬6X 8.%ܛ˔p ,@qh넳'mTWҥx FfxQ?}?`ΥoF}% ҧE!jWlXtlI 6{mwٹH]*х~9y"JqxdD n*|6`9{dL#%j_q+f4=I~^>Ld/]2>f1`:j9 2,I?E~-ΡYӌyiEcM$Y{eef{hl)ġF"Gٳ8Fa}r8b8q8PgqÐI#")A$n^յdVJR v^|߻{g?8 @_c21~x@x5Ԑi_483Ai '$\Ir,QH{S5 5kr&Mϕg(cbFyZJ|_iO6qoy)_扯`87YLo"F:޽'IM$'|&lm[,؎P*ًJc)rc] F.^Tc*PXm$q9JC'Ԁ?pY ix%H ̍eU6Xg[`յyM@EeYa7QOagZ䙇R8@K@Р:$x|1NDpc0g26l#%= gZ2J]2!gD*/."G[z̫G3oȊSj0X4r6u0K +4c#'Y2J:nZcGRSRNBcMV |m# H:}:K8clVc'tV# 薙0{tB:I|+ L8-a0XЕ5BkkΈA"'T:K-nT)=>1|x='J#QʨSa_J;;(9Nr8 Udʜ=Si#>+D.Nwd`mSIA_0bg 8aiAcL]ɱ7@q_G dc7m Ag C: ieFwRu;]&૲o2p{%-]{r-Fp[4:TAa螙̬ 0!j_rgFr{BF!YF^zԘ"Q*^kZ_Jfzt @mT}qyÖ[w~.ݸ4E|@*7o atf;k(_Q'/ G-74VY${ #J_d~k #PlGY*zUzrg4u1UA# BFE/RBL~]^zۚ>4 )&"=UJqA7|%R2`*$'>S7b_!x292z \,/*T'iN NGigTgV*,njɱ @bCXOL9ᅀ$amD7t0u* y',j1Ƶl|B@5z! IeD;51Ol yUgkt|||߶#߲r@pw #rǺgAr~P믾 `=7QPB EiИoqy7)O8xmϳQ@8DnDeʺ9uspp흣ks^FYgTUlPnpiX3Tr3縄Q>nF/Xh7+sXik{^VQpA#cx5`X6GkìmW1_X3Gג&|4< ̎|e;k?*h5טSbc*}AWcY4;;nݸl:(0rRTd `?)&jlbzz셅ŕ I~1*F<UlW:2(j<}  ?wzoyd?C1^rIW$'ʳHeP㌈؈f#8cQPb41:twc9He^K/Qa(:chYLHPY.l5jSZN ܔ4U9ZӁ XKgysSd<:N?v-JI;qCN$YY ѡBˆ)^6oo},c5 5k}8,KAU'疴[<:#4{M"+9s]AGOF!|tEbnz;4{nz$Dg3F*TUtcb,3< 8(.'%:Yz$t1NJgX% Gn|]8ʁ:[ lr&GXtdND,a^Xw)Y5>(F|3K--.əǀ>A3ϱv8t8vDtN?Z|2nʲ:zuvR| #d gϮfvdBs8xxegա="gd9x" ⎂[6%\cuG|& F0MHpsMvч{8X#7(rlS}70SV.2ԁg{1xw5٧)4(`' " }aԩ/@ڡ~GB9g:ۘ}Ub#7J<ۢ %:{!#9K;9j//}+-47q?D62IfV]b\aO.*㙍E}}u5M=yPNf?Wq ?nxpO\;(ƞc.N[8)FPyED!>p]?.vKΎq1Vߍ^ydKg9&"fxZ~q±ñLQ<ݽRxHm8P#$TI6lZR/QLEccc#1FNux4}#sP@L[~0uϏ9Źxɟ^`!Іc(^@Q٘Gݖv2zn Aps.}զSu4W"ͬ?~ ,ϡ%=({icc&'}(C`g H* M%U޽y-]&6Ã_y hI^K}߭y1"] DPns*R%Xĺ1AM o~M߼~>X;wի1(Ra< :p/":x_èk ;H:N ٴ07ΝOvU0Nc:y~Xfy-Y,WdM@Ŭ#M8cb4n, ᖴףq|f[(?D#rC}? [ƨJm3*TIǻϞ9!}(5w!1Jcݧ@o9+v (ޘC4G$~z6|cvu[_gk?>P",'KO"bT9]h]T9HuGyN9Kaki~tl6J[Y1'l!yY` 8?vsVI/oV2q䧂Vʏ5bc~*u Q /GtO~${:i|MM=8|YԔ劔60)}+O9"А݆BYþ |~10iY,ޕsDg<ZK^NuCr xgqD4R!1vhu( GmV%hQ5@1SCr8:fe_.̯MDgvZ9LMQ%] v0tjtn|/sNC:҇cx\>̳ifGoetE>[-ynoPZ]QP_VXǬ7;;d`{@S]ǔBȢ.CZSfpR|+qZ|CoAX3 Lƻ^D?aO_A4D?r',DfCYH BDXXCz0;N)P604|;AQ_=iH4V2KGZzQ( :Sgu<_\gzTsV,,m%J3\8 nQpۡq;7fkc-MU$r:3&+05pT 5#z:+i׈HgZG)ygȻ0%+ jzD3RE iT i@S9{bzӟI᏾G7P}b o}][Z &9X 'yk.\>|rJ*Yj]\ڈKbB P)S!d e;^y뜲QJ!= tUs Xd\^#;+GXF?RCާ)y3Co*W!:#YVZh?6ͅ-e}*go쓧BxxSAr@89o획lPI{gѶ 7S hcᝨ]|p\rn>d* \sqqgcJA>"߮E<8? gX;g 7D?#ѣdMO*Y(EԎS-{uT-LcOg!8^S<1C+EπD;ci]B-m^ ! Jw*Ǩ ^7"@ٗ2Pm':֩'rl(5vWP(KP[kQF\t+igxY |@:u4u'TB J$}K)=F$Ъu`1=Gl g39_q282R35x/;n]M#VZH:]ͻswS3z:BD4bic^f4Αm d]eI USf GrDw߻Zc8a P\k``_&*&4vNbS,!_D8/p!m}apq Ȝ Dgǹ*3FIk+ uf(Wvp_І]kGwZEܶư%JJ yp뼢'Ӧ'0LNc;G#= s|K}|\Ms!˜YF3Q XTtH^q4ۖ҆ 0+/0HM0x׵Sc##,0VK(}itSe{)6Np4l|H|t83}%{TznN=L{\6{V/)}6DAfv1Ā,?c{'OBA=ICcWO}HK!1☣t0ꒂc*[(/IaDݡX'5ޝPvaLH{?g~3=Zvz;4+xXZ$?@d T(=KW/WFaiZet~_ڛwoӗ.?AUpTP Iz d^hz?#X"5H͌qL~ {V1nm, }iiWW0m%}UTb6F5{~_hii~H^l>nnPAض!Ky欯a`..@R@2A N'#AI<ט4\6ㅧ3\y{ߟ?^ %o?ksT/FEt-5m.M?o,z"/Ǒ((`]ZC=Y[\wO\&`z8⮓_[8f +~Ct2ΈKtq<*q[{G=K>`l|SfZɥ6> eM tʜy1IsHϺo,pC㟼@8km#K{w?%49AAօag|F79>@zu)OeJ Ƴ& ENO#w,k?-'͒JסWW:2-"{ ɛP%lVSdp ـ)0ⶉA•gRg Ȟ6ux5UbP ޝ9.s C;O؎NĈ1W}jCc;On< 8˴I?"l2򽑅Zlg'ݣdޕMZYoCkdARgSݙQT7wȳH)1~@]K$O[]GЃg  Ǜq1JHF:'km35 ,ү(D<,e f{22{}EYYkF9'(W8LW,\$MJ:JuKe]ls~k4f=[uso\{S+2!agΛJ6A04}3)Tz,;:`* y 0lnX#R,mx6s8X?zTǙ6&x7#0 c} 5J5ng9KVl}Nci{}5`)YAPAZ7zs$+ž .43B$h8le* e@0Y\*״};:(e 1Qp6}Hk*:0F`i>8%߶yt=DcedM)pP^2~7E!C~ 4UZ"ezyǣ09B0qU@yoA5~nGݟO1J482^ ~8YM0ڮuT1w ˯RnF8Y/W0 ?Hw4~#(j]6џ0](q@0qWxEV5H|;!( x{Q A*.0VlMVڅ@3<6isz3FPL.^WL3zFɻhIFXBZIa4y˓&G'޺ӱZøZj|R A25X9 ptĴCPmIшkc"A4@w6L>yŅt5x:xpM3ZmڽCYR_浀xmi1tF|2l mSD`ކV-n!)닋%699D5Jc>#ø:LJ~7ckzVbȲM3AZ9QyK-@IDATI i~6gZ>u5CFJedg}^C,֡_P|hm{pRD1VSiAȅ8 M9sucOw1v2h5>ALGxKyuOwU }# r/g >bذVbINGT0, U Ct<d (K-sY6=9<8n);9:S ŜtG>#Vaz7!:lu',wukqJ);Vy@G`༳| LYouV)۲E{<:kg~4Z3Z:q;NkD2K7sqq:2*?uj`":ZeB"H 0s-g_:w)ou@PR7m/q,D[[r&Tv;ͱiC[ 8or`3>RYԆS΂]3Lݾ^Ù ]~B^Kߦ4 u?@<'Vbxy|_W7:ݸ|u=Kdsd XtTos\߿cS| ޓhԎ9ݘ(;(䞔9`ٞBCUbVO[3@>ƝN9*_'2n_Y9w:?+13C\iWK|o~)<牼Аq#0F b61(`/?_[jq-kQ_ =MIͯr.O`0P\mpN*\1{j֎T`mC9a81?6=믽Wi:'DEa굫DlF -NVRgI 1޴B3_GdV֥, 4غ R* v.uiSa~%B,䟀wgm' @BhHG˴PXM n_cB.PWI›)XF*wܧs ʈ^@zt7#uYuoXER( 41&9`Z#e OY_R%FDRa->#CÅxȫ1+Y+@!ew/Ԩ9.gN-\s8!;wۇ]*|bxh@\m>_d{g|-51sPX| )w.y?d~\?&Y E_}ts_;=vURcc UrTQ,.WezFɨ# KPG8>sx^H #P4tC4 _N!,YUX:Q(+["u {~Ы @KQzC=|W-]x>ɳ8_H @5%L{Q VHވf>j}ސ+4pۿb7K8VF1bc>wfya׽P#=,sctg\=dBu Ҍ$U rIE>Wz8[qmathc{QƲO9E`M4Kb/S]cyȰo#s3 '_'>_GCHghx%/{8)B:H`'SD> @^:uj"m*aC0P=(sct3k;}H}/&0mfw[7o6/ug pz,nڅyJ4e83 WQb\dH:v(SDv8(%la>csR0F@am7Q,Z?vWTh =Ed 8H]Zԩa+]޽#>txÃAi;Awݛf0"sbw=MK  l83=25V+Hd030UwįcXq7:@2l[Hy`pnmiku ] dWMȴ?::(*ƣEW3>UP6 lj[]Ts#CP&ZGGƍ{RFlBɄЋP_O˗ t O#  D 4ܼy3}s8Gy0ng(¬>՟j79"mFeѸ"F{QΞbnzK=n_xD?q3?8KrL^Zc/ SݧCeyt_>*Ky^CdmwcgbwU xmAuitz(d'\=J ـ.YP64I. b & j"N9ԝ⃂\D" _߫44c"rE譇v8%P>+T +(W@i(32mL7sA.C_UF$w |0]v.m:ug!`3NmJty>jkV#4)R*e#ӫBiss (Q Tkx*Xo CwYqU̱]8 ~ndRnj(5Jv-(;y< @ME印q_d)dޱo&ѕ7 ᕭOP((b$Ռ4LF2ys)sIJo( Ƭ蜮bsH×tTt;=4R$TDjf9ya&IB apƩf Y!̧Xpj&+C3a<0uQt5Dy;ij>S;gㅑ*Fs6o(QIo~iA.N^M5##p;;i(v>ac:Ե<-Ի]*raEzgZnϟ;g8MR!TbuK ._Wu~> r>=job4", =uTGB5 c+bN!: 7=R %ٯ0Vqq꫕Tb~zȻ7?Dkkxd_;yGmg|JyveFs4A&N5V=e2=S 쮟4Ft-9N0-p¶Mf#a{Xow.jȕ#AdHe\v\з~)'m*mMdɩ1 ߕt%q;y:;B )B,ݣsbij/ͨaf$8P-B.Ajq <$iP_#jD'c2$mU&AMd-";ɒ\wPj3^rQ'uvN̝ut9f}hbd֙W!*8_4fWx#C#qFi,M/̋d#@0ݩqdV20MJƐ士8?֦SgęAPTdt:X#d8)pS-޹>TYxaDQ0櫫+YRӕER!~# l6eCŇ#kx* Uyb=v ewa8LO/d>0 cu/YQ|p|cU4Xy#c+qqhx(mmw!aΩ;ͻ٩t㍗9¯}9MN2nx{UDr̥H$C[bL1lD?Ll؈"HsV6"ϫtC+p01vY: nKY!<*e%qXYgJ~r/[^jA)  f6`SA#^{݃^?܃WFǧaF:_"C_ɳgHOVQ5~z!x̲Quh~_;yEF⿂BZ3_BWgFU遾`2"YGF0zΜbN?٦%SɀXub&j\C0jY]:Fi'_sz!]OrjwaǪxm2EUsZF[E|fJnlҀqD ƪ&<5)kPnm0L5e!QuÀG6עBVԸG(hWEʖ5.w>5׿(), 2Fkx{`ڱR'TVX553 2^:ťd Rwg/"%LYw:HD4Q saW ,B(><ؠT(:RQA}w֝h8]J;2U!SKnY3@~Af]ӎZ%JZߩ" kW+ҹZBI3&gCWy>{ulwE:=cW`T>9zD{SO04ZNA,#ˌ>oV}D0xNPuuQG WY2ɥg8)XuF?];Jmj"b LۄtV~=nIuX$>Qk8FT2u|7Bf>O`4vTzY hT (2B]ZLy*O@ͬP}+v蘖c3NB5 #?ƥz%Z]k#J=nGW7"݁qh`OQwL(ڱP0f-lP]2k#:k: pI/^"o hwl\k_KMD RkwEw_z*QھmEV!L1LMx/F78=<^H"NB;]ܓԹ5JC:i'? )WXsActn"M~2".k@{4T( OPx>o 7;Omad6rRsF̕::z/7thOSO>pja3W R:7B{l}Dmm2{!HcFtJki=;"dݥߙ"_ñWu[ vę#)r  @:k=3X|}8 Pʁ]GAڏ; )KoquN70HANNT}{U;*q~۷n7".$ p@mgQZ'X.Ix~>FeJBbQaD`,ݟ10aSY%s'MMHu"8P_* \Pv6N"' Ṕ$ C@)pb=PN*kA\\yzO9C<{5r^dDFg&0dwY'ш85T<ī0GeY$iqf8qٶX[UZysk5224nBg~%}7~*YfX=.0ցx >ρ@`GUtwt7!v. @sԩ;őJcg")#*wF>y"8}ZQpeϿO5Ji]+KizQe=/9Ԍ3C Bٕi7yQJWtH%L\)'kpӫP4z1^[ K>FnVͥ5JD)ރs(EӈQ]YA{o% N{}%*h{D7̈ӋCeuhR"DIʃh(WYy3G2ܝ0p~FpbdH+O@/㞊|~{GuqאީL0"T#KAcfYtȴ/nh:ƫ C|N;Y~%xriB \+8Sy择2$O! h"˟>^QVK^xty᷹r/5Ԕft[(A+Ȯ6 ݧ}æCqjl-G>;K60ξo x\}~@,ìY'Ϻ_{DǾm-ձ"x&2Z;ugSwur$Y2{_YŒJV=X1SϽ`܇.am30fBOķ>2#+]x k?orߋ MsێaD>{Jy}f0E ol^4Npj3tuζ5Fy9_h4u,EЉ~A^3ⴅNsҲxEQ.O2M 37K0*ЙmD{o+kT|*"cFl% SN%30e6 ) 6v(G|gu&ѺR;1kF~d&T,` )n q)}TT:vnl(~ <?!}=c Nx7@M:Hp$ K4BA" @4bgӧ1e  ߍVۓb!5ldﳥ?  q`~$΅aW}` >(haYY:Z{RD3~܃deL s"# 9>Ѧ.svt>S[ N%۳W0xGHQmo s|އ^n/o>.Lu:UHq f8g/M8=šN|f~>zk{ o[zՆ!r3MOd h}x)] __p_I!ԓO!ĭ#Ю@FY<#.P1P bYge; {02Ƴ?{yОff B4VNж+l~$M\y l :`xz!""ZRS'p/g*E( R{HHQ Q[>kqj QXl MTqFjgdХګ 0Pz!GFAi_%G(lud0Jݘhv#4;7*l*D\[ֿ>3ezC`Нe|RָHsw{] M\}ea{ 7t.x l(P^b*ƧLwM vFȫH;7oJgfP5fVLZ>vVQssD/C1bj4ޝ!׿n rb흛򣴂CC1fr:G* t`G6Fo^x^6_*&#])ߙ!&c2cW0n+?Ir @rJ1$t>|'>8bs*t2C^Ǭgz7 /-sV:ax*]{{a;"h(~hxG)/j䴬uQh`eI;h)hsp7x ;m>`%#`=gtG6SATV)ج4pzeɐ6s4!l)K3fѓ>y|12!ÿ+[{ڈa֦KauuS@眐|e1Шm-hi#u,[:XƹЉ>M'NJ:+ch4\BîNt8#lr'}e0;[*!fY1wUS[àSȌDSG2qX{{^gN#Gs {znauwM%؝*"m!?o ֶQ0ܨQހ(` y/+iDwh 9{Bxʂ ="::JRYs,ֽwޏNy 5ه1+K}&C-ƾZ#~K0zr%2ޭH4~)md⢏)֘d+|!iտ3)?%x F8k#MFLT>_zH?X>%RmcNV٥J2[lL0j҉9|wJ@Mg(WE6Sk%>p.[wl+g1s @eKKze=eGbnw4Ƴh^fR^Pzc> <}Чa'E1 t<0v=;wh f =ёn;2rfe}Nkc \"k$Y䷴C{~qvyC |Eо%4{ٯe^"sp:˭NHo~ڨtn#M˱#cN#z7 )Tƣ~/? 'mU~2dCAѽL%OW[>8ϻ"ؗM:x辱ܳe n8 b gG![SL@IDATyχ7- [S)fd}CGLXq/xd7&:gpzGT1J e@r{Cݏ cf,/ǥ8 4l̸qo|FF]3N_} K%cWf~DXCT6~6W5yw q ~2re!88'BgVVnGKqK~N&UO/p!DgU32N)oPֻoptuqDe K[CV3~tL>8: ZDžLn *qAӵk. pM+iٗv8uvDWYwz0R!x֜uX"Hd⦦]!B C68y4fVݐ~k'o n姂m0z^CQRqo #,"؄A”%u*Q, JV&m 7`⹯FVugo X:5_,>ؖg ~w {< q`E4oɦr@≑O:T4iR +{ F6h Ҩr%ҧ.A ru<_C5J˶;ʙx8!7ݤq/mvƫF;VVg"4wҤ_ =X3Mu*1ƒR62|S]3zKuorE/ ^8 3^ *4:8&Tp5ģ(AѻiS@1.2O p>#G45JO_⟑SU-מPվ;x`@3Tq{ƏBAhԣccl]-K'9[Z76sf4A޺VNi?P^O~G9ɓٳ 'k3ҔMIx;\cOXwҟA y+Ɨe"oP=!8qKOh7Cc1jZ^ nz`lK./1O?„i;/2 t`ؼ@Po?097*4L}Օ% aymB퀀/Nk@ñ:1 k>\a|1ҚG;˾it*̑i0Adlccd f4.߸{7y.˻Ƶ7g޸GzE֢M3ӧ̍+{c'7=7>cƫuHO{~߭3&+ٗ#RQNʽt,Ers:{iU5x}+6mF[Mў~[vWqN\z-#YVM8[5ln`'AvGQgUn‘Sۣ])`58K ]TX'۝g)c} :vJ=pac 770~p0u n8XuʋֹNmE$Fcc )EHTq$Sُ4D{+߅c^O_= meƷ?{~N:Jc`dℕ7j RxN9o _e% &ҧ$t}jk.{E\ 5@WuBu)y-ʖn*.V{m++)Al҇?+kWYΑoݥ[G'7;vw`@Ltb/Y@.9ͣs6p ^GCPQ?Q@G0ǯ^z|STkoj2XiQ]ת+ LD}{ƌfu/&- g۷*g zH@|Viw/;-ԧoA km*+%M>ڙc\WV=ʑdrfXW`t_]Y/6pg<[JXRGȱ_`*P* 0^|)LׂS; r+5z.ó:3~.c*M"-(M- trv::<  NFDu{]8y3i׽ڊ3&YZap:Ɵ#`Nڱ:ZB\<8ZC+wKP/AGMun u2N8q;{^}˔<[ĝL' ,Ch4={X}('#ʽl 7/ j4c9N|U CD`uA0`fsˌhqzT*+%>51BlFy)r]W!tsΌ#w6M 2rLHG62Lf9-mGty/7_Pr[5Vѿ*7O M1<]"')qG'?ApHfV4θV%L#$ݼ6!𞄀„gyBNy&#͂G@Hc#ipAD!j $5_;ٛMi)^hXKhJŠBɮGx {ivW;ZGCEV0u:/d gLqWprzΝ>D{~׃A.ε3:5wogy2]T4#4X ">O~ߔm2Fi0p/Y+_g2tgC?OP9pV4c,*:-}z r"?c&ޢ+G=:yN_e/Uޖ[Y~MᦒLkJg{\{F<#H YUTT4w:(Z*P"9:5\80WVQQs8wFYwϯpUT3lkUhTBqޣR&YDQUo"T"PR!Mpθ!D8Cy!$ =R23YLVث00 ot&2`RxLË|煙 L1S!\]&بQ25|[Pwxvr)mXp^*gΞN_w.Α}$^2YsPR;SFi#WNӵ*{+'\Ov%p f8㶃ulitsN'F:C6-iG+~5pj°d1coJ#8A-;3gg&`C* 19d:oӶqqdqw`aN4f҂cV 1cM gy8N bo|o43uJpJP}cҞ4Ye^}uaV4+&[xwxj ֈ&/|.ky_^ AW({߇#Kuٝ*9>p>P^|0Y8߭^F#Y྇?^~0Cữ/_u׎;`P^;so-ʂ5Sw׫yc~m^嵷70]+ese]DYIyjwm2~+{# 22G[-pxNa+(38NzG:kdspi{$r$uxya27dB39Q6]V3S XTZi4䦼+n#f`'(7rΰuZ͚zՔZs\?qu֎I2N[eH:唛#f<Ȏ2;kh"y}EĻ{4{SEmUʠPOĖ.9VVNE7! ZQO&uNF4yQ\{ySޡڌ9 xEၬOt$KSZCWߟ[)W {e.{odF-@bs;᯾C_> Ώ%sʧ'@(e-TDPK|s:܆?2JC>C@2?BU]gԎ_Y71ʕV~dE/loX -sQ` h!zA&Sh;$GXDصs\ p>K3o dd[S3 ~nK+e9~/A㬧XL?D8JUhU?M()b4IPzSS ^S&G[YeZSIT䂲I} }wpɠhҝ6Z*Z8h3S #U3"[*IeK;McxLwdL䑞kLh7eݱ'|&|=p V0(!ѫGjRK2۟o*oCB%9mۺ 3b gyvyZJ?geێv tކ z ]$dǿX92O:4=\҈)SH:UWK wLj5 AIF UA"D*R"ŨWGeϮPA46cMc^r Oób$i!3DAWD;|돖/韕)v AN3ޅc-ѶwGy_kJ{t-#ET_+Ll<}.])?OllfS*5;wyδ`(g51Pm6 wFApY S 8&8hVeY&WH U\ faBq ]Go)3 :5ni7A#yUUKz, ,oVǎU$#O>2DB(ËPj,Qs6hԾx9dnﯖS'O{V2CVxV%hg(Fs]ƒ}Oz0A#`1uB3ʕ4CZ/lYsnT2Eҿ0ĞU{Q J$#<k^sqı'^ l:u&+ۇʒiڷg*ڇ*7 J%™T z-OO4(awڶ&"Wk47ۖq2hJ`=TlW>Ewi$1H'y{(ߏRW!w Q~)}@=Qiy:ds4D?ZԤ":煉<X.kqרbPmH &Vjh0u=#f> Gq SHYrT%N%6F5E(߳q3f!\uZJTSdM:[)uܽaV|^kv.L̘NڳwiihjW5ƐrwUqIյѬLT:_Kq=uM_;sǘiॼffzᷲdbF̳-3/PWzIŪ~#YG/iw䯋57Ͳ^d&_mc^t<ާ3.mhS8;ɪs S#} ^} HeMtm:1M]/}dlgGp׌ñKnymBG/[[%dePQPMhb,54STޕKB}E*NnΉnqna ⅈJ?Ч4&_R~0l=X`͞A&Z _>ZGi_޺y p#x׌lcn|2kxoky}ƮZq%þ\<9FåurAƙ'LV1&dk>iG!bl*>OڣM#6x36fO ֶƙϽtFY@hFʃȀک[| ~.n>ųX'1bmClWƼ :U<]gzgQqяN^^ӽQqjtV6ϕf8m ,)ίc} 56@[Gz˲{2c}.rl8&ssM-z_e?tv੓QD^ԡQI[9Vn8tiw gqL<w}8O99XQ*yMp&,:҅7dB*5 $6 fZQ< ?lADoyZΌb34k N6c\-##aL hy3貝?s%HRi97o ,SX^,8=Nm~"O΀mۉdBVYfB%w*#;~ȠssoݻrDG#ꞦC>Fި--s}?7SOrr!蠥 Q>™W(g *}uĨD!3}okDyg˗eJUR4 5r阑tTԹ)V]C- -N3D ctH.k8(1(YʷF7-nd1D*Ve`Zչtc 2p"yPmcP-?eu@ZWƧ^pq$J CF%Y0!p"ܔP2s(ubܱPIUPS,8unK2e 3} "-g8G˱;x7Ne= .DywkȨJ,!0uKҦ.ià>l? !u.][xhFܗƨC_i2|\FS?+H[ff 4h,.t- N-Gأ_oϥ4g?ylg} ڳ{G$`g~7bw?EJf/X\fPfݼw  3@Sͮ_*M'ANqq+-l+8Jء/u;452|DU^ )|T"I ,Dڵ,SӗV}lZu( M9H wǨ^t乧KqCxrKg[d/+p>/elxkD!Kwl>sK NȏNl:yM [8Cp*7ބ]?}}6r2:;;Ku.jWbyn"2_'<%1Hݏ#tg6WnjApzj*GK٦+E*nWWE^%*fu66E_.*QV=Wo%OYu(,m̎{L@A_IHKm祐Th@jGqV9vQ;`2G ` žJk馒.BB}2YH8Z NN`};P[cU'Wٯ P&*dbsK`30]QZ9^-v{NmeruDM(k6hSXoP07[Zփ@M|mB@qKX{X+vB}ut݇瀥_X$[9v*8uTO7Nw-k Ec߰L9seę2\a.ipda-ԍt>}of.{t2Z!@D^)],  Ii0@͐s8[ "Q1*;?JC7 Ł;WP ȰTΰ_{Ⱥ`kʟk@Kl60Y~CkjOΚ3/~u=AtޞeX ՇlcK({FV'aFO',KL:%hk @u#`"8M+{6||rwe,:-ymBiV6͜})T $ U4'cy",hNz&i= YFJ@zekMVC,_ǎr"gØ]q4*;AJwΌgaěC>L7Mad#ݠ7ܨ#Cz}oWMZnܶz,BI;4熯㨖L pBOH{RUS8VgV~T<{tLidxS$ܢיJ~r iF4ƿ -s{:#/X 0Sϸ8M%bJgT"(|=KxmPxt_1t ~1lgLZE3]{r!+*a0$R98/D~x-:{T-ooP;u*D2KQ|}v'3uj -qXkmqOX-DuTsҩ/&ͽ#*#Gc ̈9%heyVp2&3=g}ۯSӸnY9MY/xRW}2G.a8{;TRѭ lPf]0?˃lb21VNGk":h\b53t 3J_ڴ f;qi>u~b4~1Nd+cv1N& љX2f/eeu`s)d;p!ޫKQ)<[u^ОރL%fUϬ\2ǎT~rmryil e]9qNp|ޠbΖn& Cdr3aVZ[m u3`Y΀KѠ99uy_9{@5 ݃Df6qP<7g=ov8,3V]92MLi*eXS9p'#PW/OMGa }w k7rfe|5O#౥uבp~KNgUs?t\_O.0Ypi?{}#-r/JC=Y&%wK WڈQֆY'<\vܿN5bRVKZGy i8{|[]bk'CtlTR~g*ש@}D=k#3oi6pPuD6T'r/2B"|'iSq }oʜkv·gi&LW.Jð!ЀcKV$N(( %09Fe$ɺ) iJwhx@@}n89qIo z,F^+K0N.޻9y~YY{v`OedBt -6zDjB+KZdwřf;OEv ŭ Y2j?T쿥toBρor[Ye5mߩpLrjSf\~它^q*)WxM[**ba.r#Hp$pUϟ8%(A(0="7+ܚy=ӡ8EA_9a@X}wz{ɨgf>2thf,#l*XUA'xl/_dܰFXq}WKî4ZhP-0b職tf\9d~<{!<`k+ϫJGF@A>-v`(>]1P^bV# FiI5d xeg,]94kLuO4IW~BWG6~4ƟZ B:`1q: ՙOcauǑg|;__|pt/ve WO :CCAI#(]R?5L#=[mMyt~,T)@߭LU{bGT)_y^p 9=Yx/hm#h>yCϗ{g} }8 pސUdzX67O[256uty5xWp$/5U_+ʍ$lVM&{&|fP*|`}Nn#G67|q^J*cP&֗p))Y*]InItm9VIR.I6V׆SQ݃GY8ͬב̱ZܫWpi mphtdLe[hiIb"Uԙa羫-~yN\CRgf[*JqXPΡ꿞MO-RpkX1Fsf$[y;_uqC:km`TC}x0Xm72?U/rLuvX22eM ɘŅʰDԴNoM2zWeX Vcifl6Wc3u^V\Hcڇ/_Y&iE'}ЁNmѫMBgLF"_nAIڵ:h +kY˚k@{ҫǥӯvAV;]<u;Vnw %kCr- \0[V"s#kD x9jmt@f;DiU >Y̫/F J9DQT\$pǀMm=tuifr 8S=0ҿ|G(ӇȼaAޫ"K>s,|s0#$[6ȻtbԆv~ˍ]ؒg](_6y ^Kj %8҅m)軇IcQ:3Ͻڡ/9#>ҵMUloi{ *8.{rx{Xj puRt s^y {ʞݻ`2F}9,@i(}^KSdx|Oplt6Źjtt9srȟ(kUr!A79xx)0x VX×cR ְ=qJTKGux^GhH7%`Ȳ1`@rj-QzbFx m!n`z?W3 Yu{A>p\=,&h(J]w6gȀQִf$Lp}7ZoQw2r :R,Qe3\у9:33( o! Z$]W^𣶼O#2'̏٣\dP666PvSYrp'^tfмmfv3YVe|iL&x5Y0pq'򙁋-i3PW$ 1/)@j!zpٴ{r|~v~سSYnx ӡf2:ƍq{xmwov .q˟MRe׻j?k p: 0Jc;}|Gqfd:ˆǩ89zKWdyd rOkҌ'N/?x~S8h@{ˍ}g4i6**N,Qc}{]6o'ǥřCʦVݲomtE`/e_9T]ZZ h}ЀЈغУg/zU . fǪ egc:t6*ߋU']CޣmEYݹ,/,ү9 mXنU1!]CtRz+cwnާ~ͣNh:3w7c_pAKڧ%esЮ"l{+znՏ{I/jil\:-;Q<͜j3fy=LM", n:(9"17e(aeҀ'W-O2bKgN M*0)l@m|\f 6Nʣk[$~=\;aM< ,øL цv93ݝTI8JbaaH LRvT7!aq]Qc~QZB@.^,$㚣=)6TyֱHGL$|iTPļWIrn~`ƌyɨ% Qn5:ߚ39Tp-K1  tr`rFݗxet25&, \ҪQ@Hn4GqXd7)ДE!T@l0]!i7D,Ќ~ɂa>9'C|f,A!L `7M =Ȭ9z©<|虲[{G>QEA扎;vXygZ)| 2ӱ@ԕrV RkJ+__,)H_2)%zԩҭyG;&zk?/g{k\ OMUKC/N5ixXÒtsȑrTAA$tF*)*(5*: m9BLJF:ٿfIGd?BmI+*} L pA; V*"|A|KT<H8ÏR{ї [6u}|ޅx$2^FC~8Sxl.8=ZP=Nv7a8t~27wO~:??E'j6I<*_Q9To/|~ДAҺ%@O$QOyrsVPXmIq>3(*:t`cvڀJToH0LloUv)f+׹cͶTX%6?*&>~T4g*F}M1cMAc/ ;rb*c0qюBƹ:nEx*sfpG)fJdcp7u0 3Y{pT[pu25-xw]b dhf&}m?xǓ ~Wo ߡ4$ͺGXmkmE=Cy{գ!3 7HcZqh5hl#HuG${i8̇Wy2WÏgƯسw_~ko߽Gu_}+l}@# nV?Uδ+7ѽ!*kwRչcpsA~7r%؜jIr%1r_: .`t2B>;/z5~)4p3o6>/k?\ƱHS^y+ދm˽ޔo5)W"x nZѯgq>œf2cWN$f|V^&|نi˞AqI xvpvppxr$C:С$&G]ϔ]fe [8Qﬤ~/CsB\}c E0F앪[T:uF&q- lO8W[,7=M%O6|:{8=A^/X#u6ʯ/Ž KHg+)P`"ps K5iʑK8fyD6t䞞^ƸP.b{[7̠vQ0Vt_҆qok*-)C&o\_m"y|ue3bcc#,ѿ-00TeȔyJvNM'j;+" uJ+7{T4%$,Z>C4(K8`c降 k3N Mx#ߣ)kOϚ}.9Ԟaoj#`MS||׎rlKDbz5 A`Կu$ŷT-w&*gqr2 >ǓDt%Ǫ1m׌Zm 6EEG?nL(4V &! F73F wlg:Mv$)wQ]X%ZNҐ64[ux{Z!{o*?F:I_"C9'S[_^uczƚ$1YU VX'lA*G+8vu^|Aڨ7kUqړDc~󄋁(VNbˣ!ab&`o#];U[?:\ʝͬ-~Or!mvUkjV,beD2]D,ڶs/ )LC!v\ٌ˗^:RpBQu9`1&dt(!Fy$J6ΒBD)0-;FdhVXшꔓ4V_MWQ0‘?r`rg RC?P3 p- m^x! /F:Qr.ecWbxk648: nqg4+d+t@_Dy?Yߣc_ )- _}2K{Yƨ[گ2?||*,sN0rO?ܿ-C[#5p(9W0;, u<TPO8im Wi2(8fw i&}-eYz.WiB!p4{Gh$Y/>T:Tz8r/|Ӕ-ǎ-/d2M#36qz=W>]%l e;Jq/`Bg^i9cL;Y#䤍9ʰQ>|_|Q>}æM8ֶr "߇؃˩cǩC2⼀@GCӄ/kz3gƅ?qi .ً?;3M 6$'XHX)И bJ+BNfd"I=#mؒvTЗlksyY_qqx5^xF= 4x.Rgp`ؤ!sXXS-c?*Ei`.dW/JVՐ&"$#tׄo@$S"9q &N\eQ^e~]E<<'cUZƥ aCM+t4,u _iC~3C*_2Ag?,h/|s?|~NẾ?yp{y e嵍"=ym2(WGWuڏ6<8locyZk4-v\?6iƽLǔmG& Bb9e!w|CkH[e֭V/7s;LYvq{^@|}0ڞdSz-:{ƭe ;U(*4^v':*kMtə׻{oNl3eێ`+d_Xi~ׅÍ.fZ~cSn[wTFMKuN!-:%]VMH`Y?uc{p2*kCȤ'v]t\OԅXCVf"K'=L+de{0  V֧Q5I^nx䕬E N%/b-:Б秐;zIB& s~©sqE6RU&YC_] ]=&gp{zg{7q+4,33_K0w# \{t$d9MTRٮ)Oj L/ΚB݌$?6gc=nz5lHa* .K0ɂBP!q9v3ʝya2v7|9ZXXL_䫺ccg]'eO̱,{3#88GDMCfuxlGW;LFڨJ#u>,,![xS- Z2yzdNfxLB zN':`b<:1\ٍۜ-R4l|tJTZS>WuXӮNuqbPhmz:H[dUxj&HzI3ap+? .-p;Y'á t4;,s9MԪyfW9lݮ1?)Ú{D0=8\yW.~xxnp@vc`xn2 o6153/aL}^$xcLx،tۦ4'MFgŖL߱V Zn Y, ,3va Ieo`scw Ra WAI񂌋,LZ (ؖ)b*)Dg"H Bӎ 7yDcJ fi)gtd[Ll a!S}Ү1C#slo 00 ;\d/n <+SW y~cd_'tR\Ҥ_260 dRʢ8q?~:Pd*F)nf)j^2bu JO/P,S\Inمʋ(49knN=x^ (S{/v9_!O|w̭Ys!4^a@r {t;C:hНİ2㾥i^ 3k8Tyl:<xӈC ,F}!Bx>!ܢ= 8dFX5i3g-X+2Ҭ 悻:k &Wv#Suə2IM:*]DUa}葟+{_Jo?Op|y,|tϾEJOvZv#EJ oN6'0s%,M._ K$ }+5J?sæې}OmcfzlݱwTuqr?9 2$w'\|v7"[aRǐA2z뾭%LJ80S81dOkG0:& :?±(e<{rJ?{KdRcyXҗsccw\̚ XK@@:׍x>bmGs\Li+䢏5a>r]@aUq@*΋A "RB,X qLX|LIܒxo%϶%f:1@e[ ͱ-KjLe dT^8~xܳe:v3@Nq*=Ϳ+WN|7߃z$43 Lz'2?vZ.eܳ==vcYoNי+g,ǜ1v G0wX]T8ۚg&c00#` } ( gN#|}׭A΁u>/>Y1JΏ{8Eg^>T^~8 ޽|Kq$ڜ_ " /?K0֞P}:ufo-49t(7A5ĀYX^ X29=CyY={{t̃$NTx:+-M NuyմLBt^:p%~z9,ՑZeT{$*vA|PMo/.Z"|ػD\KBUԟ*2#1(YVsy ?Ĕ)wޫ=LbCt F|,OoWd~B.fF^;^:L AIm>L@ ' :?.m!V( >&(-7C^GNjnUSZ-PA?*H6 x`qfcg9sr=7籺A]."1h٩:;O[9]Չd7($rF40ٞe0bO~^,e]IrlU]9[U{^G&GWjVս}sgN|Sk#gfTqOgM0Ư!"+L30Lm۰~V#*a l'Ly(a`F짯K;23i U H޷UoO> 8~ge@@eT!2qkٳTrN.MR~:c!mnlW+1 ˟ҭ$r%ʤt cʷ.Z$8gS;s?>;ӟWbfV+}O!tL [;>_a?e"'z0Jq_T=*j;k#X46es[Ҋ|e XH!j*d tnO3*+mãqRE5 x[Nq 6z2d¯NcieE\y%/ߋEH`v_:@AaE-|LuFo$NB8smp1f= OdC:%j&~Rafy_ǐk ]íI UP Bc}QVJ^iM]5M[裴uT&{ D}P4 %3 Tת,a@B+: pPryA@ag4S=3T!7N[يˬkϪ#ٿ55whG.U$ IY*48N>w[^QM/|ϱ/9fοPD|;7 6[:oj7a=c~2dxe[e7u`mOF%tCo*t k;7w ?kl>  oGWx;YyY#їSXe+ޭ 9 ؀dvQ+ǩq&>#m7Ihä xgM8: edMqӳ nSǂ8/,6G{y3cY$q{d X ,V{M1ītɮ"5Ë^,n8ŷ[xOy7|uE4 {]z 'V"2O\}2ƇMN3"Ƒyp1kK slAG[Ҏ ^{@flUn:t{%lG_ 3VuU(_ E6(I |QGRV[[8 QYZE b3ף*`ǧl 2RX40.@lxVVɏ|Z*/D9O C&jHd]jlkL6@d֠HM7H±?Fu/SJp-r;z֠SWYiW+[pwV*pTAץݴB'4C Й뀵Gfl}v- B*fNv)P/?|Vn8@r=ϗפ\'[$"uvsi/atvqM%IѬ\ǬK >F;Z*OR3/tW8be^97ck'Ff,Q6dnl[jܠ]>1 @c![B~E7r\kY={4c]͵Lcgyrˤlg [ '8Bћ&5StRI@P0v*]ԂM'[q7!8_kQ3B>s {yf-TLi >v;&u#hF_M(_L`7lg{kHc-T?UM0r?u&}*[m&`} !#W,7]O:M|0,ZAc$0gDLz=C+MrL@-ڑҷ4+ >.lbm+ׁ{-FvCIz J 8S^vkCP$LQVp~%!e,:K8VVȀ8O>!o!{a|aM}(B"H#DfPV87. "gQ6>riJb4D2[xbR.ց: K u3G|ʔQVG܅Җw|E y|P|tG!~c-Sq:Ah:u,4^ü YQ:C8G+/tɠ|Qę+2+]L2aEOW^MeRA}7҅ǯPXr=]>qVw\g=}%|t5YuI 6Wo{2΀cFVO>9|.|-|\K&yN#D0+C(6 l ~֢4y̵U/L :3d!~y6kd&cdtf؏{_'˞A>*Wa nxZ]=OڅϦ/|,;ZAWq;72a|$]?Kپʫ2-7^'45ti0xV'|hz h ޙHYq|Tumkiriҳ~C+s< Kkx!Zה7H 3iWC*-;3r87LN# zyT3r5x%l[?s%@a@IDAT*ҩϦ3,UQ찋50C~[$%܍d'٧_#IKc洛79Α4˩$]x |H+bQtS3rR`ěߠ"t/ӗӊRe$罽O(>lMy4plx%-YP{ykMҘs=OI6}< 7򹣝47nZ1rICW0 A0{?ͽTw6Xۼs>GA5-̌7jwSsO=.| ̠^N]zNKMCi/8mӲ-}{aHK'.=)1L&tTŏrts0/&M#QrΤ2@}[iw5dPc#ϤӗKeۜoK3$LxsmI&W}9p>,> A勩Ae_԰fjF?[8TK+_FF寧(7 X򭴼8 8e ζCt 5z|eyi?s ,?3ms{#(V1 (nI$RZ&ySm7%h(Q 7W(![NScwC'no?OwQ& \G@ $wсobjmfcl碂x)I}7_3J#$yj0U7u-tK=R=XYUI}S^on}CO8Fua;FU* IV:7!g7"Πj;CTVGSדkp+*#;bYźCp@6 8үڤ 5oo ;JA>J~ ձ_1>4qp@E5AL꠾|qjdנhV؂`Aε+ 6#~jo?%ѥ ` ι&+ĮB4s;b|2V𻶡A}k~Cߡ02eM"/ɴ>쪠._:%;S0):8O) n<6vn$,o1Iki/,-Q4G5}iiܩIrӽ5pR`u]Bq]}^dvvj'KQ[(!~HpݗΪ:4|-SHɂNazF 7k]t- ;33Y`qQ_4M a/mjh̄?4&wvK%:j7mn '|?Q=m;{Rv ϵ -uBzv,dFZMb#gkYG:I]d%QF 7>0I4p7H$wv? ΐ'X:R sƧJ}HdclтâyZ  {"cAy.{n[5y*E1rt?u53)a'yqv %瓭wdp+rd" &#4C [g0MKfeS*i:J F<35lvA֎'˗`0CQ5Dۯ %ך^ˬ_Aq2OO߿mHgtBUBDVSy |P%,G܍HlC(xn֦0Cg*-Q1uXٶKƛ٢*dr! ZTޕ+d0q2ѬA8֚I$pI$b^/=!kYY% T O S?1?Qf ;*s( WH%۞轏6NhN~*~:klYf "&Y)|Nh8J^zpBf'p҅b :; "~ j/Jk??ҳ=Gg;_忉'#Zr,`efhs_CPFFE%t>Eļ'~qBb(a _x\'Z{[P 31 ..H4iHgAv1APxs ͅy~P[/^86ib|,`,4+?f Ȓ܎ii11;g>gwOpG=t"˯Ml097aQx>Ac&T rrpHeA+H9!M<|,mh@\ ]evP {zm*t'_zbC}2c1[PU 93Rrv[j.c:vt>$qI< sfT=42J"z3hn~:Z{o"{ޛ?Q86ş5*'nv7pS!\_V-,.Nln,RxzéM:9o8 Ckt)#[H‘YRK—%J$4V xXUcPhq2ܺT"\0z:&7{~kUUޫ`\(i+)-(5Io5Z4@Ow^F \>}>U^{*UxIsk=]~Rd6s n."p'XTC9 СxlO5ᕍWZ޹Okth)Aưy "#{ \d;՛G_"$Y$o;l 84 j Kk33x8\g(@ Q9ZN+ $<`ی!+RW[=4A&tc#ʹ^*Бs\k~_AP׀ZK5$cۀ#Eew[LX:*/ /N05n^f׵) &+kj[oOJ|tOСRNX%ol^dBm}YoIMؓz&Y›s8Kit-A3su4vVWzNE~sY_/ LN8?~MO QI׷]ANЮvl& @2یV˿v.ԮބgU JHM}.OKu\+(9\5Y ֦q&=/dv(֗`w,^X&讓 7pD{~2s6)^@ǽ ~`/ t(0нr>p<ag󺻺 1KBi8\T܌otu,ud P%QeD0>nalF/!w @fڹnq&+!<4Vɠw=YVKͣ#6!@V8LX?J8oN LmgX/[z|2VC I/*֪U*N2s#[aЫ NΘw*>tj2Be)^G|أ#|HP( X23gfH`Lr㺎`=_:U@URh?w6`;ȘP8 ƔIq>7  exTF7b0j( -bJ4XF^'V"m!-Yʯ2^gN.\,*Gu͵JҾ{ ,#e]g CY0ۋ5\g!qqo\@)YFԯ„/ln6pKJF Zp}@R|d8nFa{gҧ>q~T@[k@RCJYvY5.кF xՔE:|w2ٍI]xX#Hߕi*[q@'8#~}} GRE벵*5梇\sWn"KpQW/\^4ZIAf$KL ' vRgwfn_KvY ]C`Kr;|$s/3=NYU*Om 辔lf' _>o5d o@ Y\wCVhotV(w*h%IO\z:]Si9:o G;i%h "IЊs8s =P &p d[ zVE ,Ms bq{4mt! if|HKih؅xfG͹h"o:aٚs'c,ݳԅ28= ҄{OP?wJB?WB6gNOЍ:N3! %ΉW#8Ԕ<]x+"t{.p]{6!og\Kq$ܟ-lYRst>?`ୂSW˰0QzXnP!ȡR5>q@{ԳϦ~s t$T-..Cš#j!x.]y*`S{Y_[ʏGgp["U?IY/jԚh^ERzn|_A?lsN@lAڸtoG8m}me(R_o;ɴ g-pγ -$6y% +˛8c|D T$\k@9wŵkSV@og?*Q]wgyˏ[gb0ڏë6\A:Ri,`t1իTg^(2 _RC{7v 0 ֭ci7u4tE'@!5WWH׾AL <gu1olʐE| _'nD@WƵז{S½J˩bk+=ZA}&pYZ:*~9ReOJ?_3"[TCϰx'-~-[! -~ QZ}[i /ujt-x8 svnTVow SOMžL,ߡ}D "iqAktn;6'{oxHە_4@5TP[FΔ6q=ߌ@3^sq]$6ҵFOp7sf3`6MVQ7v 4FUzww5h?~xp l\cnp[Jc ܱem-  >Wm|1:NXg-OpQ-$8wCMSX~oųUpŪ>\LI1ȣNVSSiiy`eФڃ>gu0S Rt|a-yc4!&1/ a-EhuAbmw sj ]3kwuZ&Hڠ ҙ]>F $f$z l8SWթc)BOzbŠnv#2T{䌵[b̫i{4bⷺ, d^Xm'np&x/Dcc m}Yf"iEu`"sD$NH4}Ҷ/mn݉ )`߭gVް-4[ P=]§u?-dS>8"~5lykV 3-Xccvguӱ]Q' [`$/j'}KDr=:Ci'LRrUh2%VS W!Wܻv؆] b"2Hvo8* \ ?Q5o;vڅO)"S$mTakOh_J&0KG*$)3V;uϗ/ٵ+ߖ熯 >u:]*=)7)yd'ba צ?W믾dw}\UȖc(8Dĉ~(vU,LAF>W(4®30\Cz*dx ɇK)")AA htt !2&Wb^#aVɚ)䢲*̀flJٌ%  2}Dpn@haHLFvkpp EF*16(%%K߻{;ݻNZ!sA8>U ΂p ~P|tGM!}i8cB_?KHy啟f 7[ Ơ^00` ar"sי%d%9ӒkeRҸ1msmϺJ k׮GϹ}cT~Ao_;R! KR56҇]%w*L=ƒv8#9#%c bmCuIbV jr\ _ϸ&h㛟;U5(TK&GyK>S_aD5K-e_G@@!Z4 N/1C 5lPiQ |Fjdc 7p8u@_/c"c%t =G{ts}7֬j`VagϞK;2z Z12c`)h]7F )#wubw7]xKl0 ֥~o%aOteo7b- :/R#=ϋkyIYU V$`4L?s ܻG >WV"?9&O`fq3~j"ڠ q pc}p9 ,>'  ']oנqA>~5Aw}v8Yt.h$Cs'yڌ g!m0Ԭ\5`+\p2LMEA-#9gNgFΥǮ>H~TDA~^,5 m{$y3pH8JgҀNMԞsVS WḰ%C^u\?>8{*C 驏Ϧy*uxQ%ZΉcMFXZ lg[ r u|Y4#MQʡ1q׹nV[HPVyd r[N1AyGI2L6Tº 㝓~ҫa=OV_#ZGm8E PM vЩêG=͗xx\v:Uْ2GVa2q5C76V&HGnJUIc:XL4x~P'q_P\ '%ҖxhusU*f- .//UFnz/ݿ{7u8Rõ@ S7 U&+@K8xw Cl~#O?y `UMSi-ëT#gN:> Npir@@&m5p޹GbG:L5gY*.8?[,ͧ~ BU5>EhZB^cF:;< w# PU# SY}鉧? e3Fu%T O^}:]y9a^zx6a; VK7Shn%9A>37:H`[DA4= s =Tok: {I>=$OMkq&(*3S8o>>J-G۴őNz#s9˜7! 2Kz $x/t4\SN&_/aҴU5O^A7B*+[M7(xL20te^|eUcݭiArHj:mXW'^60m%|Tv&Hc.t1Zuß'ތs73'YwMjj0m~M8ܡҵLn8lO+ed28-ڱv0}tCvCَRY0đ$ȧ N :B"efz{Va{5`I!-,QJ.|`ùtx^`!6[b@{`{pOR9詽Tr޹ /?\Sgkw%n56r|kuUc$xV7i#q m-4` \X*_WCTnf=̾X/;ct_~{C}M+ YyAή/:f :4y-Ehurn&,pvoE&gC4.]8ѣ!b2|&,H3m4r:L p6IWZ_'ɞgWV2ƚ$t7J3w6 72pU8lD7|TgmV|șxfŢq9WÙOwS7LElcMh6i0m1}!>JNqUV٦Q]c|6X2gmr{6B :=JRPI|ʗxs_BW\t8Ȏt]6ueSvf4A`ρh$Dj|Pڧ-Sa7;qXʬMd /\@3]tVt$*lF)ʥ险o}?m,ZǑq}:uq=*3yޣ#}@@LSeUδ3c$(cNZY.@`z^{⯿Ã$` `&Daߢy8ڏJ2z: Idfn>z'^VfiJ:}GG0!F#MW/C*}d-V0}??"uZ#|2]KskQuxbİ2QjqDYs Yus4\!})(N"r$/_5}_,sD"AѼ3b 2 r^^ZΗ54{2aTGID,F1`$2N=y_0=b~ =)?L B)A!pӀXw-'@J.t7W1LDd=8A '҃vȨVT9pl4dix\,ȊUd}TO5wy`oθB}%#EߪAaeѱ-2!Vd0|Wvil 'O#'")%بA; _^c`=uy&&(YU8" #3}݊Q5z| 9_ k+Ðb>3{!w>:1hiӣZu [5C.-3: g">>3Kx^|>3ܧlQU= l UXddoٱ‹s C2YŶyfXz~:ɱ\e:>2Q\V;B޵gO?Iz{igyQ!SF\!)@^O8i8„hMYF&7d_vkņ<8!վdCFQ'd]/hCLu`YE)gBfحޜg%PӞ$$L,oU𬹊~e ]ڴ=E]W`gwT2X@:NT;▯ch^~,L]St{k1;jӪhu az/N΍473_ VևYD%NO5hI M~"YcY {kƿњDS#1DBȢm[-ξ}DXq6o˹M,YU_tpMM{0;& wsHKwQ ?Av/ՑW-N*gJ_}b*\x]NuXϥaP-"QE?܄<%P*_X`/%M{ٿA޹N? ۠S k]_bG[C8XUlu ]@k/x{%i9TSvzL*t-_5YVQQgE҅.,}ltڤG..vHz:[{U&=$ nYF +M.c9{>J%1 }"*7-*l `S[D!T;J0S9\d[//o'w2R;gq07@83u;;XUCq`X#ptQ6Ry $f8v n쥶n Yx9in{mpgBz<1YG49?ޢtGv NOO-G[/ T$*o7#Q7E֓taT\vmP[ @e:W' vf1(,hQL 5yMg{JI/]#&935!N޳9VX~P]0=govrX5Nh]p'%,[']y_+ߑ-x&)dyzq ՜H!*68mfz#W KX"|Apa㜇kxF1(|v8=|8a?:6ݾ>*Xcl mDmUXTK$ۜB(f* Yn :U>)~m .)LI"ck_H?`7M8E@9.Is=}&;NH:vF$ЈLT3Q_ܘy ^fkHXi̓Q$&˗.e(:7HYWBwاc\І\V .G_+]qy$7E1Mܕ &ݝJw)R=  37[n:6yWf[q4Jt>hLk t 1ZpNLObQQe /jHAdbf0L߽H$*ػUE# Vhʛl^@riB*aZTT,sdB"0^ 6TH}S0FRS |pA'Cdluga4cL) :~9YRSg4u}j̍{ n@,ȟ/ /~d#QQT2AJyNV)Vg0Qb\.1w2z6CfyiNk0  ,3%B2N]|(|w3ai\&Ye]JYѱOuLr~QhZ#FfM;8T֠ `Dƫ$Mħ2[Y=Iy,{<~! }y*M3-zPP~x}<7jJoϫ>޻~ }dA-qNSX}1tR , k=3S g av, x&3I`'Nno;z #@:mW\hL5-qmoL6`bPDW-8HUz״aYk2mmVWyei/=n0U! e@!,!ǀN!?} D+ 30/U'FAw&:p=26~hjt64T$٧.3XkN}ЛxϋZaBmv9T Z],lf0} fijE"|w؍+##\J8zkٿVGO|u"ofޯ~3k$9VpT$щxIméޜ\I{G^өCtvp$N+w4w_a,16h@ ]T<4`#u=g\@IDAT58ziR6@99::ϮV/lҸCU׌J`LQ+GtD|9Lx+UL$5y˒W6z[B<FۗpGsJ :Ƭ`c3=!\%\OGǵhAk-Z l6-Gnx?&PWb}o_/^ Bgحw&q]7E'bEI\hrugoiϼC5%pv N.????W<nz_xџ#WA`';4Ќ|jkH?贙U7 =V]tWD@* }ܭQՊXA !^p Uwhe̵Vw1 ^Woy h+mwM#?"jk: mᘩWN>'ZSȔ-]?N28ЃU`L%c l믾M{X͵5E__IXUV*zmBiG|07qCt.tUTuvemSjཝ2=7֗D"x@0|~YŚqolGL'ە{RcY[L ng);JS|hSioe:Q}ǻ*3cE>(-Ƞ;Yj[:hg`˓a[l{Jr$ %,h'a*1]`h^>_u޾z#& ֩aK{܀ek uzt٩EYCb;M8vuٛ#Gg} Mo[/v?:U9`)Hd7^+ߩe?eU0 QVܥW;(i ,OAg"PoW+x "QI<&;@* b^@['lx64B#77wxE< G8TJW5golDb;|v#:"{>U5P3DzD <@G,Y'qfgc@G$u m5$?;{Δ(:y] R ;|Jţ2&y%@oVu頒vV7B%E?jtW8n\(A~5NO&ٹ*jtྸFbJS7\=.og",^O$dkт A@delaCkhơ- 9#?,oÒrEeu:.Otm枫;˛Ѓ>gV8Ų!ރa8“*]NS1/B$omSlPwᵵ}|ǀfa^,Ó$4?~Օ2$!vU8bSSs%*۝x&pnӎe0kB:Q9`f2.?t `B2nawƊ;1xs~+ t_4O\ /rDjp3l>Xٮ07S窌0Y8Q,Θ&Onq]ur;Y%9$]Ցmd*>:<(2H7`S 'I">u4> >^U$8E\v,b;cޏf!loG;y&v:+SLV:z2((]|O1~bejmJJ Y" HR^m+ Wy)bH_!9=V%L"cG/;ߓvfqHGIڶWL# yc̅K+u3F5&&F P97zOşh(mZIc͂Bdh01@ OTi#_:`2:0eմ:]@鬑Pd}<*2quyZ:8!f)ճ@ uVJ>>Ĵ;}jхXcGw?2CCiAovvȠIq3ɐx2bIiCQOGi dЎZ&4 I P`3\'שFF8]$y2_ᅬ8J:t'L8-%ZQew]S~MMP \5CpB!}[AwCY}@N|w"|jt+ӧO?'_$^޾~?JϾt6HsvdwkUcpp2`7څ;CP,\FߥEesC`9{t9ƘZN\I[WT&u~ݯz F8^|6ԗ׃2MsSCm٣|0T~'g"H8iS}|}DՀkaϧ?r8v쑕%ZR_~ Š r8'jQ8+|c|nf|~:#Lwܡ/W&pTsepQo+ :In#yC), h o~cKWO73Zށ:ś, ڇ7AMFS1Nt 2SFF[V2rZg"H 'x *:] oӭQ_aÊ38#^e mT(ҏ x'!+tDUr: \TYw]{T/ڝ{"&[Y}M}5t8@AKfAG|.7[s33ug8%h%n ry8̴eYL.|';xv> <޹F҇'hwT@[|#XӱRљ5xgg\/D5\ãނϻ3[֓}.x|#髮6Tc{1mqnrŭ.-T4_.kITb#1Tۙ٨NsG5k:VFdVD[׶ bﰯXKQFBCteڄ^WZI*oϢ=tWmgfgڎTAxn5:U/VE_FbCu4Z?\b0:sFU0&x6`TC┉q֮5;,ktc8FS轅x:6:UO)kN[3:0/wcT5Ϥս=pDmQqdl췼!ԯH5.v8>^HUB >Dv6yƸvݖKVJ[gZy.>cs@Yh{>Dxr$H3 nV2N}lVGq} ?FXGPXZ#1v Vm 6FvBȋ|[gj}|8+>`i)d ְ OPY=])SS}nfC̱@12k@g,2@>}m \7mG 1$&t? 8F䈉uQHPok^thMj _ G#&Yi.<;qXT#]\΄ڴJ2`Y2S B܄,+ӃX[.vRp_rt |P>߅ f<Ϊ԰zwwMhV@;+t.-ܡ)o{ody}}{ݷ}Y03ID+(Ɋ$+$v*T?S)H~\J%.T\%ʶ04E2$A v̾uOyϴ) fs-^-A>z=*Y^V$"20&"͖~-? ҵ(LS&&(J]@'=-$/Y"2 o0FR^14sG7兾6=#@ids &qyEse"¯cA9< 5JR5+0Y'"!}v鴱AzW$(a;q{l{qKPc0tuT]X 'PE8oZS%: Ư{&  QBmd.5q<縉r4ԉaL .>L ~M,so44;?FYy}]UfZլFJQs}-!}/`إT4p.Bi@/*/*PA] bPr|RJfﮃG!!Tl :pwuiX@#|lDFnݸA3+n ~wYtszs#|R.̿D> -(}p'Fo77ʞpep :263HHpG/;P/^fC.-YS9P';_(| 1H>sِ1E".JMDY>xjj*"+0>6g0O `dV&HCsө%*G@r-q7w5츟Q Bf*2*o:AG*d3 sU| 8ssw _^]b0F3@'CY($=cآ6C7?=1J8u!~3:O̔38x}<380n~)Ek@1Us *5piV.]&dѱr*=SKY`){cTA)Bzӏ'?gO r+L{Ò#/3r *lq쑼^Zm³7{̯(x4fzxa&[Ekd.`Ͻ J|{NY+*x! ]GJ!,j8<.L"X i39ܹHEip.ju0s!cMudmFs 29|,0naݾ?;!3 b'9;Z V@V |0e|c㭔Sm(7p>C~霎?D:68LVj;iH}ҳ_r=!2|X#W?ߤQV^ԁ:>3lϟ={;cEq\091/4NB|u>sʁՙ&m7kkЏ Ё-JVS%x=|U87c{ BZ_!B滸^8[5G Nn5ךhJV|7C|4w[wț_0 E$|AzpHS S0cl"KdN@u cY:+*Zx* d+CjFuzyfh:(͹2U7+leLYyytB 8C-/ϓ8wK98q qʑORR_Bi:ov@j~GFX 8`vQX%qwx@cwXQ2 '9ӊŒfdqkS"5;E2}&Zg|eqVRbYZ{$"7:8E2s 8('a=ྊ\N8sh҂MIj<sÜJ K3ڔ+ S@;D 9"08I&xPVV{E΋r,X,q:`BNcNڈKx/Lf{Rji8}iaeVq#;I G>S.| *֑qz570< ^VWf3ﴧm+u8:nH<,27#| Oe#S5 };|^ԙYztdRjnMs!uwxnQA&ǎMY]>l%L Lg#pU1u6eQncl2 ^dZ9 ˄nG~r 'dZ*4Z) 1^(w'!t"+Τ2^z7+-tޮi*?) cV8GOgΞ>V<&N {T/4Ҏi!L͵`zÞ{q:e owEn .A|I6z J)pܢ o y=b[scHUUjZ)/sRv}. |6f; 2"~(iL-N}& | VvGV-)Uaа"`_1fathpkYj5 `D6*lt!\MֵyCڤ-@W珁a᳁.UA?4xOEk%XGЕU8Qd΅;i95z<ĹX`3< 4=.A VYe}=;@5 |^X,9.Cv# mγ2^9m` s .v+iTTgr7*ŽH&PMs:[>]S-X/J$c7 A"""&A.fdB2 '!o0޴C^!2 5;;8a8i/] W׬0@ 5`Z&Nx∀UB:"8~Jf j$m^VJ= jC8T9$T Ew638|=@h߇W^N]ݼ lDΛ$w;uBK-  o`fy*p\ rS-F+sk0K2WŁ`F,iS$A"Y#$s PMs0ej C;mBсC*˃$l>\0q1%Z[4=1Ef /SdhF`!@$guv)UaD 5srfwB|VW0LQQwkXShPс0?K\Ώ l8FU`?I!< :`iX‘|/rpQh\:%^w^,fF^yԏV9¯B0/we9?ӱn)O\WgY.9|°d> c<1FqQh/4A|6w't+cRi*I?xwb,)Gv5rYO{)ۚ4ƙ= tiV6SYZkV V٘;+Zfg:Cu&"# 'W0XX68X};,?;#`'dl^G Fw';_b:ɕMȨ]E=񤍶BRWq/P71fG:Ǽ4N${'CxfٮpC`4e}=8/ϰ$ZH6ʟsq _?9Qܼ;,AGuliA~"n<5UILs: cDeH~,GWe?:%+o^]_ a\AT/?|ggnoI㊷9$) `sx4,;W-e]gT贰J5-~&ǹDi}/ çFobdEAۈad43dcO4?& X>!qFgLLg盝?%^#x:gh?|E+sqv] k@!f0EiztIZ\)' n$4[ʉ~'_SIxg,p ߕn>c1d;p{pxtH\@8ߣ]0"e)eG % tFh_0K=*#m :mN{NS1{,=p{ }q|IN>B2B.`q: K9o8:M_pz${ns/~y{<48vw=|ksqekҥŧ 6Nuk~X1k.eslg6[B#nz8 xCmOa?:#9u.\ge1?oF>bF\ށ<{awi3Wa/gkr7qIX?B`ߘb6;g>9mrYkq2fjk2 Tޱ _gfNzǎ9v;fmV udgzly#m| wnp;΃3?󍟏ps,w5{fg"!󬙽t3 9BZ52FȘ~xU&$2CwaA*u,KY0Lr`UĀ2:Ϸ8<@m<P@w9NNW-:)NBYŶHw[al?k>` ɞg}L2PXѸAe*L;صеIV6{ũ:Axhh2x!C3qIJ3؀Z@J/ԩ̦!itmC tD8:d)wFp:rh0M2]n* jgݮfᬓ6R1Dg+t-SާoYXBCu7-VrYt~NTa9K[) $uDVYfOfh/RD'aZR5]\J_H8!ԠCe7{y7GYʽ+tg묟%q fnΐD[V2NUy3SO]HnI_|2e[I|%+H ;wU䉦;sJp|xf=f({ V[$}&*soYi7Xa·f X_I3hoE>#rpr'.j9#=ԇЩS!g/؅P0|{;#@0A7ֶHR%*Ù1  ~fiP!*J!| 9JZ5E]%Dt_a T¿2A0f1q3cf{׽x$փ|0ɟ'M -QfqlX i<:Ai%S2HD~'1O{ e0pJ{D[FbH@=Y.$BYY GJ@3O҄FC1*S\ykJ8JV©O3, Є]yJ#f0yTĥ9KWvV)k?AKF7֨&ggܺ}P ,Z`VeHYtH=0+S QUDž pG,P2 ЅK^JVykFzs=duVj@ WJi"v 7ȽεiL po+8~ T-C2RѠ#fKBpFLrֺL QPҰ.MSaf` `FT$k)doȸ`N8# Mj\ eAel%L!@4  Ot( }og["\7ԉf7O~za/ί믿f<`9`8HO 5(ЦC<`x{ }e3#:8s>a0(̸(mmSS+]Quš?kLL }B7Ө\rw2-0{`:9v 0`h͓_>&fn~j0f#Yk:YAe9Ng'mqKX/AԲ1&2mAC^ LyA?B.Cgϸ@aC)5f | r_B㤆__̪X4 ʡcK8KVᾚʵiCN13CN{e3|o<8Oaf}V=Xes p1f6K=`a#'>ܒvSpa1p^f vcKzmmmo=/ޘUGiE>^a5@#Ș2{_{-My~ /O[WgHS頋zjʔjo%iq 2tЅAԬ݋YKBp|߭p,8%EUerxe#+#ȿf1%l_mfPGUx<'][]&_f!3cX^s:U3>%r=A^ӀՀQP&1Y+{ww&v>Dq;6U? ;Q"qXGX}ȕy[Kuʋ]T:9pQ~.Б8o8Hvp\֨S,L-·}y09y$N3` ;aO^|9\ '~. $FneEM~aa6Zl^=r`5*י8WXr^,p38C#?y^=EyNs1;e{?Oy.GT>#Ў͍=QN9|>ָ! ,'<͍1|#.d;g:-0GˠQev8wg>\WVle[J9y, ctӂL L,F&+G'Cӿ"{7 }E BK7{8xl ֠WOF&} ]%`NҎ NhQԡA\sQɿ9XUȞbٔȖOTi2x-Dl`ЁsA&υL>NeT[l||e%p{ZiG#Ǿ],aꗶB-֠C9p7a-8[IV"hoP@W] p!t>tmU`2Ւl*IT22{_4,-2Gn#'ORe D.9^_op$KҮ 1@!D:Cc~VȬzbb{Y07ѫ?ua[+0JD38bl Ph F6v Й.p J  Dٲ 7boe_4NH}͍LI(ɳJ~w "%!X#:1\%.~+F k;>;޿v@Xc p2Q#"n!(4$b) Kr PWSULK `  YdBg% Q35I*E:`YA̩gt[:TI3i{4LS >&n‹y;юa$8.T/m0Y ,/8Bi k8,4l0gn+oU&rRzӈ #+^H*ہ#$NP21~'=ē`UIiIiNeATCe u#xZ &ydEW_M/ЖAEqѕSO`ʣ$ g}dlQ Zr,m&6a9PdciȆA@'xMYǛ#g$h4UWEΥ|e d J^kw2Z6yBqīQ]4Q?(;(SWdoiU0J%~ƀ亽4J>S0uiJ۩Fe(澱VA[CY־\Hb1j+e&4+ Ӣkֶ0*e3OՀ渏NXAw܂^E{#LZV%7 i >ӷru/<_뿚>M=v~=MxʕQaGܐn.h`wMnBFWլ#OYw4$q-?:g,Ey ˠRnTA 9;yX9:폋HyqH@IDATwCw,U1<>jdjziyY0#G~De"۟q38}Kf\|eZg7iF hKlQwIUw__sꭙaL@ UVq̝459.صڻOޢe*,yaxVX7ΨdY`01^J(nT@s,XckgD~X!?'>Af\{T 7flP%uCW[>(?r__5Ρm4G+u5SV逗t;WyaK]q Yf^| MIR`<1ypɴ./1Ѻ |c3Ɩrr| xB6=r2٫ \X%~wU~ y0_0Rm"e,ErM8yP$!8eE85>+zŞUuf9hy*Mj[D$ )cE`p#,,xS'30<`%eߩϫbg$?lE쁓U^JWC%9>_ u['CyEY!;s=.^ND7I3M]m"py79=6rq['O Gtyf 4>:O__;maT:}DziS[rD?Ѐ9ZP<tD4RcpR|8(H&oqs g" tf'YMj7snO=-ɧ[F .N1`m|H7.v59>}7|pvf{/뫱>6pqs XpEZB64Y"YRFiB=١T3P{[M2JZ^@ϡ9iJKzft %t0#^2ǃN$dVI46 1E ⱺ4@Y1CU ̼(=6!>vA*3/Vwqs0H[O[7Sڬ.ooA f/:?;;r"kivsTvn -)ZB47TA V{*NZ6kWӕdmߕ?>y?tܩ'ùge2}A8ҷH& ;&In# ڬNH/]6+-c@g+~D/a&. io{=q%;Y+ l _Kp=ITϑY39W1\I8QPM\x@c}ww(7/ں(4ؿ}qS@QM}ε \vdsuYuz##ʓ ȃ `.TuO2@i86wq 艾<2U+*+%Jh aзQZc)§hIyh _ J;|/Go*"RC! O-ukv6?9AfM^B(@&"z`R/{DPe~ȁ@!Hb `o,8FBgOU!Tk]Lad}?@5_B%Kye'ϥz<36٩ؗpZ#K/\l'}#ܻ>;{q!z ,B6Kɒ-AYG>0   DrהCTj66*2 B̌,dX>h8L9FSaDj55>>M@eY>e!9{3Ƞͥb0h44H;@5_zO?R+ߴ@d("\Ef]aZr4}$(wFVTZB^`K ԈWS:D?ݛ:)~A+91<m’J@B VOfl2 )kD^q!AyW#u>硱F\E zO~%jOOo0uC1SQ5Dj9D r|.W@7hjtt4=`>_};dKd1 wU/2.89p|$=O?W<&Goss!)?KD9sν46z c0=+ |»Y$ZlO?LX7ȵfyl w m33Dic m 'Ә2->JSo#2*{i*,y쯎-Kw,33ggKv3Csr(wR [8̨ނh -1Ht7r ӈ6g95i`0$2uIrk׹:mi 2G(v|3:߮9ivmMdVOœ=̔3'a$Cy C.d`k,,QdU4 _^4O[}O8})5u]=ōkiexS'M2{Ho;zmOԣj@),P® Bs(' 5Rx6j2Ӿ@P%[A5_-aLv×pq{8 +,4}NFöÕ"{ L,> rsg}يWI8WYi Ќ`׌'4SFNwwTGĸ޺3"/BO}޳^ٙ9 D iL u1$0|u6tF4SS&W!*#wl70&sYkd1--)^!tvCg B(h8؃ aQbdK44(@×jPGuZTI 1;]7BN@>G)dJ't[ɚ~aՃ?'LǏ; gF)~kuQ%!;8c <=s[})S!wJ y0tWWv'iT )7ceGMs``85o5w8ܪLc 8''=n4| 6t#֝&gbxsh_}j=@xM2چ!tIәn[>eI]=m1*e>[öq\J3 D ս.r-S!L1M:fIGVcQ#m*[٬Щ?[ѩv/SA:ښ <_oA6""?֡|S=cA4cnS.Fd ,AizAtp708ao&(0fAdEyW>u\> ĻČtG4֖thUAZp4F8jW]G~4TGZEzIbر0ʿv:8c}ښNb&HhGJ~ HR8t2TB^+/ xT3.]x|ZH)k|r@CȽ8KΟQϼəu kk]#m7ס iӾR -.k_Aڦ}gq _#yp{Qݍ9Zp[+8F] a8|sr6ũN u e`=F/(˜9Z|WBU*P{^ƶ{SM>~k{T#cOS8y裡f-3Q/q b"zTqH\[ÞU 9-f)g<83 \߳.,\:aYe  6\g:#Y gf}# YE-=PLoDˏIE86甗MRn-mdžScww$ ,EdT$ɩ$ģ=s22U,9u2I};B8 i}#ܚK/t%ƨc{א5*SЦH);0ЗT#{[ ֱoy{`O(P^ɧ+̜[ĠJ)J I_}!Z%f&5SrcC̡@C _{E"v1aRCo<4M#""e)G?umyrJIK;YUOa;ͲnTU&"Rk 0ыfUa[OghHQ޸)cNՈV KL~ +(u4Oׯ1'b]Z'YryeKl+v b~-ʞ;WHq^M,^YiK蟝%qt9,^ Pq˂9 bD=!. v/3C{ewQrZo|T=0^75JAg|cVJwէn 8|qnRCQB!]_ʘoeQ  .x@qhYdt m73K9J|97X^mQ?Gh@=wUU(ݾA Cy -R/kD;OcX^p%hOl?Zezg9#OV̙|g,ˌ9ra+9EK`B`8 :.8M[L bf*:ᱱ E1+aIa%>ʰN`3vRNJ >`C* e7ƙ.M\.LLu1Gl;0 ope4̸01>Ϝ(c* ##E<-̅Uo^-{!vC{ ibu-a7 `)IS{+ҿ\4d|D>N5[V aџo'φÁdZX&q3+;O]wv ǡS~-K0/ 7vпgVQ|b aqYivvZ_lH FTbg, TV c1M!ʹZ4L{@m nm-3^s="3<>< Z;uJ㵶cXٙ awAg/F(uįXk4*.k8>۬θ Cko<1yy9օk7qܶ@TVA;YAjJ#dV"}S1B 2vȑahz xeE}=?u2汈ʟq-!+ԑ*]S(ukV,Vż!{I65L̓˜4<&Cgr sohZHegMM5A;{zޅZo^bJ>@{w8MKN[YX{A]BKjrǠI{U # .ȜZ)ݳCz>gWF|9E: f9Z[eGy6)URy;6&3]z5s/ >,ږpT+6@V)V=X&E]Yݠ97N_-xǴ6^JL4P-ZH* TW64Md$hp lG5 amUjud9SW5ȧ56gQ*PR Zrx@!sPxRq2elfXy k᝕ȘdV:1:! 6ك=E/ |p)#K͸/4X\WD'^ˁ[I̞cWO 4!+m $``!7o,mDdF"Ebs,D/8hp<x^x㬬 \C PWoVt>jO2W-`ouS+rp܏ zՓ)8sbK4:d}O>:>6Up{e{ x{1:6&@M*{Bڨ8F`VʍWfb ]Cԋ-^^k )x6Ј&| }&BKԧ3q,~)eZ؍וΝ?>ԓi?۠ս4^I] :}ÌuV$ z%IŹT&/˗ng74QLz#ǑDƧk;o٨/g_Og>u'GRoJq=NܑHBj#Y=K-_z57._Xу2p|(LC Q=!o3 WAk6.;r>Jեts_1L2*K+FH97[=a,@0 etTj\T@F:e?KQ p=JjWq J{+Oӷ%Sa^ J2 [ԏ#p!;va;|u'H{93Z{^ SC7\4P[ÉEq;gKaKZEBEBY>8[@c1yY0IF? Q mʵʧV%1 "@͐b?tPFh {sl㈘swu[ᡑLtK,ņÑȜ X~W~[8}(#LFewwo*/?d~=?yB^>dOy v 7ʎ(7$y]dAt;٩/]B(_ߠ9*U1⿒%jÌF0av~8"- |LtE21bo!c 28ut4u!ǰ"UGFvK# c2?$zyu+ilV%Ș%BFE8ƘFRF ط0 u,k~z!]rg5ZAC"[[g 2=C9i^1wq8 L630IxQ@F]qEp%^WgHx49b&%H1f;jTF԰as±n._1(~/>c&|RvUd.BZ[0Maa^bWu z|8v)ǩ.JU|ilay()G% n߿GSyE:5 `<> juC'^C7􉠙I/2OLлrU8-OF0c0jF֩NǠL{" 35)ll"]<}s Qou'G?]%\2e sC#Y5e=61}Pt<8s3a̽K0n с=5P331As:ݘnp:oa 6Rg}1oƺu{4VyߥTa aV~ʘ]LNL_8o\<Φeqֲ T"X_esɞV[na f22rgl@BQKkG"7^fͿ U{𓎘zΒ;n˯_HkKcZgi|=մ U^.v1Wvgځn3ڲFo@6LgGfW|Wert4dYΕ)o>m[ Xeqv! ]CF_c/#e6+OnRBFҝ.r8=>A&2?rd8bqPo@:7lTeβc,w&)}N 6x:6@+V0GWF #ѝ P1@%o{Llpr;UdqzA"iM85}BPgؓRZ&; B4Cܪ'Ms`I pi0&uW[ kMMO!]x+TVYJ5 5;1he ݟ>O}7{{8"[K[}׶k啅t2<:h*d~*NFe$1WDqS ]T;,P b p/dNk;`k渑;8G4P*I>,P/2iGQ>c+D= ƕi [QUw)GR򬊇[u }ؕt;"؁*h %|y~prz?Ob:qYSVfN?лǘ7<ȑ^ܻ=9 G"@*O"x$ 2 \wN,igV(BgDʱV g>g©¾4ܦ HRNQmI_yYkAUTKDɖqJ8q lV)a=29!O\ :$>!\bs=hZvg3H ξ6d,;p;IB:;3\MGy}\-ñVLPd7RK?>Q 8F@'~RΝR/?aC Wwjc!)Ƽ_ǀ;Hvdq^*K&]BVcH#yx펪)_#[r>r39rй#(L'î@x7߹]*WfSK.\M=f[XwCeg@lű A߬#pAB8mdG*07kIR [pWr<'zOXb}⬟|#pLObZ%N˿ L 8QAdAXB>ck!jS2 wrReR?34AcǪNq |'/?k$Gy<]Cx2<1p?Ǡ'BiVEM8?NyB*VR=_> ±Hu9ᦱ㬉LJdbd4K1d3Ӑo仍%NNYr '8ZnA?\%?Lg<Z6)|l瘆Vf~#Rk60$бPakˁӈ/w= T7T85j(k=r^X9ggCXQB⨤GrO,%JBRt8#F n8JA{Q6Z!:#4% :APxPK{ B'j#bx03+las-x/AüNZdPm9YFNq=28@ (" (ۡ-ߚ ̳ek=z8約c2iVojMGhk4!C6sdq 7 쬭؝:C5UTa/@Ü82;,Q ӭ[\38ŧeKf,sv:\ܤ< Q5^nd8[6\RJkA_Y±kDJ[f*u?|y=$<<8>'--&f\VVɂG|eFsab"\iY\]c{@-[3f'Sϙ; H0A2:t hJCyrGZm+ p+! R?- kgʪ9珎MuW8NEZ1o5{K׋ W^GNISD |A l83GkR!PP{ ڃp&d`аHyȇW:Y,2{n?@W"꜓`8n ӱebYz) ky2Y1-+7 VNT?\ ( ]đЗ2z˹X!7\x0gu }5ap:?we D'|]la33XB46(U3v:IY j)BA6v@څ0ٺA0p{3`d<+H_ ֹ =d ci:d*V7R̄233SZF e:mÖzn@G:mfhII?TkkNYX%! a-KӟJ«>?dkiOSYM=wny#u5«8*geAЗM| JXk5mfW<$V4tw 7//~3PGzQn&:^uI}σK__… 8ռ&Ge hA:k 'y`ޞe^:ZO'`! Htjsω?sK[ڠ8ˑ+I"Bt / 6c2.0pm"iu^+2ÙуjرCj ] a.>T@IDAT*dd1:l+l:G9}(mk~̡%O)Y]|4,O(GX9^d>~W,@ ݂cnĹBopA*+:#~ͭ~"BOoUvKWFy$Ql# `KPFpy>]cA|y/{lVu<]C6RBa/ DA^wD" sD)l J,=Gq#}%:}^Jw 4Rx fq*}] KpF~ -w%|sAbFl`_eߌ b lF ~[9FYR􃏤jtF)N78J._Ht2VMɧ29oBǟuߏ>i=V?Qߜ8;J$N.ʷ !P9Ad |tS1ub2H4TSgۃx*.4<{ O{ow{W`hJfX㱝?2JL28d&(HF5rl5Z D! p 7yA@4^򰻿>S.BdTqi(7rTG@B}-ZVH123AY]8Az ` eBV„Pt]Oݩ7ag x;nhe>'q~K[ *CxB]`'f ?=Tٕ,QE3lb0d> r-(g: -.WJ3*~I! ? @s%y^>K`BnQ>3\EXg}2Y8d6tn,Ћ# ϰ;Ur?oĥl$(X6s*O[?7wM_m!i!1$/b] CA@#.%[Z>tp]*eQF_;ƿ'ƵŁ47c[T-nAjhӠ, l;4V\V㐽225N#Wurz;Ky_v<35"e1mQ ˌ ϋf@8]OS=taj"\3RЩۭay?'aĩd`B#j\atލu8WOX>JL`5!幧6kϬday/|+11L trž_=li k[Pզphjj:hB㰖|ڵɞ*}hpA7BkLEUx{:7yX h O!\fVX.( +,`/#c\JV "_W. a]P@_n[8t;Ǻ`/4]^5x6yT|8TM~^N\9Tį aڻ,U>U:fp#TPLmC/k^T?6)H+"3 }Р?e|<*TV T, p {TX=Ѓ&"bUvd:O=Z!?(]^>Z(Dže[8o9m@+ʶz!BQm^HilVmK[Si23ܥjH)B(Ri+qVz*ӭm[FD#Jw[a|DZEG[礎P/#T +8;zYY#DڒW5gmA90(H(En+F3 Er1p@=&c e:,r » F2d|t;u PM*GC26<}LPv8]C$tX24P笼 ."zvomTg%>ip-9t;{Yt־:Et\JeVϹ@ *H)=3G.nrfU۽ehcp /|S/[~߀Q=SyZ2ǝ3vw? +PF{epqlR€busL+$Y<6ܧ~=GA_ 2ԠYg>%!,X  0Gc0Aq렫R62h gLV?εJpX ˜!҈86&6ˉE/rcuvDUd&ի_쯾] z:G{u.N |F5NT^//O?HzT u5k#7NEMO!A&е #_4< Zeb+n6  @iM}T6 }&hdoJ{K/]HO>sJO|齏L}K {{.9=~OzԂNSրdq}=[$&tX L[7~7zz:8chWoGPgSErH>T'ryoAh{:34q?xV+T([S_HԷ,n; 6=8ܠ(~>Y:M{ i.s܄/my?VAg+MB9 /F#P1Wr ZIY%  xD%NpVI0_A/r+dضky>$Eiz`yqqZbE[BD*wF=B  v2%#ڊ)9N"ʯD*Kf,Qp騬];Q F Fsa_ gn-#BN`X# |9>K)"?ʹCd0D`"g0S&o0'?chh ,p!-D&05)njLKFӨ[{2Ɔ>:jd+Q.y(aT1yZ^YD`lb/ rsfc<3|W彯Lqhu׻nok۠L6%Ȗ)$Oc/*92^>rop@te/۹u*N:#kXwQSJ\){{nMo]`EqCD`( c;̆4M녤JcMa*:X,U>Pא4>]̝A2#ԯ3 9=CL2Z"S\ʲK8j냿Z˹{UL V|,g';M";2R(k(6yñs[&#cԱ$e&Z>#oEYA&1fkxWQ '8{탖vVnȲ'qJr|A_]%ةGSֵ@AuliWu8-c]9 {>~7 >z(h΋H7.^JS#dhAg&g-S/pEiRegO/Stݍ!|N ?2ْa#F2 A, J|W[|Nf`Ek>UPl aPSK HA}W7m={Ov;qb_K/H}psWЈ25WR FQz&G z/Q lMXf}_ kc_ȒflP[PX0q4J'Ml88ا]J1ho< /q̀WG쳰瞕@P&g#+r1 E)py pLzwug.n&60bֹP&}q^wˆ#B8;Ͻ0:*p&^o'_OCs;}Y)1΀Osq G4rkSCJtzC*oh'ʨ!}Ҝļ]AZ?u+Fࣿ/w υY4RQJE2 kjI*g%Ql$qS B'ns|\zKF| (Xϥ McwM]dcq`md2mVI8r5~|Xoe5lBrYn" GY\c c2b ,;/l'b@9dRS"IaDV2ϠDxuq.fr8hSVجEzA8ԏx2gc[$s:F`͔UU[5h\hNEYRױG%l‰g2:~=Cٯ:f̬{W#svޞ<:խ&s(IϟVcD:6~*++|/S6$-/ǩ%W&-{Ƈf `#\9`LX8xr̳5 އ|dg^DV#h'ma wK yGiJM bʙ1FyFTrRw~Qgȴ}^=f+=7 ;&!/D];߻t ^bcBvett6azrh[M%GQޱ0AckVju(C9„#un=6dN:-B}&wz;rDIF[srJ{{ ,.('YSq,\"EEGYHNNkoq=~u*w(;vK XΪfA+yrm)xCyj5|PaחrcZV1V&dHd0>n~-hv)m [{W)ejKgBWrh&F{O*r6I2šK D]5KV]$p|͢41ZzR+mk6;R#` : F΀_])YVP6W,f>A8 |>}l_HM'Rw{ @_?z;_'iӚi>7цxokzOAuzi`Lp#VRSJ$},Mq Gf-a;JSߊr)CFt/Fv/¶r-Φ>p<-g~R|ASTߙ>&g3ϪӇ>wNg F߁զGW}L_ft 0 ЄQ*{po錠 /qalEJ}ipvASV|`](" -OH*CRNUh2l!CQāmq<=3#Ʉ](Qoµ>BY6i!?R&+(R6 ҂.긗fd;f䔰mAVXAe/v#ΖyIe_ ДuYs3RBٟ"XUE#eHJBQwleBUP ` @ZBψL'%2he#t~I8X@5Dh4K}D=aL)VWɜ D((0 d 1[}3>k En |dJXP fU !Æ[c!#tn_BaI]g/ s D@ G$z2UE"¼#P!dy*?<'HׯJy א;u1y3D&5įlLq1} FVVQ1+\A@ Ӟk/}賻x^Cq7Je*-uX(FS8K.Q1JkD5B5-! ` / 3^S#B3[]rnaeYf JFUzH+.3@w;[t陯}u' 5BF/Ew kE\C0{*Az\q. %vY&Ce@7s+'}>QLA;@eV"a:v_ :{=PɤcC\gٗ?VoxO05%k1 TlBШCeހU .uxZ-rَ㣿o3746a|\Bg\sqeZeٌHe sQQ],,o;::~3 <ʮaXce2\͌`V򫃱DٖhҒ2بUFQ~磠4i$ V|(}S9Exx^ >%j͐b.m1G>&\(d+p oFo]cK\gܯ9N$ {/:{urf3W2P$+& yE%*&`9o3uK\BV J8v }>E66Jנ(" үr!MЏriF=u# .:Qx(KO>-"_J]80p@XgO7=hxϰˌz|[: X2N*R4|F/y{!#"u30Xϩw2m#)b)_I/_JOx(6F4mJ8LEc]:eCC@>ɴ- ]FHH:~QULQ>v΁I7]X2Ѕc2ba[K:5^DRE8#y:RI aAG( 튻O5$C 3:S̺*XmUB:6WigNY:#CUWa_Dv[yYȁKט$vEJP1gM<_u- ttح*⨥}2KMذ* 2gZ~QKog4"ݒmvlOP>ʏty&^#[߅kePe,NNxh uBc?Gܶα2^& >hw4TY~*lX|A2K%VPEԳFK Y`<^Y;lζئ)" 8U8 F ,$`G#CkH|VoktUlP/l's3S)&62Vo><g2ܣ`S+a@9<yo.5NriLs~c֦{úűWSZ$vo^t 2%)_^E0jlr`j況VÜsowVAď1GD% {JF+\Z񰀾λRp@RFa=+BlX6ȬxP2(3w~jVN (D>!dseGp|yt9HGWfwFdZH3XG.XGh_.˞c{>T+'l K$m4jip{{y,XQ|=@R ڹ8!we觴^D-Џ89d{Vf!Θ&NӑN ɔVxn.堸='>>5E%Ac>9_FE2ֱB` _ZƦ"֖y8yI8hmVҎr=`!.!o r!i*UVX(~1"{XY#8M`n/cʰ]{z ][~,u44MRvP-.OV-ZhIA[2ӗi@{!Z5#w7U`tby.5er|r)Um{;D%O᯿q%U5s ۦC0*<}~2MӖ#7?IW^>~we')=N WY%x}?羑)C?ۋi"FOˍ~Zn 8 ^k8ՖO%@U# pg-_k "mİL5qut}( o&;P_dS!ExWW#40Of>˺ 50H&=H~؅>9@Gӎ+rAUW^#|]+qo[|i@VwiXDEںvAh6i}lr Ce;dKhj>CNN3Y. OY ^_gA®B=!&`\gFD2ʮ4X r!0&2d CQAEEYA rP:@V6 _e 0C=/A{O',:$,r7@ ʢM"B ;d4Lh(݋s81bIȋYÃ1Hr0DՒkԤ˨-*FAҗTоv}8=r k6?ul:hsL_?x(2/{""zZ9j tf11/p1IIW@m!"m~IVdՉTBa-"#UguI}WØ}ƕH0P;VHcDR@: l%z2g eMgM"e&Df/]Dsme?X{e8ʓj\&!T1yщ9,GDVIB|j;XĻ"^oH̗rkFhk'Id0n 42#}3  K\ǺD. =:A(o\\{_,Ci<9xnay?g߄?}k'yltQ㙓 &*6dDd/?Jb$SGDj4Naڲ*:q<̟z82<?c{8¯PD\~&G n@h߲t/s*k`SVNp,S%n#wb*5t $NP8yE F+ xчt #\do:yyd  w^餲e/_@8'wzF7F# 3@Po<]O6o.|>λ]:ҷ^:\OIQ1R:Fo fGiI]$Up 9>tUq4 . _A'(OMU+%^~Ÿk7*kWOC/8p)>{mf Wm/Eq?rxNmjκH{(YƈW^p;x啴85Bhp1pѬ;N[>KߺrbCG?ƞ i;o Y@ ]P >tDШW!0=‘pF*C}> P5* \| MW1C.n#d.c9kedk\`| _AßL&(4htNԉmwA`/'0 c1־5TϷ[t 3Tw exao^4әo8kWUV1Jdxk3k6N$je:8c.aǨWGfkή[Ö1:{2auvO晸FXY(ss-=^"hjzx?: ^:1ZᣩQ,PYȬY_l>P .I,FEZaz].yn`ﭷoȲ+sonn$Yk!PZ@"P-+UU-B?p {4n+S1\;MF`[m nLQ3t(Sar] cŎ 'JtN׵Y b#Ka6=m mR구ه23yHStU 8 0#yVrfYiT&uD".rl-tͺf!$_su2v '%{K17vIe>h%d-]SѵZ V~L¤Ɉ]Exn!foZ@ZZvHwwY ?b?q925o+҇=n1ձ%Jζ0>$_zU`ӟ$e8ItY+ W']F8c0WzE95#F^{9O[;8aXKl2@q##df(E82%ؠe.\C6͋uMˋٸH2Y.m>sT-]ֵ /os9Fm$0SR=iru[ tMnq1پp38}N'M?DҖbդ:Zh`"!Y #)g;fes}T:;!#ŵ0Ӎb\pP}W<8dpP΄y@9Yi =~'i6/>ep:wQ RU^QIDgЩz3>E/Jo8|J))Ky{^&8`}y>G e&Cӑ'S1xw}h0h^a!J5f[:_K4o?LeY6nԊN gӻ;!?ے~ ɗ.wiL9et!iU8#t PYmkSWS%8epRc/89V?̹d/,CS RHA<( CzRJ$~L=Tidڻͦ'Τ_F?~_Q)uH=p KicS~YfS `6q॑XU;[S\)C |/a\j2McC{+Q5$M#HıMfo \Er.D?-q/-M&eńی//CsHGl3B :#s"/[TE֋sVFԑů\fbyRΊӄ-઴<2*sMVflU+ 22.tph-{(t*ʥ sMQ>Q[&fhDzqi& 0'rzmnmd;5Pp".Sƒ!i>ZT˭WFRQ]&d.Dq )Dw}U2fXc;==,%Nj$̿BYa2Ef+V`Ѓmr4V@ MP1=gSN{aA/2j'ђn'A)uy15hcX$bV> w4W^cy}G%eT$smd:Uu_3`M2ʔ uuu9[xŠB.DSۊ4Opdx=~4sF<w^}:`mYM,sDVWTcQMޥ,Ĭ8@O<3tu+WHXZ8IJ8m? –YQ?ud G d[n5@XQ``|SZA{W2Ƹ W>QN^z̖>څ;+.SH4?8庱!ix4DmuHt7 L5A`ϭ~x2QTngAƞ1Py@ uzkHnKQ|#|=:1ᚨag&с.@ I\[ȉ¤ Ѧ:M/„:7iEߠ̽=*`G׎}f}ly0o~˭``t_0.53* ԨZUSlNjI1t숷L2 G&.Q<+ϥ;svOϚ__Gᶷ84m:f N%K`h(ISM0T./]&V[bGiE4u,-*#TPp^3S#㶷d>:]FfmByuDJwVu?w>>oL_{ DS+}%}I>~xK־BЦ)>Kc[#k8.ڏnC[+ eO>`cy>Ck% -,!aF8ξgVV)2$AЦEP?20Ω-D>)b4ܷ[{c+3;WCѡݰ @ݰȡ@Jڤ%iS+<@Ϝ{+UcJ|f8mqU^Z]M`/&`n% d!AtBVZѣxMm#E;o*/ZȬ&ʧeiY{=2Fy r*ptP4q^[{S%RV1蔞Ig5D: 7*8RViʼnRQC2KF(㲜 T4*2E~'!ߦdBk?Qfτ_8Yuwxmߢp8*hCYCmkz bӂ1]Kr9VC8Y} C3>(*ҮIaZ^y O1J"_tK[HyRP>w}3 P~k%gt.4'G9<BIA*9edMZ>"˷ V16>MatB9p`iaFw TA8-ܣ\gumF "U\K̩uNv֨5 sU+9k7kƝoDitFv8LN>fpP<Ø^EaN,}a38 I VZg8a{}ԁ8tZ&`c1g 8tNwgVPB9C9O/\0t!^ SXxEdG=|fU`wz^4ۿ2r+  + YBVYݼjKRc~l* ֫.̥+G g1kvѡpmu?cF0&a5ggtPC;3;f_ev`j? |a?- kff*_3:':,1Q#`頎@}蠼dtbf^s/kԱ~hܟz BǗ&S"vDyoq9r?f ZU<]I_FҁtM*$B@o=#"59 A38&SL>j!NG8/}&pٽ^m+SUU#TaMtOyK7pOM$48 tr@Gv-A;7)tOWiY)Hklg)ԉ{z*rdj-կ̧#T"X`s%=|y8Ltփ3mDT>OAۘ>_N|J?G҃ӳitj9=gO;?6* L9WX_.V6a3t␃Jjm@džg0kV6H vv5aBlgvdmu:.x)U*$}83і:&oђ1mGE]}`8yk(`f0H/H똏:𦎻N@&dO✰H%*-J D&3Ġd±NA:Зv4Bt]ұ1-rHigUahK2p)w^XNE1)ފ@<y0 D5. y#qT)U0Bi~vRƤٌ+ 70M5,cq,d7Y"mȉi'<*NQ[k'$%jyahnL}y"qåY8;&(AC'j-"iU@nEñhh%’ *(kw=ц [&*a`(,HNǸ(CqQ 2k }-ǟ:-a?)G ?ܐ[{cxg}YOlU8m_;^ܳۼg߽UԈg)t068}vDV<}vؤX[C9KmʍNAa@8^5IʁJ(7S'mK#k;cZ(H|,a{ YbɈHkUV|}$5 An3/YNWvǺ ~ؙ}%׃O?Oh}-!8K=8^{=oo@7'B/-64zk%ej@ _亮|_:zX^~_ KD(1nJUtJmjv8D!:gW e2[YEU>RL5â 썤G\7i\| LF$pV?:9T@?g(ڒJ(9%BɅ~̣Ҭfd? zPq.;IO# 7BoЮ>DI63;pfh=Yȕ[Te;>`2C85gAkl)t㐓9FiG[ Q EG(eOtHOFdYbʖ^W2sdNkdu2tEْ' ҭk=?? w$^J5л]o}j0z9 hY _xE,OF ǎH5|Z5ju9ݚ)A{/MDraZL;Ց6ҧnJӳg4vG> pʞGamAA6~:t+K"t a "zCgn$n46(E =C * #1WT= 8s`\zPzϦ_~ r7yTN 3۞ߖqx-RIՑ%3/45 KOa@^0;G#`w5ռ`<4A~ڡIy54A4]Y~2[h`{:WX랫 "w Fa\Nq¯R#.&(3JmЂiW itIW^& x>-&D_ysݑ[J&i]^`UCv72{eK,m1Kט]$"t.;vTW9HOTȫl2[&jc2| CPY@@Ut.nb>rFYU 5 AӮ83$a;UL94J5FZn[sFfX=<}#/@&iG/Mg fZdisA`?w!뜧%Y$5Hަ09ʬSRZOPHǑ{(7}.-({ί~i,S%pdV`.dZS9 2uX*I[]+U?wNs3Wikr4t2:7щ9,@aE}G1 >Ifij5 fP&Zݯʓ  ý8tց3ivzeNF\Л<"f"s9XB7OgHTVTŏKQ` YD9w'-qnq>$@b$pYJV3mCaсD%7i{جЕa ]}* Y;7Hm\2x h"D ;\ASh(Bнo+&'b&m )NUUr߰3w¿pFh.L߄1t`"׎N+`.dCAd+~n`&- |l(oKT>˾j5 KHa gf硵dW0Ou"q:\ɵS ߖ 'L^&m*|Ce5: sT⌵>þ׊ںCҸNCa|Vd->.= ĕyJ?ܕn>s1=OP4;Oyh]l @{ URǜ.nG0V.̰)p }A)%}癣gv6[y Ϟ]}eg:{M=Z"O,@k,Jw^n#}n*+ oH C ـ5p~gq(L`hKk@tJ{WRڱnׇgh! :] >8u2g?я~I5~tu,_A,EZ ץI2 䝼g,aP>v+H |j::sLh7 L)4%uU۬gJȪ^kWkB=&3*e"l%wpN{xo߱F[+@*D5>7#QN~1%h*~Pۊ˳PKmU&[YE“UN34>ʡ5awL`ÊGTR10{uگR Nsqc$a| osg»aLI֊"j "k4LDlluAYJ_zUq{M7n^݈09[ӓ}(G8T*_P]N8<"њqDD_)"MD81f͔ #cpQhن H\ODRhxaeS z J@%Q$Zy}o$nfhЉC@g2e^GhZ"O5h 0ijZ7m!(!dS`2.K\ys7FAhqq(a֍vX es^4Xڷ{-&d~Ͻן`f's_]&h{=︎{\ޛ핟{,"{画7lWx5W[IN߾r= ¶ph#G"[,O<0i*LpCSQF*iMDN^N=FzH)Zx*kB**2tc[|NX2u%h#gT8ď"b 53K-"[Z ek"u0lg8W,?J?X:}ug r?sc\sn[a?ysS8x?|}>Aaq6MɣΥ\kǸ_9F\hN N5"Fm伙Qao 8SqdPMı;ߛ>K$B.;Od8XA xK^*9^opa^#p^8)Hˋ q[ZGP(޻*2 ݼBRyBIVR9 >zzz0Ԅ_ȑ;ݽ1sZN[ODl,YJ)tA߳^c.ןG!=;qVt=ؤaWQmi1(Q2xO%e [ 3z8 죫23k%VA9=wu)zdk|!Uc MtD W⯰󽣿mS `)v0TRAG^e2ʉGSwz`Z,:o4T^\8^DMή9FD/b9p_9&o4^j!vs>c0*Yg,8%JC&\U}:mOxp;qXDF |7 BYy 2K%pVp ^Xj߿#EڻGݗFpϠ 2(Knam#YKIX%wqQw>=>~`Τ}߻J"4s:' g 2ŊT 1db 0?@^'҉S⠠`Q/r Y"#Y5ٗu8͕s:.aK,;7I@9^EitE'![s~p4@Xͣú^¦kTp+4?ot4"7O0Uu0x!4U$gm_"7gF|ڧґG8lGtiqee˂TĻG9& azq)e?97hjF9uG$zޢ4~cY`8cH[:V T>udE^\fYϙ1_yi5RtxḎ`u=gj{T Vדyv94`(JW ko޿`˵^yWZ+, `rNp:?7Mb05I'~ox /iL` ac@VyK]!xE$%t91a]Q}*^_܇7āxM#eRu@M=XG0a1}{vG]ZMODQ~WLted':lAc΋ۥT3IG v}*+E(ݫX":o' =ZF*ľʬ=4"f9}(ޏeo4%0X**eܣp0˂l=:ZxU,MOIO>^{}JW/ҥkcs έt9W7dmJ_ S$7RO2Df>t%:vUGA[ryqJG;e|5 iUy˹.{m@հMvchj -!hѮV{v7*cyIR&ٍM>_UBKrvf[ȻvV(f~ڇZM`:)pV9^-o_=p?)7yF\wԖxcLڪ?۶隔H;UO! DIn([kCMœGjSnŗ4DWQhB10(W^E 8sG0Ph y,]a,Lʷ1.miZjRf*`g$M4\SdԙnkŲ+ai W3=` d|4& ѹbyFzPvm+$:kO6O@Cvo*[&ab@.$ ǎt +z`DKygɆK?DP3@/SƀoJ.<'D ?*uW,vC0N< R 掮J`CXJQ~8h8zHfP9h-ز3XH4Y ً8BF/P^d5{҂5ZTVʀ}C%d0-EXFp[* z۬Nb :l#;f1Ft5,⹟tThL~FڐO4lp`ш= \\a+c{/kuυ +rVvfSfZgeJ'\%0}V%ЕTU]M$5F^[ | 5Q28CfQc[}dVEۮݠcTYSEKIo. }2x_3ae΄#HZnyMRO3̃-mAٝ ri~zx#^3c)_`Nk8/MZӟ2]BNG/,y 3g}݇AzN7"S96/-9L8bx{O-Bi~LuY-ݸf+!pPjtq픛GOy ifȐΑed=Vct6`/`뽗y͐};&&&R`Bw󆞵k;Jfl!8~[qNҬ:Px˵|뚻<^tLk"fvn)e՗a E.PAeji WY9ڮp\svUY@n5Nڱǘb~! w"{f޽G Nwh #05eF8Tvi+f<ugi߽}}ݮp귡 "qxXYmMFˆ =+l j@X'ֆvѩ6Ӧ=\ _Bkqay7k:uϜ1MnFgzDǑ-IRfVL)s)+[ޟ={& t$*9y|b*~^yŠa@ c`i#>.lUncƵ7EbC}B I NGto&v*jOlΎ&&hWb-᎘]s;TQ9ߑ^^b$#i1mklm5+zyGA#HykWjy&)O+ w*7M|c _{$&}m;5tr 9dy1&8uZs`wBkK=>w|){j\׵qxҺE{֭u-d͙Y͹<陵9aitgfMDw_'I$Ϝ. uh?PJ"7#x!BkL#6Mh 뛚yv4ܒ|,zL:6~?L?1<BC~AsPq BHn86?(ڱq'utw?'f&'bE/㕝y+PVqiX8'y{rl"}_L_7SPP>@?aЍ+/qzn;HW7?osa1r3U7;}dN3X}IN}%'oͶ̫JN?>+>)W֏9Wϔ7b=coҏPZ} )-3A ȳAh(gRR ֠xRxOCޝttb r71!qBqe6 +%-dr݌2WֽF]=JҨ82YZ>ὲLֳ}2tB$QFk{R2p1 * ̒Zj#69$h;Tlۺ п;vl=I9zI b;<U@շՋ^WelڙAQ"6; dwxϪvG6Re)=JؐP=$hQ=d=M,uOvTRI`ez[$.6 GY]p^gZ|VLymqbb' m-LG[kqHc|'@IDAT.-4I\KzڣD`ℑe} t- g7#\1;];dYZaR]\`&X5f5"q=nM<>BoGT}`]A>I4{t oCFcZ*)lK 9 .RsMrn4M.NwxX4Ȼ,_&y.xAmb> %4Yk酇L*NsIZg/̔6ԅil&m 6m ': ~޸1A r~jggM^Fqo.`{@eȸ_=2]Y^9LoBK Kw҅CRһ}1ثZ/M{*}᳏o{Tj;?lI>t2C'O?dV6 {He|*x ;m{㎙`Ź]FL4L:H =DbE#U%bny:#\L|4jLd]g<&Qѩtbl7[}GcʰON&aag9ff)BGv/+Zl^2$ߺfs<ڨ)gm;v@:pvrr0g,9ǹ V7.f`i<ɷ~Sa̝Źt'a:>5bt֕tᱧW Υ!\ß(_>Zmhm H5_]6TTг_b^U )f, Lc0qKW> =Dݯ|`pU+!P@O&)laEH0pC6*`KDRJl@#]@lfn>DHa2-m9 Va < Ct< ZvY2'1\tl0v2'k u Xe X#W{:bٸU*%.n6QPcMUy=1fCUf2F 9& pLe(Wqaj" f|(̢3$3ƘaPA0 =.c;BFpE+NƼX[F$kkƜI]<&m5PLwv䩇BV|#.VoV"w=CpJv6@k&ݦ @^AP8\FTD镐w õ~-њtl-ۂ( rȾVmH*81 P!p|N@{8JK;躧@* ,·^Mo&Ldtu03qYNtd} Ŧ_^WzUnY@Rp>+5/kJ ^E,0=c?_B) <0SW_ 0M ;KLp^qz[#}u}S#c΃ QuЩJRZLq|833 (B+kk'q8R2dv܎/ ̗$'2`ǘ3CWoE+~6M |Zsy/^TA?A\#vC{vs3@3?B28M}6"u>7CL=&3qN4<>`{#c*V=3B >7"(h+oG R%_]ZoGE KKs1ɂ>l5dΏvhR<@ (fiuT!2qE=k?ē*|W~k.l:/muUked4gse=Žu͕ߙʛfsG- )%xG"1O`Odci߅ =(Y溓~7.*db$Cj堲V&C pqlCZPr@ۄژ]~9d#hlj{L~i`B0 y6I*k񙓧O"[ ldOGVҝDvI?,:@k%C_XY^þA=Kǭ|;e5f?kcXYL.m ؙ0 @#YɄEimb̀'ɓ]iWߺiV_5WzuoڕؕcpP;_\K]"Ρf'ಬmO}gzҟob{JVh0[iaSbLaL*]V%l(4j pw $O_H= q=}tj,յ23ՙ5F{-9~vTrf[xdTX\b}iO{G<ʸo>DU ul%e@HV#83i[{ڪ'OS>qcv:1ah+EqYT: vs|.| f/4~GHh>l0 %c`Ղ&% סh[-+B#Yucla6dgJ/j.cˬbwP5wPLv갣La),ULmvq }*ly9%ЇFRH{.yX EZ52O"֤t#:>.~1znmm86ϩGE8{܋-mO Aj 4u '[Z#x٧MP-|6{;DӖn!!yr*CHs£j7+@Gi2;iFl1Y-cP`64BvUl#> o{ kۮFw&d[\vwq -Qѻgi[=8{/- $h5mWa#_O W OU|uM~_K*EPN*i1w $GmE. ATMi@ƎA,3lFiCi^Q>;:VA#buh 1m[n=̳S| F ENݡWS>lx8Eq|ݗU-P5 1ehc lNىsC$19JGqɐEq5`LԷuow'AU9E5&0mH)oh+̪hzBHwꢳA;7黎ih\#I5 4,?wkRM9$ ]xG|6 g6P4kpi~ITR偊 c}v30 HBj؝jn) q0ooxAZ0Z/Z:z 3,[oj;x ?k d!ؚ>2}Mrǵ} ?N˃fA:l&,~`a0S2K7FC=DeL}=4K>L$z ;ޥ[zz&K'mZOH~#>}{Se=8 g?z`;/y1*υ}!Qki~ d7:2J[A]}~K"=HKts,YuZH3ky]WK83nVg,Lt i-Lb:l4'DY*džipQilLVP? }7؃M֣Z@BQsfe?}sϤo] Zn~8=̅ }"_ڵ`Z[sd:.[^eoZp݌-Uar~@g&O3fB@@64LX>/SNk;hX G숿LS\ vvTmoosM_o?rn})|FLm}T (9d}:nm,J.|1h=ȲwYĺŷ]hhS2X_?#XA.tS .&qL`W\L]vЧ& Xc!.63@Ȍj~(Z*۴xxG,`Ʀ&Z ԲBaPAHŊ^$9/r:sbk˂bul44yO3<0³ڻixPO.AGڢDxdL۽D-0VY{ ( {#bf #M-*_T!*M>ڪls%#uM2Q e52w c؇N>.<$#k%`ɼ\)cUuwe8 S8Vv]HZ,]J< 79 ؼfE 6,=3Mb/A鐍 r!5.Yl(e UiZRB L13ol>C%d^aj`.!$Ӷ$D :IXq]Oؗ }g \iG0ipY+LzѷBpum{K#%6PWa2* Ys"hcDr5luPaDuE^U`"[~GMÄ@,j(&<B5vߙop5^Ro[yFhk<$8~| E&R|.j+o^M +ǦB?L>/!:3f.ς `W|d,:I<HVRpq=mq"ptAgW7-iŽ׈$֐GA2}\`6rN$Q Lα 2:w,ԥӨ{S P90#Q0>Fi8GT]Lf[-2 kg_x"Hf :қLp[XK#c$Q\0JWxH) :=ۗK#vCDn.IdtQHwpd ˇ́ޑ==Jt0Gv~|m8YYDsfaV\*/ +Py-sFK5te!Z+8|5sB"㼐 B2 j`.ZTEgPөy=ٖ$ٱA ͑]stў ˁ e>ݾv38Tp39y!ۿ" T Md( t3[~ZMIbTjʛ<=JX-2~T$u6khW[VȀq։俉Qj':Ds#`T>6.Rڄ3S[P& X}Vhly9ΛVv{muu/ Zka2#]#)"4z6Q྾OGzGOT 5s{釯DZX1fMPArǢC,FV{[MF$e}~}M0H:aT1v{ɳ{t]RGGU?ReAPKü_K_|F.TAxg%i 4@h\Zp\\%jSi;ZlھgoU&ܗ\k" YmD=w X`|dDdGmO6kS_OWPOk!0 vb|ͽYs r(sLV|"ޝ;0k?͸ұ-Tz_~t'g}ƌDnXD).{=7b26}c:Jj[f'{47,nc>HH0_}X'tW\5tOOw?]q*bUdſ^^[&~ېV湪~| @E$B6V(7HT@yI0vi*4gi oHG򞝛'!R VzG_U{y>Vd όI& @ܸ}/X;AzmU՗CvіNF) {We-6C>)OWY ioj`ݷ x9W"KWIBm0^Țg]ձ՗==&X<)cke$TUКU=?%@?7O,״Z-5L3<%BP |Nj0@gR1B2%ջ$1'&Xw|4|Wdi*f'Vʢ \U^؉ PY@&6Z8la8jǕIن(03]*%.$0V~@B5؛1yMvBLI}J_-UOjGH}~F0lp=;5 ^^aTu*X ж7CNc3qb]5!t!W74 Ѵ Rn~.߼  rl),_*[S/PE:3 teQ c\Ц*_ZI2xKG.쟫n? ɉQƺhr&e8`z (~b\>G>~ݑ4EW+Ty!sDхPط rbe{GtlV̉uV6".~|?;$*UagU *=mtyĽӶ>o@uy*H"DnINtMFAștFk|yp1dAjO?vD?yqˑ2%-{:<9 ό`k#`umƱAqK 輄"6^/z~(GLlm.Re.y+qToѐKpvj+j'c=1xw4m}F鏾մ,F~+iyݷ-xvM ?Ebt^~J vڶ]Ǟy@ aM謢%Q%٪w%bD0< 'Kk#I m׏Z'Gv23 & li`>\cqiH&i8\e2x@%h6 ,F ソi(!gPD 94R>1}MjeB$qD@Ұ8fΜ;]}_03f㏖|7 b-R}Qu{oHO=8qtΞ kvp]Es7?|Z7_3qPrhcpX4PD{OC0=q/q *`V1F8DX-^I^-a[dg[ъMCzkWfZXl6Sn"mIGU9 ֿ?_yhO)IAJwfʗ=,5|H}%dF@~7ۺjV\E,  }0U`odO -@(2ʠ[A_Uy6UZF!cM&N  zegмFmVP\3+WҬaka]X>RV4a=ƖLщz92ci}0oIX6X[S:+(~?rD}G"Kv[[wA[U=}Yo0yȧ:rQ- N#Z?rbg w0ݽs%ݬ6fi @3Fumר796Hh7X.L$$ݹG=0S1Ypۻ78cw9;Nr+s89>嬈: lӣV_.`y5 ]#L{eaD%IƜcv|Yn",մ] -8miLCe^[:>4^\𠱹%YU#Y0?hۺG={}]/|mrqĦyi'+8weTDӞ,l+O,ĵ/y7ߦE-Q Şk/G<2[Z9N:_ot_*Y+Uڰ;&L@%D5]wy:/^H^^}Ml l=LlRnڵD~WǏ !;[ozw꯱q锄v!2zЁdl%] 84Q!(;Z,ڮT}Uh v i`i@۸4GNLN J3Wt bF"(ܥ#A@NK9jNvz~czkJj Z`m J 91/hBBf}Ǭ^&"ƺ煼WPBBi'0w)b&六{|d\K3CdZƬ~5YϙKy]@辎Q۶gv~5?{1?|9l;J1B#_ԭWL<|Gɽ+K oc.E4?"bbsR1H[/s.m5 ]Ŷ♧n5ɂ A0M\_$ѽ䶠9-x?u6 $ish>7K#`ID$H0+7Ã6wzKt?d@A=E!;[N}a#>ݮm̅pNw>!f6APćyTz Id+#AI_NQǯgz49{>0'~6=zQ= ^TN2OlAr׸,V9&0l(慎>̇3Gn`w}w N9`jݎd#<*Cلu]HЗ41:M-C>F)CޣX{RW`U?XJ+TJ{ܔ؈PnULYcvh$AR^0 *õ+`^1j_!҅|^ S%w9%|VF!Na/>1IMʔM4ēsD\-AG\г묝j0v N~Z5/6PU^QŴg[򮒄< x:xې״ ~Cc]ԏewU@tRu6rUJ}% H §_@IDAT&772\FlX,(Ct:LNL\'`͞/|H@<ù.R1|_^QWjѵ:&Z5fo-"+(b&VA&Dp5c"oSTuyĎ+;5qN@ψ-Jl#uArq`;q @.|h'iP[4` [1) BR+w\KtY\\ͅfE4bB]n97G̘$b_g9ZWL vA$_q%CS¸QLaXn8>5ui1d Q1uMf =\L+x(NWȽ8 wKtX͜HNYV ^jGt5ք[3TO0|y*y?WNɇA؞;5>;3okzhQ@k-^P86vsR$qʼnz%i+sFF*b-h x؉ @J=*:#@7u6tp瞦ߟ$]uYڻC(*h*:X>3edF)i9yrT%eiӎET#$AsQvs|2c{]vVJZ C'7É1AvQ1rNu>9h;XgRstI= ާj ,|W#!Կ4jC\P|H,*sjD٬o9I>A ;F2wYXMhCYD87D6E.)ŃkX$aMC( T:g]ѯk-"Cwgl^1YЅfC2T,%933*.ΉqbKwuwCr,P8@Yκ B4#0J\؃%b)vijJ_yif",9Ws(bfSً!|p/!noA Ĭ5BȊR0r@J 9\^Sbˌ]U;Ƃjm,82T FBAYѦ!c++ ,r2K{KCư#$ΩiqV_E\6!)USǘAFn}Fƶ-DSr^۶|$ry^\~!V`{R614D5(#8 8fc0~Y:ȓJDcŴn&S cJN'0ӛozc4f(+H4N$J4 U#wUf%k`-@$duηNDiJb&JWa[k1@EpG=˜1{ls* 0@#Py5# C[F =Q5"1Osۉ:_ݱEW9LQ*4|ͬksr| )QDq'+_*ċ0c_m8&.c+.0. !swԣSy^>m0 "ċ9clC.F;Gc#Ά2⬆3K]H9[}co2Jv%SM+u !g&Q٫(`#HlBD jm"DЙ61ǠK##jɞk0_朵lDHF6ie~4[X`TJVN.Bc+XGƞ/fd`,uK\Lz&I&{dNa8hzUJ_wދLx;_#ť H(cf+  ,Z,9(BȔ.t*7WCWB%FpN<r_ն{=:pȿGdJVu~L,)ޣ 8Lא5woߎ*w-3d왼9x E *I"pg@]-8AM7[yMJe c>x]`(x ^*F 8{ƞ:vCtgp?|츽tƇ\gȬZ sXoetfO=DyY3{D*D߻q=M._#V-̄~MЄx#ls}0voXw*Routiⅳ'kLʛh.uTOn߼n^BDV!&(-4z; 6p5H D< Q;jq*Vw-T•/ETo޼]N't bU^VJoV6biC)4lo0>pNƒζW7:b_l52Zny"#)n>udp=/o@QֳE t$γ*IȺ3KNgҩ~*H؉TF,,n-9;/jĠ~ F}CzGliWۑK;@@=iGwy,x6yh;oUb'R7~nҫ^ׅ}3Z{h)ٽ g Xۗb|T2Y^WV*N×_ZG~Hdu?_ڗc_ |[\h=Sk Tn:)=jeKdЩ$S?r 8/#N]U'/ Y/Szo T&|?n& } ]9_wJ-;J7xB[ȸ=Lb)42"!mK#؇.pl}uP|.XF۞ :2T<2/)?~'?G~{L,蘶Cw"H'Gc00η{!;fIR~t;?ÆJ?eL@I[e-WL7=Ɵ .{$C՜aZaaKMLV ϾW768cM; .8hG׀ O = JBfa1 MLc1I¾Ad1i~+Q(<#\m^`ElEU1KR`+Wv} ^ڼ&kmE$zdG)`5jA:0F֘J~]589o{yze_b^Ӏk+$ 6Qآ_q&juLp=ND|-E+ʕf0aR m; i,:؈Y8vgq M/O1tuɨoj4l6Nql\_UsfK_ Zf[XKq6_ md&3֕"YqbQؐxT/,GRʱ>N*7IPl[y* yLi -[}n *{j+ gYwUʋ<%i"cbR>=RTu=ud!tl=έVY"*'Oq D5J#7{L >ΗSFyB69W*ibY`1btb] ]B!U>]6' ep&uc]]XdQPD_D%0PdPV*w٘H>30Sĵo߸o_N=XӞ^!c$)h>Iuئm $`~;'2Gwt|x;WX}=WT1[54pyYy]L^W#xO[fIQA؄ClOFځOJ fB#Z Drrqmfv5n(@_kفY\/;1 (~SءaGЃi?9*\/i͵2(~MOm{j\wq8ͬNV-\Jeь];E lܗF~)qˑ&ͷ;2{.cn!`$g?]G d ىNMOn$X(WTBZ~oL!%q,M!s?yr)qOv]Oq8tRY`/k|w;rH@؊?o}S&!%ZTT!v* lnE CP&y5lq6ea[H!ЃUCUuu],1>L=vC?P2Aj'/y}#{{kneUXD -?E򼐌amh6R,v 8NRI'2]$I8͵.m FKYZaY (Iݴ)cByh,&Z9o"60-? '}jU 5^T$7 BcB@qf5f9,:, b:N <~ϚuNDuqf@;Щ_(`[Ȉ -705hZg->ݻw믾9u8/cmm,c쾶24X_C1Qia -5*z $QGG9t枸.-h+RtTfFuF:xrpv~O Fӡh>4#!B2^Asi;4ˠc񡣣1 3<\˱`A\Ccpdv~ EAT^hGca||kqwxxfa 'vFu.bGhչD=fǦS!/[9kՠB%mFdl$;V[XjPV 0 dc|s>?k>@c _0%ni;eI;CŮ@} e@2}!zKSVܘC0wt6E\4BqiL3.h0RW#ͺU:.D%}3zS4v&d"HY<1)EQ71&?97}ru:u:J0)%;wIeG79@m07!@g$׵k/(4ʠj kX0tIQO ƚn-׫$C{J%[% (/p @-3u(D+|H Y`Kl;|AЋ{%i-a%Ѳ[@"$ܤZWs xK90ĞbL%}ۚWCO_EWH^GD77 <{8=;]ΏAL9f 97ύr&owTԦ9^ :8ީQa۩ԀͰǺIvҴ2C>0_}+p&ҷmTo2,<=2:r@cӤNXC2 "~g|lG/-ߟ3~sfXao%wߋ%)l y9%8@NR@,96bɱ,ɲm%ކp\Rr`[~v3u_BIxT7 MeO/&%ޜbaÉӱTK@*= 'z/g(Apºl^M+0j;$S<z<[뎯]hTu?{S e:p⒧x =4\+81޴W%9mZB`57%\gKY`oifIYLf=/:eK_#it܃ǿVW3@9LIWM h$hcwp_^k7u]kǏ $Lv!Wd܇3N`1~UZE렡V~%Gm{ԯߝ`zbјn@9Zr4& [wrܳ%f`.gq$X7xΪm^/ v$U\ݤRႽ^4M 8x$_˿nIHmY'/HM _gO:8Uјm|+:p@4_Jw˹PY=uU&tU ^b7eckr! N|LɊGE><㳵[o-m0ho$D8! Ow|{TaTxnyov%ԙw_tetVJI\ejzؖh{U-]+<WƇLҷ{τ7ƾ-0yNowv=슷s?,%kq:3޶AZI??p@ue2Cn{Hk^m50TLǡ{ tak:]>`oH1V|&I06}hk7lIwJTe-d5_u04V Y~vb6>4U%:ܣlITz[F=:hSf]cLv,;o$!@czS"J-o#tZW ;=ZQR*O>C#5hGroC kFV{K ulJ8mwp>]us:u,/Udl/:]UPcOܵ<&7_3gz|lwN%S:$Yw3[>[6mfmGkU' DDϓpΞ-#\S8o]X?H._S;KI8k~ıh?TK6>OBnjn<}>"hTZ]vرs6MGW$lcȿd۳,hnxf\39/oC|0n~?vud\H>t#Й{Ki%d>tZ2Jڍfס<6>*wdWD 7nHϑZRFu/+RU|cG׺Eċ%`4ľpw~-Ӄv̀핿3}ҭu_- ~ࠑf4L|xnY2 &{ͮ뉉jh""H2}+'|W :'BdL;C~m:ۧFKM8VBk`-`qfxOiɅS~y*ǶnRKo dٞ$w~!XǏn7bQq: TSb7jn\1>cyfr,yY ǰ!Z%óKWeSǧԾ`A͏>dś^&8KЙ?~Uꠌس󳍘[w;ՒgCX3ёw+qF虝B˜g/p7Xϕ IݹξXd\V{_~?{P-ϝx'?^]2^ז=dK[s=]<1'[Nzt@ i%1|tnzXUW/-tbw8C9,p(aI9)BZѾ_2pskSbs[v[q_s9{)RU /-cWN1+#nNFlU[_23Bq</<|#!i>|S< ʷDNF{e/z=.KƵ`ș2{ ?Z0TeA.]# K s`[:7oNGO FK f"z x?2]TOKoL/ |fniVIڼ<+k(*YWwq8(Ti[vg ‘=}MwMه)9ڮonS8GG3JRlǽ1rp`#@F%='/JA5A !^YJ콖we(I#!xG8.Ayf= ֛CG7e%v~b 88bD]D`r bSn-[_E+9{=8)V=K94\9yfĺec[P+H*Bi0ozqQ|db DR]cG׽)5j+SWg9Q1ȓJ \1 1{SkxJ dS: @Ӟa7WcOa}Xz!+VzǴj17w^ߛ8>hPf 3t .rC( e-0z|;/BQ^k_o e0g8!oxf3Ct~F F)pk>o*j$Pm"dd\Z= <@1Z'>~?%1d֋>n|]Hj#;e Z=(eo?G;8{tR]v<88օ*]T>J’e7&!4-3X=)['VeAg:|w$S|N1Ux {Ɏ㻝>5 !2rdfsdj=\/^]i?e)%x0 25Ҵ='`*TC,¸Zb v j竟sUՁ3g4 ,8W:y/1GV;nSo֕D@Hgץ˕"׾ k|I+]-cje?e{(sėE6k'th?ϛMiLS9t#쫵eط/=^OC X^f?O}p{7:tgQӌ7}t3WJFj.յVcM-F_)g=3@wU;@Q{7k"xRo yhs g@$L$d938\ML͆)v*XzH\>9lo! |sKbˢs"1 U]#aA7@Jϥ9>t/E_G"AK]I{1uTsad]lTYt.. 6U˕,>ΆO2=pl=k_sн]xV֯∘V|% 'DBSn-Dz3Gp*9So%_RN#TꫀK6tomϞM޷GˎW FG!_2:q-̾VsUO_mk gޣ|V_ͰޞK^z F ;w peUM^F<]w0 3~8 ".^NNC/dB9!jrgl<3gbk]Ugֹ0t>k=M|􍯿(q@L(ǎ4 u!x *-پϳuBtT g dW66>ZKxw;]ɮ*KerAAhל~D?3Ɔ3dLAN:4g?`]rdoi۹r`{:ч}=$Jckŋ7:5z /WZmlG9񉗜3'p Up/ٻ\zk8 F2zx[l| e /wNNBȮx35-9͚Wbt~&hC035[tx"WrKzFT=pZm}4xt0y07/]CA=f+I% D#}\'"I+MdL\|?ϑ;ׅwtٺ޽< G2&U>"7&SllV3/O"~|md/g.wk%Aqmdۭ; $U fiMlH_t5lmj5]AZ\Qh71]hk~}x'?t9\s5;h [gG:^%Dubh(]ָ乊;~8uk%g~P)J~InH%<́ Z7xbvF{\Zb% M E cXY+ɝ*=s*wHO*n왪N7~$ 䍶ѕ1O*ӓ@F hTpS,l4VAlXrD@P6D,-%Eة\:i=޿wwe͇&>yHl^3~{;n>?fs I`2/^#/T\/ ˒!TN=yr!(]Qc׾l/rzpz|+*ۛ\t{IhAt.>|\Ǧo;%0{Kg¹ &mwu!h2=mSHf_ӵ7o5JN]ڣDvx H8\Ϟ=3MA۶KlB)q]~Me??«*i)^^>0۝  Jw\j/ Ӓ5Rt(Zw~1~jZrXR o 6w,\g^xřt$NC79M8G3n_ 7 z_GMcI}t:qU7*xry^ELDѢ`p7::X7wu%l0].)pk ZV>FzXthz3o/]_2ȾPR g-e ?~m_U6۬`9WC=ur-z}%ݯG*;.~ |ߣ*;YEݭrׯkNL{~w{sqzJ.Eo5CRWxm"ԩ7z&e-dvVMؐ伭O]4<@KG̉::5W]6.eFUE{nξБaS{Ek?#/wOξeדQ|zPpd=\6gO[hrCj6'NO72uJjq 874HKvgo. s1oa&y" ~_)kk_gDe3CiÊU]l%Ʊ?g:*Y'<)Z(2n}@n&ΙWl"'T!(,ŦIU+קW? Ҧ)SeF2~8XimBU\QQ>w#6Clm1XAwo?3sOK}{9+JY;sl_NׯN=W)S"=Qۉx)KPT eY#'xUZ>δ-#S "S8(wrLj8Ҿ=CC;%@CB'|#iPfu[+̯{~{2VѾJ)|ha G9dΫht?*>654Qߴ̅K;.Lw0U`{,pR)p={C\wl/!mKUZ%B_7̿a(DGh]9{ڠؿgr 8({QjPX^+4 ֕Q!g> Y3~ܰI[{5O`` ';fՑf>|dy:λ[Z7W}jMEFKKGɎe0?^OHo<<^e]ܳ7eM)ƻ $T\_xt>7#Ι7[G־ʈVtE)Hf J 2~y٨ ͻKh0yrh>z|72[עaǁΙOߟOWpolmG7VBIzA/yGu$86YEZƜ=v|vV?+/ Q$r+HF 93r=eG{fꢻ)9)_ )׋C-Rh4gxO|)7r^k=e_9KVrƎd|~1~! ="=R%=g2VrCczŎ[De_3OI0t~zo>̕`)#9GG&tِ ),hޘ(t'U }3}9=m*2XJݭcN1`4 [JT7cԺG %!&ַ'x$^1TSϐ`6=ƗޒT5YF =Ӿu㉜>M!WHl^D~y~no1>K?Ƒ̡1LHZ±$,auz }u&ޯzO?s:q{{@j2)p1E~^mtfBJM}$x*xF@IDATn%Ǟ d%wZPep^ ~ f94\lTF0u_?rd[?ϠqƽA89v8I"M' S=O0Noˁ]Å▒Fw_u=]L5>ȑTλ ֺ?>/1ǛENkKz*ى6O%n!=ozr)󬗟9-LY>_NGugl}Vŋӭ4kHC?mg;/;G w-}遪^.*w8gs9fe٣مm g3{:F)+1"Й&o֤.x\gtJ(:p핓_BǼHRy J虽9!9T ģj诲i^t%ƍӵFPofÁ_f߼8-5XW* Χs?}$:Jn7#:Ba77b(P>~1ȧpʽK&9>ޙtWţHA<s4Φ^~b vl*A2U <]Kx?<ݿv!-sq׫3$#;Է?eP}|CC]_>q.O9uO57{yqW"%|qbl`KI$ݩ;O$ޚN`y/%D*F~kxU|!%Q4x5{]YMA䖒hxtlƶ3jpL2Bׯŝ=t |>;ϕ4*ˬ4R/~rgD'%{}ex$5K@ƙVIJwomٝq zኻJt9>?~^ -v`/wmQpęI2{<-?~ :-OKVh[쥫wmD _={wJ:f5C6]]iTڼs$IYtdb,) ?t((ٷዹߨƃo(T=|6ڠߍGs#I/'ܨҭx{{=j/LZ,7ө#(0k \׾`{ T3J~{*->%E5*t]Ю_ wU~B;$ . w| -Y~ D"2NwPkNyR±|22p,t[5G{`8#$)1|nع'r:oIV}1?) tl;4&Ut'ϟ>ayW?Ua k+q8.Kr{Q7Ӆ+kcζ![ndb12G z$Xֲ,45Y<| :mDol|  l( `.CK;PQh96\lF`tv) dGBtݻgF b}\@*{fgS2g3ؕs`DZ@I%=dNLe*ý>ȕS?3⸥WAH֡>1Ow̴1 , Ώkd\ EH;ߺ_0^U "@ wpp9k! C/(q. [kA͘pDV3=ՌAMM#pv jAS^6kaI!&p#qJ ӓǏ0^H3=fΓ]}s r~ZerP=e|̀|U%t2Z3Ik;9 ~)xϒ$I H۷ytHStd;d%wtx T¥.-9\bEj eAƉǚNe52|>"Zs Pê-?˘t?:6r؊0D%ڼ<<UϬJUQ92;< RH\/KW h 6ݐet [.fI r>S]ըJVad@npќ` #95LrF {g|M'=}.@}A#v#+F@C4yp(yrp FPu0J߻}y]c_}4dfwF'ӅC)0ZŪ!H\1;Z^xgwϜ (|W~eYIT]Օgs6ëupPs}vi"T$3rK kؙdAwV^ek$sig4 /tF{osZzha{GI8K`-`C/pb<1,U,lëq y9aQΰ{HrI$ee؂p/ɀn&IYkIF:#W>\W5_m*YLm-lvliu.{n3o ײwlulbf?*6LjW?.m:طI۶h4^}tA=*@*4UڊerWdwSt>y`+Ι=9&80Z΂2sx}n|>xq漴6ݜ'ydp J_>@o2G<;N:w6ҧ='y hV#W{|Xy̔Ļvcm#Y%'[~Qss2Q>6lQ'ϷR:Pb##(qw\V8\rD:qlV}x]pOf$9S66ɺU:~O =pbȽ}'[1k7}]&FDYV. TIs$rс75`?]|ӇEՓ[{}@_g>`563} 33;)Lg{GͫGBD=r쓕+=Ewn|0x`@NdĠ?|wa{ztL貪*N9rӺi}vq:d:\3g>.8Qc~:'i g GnZ}^l8D}I#; V;aaol 4CTsACmV7|339?I*HIgIxm^uDs3g?k{jHYO|tTqѱQk=&; }}($f_0L׮]n>NU`K'К}v> hwk[Vﺈ_/N'f7'__\SGU~x1tl9={YJhsj‡̙kBJ{iIPȄό_zrAI^ħcxs> %Y2\8|zRx[0s%V@8qϼLK:HnѮ\,|`o7{G?8F{T'/\ ^zZsﮥק$ɐ#0|Q!zx%~W%2د`ĉFfo/7 |3\o\4ݾ+Pϝ]L UAӕgɠ$h?jקC{* 3 ȉС܇"UD]>n+ _|$!B}If '7eO4.,NDw|~ϼk>%9*avK#A<[gҟ_k4CIxx}HӐs%. uM/W/`t"MgPC+$k;D LV~ٳWuF9a9^"H@+}ĉwrD?=;qb%;vrȅ&]?۽OՍЙVAxሩz8p(w3mZނ. -u8;WQ|zU7"w_vA?-!fڨ^>X 5Zf pf#U,YtQ~iiN8߼^k݃|cN޾6m>ⳑp3ta ӛ컇S'prFӑ%R疲L7YyrݺO_uj#t;#%oI4(=ZUɧ' ~~$!_'[Jӎft5{NoYbwvB&|j#qG-Ngte[W"T_W\'A?b>փ5:-LėM- hjt-0O#qfZ ΑH Y`W}EI);_Asp5F7ccDbQk0f{|!*v&{S)pp#^WَF`2'9Q,e {X3ΗDoC 8ķ'!8و9iރZb=@SFyTЪA/@pc5` jلeҴ >HI߁iCp]G9B[C$:ziRѪp cBfg96iT5c2/rIY*)Okͷi"Z6@ֹmZJ՗WjGBI9i0`LXXThM wm.1͠]8=!NT vn<3yrs"8  L4` c06hfat3n=u_f,S֝{GpHI 9_Жԫ@x<3GNyP5Mj g050d9BmU l>ߟ‡A c1`G4uwUv&cj@ .=l3:vsd?LlRQ7c^kF[)q]PFeO&((&dN ?m˃ͪl-.@+e290iL/(oKtK6ih xnX2X@D%wY39~ t<ЬirT< x2`X2  ϗm(H sȕa8mK6{\H䝝4N|Lk_h {T{ݖ"^RN'&%Bfr]h,Q={gD}v$ȉxĐ:Y5?':0|e#>`۾eQ>y]0j(Bq?< dM4g/f_d]Xd-c?s c{/'/+ͺTiwOz`Q%w=w ,s\p сTn|*{Wk94&9s0`ǩcG7dlnKgX{S2JƚJSNUD2h)Pa&or [%2VI"4V%l<\G84H1B  -Y=8{Ar=C3XԎ9k9Q  &U 9TvoG GEnIs?wWs0#Jhgkw/IKkSŏY4l~j[1 zd0vp AWhC)[h8oO޸ߛWSm>Uv}9)[-Rx0lUUG*ͅY-k0 ;.zLw*|ګdHJ U|oFM%i3pVYkzâEc L2oWr(ϏXسxLOպmI=}pЁ3sNZX38*"2ڳy;sUj!y_tV`hg-S a+ _p߿oy n r{mIUCj-H=%Q!>[g^d(؜_z8 <8mtfc:K:g2h&N@Q@ i5͋UӞ4}U :lN?2l]z_co3m>[U&dkoe=/:.S6;/.'z~9ϥ;*iJMz"G 7: YInu /R9yU>E% n $·:Y|ŅO?^uv;}s}ݩ[G>`(:[^^IdZv2zD m%z{ۜ .*ѕ `g?}$oUy#il-J0ŕhѫH2(˂={f&+SȮX7h/z՜m/~>Ƀvg/2^fAVNNs#3:&1~4Ϻ '^8N13VL>i-0.+xW|` +~A~nλ[vdAP`^_HKLT?sjG-~ݗdGAA@lFL1\x;νcQAbt&cF~/ r&gߒ/M ,JT .{#I+_sswhfq]ҩj OZ*5ւu~x]F+?Gp#y%[cit7d˗6<}?a0rCg!.gmb4㼺ƵkchJ@ I/H&g;s[@Wo菎MW D%sOKǝ5ׂw{/\t;A!XݕYϝʷ]PR.ؔ -*a㱍*"t̫QUUAYsMƧ9'IcS#O.Wx_Nww]W kބ 6^CNpS ;gy?K_08n_/=HvṋEg:Lqgu]);YAoB#;Ys Ft:c4>ݪjO 7gR Fx9oY*Hiew4~ޓǵ/8J/W/,ZKQb> fS%@{7bs9xzmJ ݔu"yEרO_3V@KXM" m6,%u^42Op"*UER7F">%1ĞĀnޘz@K"kWh2/M_DF~M<Ʒ?2i`g0V{>ﶇ];Z3uP7ﳥzy:}0|rxGb%-Td\}x/^trӑǞ G U?@˗d Dכ_|sڞdn8\w9]AL *1=V7kq\__Y OC.n1Rea:R"ɑT$I]yQ'f{6޷?7)Wxn/ QGlŚmx!|0T2Z84J3>@St(H3 "NY;#@+p/Fsd^;00f bhè`mʄ&0a 9g|)90J(S8r ˮu,3 _)g̴=aܶQ4̃FpS*sL;03(CF9[kRqY 8yPoӦ 3??/;ZjgUX;z/ \%uSQϣѐ[_y7Ve͎|fkΏ9,s)[e(7 BıgՑ ?l)\;&ZF },0Ĭ*s;Z >YTFzJ޳YͲT[ r$6  ){jm{8) `h89-( oNO[(@]:;f&2 8 dZc36 aR;R3k&|h.tO]U0݋b@+~%;j87sP9<8A Cu 3E1kj3˼L||6>ޒR7r~aOH/ߚ??}qZ ֺwikZx4~ ,j`Iļ7uXKx;J̈0gN~[g ocJ6a!)}p>ڹIO7#M (-e/\vF9BU(Su8Ϥl)1`0KhRPA9d5S=oK+'? UȰVt=R_1|_c4/e C8XwaRI# kס^0'pDJxc$#{PNGJf(uB^eZ<mHhe%~0[{hs1IdĠWsV;Ҝ I$0D3aM~d6C ohTɅ3)8E`O-c lۮZv$]Yƍsu 95>P)p\ =O%3OE2L8O#kѽW`on#2U~g)f ^ݳ`tOYClhq}O'|NA ~/UHm h^Uvw0WlTsd}hyv]UG C_s8 wGch*i蒍{<9qn~PbA>x֪j [s;- 5f ~P+ɛ^!=yө셹eƵ8؍䅀0tڞ/<8d=WX ǹw#`p2}I#s<݆FbVq$3fӡcM ^J+Wh֫ g>o>y.pVŲ$kM\}chp2G7o~Pl$zUd?"b7^ڕ+Zʮ=q2%wM=u?}g싋_LbZz*F`ZEdﰅǀiT V:fTիKcT:|3יn@VO~ 3ndkLk!J#O,y{-w悲/> W5\ڷ?~`oeьO[䇩 k87_'M;gݹ_Wp|tJX>Wd|RWp>}1tFyQ%-I fHޅ \tCJjb簵k>;%%Cb|N] ~,m}3v6:mˊZ ]l|=^Kx̎3SwR%P\C7xj9-3iFӕ/“l]G ɠ77ArTZĖRSΜHbnLK&֔q'}kG0Y-$ŁkvmB#O}\(zu-xN|8gB~mɞyI1H {%ˇjv\wMGVpk{FYg2}ڠ =޴q:v'GI] a7r>_xG=NP2+0'Wg* KXP HSaUq͵~6wUHĝNYL| l:nk)wzYGѺ]++W7--%_vO~1}G8l9~l-]t<0짿wz`>k݇?D׿c _CS3ZsC֛twdO+t?64rղ7$[/DSGOݙ~1mm ۻG Bd9 k%D{SLuZ],.NA8N/=_!W_3ڮ(+xG7*EFb=ME\䚎ůÅg%K^  >ᆰWgy+:-]c}BK89ݒm y[kE6ϣO{ZW %BW]Lvz?uOz9@N*pڵ~~22|vJɋuHҧ^AxOqEf[pzileI:<2;~2?w赒 ?y8:,Shm_DnܦkO /7|&bd/E'wKN|dm禋7nL¡{Ō.4X~4*F[‹*ܲz5Rpoߺק'J0v߸:hqێ ީ۫GEzˮ]1f>vs[K֗.zNOaID4nbzt Bsγ O:ׯ'~Ca{z[bRE|xMzQL7]|ż '|5 >:x3:;,Ƴ/|;D'3zetFؖoEqxl<#ߙV|o.<ޕDZňN*nAta: \1/ncqMWIpˍ sW+ߵ3Y^ ESV !0:I|[F^C@ t~Nn7Sx3CJM!ev) oU! Se/\ܼ֡f=;fFLmfUV}I>7v.pS O9ra#FӘ6cBȰO1r?]BB?5^{s 2X{Z`'LYh]gP; Z匠 @ʠeNeoB|F ;2 gղNdhYAqHxYS88p6={ 1Sl[j0PU̞,JU x>r]0vZml2u_ܛڌTW e9~ u S_o9 ze0ZђCyì"ƍZonCq 8 )›cL`?}twd01Cۇ+g!ϩ}GI 7đuyFBiW[ooOg,կAj-RùD0cqW;8; Nm]/|);f) 9oSqR3xB]wςw.@IDATCLem=9 8p)x{l=kQùYK:[ke2P( ]ܝܹ?f10Yvb:D0Li}Wf$øï(az(+)G;ף ӵ֩3c9=L xa(H*\5/N"%'1@B3fk_oQo~ڎ5A$kao])UʤzZ9rlfۚAq?yEfFu3Q p̅#nt6-~rJM6~Cik;] }Z]g3h 9$z8hFlKIn=i$ }mP/C?[.-xpX0kp~x=Wgw&T?O!CzPu7%p$_6mC9>I9t\z+\P5[E5S'ncCq(LP] @ ow~ɋ@㜰^2BV3VPнe?~4nK^p*Mә#qs`oPfznW?mΜ9=޻)aW}8SGU.3wSFU_9sbӍ52ȡqI9ݏX8R0^A;}Yc$a$A|+ϒ=u|$6/@Qqg`dD'焖FX0?sR@ddc&ݓPO>|: g~i6~p0{<9g' ߝ,ʹiG~=xݞd an@ɠ=1!A5j fKG58SF d^=&Ylnm^IEq}2 xNXvFU=Xha g+Y h `wN7ڝax42L~J/::s-]dBsd0Tbtʨo{UΥTPh@4Se(]cVv#kt(3;ukn*+^C_z[|Qm|(3tdqZlQ0w+ZYڻJ$r=KG9*=vi 3ڙѧF?J,uUd,Ҿv<_\ĴD.I cW97)#6mk%eJ3?m-5TW /b_8BcK;֍G]=}{YGVA*ϏL ,qt. >lד7f́; ~(!%tk!Y.;^]57?QYA|=; Br$gp-g/.5оƏutb? R)?b宿ZnUl`\?w<{OEI19Ҙ#U?T{^oI`]pfGu;ǟMϟN)ֳF:سUSwe>swƫ%H{Ndd̙{G ܇%[;l, J@@S= ۱o>GFw1$ZsiϒNU5#}^6zeo3mi&iє Ѧ=`ꜣ6g&lf1+hT4e={3Vt#tl莳. [jތ3J&u3vP†4/y,vA1F*aKnwz ֫9K3W4{E5ym##ϋ},8Yu*C~a E3/uF_ɮ,MN {vuLs8 NbB 2`phfztuWH 2o q`ŘSs{>Yg zt Lu}A"]2v`bȠt~VY>|舯.6>~64ͮq4'OpvlnebE3|Q::*t_P@}tgntLEDϳ|Oln*e m| :Q!yboљγ\.K׾()%aS'O N0ݞ=|q{&1%+ S*8R@1YZQ#)}(:<3>΢58c)LQv[W<Ӊ3uQH3X~;֢˟aqLʿ?JZ#L߳G׍֙xVc6K>D_hp ƗY~HX䫣O2}W 71NN?bˑ>GfCu}{J,O/pt~w'8SGBɰkM,1qsEe47z&/%⭥ǭ!qu|`}J-?@חbǀ_^;;k9KHȗjaTX{] Y6H^\6PwV0x EC{ts6R]=$Sf؂=Qg' ,JlG[}ϲ{^~/L_O?  /F?;Ӻn-]hMvYbxsѱ:rx֝#f$5{Sɾq䢒+zIOd._xVbm|ZY<4ߜ{.I#tk` Hfbx[ZWjl{QV77ɮ$ -,d4}ޯȵ>¼X 6O4^1mbSn4{W mzF>9 ohP!G5w\ P :\ynN@ 7m15̕@4_͞d΃ >WКJOr1FϊXp8誘=j1һZӎ,pA"+o ]ggq*(Ka T/KS&_۬Bl8 F&)yH& jArƆ ѱEiR3 VNm``ˈcoP*@~+Sš?0r[xlAtY>A=30wڙ "d@vkpl }~1F$=édNtsv~ؑy]oqHl"g\E@u L@J}wU[h") UF].sڅi!#A _rF{7\mO zvk[ު_&70\/#rT4x+AcC{xdԆ$mq=[Ftn)?m6dn.@z3iFFwdçϟ`ғΒn,PB`ޖYPR@gƛZ~Xoʵl`zP65eaNJAhx`e(ϩv9'7v Hgh/AEˏ X@8ħkA %XZFׇMzݸ%Wx_W.Y+-;kZ%l39"'%AZZ z5(K(Grk1l yxyh(]=xwғ" k=# t16IJkɾi$>}sn.N8>R-#a䗗<{'7\n]CUg2/5vo4vmk2@gT `$d2-:a`sњ;}6xpδ@uF3b2f[?8Qu :VUtkn۱ƇG4In2Sٰ .66=0~Mh A3وKF^&R `JY d]*2l|0PEk#8-M"ݣ2{wng _zok FGG_N M`ܞ<} jh3ـC+e?H-+a`ϟI^n޾7'ΌXG, :rd]tP&_y7/1 Z>؝j6cӝ}O뙫%DRvqW ;P;WUl-nxn2>KF0Qdv9NrPܯ߇{l,1Catu֠- ;Uu/U·M&nGZFO2'$GfSqFOxÞȣA3wKr4Tk}~ `R֔&C&=Sgƈq]v?'1fGa QUszP9^tws6?D9뭱nl!z8]¡f@z(wߞս{%jn~TIuͫ@U;E^}ް'Z"#̙ј="o?P)˾f[;nm1:]pa|O`g C 3RE: tC%ɡnM?|q{|vd (%$=v'c,Σv>d 3-5;xײˎ*t|yl[a:1% Eo.+aM;HFFmotեdm!:) a rXn>t7zd|*#`^%C_$+%.˻29#4WrqB:^ _rL.QaQh MƐCےlT:d/&Es?6ы8R iu$"Qոg/WëI7sݯ3]љ D~hma-&@mM/"cA+ܙ76ɐ$ ty/yž2p&S7g=_77*P1ocCt ~iS4p]Faa{Q*xrajη M;v?EW ۪+ɫk%῍A*^8 φ>?Nk \? 39s!h3 QuJRғY;諷L[?xݳ1t%A>K^`xsv`k%3g3ۯ${AO hDs',xMG\jزO-бۦZar^Ijo Rv]瞋o,?4\h%Xo$2 P5ڧtb717 }H%VoSGB<)P _wOgʻuwx}H:'@/d_?||rW(eV5ߒV=+7o,8wx.g:JstMhʰk6ٱ!~,1qaA*wɓl 6ӠG?(rwrK؝76*N i_>{'{_"[-h[v,a5Br :':x zQæB3t. qǐ=}oȟ잯+Z9Kw_ů ޚ.\::Z^ll>}ΎS7tښoIͿ" m_xdpc$vƋ.pl\Uě;1Lfѳ|b m oU~%6GO|w\I>|s=Қ=XZ"${h.&AY:qnO_d%VIDz|I0oSmhphIhu&,9KL;2hmH ;[03^T4Qd5T¾2\pwX}S8wNSKu.Q]2Bncd`H|¦z E87uc{\gdI:EK=4C]q!lGfᗺKHch]Sl{t#{Ae Id6"3C?>/B1)8b}%JHKgg·oFzImxmG4Efl Kt Tzނ[D-norŋ4ʋNϞ~w əxd>q;]\XF;>ԿvV{K bg)UUo/isvrE֡C?Uu޻dgsLl+򶤊{k_R~TJvNqBgUZ75UE:՛%lH`G(쀃Բl{nnor_wEtv;?Vf;DCcI^?:E[ctaG#d":Kߌ{ϾptˎUX6L7>vonm%c7F|y]>VG$%I̒CwFޱK%coyG d z!+l?>Eg/ _/.FdU/7"ud)o%miNG8ֱ<z]$&q~>JUhF;d1"a]q d7Zs,k޻ѣ:kei \=2>KAV#SўA!_ypr0zt86BtgxY|Ə%$wŢ%3&VQxlbUU9֞0rRn.|]2,?x6_u!;4?|a7n*\O?Ao~:\p…sU-Wzo@ӕg ҟ\{'٣̏ဿ>_bӉ|^zJۥ}f]5dUbF%r\u?GIC++Qw ˩yU֣cK}o.:Ȟ*}*w'TN8pl+tiGWWuo qIsVp1\kܬ/>ݐA\6䵀0~zc lj#ѭ{ fK]ؽ$J{gs\Z4D{|\`?l~¾X[ВGV,>xvHxj>+#]e`<^8qdO`?Lw`ZAP ]vF|Bnj4ncZ0j/pgܞg\>֠qhh]s3[WCf`R~ON W^{Hlgۦq 0AwyqPXUsF /41g!D/nN'/{a=Xf!oaF>X7~ Y]{L7ߞW-IMȥ6f>-|{L񗿞Zf7UIz}nlի'Cɲi ^[ ݜn;+8m%ջdCH ; Kx.5OGi*VͶ;SHcޒ6'5K۞ף Ӿ{_24nG jl ;JdZ+qE5@={DA{@p pKU|Z{UTS$xP)~}m̴7M8nl'1 r|"1p5dain"Y㢗1L_c_<ϧI2րΎPٍd#}xVwsx0}Άw̌oTOf7 3+!K}5gA΋Xg~$vVɊ W+Fְc^] ["(g7;3^X˹GN]`o]Qbb0;݊N77G6 H%$Z$OZ#˧Ս؛Jօ1;[JIJ\Stko}w>J%"/C|el|ƕ#l tj9*wB t [ߖd}C9ȣb$qbVxj9]dm[IXe>':'>Qɛ-h Z)7t w=\%hj8Z`KbȌ()`+y>x%8:lTAJA,5ܽx*̖lX8[fh m+f WZ [,Ntʷ4'sew_сW,-Y Ewoݎ{[͎0nhu|_38ڈ >"!et_n^dǘmft9>pӗv_*:M寇PGO>iw%\/YD4K ks"{|W/M )EIK+|%jnXszHLSˆ,ac,e.H|#sn?ޔq!ŘnW յ8]Pb!N0!PkTr/B 4lV¢12vP6QMLPn]>#. Ʀ}UNjI/ 84)Dah 1/&7 52G&VDh Tl%PƦ,  KBڍ"dl@`[#f=r@?iQBQ1iSRﲎLeڽVl`PBXc@f轋@yeVd}3?n7loE3 ƽ5~4ư6˭ h?} ޭ9 }Ξ:2eNxs2g](=W=ƹ=pfsoMQh1D" Nw2H +@>`r E H`Q&ret H PpƷ`2?=ܗ6?h!4OJ+7nO@pl8B >?jp52=)>~TpZ`%XgN-4~Qڟs[fZ[@[+>2㇁x|GC#؅@>mhHw̫ք0? efS|"ږxU ƞ};ڔHRfd 掆]`186y:ƾY_/y>C}gFvȒ U(مGw|P2p6V@1由#=x4|!m*I(OrhoAF!ښ:Fp|l$C$~F _/I6Q'²G;Ѕ*2HB Q24s^m-Ku PaMak}̸`!Oy|C9A(Ij!"Kp]z @+ #봵"G]RV;G[=>v Zb.r^f#GnȨmr8-^ANG'kMazT{&WbNxqddhrո%IY_簥@ߌ3x7wrύѷ@8qυ8W3&ŀ-mՐ9_xoh9sZ'zvR_Q}80'#w iWw8;CHhPb;;ΜŐ]HI\0p2?%0/]d*vˢ)vDq|#SF4`M]8lj[!kToOOv7h3<#qCɩZp-{`bE is-wew-X. 1b gG/Tݹsjfvƃi-Oȥoc|UHhkroz[ у4dⳏ_С?/|C Ϸ^_Tuf޵"20ǗF힣~M;cpF?QݕOΤU;<>=^e}R^+קO/_ywܨ^\K}M{}nAk7U@ǵN|tؓWCV8y"ǴNe9q4Ph8'yE#ov>> ȝ9y|ɏ2Gz}@j ɩc^. ټ3{v| yI$IP~g sG`9k%t( oFCo~YB6 >{o=g,n[ TClybߡ@|4^_Z&FQ-j+@gxuq_1w M\5KJfҩt1A#u 5q}>k={.:ǹC6yyؿD}6Ԟ'vr}}-C[tr%}\/AO?/#q[Qȣ_w9w>VRop$|x* nIz䉓 A5ӧL7oޚف"/ۑs7,G6O<~lTx=nǷ'mN?:S`ysgΝIV;w;yd(dAOyk:{<#xe||cp͝b G;TQٹs]JɃ%,/Y`p^?=>MOU}%ĠU(]qk/K!zHu%k]Mv/}Bg:D/)><ǾSiސ.^UhCˎ < l)@Y'Z͖hYy ~M6w3g!N9ArU?vM*A􁏵^.Z`??->Ćd?s] |7ovYabn*Zۣc&dUśc={{E1}:G~|Y`^|o tWNjv$ )ZYWX-[еY a+nh :p/#يy{o?,Бu.x*KƇ6$،Wͨ$o GMϤ[]+IÊ1Kϊlkwe6]g=a+>k_lu;~B؀$/G1 R Z)Ѻ7߄7EVq6R Mu.f;l؛snjRI{zoɷCoLF(crϝ]6o6+6Ck]G[m.^7 &l;u^y+Ȍ,$ȓGñAh#u ؟eW.~+nί!{@IDAT5+x֨ />}{M-O`-65׮OgϜv-`g Dg^.G)ZB/{`a=+%k8dw#ںe7UѢ58؛Պ?:h5&{^HVתUR+^uawO鰹O̭6dlŻ: MI|Uuni (ͺ$hp6mxM6hDaW;6{=Yc״d~d_أ]I:hUL:Ϡݗ1[m~kUZ:2Z=7cbF+rIFOۍ(z~ɜe:2+~\+iBYYo-U v779O#??F8a-{[Y"tQbt(xc Nܜ;1;wF$ǑW" [6-%@JgTaH h@8/lV(/\)(i9LXF͔QVSʼʨ= `ۄƈ~dHk=w@B-:;3o>~rt{gq6xk=qy@q UگhAƀmLI=pxf4DM)[WT; R!v opcF]JK4=~DéLhMymR( A8 #  ܜC17ABp'`7QA&lsߋgY֔,'e毥Q+AosapR3gUr>P2P83)Sf8=57q-Rbp(ƨ?b¼~q̳Kc.c2UhEXTL="Kcy0F:Zd[ -9RY1%  Go#<zgYT)ֆL`1zP/d0 @YH EY,{%{3g&ȴΏ7w{ ` l-e5c/G9-mZ Zhf nUN“ʶdgZ)O9x Yj=c[" s_|Z]d'`,|Vr^fTpY YFF$O23̀*Z -dc5-9phν2禸?:} ِ%tO; jM}'Cb\YW LB٧x4䪵N@w[kkHo-)LIvT%!y@W$9,MsIGU'G>٫|_@#Sw" ϸu.X}QH,+<~ ϫ$jsf V[d:n'w&|!Kf \ =c(Dg `PDE5 [3^蝑@!o}૾~J4ۣ ^ ENw=LQ5DG&͆<@7Fr1E/KV1.L6IszWK0GF$`U YpZǏ=mnLИǚ5"_r/BIM(zdh<{7e-xwG2ۯ[7&;uD:RO~:rs%ŮX '- ζ]cFF-}ѺDTjYž5x^`}#@ dKl;Ydzyp&,9in\4CoJ7t05:V{DX0cv]U2Vi P|蔢1D1TCn(+=)(d$8#=/ 0"Pɷ3x((#8eO:zllݪDzW{$WU/ P|>X`⃑叧UTSǖCϹdO>Umoه?\{yܹZs.t = H[\c~&N?\) 9wh._ͰZl* g6__ j_8wvؘtsO/?s]T'?|!$wJ-хsӥ%.~gyx3N/Զ7/H}$ZId~HvڪH~ܸy{"@dS}m-pf=,WJIQ fw>dzggmWd RGvQQ>.P') u^ :Dv5w/r}<?7Olʮ~7+K2)>1~"i|]6S5~/B sӻه/-63;j[Eɖnb7?n(gIx i)MΟRKƾO<;/^Rr$ T ظ[}aI1ڃɇǏv&oGpYaVVF󁶌Jw}V{ОD?. خ 5N?Q׊ Wf$PI9P믿mOJSWdٿ~1}wى#)E~2̏>8ɪϝ?whIKO:6 I:?z. ~OIL!#.WT>uV%T!KrRO/'ss%CGOZnz#N矬WGUG\ @;Oxr;ʢ1-9\pێep^ek.5_X݂e_3fE4;ylxg˭轀YI>Y:$ϺuI4Jl,(ǰj㷡ޠ}jq+'Ot-kX_W?+uUaCS4Ƶ-_|๎Hϛ‡)+=L.Dwa Hi|dDxQ|Tzsэ|u%SUZ1aVPw[<ܭ%>"v> hEd`Bf^1٫h; }"%iTwƍ3bOv%֢rmuRqSԟj"|ǸC҇s6 þKxo7銇sjrs$w؞JoV hhucFD X Տ7agt]7<]t%;|G 1U7fχݺuQgӽ֘hD?^tRI 4dZ? а(ٶ&ە0<&# _ؑ^?Yb"^TG =yVa滐%@yViϖMIpWm'N~~F/?w"Odͽ#%ӟ_L^KJGp@R6?}sK0:v">Uto7oլjtf# ?k[玙Z9ڔ 녎U:?I>N;ݨ?rǗC6gf_ ВJyBz|:>پ?{`&(}a˴<lWU`~󿸒Z#xi4LEn \n v5Vޖ?dI~i 2u=aݼ]I=$#x}4~|@~sVpiX*B[[|W0Ѿ. hJbH'nq5-wT+Uų _ݵsKi߮bg $: 7 ;G6Mo|3-]MWXy+BZa^㜳؜XuÍ~qپ%:֑uzGŠӱKz8a]ǧsr=Z~Jߎ֦:wwoLOk;0[z >ty߳ktхuM_oGUY7ĞLghn;QWpqu{nUd>!ɝi/a;| F^ ymZ>UDmE=t̵Q@}c?Ϝ%u%qǽ/6߹;>Um{nΛVSf? +S|s싟`fڝ5}-Q]!jQPbـa6U7%?E !녛lN)km2ucXB ˒ޚ_07V1kquf-᱐bsI`]ŮxvP9,xGkE+Ab8 117#-`ldA;ܼ;}KON' 4}/Oi`*[R#6fMۛG@iK!Do1 6cSu5ΐ]B14f@um ׏ NraH31'%> d*im>!+vkQZ;KNڙsxTԯRci1c sڕ"Y-8`r zV`mKk# Eژ)g%q)+{&X102$Na6F1b~ZG# Pr} 38˱ϲ J"g{^p0b̑ J32jUgF7=GLk8&:QNqGX췠u8=baL-Md˴(}o8:l Pk,?:$ <,ZFy gt&fj !Nusо͟GGZh%0H/F`߇ќGo\W j5hTexK42yȄ>aݬ;mъ3D)ߌKʣGH+?2_[ͭ=Pp,V_Rt~tݺfvecؚoI( }46z cBz6 }Lx6٦Rbʺ> c`hs>2U5sB:60MU5\k7砷7ƀGsp½?h^w5Mk$mk|ҹTt1w/|LD:baA/{]g_|Fx^1dbb_{]dk3пi ѡX8]H2|E>-^3ِaڏ:yK3cOV[Jn ]Fpr3Bk }9иN-ȐUUq$(ghv椝bس';W~G̺ڴpn+`_A+A9My*?x.U=x J}&[* &O7$<ǀ1Mk;KLdh:jȲxM_=tk$ɇYgku Y˂*W9َd==UVp ku 0,x9ツ Q{K7:K`Օ8WIHi*ڨmT[٦8B9nGIJ*5$>%Xqs2jOC+`aa 9%@p6~`%i(??%ڋ' vVv@9{[m<1-1E;n*'X.@ T{,ڰ-?Z43'I%!c2]Aې}Ȯ#W0Tap{g9=#St::>b[hiïX{.]7t^ w| $ v"?:tH l]=A. ٙ37xf7^{{';]18_֧{qiwh'ӿj/5p6|<O&Zq~*0grg裵rkyFY'K,HcNA`+,= &.ܸqkM;^I@oViZLA͛sgwW=x9cyfsx7}OKɐN\:]sI}Qہ:۫0?1GK'u歂{Fʳ!y… cLD}v9@o7v2TK[;UJ,?8ư=?{NLmqG<||TY`iχ=TdV@VZ^t|GbgG$_LudagKFng/K<"ozmUl5_^TF& Ӂ׫?xE^U,/Y;yf?. XZb-?#rM>vXu' ֕D~|&ѐt S>,K2vסOߎduCҏ2vߨjGBJSi ֬exM1 =653rSwTU|G_Y"dh nmns/~60ϣ1;6#ڳXA!_-T/U}Gs޺m:>LM@oW`:}\O."ypwPX"!DsѢ؛fOR|ͧ* lQ]Gr>r-v\ˮcZ@*$KEI~} 4&bgS5ւ[jKsv aQ$}෣tϜqKkTId^qϲ{FR~sVמ2V}=,仌k>Z[|[Da+8Թ}F;qKHܷph髎7Y XtBI%̒=vd{/ uAUsU5[cGaԝ|gI[Fg).=&mtoUs!⺾3y7א!K2SdnvEcÇ/ue[`bgvZO92M{P^_޶oz^b.g$?UT2GaM2q୭Bցe?t`IMOVdbG9u ~j:oO{`vLѓ>RYR'uQ}S]뵭Dxd_vZ:9Jwm p rGߺYsHse$mp x"UA_>A8w'}zJˆ%?ę[{_ nkt"/cSSr8lD~9yγ;\=nj/^"F̕Y.8H47FӠTv֖KwH8J?EM;amr3p8ٲGKv$nYKiC=CG@qamxJ6ׄ%<jA`u[k@bb.FE㼡ͽ_ CY{-̡CG15+F{3_HM_]{P Bc/Ml%Ĉd1-1zg{Qkղ%w,qSUo( oޟ漩&"ۭQ.CgGWXFK_̘aqp~lvːP u6-%$)]9C`TcăpTb~Ƭ*mlz(x&\AO Hmr3ѼFbׯ=3E{))hdk9 0N hnnJn89V Rø1p}=H'>c W7aR^ڛˀ@*}U{}Peb(4'o<{9@m8$c. g>O՞7fǠ4 c8` %'#y2ţo*Y9Ief]EΨ1qSx>2-%,d^W.^vW~ϓh58!>do;byuK3/_W γΪ?r M[ ޽2!=o1hͭcB=7i= C7ER2?x+ )xR2)a=SEx.8G6܃V08%g}LԤ!}|S} hV>(VJݬ PI9F9: +d[ڝKtΔ}l30i -GrgJd4~^mbS6޹ Esq@ p.K?wW} INԞ@vxM  =&#']FgΔ{9c9g?k$2jOsn|T=d% ?p`wF2uClid !i /xB-]* `g@xwA/a\#]>ltz:<Xi3l#l2h?y*b8tS~wNIdKD|ڽ{eS>׆Tc&]2]zOsue7o ݰH^djSg>"Ƚa&'ܘ35m@tth^cs/<RƉκˠqs8').6>lo.n2c'Sn@<ۃ.93\I GH\s7Po݊VFUR/{ZMn],_OԵ"<17Zi mѪ/L~]A;][{-7{*h\|+Wrt:Yr:va|E밭1y׮?2ƍۓ'ӥCKoG#\|qzzH?|g5wN?: $Mm80.~k Nxa=+4u1ƴ{s%h~¹tZ~^[\zu?9!K%N'w9 Bkxߟ/PAvnE`ԃZZv7ٴ7wzrOZ,ξhM8Ir螜"|R8|Ż 'c[Aװg@VPW ?y㦮2zG%,`d #F6)7\\kr4j:}%Y1aGdոɲ&9 . -׳2Vn߽uF6+g?~~^rmѵ.= dc5gW&֪0ZKhnJ{T ܺ}`okSLڱf3,ޓ.Ml?YgObOo^/q0;䳏?ws6vײe#?z= s킎%~}Gӎ?(6Jh|>qjw2 F_*ym$D{w|˾ j˃,pqà;t)͟!3gބ~U{tc>Sh;Z3`,d}L F} ~ H=Ll.!2O$dg1^io 1Bm:79#'; X#ĩ6ZBW(IhmȩQ3 ̉A~ +rt&>'|,wx}Y@RĖ? ߌoEZݷ'ҭߝ~4ۙ.X#ag "aGݛW`Z ;.kLh:7F҃J7;z΍u7x|E W%nX#\kO[ZAW[W_U ѩ*s딓/~/ӟ/k{uAǗ.k>|~܉^{l b۳G++ H\J><-A!ڑ@JuBg]tû{$9uIͱj=:?nSPwe^ųoSU7uy_RLG>Q.-6GEɞfnsYx&!af?dWv޷]T:늉w¹СHl!AW @Şscm$ZkM[۷OJTRž>75;Y9뺈 ylj $ IO Y,!|  \2³oclJP/Ɨ{:ji4Бtmhu/O>|7yu]d9orNfc;wb c]{J{ZPAIo^:K5}c%c8O{%t h׭}gO_MG# *}Z,unM.x[ }ntA]3,N3`GTb1Gm-qc<71ËGW&=W|lv͏adM:U7:=lNG &v%co6Y day1%7Ӄ}8EEd+^FKFwx_U)߾q-l:w|vh]vs;%YZ3@6"矞Yr|ɣl/] ^"uSX-.sǺHjMhW%ڃ%R`س*S*F~:rlF/ *TޙC༟ɩa Zl~LQ57k VͦHEo'8[V*U Y]㠪|K[Rdlhy>Uu1*T eض''1FA*UR,KɹB Y1 ȧjS,a`6J2lTIieFQ|u)aXX[9RZQRy{k/~̾㵫ŻX { #27.[&а8H2ebf{;+~ 28`pɜ*kRuKjɆ%K{_À!-{avCMYLc FAsZi-VE2sy5v\Ào},u%W3\*!Ӷ-]TS ݴҒ!Iذ.ƍZ5LPd~^e*y$3Z uáaBѸ1 =)v\eS$;۫vz tJy #z}-ͥ Rsawf-m<9=wF6 >Dcgg# $wuc,ֽ=x=<@4CK ̼}XƈG[֌*ͯl[ޯMKˉO.l]<===ehykzH|O#[c??D{h]XbM4eLnG?2F t3VBx&o!+f@!o TMP{[g( C=R۬ :sLΊikNnoNF&Jsx+)Dtł^_16/{>:2b3#C-Ѐ޵{7i4w1azs{W7~ mf H,TGqDn..d8SG~@z"&ͅ-=Q@eFZxXƶa1=3f8:=~`P.y(!ၜX^ |Ę9 *5O-w4jX&[epwy2 DzHZɱΓܞB=EQ)Np]A[sBƦ,qfz3 fkǰI Ky(h&7Bu sn۝]5Voѫrsr4|űfOrz0Tdl2e#k?Ue|(K f8R =rk$䄟:uv pdߨ8ڈtl$<^}:O?:GQ-F7.:xP3Gr|Y%V HkS'OS`UV<@(:=xlK"Tp8>$ڇpz J*ao\)8#qߙD o%ǜ͐^K]V7jdhU(9D'3a>wO2Law-0vUnZOT`-+ YsLxޓ} A3(-UbL܋-)Q} 'ڶ Iݑ>WS/Cz_Fֺ{̳2lfAgQٳ;ux7^/pIA*.U8DcZSe8+.ۖ#bq.b{_]%+9P%ͽH^2${`݋?t~(`{l $|{Os׻X芾ٿI6-Yp69~Vim[AM=ɵ-=I@IDATvKrw&#FrABFmjZִ7%Ʈzit!_$p]'b%S6/bjK͸ͷo\%<й٣/N9}{=ڑw7jU=X~tAowOa0Nx "ouO ڡvݕ_>RZ.`;pj-vDCvJ]01t˕g/~ra]K6럜9qAJ>@mсP%a3xNz/ˮ1xa=>  $iȼd>% cF9|&[kG1[>@;C/Cml]O7[;?}0AX=h$d*d(<}-P 6E X\ ?β7R|◱}7F4o"( nG+]ۡ}̑֞P8aߞmFAJc[)6'laӌaϭ{<*\FMvC.4Q 'qv_>ŎlR>){;WƾdA9B́ (X9{Mo-$Pʈk=l{qi}9ƾct`G 8$XwgS 4!ƴ O:am<q4K_#!` |N1G;kO+"t5_@Ǚw7Ty u*OiosdIyϹ)S"9/EqF%mmvBG|WK|/*?ΗЍWt2z|ܬ__'7U'ccz}3vtk)LIr6lĢ$Wzdώ1i1Yi&aױ -z-H^ 5 ;5{D@1?[y:W,Ry_C*upl;d|sYz[: Cmg@=WҿsKkޣ KtΓ~-ѿQLg$p[Z͎adU3yd[OO3u#?l:sR ݽic!"{lϦƊc)7iv⋒^澟Nwgo+j(yXɩ`rQ,>SjwX5*)u, YvWB%ɤ%ݠ1{&w3kaH7%Z$zΠ'wfub7~zqW}&;v̎tH zHZ'SR1Kǜ%Ov xe?m;[a7KedKt6߄I'qӗwӣq]Ɲ zFxT~{~DwHflYǢIVzUܡ9$%>xOg`Բ}( AYج"8ګmNMm{8]@`ڣWXSyJo/>] mPjt@W0218j3dV1|UhEP~ n\g0%@k @NcYQ9%-Ue)ec,?N` Uy-c8 ?oO̬>.X\E"$[z^xW?9>Gkc=VPh¡ʠ\36G,ֻIRףW#H;x(!bb`̨59z>i3 @2-d`<[.aV-)CeR^:e/.}ܷ3T@ʆ(ks:1CŲv)1Ҟ13^`nRֺf#y"8h0%!SP;#C+G/نAx1!U{CFwѴg&t8y2pH=d 'sȁ)@#Ae,jײ|jyk<tOGV[=-:/8s3, 0g)n_\6gYr=E. *v2vM*2s쪌ڣUȥ*(Xt&^y^FL Xhl'rb *@lvݤV8kϹVP Bsytb8ろz&zg @3Mّ)ݤm*"ɽ-Uvb8}W+"m`!RaϮA&RiFΚV>oljJ3I;|O2e/oZ -.j74s8})fb{c[́4m~ ̳v$>udhL[;8w 'L~3\c.Kbkzv.p^yq%ݔlr.(/3W@!\:D;ΖL ,0m{JqzKY@+ l]4hlRAK9ֽEod֜ ?k`oG^dX: 0Z4ֻVYr8}_ ln*GC.QGǕ/`/ׯL/i@Zk/5chs@ @rmDIpO{T#.XjaLDo5}PPu$`qcWn}5ΟڿLO߱“ۛvxPm),9wFqW֒@ ɓGI sgTEuz<\{ <gG`tƶ١uRyHViV#(&O>to1dsv9,uNUþYyNH+K%UWG?~|d ;W9 ;%*񹹀T&׿OK4xB>AKH ] 8l1Iv,{!HInt]pvPA-í+: KV$iSNRUG=.9ØUK2 ,/Ié1mI8n5v[-݂tċy𼄗-@cl[;t]ŃT*I(]kIš_=n2UKO!t*` aT%efQ}z?2H G:yJl./ z0GL։ Lf3ha]ePzY>V u4BO7bl`7m~gzM7,rfgszB͵4!v, Ml_6a${[61BrVkE7Qh5CVZ mop f iʞkT]+`1qx8 Wkhd" KR~H>`f1ٽc{23".,#۪dPzp{֍KF#|i3e۠#> :/m rO;f8^l 2|G]H'|U4^0J}*>NO#o*ۇtvjgXBemTG6K|؟s}<ۭgBKtd؉UX{4@~PwHjϝ7k§0љu9T"-G-NQkDeO<= ~s ||vvlĖ- ؖeɀM#?`*6A W\Sh$+9@|>k1.U ݋@>5"¶f'+'iaL[7𿾻reߏOF#m1o6^gw^0|w1_hyz_^kbƇATn?[dv$辪a@ϻ#_eˮ7Ç(q4\zQ>-uQ~>f2 cwŷ_O_3L~{w yqǟM7nߙ|w|IϿKrC?/V񜝂%pσh8 .nPUtgIg}鈒׸|I_ut%y;߱Vc@2QX{kUoT*V{}!Zv-#gA ßGI#GAc`'{0.?0|'ҭY4oN{@ X'[6tDFML" 5nQ!kthdBLNvZɍO?KZh<[}꽒l6J~%5/vs6l?´no6HMcҬ;i Ǽ^C4.G?}5F_N{f5srdcWPyK&HV#Lۚ H5Ԟz>ޔiAO=mhr$]:~F ̫uqGt?VTg>J^,ID{muܬp"l3'G 8'7dY<.іP]l\2?֦U|nݼ5Q1ܜuMp%ё7إ}3wӧE랊`̻Z'\q?:FN`hB`#k;톾QJi>|[7$f$h㑎Kḑfxq|h]@i͌VGbTv8Os_k- $\.+i=8@l4hc#Ej~r/N/3Jm=<Qwnd=:VB_]7p>#@= I ,$ed+E?eG)Qbah 9?8R"(`58 ? P=VA8}`8|Anfpv-v7kA a 9tt,;4J@0|ͻ&FXG1C3+DD[Q> -.HeC͝#Gxo##n6J j-mpf`?u@Ĉ@ܧ !Q l6gNE6} =UMsٷe>bjKMIQ.-<811U=KۧdT#҆g\T-Uư-ʄ{z9`ݞ=c-4N`ԕ -׻1vcv0v`,xxMf(zec(ڽw 'khc3n_QY o_ɾR9Q;Q2(͂e2\S:Woޙ5=[,{2#|}<^ v{h xbw@%ahN@'OJhOy?xHu*Շm %AHPyrߗInoG.$0tAMpZ8t@4:`a;ÑVgH8HojŅ8\!ctkQz@hjcCw}Ю ]H6KJ88tDrg{*^eezk'U?_4߮"Z4 PyY?q`?L7ro^P~}$訊\b:SrбK3O k/<=!)/1 _Ĺs૳g/}S)/~qd׉*,k&@{rw4u70ĉus@]e>I!Ts>>Ν=Z xi٣ɓb7o (*"Z{޽G`ƍ;O `Oq>҇9Kg߁Р_8[K@xdf s?pLbsV.ܑٖ{ek=x`y~/PڽعǭF~f>}{gOB=U IAѣ'#൴|~v-fܝ.4,Z @Z/-q}Bx&f5 tkH?أw^6GkDd9~v-9;xAvg/Ǘs9ٝn`l3kNUlX @nNve^$>n X]kiݛ!ir}\G6.dz$ѝ*G Xu_WI= ,#s7-HZ@4nONj@`/[SS |7{`~*=CN&i'vĖ"cTaGJ8 `i_Jmv P tMb9ד[p2Tt% j wqd4eZѹkkUѼLscHm-#bKBՖH;'\喝lUaCixpIW5t|ά^bT׻GtZ .ޗR?_ ɞgKwvǍw>A+kAG<^:h 8: FP,mEfFw_,ZAedL#r`N]$%?ܷ:ucJ:>:hÎ omJB~$Xg_ TtR{{ ٺ 6Γ' X%6{wg@Z5p {hc>AhN/z Gh^ܞL+V3?+;jj+l׎ [/Ϟ??< |\ZuV.U׿~x;6E k_E#AUPçѧ kN㷿d7k yBVē #J'_~Y/Ӄル߶%#DZ.9ޭ;;xٛ5i}b^Nb Xօ\OxZ?h~`b0z/UIed7#z`G۾7Zh' F (_s/ GgQ]3GY}tJ8P+5IFz̏ VGgkXO+aG;DpV 2-b _7nI%Ȫdf]zpaYn꾰-k(BA~Kz^!&׻tl^JC/JkK7ۨS D O. lIT5-ivo,m=bZ}o$ȜIkw/_9&l$O4XC]6WjnHulX>zЋYjW0< C$ٗ0QO_؀A} LtZؖd L ,HNhGp{xi-"Z _(:swDҙCv_ [޿u%%l:A3+LIhBRw肞GPP0ƕeCVZW֕nl=mZdnr{Vz:162 lb]pxe[zv,)yOܝ*ϒ)֭3svT`lUkF{<=&k!~?ؾ~<|dj),4v]QzE|1+;46 :?fs双1UUm< ><ý|;xK6&Cfw̅lr~zk:9q=W;J3y|kLi:># T^Q* G6:q]Dƪc$fEK{%\m⍇t<|rq4~BpӟإOΏʇԟ}vivuGۂ3=9al-6?:O ЅNSg+דۋ;|Ҫd_ roM0l:n \j#qtLzxD T? {w߮ciϹ091uQANZ<*9ud1~:wD*G%P#_&߷<1_fz>8]ˆh'C5Ϲ> ضzxwtDg%on.bGFEKg']yZ}_NT H3#.z˺5)d)}-|e7h &l]2⇲=Р) ՍѹIH.AmՊ&4.KbB ),F39F 8O8eDX;/|-0PߦH{ص@m YkG2hcckYp3Cc|%a0>@ac~:-$9*"Unuj2|w EY2@' (JT6#s)FG8v̲m9_̨ADя=%j 0.̩%%/4^mAG]ځOB|fldj{h{ɘ)@A-d=ȀѸm#P llmuSb |ΰТUg̜ޚP@1KRhF1Z@.b L&zycKs|E)ٽTelO6~XWAA/N@T[&wj[󌅅& %b&LW?#*$7;d, ՇSG g4(jk}sV3$T(kQ &@@d3-3CGT jG"Kt< a99<g!~(X;$p,Vbȁt Dٛ7U|2Bb"]eH[#G;ВI^ص-|5sg:" C=wz?+[4%+<(DW41RTtp X8R"vƞes٭e\*+>fۣC)N`ɪ؛d,92%k;K:5/ Ȯnٽ9<סH[ssPP{G;/Ds*~S@Pi&.Pm'M!g83nOF˒(v_m] | 92 3p '$_S=uT5` UŚ C>Gk׶<"[-[GmvgꎜkUlyYwdQə/ꭿ?~bf6ʑ*ު |_5ꌼG7kZ5@e݂gwݹ}sz=A灦:<^p@+8ڟ=_Uq@?sM|O>4ҋn޾B['ahYp Ξeg :SN6|1Qh=~jdNw~[g/*y[rU*3 M]׫7'M' ]Sp0ޭtluw8JP iO:?O\q}6?@vHyŮ6肙Nr9~Qx DH3=kZc~&[_v%̋z#:{ޘ2$i8'Zgv/G'ue ׹i71~Hw~q[rd;Pd6=ӽdݶ|U7ϒl *sc@-ka.o>GX֗4 ?с`ŇR,D dM>- SgI1K8(!C@+K 1'-W%3%a)- g K!ăa0 㑧 ;Lx@ Z!V/[Y~qKi=wo/wƻ q<ə9ϯ: ;WW@soCkԖ޷Y%7VE'7̀Uү_^|gu. `D'M:@Z]z6Oɸm=ݿ[w<΂tx \ k9ZaSߥ_ F\ɖi{iQJb:Y =5Ev%}Q=i o.M=~9|LuZl" ^aWεxu`Tu0#p/ c'OO'ΜkNmUl7^5$4&/GtLOI7K@ﻓ pUϐpÉd, ]q tA%x6pְdN h ,S"$;kΔ@Ҍ6E=qGӒ-v=mc^mhcЁBG렭K=/z'7=\@oiNO Nj.]%QH,aD GүaG(W-!~ 2 = `m C sTh=g0'ϟ7_@:nwe?ӱ: g'fd%-GxcKfجߎ sVGaN-Oi}^k͏,A [0G2 f6G)6o/wm9"J'c.c A;kg=m2:d'ײۢ_>d G~D%G_n2kN? B2ztJV*cqw@{سdf:3GBкkU&+o4i-L?m>au:;l1: cpdkY^c?G[˭Pݝml0/Aw3ڂC#{nO<(Rn{ulhZOQnG皓Dpfs)9" _`kyV6/u>W_ =o[z.u=?E 6bNVpڠ7Y Vd-qd[cX]lcǖsU&ASO{`~Xِѿ. MwȾKfm;-;th*l,f[?BhK l||Zy_W=O _CQTLWE&{vpڊ y_=ݼd7ݵ7/a*GJx3OgREъđN;=}Q+ũn?\3S7gXCg8vY@I-ƾ}ujM,=ϗKkuHgn,{Js:?_L幃[/^EWWn?oU8tӟb%?ث0Ltz0G%Pk bFuCe |OĺFr|N4ߘ$7$ +$}ۘto%ݽ{{4w|1:$ d -\Y؜.ys&m~Q %v6~'kśفi=S`GX)ueT#"‹lo䂤΄<0*s|b=k@g52#.=Hk?B:l8c۶L JG@[2==ɈAȌpE+e4Fo=BbР|fP1E%-5sЦ a<5 i-5B>Hl 7t#=i.2  a0:fM-[p$ Ay@aS*s0*B6}YcyRd5%U"øs V%!D$0Y`c7:\aQ1he_Z]<ԺnFg>XT}p¬ۄwծ|:7xqi!mFÚђ5&?قp:&ۆǨ2M PO󬥡dD 0v' 1t%Oxyk<'>ٌƥ!%&(3>s1@}"@1ʂWy@j9ד7Wl27L}f:CC6d:j$#LLsaGmDݻښ_jU٭z^V NyN9Gֽx:Y6sd^_ _ uoS7<5mS2Ğ*>忚9=xS2ɩGtܩh=&ٹ=h-TtrI< 7|[^[gÃ*Kod߹u@wm|R@bG\:Zt{3 qJ"9|J`4x(T Yׯ`>DzpڋIU=fٻnj6́H7g8~s nܼ3W`TvۑT%P1?ZٯWo: ; QpUTѤ0x{=sU4K*v@o7ktφ޿yFa;ë'Nl[F:[b4BNloZjtVqϞj/Ν{%X<.]LCL%~gCFҥ +_i/_.7V&j["ݹ#`;9g\spT~ϓᄏRζ:1'˵:Y=U3]/TcOd󰽖TqLQBÅs;G';<.~]j&GAS4v"GJ%l,D}v a'vrL߼/1\Wb)g+7T4:Y~aծw;]&gyUU;VO:}#9#`X{k =OkC 3]sgC]/Y8gkе@(t8>fw设gdޔșJ>ɐwɞauõlѨ`۫8h xY.BkG髗\{әv}Cͷ_} ArJ/J:ufw`Qyw)FE{vF)}dY||5`xu;M 0-pb6>>"zmϮ+2 ˪ 2BvJՕ$q=&lnudY}0hVDE u1`1بD{:9;ڽ֪XP^P̘T%~t|]#Y#ے:7uT{Ai5ws ›x㯱E EǢ{-%m^OFU [X<>Y[Sӧ?EܬC ryn/`{|n~9\$gGlNHel~O/ HhCĩ~vNf4WuW=}gIA|l,ٳaQ>ç nQk{U#ب\$ >UY(KoGK>nPR/|֧=w E sFrXv(d=$5{o!%9t'uġG62;C_5&[qҳ|%<ĿcVt+|?~WXwZp%)!(}#5g${! $ykUIc\ְeo ɀ_)uE V̺&y|aXAt3}U[ 0k ط#W$ ==ř>K!56%TmY&D[J{>]Bv~KDRkMt@`&ښvh gjo8(*`#u.> ,XQpo(g*$u fTB[^MlB8j ^]݂ у}7m#ө=u7z_G %njͭ%9+>6_ Zl#8Y`? ; nܜW/O0 $3%)'P (sãdetIģ@8@`g(M+H6 0S}ܙnj Lxg&´]?&ӳfZ֡w&n٤RM^n7?KGlr/xC\V]bA;:a&=q&7geC:[I#$%Fa|<9J^8y|Ϥ.t==)Axs4] )ߴb6  u|8"3'&쵚%Kt6cFNNVNgf#I-paI62nC~ڡ!yKs)L 槰\]]k~#Q_̆9Aغȉm|Ĉ>)=Nw-Kߞ\/ԝ+ǽ.7tTtoTqU~|s\tgQy4M׾]TZ4zzsjUOIRq|W*vh®L'ow~>hOޭ@:V=Á n;=}P"(;u"Id?tɞ$WhH@ОɄUa"p#`} 1 #7ړ+A -r2E%6[S2٥ V-ES2]m,%Bi0P?YƋXK2bf,Q1Zd05+~\Nu~OIim6hd>f oAۚtn7dy$t撜mC)}"jFxr﹞#[ R6_RyBHb?T'#5 y1֖N0{3LY\#Q3(?S:c`$uv͑ 'hXǜ^fWCJ^Ho8Ɇ s<9F;/} -1^In~Y#D_@G`YxA[7d{=h[|Ɲ_+pA%sS'kӷOV<.ITt}N=w):źCnu[}v./վg|~֭dWׯ~_t{]tdʃ Ɍ&nFSkɛʷ}nNg]J.U<\ָg}ҥh[pǀHZ`γ::>Dz~\rjW;{FKpxg_`M (/ڣU:=_~O'/n{.UKf:V, 38!8MF8DB>\vټ'(%QIvz>?Td`#KˣPsU,Wu(dnȁrs/=CV$wM' ڥ`*?=s-ޔ&t: 5>]+@oW^U|KI(ۓUJz a1`Qzq:W/ xp'R-PYrAroG&Cfkh~%^fȡ=ߘ6ɡ^|IoQGJ| :;) 6ͿgdXj$Kwgy7Uh)y/H'f} aEۗ]e% Dΐ7=ḗ܅3CozdJ !yr?Vuvzxt3otɁlŎzHo.DNj _8U '8 ߩŀ%~?}/ կ'Æo~?}zĽxht\ը?}mIE{/|i݃ZpgDo+%=P_/: KJ|Z3?׾l8&0ds{kB3fwG ̈=mf{v$M7тw|YZWPQ7>E=pd"E~tdnd[k2V*HGdzw]񆍃2jJ67$Y>.|ɿtpa1oAdKKZdK8J@g6k7H^R8:t%Ǿ P%PR6ߝO{FMu?mBgzb^+YϽE6pߑhuGa3wTqߎlv؟X+ɇCLkى }Tk4cg9^ NG=Jpzo)gJw6 fϹynIW ɒKϢ;?q|-aY~)O-$wkobnMTə  UN8f`pt TɩO?s\6{d~9N'S/aG/6/^ozs]|xMLrZ6OUKJ\̾DgKUPEȅZj{Q{PcZF찑HV'aӯ' Y>p+<ԭ v#2sɶl)sA7܋&:f$B :Esr Qn2_gk뫽ƻCO?&s7|q헿ɦ{+Ta^G̾|i됐=q=9%t@TO IK:B_{M$+dtZMfZ,Jn셾%5C|O 0(,,ZOFC_4u_o=DjZd 1!tv(qeN2" 3 tXn2zK}pH:jPOثƭ o/(yٛo:kao:QI;VTo1!ٞFo%8cfB ۜqLZ?=y-܊ @lxxN cd㺛+sDtw>~9e¾x2tr ӜnSD>j6C'*sz#޽`Ɔ RrjAgJ6P7>9=ixXҵ[8`zlgK:YKoZ׍@uvA J >~PEdv@yݹ}y Vlo @>y7;NKػs֐}ϝ+WoT'#s.\hm)`: \YɈ/~ ؘ>xl7\/O޿ZO.k <I/^ѷqJ@p$/], H7}p+~3tܻ *l/-A]𱯭ݮwpzܴm}~G@nkr2[WЋݡ]3}b돬xi[(;L`RC3zݛ5G}=;wULaHT#Tо +BЕB $aZ␜kž'DbI @{*[nȋt |gSъg0<&ƖVCQ~+be}@F7n~::: g_Ա5hOS(?;ԟ_Vh%w6TC#]}Zi/2=MqqI/n摳&HD^I&ɸ_asfHhA U:h_b$R7԰ISj^4*ȆMrF: ~[/^" W6kbvB0I]1AⅠGij8@{h255V76փw/~m2qe>XmOIyƺ^P9{Ɛ|D%T=&XYCh5*g RU:F$'  oAE@ ]!&K/EOy%׾?"iBAV zth(?#(C?;; V̀tj9N&k5 :{jD82dT j)S@3|[.1 G{psnAi&w刴f nd[s:ZIb |m/ =om✲ |yMk`FZc5f[y4hE MM&6]V l,?9=4\infu|ek2" Nb&[1}۲4ŭ#O2vqa3A QlYr۠.wvX#aM T7]~*'y_5iN3h>zCd3"Gc/_ ޵O 6Or*n^֟Wֹ~KƓ"H|q2@ғ؇ߕ vґ.&[G1 93LSEm}mY+Б"ykũkwF,vjI߱K=LP|\pŇO%V@B8SW95AuLhIc3 ^?^EE[̳wx?=GI]} W[y|P8Cٛ7؆C@Aulݑ֐<8[qۡ!?O[:CtgO>Uvk$MMlA3g=x8=ldJj9~ZgmfL~5k}}3aWt]$ nVg؎v`sԐu$aDudjJݝm ڊsFQWH 99ytUb6tfa}E ýtM ױ =٣s#/SGdl4K3vikN%`d)xCmw{'ձCUJn:x|l&xul3mq[FvgVoMk3h^ k_/+eowhŠ<~b%pG$ngBG)S k$$櫝~LN:.I}oގ8RӪ8q mSx5 _@{PK fmI(LTQg2Z~ZX^ l^;$[z}"&ZoH1#B\إMf;?DP aZ3LsR?&u$ >J:{vB_ dY]M&>&y K+AJupGY8X֗_٩t5#I>Q-.ўgq/9]&('1 CYnIl$^z3' ŧ?O@Ṃ +[L^FƙcID[>Xx</lq G ܫkl 1y՘IWg?twNz:g k2:>ga9GZk7(W8bGPgщG]?'P,th᪠=Փ3 s=n YvU(O/%-nΡ~l#ߪ\㽘c7D 55O<8秊1- Tރ86 <gwcm:?1qdss<@)}\Ш̈WzijġӔOKmGѷ2w=ҴUzW}-;N1fyIg@'Fѫ(ӎ+W^B[ am1OB&3΄3q A}5xu i:Q8 U2mU<rDg>b)AF8:7h |aGe"R@(PNX=q:#sbO )g[õF3k9hB AH9_ƀu%sT1`^@E`YւJ%@'B^HڸfKjLG1[^0|cjՊN} K31m3-0jct=f?LAu*rhs*QK+75"(k t̮(~bVaP|h)ޣ ]#öyڄ];┟,=R*5ZUҷ!>&{)/Zi诡ޘH@ &ֆ RưBƘХq[Xu^r+EЈs #VT04vB/̅G%N[5kg<톄*[ŧ7O^_~繅LQ]u 3t|s0`!`e`Xt> Ւlgqv Yӈ3q.p=M}pL87?= r{~f A甏Ґߜo|nDy㙸wj0l!KY+ݨd_㓿|7=6AQѿ!OO} m10GT}C Zf:B>p-{kd2lц<u0e*5mn=7E+*V 'Xؖg}W_;tJFùCPŠ`H"{L[nWȪ NomTMpJlInVUx܀6V%[g+o1V&N(+i:gT ,xˀ:mo V1knnP5ϺͷG` :;*9?|r*c6~ȶ-VK:Zyvmff✎}f`-gY"ɯ8<&Ee+w .Nr ;0xUUv6ukL?ZG^ܞe3WeMLfP3R^X\}I& !%3TM)ӹ=l9ݰ$E4f%kPӪ!"cO+ m A*~?N$x?UA:+|;)nC(oa:[!Ճp]ݦ}m?K[N{?HgU6]_&ikxkۮ8.Dٗ HEG"zEgk@ҖWc\|E%ߦE~Fnnm?/.R J$ O{;8oVz.rnCіڙG|mzv输R Yܥ!- hs\#e'V <|GҋO'QwDi >A')A /"|߃>?#9`%2 voC&./Eg>`]޳^LDY_ 3a5"EW7bk8z;@&Db:ȖНQdii jހ>=|͞׮$-p +V8L9D+LUʹjжk߻V+ Yn ɷMH B]U%FpyZ@iHt >Uڣ'#M;ߑӋ^*!ٖ*HYЉ9-$Ϯ+h>:ia ~NM2C~1 9х dx; >n~5G hqxWqYܪ.03.&A1<AXE`씉Q*CЗ??4aÎ4bgV T 39Z_AumP[,}|v޽$Iup|0#'/]<^q-$I+&<drʢ>Jk1 K!]{HzҸ6;aΑ Xs8p g }wIW )9sR?y‡<-YG7=>>9SX̣%k7'e|{o:& ,F Y.(d|˹\Uӵ @IDATyNhj7Pa;Z{v 잚^/ dV?gLITvmB\8 .c^_ /u˟lOZ4Z/1L ]_¿=ln3kj^q-*٬5αq&<\*^N{ձ;X9?1"c-cbT ҧ{u+Ha\Y+-Ń8'ņQcLpXI{oN-o?:5')DEuuUC6XNI1r;/EBvҨ2NR[ jnBϸv-nsخKasU)AVsٰ^w<[m!oKwxdr4[,oߒÙ]o`??mEs,i #l\4UKO[֮ TdTS|Z'ྕ6Q$kf|ay[ure4g̓Ci/Cнz&U40RL[GpVߒ 9}B܂;KtM'ěđ%xA (꿛Sx1 I j0sJdKSD کJ&BfAs_5 ΰkC({]F2.ѳ9Zو :]d10ܐ4J` Xɧ>S`uqZkh^RC4-&_!MO 髙T5L*rFE;}u]`rE|(qiɮմslRwe-&wK{\_Ft/櫀\J^a?76&^ >je`> 0p#),({ μh`dFyXQH<6sGM}] gO2DgQ`Z5r5u" %]Q~Fv B 683 L /1fU fF# ʴΔ: 4,TBTS0\6c8p 2+_X|TrL2{@;8d?b.u,ML!r }S ls ؜U `ppdկ@@(pU5m;3HgM:"dg]7w2`5MdmwK'2TZGUY80*Fri@"oaAc^rtHDwv0 R:yÙXae% J]\ag?;utۚ/*!:us%|15J 2,?9; 8ó[< # Y8M74ȣdX py NgŦy;SQyrXs,b?F@ #{VU9|Z|.a/IhՀ08k fW4t xzƳ`{!G8yF d,͐o6ahh{\wZm o4̨w>z L8uΘc12S@wQ8P!>^VӖ G}ײ-x\Iz0 49J_|'-Rm3 1 HnH}}g!Myҙ]t&)u0Mw*(6pӍB,?4>=;d|ɍe3fa"?7XG;sixE1Ƈ ZmRx>&я-2*Or0cg  'r [JAwS lV@zu$349c @~v7|dcېxuSU|ls8[ 3ԖnLA? "LQ#Y?`Y0=|*r}>3tjr~MbйF>T>Pfٷ>iow^Qvrx\G~X9|t]}Zݤ~>.>ggi}=8R WȂmeDݣZJxQ2pL NaVÊZK=B &P#Q.ACfB؝ "Q}3tNE#cU-\4ۮǺ :Uq+ݪ8-Kc*{NNٴf:W ιᙝcn0NMm5s %,YJ| hNgi]`$mme Tm뾈-,KVZx{JM2߬`F * 6i{]kNJa'G8\ґ N)1 4א3V+gv N Gɠwf8I_;~ʃٙ:KKK8'T ъ,e+TRr=pu8Qd51,@JsVH>dO666[Z$Xηzw$U |W+.tAer NAGBpF"`o7O"t@t[3jʣ݀bW mwd/u4f6pg[QGD󯟿D,61< Z( &ʂ k3+Gd[#wʊ,osH/@O_ ?H- NMގC~aC2;lw9h۱at p}8FW#1׵zLrӶP1ȹ`"e`Xpwd 9 3Xx}51 eNɀ'\Ƞ @]a(D^yڇl+GUhx=za#!Oʹ ׯބr>- hnۉg ku+KL~}1=J>_^^JP:=)bo_Wا9M\CedC@>oo3}IM>'Hn`<`a$d"?~DJ 610Ȇ,`sS#h`YAY@U麌#$9|kLFBgj[m!Cqv|pL6hUjw]̤#\?:H?$ +m頽 ۲xrNGk̀p" |2VjQ= :{^PHD5]mm6ZȨj`$M*ס>Ŷ؃yMd 9Fi[\L.3i^='B4a% ׭SI_SWA[c}H5m$Rؠs|u"A=񸼢zg{"k:SN}XMZ5َ/to< z+m@m{/ߋ*Tng=,V2<ww{ 3oiIa'_#q`ҬxIVjlŦD{qu1p?q]Yqth= hxz@AY~7%p:ΐwkG8v$'>hB;=^'|ސ/UΙqӹdxMKthxyL]$7YpGYh'mW3v&ͭџ :W 3o+\%,7)Y/1p^bN(f,~}IR-J &]8vd%ΤϫMN+V{N҂؃NH|}|-9jdX5w,TYi#X L<sE/Ld4

\< 'Smv*/|@֋!Tw4IN >P~S[.G\ BK33Ė[Ott-\L9vQ7!L,V&M,?g]^n1Q--DVF%S?ɛ?Юĉ#:tDU9Usm`*`GgAO| 3Ҧ&Xd8oJ$3RdBB/0(hq r )qGCAS!csu52oXBƇCPEg,JH6 X>ϭúx7GCfuּ$HkSjJ4B(9 Lgq>a UW)F`}z6à0 װĪzًtj h 2 A< G-'xP 2dBt?c&Qz |i0*8)f{ѾeF3U:mfxheg% c׈RHDu6] v3s^ BMNj%0SOa2.{8rn[>ֶ 6Ѝ@fل *ysf8D90ھ[Z%BMQjuRQS8<WQ) /ίS>y4PuFkKfH,.jzM*wt..5p~0TB7di }4} kȖX!eVA9/YhvFpraSnEϫs=ukLpL,바?0Xk.S-b09S&`b>ܟ6{PUCy( *{@wj ƾ3*QypLx_'>}շ!G_GdgZ{ G&z|Sg&'OOѪ9!\]*0̶򻀎:_"!) H~rs+kS7y~Anܣ5K|8ңA'^}LvPs=?kA <]&wzB6yY03(IyΌ3zO#`j7Ayd'{/v{c =O #pL86 Z"j\e%AJyo#x;LpDV4;%b{}7-^ۯ~"5vp+ hۛ"8*[io{'|MV3!7}{X}2p55KP6͹{ֻ͊5f}n??d}$PFzAS홅E蔖켮aBghϮK­mk{3-r3:7}>5>"a'5:xC-Vc`ݽE"'\1 DP 8k0jx} "'yVT9EM y$Mvh_n &59 W l$}쫕|ArMtc WxKE|`m]$m ݈}ȂeLqVZBٖ'W"ԉ%8_TgnT-,6@k.i7Z޺DyH8& V:Nq <6{<7/a+~zG扠Cz}J$,hX1>r嚶]yVWhǶÉǩb*0G@p3hXՉ{tlBB:Gӑ$֤cW 1m L Ϡάcǡ 5_{Gg6e6؋:y״ x C3Gm,'3PYD$n8(5d< J7}&hkh߿rۅ{ݕ{.Vk0ܒZzlwEZn3-rhl0}IFYt%J$͒21L' >"gHنސMjooo] ygGхE 2"dtv)ҚR`jxl$WY ߖ`72zqn \6=g,mکr&dZX߂s=;7l^;Np '3+O Bc\٬'lډşa* pNCz V0]S绽Iw*W1f3%X0Al 8$]3}-.Cpau H 1`7.SX-l!gؘ`bg(>uH& )9oy)]+p wx bY -e6,%oj_)[|.}7y39M[$-v:W&}iIV;gBWTʌ 9:vߩ y9^(F9y0֯eo>/U *:C&īcoy/ TϬ$$_XxC{Y*;;q:Hz3wnKW~! Xb0ʹKً'%QA?FzɰHlC[3MWFldɶA.3~*bЃ>i1}Ga<8sekmK%!v9 ѲIǠZÂkq\r/J8I~`ˠwg؎Gi ;෉=|҂ncXyydI9b\}SWvvuN1xzLyEn$Vv$ӖvHwm⃘h Ơ]R4H)'|jL|Y'41ɨ1}}:gz݄1S[ tú9?~WGƉ oO&g?wHW &6707[/#o!LH!aDԤ.o#v/S,}^\|`aM^S"-WP !w'<'rMg}B_bVz¨%b/MSʛ+Ji;PhjjIvfۉ#^N7~'O-Rd1-sN2y#N8#s/D ,`2wYV+gza҃?kY,a?͜/`aU=(&k({.gYm $h :2 HĘYAj1vӂzBc=FTÐ% Y]da *50L@8CCs !,ySy '(74F-QW EpjF%1TCPVFBz4P@4YV>>\^XB?7Z[l[Љـ4em̊vTfWeWkdD!7iEwZpt6NiV^H>:| h  XLf%AFI68 ^hAt^DƳp5܌˾wAr~@;ʈ<eNhB+]\ %#=ˮO:P+ٛ&xOVv[zA5l*fw?ۛq?ev+-zɈF~xk[,HY(9w0 /ʖ\.sDV`rglu7A7-Q[:btӴ%R)#of 한UAȆl`97M~Npg޵۲+F(W[FLŠ}&`6#VI5VbH{ \1sӁC=<#'@ed]Zt/qzohs97uhoǜy{8WYԪz6'+w[ȣ )-ٕȽR"9*xN g3_92PdH4AVY)&y]_hx'yxN ϼy,O2Mq$ǠYc 쥞=3߳, G$]>?'@Zڊr \nHT ly`j,[L $Cy S$#qBp`ߊG-Z/Q]N p,iݎG:T 9=)4dÌ֖2ɶۛځTKTVȀ 9#{J+B&Km-zS8'^G{'mVS^6,*l,CôvoZ;3$Tprc/ 4daG"2A1/3v?sf `SJ+K򮾈 (΍5hq~8=5c`JL,$&$;8L>Ty&ImQ@73UڑAVK]fg YgKse|bL&(׫:{<Ԡgaa(-fZ-*Nܡ7F MC}yq,}hkE\41528$*9: lybi&-w@g|li~pyѬo~7yГ=Aΐi[-pT*Bu{&TjAa{1 L:V ~OlC"&:Ի.y0ř8k7.d  uZ@ؾ\q3y/vB˙qj ^`<.4#̓k;BMqwBIƣ%͛PACڲ8.N.0a2ER<鴵:y#sa{lL:Yx 0fM""}ѱy:Rݢ3bgMcs?2Qj#GC8F`^/"vGTSùh_]% '&h:z+Aȑ"|rݮ }1>ݷr!oLx%m^}vwb^&cqCc.F]hjckJ>;FYn|khbgR6:)dziS'I_0.e+M0Da$[$eAgh>Շ^~ŐU(ow,UwlK: 1fD9(TGlQ&]d~@b{뗸A$D\@}}tc08|a9pxH0~D#H̭+M(-)p㴁-Z4Z _p;^8FR~w*wt6>h שj[fnX1$eISm/i`wq]V䂣ۡJ̗RmetPEjimq3@-L.Qyhe%xNakSsq&kkO_1JɘH2Ihz{"~zzD:bP`g]&UE1 3W69 &^zu7}hNcwܚ\3P1za|& ?%A9w|/K+~)D~2_253NSA#Y\G;l"c|uS7rpYظ֑~H .]l p,#gKܤ9;"@glҮd/*"9WF_Vi[hI%߆?Y%Yc3=@֐4SM3 "bRe(ح/Y7d2;JZ ŘN\js#r2 3>w7Av &!۟K0Ѭra* &p i:$g;࡫l e-xfS" ١*1@F3a 7uxOL7µ4 C,>d Ҹб %ZmT #kB 2 5ޒCfv7R1J'S@@Y">"P|w=C(6ժŅ`> y彶}Α<ʰ& R‘&0S`ՂA *9 ):OU ;|Ϊn^A'!X[( IC:Pue%oA0((l lfΜb@ #Kʄ { se *=+4Ⱦ òWffl /.讋5Y% 5yA A%WIf x,hM{-6K@xI'Q) >#ߙ'笞 ܳq"a.f *e=G/nN2 w =D_̞٬OxT:p]ҔAԡJ91ۤQ]tez8p'Hę>P\\+A횹dЌ:Yوpk(>ҩ)iL=~=>Gy.|#+6k³]\$ޔkp\$^g "rs\{e2,v=>t0?S9JVeMM"2*C`[~yζU'dL5amMH-fLX5@C{T^v0HaR9Dy 4L7K-ux\m8CaHtSߞ"ӡ8?4eq `X}sk$&ęYY^7fw^o\;r/+0ӝƊtşK4+0td~x 2A߿)[f]d((zLS:/v7K66<,77xß8.a̲ak9**57SR'e>g7`@'쫶#:p~qw쬕cc<^Ɠ_[O T1- 8I`: Cc=N&"jBpynљ,KCLHE: 0VV͔XlZ>td{uH]g;:|`hA~ٝ&pƬ@ mH/\-L?AktArpuY2^P [uRq. Ö(-ao9N#+mBҲ6kdrHMu on3_{_Qq9MP:! \"]V;{N pzjf+ V@uQ?<8}+  ?ChY6vзtku;6;س=*۱E:/yvE1Ak[Xs@{NJ͍U}F&+gཻ2I'ya坝uy2YX[~D9ۯ}{дAQ:~%gBtT:#[,XsrT#HO[Yf,b'Yrm2UCHPu6c0|b6uf2k 1tb#I~l` V+ -ZrV"SvmPZ|VlÇ*F pMK4Porsf*N]jQxNpA tQP8wDK$G kˮ:~_-#V"As& ab y܎1*K4Kż7:*a0>9\IWtN\@&jBaT+QՇ@9p ]Fke&exC$8΄l$ n.xF;ۍLUɨqos$tޏzGgSWDkv*QL3TlFOlo`MDc$6V?Kjohh58va+9VąkGoR"W#GQ>w;tJG qcF?s4tkV7I=.l8ЖNϱw_19!vWʝa1cܓ aiўvhg5 R?;ǎoؿ"]F>  \DΖ%ɝ(KEYµ釿j{1|k]ҝ翨pe8S;?H6@?NDuwhN޾z޲7[% ҃:qG{;Ou: FUcU;b7zW"gxrV|Q\XG|B)[x^YBv>{Xo'kҡOEG_*4Cb*`&H㎞~ M'1&l{E$Ƌ"mgexG6q h?mmqLWhsW^ yI`r`[~/ƆIc{`$^-|8}iuY JfljDî 9VgzGuxM?};~Winf'r;e0.f8{Y`/T:@*hoFxc{QQԡ1q`ؔ\ VI lͺdNɽ5$Kj .#Y0W jVEkrfiJ~f!>-y\U痟Wdh@*@JR55)@kq/ T*+UN\@3I**'=<>, *#dv1jg-]-OوsMR]|1`x†@]:D4|$DTL.itsMRe&*vxa/6NV@eO'V(CB|#MB6ҭg֐ qo&U-΢GP\3o^=T/gF:{ʀy;ucmb1xx<ơ2Z'Hڶfp~i>"GF< *d4cuY5; kM|"9pJ7V,<,mML#TmoA3Y oȸQT᭾~ >/Ǘ:;5tr]|(ꠗϿ :(]׷D!գ1Ϋ5ȰL(c#!2PW>t>֙yc+1Co;5$([u e O^L ^滭ؕu4dydr|0h1+ ytRb |*K$4q ^Ƒ:&1$} JB\g#VUs%>Ý̔OydL(d=wm ı;0e2߬Dߑn;~~ Ru=]Z!a2kE& l&?a^#bRCWv'reV㳑|ʞkgdD[AA!G"KZ}xsCwyݠ,`Y̴]DYi"Poc-GD2|N ]a_^0@Skt3xJPDP}z>r<:|[[Ks|53&iM.0aE2\yHha4(kI+V3b˸ |%:T ۂ3Bg6Սy^?5pQi^+hL> r/c\Md7 {_*k?-m \a >3 o9 ]5i]*xrJZnadKZH__|Ml-f$~0*6usČ7(i=ߠ[52!y! NaAY:##K89}dN=;L{+f$Mv-T6{Gs {׾% d>MFm'`0̧}5י~o SL+W0-g>jP<=NQc?4|}7߉3[{WNU+hzK{uzac4ڗS?*!&V5{2]}3_T霤 wPuYnF;/, >v-W966rVrwmo]/:8=Ɖ+WauiM᏶Pg:K.Jv`I~5BK3p~Zz)EeFOm(o3Dhyvr3gSRl68c.MahfVUkܫ-J،gmO˫mMBfW;u`Z7k7˭,3]h:~V-yn\OsJ _|ѶrOtٝ>l?Pt><+xU޻lvlo 7O> :0>zڃdGLbpKI<4f8=/*|ߖc˾tç6:r98`CgxasRkjϻ⬺wtC%,dꍂ{{w\b|-YϢl8d3r!޹?]ARdRk[pQ:.\{^w!o x˞G=`x7?6&8-[#㏘wtp5НW^#au6^Oޢ3;%O8 r -;u@Xv[]۳Jn}^7ĝKlsOxom_nsI/)`_jlտ_t,d,vNP;aǰm$o^!-{[O2ʷwϜ9^s $jѫnPE?˟WL*0[yk9 \ϯSAra$\OiMϢ;] V.$I{nܗ/q/:Z˧p8{1~SZҚ[>ugv hٛC+Y/w0g!2NQ~6Dີr [ _|;%;I5XQ^+/\#2 = OxI*GZ|{.:Em>}7fG}{bWٺZl_.o~scváT}#FMO/k/q\r"Kn6>p2g*ff窟ݟ/:NV;~xױKw\>\ G=Tl"]}_ Qvid7뾻w>5WuN'凿֯8ڱdpt+mh2{66CǗٽײ]Ts,KCp/}nVv#z}hl&ӵ=3ْc':CIhťrq+1c'W]ɖ:rPwJjhog2;PrjwIZ=|={K&\Y6Q|Ul𖍎W; Cե<~Rj],TZGaޚثs!]Ykg^gClL~U(zNM;vIu~۾CҝhzYZA]}y+_]^7)q˒rt"uoXێhQ.R 2r@u|5Qٳnt"#ug OLw Rv hpP8\g`aMd. |A)[V9 r̙x-DVL-bHK2Z՝eʴj`1F_$8tJ!YEs%e\[ m1xשB;%~ QL"n<_Em[A: (pƍk+A0ʩyo7u:?6ZR6չT['RY '$\ l+եv'[ n{ ?bP06s_v4鍪%pDgᩚY ۚH/ &GсT|idC%i3@|p -.hDBRgoӪNљ᩽p4/?_KM);J;U M 6G]JJ3ʼn#z{IX_9r22 ~eN>. wN45.G?!_f'= s*[7xlG8jq0ڑ3 Yn=^O_~y& ̚t*iM@/4Gy?tฟ᪎;KDܟ=dlq`LArÎ]LPu|%ž85ӻې^͇<#z{3rژp@F_i@vm_;\}ptb>j;|O9RjDn yVɛt_I<*pNcj/K$qDXצu{ncKBq4=bV oR5۩s%ڱSdstS2j7<9mu`p>۞RrxmQcpyMг@[;p:,ȶ^ʷs/:VV A:D;K۲u6L1%wlc%k>Tsr0X]-D: '~r$k--~h$ث]Ul>kwC+2ڵetθ_qg>xwԪ6{Z#/{t'⸵>ƾ%y&a{%!faGoܛ7mO:f.g S}:̫~В7w(3{?T"hQ0~󋁿;Y7;N;aRV24 *c&adEՅS,;05yɒ}0YwFJc﨑8˾8[zctڍx.d|ܴmMQUT;1ԭH@33>짛W.KufnzMXBsD#bx1>{4:^lo GvJcpUآ:O2^ʊ8&±f*$|n˟l9/'.f-66dI'[=$u:{X̽W+ּ:d[s^㻇k!8!ɞc_ Q%$ SwdلH/P),>ӱTK6#pcS 4"8{JؠOKF]*o/~9)&.wWa勗zcot_Pݾu :Q,ЍgO~c@_qIH3_:–Kk#.7N/˯}_~?rSvo|k Ξ=Sgŋ;:v)ۏt9RU7i'^Yξ?Zw~eGk'|pNY'ϒ*xVݻS6GN?MXakqh_zU=$o/ҧ`-f$;zoɖ"b,VzgGbQ{*L8nDw}xUpk}~Jƻ2`tb@ 3@3BZ6̝;*.\)8F'18K198?B=HR*N@>w^eC@9`/m01@la՛9%ME35>x%:?*R KWoNlۄuALe2ݫ˜7)x 1po֌;DL$ >E0rMr 2,g#]cz^zkU-3xOf 1 ZmoseAHơK P 5 G{>LC׮Deق[bMza9+Qr? ؒ-8&e ɸJ Nc+S&jvfu&TFXeppn]Փ2&Ku=טּk9oôEmÞm 29;(/ku@6k&>֖) %pz=| (<΀h10rs9 }-]MoJUS^v̺;&ee7͡=푖SMiȶoд|"KUev/j㱫Қ#wV;6XT,oYm(cKh8cW0fO~BR2Qx8ԀZWTVb>7QF@ic\RWx]т3ιV=QKk N Uia7% kt8Cia5m;B?Z8,|9__ϕl+OI|鼦2SERshϾrN 1<ߒ.r26SFùOf`F_?*px,~P,?JS9,<ke()O6p: H^'}T(#ȩ'U1bႠfsfH8V/AW[/.j%#>$h5y#ihMn7MkIk1It }X.y-y GS1N.*9yq"ZxOU" H(^"nogRFNՉ4{c%# lZ`Ì]UssKj]x %0 I-‰%3IǕL&%]ׇgɵ.Dgr Z=3~s_VVrs EӏI ^wz:IdBMq5yį,0<>g/g{/@e.hhtر}%lH?V8`w3Z|e5< G{~[ANSNUypӐN<5FЋ؂w.|[7f3N svՍtisفދN7&$9M8 =&=-]Q/,S|.Vs9z *m<&OdVsy{um}!FšANfb y84/kµ=;u{-/gh͋ojo{->*|q:0%L4w NC %%?KWj}ڣ?lꏞ=:uРn䜧uzTZ=6/)OPl Oji0ݲuS2vaFM*3I"xAK[ipj?mMeC1 ~ Yblg6 Ѿć2x"–16 ;t|h88g~g8Zf#9_7oש' t%y+ҍ1s{;Xɼ_gqvKhk3Kng}z\L?*資\~J=o|ǎAP) }3@?ժ=YG5p ^}>Õq3ҪlHmOuj+%AEAUG?'.9@tB2BۃǵG[sg8f{lϚy3n2r  'Nht@0{M5G'Ӿ;pw]0xۀ=S+W8*{x<_w4 ɞuuiLd"mm81_okK+_7Y-c N{\fg=^2 {NǾ9@ K~(q x(_kI{w*yQ8씇e_}PoW7N<~w~䫎Xxqrۢu+E(dV᾽I,:Ucy#ocM],= .Xlg[oTU$vzprKnOw,_{r7拼S;] _/O|?^uu[idF/J`Rwˑ./{PQٽ?戴m%8(@~u1+_ S_uU65z(|Ï7"x&1tO&_ts@;ɷ5.6b/‘b5&FkrȽ{j۾5aw/7=ȃpG%.4oWE/[v$)c ۤ`ɣtJ-SC!b\i9|_|#>HG6{áؒAm:}ޱ!z_}팇s4eL~ (SC=<%^Zus/ӊ f̞)q"vN?XG

U al?oQ1K<Մ_"&cFSskYg,SRi1XaY]cDm2Tӯwp1c.Jyt#[E$}G8ܼywY<9яW^_gO*NI2;< m#z. 0X.ھ{ӫ_ ph> 9p{[2- 4{giaivwG8&Fwlr(*\1"Zb1 >#t@ 3uqT+=YBd 6\p8T|}JPpP@cQr*.#MfJ&%BٻE8 W[7x~A ֠n?v6-eka6#M@)AyE]!+孄w =,g9ЊϊV8n*EKtuȼD`T$ ౷29 `,|hd`45 JS9hes50&UF]a -WiKe&?zghc3z;V4rBX#huq'vSagRA7͠s%eg*tV'+4 1gȾ7J-<&JxUܧ' B#w\}JzDi'H;~aQ8ӽ;XvȘ /ϩ^LN[X'MaJB%+MZ\Ų'yW_{)KKxr * JעG*EfH"n\0'.5a3- :ꨅV4ڡب D[hנA CaZnd^yrͳbtXs!g.&mAjle~ΘiCI'׌KTb(#{[*l&hL9(-<٩R_],,1- (XQc ʓ ZZ4lN#>,Y*xq,:Ȩ̫븧_KJqWY<Q" 5멧(GQI%U܉yGƟhkܢu7k8/ꃏVwEg&: hX,W[ Qz)@"vpalCBKAe;6nuyЧn$^u2j4}y̧7q5R%qXPt v7 mPIVf%`wtzkL[fFd3̜ `T89ϰ56 |~Y?;^ 8_,0 娾S__)W ܘÀ9u Kk#snd, ޺q0ۗ&$g޴SHH anM^; P*TF'> cvz1ӄe F=gWH~lpKׯ_lC>s.$* #q+^ x K8[JRWw'(߰I$~QE6%$Gd*GzZ{$N$&{Jv.[3|h+\zo|-Ѵp`Btxm{Y}i7k@bW vȪŮ*F` tO%Ͽp߾)-9> AMk+x/bL?o#坤rl3bld̷>0 w̯q)2O #+;33>sO2&|cߌ'|+Qg6 89cgpVPntzV) |}H27<9U\_,09(G{NT<G DyuY> +ͩDϵ@zE횪x?~\}8$.^x|OR~oG}rcyD$><_ -)⫌keU)Zy ;[|v{$ٞ]D1 g=^2p fquY̚6K>| Տ@v)Ӕ͏x79fC|R2= eCSKM.@4Ç~Yc$p--_]$hZˉ;4-\CGғT$>.\|liŞfGP(+k4 E;Wf3P'/v^]o?J!gS{נIdʣF nQy_nG [-r-_5)Nu|'~&vWmN٠\6e [MA~z |W*3gڒaI; M+9s#81FC%6ՠ*Y =3aT ﴋfaO,SֶWwQ>gBױ(d2 _[1[%Qer$I}-~뉾:f*zwt&4ZNeEa1] Hdc/4>?gb4 v?cP珮zyV| w:t ]0n#_50mb>&< {j#@ V6cx^.CC.8|EBynٙ1;G">)9ϔvw^NA12xm/ Jy3eu5ף)bg0qW1rmu=I ~l<]ƌk ܻ{" T#< }Ӛ)z2ܫH-ZzcTk B{G7&2_]e*n݊qj45J+{ݡrgf83ތ.ϥS%J<(2i kgpP!A>ӉSV2`CD0iն:l˕*i`duI½= g%DV RZnx5WP2]`'c81z[:Īo`HB|Z>jptE *e\Y ]{c1Z֦/ L'A@%ږpH0>6nW8-Z32*P'!B=gi*.&&<җNɑw$fe( V@_Κ%SSY3*zϽ7vc:~:ֿIUEPTK?}o4+陗>GӏERwɯ Stk} Dybl8 _$/~=z[źVe+.uJx.8G1c7 H5 mTi4غǻVn޸#\0|CweLNOUaH'\fս~K?Y7ce=WkAth$=qQ{Rc{Gk p_缌^%u,ڻt44<<@[n):gQef& 0~s/F;;ֽ;8 7#i׶9Ys 4?6 ûP[@IDAT)U$͢9kDA KϽrNm)f''ҩco/~wA`@k{:h_ݣo2xwl% +I-u1iiD+*eMgy{)> ND[5gPB H.a<ٶPFU5 .B ّ yƳ&Jߵ_CXԣ͠\V9 R1΄O[e{=ե徾4xH8xkZ OVaȩ8Xe :vԓq?H^;wy(ΪϨ G$YAb3ÝiaW:աF$NgU> ces*RkH%un#[)2 =7Ƒ;,s+?m-4-0v;S~'E5gC= evtcI|#p,}ϷD=9&8 :MЩ|zD埶J$56Wq؞ʱ%' XX fv;9-lgkmzK.LǮABpÄyPѶ'*ЃV(e7%hY&bi`| k ԅMƴS17[~W|tn:|0|yD283ìpĂKIjjMKcep c|2[Ymǥ+. rf}I/Z֒4ppp`:eԡй 5Cڿv@WT0 c Rš%qOQΕbV̀V(σw;8'`"nǃt x/[d^3ǞIsߞD":gzER=g:ع{k8fH`#Yn! @YG LLAXKMbM!EK؛oW뵬1Ч*<>kl l 6Tp"pudWtP/芞* xɃZ>~=pF3G,ԱYi^`D>Ay4$g#[~Lw|釙h¹!%VXڂo@g/[zeWd0NłXrB'۔a0 QQ *W Vr"`/v4uu唶<]X 兛xH 7\XH]#`6;*ٳ ?۬B{ Lv6 iG9Ky|.>gȺv1VWQHϒm9Giڒ;>fb~LeARg,,"P|*O)OV^he8XS$X@mU;I!C+oWʗ(?]| q]|j5Ld'N󝣢[: qAsyN?] r^^k:b$gfԻeuR/3 g577{ -TG`oߵC} iؽcK18/ٟ"v$gL C $46=w=ϕE046vnpkܓ]_W3Ϝ@=|TaBw$@QݮIwOG7 _?wQ3↸ ψ:yp<ػ8 gTY"Hp+maΡCknOc#[ N)`-t૯5]FbC(@OuCY>XRt ~xna|Xf ޸~5yi|5pއVeCq15K)$S/g/bISiڗgeL'="K|=S4)ЊA+k7nkƯ=u'$Zs'm2kQ% wq^ b/4Y ٕZ/|˳Eh3b4n#[lܣ-gĪۮܫ?=9pЛc!&;@{̤wegZ[zyFF2~^#iGa ;x6}: YX M]r `ظSĆN} {yX +е>:})g@EM^=a`EEv)ETgS Y?9hny Sdའ(;$+ &LJm4 *;+ V DjЁ' QpŤB 0C4  $xIJ.ϼ*.%JV("As70F寁]Ƅbpekf?? jp`Dh re sHU!o1`/(| [Md\砫 BxT.edgGw:M ^۱{94 /VtpRB*ݻk OJArc7$<:3ʕ<ߒ=nF/ͤ'lΟ>J|7OoVO_+eh{lA_ F잠ugւ)_NO>ۡ,~& ~t8<<8}@^/9=PEרf|' ?zޅRY`U'QAqՠw+u4]O=Ba/$xM)ٙk,e8Z+ H/Cރ_n*U&TZhA>};j%xw @Z+=S$w<}C csX:3qVe0 1*$?yPz卯D`߉wEI-NTU hw(C*N]=y(/eL"I$׌ &a|jc!dE{I@*-@oy(U9] ᨂ3G>=K$,VZlPZӈ\@!qNE߁9dJ 471>(0>`@aN] +T7Aj7`'aAe|i[pىe"A3}hB l ;r{]"YIe/zw&}:^p.*&ѱSՑάL1lj`ߣkL:{3@⁼H=qVT)g.y?b Y4D׊ٛMu/*OKT"Vۏ-Eqfe #Ačj^ EUƣ>=c\ЀR Ɵ/hg"4b+׍FEAm7P{N򸱉jP"u4h6uI:l1F ƀF֍e~14ݾG@ c8)aLN 7=bʧtP/,)Lj'`5Bod+$TWL5rj6DEN>[ZZɽd }V!C\w™SO"0ƆGXGvLL&9g{{+xg'Oq\_ <[C7Mt pUYm@gL@4eSʩ'O~ d8&^ӀZ'Hvg@co|`2:@V~Kҷcn ;Ogϔ)mΧz*:RUCK|_%v 9؊NFR4}+MЎ\7'3 pR;Rنl&ܔAЋ>p.<:]搞VEr?D\]7Y920jUw{:sػd&zJ[dY)QLuƿenU>ʒ>tḒKҮɁ8M }D0iFmB;aAQa<@N|Xʤ]MY<%8glnk繭4F;9$vSw:D@ٚxGOW40Wxf~!{}ܚFF ޣ.{=_YLoN 4[_Ntǥ#Had yIagWd-\Q~߲WIw85|㰗&L.gh*)fȻ \왟Ϡ|$>(Vg~,F`!$K ǎ_y{x̽JSR U_N썁v7#nwmcu_39'mSF%ocր:@̞A4};ZuHw]ěC pfS؄HvѿZOjC"}&wnߌs_Υg-&>;kAsdזeAzC ;L3Rb uyePwɮ/@vH7YIb $t8|3u~Pg.v41ѽ[A`y*Ga82wxnj&D<<ᄁkˮ1ϴb3гs 'n1 _.+febX €b8f2+.TwOVX|yv?G `1 `uM0ޱQ: Ja({4fxJpsos҅0~A6Fɳ@\50.ـkϱ]hд4j2%'o~i{Дşl(.+&Lo؇q s[|-\<[XvRnMRH<~XnqEPVLd~myʠRr5~c"~0k7Mm`~mn1\Cã$;FRd6*vĭ!P-͆Yم}i~H+S<$E*a 0U4o'/ "ژn30__W Ky("D$"9iq}v]d6>Zi@2g[y"=Wl|tKc Liq,XL`?\͹vY3+$%لw*_ nFz5aqcy / :u4mU8kNY*JX9睱7<5B8khrrU:M)(V춦@HK TYd16~UT.ώ ';(Kq#R?~m/K m2Q2i`kL۾Bz0@*IU!5`O2ȫLFAZe > *2#Γ,"8z 8A)X;{9`H@ʸ[ev~f sw~N X-ifbb4N4r]WDЇ=*@4M<& | Ũtrc$jB>Dt{wH@XgP z@u%JO'Μ !Fׅ_IW.}Pfs< A#ǩ|9Vr. }Cg`<ձh{5ufZ)K'HEE]tPJs*(HU( G`+{3*2V7?UgLh5Y΁ Tt6USɉ1ln٤^bFpΖPUjULP!ݡ 24@Si/d=x^Z8KUSBN*u"` ʥ-$tj(O8U¤Gb(*GUjVqpiΣ\FPwٲ*>x yKyt,!񦽓Ly2f;IUs5I\K:tܠ.z)6C=5m1[VTW2Tg|Ic $Xϙ pL$ku?AN>w%uƱL:ȵ F* pݥ-) -c%9 I6#y{7)k?t|J-\TПgƇ9<3E\R{9V:4|}zKInTyqޏ+xO\\g~&0 |gG ΞCy#ҠAcuQJ3 {Mdݾ>g;!le sr~־c< \dtL3[ŵeK>2Tg]H߅Y&焣@;WG^~d&26cfwN!3?~ksSe:31*;70##`k Հ4bIJY]h!gAj;gkLpT/z\#s Qۺ$z>G}u]~E-v5̅Z5j.|6VIԟy%Y1xcxC{sG{<SgCY_!CX?p6QMX9|i/Ȩ+  XӒ'|0tB&\5k輮-*sN#BAk0!y>[]]QlRs8012ihTGb)cF⁺4U`*npJ bMGYNσy|>}֒]e ivV/бɮ*)ymݘzx3MV4Įյ|9_%&#].WϑO,Qm|ѝhЫך(,/sҪv>TF?]R?I҆wۥLvxep"X?jZb ؕT^c6⛝"t?m"CB /'(j|k1HA+=d]N=G;#YMspᰑf788|!;f;h,WZЮ ҇ՠ^{j2ֳ \MUX책mqmydIWv39 +#{|Ne5YY[H?5>5 Rg-/bʒYiԪyI"Z^97lk{2ݹkLk Ԫ?.r``Og,aso\FWd>^JfC92x_TuHPd?ܵ():W~D/M1uhv׶,cFB16+}Z\&9#s޷+&g}.]:?x`fm<IuCKೝM0ͽ#KUUFvuY&xBɞ[1BI؀^ l21|s;F<BWL(< Eݠ7}v_d9hwo GEڥuʉ0"plRp` a YTNUG2zZ2BG}*+d(|e"~`@1€:nU %b4 fʴ ÖuVzp20<f!.aŌ/|'mH hnmGoʑ+ NL=~uswGOeM `jkn%eu03zo-@ 6,0`y͂RP>*lMVWJ *iP*KH&a-R`HҭSճ bP]T,}0%c2H_^g਀a6(3o_"84ޢG{*뎘?xX OAS[n_#y#58&'I4;z2*&b嘂AsLۆs( TRUɤcMOO%xWDW)e|:?V8*~nV3_ LMq4;1ZM* |;-R$lSyP%J CL/??8_x+'`(M Hu\0z^ gsԵu*& /" 2xi*#3r4VbK:գlb8^GT `:uCS!pz>4!ys}]t9J]|ߠc39SGڑ/N5Hۿ+i#G{3Sgd[ZN0S'mG ݥbV:(dG+LpD'w<d/XtwSWOAtʶё23{]]*:8$qQxb!V^nl]A~}R;ᘲK.{0Hm/~~D;(N3 t>؂oR1>z)` {9n$#4S&3I⻕dk6ẈQ(zH':]S-i}iAoeo sttQy0ٮNϲK )֦3@j֝] _*}id2i3Qds%$}U\.+B&iJ(Yfm(@bkQ53~%pX5e=//ۗ5ʹØK|a~߯x,/i 9DŽ4z!@}~5XZÑx(IpԫGtjHKқH!1dԖT e>M/9 \~Aŗ:Jtn /e ]|;PLܹcr[a]bkAKf3<6eп/ p*x0 Zϱ.|m1WGRgEqŤ';e*\::& VVbf $OdUO8xpSgQ 85'D f 8u m  Xyӻ vlla.KI`z9܏|$=dr)t֤ C̡lCY GXN97x,Q^PCx []];ݯ{ 0kgbQmmUڟ:@(rkNg/jflkU-36<}n6<jMdgTZN_.^;PC:!5(Jd@4ܵcIUWrm~A$KWGǣ#c!/t <%:3J{t&8  5ReU~$µwE[o!/KpA-[Ѩp_ffF3⿷7ոeeuT>&{ 8qB[a>-LyMfd?N>St֗bSM-.iw"A5t}GH8`onfVqyv+S1kb(Х*uh.Ot4bn59m8#Gm!Q8Te=90=$ℬ'6ڍBi^^-IO!gthTgi;uT3^&Ot.zctrҵT$-κ*ڀ' #(C̺4ݑ:?X Eɔ҃WG!!H<1q) 8u*Luv!?~A"O[{М3MMs/A` _Y\Vpġg)3S\fLiޥyʱ湚jlh R `n߲σm L&h2%:+P[0-ma3~ =x֨s/i'h z$\L"'<ρ[&/#w}$희*z32rq "@^j/tS7L![݋|RVpVs>CsAL-+ύ44|BF&ן$x5+3W`Z3›߭ypb[jYWCo ]#g`JW?Y<0cd;LMO2>]·'۞2&2QָnÆg;00Sʎ1xgAcD<sԶH £B>Gg& <_>e=̣6_Rvq&c x.p{$ź x={yy$C"ʠ"s 7ߡ?VB 3W,9s3H9//l$S8el1` T: sNe ?ұտMzqBSyDq~;Տ?F&G\S}~ic L&X+-iÊg݇lE&utvE1H{W7~34'/5*~ N{O=9I4 %3 HK|$3+>@{fW̵%UoIk$׈-L?KW+[WIXX_\06q;y:6_x%l@ǜ-Mg땣k{l#nne}ږāE&-xVOrAɺ | 7 <]_YtSttfNY]ׯ[y^qX35eN&҂q5:a]Vmyd8 _2(;әyx o} &#ȃ(%on,AVrlv19v $#зjuu}X{wtR2_4צ/6EV.Ʒ->&׶z+(j2G7G/D^ar_@2vj]0ijR6^c+6U%~Co`na=~+-\Ƌ2ؔ > _;>O6j-V+"yy]Fq_aؽ262L=:wƨA._ДzQlmkkEgu:@ٓZ-4mIL:A8@8*@o7lCp8|_Np~|2-7. Y HhzvN+^}Wf=NUR:MZ 6m]V= 00-Ye*:tZy5Ž8aJz[A=m`-V.Q@IDATTX=cfHL SM`e]&Tz?|Uu=w0mmpd2T7LsVsd힄6w%jҚ}jrdFֹ]`H+| c ~` g(ϔbA^xM@컦6?Df?$S֗tD*f(=~TbE2Ӫ`5$.6~5)P;7C =N?s9^z (6ڥ|6޴;wB:~IJ0CGKr:ăS;-{~#Ю]N˦y*hNฬ6}/HM?Bga) f4h$M q;xdTB!%oX#huq:=΃T48gsnLWu9}2?r{./~mR'C:UHLrG |}v@8J ze㴴'+pwl} vW5uJB*YƍJa$@C9VjLm2Izݠy: ;F`@;wS-&lZ]4xBD<{1eTab7D@U5pR7~>|txā3ϑPRc8Gb<7kGZ{gXpVGA>h2?$M4^|Ͻ9o.xBd'KҍmT!{̱ /DBMgCZDa%O1l<ƖҹW(>;qL50"_;v @܋|1s׫1 mЭrz 0& :_ 2' iQ%^q1? hO߶2F|v4agW-_L3QKŴA #u^f Cc仨'2'_:M]yc9Ep\": s]+ 9ףaF5H{-aiv.YL&ǨJLov4΀Ԙ)ɘ5h6Y?! +fX )/ om6(}X!^ &i_XIS@w7t"]+ud.c5nr:2[~\TGц'9A\}=ڙk#9| rd|D!O|s DDɧu{C9U` O'ok?x?tZ :#dpV1:>vnsޠC?s*M[0v{q7:0N[ǁg-|S._Cr_=_*@_g4I&}tc&LZm+M[{gVtxa/-k 1I U:+)m.Axy6)_tZH נo6V t^bKuxk:W&IdyW*/.{S{~c <9Pi( O /U@Yq8P{CcX3dS۷ :K׳4PڭZR[O.:wE~AƖ7 ?rY;6:Ař];Йյܙ lm`lsXOGQ۸otd$OIK {I^:>|:r x6e8#3|s,K2Yⳝ3kӱMgŠUAà|;CeC1zdl3^G?Iܐwy]_"GrAYHa\iĹtfITȍ 8 r`p0ॆg+3̻#$Q6)3})`]SM<}Εwnxzz\ݵMa.9PM4HkP0tA`!@]Ga"POs]ͪ{R<kxV)|5Tp ~tX)cL&s?v)ݳ[֮ۮ7S&xϪɼ]ez¤Y4d9L>$'N]Q|{%}^{R}gL7qa ĸ2/z9+dTR{3"_˥ȣd͛7??o\W՘~;(@ssMX,9Bv>,AؓX"\ q= w-;Ҍ2[l`=&Κ;7(5շF`;-tJ|k/8iTTw9#u&J9/m5dgڵ3"s]ΓVW^?${͍kxSC:pO< up: =CtߓZG'u+=:obA%Z !~cCOgDžBc{t!Tu[=E,p-p͊*?˕o)tOVl>j.XPT ŗɈiOۭUդ mrC}H&)gOAN}ȡܼ| (P>;r\VzUz,x5$C0(QB /dYgQatyP4[ϳf;|ަbCM=`i9#=Ұ+]ozV-vWhCGͥ Cf"Q =bigs!Z֔t}8! Cv5v/GȒ((^Huũ.@g#Q92Z`V 3ZCCMvV\\T~K&u/ `$ h;S0#vn\CRF7ocvE~K)&2") [w{Z ~$j|=즰)W{L m q_&HBx'4i_ w >1he?ǔ$kOb[>`ij^+,^q%gFz[V=_Cr|k s;\xEv4v Ûi?aN]]\`3a8!M `aҽd2@5x]Usw;YHX2鞃]cH&lHՍġ莊,-6;t(kogniy8u~gZg_!EP23pe 1àV`D%ne,FgVX9HK @#1f= $l:>yx3 2# *ԖgC&> t"LSdu8i]:&azgTTiTKXğ5pц(2RJI)5>tBc['込c , fTj7!h[S36B,3~ߡ&$?`H8%1*|yt3(n"ȭ&dnqv/<ݷʉs쏹{gBZU<1_ALpWP$2u6 Gυ^ L F'l=PKE /W!`VrMkO$4As/c<G(r?8GAGs CJBGo ]q=P_g.^cay>:2HN8BkIP}zK'ZwNȐؚkѭ32"uj fQNtUu߱P?61X//z %/1Zk=H8586y-2͚Ķ.Ӻ @ zz_wG齟Y*:0p/;w3;?i']mId$+Kڥ-&F>o3V!<YGBJ܋3`.Ѫ`$ؙ:;3= hC1_|=zgKiKmv]6l@=KZUSpT:4[dZgmo8J1zHqT?n}~3}=No#GTf/: ~0棇qܬ$!q “v%+4".gGKҗ,e8HGJsS5||ܗ|07O#_ 퍫Cs3r >k*ȒmUpZT(g_wJ9~koF]3n2jZ-eAB^MhտXE;tZ'4rob [9Pљv*(ѸmʅPgRefqJ 8'$u>VJh$qlq*h8VUa>[hq`w1Ghi/^EqʋsJ[҈F:$W>y/>Je]җ[,WĦzHiZt`n #4V%p้0:T@\7,KWq0+s+]҂!r Щ\ر4I%\l"}A6LdyV;qtkk\z,P% f Cbߕ0X&m$ƀOyOFIE}7h}6Ax`* ,cs4{Xt-TCts|y&zS69/H&cmxgsLaCѹ#)s_= uKv+n5]GBxIXz$7X\#5n;glq:Vɮ3K%*d mnz^ Mϩ'B<#4ѣk]=~::PTx'lk\G-v)D/&ӯaG-q`=yx8N}&وOl ?эWš]H٫^ӻ} ڽ©Ϡ`Idj Elzq씣#-bżg/zh3 }qS MހqPe.HЭK.g6E埰i얢2O0psue~J3\0!C6P)y4'WG&<H:VY>7']G>$`jp*E*FA5.< |lƆ-*hsEWq0s"ú z&pwqTWvúK/Deﵻ_5 v:][Kݥ]&䑫t ?֥Vjr!3W^X^>.̔zנ3~ y$n#FGgS؏V:OX-jy|:1k^&c8wJf:B*[{3y]M̺P/ʻ 00gdu(4oWPeRϘpf2 巁$Phc%tNob/~ '}>gqf-ǣ&Xq}Zാ&|Z[˥S 6jM8nghc,QΛ`f?wyv[~eӅ$-u*LRIZBI$`@Sb5 Y|!PE3ؚMt1deRajߍ lB߷PR>+ѧ38d/K~AYMݖ?QMSLVz*|)P?4fg<k6eՎ/'f(пt$ndbz| 4@Hc&>zV'L](M$Mvƭ#-:tR|qs<}@]>T^}-|dC^yU]}s6(nE4פ| #6~iY~Ey n{tyf5>o_JoeDFv(灏y؟RY^^ǡ:[݊gJnb+ؿ$R N,SPXH EdtƗ K k E2yypoG2qerzvXYJ<;{TOSˑaCe [MHA/+{/ XG$#5Gdpٚݛٽ&iƍ|b^Lܥzswt~!7QL°|=pwq1r *J.Cy۾c/I 1oMVd>'cwG(4ˠW!cH~*FC~pj6y\LFn+k~~H;6IN*=9{꣘Ï>N6^;-XCQz ᐝf9\cd~>_hZ"x.0oI>td|ԥgT !h<տuxwI% vGw~ 8ɟaMо}! N-nSy߻rU!:NteߣXE_a8PUm]xUsyy״$i'2@ AY&\un2ma^;O}5yNT&hbdwWy%:Τ/2P;br!Z;ǘ=-8k:ҳ?bJ&'X/PŸkpG| guqQyb 4_ȫYCC/ &loq!n` p /gU]w ]J].l\%V:%u*筢O"z8#>E:mqr4y{R $fݼ/&xa$AHۚmL`o$}wC2F'1.abb`P>lB<]fcG;6t"\3o;qqΪ 鎁\le"NC5|A6_:;M>bp U,5tuMs^rG6o3jˍrߖh Y^lY4QQun)ջVtАTVTeZS+C49t˵HSHeW ҌBRF{ku<.%[.n2ޮU*NuK&G=9c:u~`l8m |LùaRK3!Xm9'~c[t Y N*Dt_=R/B"uF-\Aw_F$W nmTD* M9pi.S5?@vYjշv`3IG\w?IWߑMV]`= NT' Nq6/V{vE& 'EvAI=<.x"gNvVY3dT^챾Y˜7 Py(Hd<+%N),ugI!cMϩiG|91Sd,hbEy8X!SG2EpH!G:"lb/)f|sb, 5<@. ]%d20OS]"9fG]6lm'm]tF| 94w )-s3_Gj jMRPl%:f86vF=_MJU({O8Oqu E2GϣO_2ey#&w=^=bǤgm90+,b7ϲ ލ_ q!X8,3ga1_W|EMT[_տt5AhWosLϿoQ"+4{ (7%!wyr̝[N/}n¸;Z\po,Gw\C 6@k^M7b>}m7sr*&ye){NW(LJGFiolH@)찓@s`/@1Oyq@[ |B SL XܫNY kH?2D؈j֦6W (lӂT^7 }\w5>6~ [v(mu/w؍pXJ=ָEƣTO@4=zlhcǎIG^/# {m_g[o]nv)xFܯAؿ[r)ԧgP')C7;~{`dY&}v^lYjx0YF[$,!3#Dy<jESf(b3FCh0D'%HoGN(4 BX\a(CRrr|~0dyLQTPwZ͟WwcgהU?_|0NR΀Zea> }8^1~6t8,sq? LLiv̪!!b7 HHx92bp=2{heNwx+nN8 qLyQ2@S(p2aE4-$63PpFx0`A۠τ6Y" Bi6f[Zڢx[5~ig\YUEk``! ʥ(yi4hKػ /eǕz8S){;prC>#$jt~-"bֹ FVr"#W׮daINY3QCikc}{Lt9}~ZWw-@2v ʭ5_ku!>=L8X\qTjbh"%V609}!Ӧ{%<+ZHT ?UƴͰ0. }d,93xI QѶz&q~oy ^UΣ.ԝ#YL`gL2/3%ΆnL:hgh{m yZzi, m#LL`4)Lg΅ oÃj&(^ϻ=xm`AEz@nG86RG'6!zNƣUʑ=ҕaDdBrLwm8~: '0\vMx< ILv1|֬USQB5׮\ Y%ϲ}xqK ApJ5fc2LRu8{Sc56Qet| 3$K8.՝=;~:YwwHgS;af8T6y3'C^ ]#p#A'} teKesL#NH48!&13k6`y:I *LA>zB=-j<ʽ7Ʃ ^Zm&tI[fzU;b]6WHntºQ+:I{kt-oiX3TgvBeIp3 Rc1EZb"lCX+;z4h}®cKo7 q` Vv@%Ųy{v?)'̠9B: 23r ]̦nX:`'[pڎX(OLa8 ރ{൉0ACG@$AJf_eGۀk3fXhC>*~ץEՕKH:!zILkעffڗ2I[h҉/9!J;'ys< pnn7qn .>[0 g[G!gX1~"ɚ ګP/rұ0Eb1C^.N~ʼA(:ĜѝCyZ qꞸҠ< s᧺ b-"| f߬Nm~֧QqXEt"aXghË3?du **1`c_Z+'L <'guDaidqn4%0T>(_1L{HK 5l[KTW=Ϊ?0`j%9gq]2I# ! ߹6qW!}n@<7A1(R_5W~i$hm+/h [:xG1{5K3~c~wC뱫msތ9OT&y0oS@>p_^!1Ŭ╎/}Z!/oWβUʂ)Sa.\;B>SFcazZ_XFU@#ɿ{=ܵ;Ojwßz܂.'=%JՅAow(R 7I0YΙvUh΍g\I1֘w7k4iiטx>T&X~Lڽ?56CGgj3Tϓ?u-#qfÆ\{|1G=$;:wN(AE!_-ZvT78,^8:>8ؕ=A+u|OWoO"_yjKtSv ]GB(DDUuS~YQ;y^p;~jz飒yG]\L*SlM$׻L!NQuxU'gDHJ82if?޽ /2yed?Of^TWp/ *\^|9nGeϞ9zzGZ:kG^)+=3y-{ґ#I/}dzrT)H]=@o[wM>pr@8c9AG~,ݑɷ'.5'|:]|>,?0JpbK5Tj7`.Og'':]qVR'q0@NWRwO_H FeÿvФ[b ;Ψ: z7xpORIrڑW-g~ר X2O(}w/ 0`KzRuH/?^M=8q42O{>|1kQJ\tbwhjY1აma@>9:1u>͝2xK4PaZ9 QTv:plߓ3t+S_E ƇT!4  _j:t# sf߹>^-\|gH X [{]y57 W&ݹۑ9 wDv7}UN'|*h&|qrԣght4MJ0 xŌ-Z5DYX||PҁkpҶ-||8?Yxrzk \cBfk_dN!8i$h]v4@m!&M.gc-H䳏a8Y̎v'V݁#@.N#[Gyܮ^3g'"rm*4\*1:k;7AQ!{8<6gʾD:Mi8~X<0 갵ւppAc`?oVN2YV^[ëQ]6Q.[ׇl+Y׮\wjF4ЭAe,[*p6$Uu#nԿ˱';hSC9t[`\[ յ:hN$]' ]}償CLt_"l+=glj R9"w`MBǏGux}\M'SJGnNX;yl71il B{ie Ya*h/~<*=t3@07)N%vW>[q{v~)Sc$F_8{NeE ɾ)͚N!$XUAā#79NRCP IC/G"V̍XN]S+Wuc1sԼ ~މZSЙ+ԩ}t軌Y]hySfdSct7A"/Aŕi2^'H{e p3GْM~[4`{Ykش*VBbW/Pҷ h"38HN +f *ێw'b?\y׫mVϊU>S9z&CĐf?Bsw+MxgGY[&jCoTUCzt-07t CFIdܞ*KP.ﳳxT:4t9 ȹ[tx>}9H?2A&K_#Yr_0!`ol5 gCWt._pO3sv_{Yg^:,&Qo5(G=ܙ3t H8`&Jʃ.:0uply#sJӌB vi"]ږ.-bfGy(𫟍1FAcc''6C9ԳvצU`(M"l( =^%} oo)]4XfWy>#n*p؛m'UxoOoO{ル xyٿ+IU7_()Ŧ^oGRmr[f ~@nWRFTy0CDX}_/[4c&KqML OqI'ccrE\.}]!١,B>aZ\JG`2wkF$x#mQS d4b"q=Rx x$v2O?|>!ɲ2R{T&RY3e-ik!"-^1SW5R&K <2ktzac倁2NeiTi SXiLZmK+Q$ȴ_Sɾ6 mf<Q⠳T%fF19q=hƆQAV( PWVJ¼7]6k~D!95ZG]z쐑W@mPK19ぃmnT /njq %[VRP!P#s#/I_?4nt3s߽xDqOw$I˒Q^*wpA߽9ܹs8xNJ\=2ޞ>2nL̜]_hmM57>~%< =8JRiϱDU{~cd؞go,J6 gPQ蘫!k͵򤫫'DE Q^f@,A!p..] 6l(xUw4H]F*ȏ50>!ugOfX0Ikg>z.}D.|| gUrŃHr&{à X}Ài>]$:rswy̽ʍww~4@!r?bJ 8ZB. K{ٹ& !x8 q* |S8D܊7m. [1">,*uАoc:M9>Lw[ۑ$0{F# mk6D`@awꐱ>ߗGuYN-|wo{ QZ ؙ& 4k_txbF$8nGCҽ"CG{RARXC Q1 % }![e.w1'D%r3b'yL֭_+x2g, [j3 m$y11h+&ҹ"Up:(tRA)˞vav9m۹ZlLy襰$P'z c`xesR-.`0k7e=9Kn:*ՍEeN8ӉU.uis( rҡaHz$zmhnugqw=|>D8sa#\YWg߼=Z8F # qZ!/_Ĥ?H]:YӠ?j*C  YuS+Yd֥"QYv.1R~/QY :pld&n2 .,s0LƸVLRF@YG@w=axCh6\DBw2 \smK[lgbUcUcy03XUҡ$2~H1A&t$,:IUr%EG?8tAS/Hk =x9 1yy#+Lݽq3i/N{D]N~j N&<2CA!3*cyR9t8ƣ;ZyQg *2&@ȗ_ e$^#BE,Hv܂J-Bco^ pq^ VBnr ]lO ,L3Fo%o3G^)Lg>&/ 5Fs?P`KzҡJp9{lRZ0@UfG&e.aI3_> 4jpt(ҥh2a|A` R\$O^;)C K]?G> 0u|v몧K,JVIbg{ΥZ!Y綈;#8~>n|g$|Q~-6τCIV]_u ^~q9EҚ:|;tNZ='Wфo%>x`lҜTVWu_dyQFYX&Mŋmh,ű=(1OkzTt}H";~"$eG-OX.Z/QRYr{㼄x%c etnv-252-kxq95ߡofQ~1aW/W0_pu]@vKhaGuqTy7,S5k 7e]LVh?s"J&zze{klۉ71 +i:sd);)NV~-#<:XjB% 93c";;S VW~WXIɢ($few+cnltbmn!v|H*ndvMW=Z_HZ]H7F((o* ͯhE?GQјTx+A0w:Rᥕ4vr*'z쒰5-Nޒ$r/ll BC5-M:p =Ґ]lޜ>KϤ%)UIVK[a.ٝjNulvK- )@%#*ޚg`ckW11`Xd|^#ŋЉp+{7}Bk[xسU v30{_|lqȐ_<#&;wyhGު_Ȗ!qNQ2L:G]ROڑK؃W|7 ^/$@8fz;}e]pg%l"7D^yn[i>= [1=Gs{j}=8[Q,zIgxo r-cvD"yC0{n burVbCCSTݺ~ 6R6s 8E !>2@eB1a#CgiefUWbg#sA"REEgV=4ͬsںuNMW݀X{|V +OFTjVH ųnRQvQh.Cvv@lQ 2O~΄CYE| 8+ ~)w?w O* |_Nޱw8[lT{ҒW>֑lջB1Ǡ{6hKcdžh3~__I/=B`rBz;)} I=y?s}Ts}3˷4W0 _H4}%귨rMx5:ח^=Q"s2J ?/RF@ (` s:.?=` }w+燀un~~/ rRn4D SyR!ugys ( -+5%e< &}+uuE`yk uђSGv[[;G<6i3M,j}RJΡx⒏Bmi_^lA `'pbqdt8=RBVR[E0Ci(qz^hNYy8?d@znyhGK]Ze KLj"'h?{)?O "U) zK[F4~g4 ᷿g9@}I xE*qP Y/A&Q=F$.?="*Wv4:[ v :e|fή4wTG㨎a1dU/W_טzSG|B喕Q:P0~6u,rzGxkzuYqFcccMN%P%P ,צxp~8LuDt޼4(7Zlv]eq:5t2 5p$?gPai\K|n!+T9]5]oy_ɞ _d!kǐ" ^^ynӫXXin%';0pN%.aPdӅ> yC67ߞ;}dGI:`֦ G#O~m;G$Yں>.^мD5J : HhvVu/e*E'AKs[zpkr\9ܔ|qam}b5Y :V);+ udQ~\Z9oV~+#?@ ?E7 ѫ vN.sL:D9Nu_b+ #Luq(nHf˂Xlg…P+p2;wtAa4 /u.iԐY`]^ͻyo4U2u]m3TvmDbzlO8-0v9hnWc xcB|ׇMJ#KᨹM;vZ#ɸ$b$<o v-L71Gm؁viYEb]Ji".|6t z?L>tso=Ej?[!^B:_2aPZ1r~.E9`] c଼P}Yi/csC43Ym@@[6|kCZxcrD̑o@. /.2Ͽ? 9Q[ s'就.CL:W 2#S>GxY&ph(O٘N2!+yN swfn/h,[A/ ɇ :ѯ_p-pmlC(oy}p KzX%Z ~G}J>ė0:5K9iuU~뺅[)2f GL_Pf[&{ ,1$e a Y"}>8Lze:Ϲ`N L n;gU^d'k5 ?%ެՔ!>cl߭a[)đѣ īӐ0z^[7|7!ueqwy;8o7P&s%xz `1N X~1750ҽ=;qdzqe;šQ&yHwN&עmt2x'(榌d7v*+ek Z;|혠d!^с>>&S\cg :/q#y#0cK$ڼ'xufsSp)'wG7ϮU!1uа-W6(OmC׬%va28pɺ|Zm]!C5yy܃>Xްb\c*3^;rf+ϣnN~VT6/@'_st e\Hj;{:7k84tUtZy誧Gkiss$ ٓcWҶz^E_-8J{:|ɤ+6~-+U&-S^v-Քy1{"Yt[]#}X]&>=~,`E.}*ǗCUl[qzp/jI\ƶW16QGR7fBg"Ϙ f.hnikMO!;{#lk4؝N"lgE䓥#6ڄ|y~+dĦ,ju `Q<Ӟ[c>SLNQ| HN t( xxmqCy$.:13`e1[ϢJyPG7 k{؟dKzN#)7kf^Q.pX^bR2&MѦFg!-o{,G&im?] ?qIVŃ ڹɤg(.tLO;wnd'CΔ󅞡SC Ts3ID$~f3`* X u Ju0P%ХcA:ZԚUpހ Hc({Uߴ;8kՋ =Er q'%r*J 10.@«c#ce٬p #Z{B-DJh3hu̬xk"!E tt{vbHRu0L25{>8HN>+rb?+ŋ3iS3-%ԩC_sWDWu+i{o?] v;(`Еx7 g?cЮAtege*>s'Oo\u3 ;w&9Yq0caY_r:z_EpKy'uG{ yD?9}{a??l|q=( MhL9.ɟsacwb:I|9,o3cg}*ށ=I:5īrc;V{f?ۖ^1Yۖҕ^*GB mmhϕafٞ=V\Y[U+@y5<3A@EFoYcSsб] JY5&Duc:Ojs[l#PPlpu_1o6D͞H3ζ% pX/Qc+he)b;2T 8V^.^4@@舑oUrqVOE΅ZdtWéiƽfZ$଴Vid#8ęF茵B4(#} '?}w~+pnJyiT#]",1p:˨S5%P]τU߶Q 6:pã vە^yʹ "gbdFcW wf0v$K'FTl>h NpkPSlyV߄e3_usuY9S@,e9 I/Kg$Aslgy uT"]J8Zl q'_0GK 7n#)$'n%|'-d-| eplգpL6Ι2ည1%wpp`ET2Ȁ 8O]h- ̬.,Ćb:J8~h$7EbNjTuG=`{}֮? Q|ʤ&hDL_پCgҵӕ+$?HrL;|&ԁ=H•:t } 5vu:V$O3ƞ[&- *MࣲNς$> I)H[~vH, WP"3X䁋[*ЏrD9@xs&@C35YɖY0NYUA]c] H0^+tu&HPǮ+'Ae)#f֕I:cX(Kn~~Hzҗ~+vΧo84 otWޭ"/y}PW^a.o1s^ܘ#8snc/ hbռKD̤뵝p]}wVGzV:WT8 Y.c{z[ouO䍧~_V XDr8&^`YPRyGۉ_ ߺ'fĿl noOJ}r"=qxp&1{b}ΘF)71Uvk^Ls80qQS&]Ү8/;y]zB*Z6|{i?)X[ki^#eU )pNt~0?m9[ooPY PI$4}/+[7l&1c ,ty옭+s#I4G/ZOB5/&֣^n؞ʞedjRTi|t`ht|8ˮGy 9<[b‚6wVU~i+t`,7'*pe..#@֭حNYO#cRS]:G*V*&5C;zzV1J3p-ŖAwW7 +*M"C;eȃD {^-}K/(з@w=vVw`I]%u$gWGQNiᶼ$T! DDˀ&*l @phZ.`t2PiA<uRg+blRcL(ڐ JJhMJҁFlH1mh ((|o5OpTg-N8$l5=eM ZN#:ԇz[~¯֛Z t !89tg|2x?pb/3OT`l~[/={}{ErQG0+tFa O/k7Pӆe &V>M%պn$NM/կq,#ÜJzA/q>7o3 c`9mqq_wt\h*ǎ~狿GGOOwo:gX5REWWO(2p`]yOA;|GSo-(붦֮[GL=v쭭qυthT~xKb~ F=cMQ1]L> ^"S8P$wI3{o iUeG]ٟ~sE w?~9% FP><=wx챿8[Gr12?Sw>^:93 ngj[ţ7ʒ 2x=FX&ܷ])*ZڐC(BZP,r:6!oNTa[ emhj|nIŲl8xm uU>s2پژ 1uZ.R.ߴZ皩 z23U.y?u@cDNjUsƤguـ6V%mT t5Iv\e:8V۷S ^0['/EGp+|t:HQΖaTr4]ݞWF9yNت&D(xm̼vjP m5o!KT\GE9V5[LZ5!&RQ"&h4ʌ3Оsձ5fG-&(&fhQY~9}Ɖw4o57D@IDATg7ikkc <R =- 1Y1 rBBx3?HEѣo7yb) o$`_Y9B1#Yg_*t6$u^ vPV(K"kޛ~K_$N|"hM|L<ܓwkꌀ|uFx Sl(6+Ij y`qEd\6g%CwE4gXg acG~h'4&TXo\붆~K7 ձΜU {+u[JpXMl`u<BãO?Sou,8cm3=/}M+,)!4]]$ʶ 9+fљB<X)~IWE,31*0 " ۉ]xn%-k&:gp43G87W]ɽo('H$NƊ2Qq@^j'%"|NUv5GY"_\ /ز2]NS7pG64KT?슡oiEH,\Sg G"j9S^yNRqP&=]ǞiQڱ&3_EIH>mL z]h^ܯݾHEK25i.8ˀɐN\Y-ԡiD@Sc8C}_syl;Qp @x&`ƱWY'ۃz鳟ԯc{mAJPqn_ǍBÛ=|,ǹiHQ.~Gg{\=2 {c֢tu0J2,#ע깁$nmCe %kgFbx`RZ8gLT gvuy5> 8Hv,l##i ~([CJ^:= } X[,Y_c$vd& GgˤseNv܊I" [8gTzoqWfQe A5\` W^{dy&}sn3F$AHs%r ;)WWrZo׻5]{4#is$b&(0D$Nht9y=3RI.f5+p$9۠3p#Ʀou,ǩD=- 96Ҳs{֚6}<~cOw}w:M0Ġ //9O񙕹8+ \co_ĝU78gdzCiyC)AcZG/)w (\AL9Mt gS_|fMF L&-Ŗ[Al!0l~wozf+l҉ |/sbڳ/?z._J}mZ?,t'ρR njTroTh3p,bִMBWG.Tor[pZoyjY126H_b}[[̰ y}v^`SXnYAFhmxЗ ,~B~0/a/v"fQ/GG:\UdR>N<1-J0022H~Ί"ʪ'pzb82 V`WilD3gzs@{%LfV|^6+ Uޓh6j`cX쮦Sa?qi:A͠Wb0P ml-@0E^uY7:4=}LGxOkǎ~ַ~z_?fV>]wyFh }3 xcitмs70JwPUݵ;_ YizG?Ga;{:u=Ddk_Dޅ!&=c7o[Wb0*7|8g0Α O|嗞5۱W_{)F!s ͵,;2w=tu$'CЉhW+/<+'PLC? \xW->OzL4:G}CI*?oQ~* K9MTn: ?@\miJ `*&Hh(WtE*UMf2:m\SWO Hr2l, rL9qSBުTէb[%]CF4t$snT]Etj~'k7ua❴.bKIosxޡ#eeD g#S5THfvN]hY,yS=83xf^ǧ*&<[~9 -V~1og(vPiH7jGs'iIKbYjH׌grȀ'wו0j(f5;:tDEEVjDFjg킓};޼^[~\ :q= k[ Uz$ʣlD-bzٽ.ӍCaPXC |-IS& NQʅhsDI+$@Up&A&>Q# FYqDeXA7SE<څ.X|R:ڹDngd-Dк7}Ya014kݪy4)h>VÆqO_92" 0tӀV"FB/`x6u9E`~ ޥM,t 3z̒{8W })7?/ C;4{5w7ూk5Z,˝K8LYz8իfI:=0N bq-?#aG}/]|]49Kev'p? 1 JZ͗k7" l"@w< g Mg!ɟrMj-k7KR[w5ݘ5vm0 Kì_љM,㱬 ]vҮ7 Q < `hH?B1*&--cZ&/?g*Y@._8zLA{`8lu}mM!؊ߗzx Y.YtvAaM|M xvv:&oDiQ<3>܏yy?;y4^}XzҵWo.WLGݑ/p~T@cqׅ6YGC[gde0e^⹳Y(8-"ٸB ;l-SѰN.. ]TJw5kKd4U.^cOC<+iAk!l:AsNMiE  &| ɴ=lNW2t l[{U9AyWN DzI6Tۉ"bJ!~q X9Nd* {.K"Yd6Al,5gk__Md1u3 uo,K6'h(s36 }89c(PE l`"Y rs龴jgؽJaɵ5g~|e6mzrr8[: 36νS?sI?>^ݖM&~Ɋ412&ϓp1Zhs\ഃw"`S[oIz_QQ>ւ>^ʺ8 pD^@CrD u@啭| <|?yBtzco,aCL &Grx|G> ![W3q#w[GNZ;A}ńw}Ů7%d5A)#X ߗ{cO\_?_pd:]&%Xuh [cًgrW9WمbkLdE^2mE,1pg2LW)k[؉[>IB  ĿSʪrΙ *IMSV¬(AId.2-Af[0 ZR H*|d7C@M{5T*V. C0TZx"ɕe?L PFG9;t^N @5*:3kljIE^gjߵ/ #N({%l?]vF)?C@0L)(*"n(8wGŴJ e2d{v*C:̀SjsQ^KlXU9.MC2w=Tuvtt>—>P~qBxTFPnӧNG]ݡ|\u{2 mG$ٳ/=ǂc=f8} 檿ʬ?(#S?+ M=`y#pEco}kw^QhT&(HYgIk4QZ )ĪlEf񑣷p䶥|!8x EQ\2J|oܫխ?Reᄂ LuvШUZZvF2~Gvfk(:^8MNʑ-Ŵ+5.2fh 8c:ۄRFIJd=Q}G*_-;: $N}狩c2<]Gpܻ #fQ 1";-_ ʿ9r2 [P2K 뎁"pmb|$X營#gc`KTY 9rFr7o% ~3Mƈt@qlWc]qp{{i,st9Btas՗ed/@?"|0Ǩ@Y#M舑o(^zm+h͎beAOl8tH`@VԦ!gLx+ѥYsK[!lXoNFnʳ~v"WXM/Z玜Mc+[^S9C&,ϰ*70kkpE0#Hm\ǼW ęLq-T7u-Y$#UqS?~.lv *Zmpu8#L4&?|㋳Q;n w YF+Mt9D[Ċ2 *̒8[F|8쿊$Z種2K_vYs$Ae3蟜v|W8}? 9ulY"̂%:*Lоze5$Zci8nI!* t6ipRV613VgjrԄI8jaDmm>gqҫ-i{I8^JUQa\lo$<8#YmM^&g46cS-R, p20=Wq1>ߣ |wsyZPn:ٜ@8ܽns+xSJc;jE'?X=H&ᡳϼT;PSE=g;=93m=IѦg3|=Yy]; )k"Z`oUez8wwuL%4ĩ̉jY\#gjJU8[ ^Ź ,>d=iBWOO`'^LkQ /g3dV }T:muGpX(? *tfk{v1?U٩,uLɐS#A= 5d/_2V@H+,4G]R٦HsWW7,pYB߮I\ԶpmAjussc7:WLXEޣb_|fbPCExK&BCkq +uB&c8[N@k%)_@D/h؇#9R]~gG{mR|dՠSƕy)Ӄ(a#X `~~G\:x%н:}LKKk ԩX+V ۤk3ﺢx/zqoG,O9Xѭ>&t3X""B|>%;8YE824]&s=g˂5o*y|%pfRTю> cF2p?qE,]yiض+W1>Q7/,.ڣ&|kԳGUMnS^ʻ=?TU֋Ɩq2ytyN4-׾FRIvR(!9ݵh&f$e~{6uæCf&)Y66Fl^i ^0.:mTJv8WIZ:ڛSO|bV`rBaA8_Yk T2ꬡ5fF 37)JPQ}`ooCˋө( (WkI"sֻy'ܖNńeшJ@j T ӝ.?GRzjm ?uQ;vtou#[G'ijn #SmNeY+uUNMV@уǛl/i[a[޻#+JDx(rwni+G.|ְ Z|`gHCF|e2\d#+40.2$%7놰Yĕȿw:<"*g؏{N(:ʂmoL)-Y:A[Z~n |0^.B\Ί'M_ ^FG//4Tt3f`heM#QƤJkpT ^dcTI= [X:-*/.:r Y{ܔ$4:3;[ "%;FctV2*q ~ X] tCl[]焐fUT=ؓm#>Z.J(=@.CG S'iRZ 4KM0b&{GN݄9s:uwS1LyHCV/75Ӗ ,|Թ fOk~+lͬCїRX>>:|σ6F7H6B5(`Xiy/:>-7k8EF]@da A<$TtM8u;?ZՙpXws5wOFvVvS4*QJ {  ,_xw| B əb"p LdY߅Ϥ=T M[ӉaҚY̨62MGkw0:tX3*+UN&;FPQ9{-6,taK4̩‚#M2'-Ҏ rvoudy5۰#xAi^RI*O40C4 2ǶFƢN a<>AfA<[:7ziRهI눶ҬAσðYa0@ua4W[ڛS 뵂[gp+6:ǺI$4'tv\ G1tyBuc/ﱡ=ouwĩ=Y:] 9o]^zص #/ wp 9eGKA%vXx/~ 2_I4(1lE֮#5i-;&QxVV2XVϕJYqY ]~l+,m= atѶBFU<VGq&t8 OXUuY^n}{n/}D`* K?=&9v.-P9Gǖ̽OX7)#RW7Ia .Rs :A:. Np!Hx6F7PcA8c CON.J סMH J3$KzbE$ ڭwOß~WLd)ـ> AZ,qFGWF)^=;{GRHU'R.oUfk[:#bgV*ڞt&oPf?>30\ G0(G- SW(3@}'K(k]%e1A;zWna/rҧ:ywKUlfF;Mӿ&./mFߞGW+ߢk>=83N0)[u/%`a!f,]uל pf^= `!R+L0c'` LGO1n ,6)`ԃ}#;r dkt& @D>(]3/f߼0<6$;Z;~AimqFϜz'u0*#i"VZSGY3 (/Pv[: ^OǶ˳wEHr_Aa/A}8AIu1Թ\I<)j W_HGs<; 6~]$L= 74<)'IuD|_\ s%zN&y\#z:oʳR P ^,]=uoe8RyY> +r-bd~vd_ڻ1 Lp&!' ?G.in|nEi`쮵yi@J:+ [q3<>i/]O?[t\B=xg9;K/Ĉ;>^"A:O[4:>ࡄq2:~~1d[w0*zm-6FqAO>nQuixۂN\Agޞѧ}-v%ב_LΛd^v`LkiŪyt0'V;H#s>zƀP\{]";Rސs$G|6o,M"ÃDokd9*Xxy]g~g0U| H]]/P!v Wv} *Ɨ^[[zz/?;kVa8NhɱK?JbFʑ?ii~ ~x{Jn X%߇ӟul"Iu"8ZT_Kol^V^ty*QZq W0.w>UN^vשю!6*QR򣍆Ъ'h aC:V Uaf[+[vd!}W} ƟU4qva?L`rhhS>O?q2nesy_]U@T/KV~粊N(o+S͠J?ORq*!s"mu z38uئxU98}o ]yzd,'f0Le%3eM#zGSQWR: 1peMqwU~Zo} 逯*>I;'/ɹԼZNg}+'\K*!0sG<*°5ߊ̠hasn!*Amt_$S9hqS9XJ0]U56-$h3@eS(fAFsF,8zuP wq< |]AV/#5 0$ }7.Nn5h0dsѡ1>&r6~^ 2JL#s?UNg:攟:|y+|ocd7ԇp<,Y6 8Qϼ62J6h:;{WYk /~g}7ZC nŠ.Xd5ksZ>{(pSϪbv:"5Fs j0=_7q=\3y{ט] N'eϭ\:b\E9+8ᰎq/qE#_N>95*4F}?Q,\0S XՉ\6K8$pw-%׹K4{q/sfzU^OvjD }sssqa:m*Q{QdjuEytՈFM8VWDH хSJp=zAӮ8:` B~cnw :</:G;"EoŅ1B#z v)rGE̲&ʲՑ` x<{@qC &.'vҧAm/:cښuF.A/unqY;ҳۗkq|yboӋZ`~FcI7Œo%FsHT qurSŔi7o*ܭƨhPPI[[ޒBp >qmHFᳰY8;Dy~ޥ{_, +*ػn`˩mK~fvɡ{|K=ɱ>W}Ps?}gkp KHO$G{^Q,lGc͍sUBtlre4ת@@j)|ch\8PSk0VtYD]A2}AzL5T#b޴ &[*L1S@K2sE,KٳQPJb 3>5bS>.8 ~y.39{y/jV#&H?rSζZʣLE@IDAT`8FOu5FW)|J]leUߥ+{>vGby{K5~t,/з6\~2?W.Jg e-su^h|5Y|)1$ȁf3D{Lޔ˪ 'ku ]QK8ȅv!jJa`lkR]nƔA:a `7*.3KU'*q3R ;#gKp{ JÍ|(}EF=A<&ag[EsHG*6ҍxu=s&?Dj耐3#I&l rK1F7Z+C} =]0{a#>uYM=JtYx Z7D@@3ܧޥ*ȭ(q ;kUQ~myRG{GzVBgnF¯~bA_]-G{ϧ=t=BYS@w{δ}[mzcҍ@q6w~p*ѸpWǯCp@UN?〧D}=̔C?O)0榟=s:vUxJn:v1We-Zӛtw7H0iß::T_A${Hw&ir:J4hC =h$T|*6XY]]SB_uԇ=g9+fWPSrXz:7 l$rvȘ&AƗg\%O[F$ t# xjVbkȠZ"SNDɑ!2p;d0 ҍAckw^/MGQi?8?!a&TqQQ|$G|eq*[u2Y15oSiw>F"t .́F(:G^`FMY諬I^ɖp"URV&x:-tu 4)$DyfTjKr?l)pCSe"G< UҠm q xFToAg$ǐ0gqG2UX2AHEN?H☵YmD|raH#95~Qx:6Qf,`t*]TF#w# ߹;_l)< MgbCB]&x./:t#6\g0lo9ф'm'_]Wqe\i`M=Xn2083G o5čvt㾫X&^qE[ 9Dd: A$T *.*v [.ywk9O)]8DC;SKc}Ts.?|(K478\Q2IF ^"# Z+&HLi% 31/ $ BҶŗ4'*e&ZIvKy5Nl |kS!+OOr^ybw>? "S"Q*?pG%{gЫ*-M-8盙}HMs,1.O4+C:ܕMh~d:pz[0WkF=yًk~gUMۓzG:6k8)̚(v:TmJee3v_<v"/y|;!gؓ:wtb L{et e{mg) %d_?aC8.$p d7uC_n`;yx ԅaX?kҨ⊟O%tt$O~$=G;"hٗAS =]@0yQI\g:m} X {32> ήj`OmB_'k{<[֫N`7L`@s.LJil7seYt_\õ!xϭ^&"~\4ΐ 1QІd n.;BM*ю5ACiq#T+ϣvO;on݃l+ 8_+^_bR<>x>U[vfgӽc HnAKM.Z"JMYϮ.'^M؆@vHHZƨt\^զE:+붧9dϫ=琥i;]+ Գ1i? t[^I@wq*!7m%9g_鐅Ovd +k!TQnU>^QMGC\}ϥiꨄfsF8'k4*&sQ( @Syl⏱۝4f$:D:ʣÃD\:+%U_(|=Fmo&iK}Δ6BU_zV0Yܷxt C擌,`H(|#$xc錛;NLj'#+c iB0a(gsz*:t8 )8pardب9LdBs3T7EkL!ԓ Fk KkꚙՊ1LAxn7c$| b)hUٶ6 ;©n"Y.% KCAe!M(m{rizw?DEhį#'P Yx?O{ f{ώх@ pa[sn1ÇoGq,hqi7پ+?2DN{r#|> ARɰOO3mߥ{lI=̱I_滏={>2FߖK_LğwQ}/IzGU~;(`ydxM/\T;Gw7eCl,F[yaq+Q/|wv&F;o-LMox(Vt:rwJWܑs wCOOg{Sߓce>o-/喲NYn{߈f>Ym04ew`MD|5gqTԄPaT}Ť5I蹜:n3\u빓&[ֵ93 R ,i./g[W9Z霶z*9~*:KT)ʕU9Gk&On2EGOH(Q^ZIWn.C7Xyy pD}OBIU9OIr"}[YDGWʶݥʻx84G+MqMAg(VJʭipެkĩ`$׾TPMPu*]>:z)g . i8`8o]<1Lֳ=4X6 qCS0*tX`VTӘ:t鴵UutVjY罭RY3MzYIT`sƚ2uS~s4F q+ [l]w2+a4䳼NnF@E|S⺵q- dypKdžϏ*1YGp> Vp,e_; de8x==0.e;;0=CP5{U|WV׻ kX\_hW`KҠ9_kgpWc?0\92m ufqƃAmqXlQ3^"7IdV\ G #yg!Á  : ʒ` Z7 EqS7K2^g&/PVDԌg /W3qn\wL\i} چ]q< q_篩ww3 Ws^eL8gu8O$|CC,gL Ζnml\xZ-,r9{&&IqX:>te~b`NxepBY#~M ݘt+[5=q \.\f6<|m0g+|tW9GrH߳{7- !0~!,VaW8AYP *d87hɼk< ~%er ᴌzTU0Ҫh f%VΈDS.V2p?{#0 *" :v˕9O`/[:t£t"CqWj#% 7ƹx@)-6HÆs׬=IP[vA6O$|aN2XeEyd{#@1Ѯh;v^b{OGh `G"eu` x sϮ!H)Y~zS&㠗:|֐'"!!|\ݸ{-OQg2tUoX sç0d/Uax \)Օc"P &pp[ѵ{_t̒Lh,H}#=>ڭ}}$+瞑ؑ[$ܱx v?#sgz o zq%~.].V"f#8nɵt{=lhl}j !뭂u&x_8+ ,R)aor~k,r.s$p3&^3]d W" V}a忭@UT'|v[.o>[n+V B.'ŮTK% \qdp[{c$h |ʑZ^STDL!44iWl3d" `ىyrz{Үv7t5sU)IӜ&/~T(rR]#5zItu-GA)I&\_glQYĞXߨ!Ka":EƼu}W ^C!nI3lKA7,hpMXFu'61I9kw7z8c6eg4z\NZ.o>Me&gF9D:s F}ǽ9LSUUJIys>s3PWEyې>}:bI~Ƀ%e2|ו,PKN@A}AQwy4mŌ7Jv&x_" c=NuAevvB|tY\# 甝q&S((Kԋ7Ky3?q`˸hd1,~ơ=V ~B':UU>L(X,3+v@o CʌLUfSXIiAv& dFm-͊r&:+5;?0 B,!mN>8N7ƖV~9CPeZ[]O(YuQ}BqPlF{N}1k[#;'A ?'"}S!,rN0[{3u"` K9=xm^m|1i2snDe3z=d&j֕<Ϝ-dho8H[qD?ģCqi0n0VCt[9SCI8i Vؘ1lfsV$ HN0v0Tpxyߗ^5a86jj5#gih3H _f:<33g {71?|G[-?fI;]s=b4 Gܷ0 T[~gmmT7 :0rsd%GL x(OP&2dg,\/ Nlo6bxOꝷNy ~=pV8Z ˌ6B&_? :t6ʵ̪[%Ȟ+qS 8I |ND\c7I{D nq? !`=+u>Lx |/';s^O=>N\+< s&=8I`oV([\4=e }&bIvT+Yjn|9F*Q:rrפx0RCǘAyiM!c5y҄:rQqv|Q [Nq)N: dkϣѾ ep6 avE!R& xdAۨz/sx/-pp#HĭXUc[T/:)6h׽˟كA9gG}hK<+r&:r* Ϣ*wtkji39ΣW&x q$$mO؂?K<cN4|("ghmՐ3 bNZwI*2=*!ӊ=?z;tH <1aOWgK&u4%G>+w(wmx277= V7KpY-d-(֘Mj*Vφ䟈ʡ%x+o_"hXC"6>#0t[i=iҰ:5O>l+XkP(LRϑ-uikgځ IS7DIb\=w1~}㩷RQKgOMg;/O]֦CZ^}7tX{?νm%qiMYL u ?8sݾ3 ZjL`SOӒOib__-eFλo֏_ tpE4@:aQƺ7}y6$qfS\GyB9L$rv-qˉ*wu t.{(γY^ B/`,S٘N=+C!Ks8RQ69~M m e/(y#_s4jΝ]dg}X21@uP2+–8KEd)A)AqPfm a? !G5xnR4hA)!2NkQ̲m F=VԶ3D et5m=p_"}m~w}LCoQB ZyOK/{} X+D}9 'fsgQ{"(v޷okNÀ6%r+:;?^xᇴٙu/|D(smV1}˟9UQdjki R!ɤ|T?}(/aG]+3Q& Fw BtE[e*X*X=A/|~'{pմ|C}=VYTk]|B85ҿHbv 0c4cΫI"h^\s8} I/*N^C Q 8=r_ }*նâOr}o!@@5H 9JEU%9/%UZ7a%$$U$DW583lg0wPBEB}a(0t8zǞxYҧζy xT8iO>:U|\o)T4 8Dϒ9f0&DϾ^ Ze(rc c9aʬE:6M,K 1䩄Ϫ.F5UJB 2B1pNk'$;$-u[\"N-HCVt0͗zժ>s +K6t#1&x4r* -mmN YD," i }=LdfFUnj0M<65DX {݄ɾFgљU@UGz%4}iA|x&U.I*K vڢ#AS_.Ȋ"=V#`WEJrAb\Gu?*K?ZE.LkEI&V۽pPgN-?4LfAm[~'=bQi RY\"`y{tk|ױ &hlh@v"oK*u"X4UA%:OE]AJ mu1\6ya8:KYZ\'AQn@ƭ- :UϪCxzdp ?eYzΣV@ 'Thѡ|hI qV QpTz{M|.Rx?5J,`M I ~B OĴ#Vl(g+1AЋ\oi0/ cWQZ!sc:HLz3Ƴt"0菙ޢBВNq$ loa) Me0tƲ8%pf ~mOGλ_ Xh[{0Q:p$t 4a(ZsU"~}0Vd\l3>zq1Vn΢7߸pw:qt0׉G\|_7* P:$ҁrxO#x+xr:0AN"6g VJ |4/nru*\1Ћ܊JŰ T)&X62S| PlNєeMdWȷX0# {>$Cw?_w>]f|IlK#x6[{0!Ym(SLgq%ҥtLF/g?KH'6{AnͣSEȵQ[R⼣:@sCtoKDcI߮_Yw0N"CGu!v8U+4zٯٙi :"3q;1ƌ9s)ˋf9t1A.VY$.g{`2q >I`Ftk0zT$u0,^ؕ>hN;UQ_E[\i~[BJ\tQy\Vs#H=HtOA)8^F>5OwUݨwй]!mJtNy1* eרrצs(K Dݤ }/p"\о/}W EE[PNkk;6 R_$!9H{kDZ OЮD6`/1ivNu4`L*-DƿD <68~u:+#H3`y l̝4>> oa;樑iJXy2>p+_e .DlNwr|Q\ z c&VSuRq^zajoHGSaabף\."v eU]AnUezZWLF$VܽClr^ErOmYjkN'P_Ϣ#Iv 6 YZ(rC+<$'9bT*M[yyt5 t2-m]yEZS-U˜ύ%br#m-l9`ǣ-a@$O+"-+k_aKMRɿG*iSyT|س9e:Gh)m^]  t둁U!o8c KD95¯iŔũ*aO{mWMȳېG 5ݢ#Je}1g/5j/GbfVo,΁O]rR3YmM0-:g f<zb0lBitDg:KB'Z@sctkyPQء@/M lR&)h4;z8ۦXWd`z_MU$2?s-iQQ Fq[A@#dATTQ!Pko|o[8$C)ICGiAj{"&;0pv* {ɔ,;iڵ)ʭv2h85P{CqWB@?CZI:V $Q1t*'M8M-EEA6Z/Ra+C?w\~JU*͑RKڕ/K\#EZձ:5~=,39y-azPu1ݎf摩|`s҈F]E өCXNJIsL:S"QeAqZ U|*R.Pʬr]"F0WBwz/A|=!(=>1t0Z-ߡccV lc`; TȢחi7<Z}(1Dn-1L pzF&1!cR[٧⾅\4m{-[w9Fr/sԻ2:GБ+[%SopB { sn"2+ gպYug) yU:̂9e?*a(}Ĉ7!55ƋNs:L1xnl8pQag' H4futsVuh{֖ #eaaG\!R>; sWWC,1wZבZafND&yY+!le+ s_Mߣ?bϫo܋pl`1uσZEGQ-=pPKW r|*>K{<.U0wiߦ-IR^cc1l&8@ň4itOܛhAkKxlm2 B#=fnaDq0)m$ghҦAWx}5 UE^]k]±s@D ( ^֖ !ςzS0Ay$xLo`]‘G>ey3 :|@X({A)r$6Ĵ:U;s do|KO~#su"O[&Ih{H$آgx%{慓sQas uoG贄g1?c)L>jlfc%>>+IO GNc 2'к:AAϰU obZc2b$_n^BBɍM9KIǮU<#qGli"K Cގ 1̡y#d%*5]{qT6p\ЉIx8\i>|F=JmXoI]ix!%}nϼ[ 6\>]v.ѵGͬ*<22j}oaƌj;NY:q U9?_ФT[O76lڍv1`77xK':1`i#D[,u6^r[왛$sBXO A匃N-c uܳ peAFL ݦgЋzc(vݍi+{4h|&˨^A7s)w@C$2g\Z?& =`r7j,c,J 63c{ԑ2$7i)z&t++6CL55H-ـ۟d ?dr l=m"v/TnKİh͍LDlG~C汉.XCh?GV;pS\ekBӑ44=4PT%YviJy|+3^~˞'ϱImWu=fU[XMQyk&I{inbtZW'ROvtI 6W.tBG2\NA6sEi~٣dG1&7+ r]CYTp0=Dll%ZOoqRu]cÆٲ 7\HQ/3h <]Nu s&lե]o)_ [_&W=b0+ӫH: KEbѽ`v~9m,)L}M7U3tU,kQE@*Ytt݂#QgМ<#:@3â#}[5%!^,ϖLރ?iBUO$mրF9/{l#(^nqLWθ<KO, $_L5}]GG!IUv0=fAS^j_'?3yO;7mcY^*/Wȧ]< {MZ'L-%UQgnb5x1{-Z*Rf˺w]}S[i |^"8GOy3ӡ䕖D5u4qSl@a&](rC7埨vP^/8qIܑ9U2+޾Kଗxv1ڶߝ^?S2&;;DƠnۀT0TA^!D+Z3Sh4"2AŠBCdateKsQSSVFp^*#hY~`IJM͠U'=[N%-HqNF.${$+?|3Ц6C|qlnstJCevs3ћu ;G輜uvmqp>eoZc:lfK!JUŋ0'O_:Z <\B p|}K:yc%0wEZ O߯qM[7Upfԉgø9W׹Vi ,FפQCIG3i8AC9я(+(WO4iqD8Wxsa}>=R%O{WӗIl%:smii I ` mp,TA3G2|51+8PP;p0򉶿(^;7jqfxkO_N+x@ 49EЛ4Im!Q3ץSi09fUZLgخ͠'+ ([k_D:VݐKMy |Yv$)/>)Tg/ Qc/c5 b`S^3Pce!!}J{דޗ] $4M">װ h2<ps߂ɍ|?&thԊb9[5sVuu5ur7l:F'{D0WMz FS\f _a4רJI5Ž#m81F=yci ':9aF̲4XE+u( S6M<1|y0 7٪]9sg.8 /&TTa F׆;:;V\*XY\EXC kWU.L T]TgZ5X8`eWyvS/ֺ̜ ]ӱq:u7`҅U+RZ 6vya"i̽ =w)WysRH:S 5:fR?0P&E~`~lVcl<>[:Jl.-r<;;[8<սt:iM 7mXnpV"8An>xN\Cq&Y$CG+&)ro1й \VڋvNV(PRE<\"P)~)mґ2Z}$%Z׿{a+0?Xulwҕ<î:7 {`~.Һߏ ϔĶ4_@9G<#,TYoʛ%"ռ2}}/ؼs0IϽ~A%{wsu dIPw\s WL3&291 \A࣬_ǵA\ 4$>+D#Y)?q(&h}+G;7n0*!\ۄ \=uf ܓ}ȟ6so㔯fiWo.哰v/25P~0{="Exx>qIx [ATڝ'ө3q۟uקNZh^"`jΐ @#-WW$s/e Bĺ?7BBS3٭Xs4%C>e"z{zy/[8iYf0H'ur'.3|Edy(L_d,n.u7^9΀?sl`r:fS_]ޠsor?ۿo3ܧr$n +iSZ,n38kN?tzuH:5TY%D(6fkM}3`%T9:p.{u<׬ƢDv*vS۽Z]jԘsEDdN95DI-iQo4@>"+2tF:OE4ӹMB~(qȤ(qD'>jy&{TEV=O\;ʝs0GAҦcyؚj耪n-/ĭ|ooL78Vѫz;O4NaCOu-/+oq*gۙv5Fxv<}D^d~=KI<`_s RA]ڦ$y{l v߮[$Y,3Ai[Mm ,s78~T+Ud5] -ö#-;08rWgS"ԑu,wiЫ9"89N^ܥEQ/Au X^~(֫$t|P>v5uR±bG--Ko]N1c#+~t u)Q)mL+MN=c#=M Աn4AiG K ű_*ײVg o3Ƀ_~6|0lnYW3zn>m%D{{7Kkn= EKtOz\?rD!ㅽh.ea7^-v7/L1"Sts,'m\P"m𩫻A ,!bW}ó,)+Ol(t+07 6rPN/.$~2)2Q4,lK[^<<:A@0Uxfp|W}E;.~gtl۾PR;/kP Ǿ_<(ƀ*I_Ku'?ʗ pvss{R%/}"X>K弮AtDkSaO_IO)6Q ?65gYÎiż:g* B rg06;Bx] lKFlf>2z"xZÊT&Θ]:~^z9]czh:1Vc^|u1i#k ƸgGktjO#-tH[ᡌp0C]^љxU@ ilt"mA/QaӚ*qҬ]qM-οq.}3^mSk8Ŵ%MΜ77m,s}U(ЇmlZhMVCT}\^`HɾIW$  fW.0ujj<)_`0ު(Q 6FPD?^e^VE $)m"{]C9 s8`f8L@X!^'cmu~d ѡQ3Vz0$6nm9ͤx{µ[q2e ͻuV]Va0y~i p.<\5}eo_MY,-[%aYƽrxp~|=UQv!h`Ye|)aC凉LҧȐ) |/?\ob?uԣSDIp"k;)_"'r6xK qX u95R`rRF\|^3 X8?rpC@G .aD=zX%y4 d)u"a}X<Ĵ(> nԕc y@+CM)!t"_x TT#w:T %uc\1l:MzsKU,'qL@W^Ѳny0p G>[iZ-atG9 ]yYdG;6r=^!ᮣW[¿4 e;?ȍq_Gt8q[cOIe-;gPͶ9839 '湎q\;C 8GNom.;@¯r}ިn nen#9N&|BOM;r ?s68U.!HW:9lH/fuRv|3[ǀ:2-(4[Gp9:׻<>uXfI$19!t ߑGw8tL^>x /lm &gg=ҝG S G).%t;No%z@%wqi,Ȑ",~qcIp:k[,x&зOI{H]C\k#^\??DF ~>K͎m/}<$p:c㫗6"q~&J|/V [Λn#}P{I1ա4XPR !Ҳl0lnTx/rܘ 3m&V|/%>{ ;.:W,{Υ<`_h c%'?ER3<ǫHU'd]ΫV@u>"ՎSJaMI[mb8R-E$?ջ |Ϲ#$odB$h!/{̴{R&Y+KgdHEeTe_OEqqEk5<ĘW Efui| s3Ξ6c.] ha]Kv{[izY"opv1:Eگcdn;kiv#NJ}kF'AE|$ڕmz܀z:#VP~~v63؎v@B^سueD^OGN74QхB,=[`BC0{m JYBjռg/-CkAk;D8$EF v.fR(+XNaҹ#18(UTo>6lj蹜OXD5p.m3,޶/D7pƑ<:3i8pDuG:YtCMKq[V rsπo:;ȟAQ''x2E8ǽyzq$i9xggpтŵww 6YD(;l-dx'ʕ!IJg6Ӆ$M/z!pCʇ檠)k7P9T }-zHtcԯ.\r8ٔԐ"8s*®wm݄W u${i;//[c-/FpD Ǽ=^Ya§ KTG3Zox&.A5:t8'/6tcg|Yep AVy. eVP5*u4dz =%] 8+b8cK F>GWPn$3C@; n:}%j?STuvxn~"uyWcz H`bO?g2 g\jc6o?G gy4-h@!yp|Ssg8o0zH8tN%<<x^Bᳲ駟LЇ#oh3;~أI}1,]=iV?gƙi:F,{}jͷJ+u;9=48t9?z2=~S:GPGIw.]kU8?~c=YTGc.^<*MNӧ3Quތ/}/i֟>, K}kw?>ǻN~~| K|Ɍ7T.}w˧> I^k@rPrH:җB٨."%:MJ2[~=ZOS&󨠴DZaj&Ξ0F1,\k"(! \gd=3@ǝr$? LQt*x*ck ,6Ҫ \z5p+| ,X luoCSMOV5Ha\6I3*f4Tq4Seͫ0ʤꀇx JgqL;V8M`9ȾDgM-^͠#_a6 v`v([ΞB|GKYo8!.ޔuz244^@HΤ9;'m}'t xo;øvB1ޚ訯LY+zPCb;WpuAqE^!Jn>O8Agy'ARtO\ݸ[z~1ួvq<;ѩd#N} W8=;g<;E}  wy31$ӗm]IH-uw3@A z3IWp^gqaY|`$]EfթP79[9&~\Z܂&0tCNxom{O}+Z*v:F9oY{NC{ѽ3N1g\C&p&7уM.F_}] sjs.Z>f𿔣}67CZ{^z&z4!鼊mVL@v߷jFg#w9g~mAл-d]f691!IŽDG,_gV`KCfV~n v* oqՒ1Cɴ+NϼlSSkwEXPS)wSu -&KuTx2s鍡tumZP%?v du6-Ryf:uMgE_ V'4ZE&_2E ZbW ;_פJh /I;$LL!c!lѫvӉ>j"~m}-؞YpZ/L˻. /JŴ|FZV6AEFө5q]sLK샅HKbvz"\?tHءꐇēRh~5]&SUisꫧ.Uyh!6Ic&GҒLDŐYByko'}`QmG"+&i3ZGlcGnC5ɿ2:#? >N %|7ng{umܫ~$7g&=>GTNb=h_;n@¿*Jp' b‹ϥfhu-u_an_3?&8f_ù}SzgȀmM?҃T+oCQͯ݊PoY"8:5}VW,P.:`?Bhm$"m3C%=4;>ދCFFDZGϨ3;٥k NPlJz ^5ԇtk|iYͬ&NV춹OO c[G˯tk;Xl Fz**hÅr:?>ۢZB=9ii3W=E*' ζ0&47qsA0;* \+cߥg̗9xjWYcLz2pcSҐV&x0U'X?r(9Fv?yΝYŶ4K{MGf <蹒$mXC!z~bLZ$:E܀y7iI k~5ΤVΏ3846:Jw դ =1keAakGRe^APT00iyGND+GE$j36utdW^8c=cy4 RB<e$"46VB:nWzeha:ӲrU' X^炰Gܿ [a?5|9L>%!iBu~G=!H\O2G/nnQEWqWM o݅}PrDT @bvUAp<3FH{H;O+`pJ[P ؑsƀgVF2: vv" <`)ߊ2;WuA/C3(A1.Ѱ,򬻻i ;toArq~)!OX,8MSf-Idu i m_>D2YRlO47J1SQZS1+kU˅ t#4xnyzNS&n(yQ|&$FaܜM?wӝwݕG'G1Kd?v2 O/cLn^!Q„m!OR^i< [rkǘZpdp3 rsz٧ӧ!O g*WOR}{4|_J\A$kTŅ{ j05I)QȿC^W9u{]{{;vAjhO৹=p[V{,Qo$` WÇ5xr+]t,0qòXʓxu .:;]ǐ[$z֑ԦI?d|t5/OCҌ,r=+Nw1G!W@ 73 "a'E;7)g&v{mj2کnbbpıT+OˎK.NH Y̯Ϧ#m=n{^lB0o$C:+ T`KC 9cU:;輔&aXnF}Q6oyL1\W?o|GRzPヷ9 & @6n$kCLs<''co,' _u 2NV! 7i%W2*fׯO1-5ZZ>POi t<֨$+a\ǍAgQm8TfHF:S`hhz Pr/ LymLx7>~W,|2^Gպ*8U(ǨwHwc' `M=E޷:EV]'V?>f|5I0@+]н=}R?m>°A2N݁筪;?|w(]{)2_awZUĄ}}y8 #G=uw+ǒM$t.|ơ}h5*v)sUTLzo ?%R>x>\KVю%h_b>}󭑼b%o&|UТʡ;NJFYnȼSe*3ChegIdZ(Z,nH8V2unl,E9 G+$ᣁ_:l]UrFҠc l @6dg+(lШEy6l駞o#agي?CUUm+*7D x8BOVyTwV2›ɬ$<r ;?r'4/ cJ:#KCuz 70ڄ0ϱk6PN7. l4Ԃ 20*iɈxx &&Q]↰6u6x7eПѠt8p/oQm9(+ rZ-4~#?ta;?`Jn{5A9kdT7RQ. ?: qWYuj+USy!pR !&4:F$Y:?22r™f IaK;"t$ ֭,~ K[8Nd뱪R|aJxu|ᒿIOy<8zpetu@MP/CS8\"W|W;aGrBFz H- 쭷G{%̡Kؿ ,g6yy:CMcDÀmΈ_iC(UVdxwyTQm}ChKO= ǑmTb64()$reE%& #{ jRL^8{fiyuW xOfzz[l3ɐcu&V m_F0[H2Nhpuui+Zۼ`a?jOJ#Js'Ͻ>xڢZx:0n^\_P6ɬe*:1I'9I}&[Q ?!hQnvR'#,o/0Q53}T;mh-)>a~H'8 0EC@2l5F^ͥ`tl!쨘f@8Y ߩ@W ,vgIF^DߪNؓE$D&AJRUߚfW6Wwͦc"-Oto"AEڴX#@YeNn3&%N5pȺQ R6P ~AmNJw}t9]!RXΑ% ų 9mrksXLN䶾y=rwTk\&D{2xt͇HLiR|x  G$ sJPW1$BLT@לKZ@7i &֠ULbt3b:oܣ9dT8bAtiT:&Bk݂iO!||lq33Dr`>8a.w3c$,jx:mPd2kң|N1YtY,dEX[XnGTy_†+t~)o 3&a8 I# s)++ vP(CjƠ社-@Q@;'`}_['_!H72kϔ!OMt OKǃﱫoPw:us(5E: 䠤86)ÀUNxF/0>)Dnl3慗^v0ffq8y:*ƚ>jCQ%9dNx?EϓO8n.1c ֹo(rv@ܻ}(#" yWdɪ8z&i\ڕ`"f G0U}-ZC{MT8jI:=S֓yAC剡$,PX?K(7 ~3utlPAO0>[(n^!!r:Hbi]x1 u9 l:w]uwu t[b D"A:UK Xmhh3djE$$CTtIgXqn*~*mV#HF/$c}ϋԡ䴲IL=ЧAD LW:+LvSNYWq #Ǫ(|d;*UVnh˧e~a- ^]aI$|:1㻶eILyD<ʰ"ֱ 2rLҜv^f7lUlɏeٵ$ #[EKE/NOĴw+`qFVXQcQ1f2" V^Ft$IU3iuqX!APsu<,!!`5v:lS<',xӡCP)+΅vWVzLpV*8HXZJXBDPQtԭ+4<=5\i?|?5u@Gpǖ+\wqyDv0yB\# ZSapPNj[Ka de Y.93TH$?xԿ r ^ꬱUi#C<#Kož74"2ϲÞps=?ɫ˘hk@`pdZR (I^48f";<`46Adl#!o#T?2CmƘ?~\ۊZz%T]سc.NGP&$^CŦ|8@x|]\"@V Mc:I=ӄ+}za 9옑#Luz|@z G^UTR1 goPy))מx#uCwѸrtcYZ?ww#H;uvwIPE}͆Ɣ!=a~Bg=n Ǵ50$ҹ뽑|$bT19N& Jsr: xĆtҜN#66UsU|nX3{~r?6i;0+/"#TyW>{Cv0 O)M!yVJ1fRqf0>?N3Sj?PSu=ur`.kANwvw7uzW|.^x}a.l`9Z/)+·bE W)fЗl9LXC糳SgĤnJp8nN~QEx>+( \bJE/ 1RG`L\y-4QV_ҋs#XcD/^yXcwOG)K>tqQ3L+<&TQE z6] #d8oQ:աs/c}l"ӿ-$3&6${1v156:^yyH?z`P0x[MK,Ǘ! =@~c|yjgv'?2|Xtb.0hKՒdk8fڙ[ΤOCgp}n<,!/~Lx>ƹ3ӑǂyUIPԩڤUtDgJv;ߘ>دn򛉻Y?_ؿL,&PW_.d(b(7Gp<O:m,GS~9JG&ba?Ek*%ąa8F .<@}M(׺B z#UZQu(4;p2B<> yצJe GRi6AFm(׿o+J<7 )V &gOxf#w٦Xsg@fhn:8oG9mCeV--֕9\kf/ʶg׮"Y^KeWC{nq5ϮA7n}bgtcWeիsϩ.g~#|T<ӹ^S\ /7m4ߝ)[@(mleJՅ;O`k\=7/_7RKMAL8[YipĬ ``R yi|]L]`۬bzd73kIo!CyYU ȑD919VثI*Q툞EK$B!;I 2: O{J#|nV8-Mıl&cZRc>k9G?[娅ߵ7ҽd+(c@)I+6*[ qt*5 C̏\0Gnݫ"^.Zm--m4偷LqAW0ќJPĥ^f&?ۤ\x!Sx| K }\V83;?10=5:Uix hLq8L"۶eTФcH` m?9:Ag Da-^y6' F"QgW;=5d DGAa Na᳼j¸4JI4dlΟ -9+Xg|]f Y)3}֣:&ttd~7xf gJfI #Xx65t2u_1\(1ضo/|?vlK/ ס%z-"iR t`Pww.^A y]g`F(:VAw.'mG9c-+lˬvItk-aAC%ֆ2pk4YdeFUp\gU_U! oВ]يKj[ghTrJ[% XA6V: "}:Ufyhp `뜥nu& lBb h̪WZ 'q]І*U+fB{@.*s0Ҙlzv>?ޣŝ{unQC[ ~70O+*)pqU鹵tzPwh'^E8w=;KxV.ڔ%LVlwk̂ev0[[:X`=yX >f]wxsʖ@ъ&::zqjUYs`&C]21 MHiep?;:@;fP[V;[ֽUu`u~|O^Gt\3B'.~s ,h~d'|*& p$K&Q0)cyv<\q~eЛO8҃&^|P.ԱokJVڭ7ߒx5*gHt/mWc_-puZ?y':<[  9sROww*:?g w-% [mgq)|$塗=δr Y&Q^ah w[tOTZOE_-pHU.n@>];CTR1Av7Wvw~[$vH:9ZhL/EL 4?я"cqL2I\("qI+FVVI ^#**A $[ν3|˄g>tDAgޓGCB^AwF?/_^={UHgHY(8 @ =I]},!`/qMN[)EWVeI¥9.?")MԔ:8kKG$h1wSG}WR1Ns^̦ C-[[ Go:qStrrtel#cX?=uNUvLS_$ו*W@߯vc$|ٳЖF9~J*O(܀mjZ6ҹԾu[to[W]Lx!@6OL*OP'_}ċܪ,i$Yiٲ;A,ϲ29KмG=u7 Cj~k&F͂i" ̀t- UcQA sF1BʹÖ'cA*]DogG*:x _; Xהv_^cJlR^"t}bhm-DD.[1 9Bc$Ѧ&s# {G TY|H;JҥγlDLvIG7`"!b}^_Ŷ%%CT`O KHpTk3\.SoLP@- >~e [K=MR}h LUAA=t#H?7>L j$ms#k8zH2V+DPS/v p ۟׼LW<Z|8Ih輐\=x$POw#7{셶J4I7aJ%72>i'pG}.a$؋+kJc1 ,𮦅ŇC!x uUHYRxeI=2":Di \'ɼ5pPۼŽ M<$|F%̗\zœr˄B(Yxt=OڤBvymJG !CΪ7c u-[@֏%Mveodƍ6GöҖ;ct@rrL9۵{eݑ_:ytz\<5͜;tT8xs-UKa>deY>2`jhEHrGְ c_~ !2tdķkqCF#cWzTYCkD\:~h:~L$BzϢ>;> ϤQMjݗP:Q}:ʋs{8Mg8x?Yõ>Fddڶ.*·ޞP$UmW}a7rG/nƿBfk9JnLjVS'80H~/]8>xJs&l߾-ǢoEAt+ ǔ\S<7U<;W%6Q hjj"19޹k7^e.pZsۺsR7&S栉nE\1KGJ T$br43@8[V:q9n0V}ige #r {?t_=?5x4ԧ3kpn{ⷉReeSm?kyʪ%<<\븗Cl rN` 7ôE#VaO@[o$;w4-?A`o@ k~?$%tbb݅$p:*FZhF ly]$:t^0OloxiU[$/y9UV t&<E?`b/uo0&Uю5sp[KH v~\넟DsL*#sTKxݲ>{Z:nM" (M3D RNp\ ~ )֩]zW[XAC;ڄ*;U0U[樐U{5) X7t^X0e14UY*t{ _`\RG/ ć77@\zVCou ?q Y{{;e|1ށ\3]'kd04@Z*sT#xNe=/W-J R\.D'uI>C:䧲NQ!z$+0X9K&d⋕W8y;v4Fgg`]ŝN]Sgdnmnv+Os~0ue[m$igCKcB)V-^wLJ0Ad_:r ai2_ik ӏ|Bə" ;X\ޠ.gٮD7d7;H7r8zɞ贬[ A|LGτ!٫Cd&':Ne ӥ >k@3:5Ad+t?4O >賎$τ̨4ſx|]*jn izy{3q>o.݈!ik7`c`s!L {v 4ouiM$/V|ŽoK}9.pa`eRfg._Z -).5~EП AWe6ז((13u{aX.ddޭ&E UDAA[* ,50< Y'!9y#k ೸$ZA+v*[ CX32ƳsQ a\Fe8Z`ngR!򨪯DQu^Mcg6Jy |h}3| L-CѦ$=O. ϼHtIAAbX]pf|YiVh" n-$'.Ձ*:_I]B`PQ[5Tߗ+h{n{1[l~GG3`,^*.⵩*OwKSVX ͡$?@D> wT 3IlW7YE9 2=V́_C ݠ#\V{+2ɃݛBSmL<3,LŗeyHEN1dϳɵ®y%tcW~")" LD>^t!?}M|@]#ʯ=F]ژO)HR$$', mQNcV,w9)VWBo#(vvCPD*S H]}HDW OaqmgY:COw_Yɻwٴҧ> Fׯ_/Ď-PI$JvN`yVX| K͍Jȉsa`&:'෢I* 53.`8R~7٭ e%UF;wyLtEf׎i}hJڟ2`8ub:덓q& }'MMCVqK=`A@i,}? ǟps-Χ@ڜoٲ5p@rЅ &2YfռN+O6o}<0宴sn ☇BF?v'PO@4СCQ5 s1tLH=`ٍ7ߚ:v( lAQ,CaFOZ[8|=nn!AJxҽ*}@!o#9Ud#lDӟ\#.mv4"qnq|Z=9Uy·N)_xU ]jL$TϹ. \ _6(NPH\u&.%^Hgqϐ0D []vrHl :t xPA6]#7k^cy5C{s[~Px{u{Y&mX9ev9|Kctǻ@>H-g ^N )1]wWR{{+r4"uOXsS@>a@Xvt t>C t.ˋ133Q-${+)#;B'{H c]ĕ@30ˋ^-ZTO7Pwh.xl%BzX{V֡G }u>zX9r$1B1 4nduQmfs3| sMR6kk 08i"C硞']~F:`U0Gܴƨ,'|>kb<'^ԓؚsK *v\%b{.G=Lvn嘃ɪx|^}${9L-&0 {P"OWR QQPҿ~W. ve05覝/zeI-/ioۨ$_S}?u)DϐoiIS%%qq*:>_ZVY У v9F3$إN'-R7]&x s':4 U3WjbHYyP:tx:07>YG;z-s==,&PI+m8yXs ~!n~ҋ7ЩV9eKj /y%[eV:NK<A>QoBV$s^fH[eW~ֱW6* GR ٱklބA8J&ii 6lm(i%$C݀3sUxFꆟ7_g[1w/l.~® =SgzTs`hopϡQa4 ]0Hig{gϝa%_='ɇjy[F%kkޱ}Kxx%䀎u;eNL},ϊ+Pꎎ%Nk|Z^a! &A%VZHROiKCr81X*#od|\ .+^//C{>rSBwЍAp@{u Gk & ga1h:=} XoL3$2Hg9cbI08VpS \o{Q*5l pi_8ī'sNtt9Bךة ?TPCV ?{'P‹/<˚q=C8/Mi{)>Nж 3I)ء{p|Vw$`5цT s?VmJܓw&M2Z[u~fRbuO8dT]xo9_t0`c`q ,^ďZ*v}GyhwVd3nDYSg $`FTܦxPr63*?a6xT*E$PuVy`k" MQ(j/*Ex:3Ԡ{ (4f! S/׹PykBv Hy<%tsZм-K9 C>F?=uUtJkğ6 d&4 Gllވ,&a/7uPi#tpA|1ANַg#6Ͽ=R̤# &QJV83&!(Sxz4:ZU ua&Tu@E;iWUk+6`k7k*,8KvD6fgADc 8I@3;o8+E|-鞘)Kr'B+#UFznTyI&:+mUvW! kDUP"[S WZ;6r\ĺʋIlv[7Cunjs U_rgU6FGFhX%=}=#RǓA2UA/|)}_O/:>}e3>`W>fqz8,g@!S 8uJ8zoK_,>h]MǎMwƫc}=s}UB0Jg~,m^?v8`t[W{9:U>aݶm'pH`$ʳu}Ͻlw wG~㟑p3L6*0Z`P OMF% I*8J1H Ʋ __%2\`m!c<݊\k>8FtE<049lKљa6:F  :&gL;\0P3g 1g{ĭПէ: f]2Yy,~Kl:4\)0R8\]/<˥ГNdprpʢpx dP5.LR^pYQ&K|^X{FأF52ճ}k:t0Qc4tz6τu`lޱA LޮBHkf'I +,ˋ &e>`>E.s ЩaNqBFظ>[| xYQl[+}i#ΗQ:NMg Yv+ +}LTQ >̾G&ѱ'ұLs$._ ? | #ج`m@ܦk }r@D %*mm&ul-8 ȉk= sLk&^<tdB8b+}8ց'S5E)uzzIΞ f@mV:TO*v 02@޴ms3b~:J P"Bg?#A`HV{ЊAj1*23QqN*+üͫJ4+s.]JHt*AQztw҂gҹ3g-:o3Z'ì3uBU֙hkf{v-YA1^JEwkwN zB@CZ4VDr!.:Ģ„X#tp[5^KL'g `cwoz0}}敎iiԟp~ܤm7|J>l+HSۺbNcel:¤GwYz-vpTu._5V+g)2 Q-i ֊έʷdVUwuv$e/ ?("ID=_2%lޮSϕV&I<_ngM3gP`K9B`nл<%HjMrF 2M,2"8z1D+ .gr,Xp:S'k.bmj@mo6KZŞ W#91:?7H%TTԧi9,65l8 198&#32W4|dkĞA5`O7Fv% SusG"vΠHfCT~|]mܣᦜ.OҚ:7DвmcEjڜsϛ <=^=r9v''7<@9G2kM$Us3 t L{"v{j'`痊cMX+}TeOӦcS~nN:RA3oڏUOѴo?G~qv:+s؊N5yX&‡&>^hʢusqz9~5F9Z/,єG}  ڧkҎ6fl4NPhvMX.0^pSYNNc =\>EGo)}O(\'=1~>c6|Ϥly2D8@2|CV2n[|my*§kґ eTf&QT(0BgYл%/&dy~!u$IW Hz frE<W i+CFq)Gf*NE&6F2b SXncM6TŽhaJ@؜+2qLHnBs*g&p@f۩0 `?"UQRE gpxd YA:Qr[M;]L~Nïs{8Ҙ3w g`>B 2pFbEMG+>|:t8?ʗӃ~;G(!>H P?Mkڝ,1G>WC0 {=s- t~@|_A4nN:o1=G~Gݻ@1~ȸ][jz߽TZjHH__, ";Ph}\?G;-{{w05!x|҃}/a)܄*B;ޓ?Ln?{W6 k (y뭷wODrs{s=jX+\~! hGaimϞ} ;cbιsBYn$aklCеEi;4p&4T]v5 3gfIJoLG\: pWKڝ F285X^}S@%12rݮX~MMdXOPyׯ?rs-qE]u ,^*Ug8mW!Qb`bHekKpZVtO35!(*u鴰JȀjbf PC];(:z25[_}57e!tLjHjjYM#B׸9᡼C7Ңua),T^2̈dj)[c1 ]Jy{+gOryܫAs{5=: K+k#4\ %+lѪQ¯B=crcDVJS:/-V0{T(pgaS0uiQ.א|y}V+-%Ηm{(nxxh5Rɥ/ee?loXe{A6@O`KA#={F-8I&AbE+:[..PyHeWҡ|RLaZK<$milwtTunNഴcc|o?pEӽ=S7~z7ɞ]`Ñ'%EY{CG"8'mD=H.]Sܚlp2J.H߄vcI=~<1-*y!\u'8x <'8+~٧<Wx7JniH&>G^\LY\hj(c Jh{[=yxKl[%_>""Q}b|2_GϔftlҩI03}U:+ $آfXiz^C~9~,VY#( t,SAw\ױLb}&pq htsj5`Pm`NPlV$eW<v7Ҧir%8q %$M۰( ?˚t.QHT٠l@2`>7_ G1Y(&/.O4(@◼kdx)'RZA 0Xn{ Ug! @G b bne(Nx7| Fpw[Лĝ !jk瞧#tfc'VIe)<]}vC=E]|RW1H_oʹ/SA}-PoX#(* t 3 -0;s0(CZX^Y \թ _ćoєd'iΠW>oTw}rwȢM֍ `7:eR!fWC=uLā-۶L6Lhih26('quҘIS2 k7P`;m.?bcS#Iv`ϣ wV=_N߷B[$Z@{5saD-B9RL&IiL]FV,wYkOy WԣE]]WZ쾩g akW:s/sV7zk{z&Iyig^Sv Xj5p 與Rz5see&P-I7l0dx.7Ϣ' @&Յ>70EqP=$A~a@_^c>h"qSouy>B;;DyE5U܃:}]8oKK WϘ-_5+!nus}d][\_]}L*4Dw NJ% a,!VOdn{h|& hRȺV|_"yA[C\[[%?;/Nr, S$9y܈I&2!tdA}f #G d+Dz#ظʇgϕ!Z;@,'9j71un$G<>s$DʧJw=ڑNҙm3;GmyF1s9RL71t5Lc7r>(G֚dGY:y`c|Df/vGb/fH.`hCV}]4g]M!ws"!utoҶ\*2*r8"8NUPmcg\4nmeMr sig!oy x9VBm o_KL7<]Y$< 8i{6!(sˤ2^RFh3A&=@W0g/dځg zo/1Y|.>??CNqy5|VUaa?"!{g+)gˑ&+#2K{%N};7UmUNGR`[F2y'S|GG6UD]+ܒ en"YB%,4Sp`s Z T _QҠ̭#eK2S`GGGf*M=J-!>cs-$^~ q+e^{_bbL!t13Ǖ2sd64f57Vɯ_ 0?oEH%4^&#[1Q/3[8qTzCm9Dsak֨|˖?cʬܙS8(7œM:G?$ 466+WƷ{hsy gmgW)1MmQzsWSlBX>ƛvygBq޾톨(B覛n;]f'~o-WݹM!KqĽ€BQ5fg-EsbW_ػ.;gqՓV7`,)mҩ#;# kLQ2nVw~>PBy:%[94fz@ Sidx0{D۶J?Jdj߶;EK]tXTٔ^: M۝ۙ=&{r^, z圆z I ԤёPMpG G0x`Uz:2=t~kex::T{:04tقqte'ar ΅2:mhy=z-^_8%fCϧ3x(TQ(BCv:= F$r7*dL A)l:#`^:lvO`x c)]̙9HbH5t1_Sey!3бΠƖX s]&-px-z o:tzLȨQdU5'vYc/L@7xΑtGC/30< >>›po GH@h8׮=Ec6"a;v*rۅ:^2E|fr{cINn9zg@~f:7chHzidw >/ q$mO>%pFnf?:m;x#^]A|7p6Jc}{>po$ZzE{cy WZZ_n8mBo TwO)0[X^sr6ƞyn4!CGg)" 5* dϏa8 O-%YHtgu`s\O>(^J:WKy$˫kYKZ8jԤB 32IC謴ǵӎuH /uddhgidgN$=3AN_ˏtʘ$|_v^3\!_WҘ]溚vk cſ7Q|gWo!zZGG6ww`|w.[&d רb 2s|ʯku?p/s]_ϼkn| A*8ST z8M;A $`0ǐbZ,%)h$8/s~|I_&YM(t>Ь2-Cx9;: @⠶Jv0m^&Ԣ6I^:}Ot 忎]yl*y @^\Tשp̬R1ؤFNtbQ^[wf H{og# E!Tpk3 b$QaSyNJMkeB<o)NAn fڭ4*YzJ>1X/n{,P|Gu[W,uNļ|fG]Lk^XɊ f#v:V)˥3[@WH%aApS:[$*+7A]3Gz|_fbגʹמR_d, ZTxfxad x64s0oynq!gX]AY8cd>T9Bz65mED_ T gd&Gؤ+8vct'Hv|Х$j#ֆ$G@Vj.j. ҳz4{a{mUЍA: q/aԅ]OK]/\jtSZˬŵA-W%$ItauF1M7%ػJ>]H'ҾыtHç:IȦ|l ?b0$xEK&{_J-YFz۴5zqrN4O`:*'>0pg{=/bol[D9pehNٞz'ҩ#$̣TMWTpGU=Csh|'eQ.S>=ٟ6?iԁc2|`A<q &_Lf4M-4e;(_>̂0qv TD6n*:uSu$v@*#2'Ӕ‘mM_@6A+-鹄AX*[']$U џoz]TQ]G s{[i4 +GGV%mjD2c;Gy`g! {}ӕ?}Qل>Ӫ楱\̚)Vpj+`+ hӊ$Љ5hYta` XƶUK5hN|̡41~8'i(/Omb:@?>3<]8N|# Gd֫asc}aE\UwE8^fv4C}V{QG3q9wp k\|Y4Ō&xH"ILMMM|y1>MSd3o48A\:z]o?giOݷ?⍎sgOz1 !<̱ラ41Aѽ/55="C. *2:*h@J13(7gA'n۞.?@e镗±4^-i'&J?}{鱇3 īR)ɣx~ykV5O`I j҃.B}){5ZWq+z ^ Ɩ^M!jkrWl=DrI*>4c`З^ɋcQ>jmm˶|6G^}%?_c MTQ=%`KkT@@'<"ekq]5w(U * apJA5X9s4 l铲-6=ÇLLݠ ?hX2u, ϶:7tFҮZ7>s:msk *~|_w-t@IDATU^b]~^HE-" qyky(U EV&QWH;c B ϡIf;ɐtё}Vl]=qq)';C#M"6EgDHD' u{0୺ luYqo.L9ҳcFGq8WnAZQҤRv!p e/Pr/|޳eLɏ)ɘh#m%:Jǐ/?uP}gceetE9-&dHo?Bk>uR^\ugzIA4QYDgƹI烃!C՗lp"gQBWP_}sT? }9Gp<*sAc$rcxa~$40~Wemsݚy9>CGgǒ~K^(߱:?D8NDA"G 5x#<||*IҹLWgSWRSY/3m;#nDK}d3Μ:Ns^}u~.u[d>M8~ ?d>Ê>>#5ܵ|̏, |sSA(vz6֣$x9<9g}׋[v*W-SQKO"13f/F9^%A_.u,nnp-^ Fɩ~=(8-cI=9w|FZ.> w(BVdg8/SwX@AFVR"?(gy0Ǫ_'y|#Qׇk3$|CNzտ?;py dzٽM+" isc ζBeZJF3ag׮on(a|===i&m@G:Rߌ }vo~%ح V xsdFtuʷ#0C1Cc*c=@/VZ@t}+# <3h)L lpP_;4OgO3Y 9h*d3IٳK 4F~GdCM&ۑݔ=r|#}eiqWҗ۵gMPR\L* ț4lHW,_0z֘0Sf l(OGD/ɒՃ}ᕅ+V(7w (`cj[ZVڋL>ƔҘנڥa)x͑p0;?A8m[_ WLTv+cl2]; &qc>+)%slCNбЌ=̉/PD PfI*tTdhN^n^d1 [6mq%Sa*S ˗.".`ϏZ)*/qCAzr|&γ8\* =?ع'TVd唡7G?E @hxW*gP]<[7[2e*])Rko'0~sϤHD <ڵkw%}or-AwU`vK?Rjz҇Rm'17o F{5 42όjtbS(>K8a7-{D0yh,ZQ[t#Stޚϗ8sp6[J@jMY[}uy ,pL9^{(m@wǏ/?xo.e$}I:tk<9VRٚe 3 Y~>-h( B z0ОNeԿF Z|W!Ns/Z-*GFXCW(.uaFҏ{#lx:J 96eYbN:.V7[~AgYWm53W!lU' 7]1xۀky>q:Olԣ!9(fG9 ?crHy=3;auB{KlOV%Ѫb2g/[F$uצ&IŪCo5wYvpÑ{:t"ͺ?|A'~8}xq{쑇"1ڎH>qow/~tt(0_yOt0ck`!zyg2Z_}۲Lxiq_i|"$ ?#PT*܄Ƶ|QjurY] !dt:2~ܹpE;0ٗofI əNlɶlٖdْcI"邢Z4(?@hPhSIڤI,cǒ(k_IqIq}߷>{ٴƖ+ g{==~;7wHϺw!S@JFxk̛p dLafkE+Y:-%2sܳbCǹf;!:Pq`0O۶eSNcy-/lde‘DGXǡz0t0& {~L|=wgғsˏKJ{N/ 砖mo4Ƞʼu D_^)C}] tcZ a)ؠTOio]# {_y k ֬ ҥi?B7*E+Ӿ'>G0OHk r,S%h.CBH n>1\tVWcsRZ/S)W=cu*+dwsƷձőHO ބoE:PpR{o^$ ,;1D[[+dK\]HR]q|gZ+3/_KM`i V(WD&C^K: 2ƿ;G l m%nk3߹@1rD8z>q`/ aQyI"҈[^ ?1{cB8פQFM(@1pޢNv& X3"c{.2 ?wsC;/ 'l!׭}!ҔAM{X3]ԽBܽ; pH7vP^*ݻ£=|@C$~ Gn/d&**i֭<1oњt{=/yzhŪW817"6-64E"#;yۡL\F!|& e\(W$:*9ɪ7~VY) 3=3}K$lsi2W2{С{Kp4; MMy5IvٴW؁XW35QH`F { ]i%6MKØdه" TRg_/%.֒W4H9nj}EFz6SRzQm+ b} 8]@/|XIƒ<#h߭T_zGASw+-Hvsst[A yI :_jk&v_/#0Q%V9gڵJ{$? 8F#_X'7&< z۪T+KX|$~䬾@2۟gSI1umQ"#-Uf%B W1 U] ^(lZ2"ULBGsc0 lxzC@JpV GPJ'd'$2{zpp_**]kEgo·1#/S2L>BPG[90}}sw.P38u,v>XTm~gt?K~ 1(o})=paT\/sDb|`?KO|mοͯ:YfT>s[ilʶȊ|gC ѳۿ!H2/^w GwΥ6#qys_ ښct*Γ(ө_zOSv9?8Zrʋt[𣏡6$NPΪctĵ;Otvޝtj6mck1QoFI ]XAypfjli$ Ӊ/.Zhuq`:tOJ%/r zpzPg=Po~3x8a`:KqyxQ{Ǐs|wK ݟ]SV^%gw`(n\l{muyʵpF VHD N஭笚=ITZU4=WH(&+vm' {FQCUpt**:o+ .@v⭌diem} bF:$AL42(JdoA6χnhf3IϞ9U% 0S$!S3FUC<ܕt&jh g PL#TjԗVO= j1Í L8l j̮s[`ٕK1p>w$*Pm֠`A:1Kv7\3H!YL,c&0Sjxa6i%2ot#c`At4CkoC91sg@4~5aS Vgh~fob]Hׯ\$C^:(!ZTrxqh+P:YV Ƹ40iu gj7alyLU#pцt'S{u|3 %[½9E _ل5̺ۜ=T@n+Ox4o&/~7бF;:6- }gb|XA-#FXbNti{o2G"eY7x(RƼk%-?c= G1Vqa|#JLE䍶Q? >HU:t\/kg.?xEn~H`V=1Zfw`'=,j*2q.<&g<)oz]]ǀI X?Mmhu7qv. gڤ&ݠer}$ Cwo/P9MY߃]z]~;+7uX%>) KsFO> l}F t +r[ŕV!)q67vt#2sw=6?#n?@߁++RQOR'1Æ[h2goZ9)|:)`t|F\JK }51`.<tNkhB4`a|Yq[~>Iʎ<.|w>z-ymCO+Atqf#W#2rO7n`d\W(!MQ.w49t} +[x#m)IA68'qQ9?jY~Omnr"H͘;rey7~6v)ՓL)uuq_š b톩 28W 6p'iXE?GubO̱\-,/,Nx9rRqM& _mW~g`_pl Ah=:D N~XUVar6upXe2yɓ{Y>lrxYv:P2`ʜpu q ɛq*DC70+]${د l+|.>e^L0ߌ\ٗC= {om ,b›v8U v@u4.v ʙW._NpM¬"0Ğ,oYv7[K҃:fˤ0 R+M{ 'ISO]JF d"5dPܟ+Cˋ|^$oOY#6^D"Fm +Q=iS[>^zzmI0;2O.*gװ9:'h ~^S]@)tQ޵m$! WTXen376qwh @`#mm{ McԛW#_9ND1vR\Ga.Z,KA,^# ^۵9B0IvHcz ʣgLq\}K|uOߥѮ>cOSމ+|{ vޫ}gRv96z*<׮yet[G7[Rmf>3cbf3҂RP N |7&HүCGm{wyl#LctA{8^kh$ٮD铯ZdĔ>VveW0 ܆^Df:s)3}By W! %0))"_ޗWKV& T_'`(2yYZp&*]]=}q.0dlˬb/Eo 83d~ -c+>\+wy1>v3 Z M>APTZ'8C|.#S=^JK_r:BM[Dϝ;~~+hu*ܛ0 _~%$$ȥ"7~p"7Ns=mao v``%ɓ8 >ztPƬ=vtױ@۝Ea.>t0`bO=EDF8Ѿ3#CNwݓoz>ioc#>gK^L֓ғg"Ѥb~1J/u<=98lk|W`S8xTJQaS++?FaZ]Qs)ﶥQ}~(g*tڸ:):5TS}y0'kw{3(VTVq:YꋜI;+}TGosCZЂz c9}ֱVS9xQKO)gPejb0p\> (0J@!:')/a,aV6{ldz ]dT7]g5ϸy5̌6 n$AK^ikkQ{3j0:#%1aG%e#﵎"'FgݿU%1& im,L?lX HҀt]ÂiahDZƺ]PVYkTvZ7N\3g roMH'LnagݶƾVֶ3TFǰ]V{qc4^; i?Gkh#̩'fkPVN$2 r}ʺr0gb@ :{Ƀ~ =:-XG6{@,Rϩ5d5L4_7A8:S;/y }^d}bo5;[qR{wO@ }| Ju Tn[ש]zGL&5D9) ̊@hϱ&?W).Ύ4"wB~U쑕>`@??=7Yyʓuh`W~iܻ f|a'E+C+} Z/+`_Xo0?^<'hHlո :Q:'3锪Ş%Pysz0u^NC2 N>cB~V[!߫$DsqIsEoVl}=8tH[t۾{MlʺK;9~GyؠAgfj5K"sXȃ^&0VMgv vQn[O[ф5ەzLn"u'_4ɮ/QyDzTFޞNơ22р{DV=?<7QCZ =) 0 ?`[( 'KЎ^8YcHI>]y"{cqD${Nlגɺs&dX >0= wC'4[K(k P@hI']WbSw)\O]]D.Nc Q2wub}2{1)ӓt`ϬPV}WK hA&K ћ~7~=Hs'y&2" (gvȦoq*^"sp~Y{<ysC>G ܔ'K2BQ ,r, eYνʆн?Kʈ ZW).AL@R3an翞][VL!D2"XZŭ-~V>& { [e *  , ԋB ✾i[[+hD` si8frYjH^D{SQ}$ɱǍ7N{ȣ<*3RYt_ZI1VM;/ Kwetg2/ŵkK&yAQyGC|4􊫗/WHlEV^B/iNׯ]EOZM} VުeYKOS3MqC$1?iHzY&τeWI tۖW\XC#\f=5M;,b9Uڻ/"~VKxt#Yltv\!xk(=Pz䑔zve'-=wس1T Ѽc|f aciw7 Mn,.a_V질,DwΟ\-ػt%uoL⯝:]9|A:Lj؋.w6RSf\`0IG*T455G*ŧSϭ & c/Jլo6: nyJJ'>juzgө8| }&Bo߾Q;9z_'(-'`)p]wu7>kO}E>!\>'S]ƹN!w%_Uza0+lAؙ)xAo*Q&u \ydݑYI@٢ǟ{&_7PY&]a7{1c=BRxz㜷Ƞv(2f3y"=?!j{΢ldQG lC:;'T8ѽV\gR*/}؛Yj z+uU)_>,~T:OǗ|V֡ v:<qNmXko~EQsr)nytdtlR(M|'b}:~1Wa{͜W:Bɺ)%/ED*t21y30&?bz*-.]8*5pv/1iHHYmph+ejMJVIX>D=28Scv˶c6}E0Zr  yy{ ՚յxh+Je]ƾQW@: Xm <6bϷw>hKCқ*N jN+lnNc!I#a$rdzh/0:vxwN槣`ccH\ލ ":*]d2y]dey]sglX lCr2M{3ߩg avmB*&\8yxA9w/ ! ‘(RO/>tBH13 qtHr .uF@"AՁpt4ju6f wWD+g|ݹo0y*/[{o|>,g6U h^Ҋ$k +*:0]7 +sy V m#1}H s.;j8N DI̽${D%-*kKIpA';Q ˯ j[WlOuK 7gØ:!7׹:9:F]I:rY YǼt\Wg:jǼ{P+d>p,u0Oy-2ؾ}72Ё-L1ho"|11UUP5g)jcuR瞗QG 8)WݛO} 6MxPcQEfYzlAx_8QwV(դMvoIRǖ^cᄐ[U??JLu/r|  TЀ=lb"p+KV) t0o C;c#$+]fy0\^/gR~iD9jNLpI*,2RՀԈgU{ys{c]|,/K*0H ]X', _Md f˙wwOg"VaPSMx.Bs T-[: QU1S}^ĺɹY>2y.}Ron$ .RfZG(Om[[.Qg s9ys]&INYZV`|9ѧ Bʯ8AY>~ ?eV\ yQVQ+g z1XTב\90o?ҹ7FfetkRU:>U$`PA12rSڵ_`mAy+kQɄ#?G`P_*2s?'Y(*}/SRx7yxs}V:VGCLhϊkqC{zG?V~"ĩ7h2ݬVTeOq]$ʏ?~"ɽw.?[gIֲY͋>鱩@Tw{>2s&cxpjN& ޞ]wCڶGƀe9x:GII&o-kAdᑾ^HЗGK4#Lhbف[Gn*cIۡÕP׶gsJ6{l >˷u}ꬑ<΂u6B>m1޽iws̅~dŸ]Ӎح&ha򖶚Etr.@IDATյ i`=[܎ >i>d>W@bAHz18'm߳M ssī8ijl&3V]Tݰz%y*C4?I8ho:7p^&l9DlJtM]ldkSKd9P nb37!L,\_N夈;p>x2uT"I&T8"la@}=GȡC<_LS+Iօ9ή=8drYJr}] v:3y~nŇ~fٖW03(闘4Ogൡ`FeBˀ{mI`(v3ZK}}TzfG;7~N_Zg1gݬ-ZfڅV<@v@GF8s0d {w 1g~0>1`b4#v: .G0mkQ{/ߌ R <@SJ:`8yHHh댿t-۞=> ;̟x<>૿֑>/2FK|PC{τRfK-Cm ^8>ϧ=1Մ?[M2YBcqs,UR!(H?; JH⇎Bu4b`wvv` y}a@3uP9gs]?Q>+Fҟgh4 Py/yP?YV kՑ0WH2諂̊WncMzdpod\! MTO!vZ/5ZO, : f*o{t 4DnMѕ\:_XI3Zc!FN9J  O- c̪ҀXpTި,x Ku5'23*:㎶_%ܳ9 O}yP:[ ( t ??xp8 e Ff±P&`cJ\-֪ r1l 2 w Qs:gµUXo6x9 ";g5-R64s"L ka;iT{9 r 1_f͘dkKTRalt]gAh{l cVp -:Ê_l}eFϚJv+*tRZjfddI>D=rKZ&ƭ^ou5q>;58۷T`{ ɟg[/+L <gpa"{" m{hy%&xy3OY!jX*֭ẓna>ja>GMҼN>2om-~2zs=%yR!dGp` >#ză-7R\Vb;o/ 욤V :k~G{A-Vϒ恗<ٱ-;lLu,kPtYg?V}|0+.(g|VWP01dO b-טJ&Óۘӱc]dl<,Ѕd \A;whL q΁p dÄ}^HTEtbU:[DlI>+t(\χ;t*O\,1\خ&8JbO2<(c/|gfJktN@@5Lt\=^gUR;%gkHc[ybRK sd%3ܘv'$"ǭpZđFp0T>6#ؑF|Lbzz"g Č*WJ 8:,]!#oЩYkPGb;8ccR.d.>$ߢcgN<V< DZb]aG3'8>s._ IX+dh+]i:t}c#GOhޤK.q--?>V~(LVy~!ORIO&6]Rމ3ʾ͡^x|% {9A iwm}=݃q:LlpVLLNAhws~88M#*Ѝ !-ҭ޼ձ 7/ p0V~<慾#uF+;vr.]յFe7HDnz)xܪU\\"P{g# b_XR,pϕFreқ2GTh֠p]<*pR+'Xe֪#؛*\@*#%uע s.d0匼MLQq{Gؒ_; `z 4P[H8Qi MY@>UY=\]F9/Keb6u4Eiϥa*w5E ܗYNU %85ۻgwG iq4!ְS;4 n'KW&( pGUvv26w졲fG'@!%oP7ab&^^<y_KAhPe Cqr&ʃ%)eү,# IfJU\kHM')9;;vUVmH N![M7W樷"'|@PAy0s]ͥIY|ʊrd庶iELh,~6},$tmd5j=Ռn1&c TYIhu{sx+Unn2?A-8~`pg)1T:*tr4~ytτ4AyJk(ieĥn# Αm0YCKEA#l7.׮]M/M CPA]|h)o*NV&B{/Z/ 4s!՝zc:xe:+T N- ?2\CYdDV߳wo$;>W)Þk \_|W#,8Q^&y3\ f'fvYN+{34rGvpsOt>|, 65mH|s@mmmZEфIPTWpow~x?B@R^`pvbpNK hsJ3i0_Y<껜%ýG{9>N~2;4`J`/EFPaܢqy{\)ө!K@%Yd Ƙ4lA݃C#$)xxdqRӮj:ڌW/s!:Dޘ`7Wi8~ <3*ǷoqULwꈫ&xRJe(R٨: jtu,FĚ`ܤ 癣ZI/\6A%Y:I:#߱wo/ݍN}6:{6ڿiwoo$nw[\;/**oe8mTg|_6΅ .?rvXmXtZo_q lGz%V\oEz4ͳ^aiF+FS.3ЏM2}9  ._*c8% ]]oݿG|W_C &u*pA*w&;tuJKi}Wr$CgU.{r 1A Pn}9anX/JXՇwq_>gR~V;0xnGZ$gOury@`oW:Mٱd4:3Ab2/L<I&ӗED:r=lGx;d.c* qpN'赏cQmep]x)w*8{mb Vq:40zYMN2.g_vQ\MKg|lzV Αgz.I?';07і 4iRǠ[ER=+E,2~  ʔdžܠ9>4Itƻ;h+kgл#tggv۝B]Q~־l65ds)u2 g7Q%*ޙ `&0Il6:ǘ*/mbܜ"Џg$ ctJǹ64 Xk%GP~"꫐MkM:'aV"l _8~+K&laۚ(  K44LVC˧@̕0wnn+qF{<8 ^[FRR#mvvχ fz|L:s޶"NBI²ɲeMs#TnS9I :͡H$޽{;M^":&m. /-CvCZ'7R4}js;հo$aMӢ@" ',1FKJ5.0詴mo/rk^%Q{cgvN)mSWLfxΞKǎ4tdT.GoY݆V mdu=Ӷv~by"d2 0b#MAϞ9]mK"8o~}$IMڠ G] dz~@09S,BOD'G2X8 ߕ,W/mVm9u 1WҊ$B {R0@{S]o=J_H҆|^|7@>-L#g2gO|!pt|:EA[H `I'(ܶـLU*㓣iUzn\2s$h.yqC"laEUG8:sH{84K]Jyj_P6dƫShRbzM5T#>tc>2K=l~| ܷM}}TtF W/("X7mM!;#JFOv^!CsVR ǚ+ ELtW_eٷBξ}5nSy 0ЁOp@FA{{HRH]G0VNqꐀCH~q&*N-fk&'gO3X_}8k^;V ĐQY\CDgli}~׹q%Rm5aIoycdssT<*bvRX+o`TN" ^Ñx-lRS 瑼Lnut^ijggAs:C1,̢ցn[`ۚӁl]f[N ʩS5סaզN&+YZ o~m;:&_AA^-7"MaQ8*0^p3[z'4a7˾5rk''ǠHuS8^{XN7^{sbN@\0qKNxh G30O45!Xqq6]ү{A d\ݚu|1?9a&]isa1TC#/ DE9':ppܟ8+d:f1ՁtI//ΐk=}Y؎5 [$۽$e?<^[vpa<7 5/*\UZ1eO9jeu ;+ 81x,m;s둻ο^5IKVbijto\ j;YuQd$ϚkfN҈{'0@6LJ6vVL܍_Bn|#~lQ|HYzY&ykxv49(n+'+m 3a >F޹UyGzMH!Y2w`rP> "0LJosCjUc m{yVͶ^ Eoϑ/%;' ځ Xߍml+eۿ: HX#~ ao]bk ԡz^L_嚶0AW_.QmPнXAҊzuGtnK*QdrW+غʎࡌk6M^):MҎ|M/M Bc~Ѝ8"sMW٥e[v8Ar|o+k ̫ `ʸ1;ixae]gq{}2 琷٠ "uϞY X2+{7 l1uB]څ 㭯Y) ??e:"u*B&n>baA-8CmE*<8OpW-}]ۀWN&hSIR Byb#[vc1p|u ˴[/wQ}$[XmX ,䭍 8 Z $Q}PI#N&[ҋQnԓP`yq+tHanE"oWt& K_־Eޫdx~<"@nigې #M)0a<@u{-ݯ j,soBx4 36ʅU>D2a53ׁo9G3Wė];ڰ  n[fCB9G p⏽9[kPyl>K]bRQN% 7ZZry2@Hcହ y`$r?>]8z`]Q ɭFW3qg]QU3&0'?BǾ)"hhVIia[p&xo%W†SN$VnC;A kGcn.WIcQe|f/6uU[e½Q}p`Λ'QSV_/=qq9@!웑t<:a*^Kk *8]M@ԉGs|&ƉÕe`B:(*Z`n6~]C v=<Ǝ3HggԺfNa[KqէIhio^;ߑp֢VB'F' !}07[bC|F>zyЮc!GmKc[lhbzbs;TGw=CAӑmevj45,tkӅ=E %yx,W# /c/:Xg/Y>&|US$c3SuEU ؘltr*,UOW;(Oɟ;W7_ y7ށ ]X"OPW6=;mبâMṬ|p(L1L`w]F^ڙB\k A2*RX7:2B:G]r >B>Kʹi9G؝vuh/$^MK#`Ņ`62`>Vf Q>Hr[^JZf'ɟj-9GX EטI%xx*`UnX L7eyYt/gyz6kV|2W]l"N}F wκR1rW!9uAM8F ?47SopܮihBo|0 F3<}dz2P"zѬS'u_~VRJR +;b1Ҝp^V<*$:WiUʥw2hiᅀ}hd c&&b<+$߹t1 8Ȧ ?Q:QE ¼C J㯧'_2~;ʾ޶T>s"S$ ܵhMSO?>JTx5]Dђ fܶʎ[ce^8Me8J4˯nk: ߥ!BG=/@?ÎQ mr1ѩc\Sޫc Вc`€Tu9:ZZC˖: TBww."){<6³0qa$6l:46^#VP"CwXcC(:4= (XـAFA\VX-A K;4 L)M\REgb +Tu8cj@C ʹ 3=YdUti͌cYeLΎ^q -g_s`*ydm81 8|q]6 X]0^ ^8'W8|[9>EF/X(& %#rSyTc;KP|EU"g;\(G#HZg: M-$`O2o&F5 g:u؊':sul 2NZ1&`𷊂$LשQcBNϙ` xdLG ̫A"n`$UD:g|#li$֑ˁrchhs֟.X1ksL/ʎuzŶF|su&Cᄔmp3\ YTHJM:Qfg-^Բ@:3 ?&.ymo ti\" hlˋ//87O^!П_|T0 l(z9Ztƨ>ɢM.+E k Cԓ@fm.ڶp2EqKDZx8١#G38"%, p>n)gc{hb&I18,,=Z'{&7&(xҽI/u^Q 2WťsC^dU3]|̇x(ADŽk7aDNtF]ޡLH=W֙@@~ji}iUyУ>Lⓕҁ"x Nsp:W \׹+G[^Ar݈`A`r8wAޮ,@L@';)mg pb.BG{:7n B>pai_=!-l{Ϟ4n;#\1=&/k$\7N`τs 4Q=km! `t2ϑvX^r/ӱǖVP'xW{Caձ&t"~y.ECT;&X.n9yR0;؈HhT"=3/ eN8y ޫY<S~UַQ̈8> 6Y;N/H?:#8 mWy\@{˖E‚m _Уlߜ\ow"G\tD~>feج3p"ֽ['d%]vL4?y:4:sL/R%zW D0v5 ?"HλM`ob "5fCT.nWh=5E$n!G5!4뀷z2pFH?|Vw9#KkSܱݿ4N5\Nlq|Kи m{coN6C&7c,So*L Lzil"v8oV^!>ۥ)u Q["GJcL^"ބ[oV ӽ;RkD S>j(n,ZAf}5oP>M9$t$i~Aw釒f< :-d c~TGзMw`b牗ROߡ^pBFɹ9B.g;a l pkc3 ewo;J5LR=-P;Jc]$[\='K]G]ݡƈHHəYL&&MV&)&w3N<͵SN57m)0YyV<7 M1gmMa~TIˤ w7:\!:ހ"pة;i9;As v@%z;D}@f}sazl.i@,$eUѦHYfYVKE[,T4eJR(BHQ$% ("l Sttxsn~Fr$0}{ .U,_&}yoUXN??.uFP*ڞێMG͗JW)iɔ|O@ӏ؟%qQ= Ϥ`|ܹg;>s1JnEVJ捳l}[#~}8x=pUA!R?4iihG^H:u֭0e4]x.{xdBq1C>pO49,t?FÑǏߕ~_B!Α7)|ܙcLS}K>LZ2/_[t]AzJJ/j"e;t u|{ez>Gͭ/e|;6˗}lM_ew+:,,рB|9 UrWs7SM-9j~~@.LD"u}845|4`w. Ho[ *V;uM*db=FevdNf2 >5jucp:>@*dAiC0Ϭt5{:m):{2:PAme6g1q<}\9[ vwZy|DLW'<@p8S #2` /{EʢEaRX5 8g{̗8LbCh9},X*d*quu73 pzt/- ! :#1W&8B @>^{p 01yޛ%bEEfPQ rZ^Ӑ2!*n:e$H_1f*^~_{HBo>h%k`$O_/598{\ev= ٳg,k)83tx fs25,M,P}L8^i2OEb#,/#/roYᛁ͂{GwuC "+Q:e,~tJ:¸ET5#נZ[6mTPyGPU -|-wp\su>UxW?A[8D0d.0\yV ڡ@:77\{;ҍZr`` _E $zOB#nQKа9n[(̿.lj |RO" V j[ܶ;>f$qIZ_H[lgoئjU{ITT? (5Lr-H\x%S$LtNׄ.JݘIiWY|?pU798xfU'; Lr8myVII?c1Yg$Uu8cDѫ؝l 9^ ǫ-٘!awDR &LpaLk~RIbesܣv&южV^to0ݵ5 @~6Ю\ f$A|YH><RU&1칼/ɓβd%ϡkXcw0[k,+ρ8FE eu s%o&>V ]DpUd / ױ<⠯;욧e(~"^ڐ]Ä#_oL2k *_J/)_ִ 20le:=XE1m3'`FTF-۩A=XLWyɤm:AƆ<$6|q4,n\CǮ^ŮpwG%`WXk ׂs,õk/2=Mhh ѯ2HnirIv+$Ege*~EKLHc$ѭyva=MoU;ĸh@*~ |R@)?=}G;4;7pvǭwll[#]txxܪ+dxwJA_UPgo37'9JO>1g-GN1TW ,ƹGs;:Q _x5*#ya0\z=Oy}!& ~3ОsY3+li@Ť?9Im_ }IVk׮"#Q@Wy$:|ܝ N{ϾhՍCgsx9Kmu>UĹ\ATQ"ęT?*O:x-F5mnnKmuA>3l,AV`@DSmLѐ5rSd<{]aa LptTLR4yW:˜q.hJD|+X^'UH5Ayf:Ð70x숀 A0쳶_bBiKW {EQ$1qpk!Dȗ dX*`&nbN f:N2}yUz̑^8-oyDأUZC˥3䋤 9{I|vQkW ^Wtu pt$d Ⱦ˃s^~Ws|_@ @Rci~~O1Z |ώGѡV̶Kop~~k*_[upZm3V~msҝ5cY¹xDρӪ2'3!BN:؛n"w}㩭#=2AEdȿ_WP$ҕ}Եygk$.o T2uj PPzc.q;ϑ+#YWv!ޠ݄ސ `UqQ1g~ᇣ:YNܣ։{x&2(:,Ƕ>'^z 8C0Po~t~%Y$|+J#\"aPvw&X y1lM<^:o`,?ґ]+_KSo__T   s/y,۝ Y|9Q%P FmYA;Hf À 6:Np}j/]E~3].y&-풢 vO⯪'[ jmjtE ip MN6rBˠ3U!d]=^kC4/\K T.Tq/vU/X_NWH[]M[cnm kʯR6*[+Lr{0;wJ٭=[#lnjnCo@<|$M.TC3aǶs~eJ\uZ<Ӓ_6d[M;j hS]gU+^Ku`{_)ĵ2Py3'm c%u nQA8M&7Hg%8F-um< ~LnS \9_E EbG=#GI"pCtҏuEWUV4?xb5ṕQ0'4soZ>:N1tAz&gX/ lnG y/숢>Ӈ-T-C+<\,A3K%oleG*O;z/Wv ]ض)/22yπ#BL^Ta)]6hkHsSyܝzx[`291ASk%8Ow-g7ٵ#;Rz :#]A*trMj{W7Zylg*(0pZWA*s)H[lj7=iqj{sD{.t,C5`voê2qԏ+mKm 2vڢ*G ;9lmB:\;"Qܚ] RU^}:L3٢.&vryt.v8%"$a9[^(dbB8i{ؕ@=ζ{$-g|Q&>#JܽZgWIvo&dT-drؒs=:A>[ʪzN<,# [qjD'E3RO8g$L3X{g {̮{WzQ~S3(@; O^g*`Ɍ![[>!@0Ä}LCPGA!+\st5Ѹ1`|b>_!Q@^{-*l=Y_{qFޗ7H?F Tp2Sf;oK v:9[_V;Ⱦ89H5+T1*MV |=yhQMO}D ʊRw7Uܻ9*l HBY}=+SYeBdr"Tl*'@^+~)YM&P`395PiڞQL!?tpV+ϟ Q,f`}m<|QcUg I"E; ;[G8j I*dUg.z$BB+u`fgVQ`ch\A8ۖyf;;F뫹{ԙB|G9.Pl^g {YDgY" ne2s,ӎU^'s4%& L KJ Tڐ5`V?plsSwtoK_N[ٗ.lSc+S})τ1OUzlM2/51@gHym !uYi:ӌGzK;D_$c\( 9tPoKs8 | :"iZgkND^H2 l9Pf B~62q rGTKm{ # gQ]i* /xygЅUg"4<`ڂ1v:S p'wNRT'[Yn.(Ci;RNnvv:<^^4ζfMʩF}?B\>Y%86$E 8=x.<8]G[U"h[iϲ ;&,rΙNt9c(G8A+{m҇&a25:" OpLRp {sXX76l<&=ؗV[޲]U_>Nw?=aeO|$8h$#'^~8$&h=޿rBz 1گr:O"#twI#GAyMjcPh|*aײ6Y4i- ::Sdѿ>[`Ց:1BW{sHњuޕׯ3?O?!2IkVl#sLBq_:IZ&- -qJAV$DARZLNT^s0egL g1&etxFi Ah͎P4eZwv N`3AO!墕 }lrAI .HIZn8] 01CaNu{dw "*)I<f58 _8{ߛR'|+Ҹb dǾ;rM5N7NG ` SG7?#/x:8|)i@Wi u<'OEKG~O6tЅ,5xooHΝ9ttv`0ki@{RmY lWrR[YPޢjѤ+{*z ]@cIj"0zR1VEyh`iN0В9K/ (\N$1'yO|:ma^Op|*C;ְI|~1a.#IŽzb Wa8MMt.b-yMe =G6JPڪ}` ^Cڥ9u GG=F&" 3t:BU! VVoZ-,F 'eW؄8١UP{;j/;ΨgV Q]w"9†5uw Lf`3YuڥEk"D@dvޢv˴__UU 6+0<#ѤvW0?Џ$ kj؍AnN_CqSV8w,,b!7渴pMGG;إ{L R BꏴsVO' J!_AfR Ek5jO%'o3fw ee) UO+KD5g,0ed; .\{\Ϝh#0@`LE Y#kMQٴXiZɠͣ_[sEB {[刵͂ HƧtov5kV:8r|mr J_ՇP8?AWj*MxΉ+4_$1aAEGVV_ѝ"ހu&sAIJ_F^q+ iAvip3`Mi1xVsC!zO|<7ѮCCǎ]<&=eI8keXN { ˠ-015tߑRS'if 3g nWjv'`."ߖ"ԙkL4㛉y6ȟ~n2`Ks|l^dv/T4iZq%!dm*~])_x k$&}ϥگ#y\kjhͪUAeG1z|^NA4^wbI&/)L PMou=ZXdۆ}}0NBv7;tʒd|%L {V+m)ʞX1za! 订3~G*ޥS"]~iUnjM%GDN `<%7q7X hʿJ)82bγwX&6Q1qcW죓,XEW6ڵ7H&P_)lRRmq<t55Upō !"z+`e`JgSCG5U&9C~vv>tX22S7TAڗ`kj͠{fM~T~S- (+j`lXK=:8OH:][__~]:J;u1;v@i֣vŷ XY2h$Hr-5V!2: I_=B\Wl"Ѳ @cdv3YO@qWvNl37!yV _p(->йt 29iJy=[w[aϱ\L |w?~m}6Q'2<ǽ+*ӯBӜ_ͽps?z :_O[nO +/_l._y BX$Y#"\罰EJ8מsq0N}Ns 83KqZb߄|&!yn1#&=4bb3lw-3򤰠; ㍱ W279zʀ'COz䁡CS'e2y^4q+\cc<<6ਕ2*t<J[  Gb\g:<{l!"?8?JPdPR10"]зGhs#A߫0cvݾ՟ν*} Dq ϡsZzL$jghrvpۦ=l6wBպZT?)ӾN&FU/F @}C/d1g;s4nO|+O8f@=I'5 ЁCq&|9|Rv@/atA`ϹCaERFDN]kcku")D Y%Y"L%7hE[VZN5fMWƄlC:z:Gmmu5\1Btix~ͤBh|y&xV~QɛU3L\2# xTG7ZZy]vw1iDj"džy_y)+L*-Z>szS1t0Bڇ (Jq%R7b:>Hs_=l$,;Vb/UUM5V8wԳwqɚ+ASwO7?پSg qq,|n60п-LRM-kF¿9SLp0(|"L)]PĘg&S_]VAO'} 宫ȵʘS1a}&lJI(\= h@]ϣfUVQ銊$ZRM!FӷiAiwr3 Tsd|S!zLDvF)bY'8*N$ }}{8{.卬MSI yV͡H@Eo~VQ9niODoNTR^NDZt#Ak/rdQ Oy{)GKkSA׎aN.^!I̼m_UNmKu4ϖ}~i>J2ͭ.QVp.4e ^6G q.!̊Y[1._2ģM#GQy-qC-׭^Avn' T{(l@f;f嬯ߔk/hP,N&j#jöx(v S9eÇu =Wҕ7}k96];C19x;tP/P}zOP1ҵy+aBhp?0ƖHdTv\c2fCinFZ_vJyפH9*·<>cݔz?˿nֿ}(\nyNԣ=ƵY`^-<c5ɀPwo_TlF_={211aL!QQpރǽ6.7iw4Bl(c|uñҹ׮qFe7z}99~Jk@Ά?x{~gQ $| >k_j'Ae ׽he{T@՗{;`4 l!rw b[!$.`$Y e%o0۠眏&jZ48uno:swq}±Ԗp1|P7WF6Qnvs5x"Q 7hhEgdіh-'Z_7>#=7r5HdZMN]=83 h`e2UY>g00n5U8OCS \I?:q(ӗ9t,8(v,Q;">O'Z`+t3Lmiiƶ:VNtrm V/_Yb50]BGc 8Κ4tmW=L0j|ܶaf4n<"P#0j{mΌR/YQoŔ`*]T:VEq`% dS:EZ]'`Ch"-h&n˜V)mUYEv9>{soq_l߅755[HGμ`p+|m`Uw!'Y讣fV ɥoDpaUe&Yao3Ixe~~pqD{gUtOVHvǎ Oޖ#@}kKN@ s2@)h]Zl(h̨r-KqxpLyC_Nr~:875]kyi`C"π˿Go_l/cyL: b`>;Lё8VC ;}_H3t~LBYIy[,@^BΑͮo`My'O\ |#c1\?F"dqVq B~y ^S'"3y/*\;EN%ṯ)c/B\smQqph@V;`=NKt|N,ϼ~:hRpq̒X`2OR]\O u{3T] 5e[ͬm."57 q|y7(գVOcP j"o$?w p _+أL,WVHϮ9:}\Kթ{M/|胞ށt™Gs!"Ζ&B#"1`ӽp⮻Cԓrc T δi<ñTU7vh=zEIϒiy*U (ӨZ]7X7`#:Q~Q~ gGz$#JL *ZV!ptM[VaZS:d"!e,}>Fe1uGi3 {u^d;n$Qc s(gG[Ţz{(Cv O;As %;'?V8'JWiq-i5@_i2g5Y$|t7kj iă32R'xPY?MUVIrǔ\:IFw1F"XIlDltu(]wQ +̤ {xBu+ijH9D^-eϚT&/ O I x&5`G]X1$,DE3[\5\mH|G+^2+؝3墲X=[r+>ݟV.km@ggKKomRnBbtZ. ֻF/Wؙ$>;^(IcRF^eϑSY JSB[rlTuA%\zi 2y*mOfk*j7fs[NϽzǥfּ(ߩ%tcD.>K`|&bd`(W%&TP=:.8WЏX:$ؽk|{$A)MZ ],FȌt*mjUCnF\z"iMNbC0jRpmDP8 ?T0VǞнlm{s3A7kLP];;9Cg2e5ڝ\L`1QvD/I6 n ʹ n2h&aaֶ K|o'F+?f1A D+a;$ i߳j~RM4𸫝} W#a>򽩩H42p)= ~6q /ׯf>sס.*);+tSņRsi =SGglf4̳Nϫ/#E|4KTWGz4z`N,ClcL|[o{\4xnR4cg>{m6.lht!2W{cbj{p+bi1'hױ]C_&PXYN +ӱy@IDAT&诙xb`fav:{ͣXJSHX߽ 97 :q^C`L)mvv;n17m8_OrAL/E$>?[+IGk&mkw}>X.}vY=-'u=kJS7H3;p?aK]gXy~%NhkL"vߩIm!r$ %$evC4r:G 5I{HT$݇O!",c,W_<`["[-BKW_Lu|dkW3#H7N#{*kfrAU7 9c}~.]Ԓ!Mё3O4YoYv-U_`/{:|80Xvv*gmjmePfUk|O@Rt+46c{`Q^b_t9 ўen{5egV_f~kxs8d# /l/C.4NZgYῐ={BṊd?ɒJKp?e goWmYaJls4uE𛬯YPi2F$э@yx+{"!L^{ kF$d"*k\f1N]Ry;)z'wzs?Z%9N]N_85,0bV8 ʩ|4|gҐ_g?XR׭έ۟g'p{mmT}^[$W#Ø%Vz|+z^Kܣ xiixE`\x51Ϫ%myY]TaX(g | X1:ȶs #)76 S1Ahp -ktt#ZH F5=. F5<G[/mEA6vid}tη^u7Ff'pCb+Zz0sSׯS[aYWG- ҡA@3VƆȤU9mm3[*< H@utzp J2~/F0YcX/A:|(٭ǂCUg A*I+ZWRuwvjU =#QMSKtnk:hI#\ GGb?嫵u &/O*W?8uL:Q,o2[%E笄9A5:Cj3 LO"8 jT!ceX-tt¶~~Gʠk)NHRW!l8&tj6Yc=mu-xUK%6V? `ْmbmX?~ 3R ZFFN௠`SWՑᱍT. E%4.bkOTh3LbV$f )gUArutk]`=tzwg>vwK7@y:&8Ko%AN 8ӗ~q\u1Wu]Sj!੺:t鍯/CN"`G:NWjp6 F`ٺ^kCÂւ< Q}=nv@ّ:O%>V .Z]gs@"=*(].q(_|^ H~=tc#N;ȫI`8+x Ń YQ(bNAf&! s^ @_<r!w6%@{Pr wt>:t$~-̠[mcL!? |;+:yN:;K/Co֠ZHb֣p_hs]ѪkȂq{LJ(H1s.W9h/XO[qk ,2#KLvilqv=h Vf^xA@v=nW=:mdt,xz AsR8gnRdA~7mx ,Cڹ_WFv%{X%N>>0Vԯ`gq&h1i'{)C;3B"IT]e5k@w h0܇EAӮ,5('V or]:+'`0׸n,苺&0u;qysll+:T8Dv4b 3QBK嬁AmMd i5QHԘg8^fgl$4b;R94[ yC[cȴgv6<] o:&hE(GSA&Ľ2edlK;E%$4_$d%44!qae.اtj㘀iC'vw &3L*U6ɰI e+|/U3/a| $x {'ejjLR`= YA9>c&-ο NڣT8Ǯ4NL}<"-3&hhIW&iWqC9¹K ~#aà:2ɻkEqhNx,e~vO3$4 C2rymr#m+;̩d #̡ȳ2bRbC(7M ^e-LQY[-vP"H=F}y+"ލ,*7Po8:;0#|$ gRU2ra eد`…G*YYr d^& ]6ذm~go^ ?)Kf'E'߲˖2VtdILyEe?Qwuuw(F;})dln|9Og亸菸_ADTP4ۍ[qvoTr%fpO8.a >/ODƷAr1[MAhwY4@c?\I18Cc#i97| rPV^@k6AR5<'pjii1Q m+h dv\>j%Nq.NCCt&=ߛ:zz"u~24rds b` zYotI* syMBcJqA΃摨wK+AeN[ vz_&yrEzyvX{u|$ke`&6Fzm^&E=̟1hEb߉ޑ/} }|:3% 66N+6`Z/ImgzF0gX1\KԎ/ޢ#Gk=ףj=dZ02|Znz:1P}:d [m.?dTuMuDy/m^]0`42quc/qWZ;Р{΀mT:! .ōn_o-EpG/8\_eF!m+/]"/$hsIgx-:"5lC[H߬VgK j/dgCڑA%55.?H{+\a}I;t8GSRfMMM$ߡTg5E2WIS$1nt(߳»ڤ>i?Ž`JcW ϖVjH #`'@͸zWqc9~h2833ͭ:Ck_L{ =.='ѫLGzT dQǪ 0iKZ 7Y` ]ɅD.*FAjz;g'z o>`%j%vrȀ $Rf?`u;tigo_})YܟO?Gx>ws HVK熡<1góǖH8>_)EڹV/"L61̎Gyva1_ml[R_x7#?~="(]^|kt08|$5vZ_kiCӾ2!ܔȨgv$g¯i(sXgv+;mk{{yӂP>@ ʦl?-VmK]RK'esLܛb'BWg8;ZaqĐ7a;0/\5"7<︃}3%](M7W_LN}wUciU9Oٻrd[%b+_- JE&^q9gX vYľpN{ OQͧ8KRsֳMnPؚ$ZZ:Ob7/\♼mV ;WGJ9+A9wR \a,u; tMAّEFmM|]x{~җm'MH-u~&]k*]bdشR&L_̽^DWGUx$ xM[ģ|=ORW`.zt0gva_sMGYE1I``2e"}vbYȣ+ }yYrȐEKH:DwґAluaR0rLONdz ^39`K婉Av 3+KW.t=M9?N+-&9qmĮEgeĎuì6 ծE}$ "Abe43@Ur^b=&ds7 .R$`osoro 3*Xsοos=k0ƦgwUveZ@#i,3 v9hXyKQhƖ493{Z,P|[u2)2#ҡ\SYdB 0}}A1PfC;o*F7 TN/o2f͌ 0m"2bq J5ort3o=t8̈׹68c|ӡãm`<ڷ@ 4u(eY|n5%$`Ln ,+˪2lh:cF]F`m;F Y9tFVۊ*e6oE#sϪFr!Ws5j ֖GNx HjM_j8-Hא\[쿙p @oL^9w#l4+5#c`ý 7 xnh!jp:2?q9wճ._Yjds}{WGjjSqd=kubiRTFǵ[fh7⻖Y[gck`ՖTXaVik% Lz>x8$N\ آ :+é%0nƹƵ_91J =- yq@ MӒN0E9*xA+!շ/N X[q 5*$s58__ʍ},E%@)$wzdf3 H 2l|1+Sg[GTy<mk8|)R1IV@}G=XyQDЦ꺽LߨL~. 4p˘3O@%*.I7$h+/!x68s T &meJ}$5utԽQi;h:DyNô >ұ+Z[cHr/@(䟁aVԳJbv贂$`/h/,}mpfv}W{(* ^*cO)/BA'5/JS&G ;æPiɏF1ƱTuϱccTaɃ&. )hZq#+xOV%G{xﮮ.:wAxE8LT  8q"heK^.7nL8uLӗ~4_NGddѶa4+;)o* ,/Eq#]`퐫++$&TiCs+/mO-`[yS>KϿfz'xQ̋9f`$,҃7>v4͟k陯SwFrٖ\vRVo b)q 1jpN;ǎclْ"[Hlmy^8WZ{id7耗^Ѓ]g*}iOsx"h:s *Zi,t n@m:d 9 -6֥<.;WG.,48@bJEppNò$QkEԹ p~;BҲg05[9+g=VF$Wh: }鮭tΦrgcc8u*{^f=l-v-ef2H|@s3PUpo^Rf""ѱv2ˡ87Y Ú 8D~ Vn1q˷ nSqn#SȦ__!22n VZTkS(zu*UV2WJ+S=wtCh TfѹUih^9I<_[N 4FpB%:"z$32<-m6io~JV!v=rf{Y5pw  \2W tw9|Y-#؁,͈ieϋڑNy.gWGeKh*,{8`ۼ:n<ͭ(;d~̭tkV#j(t12ax ak{;W5Gz'PSG41ЧgnEC.w>u*.GN`$z @8ڻ(:2ԇA@N1G3@7-9r/0 0XxQ{v5dYo",:rP2l}߲|,gQ^4--ehk&>/ ֹ0p:pg_05t|odPΡl|}|'s{&ZAڎ?7bгFi@Z\e+p|fgƽa34\54D&(lתr~y(|t= eΒӂ~3E[_0 Ƹ (쇤)8C|/[uy(b/5}>}8YiL҉Or:?"]w]gø|o? w8]C$@=h:$)K<yȬM.9M _qO{gfeJ)~gvt~YqŲ|cg^9~L_/=+S!㼁d(Ϣ_+{^Lpz eo{gwˠM`f8]|`_OzwL/( ][Dz8>?rFi#G\@<J9tʂX)\:Anu lZ{:1 f-Oi@{ci\}@vw#GOw|y TNT2ZL mr8e9 82 t|:cl?b?uR@1췞02ЋcIcw+[H[8t Z+N4Fuu6`ulm(x|f5gXc5h}Rw3rf2 S{HhU6RW%wQ^w,eϥ}s}sl׿NmNvZ3njEKz#m#o v;稒?a"从7ie1rzj"ǵ7Xr4 ՒF`[8ȝR@UzSnj]a%W1Al%7s2K #}Ϣ#g:7!3@j>iJ%jpǝ۷a"޶ SKCӔ75hӑ 0ILS(:Υt_l,W-m_O*zz' xWJLy{??W_G.:NnϏ}uj+qY#bt٨^k_h'AYEϩNz]z:6-_k?`.=L_? CO3nK=bꫴ z4x4oIi{[E 1 Z'Nm9)LxʠKp,UǽGh腬kho]N^n@A^:r*Y%خegk|ydg%ܯY ~@WЕ]ܽp/GKҮb%H%Ŵ(ЀouGY̡d*a7  ;xLBv*sܻ3I_h'rLA6 ȿ3Y˵W.gҏUXtfœ86Tޮ:3'ufȖz'P_wSz8+:L#ϘǶӱ9r$?oϦ/3w(XؠqwJ,A&㹻j#v$4/"fr26Bv#Y`V3)+@T~*]̐:H/WM9TǷ{>Y[{éh~:n_L[iVy()[U C74hyM[ɧ7   m = Igut'[h,2cdp t_u6x3' d֑_*S%[ 62F-ȍԄ<68@&m8`NZyZGTv {"S/^6աlaBMt#xɴ'SZ\}A)hdIn<1(۴wx`6$3xI2'/QVQea|x~3%8A+дzCIA&0>t\=l@x\xHmk$jꚍ S-2Ye^yx kSHɐ=zAhs6uuu7lu}Mۙ:IZ)v$ *w+W VyfP}b8PY x =:fLPʪ%Q4d-+YPQpK􃏧2%d]t%c(g=Dp#JvzSalZҲ(Q@Cq_aث]A%w@"@{:܎193 #_lOrkd-j%uH3jTd ]`h0]@e ?`42n { $nV<~*7ep 83TB[8V7{LNvp._1Z4t12Fqr a!(9|gϫ-Pl.M3]tFJ%``b י\ )VADiNP: g44̶β`2)%{<d^sJzנx69+?) k fl ﲮ?Ү Pp}4nKj xMֺ FLj;`Ҡ8qts]-[%Sӈr 3pm >f,N )ٓNBa@ң !FK#qv4uR'K. ~JsLT>\KT1uGjf :$޾8.?` p$ISsE,]:pWctFNϋgV@B pf)FKQKCz%=JY 2)9[ܪ%yhϒ5,>wK}slIs"l|Aҝw :uvuU'4E pnlmM'%8I6*@IDATt~+/x^%}5d@#CNF[Sgil*~i2**RN[?WPųZ+oiN0sۯK{>{u޻SZ1Ua#_!#i`=f+3=:ͦ9p5 l; x3pL4s>|y ښLwo{->]cuNጅst=]8{}bDfIt-[xӂֳʐ?>E۹b6b5sۻsړ{۱^I0;qGXAo:QfgNՎeڛ7i JZMiryE*ZugqNyCO:,|[g4`r*8kٻ) R(f_Yw[ B'_$}ĴpeT2"`L<Q`f4 eg)F|ꠖL Xdt%3y-oSf3Q֚}W^X}l۽ӿO҇?Y=w _gYu9yM#?PNF*:~ufWs+(T(t ;cO><۩uJ/7{P2H>f<3]@y'P̝ot-ǘ+V_)oF_wf9V[Qv)fՋdЩu{!(1<~ǰ=xz$=ᄾ}KPehit }^GU-9]"iCҲ1!ξ:,:UGB癢=:MN//˿p28~R#môj /Q~Σk(Sow>:4x>i$ [ lϥ>C3ʳ=A:wΤ7F?J,b3I-pB{1V%Ӏ*d?Ɩvff͔7h#\~1 [`&O<x &]FFpߛI~_CŒ9Ζgyf`=:mx/ `/g1j}֒Vz {"H3PݞF8?%ix8ZmδF'3Yk19jV慎[Y}3 &Ŗxv凡鳗V MנJq}W;JK&Y".2 ex |^l%5S~)ntkit#yߡ@GAp%vsV{V)tA?FXY$e L@Nm.rGshx8d2vt\^HvA6=35R!A1b%ƩoFЙ:vJ}=$ݐa$tk`I_o̻30F[(gLؿW`W x0$wR8mgoߝȮ4T~IR̲Ejs + IJҰƤRF8` (*g>QbsTt64jGENH @#vY|Y#[YYM(ѥʟDxWs(pqYE?C:\FQZxFv=Y4 [$ 2"ri]J/7e}gc}Y#O>gхk?zz?EѮK'{"=»9pVgm3G 8ӫ(s?~oWxR37yJ:r>;Z20Ÿ[˜d qٴp|#5~z Z BAr&_y+trR%aPߖ*(|A&&荌_vX3?6=pc_@"9v Ѳ1V 8Yqg]Z b&n 30^G\[V^G:fgVfV:{UF (,鋼13іЍ,ů ضg9`ʖZ=>䂥2ܟ;Ĺ2ɊA^P׉kd% 7f;ޥ;un3}} ܾN<{gi+.qysE**$Hu*$?51ڠ8`9p`Xy[MV+dz<0 G'K:y pK4QE+d=,PA)NxƖjgp:cܝQbvu!d1<ދc%"e"bgSYbm&C)liofY2nM%z|i8d4V:SWvʟ~'@__oRqH/ϫR͏:su._Β)9~yV[r2ttBGqu."`#eoe)#`*zry= úo|6Z#D.PV௜]`Nj,(ɊySpBWr JocӴɤ쏞*ΊFҴY0tJxvM4pwWd73lu};]y'=#w':=H=x %boa$L2y]|KC[],|]mPMjphm*mY(rE>[ bRƘOeJ٧AxkyiRJO=\Cͼ:烳^xoٕofz_K_J{"󑫞3Onk![XL{kv>L8zCu mWi^O~*Y5w0yEw iJ1hm@oNsOڃ[7lʄ`kkG`A^k۩Щ4pU:wVѡQi.7bٙ Zy,Qum{ñ(#tL:UxW @;1sF=3d acm_,Bس5Zmԩ87l d 꼙G[;>xv6Q:`o{vVNS1Dx}~bf;>iU RCG&Wt;!9:VߪL{LH0$-m|e']zbz{GѥR\@;̳Wm^CE,0eGeevq-]Ke T9 +W4ЗbT(Ou耹Q~}}nE\usLBPu]O-5!;eXrx[9 F[I}mX)fo(8?sv!4e_*ATBb V6vtvTLPaЧd ,! Uj FSϑ /*;x}*,/H%ЇhsjB?UҾXAЖ_ZQ%mA Q؀>W`}o|Bmsz:!f}=wB?w] ;sd*0ɒ2m;Qg@8]˽A2 =jqvVܿWǹAÜ=cgI~Bl3~H#P]1ƼʘH܏ Jo-F@_fgJ-jF}5Tcd̹>Ia$-v-kYfdĻ >Yu5ÞHMT TU(:h bHs:/f/}Ϳto[w/~?^('O}07pr54_ ufl`GEȝ藅:54pXL۷IJ77_L}g}ݘK0SZ0M}(_Ae/ rρVMFjZkH;A oܸ>s`d7@kRK'xN_Qz껯A"wCts6zjtΏWY +_P!m_H>U=Fҗt?w~;}Fz/uo:2v_YFG0ss.9d{l*ΕgE?Fg P`F,MΙ% }[NN<=HqP 7 H{:gNpc_cN{W+8u G Hyۡ4D A7t '\p%,23w' $Oqo3Y:`! Z&RPP׬3/a_<2\|zife+. 3,JV? .ĩdY+uL '圔/8gY-Q d[ xUU0Νiܱ6 [D˗" op?[h̵7^'h0uCWDyeT~H 3smb`_I<|$q^/U% >waܟ=k/p&H?wbk3巴Axs/}^o/Xԕ\i.rty臎^8s/oA6u#x:Ne ᳂)^37 +ҡnPҙnton-Olen0.%P{^Q-PgϳT;mO^:uiaDu%_Ę[1g v, c"ӔkimmM_)AL g8YX؁rd et:ۤ?=6xP2@o() i_teg] gzǝϐ[ڹ9e/ăd'NOWgŲ{dMg|Lr>סz._rfjtGexb{=H7?]@C.N}}n_\3uOR޴Gr8yN8!lm}Vpt,$`4`Lຫ/.ׁ0< f '~__Tb0E.Q?̸?O"?gH.l 3Th5Nux4e21Sgdϼ:m# t+G큩NgHGgWjIlyWW7H]3^SUz#ϥ'+>u$x6(并׼%;Uޘ[Yi=l DntV<7M: hKcɓSB!ePo7@{ȑRa |󟧚ljkg-CvAqX9gat3[oV "45Q9rp©ghxizLA"ý9{<+! #Xs /ۿ 6tm̝8ژ[͈-.r$ӇӪJ{sǨ<Uk t$)Bϩ.BY}s>Jk'?|wٻaڵЅ qj}3BVVS'pw} z`or 2lcV9S"VLm)1dSߩ^gr& {T,YG}v jϺrQoA?]GN:l=cg1^Z[a[Xtؕ@—:K>C{nk*dAY<KZzA_O ܜg .[(wXչ4`3|c0b;ҭԎR<@BS}0@GX08oS]{Gg-]z=ufoEEBwdaHC]Mn.L )!uur8K-mMPUaaltyPd90Q}W* 'gvڝd?(JLt>@`N~}A? $ ( -4ʙtDJw+!^R<{r]?uMg /tÞ &ӫ.GEEuWSɯ œZ]x.WV+x~z(?WgVȱ/=? 3Q{ k{ gCG޵%зlTG:39荒Ac5B;pcqKY;@ BPc 7bjSx֜|}`ʗG+g6Ir<A.Y W;'b#*`y DXж*6(:0Ifa **xb櫩s>*H<62ר?2}3D_++\$4?_? HըA:Tsq9!(D扙= V N34&Ψ'H1r&a4WAT)5K0Ouzj<>9ĩ]"Ӽ @(ƃ(E遇#qg^`Ncm\4G6FNcd`H j&(((N/ĩS@/3@ @xL2gt`:O{ uvLK ,̑c#S&KS >.َR;W<^ܟ f1{ $hAO;ۗd]1/i/;1` ˗qp٭8`rn [d>Šg .ͭ նF> s9fWUi'8oYZy`!joU`,h)_4C8BBضi`5J|7)ENƲ>r(Qd1't5Z h[ڬ2cxgov-K櫆.'AbƋ,d|.b8(("*k+njk}cБQ&Hm?*ߩS& Og Җ~~>0wRd4b/Wt;OAl(=y1:v]aLoZr=/|Qgk l1+ϞtBy8扶Ͼ7=38_K`O'zy+utvA;<=9@s#=p.:>K|J &v!4k'P~ݘ Cd>9ұe$2t ̰>dnQ_ _ ~U8Od 07)53a|ѭ Ҽ] 2uS}^]<SFX'^N^ޫNlL gϵtZc!ussPGX25ES;R 2෍lJ:_VGplxG8nTdukXq;e?XTϔgZ}BY%QR.{LVGwfy{'rEP篭ZgAVJ0 (Vn]=ȶ+y[~_Kۀ~0 2/`.YDKm *f= cg{ZC)!@cP0˳pұavkkfDYm>t1OphTy>Ⱦ/+_ʠ`'|&&Y jv>i~s2dX%u4Rt^NDg [>YG.!X;VЎR2@y:H: &|޼>.%zb*x:gZ>dx1U7؛5 Nm *lᨶ͵VwI%8~kW/U!>.8"tgjkmft] ku3L,6 F8lJK6dz01x5diˀI{]$١^tGe̍S]]yt7_jTўՕo, 3lhnjMζPTUQ~z=>:1>,TA6^=Ey~-ik Phc#ǂ_yjwD=z7POՙJص5$sO`2!  ^5285Rk|qi'{'뼛G/+Oڊfpp4k-  R2:8z/&*887L.K"m_oXikЪtT/vɹ5*^R}{_`*>c\`HgYK L+-=^Rq6UVZn` qbk ^_! CͭѩoC<^]Ҙrgzom^硌#f=RB;4M;[ݴ A55Y%X꛻{qI:q%PQ.8co`K?Q9_x@w,3Wщ0Yѽ*|-e_. .3*[#E2Mjmkp |x 8}\ K.m+l8ii\leFr=>~g䰆?pנ73@cKY_ͺ̲J54+PG]N3Bu* :r4]{0M6PnpkttGJb[7ThӉ ϦF9ܛMU"5F4khpKJԩiy%1 ,'?}Z҅7l_f*1]zzÌupZF} О8,a(ANNe-pqC>fπA( aɿu#1FM s Pm x-7q=%UI:h"ۊb>f BF h N]iIg :rnFqL!RTЏe@0q-sZH䀦 4~ С![pa,fFj=tЬIȘ*;D %2/t&44q_Y߂m:<4by8紻m1C+1t9MqZ (!SOlO8_ Yʏԟ3=K1ATq5c<:Ix57 [; Z(n7˗F7o+L/} K:[qܹ6 #XcF<3%Yv=:__?[׾?_}<~'X%8S؟MwlN:yYgN40cB8NMM@'d.2Bg{&=S]8GuMl/in~lx^I9Fg+Tp.d Qݥ+63Sa+3g*3~`hsy s [# !7xS刴-8 ns{tzPk% +cL$֌2O((ik~A/- BfU %0He @AV =uo8]az)u0@e@mάٰ'Nu@؛Vҡ><޷K_D!z2@hsf#.,cڬ-xAeٟ8O~>>n=BB?0Sa506ղ΂=_3ଡ଼2Hsej"8E8 F\qw"ʯLsaKT+iÚ9=vff@쵲( uWy̹e sOY!Ә\6[ Eܗ9P0.3b m[|s>֫TK 큆gʨh #~x:gQ- :㭞_u!*isB>)Z\ 3Rw 7vXrNKMD$m .[AE9nCy5CiRDT2\q湀\g8 bѺ Vu gl}Oq2U֤ *>ZyK.8 + >^f&R]|>ǖ`r^bF6--_%d Z9:8i-O(mJWAŀmej}Z]''=2AiwikRZit G]my>lR^wY\w~-{&NBir1דWP V6/ |B^6k_nc{ OFL'҉,y*PmS_zk0dhhN'L KTryiT&%p?KYv\'jsFm8y;cwq$=Dz=͆ hm;Y=yfQ^{p51oime2#^gNu0q<3R' @IDATSUulmwYG:l4*P4ͶґmyeM4ME|. ўth6@P6 bn ,*'{ɽ7K  d_Im0F|d;sϳ|QFkitiHN\~-JGi YmT38qk؇۵t>,BhX[ȬU;AT+k8uzo:8pr=]M@fϵ@u@BV =ɧL"_Z@:8 ~XQ,&N:YWGX#=Y ]|pm&ycϷ̥%cqcpk̩~;# -t1NωU9arq'{/"Œ4Xz:8}KgF[$g_r ^Kdki[lOǹ'xd4ͮJ#Q΄Nd@tw^Sw~ȹ8G:؆B:4gn3Of.+#d51 T3!W79k}l6~ұf,DSCA5fc;^[YFGntH 8.i<¿s#?PX(â2kl;{.ϻA}>MmlM=wҺUz)_QYlv8B@]*X}fz~˾ ǧQ_]PWVA@??HQ0&3 Qupzf}f'o+ߩ W,tʿ]G[4[--ȺbJ<&f9>|E;!`] =vs&M/Etyh2vGCFխQ~d@ץs.e] \t=9R`1RR=tR :Х4goڠit˅i)1L+hgo?WgC{*W|erTOsXkXBecDtcaN')UH) u,.ϲ^:k]}}| ǽ6cOǞ|&[TO/gR3YʀН`e }Gטf̓5-u Qyܣ[#P{?8Ubot+ocom?JA;wwp"c5Bϐ1bmۻ%*L m9YȶD˩GL2L׸=5@l# %}:$d‡ ։ =+|G!2`pY6adaCϷ?r=j7=],[\uH!얲4O!0uICy88ܶUEg 9|ga  U$9ø:r8e08lߡ sei^b[#.il-,,vڥuAԽ#ІV\ 4?<ɹ3:=[U=ˌ}u zf[9aAs-j-s 5Q +){eT=_v $EvT2']%yu thYVJ44M+m6X'97u)+*XmGƱ|az_v>k|d -4LR]:K zpי"Zg jd7xM(N%-Wl___iD*8~h04:g@Fڏ/*ҟE G.-{Ʀᖕ0E=PdpgzeUV K Ƅ"R2h613# -;sdq;6\sPM-m2R78tf ~=˂VIPW3fq^ PY@fYZth?u=FԿ+M|6z!~t GA}=a4CC?Le5L][3kcpW1s'O`L,b#??@C#6rf(؟ڲl)`J1J4{%4?OXs^d>ٿQG%k(Skue "VC@R c è'\̶l ƤZ%֘}bMu+(̆ :tܳn^Cƚa-(35# Aр|!P{ CDJw`Mkr~Z֕S~%#}3I-1^z6γ!  xe{44o p{d=Gw|Ҁ52 j/Tq[0u߯ H@]9оh-ν)xq )d}|to;A>C4psc )fy !]\"pN6nf\/ +A΄ q=`λgY{T`,(ggeu&&<9i⽖;|XАRafRi%V1A !^K-`%ұ)$6,K@Дel/JiTʸJY=9uǪ9:*B'i-aГs`q,y45a:o؟um! H@Jz{ҙg*m%!SjT6t{u xvx@r(,y+%1_xf:2G7HV.N5ES!t ȇОMV. r* 6]d K[]܎ v3!!f(2E4 ~ɼQGE.tMVQ3<;=?g\[zJ>?ﱑ=Ik~&8|c{%Y74tԛ3ƒ1=S ^j-j?Gi_#M KD-,+`,qmuF`9K; h)8ixXЗ (|$Ul3P^"3s\~WhR"Ry[\;|&:ٝ?Dz[ĕBte%Ǣ^N 0@vbۇun-sH;D'ݫd!C_:9(C+-OF-NrP&bj&uF'K^ f~:"! Uzڣ? _{AOj4 y_+z0@7vvd_evw&=3DZ2s ^4?AGk//ZtxP__xȤCeTe$ЋD;xQN.`P@'kZx")ilQ~~Eb/5 ڒ}v;"u^Y/0-);ʃ<:tOBH/_8uds;d ϓY'RWG{8,D+W8{{qZý=K#!xu\EǙN:tt8xBl,ꍺ >~"Bdkْ;@E&:A: e*|rFӆ!X`ə$6N6 VM،(֫7<1z!~'b'X '}+T]ig9/nԬ=l`?׌+Όwc#1@b++h]o-F47Ž4*A;GSHVWK{t9J厲U;4 6wZCGȴrVPzV_UpIp_-yrI0K*fz ocǑ3e`F0bWM|6gr6/ʁ_z=Ëm*Zeͤa%/+$̐٫ ')xb5d PWpk~1YACH@˟}# yY[hqzϮep u2/`M j ::+ydG/*:d+,1P~DVB ֨,џc4[CaiT_A4/{M־k^]ޛHJ)R6eIq02c  0A;ccKD˒EIŝ약Wum]յv{~RDF}z}s9YnUZiYLA'7o9 36tU-tp= @{> Q2YAISO;jBw9m>򡯨+wC{LI<ӽV.v(ܯ"p(8 ,t+~/vftt6~ϖ?7ũhTFiϐ/AGՐVZmܥs@/yۊ65y:$<' Lt%#QًuЅ-kb_c#~z;h<ǣZ7ϸF/9 6LLODsyH'E"+s(1@y )4 4 tlac/rp (LO5|OAƹbSo񰢎Ocl- .|#XOVn*y_x60֠&"m5M@m Yse@qUFӀJ719jr* [:V tv&qox 0^z+k/co<i|0ڬW^~nO>dso*mpr rEuXVa9v7oq3SgL 7deu1!%0 ~jE?{h7]^a;_#6V˚a0a|Vݻ.K,LYi7'\J^zws7?uX gϖ7L:Miъ&< `3 p+#8KE2C7<ϊ=t=ɤV|xfVQ9̶R$ehDQkKǦ& (EpR9)!&@)']*$;A3 :p@ŵj9HB`nͭ}sW!w}^/fm&~ ȇﱋ,X`ɳ*o~AA/ m~E+W P{OuO- %6A^7b c7E H 5ytȀO/ H6~0>6[~ҁLs9]й)^@X,FoX%d`b||%(H<|T:=>'C vN @AZ$2?o ʏ:`\ȣt5Pkғ?*e`")V {np Rp = ICGe}2ݧ%_w6d/eq'M<S5u̞sLx9zW 40 VyC+=; 935ʜr=({MF?sX^צ̳S4}vՃy;7W>^jئ~ܚ t r |?jOciroGg&r>?`pSYFTiۉv$,$29Jn 5Ri>>H_J`G ۠aA&+YbŞṺcs9k8y+/< Y㧨 L#ϧf?Ξ=k&MgН7Rlp݀[2SOeXq2A`6 ߩN+GuW *G!q0$T]k6~/=GR@rkHF[US}]0!HB{82ɀctUEgW}_Tl{؃lOV/~aQ{kLh717_KG@GqtOA$pE 86WX6X ApXI[s]އ܄DEY6Czn/<;>z> h3?ɾr}}y e݄eaG Af#ï@-CgjX+求mH~EP0ƀ fAE'VWCT_.됼oWW{b:{|*6Q]t4-W7^k1|t M,G|mDX4hh}9: ~ x&2Lf<#a耣_j0}kdl<ӱx<&O>]kX?XF y$̪g|]?w~w>GԕTm֖546׎2p Ok9xFU=d1ڍO+ ̚(/Vop6?s=?ldS[mBC3Ǖıjt ?/]?E[@LѡVmg|שWF;_Is0 -؃PwVy)OV_;F'QsFFv2@ؿ \ߠ{r"V0,Wښ0ЉG?-řDrIV{s@; ;:c+R']`tWsKA??E'A[$Xtevj*O*#cߵb3HTpKll(Qe+$d҄>mC7ADUy( 3$m z%7 ֡wІ€ 8SviNf%*<ߔ:y G .^O/ 2j8- :r( 6J+5lk /cb<3lwQۖҋ Yߣik_.G.R/.+)jҘ;p0x!`4ŷ|:>ݟʁ2F~7%N{vK 'ٷ,IF9%Q&H|A7iN].sݔ; G:"z9G~kM` ,22ָ.ްAb/0ʠ)>+8Kc#vrL]AL u3)㌧S O<SP˽ʻ+/wB Z F]n`FBSM E'/`d HQO)?,\(eUN4 mC.fœAi9`ƝHo@)dH` !#n[ve |x1Z[i0@s8ܛ{gc\Krs<?A>K%j[slgN0-S_,hq{3`|d4|"Ix:* 4*}iEEww_:t"@\iJձ:;p3X𣏠!RjfV_2a+m  VjT¼-:f 9ڰ[NUw ~ B*q u)G'Hm ,q0 r X <Gv&6L_?#nH= 4uw2'ʿ SDկʎϕ* V-ĸm3-&aܧ(48@PfO2Y'g,(m9mU.(c*9)+kIO-b1տ*-5oq)ۃʋ^:A GP **:VIs( 6!L1C*Dm!-8Ť*lZS huڼ|뭛cQY #kܵ3hk˳]$i ZICXmu ljъT-S 9'1fpI'1\"qy`naW޼4dUxq YS^Ʀ I\*Z:l Vx^' o0Uk-**tW7y?V?3ʫ`@}|cLB`dkՏ2͵=ձNm`A\?'cc;oFCT 1#ǎ 8@9TC}W^mO)'8'ys "^ ʒ`bȩ Z >99 VI6 IF gD?\>R..0 lنPW&(]Ck gPY:X6NU 6g*OL [z s|Ib@2ԑ?)f.QM;!M9Gn‹NAK/4eF_9^@$@_4h.r¤F-;;i/AFwCđIѷ9fd:}Fvqk y$ѽ6qKj{ [j?#wcEA}ر$B._~-' gl#ȳ ڐ}ȏUnwG>hoϤlQg9 Es+RCSYtXgkdH$P3qP=ͥ^H(hlH<]8w&q~ӿ߉.,Ȏ]cuv-MvCXr g E?g6#ZqQ;xgc}a iw{5 Ὠr&Q?/}C*J=]Vjߎ7 ?ywݳK=V89%pm7}b?hDp@߽}y %y)?$;c nT]y75`TXd4Eǎޚ&)!x yv&SM1-bG,w~H G398\Plk~N0O~ߣSIR&OP43Fdc}ŋrOR j5Ih@6tqٸvʮnՑ&fiոѱK/%+Ὦ_jGYS[HŦB@"l2w/Zӽ?ǒi1(_a ^+3p|jZNwrSkUm&MYRDkx Iώg>aؿ''qn'M3|,~t~5^0"'LMOI~˄cilk *}3]L`+LV9J=eq{ ** b7>O|﮹:d|5|3;LLbG9.tI5{>Nt\ُ Ug/6=XL>+-ė8^\?M KN_G ݠ d/P%^ؕd"eȞM-ށFTq:ذ{лkG.Yi7 \dYJ}G)yo9#kcKPGt`e _?7=D"];xe8ؼ\Hkl7#m9WNK"ޣ>2X yǽ~Vn`X{3^_𦶅S<1e Cl.jeQ`Gv3A]_ZM|u~$Y/gtrwLgQ8lxϹ"/|x|%j9~dmy{ST척yochk^ؾ?wh^QMDž t &< 1@y )<Ѝ+/ @كq\+f_:+O6tr.3 `u#{2ӹ1P *$P:]65F#^tW(* jdrS;*: ^ N \yL|+ hjm8u<Ѡ@{7+ب춲I0]QIy/X{ 㤵)F)ZFg H?'0.+G㹞gaڻt杷q NK)U8JG:p=WcV-,q.<wnzӊ]^!~LX%UiZ)bE&k/sBP{\reťPkf΂:f : L>& DP ^*dm88*xx/A"6VRi]sҧoNfg[gGOQbA )y :o:vg8ci' 9ɲ6pL;)83r SMZYU:Opv V: 7s0FF%Ǡ4 x ^"8s_yMqNέp[mZ;W|wRE|TA7۠c7 Z3M\yÊawj;idSK ({+/ Yxy EkJ8ߎ /E@`8g@ >dJ1C`K \i}c1h$m҈.t!~{@@&ͪOlX<ϵɜ.$\wm>nan:+Aa]r puX!-.g;dlmptXla9|VEF"u<Uy5z8OãФPr"pCE:eJn'{ͧϳ}{&Z27`E\qU   gRGX|bѪzZ"2\M+u&@uWCA^iPH7voJ,ܛ!!mKԤ!W/A_e@fɂ=vtyk93I 0ucS4/eф cwh%)/C׮I2NŦ24|f TJ]AH>C5WufC2AsT'p/ldy$ IȻL辇>-Չu$OL҆1HKb@Id5N FʊDQϠa+/<;ž^\h%DYݗHRLE5 4^#Z[N?vX$HJ<7a9q2@*`@@֏{,d *ncf!v>bܻdWt&lD{; jkA)[(nY9.?Ю`eu)$eE{'x@-c;DB6=&[V#?x[ߧ#Kս?݃IcoA,|?ÑL>ɧIS5{{gc+\ bݼr+@.LrP B\"l63GmpeD }`;&OppyE2d?oD)؂٫c/L=0LK[K$)IS/rdGdY=k؁k9~Mxm#=_L/F8~⥋16;h&٧UN Coe59 5*%crLBcmWX-sfr%zuuQnuGsA']]t9 8݁.fek-Mw& XEa_'_*'~^uo=~qWn_ZHEgi@or@cS;v~9פު_񣵑D'3x#ݗMt!CvQ`#b/u$]bvj`e߄x/sO@ꨚ2\=%O7eGY >e0{\{:<| |5Ϲ66]v18:ݡeH5yR_'Zcsi,WHra1t *tHeS{Žh46:Xo1ҋqHэP ɤ y_{gtF`K$*4FTos.BX$1Mi ew?~mVYwF*vť52& U< ﮯF:dQ: ~B"lU9Wө%mO{ta)*[­4ORJR M82!g" ~ܶ\o2c݈5W/@wi/gX`b·vv_p2Rw>u\{e^>#jc;? gDfb&o g(&wfz#}se1Y#괇c |n$m3%kWB2i |w^ v,PյD_QEڷkc`0dqǽe:612k{t@39i2PZA/1 iiD=z}z~<}Wy )@npɲAJ ^{&ødȭ>ggߛOB̤0t3jK8i$s9Nd  s~dUvhYŞkAe`!u[#s)s-Z gA]=>gxnzN݌7;5z ij<tva@IDAT/cݟ~W-M\jJl?\~ DGF- BL;G ǵٙB@`"[i_s2/I*jqDV'U+bM$'s,8 uMUwes':%\'Hk@@Ac>Zg{/`0ׇ<QPf7`]3g.EdYα#݅8򉀠ޞK 0-#%V z \k$/D%6cc 'c DUgM'SHXo8Yv2 :H}| FpN'ʎ`]"V,q+]jD}YV3$J̱}tL(>3ؙLxK2bcs]7 ÚPxsT &p[ H\yW ,-7vZ9if]q*UKE`s4wR4c$ʞϧ.'<_&PvG9 s#[\PRߪ ؘ SIѪAk; 䩻;`? ܯۻ{Amx>μ΂b@*xQe;_A'=#A5m6nQΠU2:}t6l 8: +/ؼ}"ez >zYg4%k?jٕH<Ɏy6轞Ak !"=τmf;iMPYKжDvgV>ۣgcQ׹qӊk"8x KO:\0׊fu[ص%M]p.k#%8.t1R-1X}sXn`J}O۩#X.}KNce"IۀVسWJfc#lVWުr-gZ;4r[VߥRM݋'5R6w ]moRwo)$iX6vrt@ 1NֽEpbcAuxu~e[[cQd1CӡST W/?>L[[\5Ifʂ{C߻V$no][y\IŴf@t>=Ο;/#=yVF_`D)$!DsHʄԝڵe2zIr߁gY:!m|GF%Vv-]nOO~FqzgC8n6Z/w5뤲 A唥=.4YY$5hRÃ|:faDwh%666#t"ХxUpJ֟:|-zz]&| ̤ע#IAs9F'/ά-3ʊGi@{tFX&&18AQ}eUw#X%̭_V-뚗m|6;ګi ^b&Ё ՟޷kt1A ?{cWG Ao幘gy4Tǭ(&~wq}/!_ &՚tYE ~ki$ب\cpLZ5Vhj$Kf}Z CѬLO1D';[ Oݚ@Z1 FF"XimB6t(:tacIwJ^p5<×WL̷]}L/ժ }%ϋ+Ķͥ$$GtY^equ {F _7(ԁլìuMJ]ASu8YIq2{iAH"w.$!;aX# To_23|F:!7Mz6xd$exHҪ~:Қ10CqiFt; #Hn;Y(ff۾TU,{oຉNآr5е,`6"A/#[]ܔm@6t.I&{*zme}'#tz; L^0IYĎڀcSp"իH9~c#P"Ks--5U嵂7SBX;,@n0gon8-:qz<{^_hWd&.c?Mt*g0vq4hv2Oeezp@9gG1*W_ǜW|^nl=}Yvtۇ{Ռ1|Wy )@fj?9-FYU j"#^h+O6:7A U1iƅASJ`Ͽ ]~Ƚ5^1+;A#f Op=Ɏaΰ@ΥNcۘړp3m/gt; Ch uR~R`\ܳnߟ}I-*:48bު]e+[@E[zނ6?X }8s{*+ äv * `Ÿ b[sɻ#اN2޿@ޣ##%wgGզ΢x֨jbw= Z^ #1\{1btՑ4;_ضc:Ǭ.nbNTuB ,t'HB9mya7u?PԶ[%" ?8VdN` ιKE9S~55@U8Lۮu#79WdU wg.WZ k(+er4'}>lw0)+OM ]*GcΠUN:K+ST@Rmu_'ҁCo?C`Z@3P?&<;̀u_}:~yW`oUUKT'`z 9DYe~B>M=7WRZZym9O6Vlnu.'i߶܌d[eȏ:: Y~KU'`nH{흩vCB]xb M]yeq!W%8p#v $ΘAom)&qGA9[Ț#0V^,Bb ,bXdc5uF%I&~vQ]5&z<7@e "F^l ZֶvV86+ y*dgVtt^mHpQfm{`}Im$di>x-:=<A^$j:6֮nւ6Фo'F}ŗoB[H :H ! Vaė}}$'{;4_b'@QkrA;4&b&`wy<:_&$a@#;e#wkUTymyZy62_!do^ڵ ,x믽 mT!%\m]F]]B{/0#h fMq IU)ql!#،i%8Y_ [,F'|&Pu{g*vEk=~]xLE2 {tVݧ==J\Af|z6&1Hzt\&,//\ԭOYzDP]?Cr«{)?r$X9rZO#Yt1h;@IT fR 2OS3tLNg^/Mw:m/cBzcs#^Jl<9v3Vؼ]PM\Kc7߽@Uw:/']|ӝ:;+O/}i%1uoܟ\h>_7qϰE-g7DMS:%-1 3W$s?>K%e?^"5rrck.l}~ԽܜkɜXOMb1#.Z``r3aa'w:Cdzi %긙;2 vMZ c QkYr۳)CV[CRV.8—{;MwIr)$)c!-snq3J2.{mB%렮Q^Hڵ+IX4 6ϭA, WW6"x^PHlb{dV`YsG'vm:&wqL A~0k |=YL?\-`Z>z6SLFu F{\|w yr +bY%[K).v&{Cǿ8~/RXM[]C}/6v|{2?o㯷4޴8@&$T,|q!-=VD#|Q6M5x/Usݢ܃cfL+ҵ=&*9d+O<SS %lj 7hf*ڣ6(u56r~hy `@`d@V k,@T6ёt)J+rUhvz^tvt3'--)=/`5j{J9ul/cnuF&c,*:̌ _h2Uݶif }u4䧣UtiK7Db:}= 4R[)[o*vZkLaKM2=q+/SE|Ng[6l:t0L7A6qYpPiYMP)^g 0R `Ffs^cѳEO8,fZEl9,]3UH>@Ktx+F`0%[vt20`{Lᚱ`=@izںMG~:*qXuH*.\ߜ FD>K t HjjiCUV{zOy2mh.pb\ifVsTWY2BaR 4lM7Fo3m1lR  4 `ʨ?̐>ޠ{ $6/1*[̄I=!@P' /p˝4h yoí)_[%_ԇ_LNU|S, ﹯ed[ùV|K៥j աӴ/xX蔊JƁ 6V z ۚvE=/!gA5yҵVKiY{h[wl'nեduҖ ~=mo[0tg׫ssU~3$ewftc\ Dq#q$4uvb`CIVw~N&eaKs_%y$UOT!!+Ttsb4]p&MυXu3יȊж K[4Zk1AOe5Vّ Glm/uc;GAأy ^Jų]=~]au$/Vk+dAKJ3muH%PǦnW]݈E:6׮|NdZy{ROWs1WN<. Uk2wį|=zdĈWi2#5֤N}7ZJABLsVL3m(=*^ \L?L6h dzv ㏟5A q?Htt_ӌ_/$D13g+JxG(`GkȟhkQEPtYd(d@B~Uٔ^2@%syٵ~*͎:?e멧~dGgٻaq~g5IǡHAw5W3d7k29K̝ tzN1DvTR/8km=ߤ4y*{K:6/rlAk GK_FC9dTiPn>/ ,YG[ؠ$t-l$p8br_REBx˵֨/~!HS{=@{;fvz'rg†soRWI;P.$vY฾-$ q[tB"Y@6H)3g=KY1eڽy2t") Ԓ`B`S}k;=WLs~(#{_@Ym={ V2sA!rU-T7~}/w9B )MaTGL9>Gx FZ^yJv&HT{b?NwǙ`/dp?vx1%ɋ94g.eu_q:pS#IUH rVJphQ؃:X#{nrMkaOs[Y/ ׾e_7)KϿ-I&aJ~<.&E򹹫s1eլiQkܺ]O GǦc_ Xh^f"@$m[ؗvT_Q8y&!lؽdy((ԏd% QS~yݽ@^g*P/D|YeD7숇$|CדyTJq=a$;ouzaPӟX- t<}H_n~m8n$yFfcLx)gO_@0WmLB:찗^|&`=F%{{St$ F<ك%A&|R,Nw? HGDŽ0Ni꾻g=]TDbv^%K&= <ƼMrQg[‡c, LؕV|"ή.'Ѯz+$&xĵ!pqe%$ c q6k L<`_ *f*`mY;'@OXmE1e%2:7[e⡾|1&C| h,[b2|:^[)*UMp?dx<_[S:8(aYA7҅qloJx(_I̢ >'6Ax_~1)D _ .1tS O<S'4pMMqdlS͞333ƢWSo]^`wuV44?^Dlfa"QK[a~H[?k)˸OKq`B%9~xy[D|+!‟ v~*#>#UWΜ6c7p|^| Ë8i"gylc;C8=}}]xX} 2ٖζǏ̲VV=/b` P`t.Ǒߴ)Ь@'^BmFӂIM+:謭mR ȞL/%q7oլ `@0kX~7qM1+/V넕7Gg0TYg2s ̐' ' gRRZHy@*mp f(n< wʜǒx: I*Vk'@£֙w3 \7)xh7[[O2X 4#Јg7p~c5k1, Vz4 ^؁uVYfm*̕Q۹Ud]Jau_h<}3{lȣz5'l:vU ٩]0AQLO<κs>'M|*Mutf-_pg 2jծeM_X.ʏ#g`"4I/ 8:&T xS~' Clp^9 ^eMq.b~Cw v?M.-ʀgW_ҁTbo=)HOȕxd#'Sowgʬ0õ50ParG zԈm[SKSe}!*lWU4?,7 d1? \Zo `Retw@T+ޤ{^*]~& nZTm-^Nn@_`+v;p L3{B0C^ $ > < 4ٹ=+g/?+ Sͱ663~O3C?I֬E]8FĶPW[+=uD^8s d hl@i;uo$lB_+H΄{f5pyL8,V/d4yb q"ǭK#z-p ,o0Q`T}8m keŶ+Jm$8Ҥ}V6DupXSN]*"=܇v3ёT:^JZ{/ekll,@L 7:;{M&W'?OIOs-nBk$ sɭ>zࡒlWgW'v+Tœkpw.b,X_ Zg\7n4BP!X19/Kgy+ݜHK_HM1;UӖ9bcC3[ TJ 7Η& b*k&&#l`DD&z}$3(W_x3Ux{2c :dӹ~4C"[& x:IۂsDf;#BjؾV>5zWFbst] ̣k:|$xLZy]ùgxL^u, C<3hO 3_|G E~2ڑe{ޘLTֲK *|^]ʅ~rw`{NT0Ef<\ecM?:OPons/'!r rxyv.`z=mOG{3=@\jX\( II4,Z^d (O*,wu>ң XZIff/s;Vzv;DL[H o{i%IPL־t^kYk ;~J2}}gon y'a8( @N?Jȍ].l6W Ϫo$`aJ9Zbda|EϽ$qcVxn1zD؄ 騮TwM8g-ÙֲI )M!_y ))ѩQ7G;S@MZ?Y5K4=9A 7v?iS@Ѱ՘ Ww5P8X{Wq<^V%jEE8Yq^::\L^ZBH`YyU_:]]igj|c[~;tj}$qaTK/iH;lTB/ kt` ,`) ]&V9gO,< /A> HUXڐoJ8+u^|ؿ/@L:g]!K0m+W:::*V},b-BQdrcC<+@Z:Q_So𕆶\4(c@u@@4k`2]I<-oNS W/'Xy$kiN;=%)+Ƃ/ 6:e>X,H[A_!;z`!Р Ydxf7X4/sl,sP gm g%*=wl)mz 0bfD+mxs&GGqkߊ뀁_ {V~*r>nl@ (crW| O"1G1ܖ.\;¹3gMMw Kh/[WJhPwNྠe5ri=t XyȾLUAE@#xeTL}*k<{`1-mf <"Lra;*a *ydu`(/:0p9:)pnh`= `@ۀCEmϳݬt #pecM6c<:.QʹsCaz>1x5ԇZ: d l keAD+e' &yT4YYm_y^ZSIr|(73_ڂSYl񉘏2)K r/Z} ZȈ"@^m+JIBh)$#'#h!/X`h+ن65ͭӰ]*㈁-ùN3La2L u;[jR*wJev%TrPnKo>K:;; ,1HJӇ7r9W~gGVKwGIi/\4(>OÀ{vq+=q_=w΀ތ4Jk,/ IYdC5-+]1M6TV LMcMEGPj˯:IȨ|>A\"vjmW,Ї9W"Q@4Hh$|F˂!TqF0n$S`/ʂ& ,rc&v e.RaW2)J>\όػY;((V+}& qFV` +-- so k$@ǨDǙ$)`ȂSS7V*_1kd}Ȝ|]WojYu{ 6reS!2l!CزA2/A{&yȅ{#`.p|>݀W nK_㕗|◉~&u_|-8q,a@T%7wVUW gz4 Dd-SLz2RG hyx-|(*UQV/,<NKK|V$X[ۗN=p$7vɵw0X;)*{ǔ] 2=7?EP?`q1(\ K!z7>:- U#T ik 3Zutc}Q/a02(VSDÕKBVZ }h/1ʏ'&bt[_l7]ev~28e oknn5P{_&#̓8RKPo3VROS\ tH~NSHa||Mi&Y'΂򬌏]?Fi/u6YWog/[&L,dm}2Mg8[Zlڹ!=L9˄ɡsSϑbKK+ti}3?Yۮ<&4 g I&#L/\,A^ /Foh.͝ ʞ Lq(s/g/oK}}?$dI!D|^{G <& ȑ)3Ig,s|ͦ }<.oj6tdN&LZnH=Ag2qVϰ1Tx >cLZ3 yCι؉ ?=ģ-xxby ="mՇ|$ ~ \kkľ!kv݁"ßF)>M\`Υ ,?zP88q-Mh׷͝yĎ`|4poнƦ?&?ijiinGw؏/VLbfRY!jpzJN[IUnLΕqT Fg ;ȗr+ݼv d n ldY9)_”SGS 8i0YjX &h5C#'ƈMg9 { f7aK_5彨nQ5 {(OUl<:B:>'A<ﴎ @r.J{bU5.d\QG2G 4k5 &uD~3@IDATv چ,'S5瞫9A:If ovA |YCѪUL|ϗti&8cuĹc[Erchۆr9^%@`N@u1/ُDOܮfWhgVSC>j5 ]/dǍG 4 x܀ATA'<sU#1Sȫs @1&|3v8|XA*VA:Al3e.nۏLHh7::t=lvqMq_dk$l"Oѻ8>cֽ8Kj:T˴L?[R[پ cbrNu[%}$o|s7*r]hiEBugJ[*csF 5>|`Ah `ڛ 4n!i9ʽ2@>9@fK,FϮn6SK^<݁6Y;g*b̃&Yur'ڵyW3CFSrۼ+=o@--\1pJòTt> @i۽ fWVIbC yt>ws ij6Cez3f"泳lPOOO L1g~=VMT/[,"Nq OAC%A$8hZ5R""REs{/猖kXX`r̼s*T?Q1`g > q,3 RP¡= 2Ond@F/}tBxOg sTa#yAKNe>dM̦|:r}}㽼R[d# 0=q&756Y{C%YtH/Ҁd.Y0tG02I0Ya+8f 6Б'^D&{%YjoHYOkNҋrZr69[ҕn#Wב!cxE/ޓv6%W 2sqϔQ3uw'V.IӭJby[jc~C:Bo f=ut6&g?{/Z7cd--h(ahp36UT̂ pN7؛ڻ^%:%,յ7K&xQ̕8֜|˹^*h Vqf#tww3<Ǟc#b K c ~TFYIF: JknZ1K1~źKֶ\Og @35=5ܼlR -^]~J@g\r =V@arm# r͂YF΅Q+WzT~q x{߽~@Vŀ^Uy[kB ][>z0A)QFuٵ2Y:=eATpk &&f}hHd|ڊ[/K#3YϱCGW/c]'{hO dJSjpN_zI4ބ\1hTT~X23ubPJ ѥٵg33iXoaU33\o l h qYyvH|09eG޻w(m:FSc[KT^݄On|y&J=3d0=?9z@|FRΩ0no_\= `Иm2|%}5!C&7wyjDg9QҍdEƶfҞ 2xvKc+ߗk;OU =#|[0y,%Hhmo%ƱIf$L7B{lUsϧ^cLjjךe p`jZZ~b>ǜ-uѴ@ \[XXaYol"i | \\DT9} N0H8,TɧO혤)+d%M3?/gjSk)ǑHT҇/ >D4נ/+p~po{e 5s824,r/dg /X7\%TWx Ѣpypf0?5dӟhsŖ}˵HB(o;6+z` KΛB-P_d>Kq.C ή&zQڲd-`ѡRדqg#@#j};Z8Z+Ѭ J3-7W)W[[ݾ(:?ᣎ~+pljp1ty-)Ǹo}/u׼sBᥢ?qSp_~FC˲]RzO^Lıs(fPnni0LǵgːYnjK#K T#IDaI @i)i eھ?L(Ͽvl=,g>ϳkHL.=B8DG`#(kGd7˰LeWHYhF%8v?QF(́ 028(4ߎ y200ð ##ĩӑWcrf_#}2_w'?)>_K1KY28(BZmN'˫߿q:Nܺ{i j`I/C  qXr1d6D 88ua ?0ܰI#V|0bX}54k:W2(h1ƕ1ttTҒ&؋M){;6n8tlX Q :aQj\Ǿ8.\x#1ݸ~]+@{^{=2^ØׯF@ r~/ ՝[H{.h<:(lĵFoa,4ԣ#8p@kQR=0Ӫ>FU`\{uFRϱ7]-=EM^mPR%j  é95E0wj3]]c4Ïsʹ`x2gA.;&x /{N2Foo m^\4g^ݮB'%.Y +A:mYUlmE a|*(RglA s?ȥj*ks%;M.؄qpn"q*:J >& tH/c{-?ea=flSx`e8\3:i czWR->Q},tg>^KOJg:le`Oi^x.p -,xUUy:9hlę6{̣gܽ7g(Khdϐ qmw8 g@0 _~"x`)ڪ)C^|V`rkVy?\/E+ |t7WRKkcgzA^Ǚ @"u4:ߦ_y Ul3 ѫ?))SU ޭYk{7 Ν=Pi"G3 M?b?{?Ν@iΫ_&\鮅Ooٹ45OI5yWCu k"; R&u|( 溴8!Xj%`9A456) k?G$@̗_x%+1}w౻rCS['U'gK@E_BԛM bfraM|r3~ޜN/]n"gz:v,Y~XE3t:) a@]7L07O)5t6H3-aW?}/puF4էٟ? 2ՙ_ыʶNgұcDZA$Gݣz Nu2^<pb<۪(] |""09w_\&OCDegLiuB. pQk[T];xo4{wcC䰎E[lK b"m{79ijVۂV<mȰtIlN)؜JO;y5@3RƷ]YZ߲Eu H8 r@\̠4DP U5 7"#̕uZ8u:o|,ga#c YU)Ml3Vn%/CkjrmWߋ M+nyj |~uV}?SNMaq %8~8O' suF^ "53`,2eΠ6e뵭 0 nU)XUO.0ЌpyknZ#mYGPM)Ύ 1΂&IB`%5+ yJ%+ծr@+,ɳqAo=< &.~}k T4@q--k`RHU9;^S4c3@ZgOJ RiU/2Ọ s &fm@1[ yTFP"||kJ#ʪ e0Lz'1@pK+s*#8 wBv:Q*jQalj:Jܿum-t,e@ԥGŐE t%:kC.t37{,]z݈_'}֖а/)ndk$ C<@@}KSdpp@hBь:M%ێ`Ę%p4l&h:'w{'y' _ .!Tc>>%i}n·}GE9cBTvĠC\/0^Tu,*%="J_LAz |af3-#qTp6 0 r cgSnfN2퇬(x'k}cPyWWqZMצY@I.{*xԞt$oڀ- fA[5Lm&0(p V] 4D8N4)0cZjvbgxSd Ҕ4KOGEG:}~멵˲[̑g5hh{<ǚS >VғO?K뎐_ ) ius S2$]f2&F h90.teEۙ.|)?3=\-I YcYG`A1ڊ fH%3Mi8=kWe7ϚskϬDy_yCq&| Ǥ<o/~ZF h'2*үYPS8?{Ԁ\҄L,#ʠ5|cC3Ͳ :Yuϳ-LW2AsIy[\;jG9g(3</yUڛK=z}c ǧ]N#ϒ.Ypi,0M-Cg|y2k23| dA՜džp 2@Ք:1S{ :e=Cxvg]9ͺؗnzh5_^yoЙ_W3:Yȣ/sw]@ &5Xm XwָF&"J2͎w_/v,32y0*uX[i uVWf{k/VMQ'~׮cd2wp*񽹶8;KP_!y+~E`s-ib8dtt`G_XB*(ij[輶=}Vy~s/=]|^xI?я"{==RGK=@u'p 1^2Z|/π½Zx8T_RQ)8fLm2̀W7y1dNzWs=FGtdb?qЯKe$6lG^ 9nmd{o|@*??~eu8,,@#bFv@Ӹ: -{yo UWOI~H5t)C':#nhJg:'O³;o l[sg˩`T\LkU|K&dž8ngw7i$!ϴ-;Pz?&ѐXFVhSmʉHHÓi[~f~ʛ=CTg/w ̲/E?t1=bo~+uJzc+W)]o9yK˸O`eѭ qv/)TCW9-BF`1/T$SEVco~*͞4ykk"N[oཇ 9u%C򇽽c\%ϋA6;yukeo{okT5mޜ2[Pـ&0iRʶʫ6, n`d4ØXVξygڏfzX3(- Rguo;[y}g# v12CнԾ ި?B+[Ww(܌K~g`es֩WKI $}dgB2!C).ip-_x]櫛9ʽ!YP݌9`~+BTb0>5=$҈MVLk[yXb:슚hnnm/gxsV{y4j1ʪ[;K x4-['L:ÕO?M 0dE0+Eˁx)SlWJF}X{n!aس mx5J:GI ҃o}ޣQ:: k Ry{9X2bJ1fu@ge.8viX=սg 89.EL3~H<εH9b`.<N̸|ZyA7LC->,δ\K[Jǜ#x SiCIw UV[PDfbi;-R9a}$^D 9mX]Y{셾YkϲVr2VU?݃gN'ͳȅXVh>rhV+ ^% $r?}e2OXŰ(SsK%TRy>1qL3+ݦAR(aoC3>9>HUw:U\T Ut=?2oټ^.yqKZ{~'0|8{) ܽr'87qTn֌kfwBjdkOak?IW(aϫi:$z}c "ZFG'q /w~77ETٞ,Pe 1jCg}c5MJ͘7 H?=[O`䧞n z˳.k?OC]z]Xtq]Jh3(,#`9-OKH|JSiy`YH89q@92ЕGq ӓ#d繻 EJuϧ4OPtGSGwO\xIYiDToZems&*\ #3ye UAs˔NkjNl<eedQ˷~y!2Cǔ~ -gYݗ:qa RBF:<3:*j)54H'AW Ze]uI!Oچt8-2yZ?v=MXZɆg$-/ң dm6 &.^br,:FƣQ܎Z&뜣\ 8z)wp{\2~g p0!gnR*ӉЖOo@+ x~)yg~ɓ*-SSHvh*ØՉ`zEu@-rqv:g!?S@'*t>[F5Y}}>kYV"uֹ @^yՔH23ޗ}4=&zvw:cm GA1[jXJ5y5|ƚ@TPɼ?1qSUne*y,=%6Tr[s-9S_oE;wsf3u2:{"xFޙjNoTs7Ktt W=2[JDKdhzn7K ޾q-_*1cf >m%7qLSThRlb4.eͬ G>5Vq~WS}Sϥ`,i{XBw)-7pIpX[^Oϟgy 5XR*`H81"xSrfS~[pklju-r_z@N)s/~$[$i3y)mmXd>7ϕd/tA^ \&axYNSy%,:"n HNw7?Uz3grb,i/Aq8#A[rΐ) xr*89H%P+땮M>rm㷺KȋkM>}xE:y 3ޛ'&O/]NǼ>vv(񘱷H{3AՀP:VVҘ$ "T4T!=Չ}5-#^s)c"Ҏq؂ ?A?p4hJL8-nVr&dɥkAyh6<:g꘬aJ'SW<@/1z,U k Y"6k9u'?}UA~c"h_R .]8{<JV,'4v G2^yg-YڱdB%C:OfS6>rK3\ {(RA˛ 賐YƄͳ=G٢mNf2OƠCZl3W[‡C}2I9S^gt c0 *!y`Z[G 4qJ^;N)zk)=+)3_])mӢbn)vxgs8L<_B]!2 Ҋbz9) dgk?2+pA݌NF T=wg;R@e9^t; CJ&9I3sK&l-TZ'T`;71S5W[>LU p̗<^1s/_ҷɟHy,cT@-yt%y T P`嘌׺3 K}^584@esZt 2DV:wGyY :VAw,HU 4.\@6S[.Txmn: hb{rce}hTRM`Wg:M|᫰RO<2P{Y3?PmlGe>>#+RUd[5suȝV822}N}uZvx-v-8Q.ʓd#/_l,̠ki'tϏz[סa@O|G-ґ7wQqwQ~IUWx2+?5TQq3C-:> ᥽BA/:\#0[W*xjS5kVeo 2-ػn08Yh5]G+p*h:,Qfvp/OId,d7a\-AN*k:c5^t:^A/]9_M#Utjrߺ@e4G}ݷ){u"@*p`JۇR JrtK{Y uyqid5 0!TELѱ-q)79F\85q5vNJDOrNvS=P1x@V}q B0QS#~22o9,W@o:hUGR񁥣pPHI9A6R@o!uJb7˖{/7ʴ>zy3YCifl /MikUCD0L4!q~XԸO} #ql~%0w3uW䤞'JoؚROF(_e:= Ɗ >N?4SOzd@980/R =oֶvh^̯JG~.?B4|ܝ }5鳃sI}Kc1/a=ӑZ!a$ ꢧ.y8!X{ef~=3o}U.^z(3gRf;c+83^ 843a^e]s7#@B'jy1q`T|jhK3ЃƤ=4FW:Je*8< 0`V5+h篎`WB_?,0c\?zJyRWLޮ?31q{dsuaϞmdЁilPە8\ P]@@1tot7>"W)v:(jҀ:`{3/pP=u\fyw˪BN.뾜{XWG_fH6PĬvA*A'u=tz޲}ܒ;/{A3>!͖^z1rufd_v= @_هDV ?;n ̶ s/V|Cak.l7oXP KXv!# iBt2,uN ZBZȦ -.ٵ 8ml0e2Ƚw9l1a3Ys-Ɉҁ+ }8sҝ<74e lmZ mEU7rlbi咼K5@+=Q}' 009K aFsGoġ%'@[]%ؐDY3 wų6/^m7*-ljy.w],>m1NA%^l*"` =Ax):7],+"}4cy,PK>2cα|>2vk幅Ix/!ˬ}X+]5#8(f.{ XF1=2(U 8@ 8=c;e'E2=lsW! 6Y#EuV]HPVyA=873#aS>wm 8qMKS=繌5W\Zu+j)ݛ8#(q{>s8HC)=G)*w.ce"Axk>ϱe:!aMGeƱ˽PNz▨>p_ZAqP 1ŮsG+pG+ptfEysܹ@)WAwA"QΰP$股Y^ά17I8DTn-#Pg_W K<U2j (- pA!q҈hՀFfU\~iPRms;L7{;sj3-SE;fTPɬP~ًCQ~άq{[\cе lNmYVqc(%@gb[NTDeoUj}Q>s^'^O>L8ѫ{3 ͱW;80Z&P8kK1 j@UCccI@o0pVoQ7`dh;2ۉB}D & jZ 3##3:Cp@-i0N͐VJ7`_SRX]orddhoN SDWӅ+Iol Gqv߼gLPXCH=!kYy3N¡3C[m{c3?u:JWq>R.pjާ8-UXB%\#lre4&p40EGgxiok{țCq}v#kp?sDIM–ӭ!yؒm:sKq".R꿜 2 KV}55 K˷͑ 3/htx=]KhQܝeK(c~l._:,Ga\>ɥ<LJ?X~m1m8L6 k{ GSa} p˺:8sy^tU{qR>scOmkYMȗ.:]q葁]8<5E:@4a?^=03C]gad$s\f~N,@J{nF:L V "8-oSX=spmm͇Fn,0Wmk:>2&߃'x77ޡ~ejO/-856LIG"y"hikBn P0,1f UL'=_f6/5q&kCR:j)nKT Nti@%`[[ovjj2=30룔`v?/W峭Ds{w7[s6ӡ^;zO p9u>i@G.K4#`\/7kV"[A9r G,3՟zz{9~yu/3YMŒ.ӖV7\e8Ygc84 ڻߕ:ǕOϖ3f53^"NۛSk{{u\ ?^'? esp:^\0w] eusqrfmXIE 6ޖM̦g?{c}fBBN)_Μ=079Kt.R9Yr߶ <$L"*1qś/KY0 {FFn),%f)oANiXi߃,2E1SRf&{]Ynry(bJW1_ 7~8cНHгdꄂfqVwu>3>Co[FQLdS)3)o?ǮCP |!3@h I 7˚c {WVY_2kg9Pb NtR. h`: <^m_^u5`Ksz  p^;Dp9j<0q-AF8VJ `I6u0`?Oum@=)r3~;4cm@ 2iJ۹Q2 `Y`MtiK(ڔִAwY=UBCluv60q=ڭ!{x>ڞswuJ?{NMr2t-b_ {H,R͖織 @ؕ7S铑Ih$G^h?N]XF;:C6( 6Tk?dF<i\T5Ԗ-!`nAonPsF+Ǘ#;zhDuY[0uun66ײ}7.2BvW>m+r7ɽ=g1g/=\`?_E%qB[D@p@}i; #u{`I3I @BO-  x+=KRkCej@3 ^Į/Q}G 6A.3S(al&0,*4ޢnh.GYSx&"4fs֖W &JŠ\>O+0x8ʁ iMl} ڰgAX/,UZس6CsE\2cԖXkW+u:yʒ:,GY>]0R=%ztcILX1P Fe/|*f!_r7 0_*Ap[~޳kθp)[7fy~j)5mBlwmSIL}E&,|g?rrQY}Ogmaie#DȠs=qb#8]yfA K' WqZɭ4FО~Pn~?R҆,r085ct iP,sCB4p@&lL?q屧<Ϲ(xsp/HaD̷Qz J ~xE`sŝ-Fohzqlݛtq.]Jʴyf&(kϢ߸7LҋJk s8Cop**8sOG?Z~QsG+pG+pp\dYDuVXpBGγc'3g:$K:Z+ܖ-`.4nJ`:=ZO``#uHALSgL88xC>\N8aXU4uB˿j Ŗ2 1'{(5ױ_|Xde8R߻oaګ4sx';,QROO9=qxR^[[nu 8GFFR=.<8 8VҕgӇccTU[y5O0qcQfRi矽YNJ0cif?ڻ{q"QOsɶ/,=˫pH|YUB2"1|z`hϓUUOu E=t/%1{&Q4д3DΜ"D4=X{&G*0䠁A -D!hlK|AdЬ^L^ CWR3T .%,P u N@anǰ,lH7k{_ pEGd`=ʚNA#^{'~h>d%o_]o_5F;p}oCh'ϥ[aþ淪cpvgQ:%tT(Ct|B]e1gjXtl$&  &hA"e~x1'tl:7>^: utΌ4YNpsyo)g ~碣^kgMȺ)YyeI;mlx$u_oR*:|V;l+yٴs_|@rK"q}_tr.*_X %*%g?M׮N"w(ŮSҗ_t>ÿ PpfsoN諈`2>~:wl!(-IU#uVPv57_·sss} Q ύq>>/>G} 0/*)5(}љ]3]No4sDx!ͪ g2#(Qk1S>cc+Qvt):sp  @[R6A5℞M#&/fI薄O?As)֠&\c|xCWgѹ؆.`_3:#np^,-|{s Xdn}tpd zV1c3zCf &׳7<8A +?\ &p9xxs*JDb],JUL%6ꈮKU,ih GzOνXڪFc)cl1:mF\K[;YdRC~G1пvidgMwJf;̚ɝ 2%X$O>#] ZJ5<29ȂS3-^uʆDH㹬k6CPql3:.ssћ pDұV#1@+"t{\ueӱl왠_."q @e[8p 8ANAHWt_ُyT3yPpd37(3;s xy|:ny-xʫS[I xz^tsFG | QxX16v분mZLVT6yCރ4Tn뽢\54^/҈zmGr8+]#)HY~y1&m]f=K[5<mVfOQf q/={U6<3#qy|ki 1vl[nH^W='eRJ[ {{ssxNLxblڄ6p?92C^/_\&~%˽x#dIҨA߼ qꎂ҇qJnq^Yp5f:8_ VP.:m_Az@ܟ-q5_`fЌ~:A͍TiȫJXǘZpY?Yъw>ꑞ;y6᜛l{m=oP۠o/Vr2hC=0 k.Bkc_ AZfH;'FL-XgA)+ $]r1kskJP "{9Gʖ+:s^ 8) ';[HYրmavwhY:eڥ#؋mF0tp4]/ D k={qhЙ4s(7֨]{=Z8Z8ZO $/\-c99~(**uFs3^uV@NeX~LeXNo{#7;hBS96L@YETZcIτ? ?a/X3fV ~{~>~Q+DdOftu!ʫs,~On&%38o45kDb4gy^FI#^ bmm2'2,%#xU,Cҹ `0prutG4}nYý]0_RNr\o0z{ץqx0v,:wy=O?,rYZu\TZ9(㚯-TMtTK^ "cEd9Xw=ػZ[üo$ ޓg?A[{;A%kdx~,R>J@)}pi=}?d5Et%|w_Iqð87j֒Mh8&)18!A+!t{tǻC,o>x O]H>ɶ+1kt@> ̀W{гrװ\@TG"5ǧ|*ӌ#})Ӊ# >Kʳ.:HcsW*)ؚrKCW6g_3/櫡4yWgPw^|ln%+sKGZ4BI#Cq&:{mZ9sW_I([YY;J{@Y]Mm$ 8)&.v`m 7Ϫjqw畁տ:dJg%[ T›\g̜/U\y~6p.tqt>D!-+?E53ulF_j502X_1T7=NDn ?yr:ݻu+=t!{W?"otr!zTWzWY뱠/ 0mki$HL Yi0P쫭i\م텾`!Hl,NCienY:I3GY3,Pޯ:)!5޴85B;K |^B_8<-m uysV/Z`A<{I4i36}gLcGYȒ8wΣ#V@Rooc0*FZ`@VpҬ3@!dki䃶*_k3ж)䪼Ǡ 3ҥAlH>V.MͦU~3z=orP Ht'gY/&uG;e jV)G=W b`+ x\ (J]+xX NC堗p~ p]o4 ;u?"Oym w|E0,kרHK\G,Ae"q qRBrA[2##\?. g Ty,VX#3C>C0ㅏr,y/[} bl:x3϶1pۿJnskˉhU~ ns_" ,ogu<,C`ewH@X{5ϙ4rہsBQ`_@;|'&ʵҊ6|=ڮg`$gML~Cu1<ҋqbyG%! e>:>}`! vGnqNg9`&~>de0#X_ 36kI:a ]6QyW2Ȳ./#p1gǤ-!/Jp^&fߘkgZ=lzM97s~ -^d~V_B>/̧)>= d$ΓrߣhVhVhV *]?gJUYg|;c8lG kZL%xWY68D_{{Y (՞OPR )gaY9#"Tqx }=UX34~Y>*^ǘ e!ˏ>|>2-w΍t5gR(9i 2Zq ܿ,b NI.h JTC, 0P 0zmwfۀ:0B*CH v -O.>+=q1љW뽋ҵE[,L5k39^[# xav8dO @lz0oo`4=r=?u7XNKN#' тRdn: ūdfcഫ,X)L. |M~Kw=}-'Qf4h^Da KwkgL+ Z5}t$i괲\U l mŁb{n̜?襧zBYd)$dIoϤ=Wy3:,`h 4=Y&QǔevWS'ڟ@u@V`Ԕmq~f}zg9 &{a\L7ߜrg>AdON;o ;pwޗSJ?dCP0nk.5Uയ[/}k|vUu}85870,mtYpd}8 KK5$)VFydgkvnbnm8p*6ptrSC}q^HϫjɆ!a.?Z:$ XKB!St<˱G LK:kfQ81+i@{]eQi~>U5N+gz$?&jd/̥QUym\[xg\g[D/Wdncв& {SVBU|zZ]x"նւ CLj+Yp3:;YgI3B3X×/d|댲]EI9Y&իT԰$*]#MP*]FȮ YL&BfGvxϱ3) f/tv ʐ1 KYcSuC{C4r @O ccUưVr+S]Ua>Y lYZHu,{RAF[!΍N2ԟ(hQ8&bFklX]?p&5BƖv}CGVSEF|z_wWdttusN[.mmG|GOV8 uZwt=:2Tu=~w}yHC9]q8&[/dC 0ŀ@2[5ZԳkǽ̮*?n ӟ\&4`] AKuevS1PJ;udVךշ߫os4__,,=!)IibA=^e3xEƿk띭o'lf r0,+yss,byu*^A.&i"gW=GHo=^Jd_[:'b_ֆ SpxGh.ֽ{~)sRڕ=33Ɍ.m <tˑ+='?* pSuR@V_wV. ;w58;,<<:ƹ2%hC/zZe!rtA`9tbկeta,)#+zß2z @zw'2' zWӕQ 0^s') _IO<:2! ؓ4<25sz b%ۛ=JXL${kUP 4p^:afmrl(;>u'=s'>_W= 6ғǨŹW.]IS~9t_>ڸߔGe0 4Y]7L11:7grX$%c|M x]d; nm8{'ݚr!l e6A{kΪ9J+hg/nnBoeTE}? A?j́"R; }!HOVPJ|=tNKߝ tj/Si~:I>u$ghؒxjFNα ma&gtbl4 | (6A}4PD I}.c}o:z5vFo 6FV"SBs'ۅ0Y}>.{gT!yΜ@>c]00MO^eJ? g<g.wAwnKFǦU2[I;I?zUL,SlN %#i.K764ϕg3 Qg888z)kgt8axwϔv<4sރuFNg U_V\Fg*UtЛqrn}Am5RD/:5tyr05]t x1ƇY*yk\F]1`tkP1yWȧ:0;k%")}DX3HûhP=f{#RVYq:D45]#YG&h#x+ml50*ye #`gxl:LO=֛KBPz2{ KiHR<;sTW| g ?8 &:\Cx!uF'!d4=DηM2Y ƭwoBਙ?:XZ)R\"0܊hޣ;:ERn0 Aǭe̊M޷WfA`ch}<ۼoA Y46H'?; o8c> ^iFfHTdsiwކھzjoA:N1nn5q&r9\CGX3ʂΔeJ{y:!Ý5I92ʾ i}hPZ/Zp4`fNpr3ynU䂎Oעcm"cr("XSS8Ӊ><xs0"vzSg7/*x9$@p8zBa]m={2 h KB d9EgG (4z5[LDevC?KgAOV,>}F/`YF RfT{{ b 02K8 ơ4WTzn}z.?.Gp(7ٌL^H75$&Ҹ+K2&)(zDZ_Z\#2=q駞I˟`-0\|H熇K_Zuc'Υ"Sz[Ɣa m cd󀦭dK۫=D6mozůr>p|Oߌ a<7>l>&sX]N/~G>30$ 8! g5]K:T>':fd`a~WV#V<: bBvB_ߞŏ'K"t_'new2uU@ 733S[,(J~ Uvtvm+ 6&R3sl y) 2_Hz8>\Φvy _z<}7nf-@IDAThq*c0|2u6P[= fM셁Ad[ֲnVtZgfr/[_y_cۑTi_B -h>qwduu| b[ѿ`*@ nV؏X6I5˵R! P@.zqa͏}} +vvIrH=sf^2 8%wmN*|8_`zTF&wӍ1 AV<.18*OOsN@rm3řPo6hЀ䋗 }-UykO_?Oχx?\]uƿOw!33 ]E7=08BC41禌)tWߨ@+ 3kg*X!3Ф;OV><>|gr3K >?g(J;FFv'ggkipVEstMȌXllz矴<͊lTu)/3ր N&2nSЌ TгXQ>&qϨuϱXd:l{ʜ6Y=*Pwufg4C‚Uj,<fBaxƒxƲ`aߪ쨝k&ug2Y9檫ϪM 6|C7@4;N=N+CMj5Cg3aǙms?rkVϳO+ANߓ,(o};y* ?G;tVW'ҮFπ.sq8fF_mbkVݣ~RDcrnEU {܀.NI$:N5}# g y<!;~"L}a)_4Rm:@g-^g8(o@S^E*ϏqDY/UNLJQQ' g?% cԌphڱfik$li084!-lcjb'>PaCY CJs8tR:#¸yx(F YS'eU{ 5٧1 xe_t!~aȖpiXOhVNd9vvLsȀ̒.K:4noPjNpVmn?c['(\'c ȝ}J_βOwWs<: Γ.O܋ru={8Y ӷ44^`Ɩy^Z Q/X #PK;=kp?z7>@Yz!@1{TZqrdMT+'``5\"}:!b3)avFuV$Q*ʔJm{򰙺aoc"H,+`y 9Y^'}%dMlc4(*+]5RNL8C ܤ}>B}a1q@%JųXWW2,, {p) lNK[6d_4cZ|+l(]GՃ/8vבs#$&:gkC#df>9&8ejW oՂUbU-d.؉`U0uRc?lEV npI%σ5uJa]8؂/hޘm{C#iF:N= -l6d ^Y\kn۞n[9! @FGL7 qYpg%ZNC2)V1g,)J+4,Paѱst ;b3̄4t5/9jr2ZghVX"h *gid+ȭ@UB*M8)G8Ua:Y|Œ}W_k.۟pX,wVVSPꠏEtev߽due~V"zg>;E%tWZQ~C?Ʈ< HAdU3Y Z:SZGL&!zBT6Dڻ#q :vKA-='sG ~o!Of!id|=_ã#VoYkiw 3;'Vi싀řQ{*3k3(t #sq_q=+i;L##xF 7Sr QUX#qd%2x1g`I32xI+KGfmnc{pgOڵG`:U$fՌzhmP!:_g:ƃ~Î8s>]FTw/%(L9&ym:|؜f'`>I/1A/8<S{;}cO'?6cnp4r1$[9m؏J[׽=;.s6(]Kxn!< v+~=ݾu+cHe?B6o([]% } j5<,=9 2t}]rϴ# U/70à :ǰ><ğ33/Euqm/SeZmԇ)QmJ~_6'z'^b!1&0{ӠrLylKƞ?:y1V+ř~j^2YWEp{j ;CKI̙ 30,Ѯ0`M եSagEi".͕5r{Я UllaiCW>B^٧vaGuh!@>i䱍=lU Wb (h {]y2rMUږul< B`NcjGs3lD6{M6?o B)<_MV3S*mT HW{6YFߊ 7kS\Y=͖9zgvvcOh #xh*N>BH zvh5Ocmg `*+q˵fV< Q>Ԗ4~Ϝc[Y Tё7kK+__]ŀ؇Ẋ}7wKl@nn1 hF~.(캟n!}9WʓU%͂:2]]RG% 涍^#=byL?H32 2n!KW*֣ kYwQq;%u [z/']|Fԃ\0Z)-].ӾaHShoR2sr}˳jT8ViiKam P߲f琥b4N(=RC= ݓ5oJk(f"v~ A@2d[gref<ٿk 5|vs#$786^[} C '6%UJ7ݻf:zҷCRdS:6PqizD:F g f6qt# JX\=7hL=][Ǚz8Ssdϰ3 jrs dvb]^%~:uS.|otƺ7c-Lj!pLpdiq'Z/%]]T, ;XcYgΔQ{8'4=(8pi^Ĩy 991ʏ P۳v&`|0̎=3l_pO1(1ӌ}(_3 TOߛ:2(H{@.8*d?w9*-@{( oF)- /3?-WBPCilD0-:&tznR@UwCj2}q4d =Ͷ?N9,`BaAzaAۀr -זTs XwuT:z 31L( cѡ! J{{4:FfsPNЁ;u<4X楬dR#2r8[= 8l9w{2өP͔</9?e:']t^̀ux\5h̀ &G2]wPֲphxs*}ۑwt 6`KJf+y?>Kg `Tnv< gP?cSg|9;QɵEck򎿣>3##ߝ Bg)gVpޝJ >c4׉/ nd$U{_{3QfGoQWv@0YVYo 箞N`UDrvda={sM8_elHlgJ0@G/y4{n>Y<K+o,Ym0XT? qjɏdɫ7ښQtrcOƜQCҩ{|rr2_>ǑAl˽sO(HИ}%Z4YȳVp HX[=.Rٍ#+98G/rkKyXB\xcy2ݛ 0 n*V)B_#V0;M hnV,z8Ng܊)L *`tXoB 4BJpxH^٧‚g;00&I+џR҅Y]%{ٱ/Yͨ < -|r{5ٹ\u#d1 V'<6+\0I 5ޑ_0{NP|_MUty5:6t~߱ J RPeBnFsx?cu)*E.h ܊ 0e:;;Dؐ.HQZWm"ןU1S)Gfjr9s-ww8 F/\B>Χwnfuu3U`kbErSЕ9?[k!?xFs~/ڗ?Akȝj3<"xA A.tAgc+}lLP'Uė2A4#"\!}6CVN |ty-s(dLxb  n[5 `ۏz6SPi3+x6ijMSwk}|l33nmxZ_뭾YF qRzG rq{?$@Q|\VM}%M؀Vyz03(iy!F P?*~kR 1q?b+gL'9L,~} LtTW_"uP.#l3LA.9izNi*C#}$ﹿ;}bٳ-:XCm7O`(ˍۅpqǚ q}կ#{U=1v~B<)sYB]MlL~iofvy؞["Bg}cG']RO#w~s9~'y_:HlM+k<=)TGk`i3W׎=AׄeJ'՝7o>j+)ǝT3 (pF3 Qn Li|LQ*-:3rsg//_f60ݦyԢAӆ!*pFlkp(]R}% * WyJԳ-^J 0:{/a@@쀎%, 5vY@Sc}#ԕG 7 F KX>ЊXYAWMr(, 9/OQD04@5`u [ _L$+/\` (;"Ace/4шe,f_؛ 9h y(p3s:c՘07;Z1mI rEcOg{/e>7@M'Ya2}^k!@u9:DJ/UuXN[KROzoǑY bP(ܔȔ 4?=U;dTc |~48QYzNG{x@x@9o{fkhAlN$Lv>8 Ij|{CGoq.uc-Z;f)ۖ硶h/] XD;{4I:q07L|xWm/Ve2m)ϕX*,i \VS%r:xhjP^AK긳CFɬ.^GV (10Esг fnN:ye&ndw5mrLȌHKnV1|k_NWS3*(.Q~y`z1Uwo燑uhØ=8&MѯaC >yJ>i93A6s M3J$gw4ci)*XYt|s{K/eg`g朆 Z2k,d`-&Q,3X~u^=nO˰z}:=oi͔=fxW" }Q|* i%MP,TGurzO[<2%X\B+{Rq eG kXuc!}AbjggciR跇4i2i| '=)c2r\P{|@aZ|hhW6ov:RJc/AthC_jz7SP.2~FS bSh>`XQG0|=uGi.~x0逇=y /~O7rAfs"&R׷v82YEEҽeUvZ7LSsk>v?3?z^ght sOsV0U@7 ʐa3d<`@wfKe*2a"@U$BgDe+,ADJ_?S`nr)I}yrǑ,U  EkfW P)cNRm Yd =3AUhME~}O{ `q|c . sa>fv3n앹1'MCk8Dfm)O3}'jVd*ǃ|#cr5Qh1_Lρ,89s yN, ߰ڕ{֪KW& iWʯ3)ϩ\H >So I%mnS#2y o ;{۩ j%$}K+xZ:מ,V#C/b<}ieA loa(׎H/y\~`m]v}j ^t;nh0h6tǦg]9o2x(-Ͷv9Sp ?ז<5sM^$C!92cWa(/ϵonA3']{'wg>@ܛA?] 9wrq` `+'Tvןõ1F/kK\g p g.+丵9[^~:H`RQFxc|MiýU"#Z!@?DyuF3 QgQ)衿0?C<84=˺{p Q;0!ta6 |^g3 $))Y*Mo_Jo7MJR (Z+8Y7nfu׉AZ$3K#j]':5T~}uhw,Q^g#(Ne g'Me6XD ߋ,{oDu6uJil,N5]^N/pbӺ Di;1ǯ?EX$7){oИ9 A>^ iay.[d+P~y]uk)%1tlvpHr3-Q~ r2|fU-!r O]%w+`e p~Ul5nJ@Fxf~`Vgw|b,>84O'O1tב)5xFg%1,swQnJ^b~ Tu,a2g.sk8q&>f౼@-bt7smnKřͼnKlpe27 lNDGI^`hZmdsjNuҺt,YP=E7 8&pUwstXN+O :NJ)u n3gr A~.:Y!(&Oj%>Y n'mNpƟL0fj"F؎EY|Չfd{L`F}TYvnjc\pG0Ȩ!_ @O0Ălf_\6D;ء! [{F`UA2eSM-ō)Jz†*GTz\y2cr׹yo>  Oή_9)"?3N/ Kl2SEflVA7@N}$`JCQW>(<ҕK3EKoyB]Sb nCGa 3~6VW~5zt f,եa]=g 1)WZ!G GGe-.隔<;qV3#T]:Eэ?k?{P}n2;n\O(]eqkl"P6/Iy`juP2vh >`ٳKPy;ӡΗBrjy2j r/͗|<7= |Et\{Y§ 4B;0DYw\o׾%-;/Β" f:\f"g瞙{ؠ\X^_1kW_IޥRk}>qNI~~cixxVV@W?6@TyZ%y d;(\u¾iYu#SY &v=ߙ;ފ{_@ÏF1P*7AnR.$okf졛7h5t؏]o}{W0@LטAծ=Y9 ofCkl"= 4Oi?n˩w=26/68ƫ_TNuxf175fnԀMUTSzK 9Ni԰~nF,h 1ߴr>yXUl<?t,k@Zy4:GVXCKע{#2Ľ?_s=+ >ЏN(򖽩LuNU您s#Kp(ڡ`x{z+SZ,o#ggOPy'g>?d$GZ~ =l.R*M҇ @GGT1uYu1: ؄!T&y恵 HZeD9X|—Z%P̛u_N%6 G̬imsi@vurjx_Kw$Gy~2-ęvv@]ױ5!'''z{7piՇ= Cg5BKzO[ H3Jd.k\=={3#g+W\^'{ysp8u6Ƴܙ~M#I^$1ayI9Vl ߈<ÏxP-D'< tZiY|s`2e7"Iw~'%>>>"P>4:Ṫ5&xM@>>a(pF3 `𐜜=po|]$D~{)!~ШC5<2 fY a܆3M43Q+zV GY= L u( *:K*o*:F6@=ZYT}a[~[B@S%U hsLO#cW)xT;ǵnJ'!s!L%&,tx/S"f7@wuvg`衷2Cp8nS\pS!0!`b6@ǗQDhN pfbkD ɖÙUk'yTngvp #ЂRb,ea1&X+kk +(6qAG_QR5:9w F4 QK}X-0b~oyqT jYe@J'  ONw'>uMneA+4jgg_2 (٨U\c]Kű8QpW݈UPlQ~9fppի+ao)}'T=/ZSp-zkPvtdS|cD.]`if4Aql75/_+= Fc(0,c_];G]3G ,aVqU7Y[b[вh-,ϙEy}1bV7HyYefQSi1C%ɕ G8,\ƩsU10~q0Oj <'Io.!7)#c\' 884p6T;Ȅ7̻Dr=zۀ̮v A,]g 'd WWkQ ;26tW n|M6k 3=h8} NP~fQ[&ࠂcoNg[ h&/YL)9x 9wX]C8Rlٙ)x }|_gEGQ0s/뤬yL*\YL[+* #s(|o3 x?2h}yCk+m- /1@ QԔE sS&2[cDG]miDv zmdӗי%gUS\#}Bnrvv~6am]F<(;׽e&{:Vj4/| /\.: {ȽyJ[1?އ2T |:7O AxL&`F}%fG)ղj{h3PFr_Խ ZXk:ϔHd.!gc绞7:/0rؗo~3RvM-=8De4~:GA.z;tm)\#.a葜1}]OHc4yHCJ U7s7k3|U3bl ? dLSA6@rGGU+Ge!m8ݟuofn` ru_f]y:i67>k]@,]Rf%; +xvy>qXn̽ekE_~fZ[?p<}Ҟ9*&_5_Xdmq,JpgKy+{n ODS:{s p gud Um: [҅cMgh!Nۀ~GdN-R]@IDAT} `=Cg2x9^b 1#\}$(-^eܻ˽)HSZolU4 ΗpC#{n6y涽>EE*3Ky>2ϿOB3 wZo( /=E>«nP 4e 4,^- bPK _7#N1u }dF?TpĝT!h=8m-mQ:ϭ`&cA oHccRQ=`+{~Lf>ng(FF y^d*ef?z@S>ײnOu!c._}$ ^f>뿑FF) V7pJ+i- - /ƾʅB97c;26(t@9_/Pdfkz/f@ ]Ǭ'A}Pp+7ΝA~2k&2sEWl$lae/Xw~`U dzTT@浱G%;Vhc+7غӮeQe0百~'0;5YP.xɎ-^~rz߳ڔmrv1~jgeh.4Ҁ5sU "ۗ FoHGiKSmn( Ak{j]UI5җH }EJc|O(C>xOi{KFSw'?iG0e {o@A>vD"{=S3؁+ Z}>_~rnQUڱµ3p]k/O9իx`|+`:Hs 1Pu੠ߋа'4cheDdw ߮/$?I^b* `\$^{R@n-G%q'GXe o͜:Hh .UFe gw{uF3 QG~-㧃Ũm[ 3wȩX$tl?CϮ;z(|* .F8ӳRVR}47Q=OɾǨm2zV~NHC R%BـCcU<T|~qj\9mԸ7-#I)D3-Gc=#ᰟ#ȌK/mmf6(׮©3 DJl+=8T#s˞JWñhgy=A薾pʚ 3F'i*qevtY"ǀ:eر Y:ȥU[Y920%JR8405k8HU,a}t{ f5J7 ׏uf_ l5Kk02ri  Gxd`{K|RN3|#,[(r&F2m|Y t?lf,Wb>3Y9D9[Bc̈tՌ_mf |h}Uc^z9u5䞥[Qľ~PvenλfgHgu:th42 t1 E 2 jOriñ @ )4-A:+qT(QX1SGSBޒ܉2a8-S>OFr5a= *7qv{G,6O{b -MLc-a]g7X8sj½]5Lpf} ]Gx s]ngr R)^jSW@vJDV^<+H9!l8 *X>նl4WCY'γc|Aǩ~sd#r;Zr|Lv:|Wսs?ˑn֞ra9˔'0ÑBABs\ [zxřqg- Qw|7oe_:Wt]}zA7րw[33iwmm-Γ-}d!(Z{lc \?%{"c_ !f1V{XrcG:%m|ؒFV.+-j4;5Ε$>PNB[xW|1POKܪ'xY~ v(}Bw[`< 5j=gU 񾤃a|+ˀ%Y:,I7a $X+~*{=["XW@p~Y\ݔ6{Eq V3w+Os?_s{OG*U2۩K 35DZ4 8-ӹi\;ΓJVgc,;5y7ƭ^h,;ekynP 0[M9*\ 0ެǟxoЬ >}~<m@`_O3tZ&2~;W^6e;x]B=|im.~h%剥G .lrmUg" 4+5=wO-×\ w->~V Y\6B1d")KQŊ<8[6: ls3ז|q "S {8Fycu&*{6w:iw;!4w-olJ3V/9&HL2 UN繇I?AtKt]#>Z'Kت EJ*KvZHQ!;*gA4V}NOLհl+3qut\`jj,QEKa{8ݼ*4^JhP, vwq.ii*SxL ,[ vclj^zkd ~Y5.\vOܲ/cn 0 #=ɾY_83*c@(ش #6Og} qDv-չoɞW@ѵuTU=H!!`ȾzwCd7~6cr 7Ndm^=肬z 6(TsՀ->Gw'v3ѾK@bЉ[>RtY!$BVsY&z-"Z1X #2!¿ޤPW!6 ^|ncx˳@oQ|Z h <{CYpE|~?SY{ZA>޷uhz౼HP3 h5G绶Rqͽ1ifP 7kkgӺnُ5*v>6řhpD<ɟ^$ʒd`<.(SVɂ$O:JT rx?\ãat:&y1 ]y u QL~52ܳYmr&K]孾;hH9Tb4SNBO1~'<߳q" j|pIGJ q?s u䀶t7 T\T+W5Zluh'Y[DGC GkjՂ>{;Ng8L &Y:L<=,gY+V j$3<Dp!tj8?& {JK'/GD#SM>֐PaV($[i&)}ʛ}?5aLNKnj0z;Ph{0  "pXrsQKz(yM9ӡp@ =! P5:3,93*8Z}+QzY,Ȍ+hm}lo‧9`^˔oiKK5>q~qzZ Ȥb~#(%7 .-@Sg7*V)0PԨ8T,@F'!fU\[2{M0 <ר|Z67LHjF)'y?J_-D CD+KFʲi2T}ZȞqh%o+x?!cYV%c>2)##Ӫ j!ht8%jZD8t`cz/L}x}؅iHMp>Yw+3hu*< h{It&A:3q+~%wVg^&FU qs}%.1&>Ƣ8di: QǷhc`:@ٯ_ЮR#k[(Ͼ+a3n#i kȚi{ n _ 4|lȖDl`϶t)[K2͒[F!ƠHT>`_5 js}YP}L x(=@ {oX`V'h -Zaq=zE-RB(M2+now3ݩ:n3wqx FRV~PNbbpGbzymsE-TtGNi9wt08>:i?,kok{BF:x#٠L ȴ gF.WO>pc>%p*0;vI i 2Ʈˏ ۸Ng:<+t"帋^ɿ"ZCF t#3yH'q\+ TRF_LJfF:}yFXsSt{:c3 WuΒ8l̵ۅ7*QCf:)~陜QΜ9֮T`9JV-tGA.cuDRo/rВ,"ezp|\<[+hłֽ,3:lѵt4Ş} Q@434-~f7h!,a`D}& nQ&9*k'>/=׬*Չ\;Cg\63\uP= [{^|5#=qNKS_mP:@h< 4a^gf(XG,{h6kn~&5kw MGxɝvrXbh{DۏVKٵC ,3@ @%e qs` ^*rRYe/!2+͐TYKikbBwwWR ?5ɚoݴ,lZݠ% <.H)R@z NZYd Lۃ cuY3V{:>[+-deAzySl>=9?8<{n\g86og,%ΏS@pb x('upMF?<{p:wڨ= <g>dtJ  /ˡݹ}5TA4 C霃ו!I[Vm2`q;mb܇{[>i9/̜ߩYf ƤmtDF~Snj sۊ>L}f529LebY 5etzgwiC {wDLrRܒS\oi{Ȳ%D&r N6[$EHKF!r8ŽpO vP<&{u+v $>U%P*os]Eqx&%ֹ=QDǀ?*N4h_cp)C.NY⩷Ђ~vVQp4pO[0n:.4#]G>>R6#OyF*֠sYӵ@~߇NkFp;C}<^1U'R ʖkTDשuk$5U p&qj2k -;R|L*GA x& -- +FŅiigNc3w (fxpZ*rBYKeC XOG7  +")PoJ^d 9,F |zOʮ-9sf!uޭh w@3yDȏN@"F8ꩬi6cOhp;n"?߻Y!(#}akJvgs)q=\[!̟ޯzzq($w+drxEoyÜ @*@0XQP1@]s"=b\FP#'Mf1|,ãvUZ zDdJO&<xs*(|>XU# j3HxO58ø BsZb>~b0w߫!{ma=(azsNU0}–)C̫('QxQaN!h=cGR6ÿΥ#p#s>cB<:J=]{Eԃ#ooܕKwQ5(#`QOm2 F ha}TϝFNއ~7DHu}wd :/0(aH:2NdsxdjDr׊ c\A7EdC ݗFMio< h 2#x_Վ=(1 ,7y ^CɢKit :WGH#G/oxF\+c|܄nzw*\r7u@ѱ 8=+Xm;Vo^zrwE5̲ƍz7q@t=? 8aC;RgdP^CJ\"$Oz' :?MTc>(*}M :Y_qo#]]'*'7*mV*x-|{;;>/XO_|Le^|`F#|[1>OcIq"F|򎞜J12 {Dҡf5F>m;T\۷UfF6a'9? Om+h𽷳k_V#/ϝxuWY{w;ƪm_/BX pu]P^#mmgNchw=g_4>&h18=wb3z;W!O<{*٫Qo³|25*/ݾq3[\5-]n\>bx׵i2*'Y5 f~0jNӷzc#Kg5Mi:;78CPIً;>3䖑0sj"Ћyӏ>@T&Y@@ ƾKL+3j X͡8igu-FP^~㘻>#­׳an:u2'"2 }> 1gxl3lSD cr-gnaI KW;3' w;⠁2D8וWXd~,rr2{3u0JIB"=0pc=;g(Q/@qFg %@b;xK(T:0|YP~Aߥn;U `8i*ʗ/"C0{YhᚢKu]vl T=̤!A]P.:P7ܥ0yX)eib_kD ^e܏RFYjȼ V5e:w`e9w/.q_Y'ǟSUxD`>9OG|8&?>'0P~K$/tR (( PpqwJl:7Bѳ\%7i +} 0G!@Z-GTNB58 |'(eܘ* ^x b0AυQ7y XI\:lb|.)Dt"qP4tyw֢II@3{t7=tsc!}s-dhC^ZxB1-,!R6 ū0ukd(QIm 6A߮0HSţ4SO$ PCĚui6lQpP UХxPլZFGI޷n!65I.a1iG oo$;&=, Nzo5)MmA0Q4l-֏gJIӧ_TN}G: `o4f.Tmy^qݥ^zE:W**X<5#z{~Py@#[ZzN9bLO޺%塺k@{}$O7;5sn 4(Wyj;5({uy|e<Lo#x@yS \s1 T>HkrwSgnjQHtxo|TTG}GG9'hݦ͌Ii5 `^4}N(}P87TK<\pqpDZVҵEzCtdƆaݤp2lCCiɈfG幋NEv[U<PzHu-p1^`]UAZsQb*YS=o AIǑF&w#CyA~t? 2y)GAee6=f @W DҍTar'6]Ob: k$ M#nZh8C(dn~+>T޹0,။):? 7'M&]MdFhV:r f][4ǹIKZJ 000t:xi8(m_Wx+HEp2LIrr @0:`ax d_拁JJ% "ӹGtxR=]?z۶ʶ޽[VKx= ]95 @*< 5V zI& O;b:ǽd {W4.92%6`y<k*j%PkH 4J,!׀{!o|D3Skm/?h$+_ [`}+z&lwShuǦfU=N[ys@/1*}隹^IyELX_c5hcAM K쟥'sf?֥ LxW @?:B8/9cXWDžgǖW ~s^_g ,iDݦ;BQpXFW45#"W}3M"?Vi1%7D/'mCyM} Fhֹq>-B^8j}5}wLhj|$(Kyzo݈}\vFq((PPS@"J zaxm~@nc:(oU`2ش޼Ym,=y*Y!,={U; ;7/pC*6XT[CS$S0N!DbqQ7vP\ Oe*i6 + ~N~)0 ۫^El7Pl { qS`Ɠg/gɭ'PAH$!Z`Pu(qi@ :LM!!U DT9{ ! /;+x)׋P%?Df'!6*lVD9u!O]|3MP,vFf&ʆ"0 J:y ~`-ݦwc%]OLs7#ٖ"' Ãz@NfŬU:ۮӭ^x *A-n-xjKh!fw_'&%aBpRlCنя09H '@'|vT(i#\| T:d?yOzfl\gz+,ש2 e:i-l.͇fer~n`^SsSY u"wkVv՟F\TԘxqqJi˄a{-[I(VYEi>=th7ѬaG 9]cڣ!LE`(Tv_Ht(WBc#v lﱞ |<сII)VmrQzoCWQZ>Dc. =«`ޜUyEndx @{'үT~9ŻR{nzL%VOѩ?c=^=t_l냯xtXFōeƜiCp~>yX?TeQ?Exrxt}`?(#V0~aĹG*b9EM*٨[:2ICж!@C|{u9z&Az FD*`}^?^XקǑWexΠ:szqqE*x<O[tEo9Pvz SO|;b`\ESs:BR{S6hz:c妡!MX{i.Ea r}3u9} {Qkz*7ͳ}=f6=rZWS-B/(p2LaD(p# #%9U=&x=#ӱz07 !a7{ͤwD4D @4mSDz3*Pgi^`u ;?CMZ$=W1bL2ƜJ>f4~lwbl4ǦMD?ӿoNуm*7~9v#}Y# aǖϲ p7Fms-`^cu?}ucoGkD];Eh{\~UƓ|D_zyg?pnHGZ.F!}491U?u?UAsy\O:@#>JŚ}DJS@IDATqR9C9ތ6X9j |ܧN~WS<C3;;Љ&AtDPFx'VBRt횮q뢆(;y藷~'h+~yIi4x&[4Fxbh$BJa $0$wASG@rRN0 hm 3`{00޾]w%?1?}0ƄmS wҿAh8e,<温AwH 'caU ##h3ftdo5q[mid^^#th? ~=*-Ap9z]7OL81cp a~u4-Iq'L5.>K i-)Z m>kAI-OMc-S^^{Ngw'"tF>XB (w nV6% 3H /׸Rppu1L9lD1ͮwcDfѶ|?c~R񌆕0BDqVi  OvV*k)B}j0 )׹TW:8Ƨ|3>c]wzg} !h]pZQ71`ֽk'm f RsK+#9IWu.eˬς66X((PP@AP;??~G^D7\BJlhY$@pW޵Kx.&f,Ǽ8UPY(@fYƗCYBu]dG+^Cd2ܽF}o{ZBK!"ʑI6I( z)9׹szV:7^ PٜM*[T\vc+XV;5Zz]!1C+65]n}-S`& ^W̰[cxm#`X$Dz_ФkvxotC X!S᭏˾_D|7  aJ#,@vhr0C*mB)*SoLe;ݿ ѓ–:FoGB<$N7f pjP,{Cc{s-(pOA?No)v 9GϾ$=(FQ;χW)w]*\'=ƅu˂9|;r6/s8T uO:G_'*##/1j0x =å'9xna9 Jq1nx :{M޳\x?=ȕ&> oґd gAo֟9ϜcA(n vpc} `M&cu0DRc>sr=J\| G~煭-z?;:}Q';}~2_~Y\(ک,i* s~_1"U޽=SRuF:ڂ5hGY@SᏇt#zʛ+?1-;_{>3{lIG͸s< Uka}8{?6Ócx~y7?bd}mIi!?Cql&# D04r-"78c:?ru`14>ǐg]=ek8,U=L W/ZGܔS?+.򋳷DCטtE.2$=kU@h7k"_PhxV[%~Gmjq,ϸ?H `ݤ'Z sC;x8ﻗwaB/P/iDEjg/2癃\+o8r>R?f#\;\s`,z|nF9& wݺq-{9gIc_ ^ y`0 :W\d0WRWj[do3o`aAR*=©BdB_Ϯ~}`$=[s (࿉|!5 ѻFr9 "?3z;ng{c-ܷs`} >A61f>d#Cو}$^ҧNbLEc i`4J ( G#+%['"@YTrn I7E3|7`cDv7{v?)idPU=XSf6 c6%wAO[sst ނĞˁ?Sqy/Տ:7ެҖ| 0.O f^Xcu [ȞSCn³PZǷ =ig1w۽|nHuGy؍ס͂ FBe3,c6 C̐ni=?6q-xt}4\c[cA )m /qe: !(ZZQ+$瀆CI@C`zUsów`|`8x'Mh Mm /v<{ aA{3GS)vCGD ؾBV BCK+뀹_8pZ2tY7IH}?/vCoxަ?\W, bG_^~Cw25BWDQ̩Ƈ- B\&2Qvϥ.F#Gmiwb46uܑu<=~sNE1O x?3X]ybDsY.lzoMM#(&_<QPR@Si;op\{U^n td{;@{A!ʹ)k>>g!p<3=w>IA*hYPi y*s|FAwj *Bͱg$R0%)*>} ~7H9y( q($toQư4T8,dŻ1!)ӆ0RwZzLyxlalfD 1wJ~Cژx]A\{~M!u CݥXU= P_{kQG_v Ig/ ְx$/|k.@:R˧)ِ!B/3mڵŻ OPD5UIO=#tyI e#ûWɋGF= [)o.!^(pN `L$o Uhw+?2Kzh - 'zYfLMG<<+G^3׏<wsۋ)ȿI9ϽFOU?yky%9zj>N5N{t~>Cmy=qg{yOs7U|˵Cن27xTK 7 >TD!X(p 2*nzNlc$9$؃Kп+ wmYZ0p=JW q/}{?d~{otqE79ayכ'x.^i{ɩ&/če74Y7yq-~)aIEL:;{ ,p{e-x{Nr~{}GKHeRg_ˍrz*SO?l~vQV*5/qcikɻ>SZ5KGe`ž Z5B0wzV??6&}z;5tZ#/WDo &ݼ{)Fj( wt>;3gGѶ9 a~xsA9>{NShK1y_{(eDEw-? 6)2D9xo^itHuW::mqIT#Hj KR 'Fh?~b,iц89NnD6>r "ia$ Á/X z/I|+O4<1iC)t?@tY?Cs`3}y2hLm}>Fl%k%G0֙S!/d64~qL ?:ķvX.nSDJ^S{b(;7DČjvTqoTA1ݦiSkk/e[Ռ@Ew )64pgPYzn!Ouf`9$waÈ5pr4Xm<`]ClU|~JtȽW,зlg:2 F 73D0`Mbb#1f]4#aDϽ5ua/͉c8FFMn1Z:4C|֐ Vzk Acͱ?uLISK8Ffhl TV)k:bhPe5~?|BW i-Ycg$ C+SFЀ_l c**ϔ'j otwփϬ GwO; f6iACm 4Ȅk^Xu푞D@]l~5e#O8hsLtof2vb>2 S^mM'|B|d$sv 80Q)h*l:cs{:"pcRĎc EF!T]+<d¶w [ahU6>r|֤Sha$ S ڏ(׋]-+(PPE&ֲ|m6/=H %g y6Y \1A_)a}#YTiwܴ+-@@J$-OI BK A94sST9T7fêpU0 ss=+dhJ7*AŽl#/-v<) Oޟ+A|f0*,л J$]3=CC`Q sof*F gr/n: 4-C9ij 6I! &") ww>l~O{9_NQw9H|Ee|ܪ2x[=X]= b^f)c{b?xk4Fxx6Gjջ[o Vb~zZZ0ƈ &KAWފ]º:wbH2Z~%럑F#Dw!%ߺuz/\^ק^C~}D 0@ 4Cۧ9_h[ܷ^>Uw]Ǖ-i|&<T=m# Wο~{礹d!Hu4 t;lPjzQ:ܓמO=s@py @_{7<\ީ.=wCwia>4ʴmMƍ!)*FB0|GzDo0g!h/{*ɕqQ?޾O{rA̯,~3"b(gBsG8>sшn3"F_`;bPc< \λM)ؿOh}+%Y=$ %/crܹ?Nxj|16q5lm\ޚk'qV JRc.B7"2:]i#AXFxHI6?&cJ6. MJ- JܿTupfT;~E }:tiO1?xdowFP'drydQ m  O } ΢ooau1%+#DU9,#;<bQ7T>/ AvT^9L{ @}y_~ eVp~|崝6DROWo!t1qhVzw{1%jܢk +u>4EoX`CͬL w;e=l`\H\96DZG^R ݹq-[XɦSgGؗ;;%u)L8 ((Odn*\hTݹYlJ_z=6B{0툰6ȃ=5`8l=ޭK|nyyj_P"A:T@Pcagc@CaԿgO|uBJhfuoZ9=쓠]+5M"My(G~^I~\g{s*psvz_|Oޖ*_˿_G!*iԹlxPl^e1GXhzv:|x{voڏږy:x)מVvOi4_o|>?sy,Mľۏ{-_]Ժ#kܧ~ύEX8JGe=czRlS"{}ʛ?Wܥטd㔙&"p"c#1ˆBQکD1wBiM'PWs&_ ێ8-e"_ ?,y|Ηc]ݗ3zя;ݸYvcӛF?"4D7|mSY\^ƫ^ %Z F+zW< }t~u<;[ / J64~bb$fz|@ijւFF)+\8j :iVYL]ʨzE8o͆ݞ S󎷈޷ m p}xN kF&tȉ6^僀KcE1 WQ9ӫDڨGJcC᩾J/ \;3\x r =DیP#m.t$ziQ bPz;zЭE>uHCc\[z @aa;uR׫`!=%7 VJ7-% 6@ap .ڄsԏE/dt.7v7h`"ܽ ;Xg#p!}>h15q?m otF 5(S݌1#izxuBT1țQϥ1Ģqv9f c=𭻀;Ms4`hcH[ӯhd1o=;U0! A,J*NBD[?#i4Fʎs"w-]QP@An V*@M +2C!N[X&V@rlo]ś'Q@A.}#/eyrG\ABӿ*(^_=G^v׺wpq2z?wo~ιl}wVB>>+L-zʏd@`ˏAދݏUOrV6@©ʕzӧg,pQejaw6?rP>'N{=xsNà7 6f¾:k*EPEMXSH\RA{x[0T7Dtz7\CbCk/'WTP@AR *U :&:; [K4eMgK`ZEA i`54>zԇ{j/D2 Ci g j(7)L@s{XG]nZ-)Z/ ze?//{Woً//BCl}>FarӞn|Ry]%C4ٸ5I 5g/SwDcTOs`D0:r3g9 jҺg4>+2|o]BsLNw0a -j l;9O2pyja~gMeimu-V7W?Rv"u~ /K }T fҷY]Pyb"eRӐ])LA2x &upĶx7kҺ>D9@=e[޺#r,Ϻ.n ܏sM{Y0֨}Ye6V%-hm)=7wG=pN>C[,BX7A*7^7ycFz<@uc={cCpƖC% /g@H^XN.x}Xύ;:s`c뛄׫ @ ZR;Xɶ\/z(B[B&z[Vø]s]E{Cc` 8A6Ԕ!uAEH`|T<54p@ :Z_| y'/8v[vrMIA Ox٨wt+؍ ^tl<hvF݆nz;4&WJ+BOGZh[X?ƛݛu@>O b: 2#4u=w.! M *DQM]4>0BF {j#M@A <+\)n. )JF |Ճ7By.i2䭞RY_]WP{?7ni=bCzXar|qNet;_@O||Ƨ<.*=`,NhϤ>@=3Ba|PH~y٧dr :?//,&R=ўz+7#)|B'}̋O}eq@A*.#9ϨD=Y8|\֛-JQ P\#vk!@͆0&Kr:Ң,C];7;Ox3Uw545 Q 5:鑾I*!r[5?8V @aC m?lS?n>o(]uicT5{ls\׀[&F< LNqbuH/<ہ>ȯ[׈04s# O:0<(kԧm@b wa R-b@8]$h˔Gao ] , 7 8x82z} Dz3'bߍW;uAspXJ\ۣ;MRgYe>4t{o.n}szֈ]_k}̴Q#)jh~6}LHK.<[xe@A[n,&,/0dM8a"?5BWȿ((PP(g"N&iuyz2gyǯI*ݟܓ<5Z<^P@AAQ% HIhTLZ#*^=搬g7>|(UU`j,"m>E2v'g/~yF Kȡw`ً|QVA_O~>;cΈ/9_wi'.pyҹ2U:\ƉLR7!D s<>܍uܥcMy^6{duk-eo(%RJT-,.aEojA6^6( a &% d?7 36 {?y\G=di%Oev޷_x w£:YXpg{ Tp0RP`7L#Cs0/.z^pAW_c7a>te`@!}F?ݜ΂-0[*Ui%Û`ӡM>~ӔDPYѽD^l FEo*amjwNNy%@lڭ@~h]m ({!<덬l7ӎC O;X9/mt44D3N2T,׶2ۀ-CS#K ꏁby../[pC,3IARhg8.s,G~h'?9pQɭ>7AJ7+%(-?MEu On%.}M12[G@^" # i R^qEXǤ9ch}kohQ#4YLk((PP@A ((PP?^TqrT> t5wrѿ&gPføYK9EA}̆xrvcJ`#{W1^ xLʌzTU~=_|q_Ƨx7󚩜IJ'GG^kѷY#9k0;w&;26Vs/z#]I6>1wt$/x5[$A42LuAnTAn=#x@VD>ߦ/,)\1F= m.@/e>e WOWhS@EcV!}-7?}gPxъ-Y}Q_ئxLyzHEz|wwBsG@N+! pQ'f*X3wsaģ­SSr kuAQ 6yw+^7BD^Qu;FxƈZFY8nF 7z_ JQA{vv]ݓƾ^.1_#y{!~6Ysnmc-XH'.?/&ZU AkpacY /q5|061X_~wo/øc{=}5ih F(a=HEQ u*H!]r)!5X?,kt?Fz=֍a6xo#AJ~֝ԣ^CgBpywh hhO-RF`Kk-ܿ^An4Fsƃ-YN_}Ѓ+_ ((|pesiomx^{`"6]B{qG*i*l\WcA 4Y̺, ((PP@A |LRu " ; @rlЩ NL׆h ϲU?ٿOhl~~>*: y1@QLAPE識楟vNʁ˗c^ W^m6[̇ˀ!Vw7fF㩩t#{Cϯ~ݸ9e.0}a4y_i Oe@JoK{#ЬM.u@ Ŗ׍.lpyK{xo B yπ}x`M/xfs^FplVhYR7AHzMf(՛[ gm;ynڮ7o|0^rL |>va` 69gw tRnӞ}g7ӫ@ @0hO `[x \kڤNS0Sz;s[o(z=-rY眬  SuhF]C;ó;C1re7؏bKz[\3©wxUI 2cJVa'oonqN:-TE@ ;s{y i$Op 0GqJž$z*NXo/s^5-# 3/}B< ynixL-P,$6-._#{!@-uC'6p(D)`Zu7dLjV .[ztN8Je?b9TŰ0 aЫ8-ck>|x] jiALs(#z})Z{{o5WvUN@{=hj6Jl0zgys%ܐzK+$8]foWMSZ h锣\vNVaHp֓wm ;oٶ t=]5zUxy\Ms)-dY1 &s穓 ԦE O_'D){'RtXp|)A@?tl?s\^͜t!Wo܈1'n/G7A BbwOc}ɓ{_[;\35  3&tX>;%l91٤.1n,>|@ƪ~KY9Q wӠ'Jq((PP@AVTBeh-&N]mS Ys`*Xf%@A ((PP@AM  2Ի0*2Q!o=ˑ5= d^m.VJxֶtײ;on /z ]_CNGA <'\̻l܌Ŝ.nL6⺶\:Lql劒oZy((PP@A ((PP ( Uw%=.6/P1-0|G>9K}Jt()oܸɋ.g#]Y\ijq @5oq, bxDHޗ܂X0 !uK[lrf[> %X\N 4lN6=s@|ϵs--qZke'OWgwg+L*֬zmLo l Ry{ i>QGnsnTLXpXiT ߼8VYϥE`{Nϔa(#fY @.\mmX W+(kD2[7;Dizװ}z9j`F6B~gQ{:";5TxX\v`;"N7^ˀ^c` iN ^~gzvЫGn5G&J[|~TNzrOp|E^;&#A}nu M]<Kqq4.kmh}@}hPЗ+zXwz[k6Ə~FG1# /^#L>x ~rX/kTWjZ Q}@mW}ACs}O2y.95@HeOg)C`hC @"{^-ڨW;%mʋcbT~ :(wS=w7(4@.z[7߹ՂgĄ xYÈug ~D{㤅9 )#` b4Bf iÏU1W|^[!:ߔ .CЌO_RAǺ2x1X[v3j֖ Mp@Okl|2vR?DިIENDB`colmap-3.9.1/doc/images/incremental-sfm.png000066400000000000000000004031261454702036400206160ustar00rootroot00000000000000PNG  IHDRQiCCPICC Profile(c``RH,(a``+) rwRR` ɠ\\0|/” RR N.(*a``K @ [$)^bdo!`5 g ͗f3K@l 蘒򽆡&~ JR+J@s~AeQfzF#0Rþ9ɥEePc JJNЪ pHYsgR@IDATx] `E&!! n,x-("  ,(" "9}{3=$W0WU     y!r^%     ( p$%'  p#0,.d 'NA@@/Ԇ!&ݻwcɦ`9h׮Lrϱ|տ.X ?g%Wi:kڂV.  6WZVe2u" Ɋ    @E@[wsA@A@A@A@>|\_`v؁;w^\Ouvx~#@=E9/`=m۷mi/h'A@{ũ/QdZ$S@رcXdMD[mrrq pы## A@A@A@AСC-i5E@^X%uA@A@A@A@K!.b    \XhKꂀ    B]")A@A@A@A@vaA@A@A@A@.hD*R!    pa/     \"vTCA@A@A@A"`K8s6oގ|=QO4j ]1QNF$*(meI5%)8|, %DMZ"j\>jԙ! WWIݺy",4 A~5Ʌ^ ,%8r8թ GIVAS4/ q <@fQ4;LJ,A|(IށyLŮZbw|cxnxQ>}qDbŷ3bu:q,x(Q:@_WPrӸ5:ۋ9.kDrw)#p71 =1K{˖ CD9e(g7xbڦjՙ]??tMd4YѠI8|*%F$*j/al5r"K8M΃[ C-]g-F-P4Gֿ?C&NBs\/x99 oE`wVOM!m>^/z ܴ;:6HUUg?q]X; ADPΑ/w\\~BNj]T+>"XKhڟ[Im\5&[uRY4yi)h&%G_LVE,n"!Tl VѳjO>2tHHHwd+6EAvRN3!"Ce_^K%KqŐjbUՍAـY "V7X bd z4vlAx$ V9(I˱N׻YdHV |Vz#dI>׳Gƿi!C`v&t6tu=\.wzSQA@|PB@OAVObjdB!̿3sV^ڏ"74ʡ=ӥƠ.q"5έ iav' p^Pw'=B>~yE4<.dO?H#567>ԧsg'%!%W4CoS_D"(#XF~ٙW?*"[i.3ΰ:j\.C ˏ 2pS9Rpd:߰ Fa>-$⠷ M_gmzֆ4 C?/#[wqh6]#'fuF7&L]3+II\n0o F|diE(b^0A<9s^[\.'CN&azu"EYy!|ug4,?Ǐlgrᷴ5ySrsklK0 W,Ɓ0j +/eCng*w`r^0oOQ."ar8y49Gl}ߔ)Yy;mFeʮ:FQ+reֻ;v}^ZHC50Zrj\|G?}jjѯl9G3@9m^1bS.ʉ@;>7:՟Gax~CkdY pӽA9G`OQiLzݣ2zi=HTѫp#ޘrw+\7ݎ t!#)  f5͚{h #ѳ  nOz3zi*Gu=;dV#_-=ob쿏A8n10QsOP:8_ԕ3CӠQM}%'%w~9mD`du)usz}3qÌ/1Z\w=޳⺮nywQ4FߑԓX2|جY1uxj7b[}D: w4[iz?>㵍/)0ZWq}]źҡtr!\x@VG>c010' dDZw[} D.3rq{74iJ3wA -ZTI>0IIgЇ##_}z9&lNegjF ,$ '+'Bi&FrZig裃&Gf>Ȼ*!ӌ<gRLݛc k V=L@bيMN}!h@  qν)n5v}xdhDO-X>=1M6zXL*VxphR 9"4iBsA[ Ńu^P(rLH4cV Q5b䒑s\;-{uo_+m{՜%v"h}5c"ZycC pFyv*ϵ[|#asi̐v֋^FL[|K9D[r"@I1I"ʻ|ޘ:sk OV|{ /}uy 0l^~hcJMvo #B".r+l.k=Inm9F̃WDDP}qىF9C=;j.G;isKO7ByqmgL/eLq' >& |E>iZ<"520;X#HLۿyCNP/1rW/ʺC]W]ҸgXbӯẛpeybwᖑuU ^6KN.IJvI鲤ȞYۺu)}9.i1ee̥,?YʽG4Op+]{U,XWCóC{8ll`6^_[]0!Yw6<7 Hmu6l[_OʷgA]DyI=~1k$*ͨгK,ڨs"<_ 1iƴ34GqIl$}'-^]Z31_7 izzo4(Zumuqh&秪zKl)@$+[}_B(m Uf8Pf"%c2,]#H.Y`Lj+6Q>{nf"R(\,!>XlIpvO -ol^ĸQ)Iz+(}qF, 6M흯پmAK+NqYH14UfS£!ȕqxsʺw DL@R1-hk,{ ﲥ Sҡi~j'`C2gfa'cLJ^FN;Teg} oz;Kq3EQT`saRUG(֤N|j'D#(H v=?8AW3[0e*éų%ک^ө#*:v+۷C+V8͵&x0qV鏸Ȯ`nP1cx[܎ :yzՏ{Fjo,n|9o ɩ𻩾AES]tUuAA61|ůuLT#>4C >e'<u!e,&,=B٪jߚxaY-7 HIQ]m\11ja1EFY|kB׾i> 1>#a[x#CwRJwcc34@֏#GC0nNۃPfc^EoI?F.O$ǃ}v~6+2(=x8Z4lȆЗW<;ꯊdK.x|J6:+7plee"RKqN+2f뗟>1A=az3V(2後'M j3Ge،uS${D]=[hO/DoSl,G[׶T;Ρt2Z_˨70= }7o^wN#O;j{xO!=l|utum0pj{EWɦtыH^#p>{HPΘNΣQ's.NV*T@m#SFF$+i{x9ʄMp(d2ws0\Sډ'FL@Y#qc>ێ ‡Ȏ9eAqDy/rͦCOaM* I<Ŀ!;^,[o Hi4v۪-z3wk$+nW{F=}Q1fH6 䓏! Z1C#w?>tg? ?Xݭ?=22}FHvsLbB51}|H6αGp{zC\+ 6d}mhY/z8<1F.Yր> {1 |@cQEH.Syxp#ל'ٯ5_f^OЈxc!ErI?r銇 p)"|dgg;RaͼV Ch^1H67w .X/ĐDKY_k_SNE܏`‰?~}9∔d#Ky$;c ,Xïun[ҡSM4o V.]%wi.6 {nͯ1u\SR$߫)X8! h颏OҤ}GMkZjw=9t.>;&cW!DxfgKJK9yE ľf61{}mtco-o eZ v'FGn!X-hn5=qb w=^ zj`Z dϙMfcwr>8v(Iп#: b?ީ_ w9-I1_d<Xkx)bA OCFG]`='WF^јgQg=ﺖZ1Jı}:yLk[r,(JX]RD ZIvFhZIrn)OۛɎhtyfΔV4AMNi)AYU7܅+췉f\o 4aiڬ$'X,﫿Qܼk0Vw&t$yy9iwM tl=2tڎ[Qq3/{7vEF45v?mwx0g|-@ Z$;ɧӰw <:W2R1z5?YmW-UVYhiA=8>-^ڄ'8΋ɿÀ>mSFWuLQӣ9 --T|OMnH~ Fz=+,4%pD_a\8îU IS_fIz~Q]ϰr ϰwD={;Lyg:,(qĹ_s\An9gRq:9ZYUֽOJ@ IXdX>[.[K{+B5}++A5?nUnheU,CYRZ4e"#H6 t=ґH6++u=;cӇ&,8ݯiROBIJ2Mv &q"ڐBy_G]fh]ɂI-MtO*vI^><ѴmKфE!GZd"Ho^4׎hAO7mBDq3}Q|qCSpݶ[~@[A;oCc|nImI9`|ͪ=K}@甦:tS˄jw?kd?M ӮjəzUVPP6izY͂ZּL*9 G̵ CHw=~h[?'eX 7G10F;O}EG/w֔1)ozSrXz5u2SA@8O\?ˈڲ?]5yډ|U=є0Me Y3_nEJ1`݆cd˝r,W[鴭et:1=:q 6_n=>^s3%[ykiaHzӴA|.6MH:3f/D n޶;&^{4w> 7nڵҼrdwf-R4s]W. \c;64wނwQ^A !b&ueY`AB Ne#22px7ؓnT$%bJ-I1vQ}B 09ߘ9Z5FeC`\}Um{j8v&R]<2}o0eÏ7AYfvw̕+VPŧFAS:ۈWC󥱊8A@rY%@Coyy妛}* 0 "v*4:>BegZl>S^@(h"};j?[XnРQcᙃs!|Lic:&Y-L\K)YFdT(q<_|MVw6cöpe`W| qOjjT*I&ueM$@E 'fNۺbZI rNbױv7,[~gD]#U^t"Îv",vl dnum4xO9g.B=c0rHgr)7TՍzPtۏ6q샦&NGT֦ML=ϵ(f|*Ӥΰ$zWPk5UQ:9vvYb,(B̬Ip?슬?~>:M&<9wZdQQCxNh {c:GՇ-[9iF dO؈F09K6) BU.iN−|185VhmbH:,Ņ͇LxMNʆk@K؆~cP`7+Ym2*{޳4vl{ed:sˢM3(o6JK6uH7B"Fnxk쁃$k§Ys!4l>:P[9k`4nԊ~4J>[iV17&+gIi抳38*tt]5Uΰ$i.9h!!E:yvıcp($2ڄ(*$^A#56ÒοcwƟFGFp b.mRd2ux4J|j ޺IJ&YLZ?]I*9VowD/@QҔH)ܶ' Ih-{cWI[4-v(@~ ]S8q.5=-.dv# r^wc;<3 g-B lZUE|Z,m;܈i$U (f^[+:\s-tE[/i0 dOi,y!=Z F/)2ݩ w6wzomeiITVM]i*io 6dM(,͛5/:]Q3ny<ΐ8 ξw&~ bh2ߺnci=j\_M>c 9GpLMru.x:$x\=}cH71}tH{JFuv,KqoDx Ń@$OT;c&ߴ"->]=6COÞWcKRXmY[G;BR5aq}#.oZQSM\݌/n8E;?5MV{a*{a$Gߜr]wPbz&N<9ϰf@wϰ9&m%n؉Š +\ L̈́-b? gl%"̟6g1mFKLtaX]?{UPn-{Wa[L[f*?14eu ݫ&<@rFwbx؏_g1LpK24F~Pnw!x}Qgл|mkH8MNz>|;z6d?Eo9)h!-5>HE1$ۿ߭^RHnl:6WWa\.wENp2W=z'*M:òkPwt\3DAsǝw@r25ln 4&hK1zgXf2jT /n30m:d0k |򫦿@^G]"ґB =k/?|(rKJgNA|.C&onhn^"֫Ǚ}k0nzȯM ]&2j73@BT.[=U#mjHp6zXt_vTN-Ov.=s;LWD$N|ܴ^o}3 y|Gx֍';O33םqM&e&Hdlk%s?oAsbEkZ_m`)?z>ق3VA^v|/ZWusC{C΍=MR?| mYb 5*5]@TrۗN{#M(HWoV${t/A OC?=DUp$ݻTq.=ިKW QGF H6|OH˫ xPv*eotd ףK;c5Uں=k1F/~֚kxq8c1~X#꫿zz1Lz }}э>)GFr_MqڋcM{,KA2E:܀ݛf[w}l(+1Çǰ fߢҧcƍظqEK{z&ovlY]v wF{M0fHx*Z4lz6S3jZ޸FM О:mb66s,ɝ9T$Wq K>2]o-yi +ٚVDcۇ ȣcXhعg7[b$b{G{Tzu7AhA7P|95~vM x~Bwd;Uď{CdX|RaR1Ye0lhID,=wF3DSS35%KrK\WQzmuMCŸ4l ֞>7־тy$VYƶqyxë|*{jXiX`ĤJѣv|¿#-ŢRӺhj4m|<9j)Bj4s5e?j#~?cڪ۩|݇?ϚD_zv) 3>nIʑ߼8F^AG\G+(\njZSA@pmGgo$3䗱,4Y)-]j\gŹi'gKo+7`#[I%FI}[-mLV}|83g`a8߳L; UDzLϜIS|xE=<^-8X1m Ϗˏ}v w^AV1y |2V1ZoD830lmj PdG!H}z Q +?#O{wi=0tnG9Fz_.MV܆_{v^MRy{1NWPu|+P915V^sRϗQueI py"` PYIZ=qC}嵄!<#̝<* .MdU TFL3QSP~_W}(8 @pXhsl Zlirۻ֎Iń4kMFzZ(M׿;[]N=U_ǼηswW 3Mg ;H>cS2YoBO~-дF6b٧2/M)c7ѫaIe#"#tТtuLzmA$#w)LG/R4m!S~nE@DyueNQ1Shk%ae&قge{jG[Ljt\9 dƣ"eao TW z<"L6sMQq]9  7ZBsƑl(~jՇ:^,z[a&:t7VQS|~"/ĔϨht6e1g\;>3aH0z 5,()YDrȠI-]]G6QWj kIYY|sFؖ0lލ&_+=p\u6ʗX֡X>h7.B1bjtɈ5lM9ClL hb%)>F7I5Krn>2rFݻw 7On1~;Xm"шUSIP~xoS"z#^۶ŒLSd!7 -U_eދ;cS1d$+V9+m43MǶ "Opt6#дU~(_uQ} ê~zlڵÔ)S單nϏViԪ‚L'"=!E(qSy83qP(^q1A: mNe658X|O7?F44iZ83)= (&Ѳ1Х,ϤӾ~-Kh,V DFU *ПP3q"V+›qN\V*#8AeZ j.r$!1U#;|Bl8)_5/.ӓ(Omn7!MVT/<:7|?SaЫ>>I?1]_3-|Zo6U(A|@>zg!%ׂ   EeC5u??uhߡ]VϱB F yp+A @49i~Z\J.9A@A@A@t. /ax}owZ%:vUO&IP87x̘[qs%A@A@A@A\VDCqPA@A@A@A@AFHZA@A@A@A@ABhA@A@A@A@jm5KA@A@A@A@ BXSA@A@A@A@B /A@A@A@A@*DVvMLLė_~YaY=DDDTOXA@A@A@A@.:jh;y$-Zt>eSNBr"55Y(-+eN w7U'/%%%زWqeV*'NQezvF _l;Eh׶n+|/D@A@(2ZPRRJ@^^.`bI?7V6X^T (\YV檄, Dba@Ie]GK)X<}(Oʋd4u*Q#ysl'.΋rN QaЕɲNyghr nX={CQqQ-h PY%J/ڵ.ݵG>Bҩ52@z!%%Z)%"v)%+q QK_vߊUbCwV!A@ArC@1D z @mnNH rZaQY1(28R&ڈ0SDE8.Lщ+,,o!9*ť,X4PRıX ua26"HŒ$ >1*θlSL:g} 5E ic+ !~t OO*q1oꛍ?o?g˂, X .vhrVDw0A@`0Q~:9 Ϗ*d'̖l$(|댺yx! gWDVw;*qJI .^![GD@A@` *~ gLX\FɸO˕*ƀ0DN=৸% bͥD)IE?.@cc5M 5PvKtfJ(>3itTDnYFrjzf(N)G'(-Ҷ59ÂMMYqة(p|)Psdcۍ()Hs }q5иI}:*:F98AԪl"l:zijuQ_SJ#Oe+svl޲3H0;W+^UKC n>hH rսኯpm+(fI7 R"c]@q?t,1friˋ?Ĝ 54*`2D$馑ItDjz'tL:R 21G?ʌ'ڭ͘l+A91!cKJQ$M?Mw")?Eq#-l?&4Y=.fܔ9Y1fґ.tBscaĝ?&[ΟlFuQa A@hg%ߣ|=زmq[~e[sVvqXr#\0ծ;o<-_\ WeU<5.yZ;$ka"K[{L#"-kL©2J{a2X?ƃEDTO +#]eLT)8Ci>V4u8FX#-ZNcBNcMM5e= 7Whtk:J}QolFe0[FX kq P{] g ?9^XxwQtQgK6#L Pw/_GD75yȉ  p!Vl%%Ŵh>rOa.hULO-:bpILR[DC?E)- s'y&h!M+o[Zy#)6J#ʍ-rLU5K1Si3٥c(#AcZYkd ?E)ʧ*{J?bf]Y+iG#(,*Txlr|=l89qqe/nhA@8+^2kđ  P`%fİTQoooFm(**˕ԲPl}&b31$R>d-ƻra7M'Z7KpnV J պj..ǥ%Zm.lƖn4Ǔ3&lq23OR*W*T3"ȟu_&r4vw3erstҍ'*MlUDsN&w,4&N/{m shTdCS1܈!$?Pg2R]m\~m'M 3(vrFҴJWZi/CU@^ sS/X7\QQZv9tƒv!55ՇcmDNYtM dbʉSjHzlb VKq7flB!ZRc>c?NGWfSA@A@.y r3P h>G#NA@0lyI I?Jh*ۇJi0JJkGWfZwe"B(2K'PƛpD\ m&X.H6KIYUwOwnL (2ޘ@A82\#r5-\7x{!Qh2d;q*i)IhB&e;D Gd2!Gt\(5q7 wEЕF l+GUHFi2IB;YVBm%ͅ64`5&FfK7[1!GBT6]\ɏ3HFnLĿ*"0VZLVB?K֑6@ţ9ڇ76Qyz4jcƐ'aO I0MxKVOɤd4 \ؿ ذ4ybcm64Z!BϥM0=CEcb%┘VGV6.H(x6y+/HB" a-@_ ‚BZ2wbюdF=|I4Jz\hMbb;S鼌 @@s?"232(V+ y`M10IҔu<m)njjpZ5Gq(iL)'e;%%4)hݲ];/huEr}:m)Ւ=!z!50γ<{F3[N_}$qI#4d)mhKKK)ҋlۅB²-RGq._ۺoi>?$[Ƕ$=|u;$I`&v9{DBUM5uY3㿂s&S9U#]ol 7Q op۷(m_yzqI |*7֢|\- M\4x.Za F@#/5Wo3.|+X\- Kj0@ \1DueXRקʪJ9ƠLu".ǭ޴0~˪4d>PTq|9zu[BҩȱSYmu &Yv-ȴjELb?iI ',r4Tblh oqB3dR X&SGFj*ʥ'LB! c ,N(IA楸ΝX֥|ͪ| Te3 @QGsQjԨ#FeH5.*g6cl޸]Am]drOC\fKTE&S4p)*XBz59bFݖL*u.95۲VZE@dؗi@:F<g?{' c6pb @ډ"b| m,+'ă:PI3 \K:]F@#XFT_#wyp[Fv9 V|%IgafEjHpdT ֋u&Z8v D]xlڰ 7J|rRZzĄ"-|m'Nɏ|[%~xL(BDH~;IWjfKd`9S c/u>̅pD2hF9Yʫj6R.M SOz'd6ɎZtex'Rf:y_yX^~K&_ԭ_*j9#yfX"~Mvog"$ãh0lLߝ$4oerM77WKZi c|NS/{6îZw͛Ϋjŋ{C J^ N~g֤F@#LE w~BnS[ʂIPР!́@v'c9rG'QJ,̺E~ZR \W8pS|T٠sct,$cc8Q4٤ $d$[!IPK!b)Y,<'LJ}(Y}@V,62s25I&x4 ʆ@Ё6j)E 'Fx b5$'rT1,3Hwļ%9xoseJz*K,fW\'k02*ȉ@ߴ$qi2i>A$&goBTU-?(cv_#I9:ԙˑ[8VFsXI f4Fխđ8-Ͻ !U~lo*N^{q/J`rj;3mz5ӑ<.D) ǚ6q6P &{ln+Ȱk\s٨klZzq'S#3C<_{X}ܬwE[dK]w) h2l<r̖cvun`^=9Y^jLF} ȷ?af_˖m7_wL5ʕ/OWN[wG۩쨬|kU54lxxpH{ nw|x'L#R[[ր|_ \QwG۠y@NLN˜ۑH(RB>;pllw/y|p.Kk4t46"`A 9Ȣ^p=n[l$qϧ8*bP#JChŏoλ(He16oPP8P+e48WWK2(nNSkq +8[0!# QIk+8 "骄ήC)aTso-A*QfR2d18cTo6[n08탪ME)!ɓAYPhFTkd8`Fͳ'ʧᓲaf+[6]1K1{[S Hc-Ok)yQEvpCx Y}TQCׁ:( [V;%ĹK%WOH7I#Ko'2e"0 #ʶ}PvA1%U5˩_ )6p,ԽEZ+Wx^l|\Qi|9z.p6GPxֶ-krl|L>-r֛fK}^Za>8|?gik|MCVZ᫇_rej[}@yyli ¹yѰ]E9*dTQg !pG쯿0k+;&csfx6-_2^)=HxH>*FTGZ!c| ^|( O$<HT:qɵPՌ[fd}r:攫h6WkgF6$b2P^9̥$,21I`OrJ/6_ywØcd&6eLo_{񩥱:_Q㟏oȈA wV̲mKY߶Ir{I,\0).^՗7|Sԝ+!_=Um<,2W _D*YV_h6x]~s9z׷Q/ɶY#|YZ-υ|N|m}4aށ&@ `k̺ hJҎoU} $8 @WŹ`a1aQJu6>`fՄ("@+9&a:V/$.&22\Oz]Y:#[KuWɁai\f1yc:ҽOv^AiQCZ;[!rfQO^yV@Ncn3q;3myv0\sYe$s*Y4BG^z8;:6sG66oʮ[vYr '0;R^4[*lxWO7'*<'h^ Yzgݚ{o}a<[8$o_O[)Cw7/dTx٠p0I6W7 Pia8r*Ǟ4^W{j\V~oƮ{i=<1p#$[8h$.!<ܮҊr Sw^`a1m$?%JaI A rl%+-)D4SS7$**0z H:{BdQ$`F!1lVGlORp~'+ LQ# >ز@eɠb N^R65@gaL9Ab2p"XR|]+3Á1nŶ20QX9ƒpAոk~ [)N :kW&g$͎/k,VS.]Cc$X^dO݃>dR`m.$ {6GLl7>#ymlgY>:i:߆gO|zJwݤZn9Mb\WJDu}EX\q"96ddS'Gs+u" DZ<#yydע3|RX^=8UN Iۚ~!i7.%#B%=J*Φ\"tzam7ӃI) 5{n)+?=ϗ%Jd\^, \Rez>qT@EV/+KN>sėȁo NuK=z*My!_;Æ[ ߑ$yr؋b-Ur}.w/lcZ)_^Y~'MGNa[6lؤ([$+.)IN ěYvj1B r@RffY$L81U[MO&.V(2H Q3ONj3N2`G+b† `( XA'l"G*6m'}U;6- ?m ՒuEm$J' ҳʮ-˄hknj{@~\c4m_("M6s#__)5e֛&o.XBD[TN()#26ǎU\/\3|csLɕd?(v6Aƃh*ų_.f.ٲ룲6>Itwo*Z3tEŋ7S? H8y)O9Nn'KܛO\;Y^ av#S\O~ &Ey<0="U{~?#(4Vy|;?X~\Œ7sMwFlKfbVf]',8@PQT?׈I<|InrĘ0cRҼ›򑑻su%LqŸo/ReuT\߸WֵS׬n~Hty _ɇSl+! _H_0 `~(}d2ND&`Ul4#F2WtH&ijj)ވq,ZuD1Cf?2;C0 pÝw2=$>T\p-J7c X gv%H7+8R!& >uPQyڄ53`H6[Tdr-/rNG':$مO 5i$cYj Ono[P//FFI6Hth%3!( =>G2 Ĝʜ (qAY@MF&HzD-ѕ G &sK% ,5v$28|إYl:e$ަ2:"ǐف}\Q*#?K"i, *43'wʂ2#0ac2p|ƙ F'hj3BoL,ʗZfA7}IlHvL.?=*4KeEd|D-Gcr sXPR]ou~jI(峭:3OXH]6r%Q\7#}7XY9 [ʓlkG6}Qq;Gqe3|nvj) K.3s~umN(̈́ovߚD5NtLJ'GU{:!\za$۞wwʽts\pl?@]Myaܮy/F4T,n/}#v00gGC;07:̈́"ب`%`i\g}e~J8euޙ "=WhX^E/2)9~F\~K3Mr4MQT|%D4lqVM YHNQ+l⣲b#M3e6kZ܎ wI} CN<`JY?Ζd4BUd&,kpQK+$җ04HJWvyKDeC/لFoQ0I'\X0dMJ)vŠC_(NOf{USYеis{.q;rOV!]\( W߾k|쭆P7hag"q߸&?#ϞNF@#P*4V*$u=Fd!immUf݈ںz2vG $Zq6,SJJY dOJ< ReZEyRuc8 J(@2B#ԖˉU=W+Vɀ@eAdKt3 =Z#H+H3+iI C0 _m~PAx0d 8ivTrY$᧭LQan$')G8nmPYAeЎPp V 6+ ?N(P) Du*Bhr?l/DF9+ӯx[dcȄ8pp~bB-rC[QIi[.47kMDZ@C2MDh| G)-ߣH6GLfꗵu[$%%#|N䇯H6l "7>"?n?T%|L!gk$|nrj$$7v3]kߡH66?jm.}?&pKJ?y~iӽҘ&b:"@ߝtD Ok A`ڷ^4݉g-{gH;<!mmjZo^!5E|mߴypj:zvRfr[?p4A/ JRyְ>۰~q(Ӧ~P rs5wZ{|&zn*͎s"Tv_tⱨ*h4s@`Ǖ#6":F!`*и'}_A"P6;8x,{R;]xCJƲf(_!HS4 Bgt>Z%UZQ* %<=@IDAT(ᗺ ;F`ة|K\FMeK`p(4}9x  H|)E6SQ>Aa'5*u[ L$ed+F @:`b0jq(ʁ IGevTdoƹ63LF dӌ3YN3#ڼJ3 TSʬʽ8| ԏsh Jֺ~g8| V bKzGhWܘk{[}p PwUM@II )D.eŎneH>{qSVqj}"g_ew=WSj\|Gj*lUS~QE@ѧEnG2 y;8uN7M ;yW[~!]U?*Z3{N^ D"jt.W֫&ſ4S^f[(J%Rce1qR'F #!i2(U%kk/?헶?-;3߻5~_{z^(dHEIL<kA;qf<Òܵ=(%[7C7}}>eR\zzehdZe:ܱfDD\YY8Q9i0P6|S7Ӆ,F`VFٱb$̌*Hݤ`EaΎ"0=)ʓ*ex ` NeLIaZX7Tiv샦}rE=]4l(0&MR9!ј)sB]^:I!)O\w= i's).=g/ۈMr jN9\9CTN|\ %xdEUEN(sfA êq'GjV7S&ɖcq6iiDnys՗Skߒ@ 7+2]?o8"˞?~Bظ;\m:F^q`6+Ã"lsGFbJO|] T@7^'>:J RAycG߳+ȶVm9 ;Iy^w3S|%+<#;w3O|f p oca ن/$l5b}<{SvTe00!' LQ~v~eXTE3yeoI}Gw3C &x10}|u-<7.lGD1 *(|`2نa e,]6[ᕱ=F#1(J0"J,T"XD4hJFGU`7"k@Y"b $؍Ϻ GDvEIf~@>adR5Cߒ9t?ȡ[ 6aX<$U-TR  6Ef9$"pbJIF? Aj@?9AsAf%41@ %ބ:`Bw wTkпJӔM_LReI1j)=D 80j)lm%l\6c62xDr&, E谄;0.RE !uzQlu_B[D -k+om|jxErF@#'%I P( ڕOuPߝ^eȹ,3S%/[ \MFI V'K{{ KY~"#GKV]6T-TR3G@T%itPNKKKl\F ,ʏ-eJY@DU6\,PZ]Vjw9afźUBc}^7ʢYp F /mފ\$j pTGAJKB1DBy3i8yf2G'` /βDNKK8Fl,NDȀ?:tI(<ei1f2/і1MozE,m oH嫓F@#D0`&6//Dr ?:-cCߡW4i@ElHr )}2 CNB9BdQ!4rZ+w<K(*9~[i2+FuŸx`0$?E!LJAzl3O'vk)*)F*sPE൮MVhYr GVHz:1I&@ 0SE6T񡃜:i4C?:- ȧ$hR7\+?:i4cOSF@#gpɂ?'Ȁd >هGF:l޲.(2H"̚f WA4ʊ٪h:Süv!I2OLe1 O$( p@eU+fe<k.7F7lX?BK,)FkvE%&<E! d ,3ILQKV;S& LRlNjR8,&2Q9gQ'Aj9'i&`* &CCgHI PQYF Y>f!@}v;nK`|`'o:>ctA)l/ Vj჊AJ@uUCru˔}zC#X:.LxᱹmKxu)k[ ʆS Fpe(M|h$G5@i Cg,<:CPP,oWPV1a*+r$H{KHJu6RDɅQFYt _5ɯ†F`plĈ q3"[らOF|J(2֋q'M>s}6m+BeDvbAGaN+4}Nl8^ $hcAE.&$4Ɇ;@ԩ:sةqE]i| # r}f؎!Y H5)!Rua8D+@ٖ6Ed}T6k6  *6ƣk ΜPerTmFQ,x 6M7:ݳ*w ڢa!V^_n,Q]rB a'CzE#h. * +aM`$ ͭT|~?BpGI5cNQPoRB@y&k"8܍A:6˃O XF:|EkdE}x@%Aby!X:KJq9iI 8YNO: άTy}JL 2 6["3ASI !38T@a͆dL9y"ɨZcyф/C Aϔ6*H@:FR&_FH(l#3¡_q#RE=:-ܒ'zɎkXxF@#pIkL7\=%$~ |Gol[e@lbLz^9e{R7^Fj4ŃMg=SNAqt[/]0Y(2<<yhf26zGȑW|gygyvojVxfq&n\hclH?=ϫTȱTKـͤ7G~_ΫmT9HKH4Lž2%ZCp3T~Q" @sR*Hŗl26IJ, Ƈ&*H-*K{v'6RI0$Ԕ܎DJd q jzYVP9V4!MB/-(%$t:?޼fyJwoX`\vByQ^?uiF#DD?ENўoi_oϊ/)إW爀{o7_/iٲWՒN%$>aq;[ ٜ.{۲Á~~mX_Kk4E@@eddH\! t鑾!9|رCd#r&d#F$LRC*.{y&12A ȿXvN ~/= ,8 b%!s#hfpIDyṘg.Jm8QF)2+5s\ҊiKFP}ĕ~ڠV 0* (6O@ $Z3BÌU&FE<4CA}t17TuUg'}2 3Hʱ.|d90D"a9bC'26y!`GW8r9'eWZх5R#gFw A_`.kv)RR/˴w?t E[.c6}95ծ*5-U]FƉA<SYx1wRKk4EB Er_i5UU|ɉ'F75. B{c~wOˆf$|> osLG@VH1sR,'H ߵ!b<'"*+C 44>4܀8+SI ^$:(bDLp!@70CM0in)p6hDee`\N8)N~ppB /e(uuLPd$i&I:;qbYf~XOqX']^LHxc6?Tl~Ȧ2" qb(@3 Ru(l_G`E}/ O![5h4 |24i 9%krw!]|Cuu'qDZs#}]ٻAeHe\b'V<18QLJ 4.@+$z&"3POpRqdu }ؗB~bTۡ?>:7M+G+=H^v 4 Ulvymw)x),qЊҠKe^kwn\Án&::}k ז 2Yn ?=>iiZ!&ܸfE9uI#h憀I*Nx5>e$ b'0PQ0CFDFWVcRYFN*;\dbr)ǍnCٌS px$\Sf9'Lg^Sr,L jo!Bx ٺ;$b>;PŤZyCs<$To[U DHI>ϒ ՌE"|# EOFE0X@!E2Ɏ ?gd"I*e$#Crl x!d  OQ~p"8Nq.=>x#C *p4)+/S*suI8eȧTn@:QxpRc4U3ʩJ)]oe9qH08T%jBW)/Ͳm,QS1~I v{Z b7zJe;$}sSkC,? _5/޶WnZU7uh4<2m"rܘ%hi 6| oU^ERP520Ї` Y۶(]fPd@dD2<뫩Q$Hґsf]jQC~،fxߊO$Ҹ%6’ )Q-* Ϝq46(;>C GbμO<$&#^~\DBmךIȷaj\LX}]R:REU9J)cAΥ$/ 𧤿 @N aieCQjd#E s2 \P'i VRB͸Hpjh$UgQHS4>>R d"^QiI[FsN}YU'_!? Z}E To4'Eyׯ`IwjrHG)i&С0MM9'E#hW uh4r@`5ۗ04%F"~O a`DexxPi]ܚS[*jm H,؂V ''%-LJ9FHJ1e3Aۆ=h$/tB2`Ψj#4d2#38QLxyU h#y Q:/eI_lX$I|AQAY"uD:#肝3/`CQ:MCiG*Џ1HMcID+Jnj,oy,$ٸP7D\g8ueKcuLj QUE 뷁ʂihlVnts-iVӋlܴД:_gb8VR2> w!'4X(Nt&l @*/^6BI" @qb3g+V6v(TP4jȁ6p륉i"gBp8@GGjacNȫ?? ~gA8I5Hq|c+IGth4  :i4F`9 ƚ͚hˣW.wb$TT!R Yg>e'7HCCճ?I"*bPXy $O* ?^RG CpZiqiU&Tx>}|:L$ r&zy)"OAu$ `)8eJ\(XQ:Kg'dttT-&Mm-2)-i6KW]i^ HPCv6(7KJѹ0@'M>X-RU8u<D Li8f\4p2FK#OR`& 'qIUrD%ŝvgb'VY] AeaZjaCU9M`pmƐĿ CLT4F)C'F@#h4<?6PJ3dpecH֐uwaG$ژzzz`-r1xjA1 +F5Hc@%9?<s-bN%'Bc$ڊd+޻gF@#XNވp\|F@#h4rE i|< $xZ[(CxO@ZB..A2h`tXjs GN;vI)-^Ri#Jd2_"H)2 c$xemc[V$WcW&AM#L@aT 8uT2b gn#Fq|`j_cH̑$c?Zfs pE4SF(ik睼9^ַXSI6uK_F@#XB?fͷ^j4F@#,/` O)F8܏im A)UJr* ʪ*fDWʲD"8 {p?S^ "dzs5!}Z0`YH20ӕk@8gϹKB555ɯ_~4F@#07pQ}z]#h4F`!`'AC8+ ׎mġ'9pټ?BV"WC9*ΜP0&۷m1D(}♟h׭C0Bf<2a^~RlaGe?4ub 7R9SL"\lHh!Qa.fF&5@X4쨩jl/S@bY}Ƃ2:>~Q|TA-strQ%9ï>uRdLxp^RTG%9/weAe4)i> <[uHY}t-%n^#h4D@)pǹ`^h4F@#XrHK1dɞTAG)]vJ8FW^`e8x8K[7N}[ʼ jm \lzUw¿='(lQQ*$̊-hdZV2uuZ`zKEMHI WNndxX`R Bh`50դ_7q4ȧ8H,U:`^i&Wgs_%VsX@3ZgLBa`nDO DT* ʾ EnUUWK+Rݏ&1vr|=3gN)k$ZZ*men0/m<^v0tph@1VI08.8}PZJB}s'N,-.}K_Z:=h4Kh'o3GƔs|>CIntd?MK@"wa5?W.&аBh _ Md?Ł^gqQA0GlFIC E4Q\Zn˭p7 P?@}.Ԣ0zF#ʬj2KnPx X0/%鄾x<|Kg*O$=ycܦ57=,"TIʑL@IH:@1hB!U|T% 18u60 qhAlS"wpA<&B$h<~G\eeG` 8Tl$l-TA)qc !W< B-ɯ>I_~y}. +}^ht2CO>9(؉o@k _ni>Neߋ /hJ#]}7ɖMm%\rתl4)EzWxP8EjbekrYXm˂ Oit R$,ȣ<>qUV\(  BkQ>2 vd)[$h ֕`7b07U*1O9ެ)L#)Rue@}z 2ĉm2Of:?1c?`b\ƀUcoajc`.J.#:fшm xB ԶjM'aqp#ٶSI6sm_E%r9dOgA%mdZ+ΨK,H6`i"OfQ,"mV0T 1OsZNAkG-G&\O Qqd>p$q ¾Ujg FzOu#ر㢶{1Lȳ/os|73I3 !*J3gG]uj%$sm'yc ';$;,4X$*7 Q(A7@enY>z7|`މg4H&|| \*ߊV9I&ˈ~ٚ໭\GayF|?ý,M!SɆ`4OQH)kV.5JH[22o"џаtvH6]gK$a>yf8_T)m؞hҚVf'qU0-.HؘT5g32,)FvlcFj:do 2Z,DՂ|@ǁl6"ڦs&p?4II2a34Y&ˇʼnW. g^Li c8A H:+=+2(d,b)3裏͛/DzQCbʊA\0Hs!#ߔw>.XR=~ Їr7 t/O4 = ˧l+v$>'ǎ+EueIj \V]PHq;.y%1DT#CPU[ 8\ՊaL*hX_jxWa&U&VKGIB洺`2I_oA4SLǔ[#9$]#C#$P)/T`.;HyU2!$LN SYA) %vHv.crq.7c$Bb _v*##*qv""]FGFU^0X>~[x<^%vz 7H/~7协~1Sڊ@,%NmL}I aDސbqδ$ D's sǝ73-guXMC2N)4:JMXLE 3OKy&KZ68 +FDa}6H9 44f/L7XgU*5V.eeUIzXCuc uf BEV12헰k.NdZض&`qh+Ad?( :P8 aXk_6!Op@$1A#IE$TDryk3egnf>|)TPdpZ &)Llf)}}PAI'H8{ϜnٶmR!NBSu$wBo`h*qm3}_kY:{ aMXdDqe88:"2(*ʪh@ a_BBKzߗVJ4I'4P:U;Ww*:TrMq9JBy(4ˁ/d&x޶Tl#t^Y1^mٗ@ڭǭΙ4ƒj% LrTBot9vK@O kDu)&*1 ʩSz2JgMtb7ٜR&dHӆա\ S_''b') %T摡aU[d!ӡ5Vqvg:]u=]yomSI뭷9kq.4*X^pe ;y8 (\ |=%5ă4@IDATP`:ޗ+c$?s~D _bͮ~V5Ԓplzz< d$X jdGlBIJ"a`D2wJ-w#dhLW}JGE`H?oLD]S%XT侪]]f\;sH:qr;FaThπ˷ond 3rPo>B:%]CCۿ5f+Nc?d)IKtmśxb x A LN8SN T睇WWC,h^ȉh|WaNI,40ٟ3o_9s'_m|;~>KDK$9>q?4Hl4l3[UU{.F2km:@a8Cv5׼VM8ѧpy,bsg[GQo~xEP=6 Z3 To %K@jG?qĺl]_){띈IDeG ML;1\Ynd7轶W gWo=~=o]oɾN-hA:ykj\D^N@(Wʰ[Z0lAҸGGr-)5-?r>m_F3 $M \Μ*CAt<23ԭNx(@nlJGGމ?wFf-_Am%N@yNZwk EZrSE_)r+[ njq][{R)ٯܮ]qrëDVXag> ^í+rQ/q -0) %g̰j IN2$I"wѸͳF5n[@׍=n#e%%ŋ;Wc}\IJA ֞HBrA+ K`^Μ 5W6XIi 4 _@FݸGVI\n"mH=<(櫇~.\ Щ~crCZ8еk~wBm}w({l~/7}CcOusu[/q -,H@HttyS_`d8< 8f,?ʦV}h/?-(3; !M9qf0PN^ ¼}ff_hdƜ;{.[`EkhwiJy(Zfvd#l`(ă,Ve]- E]ŬȺjy(-hndF[?72Snsm`jvB@YT^JP Mtt0edCP}(F[]]%N˓lrol|{k]Ғ2 h(lU;^M/<~gA~c8ĻlοlQXֆqKU7'*Ͼ_J 0 X 0K ־\L , ;X's WAmP/bEk\z@nv<&EM,d@A )oadb};zV=ZܫK0aq1" ͛\D6sLKҙ1 l (H#vϬaN v'+jNNAp5yQl ٯiQb袋i 'nVk[37ю/-^[k]hӵknn׏7h{9BD}&(p6 O NJ*kh~^HQI#VHED9r1(޲?dLFKO d̥$)Qk%!bЍ{% `)b@aBc-;8`YSɅTJ{H8o~[fvq}W?uDfdn%A${p`Qm2NqBAh@I Q˥p ù>;g.@0^~y#|۹Ѻz:]_|H(>+DK D99i.mye |˴m;wr)K[X>ۉ Sq WFPCiKXxDQjW,;=%pOEMb뭧bm2OOL)hs~*VTbĂlZ8>dy>.|0 ,h}Ou @1TBcgQQCUy;VoT+\J/b⹐Zp$0M'F|Q@Կ9-9fs!{ #V\n32Vo\^^?ʆ3fLMLcqtRKC#nb,05>%-)5f>j1RKQ2w7q -0?%a*ʿl˶Vnz?~6|+ce9O-4\_1房[UY`3zUMa*œmʞ`vj9/+vzCc'[UW-J>+ nid:gR77eˎ]ol iJa 9s2rc{;}n#@2? {+BP3!I$g!2(KR81_0G`xsCr@qRzOA}C@CfɲSO] m&˟2JyَJJJ:Nx:Ӱzxڿ@caa> ˨#ߖ,]bXuj@;?S\}{:Čb: on,Vj>{&!M*= <&Aj]ƾR Go?\ay4p'7 tZ$dAHͷg= gt&^\ީax:mmh*6NE|%S!q c8A Snu N$1B[49z`iZt^ts[i on[ckp$rl~)N?<]wuSXkx3Zk.w'ugzA!ۤ*@q L`z{Vۣȃ% $3 󳬧6t.۶ۈ-ItRۼy{*WVFW^!95V ^!ՐUI.X+(C6H2U ",TG#rFDA#s@֜UeUUbΊy *4>%]]5M,|IUб~ AѦhI)( Q%ks.^rmzM3L K'qBA^%nMfg4&Z$6klh@tہɽWj_[ n)G[tiԽpʫBAbcK^Pu(yfU37$YAؾe;`ʦM.n[ <s헇TrXKjS@$C(R}CoCG(u22ӭu! N1+.Jvtry N>bA1Tt.z>2 +/˰bŵw>#W)mbqd@A7dgjs5.7$?郆Z6BFStabv/Ds=ȧȿrڷ gi)<`>2ЁRWG/ͲA-"C?Nn_9 ͺqGZj9 ' t:ģGPI¡yE~x"*J`wA{_[ n׳ȏ^4ĮGzI*}vCV_!n8&j`޽{G( 'xcU>-4ƒ顇z~QD CBU^imϒQt%Asc/m ( Օs'ۜg@=J<\3ՃZؖ#==ӹI=t+^[ckf27Ö4R@Rr Ceoi0nvہ`CVR\% >"WvfIҀQ`;Ý4G/$k&N R)&DEjTjIɰ W-e=U4mmm#h;9HP:vݤ\þ:ĸUi!Ms#CmA^0%$`*qqn߱U[*}sb,I0dS_R2bg =`'{ľWߌ,h d{*A͞;%b}Rl,([OuJA0f&%680'4[N0d'vY| PPH \4r$$![Xč؎=i#th_'Y.m ),tD|2< d!^QNOiF wgwP.C'+uŖx@ QZzjhrQ6+'n; -zpꁛ wIΟ?ߞy.kڅ^8+-@GK /9v=7og_v45kּj-@XP6Mj}L@X@a-<2.I555{_bUTTLT.LT 'J>JK ixPy-&n1?\.u&X$qje~C&hp Tiw-T+u[ /$& DdrKu}. :ɖZH\z++"4UK.@uww R)aVv.@d8򙳀HRD ǢduܪŖ HZ.YWj<[q 0-S$ (EŨljlL̙(WF- .1R|w "{xgΘG{brU] Ń_ˌ}ì[j)HҀ^ͽ^ 4[SWdi=j0!H~@JrZZ`r!B$T;/t>:5icuP-l',ʰ@v V|9GEyxcA|~M4BG.%%=Y;b# lVmmXGFK!Semq3llWN2hZm&K p!'ʹ 5V[[2f (+t4Q Ec (1BRx[ n}k]7`^DKq -p@%f;e[`*, xEEDP+;%*hm!k&;Q_6 ^2Rbxn-mNqDL'X`U7EHq%8չsnүp Kv "T@!֭(v%R).2BR<m*S8%`p^2ӖírڐDl8%_ )01늋KPR-4Su(\$ i$) OnrP2LqRl%!u#dfJ݋'N M6൓UN@/ӹJWzYeMOzv5A;Giv( kQ,f,Cp)J)_'kj߷ŝ&wP}{JmZ"Hxk #, :a!NNj$];wۼӖ.^Hܚy2m9S[l4䑩(ۺ@݃j9$_N>}> 8$uqUJ7e B@̀fI'&AZ I; OHgYimQJΊ Wc3s-ŏT?Lb ҆6{V%{@TũNe.YYΧw|ө-@etmjTmr=V:Bz 7x|Ӽ?u]=MY\V(De ms책k޺Μd;}84Qղ+,g?RQlɜkWۉE3zS.[ms2OoΛfHhفk-8>wb/Ϫhw[ۧY-v*, PͦM\uOvgLEszmwH0Xl2mqb{Y8I!JhrĢXlEe˜*TT+nȅ+A"xC 3TXKT(ƤRFO9kGUe⢙˥\4+fVt҉ Rs s (2fShKmߏB8%p.:PDKMC f@SfzE ZJQ%]50po  ᐟXsy9ԙMl&OB(!N]+7"[j>iApUZCijs*W^^> ȸlk%&.e.zGK.ay6p2öW ŋܹԞzh>roA]!arvAPΉ ~k'iJ Mz֡z ?Թ-ZSSgmrTTh#]ԁ45)˶Vؖt&   I|*5_@GHLXv9K!Ö`s* ,mi9'.zodLCzHLmްV`dHM"iB.Z{g)j;>|K%\G; n(# f=֬Y3bٶN$q˛ ;km{۶t;Zit~=co VYF{𩻭SwǃӬn˛XQ >&]\4SR%EPrEoNp]iP>|-aof-9H2Ot7? kvhkN,Ůzr[p ]Jb @`ל[F~C!=xꙗm9Ly>&FS|3vI#E9KV jWÎ9i731Pmم/O+_1{Wl7|-z.oxж'vٕj$yn/ntF@C5$L4mWy-A7 c\f͚懓m!E//6>jU#2:&T6`d~\$n$/D`3[qqGHX0H`| 2a*\R.eeY>J.^W2rT>D8N~ i䊙NdtWWގ$r¡,qr+MN@F<9Dcbc:kbNs墚RS ,݃",[m~a^vR2oIԳkbu8ݍj :mm+*f+)).N5v)2j)5ݦ;^.KE{e_J/$Bes#fvlnX&0K$JCڗ\sKU窷[F0O*;?\WIA_8>ԎJڰk[t-\xlv jxwmMh c`&f3dV|Xc>,~mjʼnHZm2Ȝ3ܲfH2qV's !2Z_)$KP쵞ȁ" ct2 )fiC2:md5G<  ?IJN.\M,`P!BF{?N%FP Kf%L7Vy'\64R*7ڦukƽ_m<޶mB[mV<,7|;}b18WagqP-mܑ?N-Nv~MG{/hCz0M@DQEqQ.u&M=j͐I-ģ7S!܉!3 EtxH vQIa5V_Zéia|Ĩ6JH7ٽ5v1PfoAuI6hn/ 3GOP{=Nk!\˂ŶtNH̷wü\k(T{I+F(9pѣxoFr_;Pф9a|އ<[Gܯl?Ń. h;;)O?ă>h^{T5msP؈2D\~L싊ZZI<00,VࡧdSHib ׼Jgg?F"1)KK$9EZj86)(Y$ڻeK5qĩG!q*!+.fƅrT~,ʍUPfX2y'҃xFSI<ELIT IHR,4xbe쬝rJ%(xWTTH{ϊqלDrCu׃zncTOCQ\AT7ZZ kv]33+v<innmx\p@X?*t!{DjC:J@eMaXevrh΋K&̙Rܹc h' +9.hmq%d؇t2[@FBKh!E>~\> bWgmv"hcum߲PYO6uuKs-39 *`aHS' .Xi!mL'*W;`KpQ .lDm͵\gy4hl‰C%G 鶟A\I1c*^b .`ڵ5ޠ)fP gyO  WVD&ʇўU~µ߯;mm&߿- vC~ؾoY GZ\U:cyz<67?n+m_b_e"ϷU'msGw0v[ZWncP&7h>! w ckts<`;ƪhj~nl![wwߏ$l"ї_9߃ׯ굳W#z5OXz꿶sq^ѠX[O' -G/1zSQ]m}.X6SQl۞={J*)2Lv? N/ÚBxƵ[nQ.\oZ-c8ThAa)4ٖ9=.E0tN /Xn-dlt.mm'S(FDRoO"IfT%eLⵈaM57! {l@0·(=E'>P4D@Lbw Y__'ˁl~ އ Pص7Tn}+D@/\ }e՘[I |xr IQo ͅpiW1ovra V H$od2;zŚwJn=8^I#՟~_FHRMdwHD#!n;!@ݡڠ)+:0N' p.XAzB] z9`'g%e6QCcի%\t=̳da 2dƔ⺅P۠ۛWD`A2{;j7撤 ;_6΋t_d $vp2**)@'A:XwKx /Vxp*VvOn`zw.:փ64d, $y?t; 00J.Y4ȑ^eݺuvǻөDz-L^:n鶢"_a~eon_ \K 6Sʝo?;rQyK>uR+GoC}[oSV/J2Ѻ}Գ%'.\@unՏ^'z}.8@}[P2\-@\fr|{N m4Fچub\piv NtPk3;VYlmMd ;(`Ûv{dYV(0bg{'[F\Kʹwu*ٕ'G_mS 9s\ >3meIp'"*]:uB[P0v>~]V5'[E]'swe{og\j޳gէTݩGz[ ofYb$u=jf_A}ӗ-؃wm?wWd,ln?/m-wSaUO<=:T[=r2UCMRg#-VMrɄ YC݋"0A .u9wрS/IkE׍,C<S_'ʵC!RMwWڌRB! 2I|7JJ;-U. !=#P̅8lBrkM2X7 lCn+G@@]TFwq^޸E[lڷ2]Irug{j]S Tp#,JʣNr FV=`3z, gP 1PBe)‘vI&r8S{ xxcc"2`[QcW"W l[wTuM7Mf=d{DženoBv={Vrrk&Vr+ͷo1>wL"%!V>"u,?=xSоx}FW ܆~; a7=)U笴~3b9]Ąa0ox/xd]q&'+`WsUD終ӪVzl=m 3;Ŏj6WߝDdkmO/ħfndՊ,p zʩ9`g^4XW, u]z۵# Ga̵ irn{*b #_8?_Ώ_q,k+oKNZfK{"r˯@{f+'/þсmS bZnRe3b˭qE <>!d=?w9>{fw0`6@LDq)}qɖ'%̹Pv.2ۉ+#{qon:mm_>쁯_m%}{8)ˊ]!*Wk֏ݥ"ߥ\ei> c=cO]}][9gص˹O<%ShŮa5ȜUlg>icvJX[m|;휋GhK}7 yАm#GEc/zjo){E[csA<3hG=6*p2  ig}ήMC^㎦f ! KE%1SCW)04P M\W\$SDa(5.k䥥M!l\ۈwV R0O>|UG0iؔkz(+e1%)f'&\;~nġ qe:̾33%#]SGW'j:}N}Yʄ q,>!Bņ-$7IB'1r,x;QRZ P?ԁpW⸕!8z&'sŃlztr'ܘbY eU'ػ-A[ rf3<~w ȏ>y}U g#>݊y= ܛ#1@&˝s]xb6V3_w{[~{_ U؎[r莫o6~ .`{Lf6QOwΝɻv:{Oo/}1`wk Of({P!pe͞eeYnmkU=}WmGilTsW;ewnxZ dC9y5rc+4G];B̓[#=AP#hgm-?nFMOIԍK/EPB_l ~b[-;uU`dK\]ao6ن;]Tae2Hwx,f> ٺ[lEaikV/v3k=m+:і]5o#sGdG-I_"Ńlgszl) lϰ?~?–:|3Hf]Umֲ Gs~>1eeY SyzYuoNsGZ /=]|ɀuuvJ:*~uM` BGV~lt{O{&5W 9{5?Pw>_=qͮsNmr~WoJ٢k6m`LCl%;}ukxۏAu5wƀo"a}e]{mWgLlXlum "n=VgWxa;CN TmK]~yI/d |hb-^jۧ?&uXj׷LjݣDl* ${[6{T<32IE(m29 a55֨NevʕD7(:-kJR|}P@Rw1x4^@*nB*<ܤ PܵCV\<é0̡E\n;[NC+si?ނǕUvBo f)9sj6(0ہl!Jy7y͕RJwA SB^h@L@)68o*L\iJjQHAF\WB!DM )O@~=Ė˨>{bZBt:Z_M*sz&[F ٙ@IDAT`P$pYifX! '`rAZKCz۸?i.ȡ9T';M3N`W h^:X Xyl%iJR.IH?+gխ]TP (0j@Vz Hp|cnI*.~ E\0m , C*JR -z2ĞW"]T?ho;^Ò7^Cyq][7j-w; hw'~(dj\oOrVzMv٦ E)t6kNˮlm-UVTy-W,.9/q,To/ǒfl 昑7|ۭƸ)Yo,vԓ͘ DەlvozmK]gE1ѫ0[i]P.?_u?Tu(t,eHG﹏<s1N7+>LL|> oQZnߺQ2q)Ό6?Frg9 {wwd QzJ܉͞O쯣s w> G2Qm½vwMdY[#hPӾ[w:7O,=cMW'=Tg:ʷ.TyǼc]c^7o~"Tu[o΍v&1yΊӻ3Nݡ-0 BVyb}*OU+}}%\}_}$ ]h}o87n\=ށ޼S#;`w1?Y|ݜuP`eȶ@,[@IMBW"‚ (VuMk;Ks! q0'Zm&*;Tfa`Q8L{b}T@K'A 19҈k%c X\G{sXrdjDnBTa,"%5Rg ΥoL+%Plmmwϑ sؔxL@Q*:Sۼ?q&[ۼ:rRyCAH^')uXA;z( CڀbC#7oAF2w #$#IRp5so0lH#x hCjƥ \XdLumͧ>PČ> D+I;N,&RAM;zZ][:P@*' lKP|%$<+  Œ@g&:t.ms˗/?!kN\sDF<&kZZu&d[žhE6/:~ٺI `*lyY[_c~n? ,IXR=~^rOk6|~,+P˟_2Nܔ ߶UԮC9+luh魾Ǿ#:(S;]9ir^k֩8(ŷ_[ΟQ>F[mWo).~muIdr߽? 4i {UxgLk Fzmm@#V[KAeA{!`}V\CBt}M :[ow<_ ܑ&R^P9Awu^316߷mԅ~4$ھf} wB]uj(d6:~iQq6cJgqUi}EN(׾sx|qgCO4Җ:'e{O]{Al2ldw; V^;Riݯ$,ƍiF1__3;bc|̓l:{ݸ0_)치V}9ٴ6u vhN>Ξ}asyz}VّW"9I$+L:BЮG!(Ga{bU쳟7}V҄uwkIg柇˯QSxU[&wjH+ h )HF(@I.grljn`*eTـ#bu2봲2b1䢊{?3fg:I"aa6 Ul-"v6S0̢4a{|!CIJ0zL4>TVLls\=;bxC0pZV ڍ0P0ϩ NlenuڗTz )V[[nG_1 %ЪvDژ%,PRH 9aZ2HzkoSO=fΜ Ms.?ʤ*YE\FR,޻G+lmO0QE`MMק4-9jO'ӎmG?LfC.BTK@{qzZ.>r [hO#a.~:U)U=Oaqd!0=@(_D:H?iH!! _zq.>)ծH02j;QE} $(gOOJ)Pm guÅYS\-t?Q|,H[N~\{ŬmH:޴B6}n'7'~vUۯwGx #Mwv5OGE~ķo>zhr83F?$~Si5QqE꺚WրPeO+y}[zz0z۲XE(fumfA_kg3+Ća?0XdH[ \m]w7(=w11uKll_ꗧ))f-[Z+]Ubj %LUϾ؞zh5gqd?l%IJӖЯ=o݋`/u[VL!鮶?~2VWDo 峢I7z|5/.igivuo/Y3)[FL?ůgjkkG!ъS&`ܜr WH)vPZ Z)SV::{l@ 8Dk){"jb`Q^BU%)*<`}<^n'rs.⼷ي':ՖD?}6qv.9@&dv`YdMֺl*i@&+[jFدJaUI JsW b1FHD .^\#m-i1xrht!%C>bwvǶe%F+A|'VA=@ܐxW=JKKkccZ[` "Te{gC#Śl(me:lZ i^6:me*޶seavvp0m^'ϵڶN9냝\FrE}>9 GZ[/i5YZ쉍DİN. of=nu(C'^Hz{*H=zv91ѕN&oc-N 9 c`mwF$ʮDn3ۤ8B1Eq \nœ`V=-vB6ovSOYw~^}?=5Z[~{28J_yxU^eUUUs/bC?UK󦪺cWV΢f MFhQFas2M6 "/g,ɋLMM~n+*/$+sL~\jsc X;PQXKYVpV>rn}isxuVa.J{;?ztzm owf\,CFᠷ0A\J:S㼉`?cHWƟ\Drve-`{[~h$R6yXy鉣}g ftr͋Nkˎag]̧ɿZZ1΁:cu<`YyK_L͙]fs=g}RLV$$@󄙢Fz (8_4/E3h5{ZJD h@Aҹs!NZhRz> $(I@71Hl0">Y C{D@p,@R$-T iUwseQ9vRf!8H %:e G庻"5 \VUC)9JȨ oKƐN&r ,sNUtŶn>3P)^U&lzV$@Y.±(F]Ft#Z?A|}.f\ ,G-{a t)XmN6\k(+]iV]2 r)ɥ^S ! $V:%)]TSC.-#%hjpII#tTsQ6 :@Wϟo 2w|뎸WmGl *Нi.^x}yM_ +NLGk+G(S18N&}2󦻇Z3{t8N3Ĭx{,DJri=H"})~VN\~=Sn{OԀLTT,byeѢEVIHxG0J xB:ڣm6o&Csf`%靓_#q˾sYFgѸVk&Ӌ1DEw>Vg'upxr5r&g_D±;^`Q_6;o78un'eN똕xBN|:~;v s˭:GWS[-LIҀJOGtP ^Kj@9G_\#pd%bNߑʹ @g;)_p"W̓j<>-ȆɳV pHee· DgӉNG84 .v"YD)x?,!"zeMsAt#?NNHOHpfL&CaeR e *(+Dfqfق9KGG]R0>kk\'tvꢑ$LI[00W2bUQ瓚-^ތxޞjYD.9\Nѧ 6f\o'F!P6{tezׄ. pvy}ϿXz1R4xEӬSГ-WBd_w,Ǡ wfj.O|Nu.9vCYvQ论IV⁹5F({蚨-׋ʐ|/j{c$82As]~œ# (Lek㐦+{sw؎Z KEvN϶F h%מjGX.Z$RٓOlsZ Sbxyq_v1ϗΊ럶}]yAAGƼ9HuBg3 8[m7 xzZpǟm tܔ%np>aG}‡v=j_̜ ׼Q>n@<%]2p+`O~a ~p'\ӳpt<۪7W؅(ڶY\e ׻7pEEEv'A OMm̀  En3gʹݻvXKcU.ɢYP0˹.!Xj#Vn@=RI:Jd1[Q@ u*$H0SP̵tm">ETv?5РvD2(…m,hmd晓1R)Yo@.bib%@RHƐr]Fҋ { >̕}+mJ{e&{ߘ9sVlaᓒ$':|>H$D(SAh[p9(pX/VJf 7oݨ$ѱ(\WKKC,⦅ݙk%Ye[ 6c v!BJ !R.@ Z>m1jV/٩Ύ4╵kf<=s~R P3<)ht+{e ] ii;eTnZNMǕنue?yQo Sy p SXDG EAr7ǀ[;9l0 MTN |DIǤ!R=T@P`o &+IN8U384PF&aPg EtmA3 rX\DܮX;U*/˧?3>/A^QP$%U\;Ǝ~R' NXK){xxhT#Hdklݵ- ;6 v+?ozXnG![S5Gf#)e؍Moz-{#ov-f W݆f uԟ-1?wԖ5q>SI:irq3ӽBQgu \{?_s E(ahOhs |Su걣š{J7-+szA6M?m5sw?#A^6{ f|SQ/RO expc驕O?y%6Ԧ;Gc ]W\/G/˂tt7N] ~ b;=zJ?chTGd>87;ki3op7?#Ígߏ'"|\^<@ n{FY܁)@ڜsC9 Q-E ƉqzPˬ`SOvPMf`OΛ MVWYR<}rئjIj _R8'Ǚ$/] Bȴ p)+,EyV(z=mTBDBLWf&&&`7JݿKJ) z{mL'3q"*H.erq7R>bb&mxl=_>,w{GP{Q^?l{rf?/l&"]򦨏'XvbCdkz#4fzjbTv} d>G_$mrjƀhY|1E0e 07!Nrkk2AJ;dAf"a$ G!A'Gm4ZV`{7Wu7\e\,ʷ (Lo)r 68CDVGkG;$PʰG8Dr$'> ;@d֭s}.ƭf1q?]?K/-yѶgw/$δuT6Vdfq/dG_awf׽(^v;f|\?߲) ,Ͷ#mm ȗ`#?ekn&RzEq6Qc+믿YE,t?Ӧk׼voMT/km_||/?a/L^R-^JsO}kݟ|VP~Լ"]/vb#&mt[}kvgW nxf3Bn>}Hu>f:svg vIv;l?}հ Z]}3k%FתW>;?9N헽K!>2gWHqW>&8*O-f>{ǶƜc{-14هn}h#LWgd}?&>l.v7; l`鿷? d:o{!B3v'?lnB 6^͓.c#dО=kIe >{O>k}˜Ӫ׍/cM/{l_N+7nZo~H%N\_v~sr3KկCq~s>8S۾gپޡsX>_.e3Bu[)8E>]jMi{t񫮺ʚ?/R\@2J1ƦV.}OGi8ΐM@\jD+DӋ"./ M !P 2+hZ)4QbTX.n:(!UdB (9a:~ݵ|a,M/VG7>q>gTQ aDc?h]'ÿ}DY+{/}|Ԑ*ӔnxmeޞgdҏvEs& VR '(ZEd=lELU2:()Flr#-Y@FmB2=?;Q@dىwRlhxĩFITRe0?DTxRcI)(j]#(86J,Ƌ?؊TfJIw0𰨨 xMߍZMF3 C0b@Ba(`R^:A:JlI+ ~֏Ăs4R?Y(JU?jEDU`B VS[DGg1TMjnn[o}o;O)F"UѺ5k6ڵQUݹӶe{&<"޽{l߾dFp? 5vbk`f}e|C&A.IIdK=+j8 1e0ɉlV/~Sk b Pfl{4òO~Tn(#.3=:YpNDZvMmdv~_pN{[qgn1i;O/|~Xo P aձ}OB龜nimHm}[ZW+]i%D`~žxqt,J~Vv*m":mbvko|X~f/}}v̋ځ66|/w] Ǟ'w;_}ݺc7]v߿ էjc[:w/p)gr;?|ߧg?ve*L56ǣc=1V'X?FW߱?omjry@\gV>Cֱyg)ENyUK.ES"7@XFɚu 4Rc-=dQӍDBjzM8wPʩ+PO->WMi"|D=cP *OMMS Le`+i:U3 K\3E(Xls(9d]5z+`U}W?Zq>Ja`srN &JQa3IZ,z@62x#>.g;xBS$W/9|dAXYf^ h%TmA\flݶv*kx}#Zm[p|k_C ^kɕt 'ny b=?2{uv cN\E+(nٯ5vG/FCZlW*#*ݭsۭ j7S]\9&8 B-EP^62s,\RA]~$jݢH5؎#] P5ڞ5H.ɬ_D([nyڭp߾}GS.25Qټ@7_AWF>kg3o=׿c^uvUەvn>V϶mpGvF=x'Z9F62#=FѥSDm_VW-ӑ?=Yf{'̣>~*|>DXZ\bK~;SޛK.---gz%?fd OK.JΌ M 'J5jNL]wc^!r::ҕJOv\o9l~ǵW^kr>p_>P9ؙ 545j;μIsw`pu&MZ:w;|NO(&Lc.Y`5f˧_pR;1n%tp<١6 j+hMΨAj5 "3¬b*ZgO'I򃡳54_`; iꎝ; %\8?$w 2'k[.n~?kX3YNbG3|}/x v+`̀6 RiC-pHwۖ|${hazfv۠V~d%GֆEowzGMMo\p*=~.՗Q~V}Aɴ{ُ };Gn_n7#ORڮީ%dG{==~ձ+Ƶl`HMM``3ܭ/^Kڿs n+^nkөs!tۨ@4mqdH-rφjHRS&kh#&(F\*~cuvtzmr-_ YNJR 8 (m[PYV,oakrp Y[Mg Hw8%PTL&"R(s H:6 n;*TN*%iS|$>9T3(͂[ur` RMĩ8J &cɉ);źrRU)F@Y!F 8LFUbE} 宪r^%%&!SKkAGwfāgJ4oƑ92 wߑ|n\˩!?VgEymx&Q؏@_f14UKN_&\_;)QcSpѭȿ-*h TOsjʭUT|.6?0M0+j& 45ϡ:StZa8q$y_ =_slWkX8h $a,\f r. z1ע!wO(1vPd5IݸD7tSy޺CiT9pB|rRO20"y3P)+i^C.h|(RtM?ν,L ڪܪoOء Xmr`#2x=8O8y8}?s%ÿ"^nnLa]wsZٮ.}Z2u>J/u->klchV^z"E`)D }+aZjYeeM^>ni&B&&qԌSiQ?M{uMOM5*_T;l@IDATX\=#@qK.eem$i@TJ*cukjRqmڵD'Aq,:11];Ov*msZ[K+}s~QY|dFTKT^QaB@IJݥ巠*\Z 4t$UivKPKY:g",( f-Ny-R JF_UxpkԾ,/w (+PHUٷX YΚ5s|~?*9q1ֱW_}4^mK73)x+_xljZ2s1嬎7j )NsEmIqUPָs "PIjB];Vf)I8H_:a&u7ndSd!cBv|NU=![E *m{_NCF*ovz]?D"2IL2Yl.r|.>C\],"t}c\xʭCb(n^HqbC K%/ar{͋#p}큫Ͽzu4^#kvORK/ G@mk四ネ w}<o/ynozeByhHؿ}/cq/rd{~]VeἄRic|K HVWYk P ̌^YQD0`K)ʁC2T,+$S]@v΢(s( <Rq/084} jȚ=X5~ɧrPH*71jek`03>?蠦 ":`Kr*I}(ʁv\W KR]=ʰIT>J9*p.˚m B#$RQC-6#CՁC17,+@Z(p2TitEx?J)QJW峍d#Vb#À> .6;HulW-#ڵkMo[ BwpM956NXTv)n:iYf n1YQ3TR",IGw cZb!U+"/XESZB .ݽ;fK{;mCYe;(k[Șt':( f"e`3!k~UMU4I 1M0\P~.b7($Gey  8=G <Dr)u%>vx]w)=ر4R\PW~RfD"0SD%A xRHb6>4V )E[1J69PA΢"U((k#8e^W|AXZFyz <91:IJ^ hUqpf\q@jQq!ͱTX'nG <(z?"so\8X[-> +_lEE@YM7K9Iey"pN]̯+|AJ)Ea갩>>[[JrɮU]]܋aP9a4ɺ+-\1LqI"$UUZ{a4kT2Jkrɞ QF`(H?j6rOؚ8F <¡Q%*+mӦkq2 qW0Hy.Xj:cOwj< .B 4|A1΅ 9FP# '4J$mى*xDԸiby1ڢ6~QLRIuvP;x6_{=9Q@1ydƒч*r b( 9je!8BC0,(SA+e VWw.|Q_))a,1 2E3@J)MXDu!HC |Cy H墆3Tf*{o F^d- 1&MzxX"k8xgϻ_+B_.VnOo:02 zZvتUXS(EGܷ֐2/XF$L jDʆI_R*Φ`}dΕZ|A{s?XG%-RA_XX`S #FiwrRr64brWGtEUW /m퇁f~Y.۟,J/>-_MH0oE[XQH?G_7!AEA0pr\h7f/aQ)lhuەM倱Bz82`Pz$ jx0/  GXqf&$*/^b4 cACt!RZ[\4)*ŝop5=,"OG:H0R*u\F5t[uNz{)[b9u4.#;~43Ng4q2ișL¢6Q]|1ڄ;RL  G9oC"ӖQ0LTޭ/fJQMCCu;2pGQP!p[*ܗE(B) d €7B FT0-H!.7Y.ik$r}p\qW?E{# Fj+}iN`0)ƥtA\P6Ex={EҎv5=b}ϖbhݚGAV5M׼x8ɴ*nH=ZM7>˸D67=쁶%x.]it}]jsj q\JTS:e œ .;t[$SJ3$ TVܞ$&QUІ"FNcXD&V:n(dܱ0F+@\@AEgg`TPʶZШ+,*jUV&#A8=4.3srR~$0iU*QI?0Jr(3oS:ǪJ2x(a^GgƇcgMe5|f~M:p/ B &6RN]97?a)uET >268RJ[`4e)e#vaS[1A`rʘ!ЁVH ֛#dM˚P:eeN$5JgnDXwW8ιNW]5kQʕ+*TT2QIG",AC)ܤ"T+-6ZcA}pّw;i@wuMi 1HTO?.3hdfj$c[ cIQ3$ lS~16WdJ?uE28 E藜(0eȡbITN m>+Z4ʳ9D?t4DM}jRh/}KݠZ }9>HYTTdZw| 5/^.4]ۺ/?ȯx+nj$_&{-\p1g/z(Gi)o;_u]Uei*r=#0?7N1Rkr;,ѴvƸ! |E0QMÃ{Ł*?.@cb1E b1VICq&$ؤj%%؀Pbq'NU ,=ZZ2 l7a-wr Cr=j(5r+L.fP 'K $e)MLR)uE&uҔŧL/A@0T8OT̹A2\ &X*U|>'Y?1!lDBus%&%5Mu(dPģ)ZH+2,D< AWTcSANiyԊH .TJmXF(UIY^CK| mڴ^[3ΣZT -1~̨4rs6T2§gQA=lNqnb`8vL'!*'Wy-.B_U9'@EPYMrki[{=9+\@%L)v-#^5Qt,USo(GP:r5sV$$A!" 4ԤJj˵fȤO)AbymW\k.]SJ ck^\TLp]}#YbyjThXm̶n!2=vbpAׄmںI2U7 og@r}(mۼyL5yAk@tᆁZJ=bSuORMSw,L5ܺP Rwp,A -Sb'q Tm2P7s9\ ](0;r 6 ETK]~ QNEs552>N5׊G=GQ,2NPq@TfL(GxRۘ#Ql%\59S͸"RIΝX!tv~՜h+"nڗT\*\JVLq4q.P2wPٗ xs+EV@"D2f"Λ9c,R9z!+dP@@U.g5Z}Q[Ue%2&7SOAv,_T0z1ꪉ qdzm;v5_4QM4X'P'Ct< kK a4(8i h]m[dΪϦ\ȦAr) ůO0 YJ k^\ Zaw ů~}gݹsX}y}# IR/~s؞K7|&.Xt3M9ʏTG)Av1zW.u99Crt& 'Rj*nݺ zH)FD:uw ~,Ч 0@#&l߁fjXT[ͨƁ)fe6p5aR_yTž0hbj;+$׬A Qu,r*?H|JTSs4F?\OUW%C(ÏȌ mBF2W-daT|#15 AʣEy8xSyJ{+Q`S).8ql Vܞڵ˩a`!?JI3 yH'O4BL34N(Ҵ~.t[eI TӶ N/?@]ʑbhzTj,+''ŕ9u! 7@~şUy+l㯻pF1V"__X{"E326K^B״KvmETcO#S,2׼x8ST}]a[2v}cfHߙMUdX*ܒ/K4 ?7]ORX=H.,Q)Vbʰ\et'k\='e}>bYTT5T㝍B!Q $pGzbCC-e>+HՓ29ŶN<$BM`<'COj !@Q.9 C 2LW۪V !4 X(͉ ˗r܂{ 0(dPY]:ԩp Y_2=8p "L\n m6g}Mr #Ã7*\YSS3P<Ǫ:"e0"~NCy۳w379NN:J0^(@لFjbFHqI&S"H[VP)jV&5T=T^ϙztmQA{ĺA&ʫPR3QI\ɍ,'g7vDWXCpm{r;.md\$hC }1Q)#Me2ÎRWMrS.w_˩RA:& ') ZʻjImtƠ8M䕼qNǫp9݆h'] bnfU) /^tn}KMdyzRz'?h]uyEIip9P]kӓ6G-X_2̇iŚ(3tI4&!L@\1x2 ToJm .,.I b }=NEN)(DSuEYHv 4bFI'CU%0R Tbp+%!S^*IA$А+6SM Aiv+Q Q['aU(L(1NjR=oJ N%ݩSelڢ6I,R8 <%t%UMnk ;|Ӣ^XOG7=H38b;4uT4|lraQ<\^lqJxvl<-%Mm]tN nm6qޟ$[nkjlQ]^"Eb@?o[w[B /ǞiwtRs/|_iI!ťklU[kKEA/|_7 rK5/^E@ne(mʕf^bT{Ip+,dKp?1:Z;O:"E)꟟@(PP7jNbG0'4R8lcfzf' $RyZºSr7U*̛Lv hI/b%eK'NR[vQ($(ĉw0|e㲙 S x F]ݶR qPJ C:uqt¤ێ22TQbًlRfqFtB@-%t ʈ`vCeAŠuw:V43=EwDkCkpr\s "($ĕ*v#Vf/N冋Oh98˞ZLmQA[5+ŘvziPRFf[4)}q" Qq–-k?sϰر@2BqEfes<\;2mq'{E/^x"E/^<} *Y ((&,F m'4IZc 3z 8EۊUVYriEayE۾={]0Ȥ~_ ^hd10%PZZW9)R"W@w7"Mir&8F y l7xe6: zI &e[Q=9@GS6>UᄁG(!3QRFE8Ɓ^p^WG_n:>SvRǮ QA SNDB<Ϸku[ WE#l*+pry-ȕr)X nNAs>0O :bƱP+6?[X]a;qIs1+^lCc= |!Jc3h@ CRKID*](/0k׼x"E/^x"Es@Nj9<_!O5*!_ʋQpr&f45+{Hƕ5Wmr`(U[# ܲ;\ꤡ6CFDC36C^:gRR%TVR[C̳k7 :LʁAs@! ߍT)2/0p)i/tDt6.EٮjI(`+kc?VZpì33 <`)Z>};,ojpnRe;;`Q km]A,ܤm9D< FKgi (2964;2z@"j蓊z*1ku=t%1r A;˔!_(]K))ʂНR76:'?66A?}r*ٲ)TAugڢ6g \:<IE`Jkʋ!+LB}AU OMyhhfv2)wHDƨK϶~,kALۮ]X`ՉIR!7W Y b!uZZs"KN~܊8(9tR@׼x"E/^x"EsTj o|@$SZi#vo+W$.hՔ X pGfGgK0).lHge0@8lRSgm2*TQ x +qjnHU7Z9JÇls9cvP1V(3I}6J}dXN]Mu5.똦}@B*kmmpQS)}q |~zkVgYvNSD;|K,R00(U#8`J6*ن O"ӵSu5CCjRxQmx[G+M12+++Y2vY]uTKe(ffP=)JPIgrb*XCE:#Ϥ^~(s#'G(2˩jG3-d GHm7wzDS͝!$[O{MI $tiy^Oi\X,>ڏiWQ1 TMwN۪ۧ abKP*OmA2ne޳3^z:U^fE/^x"E/^mةJRڸO"vP)xZwz+BLjXՔYSV J\z&JDAțGA&T pDY0\dӈgqb'vJ'm^LV f^eS(PT2 T,>؉j LAEcB NJ ׭`͎KY1XRqmjj\1) pHMMS Gԙc )MS9_ :K:@Vq%R*q^O104_#ҥ̩Z昲i[E#  oy/^x"E/^x"E9D@d >i*$M*IZ%p%%eQ`i%b}]fZzzn*P!bcN{rVakP]6|dbbU%Ek>:%Yte"< +l4Tl{W7PO &Bzd lhR+`z]m ̑: ,Zj1KQ*;H0uEH%fc0)*.QFTed:!'h%! 1JgIGkk+`Y7 =e;_bt21  a>Qi -) *>?&2EA1(ZWit7S9-R)/L?N܌EgQ@d'o3^x"E/^x"E3F@fj=%\e5L"F{>E1pͨU( oW_}c& dm-<[m}S R]oE9cQL0YCLP1VALQ9W.+!utȚZAj,Z9Y935h)XOW7})WjAK|L>[˱m? Q,o@#㘡O%ԕknnV "+uX %T ^ 2B_ssxsTî .TsX&X__@@OQ0e$ 69˯>T^nEji5 I1P3M?U?q=(mk^x"E/^x"Eg+i&أ^S!yf>K`rR2p\boQ`;v%GToaT_5V._굏NeUL*))Q)PPMYeuUmfFxnSTnb J3j5)A46\AU_BS9n6sIZ>ɂ>Sr7Ue_T4,rЩ*:dA+6ZK[ |N(aziuf`@iijJ'c@A .[H=WVZaފ j!,qBC>bTw_RI&@ U=2e7WM=sj'o!=eQ@6M"E/^x"E/^twu:" RjSbĢ'Y- XLeX:#@bqN0EV['(0' VV:*>*7]4Umn_2@WA=6 9PV@kIe4^680l=]vu7mhqʚXy¨ʁm(dIRFβ*6Dzllm< FW^ dkVRP|K@jϝ;w:_Xz~l뵌&,y+*ڕrR2L-xeQcB),^,VKQ7z",vN`Kؿ'{lι@۹[ڋ/^x"E/^xXhiiqEZiRM)]P6)xjuFmVouՓc VWLJ3ʄ@EtRd%6bPN<@b=VV52ZbMk@ W8HPyAzBS՜2-kfv9}fK,H?mb|ĥJjH_E)f\@H A.K+/P\eR]ne/ҭc{ _":#J/M+;))([=rLV|ҰJA,3os61*RJͣ]iPSMLC}F9\Y'Rx#p3xr󙛠bmQ@{^ۺuM?<#g/^x"E/^x"twu9v. : 4Fp [o#uuw/~q/`1?X a$ $ LPh<Rیzg@,ΑD׼L*qT>&21eU,Mi@Шbd# j gQ0>HJPsEYVґQxPE6AIOh0 xQM-JGX`_ADi g4iZKK hg8id8hWZpqrsgpaYk2sc)L%&*՛wY 4M̳٪ܲuμx"E/^x"E/=iRI_JG6d6mz`ܺ5lYS=+CہEeuFZ(5i@Sc{U@I͌r{tS?)T,4(P xmЕ0+CVN '1eqc͐F9kdpWIh:;;iS+!rQNu KsI,6 նͮ m p0I-;G90=Z*5tʩ_pJd7tK)94=KM*<)% ռs89N+ ӹ6d<.ƏilZWγgje3ϴ3w果o~_YUu?'m-S_XL}N |}P85Bȡ``3<7Es 1UA]GEjOql"` 1} /~"E/]:.}e/^"E@)-Ծ!uVj&ctȑ瞧^"EX JlYu @IDAT>3~unp%wR`)SHibq@PyQnp|_cՀY[ 9N()5S)rՔ)@i)ΟTqR%b#S& @ _RvC-P8NjfytU|z͘6Tә?tZQ<]ͅIh{tdٖ-[jeGoB4Ob 9t.*9 I&棿-irsq%>joZLj=m||' XK/N+N%6u+avn+B;2ojv\ j*3:ˀ`кE%]cmE ']m uÅ_EҊ(Z_hWndMK^o}{'{?zθ7Ӌs=E(xʗ¯]8&~?pJ{+ou7 Ѕ.SNm5%%%qZ_n3i <' {B TQGb TR QΛR 4 9^8{RWL;U m|8mzQm6IB5aLV(pM9hi^'dJ:i Xjyx@`-P8)bnի }zf@]- r4~tM]]CmGˀ6Xֲ&KeߗN8শ+e+ }:Pno}8Bynvozzw:}w[C=*utvڇ8u_$=E`iD@@+U{֌kIy>㎩xYyеD{F Y@~lFoM怨>7?P:&'hnDBBƉ \VUW,<4%<}hH?W#b0;R$94 5>Lj9oOtrw Cm"AIW !@}k`#I)'jRFڔ<'G3r:#Uӂ|?Te TJ2<_TxI2̏}85DKUKтHKXպSh@VyzŪjMD $р*G@iS0dM{BEOɗԓGO<)'AO΅'NP |v-Th=Z_Mzڊ(F)LN&;: W꩹h`^8kҗү$Ěl۪x*"HhD\Ft ]Np}A&LmB gmqn7ymeszx<S¨mpiUsG #(4h4y5@6w)FIee~ 8PUYtT+*,,ƿ{~ȹ8\Y>ȣ5@6/ h#08P?P^C}sDn-*"mhsI~/sM_5MT#4qpX:%BD֭͂# !*#|*II x%x i1q c1 kb5'H n ^WP*Rzb5*VÐ1km dQlOH>H_DKnK; `i͆RO+GWz{&"'۳\/R)QJ\X@(KIr0HksTkhCUő~)HfJh%yҵdn hb8N|usЧEd?ɨ NDڙfNtƻ/N{G86: 96)J U垞V&ppXF&_A .t2".pD 7R)jxrgٲ?^ 4:(/"3r?}uoh:@4E@ xt kS$O'A#I&A-vDmS[em۶e  r%."uMS\!80ʦ~yHBCnn6AHi(:,XAoZ6:q 1[~2. = ]r&d2 ri7X}hA9%ҽh Eִq48`p4p@_J kP=>3=-f&O?٤,,*ǓmϨgpB‷$E-V 8o,q\J# h侩}_YQԓښF ʥ$@Ks1488HQM7kG;^TDoP\,gQw f **vUTC:/z,! BU_EMEDD(S1ʹ6K0MU[l vQ bN0 (y.4 }^JjQ9Un-4Q{ -VVsk[ͩ{&ʶ,AiVyhg$y;-&hUX0КwAd[gw::PI(*ԃn9Kqx>ʫJa2T\R˃'j&vnM7q vAi66lQG6dsÈ88޿,Sw6sn۱AObR`D h('E)'vKJs-RBnY>k|G18`pQ}/O~ }%Fhȷ{Ď(jwVzYSXHqO;W$]T5Z&Hi;K=)wE%D!yA \/LRB)'3`z'(%˵L=7.HUS Ba.FEI)bS6On,rCKsĒFE4TnJ]J\52 p7sr81n#08,c-}b(w\>i6h28prqUomҾ\K|wkZb\Ҟ-/B; *9Ui.( >Z Ȫ F'lK$Jԋix T@effdHT_ȧwJRʭ}tRÁ qb׏K #:dZ xt-_ʱ4-_x-)PYz7lت)h΅C$h|$h#/4d*N4"U-?OfVR`J?#h3jXmD|,p+MYd|!JC|< o[YVXy{=W}PQU_ ",صi-!XL%ۖi*Lvw8&c:1hC  łf 08`p h>Dz5 Q&ג'w.m]2h bL56P %HiiR*XFqЦMA7(UGg%%%(,(^칻 [(eWHډc}iQi/ZZcZ9籖Iɹ>ގr-9:DLN,IJAg')yDB`7/TQ%FtMP_( FbA" &V!x%R/Jc""lHf<,sS'eD+ʩ X|\A4esކ豭R'bk;7dNvo7dg$^~qpxelEcHT4+]vsg_ 08`p 08`p\` HNQELLETiiGp|aa!rrr'"*uıq  BؘI6%T@Bҍpjk\[:ڊvN13,w73.rDBrR J ~6SZL3N`3U VI)2 Z()(=۱ a~t\%5/,g,CL18k<EDW@6H̙NI;+]ۚ(mC 81ȸf1ȑ݊vkJƁQ'z]ԥ}j,~DŌ 08`p 08pq@rrr,##Cq t|DϠ AlEEE)?x$_&*i*(,TDM@: ŧ<ԆBpSʊY}łH"IuK"PMdlٺGJ2 z BE7DshO@6w7֣BTgrBЫk$KMKoDtnmBA1QZ)()@ud $"T4Y8hOEL+d/ꭢ:M ]tz@2Dơ0P|C!5F븆&h|0878`p 08`p2D*M/%V:)'?<+͵`uT5iK.dH '`R 1J@ɳI7 @@:C!ɦIWKWKnE6b/jբ7lJqQPvǾ=(*Ɂ76Pt'ܨ?\UJee9VmB@ <U#=e F}Ѿ Ete$FUQϢZ*QD;pb%͍n&J˙*nfk&VR&nn ԙ`GmRt}}:2 >D[!3/GA-"!'*YSQ RO΍pp ~ø⡈ ͇\ $[߾rzu5{Z7]Jj]Ec^ `§nL=rJ˕#$J'Kãd/>_. 9{yTYɃm- hY@`etWM [.$=U- -ᵏxQ8/\ VʹhNW-AZ7#7yzdLƕcg@^/6w^KƂr^ [tϾ9 <#\j֩7Νβ§~\Ԣ= g O3QýS,@T=E2͓CBB`L6*l[VH\@EJx Wii 2=8TIm<d>$]OpH5dE6=_OGT @$|1wy @Hұ{%"-ʲ<384 HD1 ]׊w4w /_Jq"NT@6x.J 7QGu%n PNUn)fFɛ.tZjE])#W w/7 k6`bwuH/׵~` ߒ0:n၇€V~t /?\.?#lRuk.Z|IOEW>i}@[T3H7KQYSk\640fPO ͆bmRxLpw){j.-S ϨzsVc 6qO^1yLT3jV!oeS003YGpe/fӱ{ Lhf W tlvIQr}0(woi4Ka옑zj}o9 >pēЃׄ53VXXw@{ƠQq NڀWG}ULN/ 3a@֞HZ%n7>srN:)UDRM(;mI&1%RnV* %Tr.i'V^@9=Ȧ}Fzm/ gMǪKRm^U#@E `oh]B:($h}9GCwz @d]tS9y(,Ce`ir\B 8l?|:_MnyhGDxFn,J:Bͫyrf ljGЭkwNdoNZOP>Ǐі8mPUJdsIƣ`IA#@ƉtoQL&g^Ёl8g,[ '$ [g(Cz7v ~#f`Dd4tZuSoG5f)?2pRˑmxDGtv͊7kkޚH}ץ%y$B΃Br<̑ݲǹO#u|{*ΦieR;nuRʚƀ3zR%g" .톶3iܨ79ā<9-۶=g2[I=n-.@fV[0AG^~Vhw*Vf$TRaGJtIndNی7)rӕ6q.>T=hi8{$^5>&58P#itpFIPc)Hy'[@IMM gn-*Egf+ZEO 2PQҚ.j;{HIK-2xX|!UPthtPlT,b -9 s0BUxE]qqpnrv"ŧbܔqp"r>0!70k]ph!vLָ\`,tK'`#ޯni](cJwѮ7c]NLF.-5ؑ&M#&ԍ[]WwMw%ᖾg\Ɵ>D]v)>J,}n-5;r7w^(,Al;/mw3iby}i$Tp>W͜p$>yPSJ+D|hå9#[bc Jɐki6PŲL0upK!7}J{ŽStyZP"$?¨v9s>f;NU~eC{6βZf 4i:gZ}ik jǜHkVT;q'[ξkʵm;uG7eBnn @T YhN 6uV){{//E}t~э$>=;RVKq,y.~]CX٘utU%%8,iRrQ=Gsa]pd*DhQ9BUu W9S/ÇQnO}^{-,Nna9_.ſ0z@4<˳ywa͍h͊>Q Ǐ?nxtŶ. Gn>PpٌI0}(.oHRofAHۑGi.CǍ!]n~O/!q݄]unmRg[hT<.f%s<=P:dۃNi?I4݅jƗmjy1XAm!ﶚ@[~;sNyGcd'6%Y0K1 Xh!&`|7.»۸!H;j$2G޲:eO.(6.jE5+JFP#vcƟ֬e>=V5.͢Ye}P;NC618|l Mm4$@.fmp کu&L_5Z6vP-Dښ88ѳ`T'o3$u{"̛*Lo\^'Io@qG'T[U&o_%`ڧ}~V݆mWZ q`˯x?[{!}q̡ ᑠ ~a MIqю}.`k4}ٺʸ1O 9zkzG̽P^|`T|-H6uOUOrKinjk/_c0fpO`S@7dEa8YcW0yMࠀ*`Hvo+>o=V-_7KR"WsH1z:}b&[.u[س+tem5# 1R UBi:D-W2Ϛ-KSmnayƍBRfFmԙuVKߍϾ6BtJ%K 7qaV ~m ÚKj؏ '78H/S]32+аhN08^k|O sL ›+*qm(eru.A1aѿ6|50G鳹q!8mf59K5 #$3=yt,BMLēM?[؟@ktf8\~˽վ= ILn3WNӘ3Eհsg 0 z s?hs& G=+1;ӮF 䝏j8`h=m[-\5BҌGƼ% h4+W>SH){D`9EQ֬YmeJ%]|l3"X{ZQ/Gqڳ`̼jn-žتs9qN(n-8&-@Xeb.O֯  Y0M T#K;vT Y<\*B9#iq(op.)fޅ/+R Ee+K_ 5;q`AHA]}psq!*Qw:iiRE^JɋJ{@qY|耡J|8 !4ҍ6m6>6qX :#} I_̐t [1l޼ ~0A9n<4"`ud+o80 s<#vJ>T`cݿE6x0ۍN.\r1VX0 Rq8t _M} hL-Q2OFdj鉣{p5d+8`?b l A 0 z v)OuIbHF cagf] a!'VF#IGajErڶa52`$s LJ1̝cf,ݔXJ k?=Jr dfb/ˑ\>Ǡkmp9X^JSc'o ԇr$#;}łXOߣiNwqtCۖt\#q٭sSngP榍*^/5w*n̸̬x|ׇ@,Fy+ ;lNZCef2Nď[2G)vs|ꎶy/c^ws#zDx2^;@ۯ|/8A,lKGDsVkZ:ؠ=sj)n@6k1مb_6sk̹+};mtkDM\d'`b,;A|:q]1qHGW^{vm 6'_'^=gnz;tTDڕwh$&̘K۩LY6,[6 2<56̝RJ:8}xoIL mm }يN`SB*ºF 13{ `ߐae FW<^x[gn߀[KJf1EGvKP/,mF>^!tc!KVah׶ |?>?O|LA79ׇ~uVagZ--sոfi (ҙ\TA=m)]\MY ؗ Sl\;.]Gd]ߞk7Pcn+ٗ/(tCi/)FsexSp?Gȭ! u͟~8FW=GT⡃^īW a܃ҙJF͚c٬ٯwp_>$ K8\fy Xq7g]vjc_+z/ԤYwPrMxeen3]˹ JMaF$KKӫCBߣ o2k2!$hzAl>*޽ \Ҧ̩WL rAWˣjBmu?U(ie/Fcy:Tbொ:6npJ" ǘhP+ڷ~ wcrucޡ`͙дXvuP^{_5}+_p S"Lu#ߏ`_o@.K^Sg'=={/>>%llN?S4Ɵ㫞?R\={vLL̛%5Ʊ,e5C%_Z FQr{*&Ĩ7JlW_jzb 4Y9쟵V|} k_Lf8kZS],ú0߄oV&b֭1N7Ă;'Kz5mna܇Dum X^ʓ:a) S,ݼ@H9C>Cxj|LŔ^qНٝꋘ쏒e<8oj-umbz{,rFEl>sh{Xݪѱsח2rU,Ad=`!ҖG ިWZhFur7wl+$fQM;4yRL 9QϤUϝd"%2Xn8:Uɟ:@6ɵ忇AqI7:tҝjisͺ5:ovl5t'!k+H֐y$a5--ҸkZ\.rSS<.+ѴZUZ4v]r0dNj UkkiHiU R$*( F(s%{q9zHk?R@' nŠN.>W_W7ŗ5 ʑ]VS-UMX g:?,›vH1%f /ȵdX,ۆ%|4i[+P_ \M:*T1ljի>hVE{<4gY#Cd3l\Q"Պy;;@6IfkCErg 4gs3]Z#ÝropM|,lUt:PFUT3UW+*@\UaKZHT67%Y(_NI2 .*KaQ_~qj1(F B5QCDwۚauPz-G63zR%)OHTC4F)6d"~&^KY.ƣ2dShύII8X#ŃG+oZsRm4R[6WCc3, qjm67ҿp48@|}_+g|)iwhPvB1\ƂC{ @+сa6#`MC!L6NOaA! ^+z#U灶IMؤPK?Kkwڀ@IDAT z5Gsx[ګ6Zuұ?Mb.J:ȾԤ4LZnOd_ ꫱`CKݦ'=jITlE+sݟrzQB3oJ//e6zW_x.Du-'Wkm|Xa^!{sʯfո=ďvi)%èKvBv$^9JMgzQB)(qR[*Th/zJu^X(oOy/6?J9MCg9xOK6p$,YEe["#gp"zqy@u4yEx|^?LU5EXɳV.:uGkMAsa/:QF L{UuuO3qw@6 Fb}-x|nı]Ip4w"~~?8H7?"? >dcMx?cS>I;Z_=[5~Iv`u٬xwڴYPb";/Wͼ|v]נg>OKF 4c,Ie^^(Ə3 k[]oǼh&T[(AbpUʷ0-AwdUVW6tȝŷ'kV4"Ƶ~y<dVTmNo7V\t,)6\fTP W <к WU3V> \'}Zq=3|F7ǚ w:u覼SDjq#G s@GO @_4zRN~3Kuw۸K&aܨKrA@\+n0EER ). _pEW0&FyZx~zJ[#jSAMJ (Q#G;{96[(Z5 7Xm.SV+ tvggԣu_ YT+.4H6V?]2nt@MM#// cjYK }ZP(@YX*Q3Ղ/7kj09 k ?|ͩѮ#%f]^zl&aJAam0NDGkH5m`c׬StMQ G Φˣ b4,$IHyO?Eԩ04MX1sy qڵ%c'UPҁAa iǨzNR;Ώ@ .f#VͅVf6?uM,ۉ|EZL-QT3 6OKKӧ4xRuTѤ-*eNܔX72LUVR@MLĉQ휬L۽ Qgcy7:L(hZHWQjLҾ)ؙHɘm\WCW}R%pP٭q51p^ %Yyd$,z ܲꢎ7n^/D̛S v,(xY;G#%bڈSvׂw1 чxI!DR( #su9;Kyb7&ݱj8lfDWٶu޶=V_lX-`H Wt`d[eG;{DPDڄ|J։lK؆]I 3ˋd7BC[NhkA:Lp ,oj.mSLG-{qg`ht;f]/Ҵk4\0lzD҇_ b-w ~bC5mI P/^( iPžj?ez֍xީf.OMgm6Mxt5yhD!ּ#:ޗt"`i;W 'Ć:Z]VE@֫+F5!)|cLʽ{}5g~G>~QPx4HvF.:ޟ9$܉>¥c1v@O{ ԯuǸ #uE|y*kg^Sz>FW/"pI vRǕCYo6pn&5k glm/h3(=G3?tTG\/I̓@S#Z=3}MUs:A_ %Bu>8S/+Bizs.U9N Deн8O=_am\+9}ܷ3yZdS#[ӧ`[N |? ֌;#ZV }w5;5_PNh"xaЩNclٲAΤg9X6Ool|TRȃ!ExA3)+d].ZJ4FTW7#eh,^9w() A7&Ms9l%tZĽe;lEKFL_Wߦaq/wdڻm ZSL8j5!qݯ9O9i7C٤տ68\kεIj6⒛`ЕOgl Xw|B'~9 }FS>rI1LDG%Dƭ7.1pAl.7qL9s7BG*ё]q#QTn^GjÑ:lyy1%VĶS7Iw OG;R=mkeyAl|Rsu@Kǜ.$4Z#rӓx]z҂=ٱjۦؾx8 ;ա:qGZ\$nz &)EN4΍5K̥ƧK8̌ x1N5#+ԙڠ[*EƏ0bG t0#-Mv^ ƷOq?MB0r֨s o(HLWhB'Iko)9미ӝ?{>&e8(YڳE/*@qiJQ!ȱjee2Q+(4^݊KZA@܉J`hYI9yL?E#KtB(efжZ JX+өAUP q"+Y!a$]9Oϋj7XVF.x{(qG| XF`IB:e e(=Ac"G/R9YӝD8keYr2Ӝ:MvٲGWhMn? (Qʱ\t۩b!\1#yr'nTBy"l &|MGqV&%FR?fj0&PQdZ; xc^>Oߎz]%&v~$&x!!̇POg߿7ImP•o>橒dF%T57_R'?Įa4!]uͼׄ<חÃwLJ'4o 4ƹ]{HG"/鷀xbJV('Z:;ASK1γxlt%ئUoNγa(WLע]%:+cɵƲz0_GE-9h;'t  .HItyECcuQr=f7?žC-tGȢ@]Q#1zvlI Ry߅+n>KC`,ޭ8YiҜե$c̳تD_Ƕ|[>ຉacb{eoŸyԘ7Pt|7moϼSrFx Ҟ/N`mHO<&.kX]ζq=vS&-݇`vCFHf*d^Ai :Q3"bb'ש޻“xf۸ilVg6?ۧ^)CWcpϨ0m1$|_|M%?D;|6.W ;'%c6h z!aʯq[+|pE!g'X!Meoތl ~e_]I;eԆq(hKp%;:bBjFݓc<EfOz%UM*Ħc%wBrE{i H&N8q8R1 h!E!mݻwEϋzSΔkGϏVS*"'^O=Yh6*i,hG"p2@Jѱ/|*.mbI~RŮ~iq Rw=V{ю ?kl Ͷ ?=MINѡd+M=T-y)ܽ:kG`'V/:lPrT=C.~vk>51+^wJ􌛣0q>s]@[x ^NW?Gsr`:}ERBF*'wu$sQ$L[ي3S.YbG 9HG=ҜkQJIW%w H#9:pX$!$ ;@"lFDҁ=Puq=ZhO;s 5@s"qPDݡž{9K̫Nچ?KvHX\Jc^9m{/x/K>T!e.$.T݅YVيұG:ҕ r-&|-vz @{ʇ{-)o71]m:3Hk'>>Hx:@]UHTI`WyY38;9P^z20s||b_8pel׬ƶlu"X)~]G#.o-ęd+؍_ I.,O+٤f zDkغxm}ql䆧B0xXC{] 7kVsE(ӁlG|i (lS+ϸn1J2J;HS" :eblPWЁlekJTmOU\`Ӟ#/(sCM>ZFѣ1e d5 ˇU*(c%iiH]H\\9r$n<Ө"ֶS'n܃ƻ̛&{%MBB͛㩧rqضmfLbg\_Yj$BDrfR~tz3īh9U5E~ GEH"l꟥S$e= xqԗ(e8GHF)A ֖/_Ilg$]qkѽ ?`iqЙQ}aŗ c0)Rx/eZWSL\ٔVaQq+?+>öPUOϊ-IJ DV~GOJݻoJR_vNO~jTw hvIF*ڗb2Tg#)q vg60슑Fʗ%6Ŷz m~(ᇂeazZm,9 B՚|+Sa VfpͪCt`iKuY޳=g JB(?_xYokMhֵhb-0 }J1_r6;vBi5 0{}5뺜9T6dhS8uԪ-Z/=J^~yW27x=mn>fNsȍ4yH4"GEӊŒ|>]y~{f=Z`DG JڸѱB3Mk9*\65,,(,Ƅܴ uѐN?Mp 7#=3;SM_N"QbU@8W ͋L~|ym8#joԮHIuƼ::mBdm{c9g32h B>=&o ~w,}bJnWGu"pxpb!i.,޼{}k p>ݸv Nj ŒYц_$7ͯKVbUjk!{ $&Sԏ8ti OjHeebբ]Hk̮;Ag.sK}m?*Rjx\kg}UG;y?W᫯:%Ν5NF5-hIc4]I;oG@9s=y2)4dsHٲEV\9kq1cc@@ cFG'Q2yuN_%ysYi^..-c?mT -%d=*kV9c>PVe;qbzrݴ*z#w I' ~~>JBH5A*wJy h9U8=h4%Ъ $cY֋ŒK)JСr`ر}#|<{[+y+)W/w:P$}eVBq(?JNAH4m}ؖ@VnVh >(=u{z2V4ZM|? 7`n~y5 &Ge!-S;qwFJf]п4Y@GmǤOB׬rvXQg~.qaVMyc3nT3l$oj[ޙ@GYk}LBBd_dQѺ墧[sO=Gj[{ZSkmmKrRE!2v&H$L`3'7H}eѫ^K_M6쒗~zSߎ(OC dJޗv:ή3SbT/wחWsYc~-R23Ǥv ~ּ}O}e}X)t&1 ~l V40D /!+d:v@PK@a hҥkhɯ8qwCńO~!Y^}fEV];YR .ܾmŸ`(4,ÄDevj(ɤbI Ǣpǹ_,qŞZeqF;4*h# `660PL_5?OJJdI>pm)fOuI"EX#(-0^  |ijbm:{ Vu4lm@42|ts%PJ8#cq4Ah+yϼZyrfY&VIJFPqIOW (C>K=v[CJ 7vv5s%~[)8Dv3;vl*?i?58c?z`KVC-cbY X/':"1bB1K 7[UT$-iM@eKAmqǹMi)pPL*A&YɩU-pY0C:*6*k:+DiY"-iO1#OڒRXBm 77۞"xvV>r4%L9 7ҸZP[Ii ? :PJ@ ( $Srz=tT!q}k

'/etp ב|OcXՀzoㅗ_ur"4iP- xԟ5085 X$X50KKEV2M7r|/d 8 <sũR7,4U߫T][J{j[UA¬;ᏥаkOk]Hy2Vj%Acę|굔L-nR: #[A-3'ý0Bt ElĐ#H2˜`_ FWΠz%3TdeR^ltJ?yt>w)aЇ7mۈCS׼Lt3l5GjL ^7ʾ|ʕ'#ϚASO emطkwl@;_CڍgR+5e rྃ`l䶼+1.m &Ji!&h±.}Kƚef4[k7 ͻtWCz DžXxpWߘ^x5X!'-=YX0j'fނ1CC{GPukjfAُYx[ۢZ`um@V~TpL-*"_7\}[bjJw鉖`Mu#a6/GeuVҙRv^V&܌*QA%h ~vN^dΆ(yY7C!: 0r@X`US4,'%[nHgl1_Mġ֡cvJLd_qyz8{͐`hl4hB .c9l<7oOg<^k ŋB`'{ h\Ukk/*mэ*SBqАQ)%ذu+ D6gl<H2cqg:xҁSXߋZpq $Ox:hC~YH0OuYrd.Ų)Cu4}!&( [ 2V{dh #ݴ͌2L.אVCm簁/umpQs"S 壒dRr7G :)cf.#NJW/Q`%}5#7xi;N$s)XF8lU^/i$P(FebR>m\~`GeƃS?WUMq&`GϚzT k~ Zv׿m 4wR!PxU71Xo# N#*7TNk:m87kh-gѶC{řRA]>U߽b>.&ɀ\$Tq\_M&ō)KJr1c3X9y7z]Fg e(}}D Fs[\B!MiLJLCBbvRv;ƌ*u ńԔR lGi_v 8F;}n:}_ѫ_&NFU?tҐ4H=g-ի Ymcp@Ryر ;V$m[| F~5\Z; _/ƞ-+bIz*H==Jѩ[_v̰ ®SorOXSsRW8W/c]iB6Cc$$8.4hPBj#4=`r[E%r?\N%h0Ɍ޼gӘP!Jr?b6Fnv6Ysھmg4طzO3}kݹ?tyE~Ύ.,ۗޱW 3t::aIFeCsrߵP'm z{kSkC{\T(dq-FUFUoêJH`*)y& WߠPԀ_5@K1BV5fK)Ov( E1H>yUWh=vbm9ˣ.`& F1X([USc{4GM0G%njnl*pr8R5+7 dcP0ap~)%>,թr8qɆҽ?J  {79iշ1 fA${C dR29/3'0#) 286z zϦT}>ןr:CG*kxؘ֮_N+/ ؗu@k&Fط7Ɛ omת[{# *+t yiOk13ڴmk=eZ T< aԉVʖhKIk5}?n}&\S K&i Y-m[TV'$b!s]\g@D֐//ֵK 6i~TP% ./k_Ć ഢy/Aǩ|%8 =Vu׌t c0̳3) F1i=~g8y2X>=UBSm 'auC[CI$uA׶wYcy5%_VۡFb$yΞոqNtW:tćE. G7_hs$?d݊ͽvD2Lgrlo: Vq PrE5Xid8NIb,!\@>/~I.Z|1OlzM,iCi'}HS\=8~SmBoϭBUebo+s/;-֌Cxg1XpN:"8(PRGT0m8:~oމ>m,Pyiq9.G~YS,W.nCO=U;xnZ!P[θݺuWwt|^3#鿽" E; " ?ԹޘfKg-Zo硨3OB2[4|"JT< mhlںnB xZ׮}0n_g|D]wV pw7aܽu(u pAJ-aCѫk [0OmPT\R\&* ÐV&4Ę!RhxR9 g,Ry/ϒ,T@Z2 *V)OsUZ*{Mr_ ϣ^|Uڑ+&Os@rtt–k¸oUyxbTD.'z'L z}Şݿbبi8! M#E#EAk`1o5h*&\+W_fz.%r,LKJq1!;׺9[WvI]bLZqL27.bp3Z@XMlj4o像pDp].c0:1>K3'p̘Io] ްu-/Xao댤܇KgSA3=؏'|lcK[$npsƸRVd!\ ӳkQR-mutj;*H)+sh8ɱc;vA`P'k>y!}Ï󑝕};f~3h) EPV)DWK˔8ڱDJ2QYOډufv͠ƒ@lhhYWC|܎]#1U5 Rdoo:'kFVy梀SdԴCs2,>_J 2v24$H=zt";?zH}ܱLֳuoP-*s8|o߮c >~Z!~zzIV%9L77A Ooum۔ 4wS^O2y'a`ڸ^)Z2 2ˆ~U)>zXbXO5zlm~QU~ge6*89Y~ $ɳ֌g.y*YʸyP 1L[|:{v|Z&ge=E+e˘!#UMRSج[g!<蝹+ R[H: ޞ=d<䫺zhJa^D[UTd8)Nv= rrf>pw:\{ Ʈ6i}sl#…M[@O% mRclamqZo<} See>W[{% cXմq:<=wW@T;'8ފlӻp=^yGǦMq`.UEmnzR~˵&OzR̂SG 04L,F;i۾mE~a5m^xMYI/cS'{D]W98ЁX wR'crZWX5&HOT @*u>0=>pcL3g]sdY㪬A7uM"~^/dК/UɿHeda\_MոO-ާl*ZZ 8ʋc01\(Iƀa+2Q;"lWL`NnJ娃x2A9#XY%J֖4mSH#+pᙩ Qvѣ)RGKL㲺CFvNSd%aѢx, ys?G+j(kH3A^gmPZPKC#U88.\LB>|(Q8}$qK'v*rW]s)W@.{aʂKuL*bJYʂ4xyyƶdˮ]Ѡ:4ƪF3 O"sՆ;aZh?X 30čl˯W>.؀m00qO&|۫Ȩ0 WSo |{ؔ\6w t `x^}61͆fhHE3Λ *+aorUmAMm52w2grKa-\|\i0>d+5!DUf;W+^r/06^aC1u,HBOD{ւj乻3f-J9R!LPdJjq򳠗ކuZ_4N5_bt$ב@}T{,Z1tJ)Cͭg'a#\gP´)׫U#5/B;-٪d*CJumGn_>uj 7+W-9*, .$W75ȶ! [w[:OI_Sa Ѳ'[OQ-7Pf==2tBgH p/8! au.ׂq,%ߵlǤ)q-/ bwlal߯> sp:] k^XRVYd ˷9<r?' gO—QH8 x$'͸"1hgwxS cgj!Y>i㏦ .G6O1.= 9nXY;7,O%]X[oc&+ј/n w{ݴ~=y$?8zY[N;>d֌nmO=Udlj;G߿Ý 7SV9izOF4`npl(|~P(..(d.Zo&(9xx^|3m+`esdA'W3>\VWQyPތ it%TӑI=|ւ㜯~F. ګ:,NKP`CLLZmut  >J;9ïE"1&qFlY(jc/7`^|ac&Q<+~ 9b\:@!~NyĦ}?>(aZQYÈo'"I:ʅ{@%g)iqSdٵL_CF/d:Q⾐comNKVÖIC)~:,CC'9a*XץpE{N(FYC{y@FE!@O?[ w+a'z<җVYoЇ44o鋣{0Gǫ0G6Eޜ5o:($c2rۜ!*Ԥ|c܋TGkWOĘckV)G{l5(L2-:> (s#df>^:㶜+`(5x+9[Pʓyz=<2Vo܆?(4/1wg8`Һz螷r-kj2Y]MZ࿮P%Z~R*:]+\.c5(/J<)wgBe(k@k-o,, )8q2WFt9|շ 5AG5+hg"1J {sܨDIq|Q[ƫ~m8L2u8V;{x)[;N %-?{>"s-߶^/̓{+{/Ibwni~l'/JQ/}6pA.4᪡)B:'?0q[** &Uz^K]u\[VTdѾ׵׹[Wnǽ6]-hjhjhjr 0,20}܋ =4c¬~x [X~x)+L)#Y1E7]32r0flRPf2%]+!FGWOwŘ]:w|Jy݃ϿJcOS)9[+r6XFkTq;! `giG~ ^|)Jp~(ͺǧ<{v`xb+1 S}ո}!!/IY ! d.aR .r^3$˒߷{`_U꿑O K`萤UXp쐧VyJW{ɣS3ڦ/$uXgNFY)L%kSBW*Z$:;*r]-\}q vɅ-׬P)Auq!FV5'XCV]5 [,h3XxϿ x9ZFPf ke)LjS8SR1f평lQʓzIޯ:+n=G1QN/'`M;1 U-N%dЖe6]I3x{hk߻ZNCʽ(y_ro |Ǐ`,^|_4Z9HsUx˩f ob/sqG(d}j3HNOsQx}< Ye ~Ы-U)9s'k&=5ɩWўJ /_[Ǚ)w(.]J˯߲mU8ed^-,G>L~IUd*YB5|(1EZv!OGǞ= 15,ϕ#dz%t/u@&L֞BykƸ *]5Vg;Ďsl<6d1ƷQ(QXpcXǡ8YÂ>g*[ܛ|];A8v/?:ACVQ#SWM'Oܯ:_0QVֆ +P]F)YY~6ξ"t c∶fB؃hFE A @u11p4 Vq AP:,C5: /ϝŌiP񧟢{n˗NFĉHtъͻ%3]ɿ"Iw3ݍydx>eNYt2Ñ} (eXtmsCVn>>LR޲X]u7nGq}IS+ **ϞE{O$:Vcu>~|"1Nz`B2zGbt j~F2oN2~Z+[ΣSI ᪟>Qh림sG%T\ \ftNS[$"I=;p\bb biTڸ/bbcuS X:sF9N\WKgb.ͧ_A1oWذq3w 0j9:uꤾU{|DCƍ- d #pt,ŵbRQ%ßO]S_GK֯DMTTx+Ptnތ%_jLQ׌r_R-r?v]GWV`ĹH Ol?R6M-M-شb.(VYFvD8 BmBy q4#&Jq 2 Yq^?YQ&믾qOg8{}lI¹K3=c.㚉|A+IL< Ջ J:zLQZS|oY5dhJol ̚v035?gI z _C~~!(TmX42lWw/oJG8cm2'z|LVUXe{vwfm!cmI-7';@⍛7koZzw-Mr<)LcfQt%Kʇ,kHa6_R2+#46c }><-PRY77x:uEWUIޞ/NVjsUJLZգWYe+({";~":t >[Qkׯw0T]eZ+ 1!j[(DÁτ,6F"sC='W *tVQQ4_*1d0f}g9=XXA2߮Ǥ:iƤ3W;"xKRى~~{+L!!Te۲M U)1rhe.[|%NZI9SHxTqŊhtE1g^رe+يp5BǐP*)ؼ2t9Rzpf?PUY?G/զ RO>yUtњ,f{zA"&Ͼ6d,\X_*MA}٦'~^G 7Upst/.vc;7z\%<ѢN>EKaiP̋tEUQ6q`~giv>%pprgLD\s):Y?MsG>:9)2,`Uu VjB?q?[VA,5z9P]saoڒ/\go* RoF232p ڭǯ׮;z1Q%TʞϪ'$m˱af8<2/}hOՍ'd^ذ0|ﷵJ+Ɇ!o ؿ*M"od$}*cUYHK5^ g*Jc#XQ3q+L!a3i ޾|Un ~덱lo~-LSM .?>**?ižTM-~ FT^=5z+O)240kKKfەlټޱ`!Y iӦqDk m1bH>p7n~h^N=:%"뗵($/ͫnޫWL%_Tj:DZ? +H\ctkGgxdH4e+}UW2(럛l儿?O6&&u}0X[xܗ :5Ln鎛ZZZ_ Țg:68N1QLK2H:x>CqaD(L}">VXJءC;4d(q=cǟ#i,+P).cƇ+jjAƭ„L&C;dHE-6)$s[F؋ 9XVm%4޺UUxEY {Zk@v^|1yj(ɣ1ٚEF/?R3^$(7醎u}Z%S=O}'{"ɢYDؽ.͐F2, 8>Hx (ؾ DIivsբjB+>C?Fak{SX~j,O@VIiɵua j:c k3.p rޒt*v/?G“*sO7xhjR$gJVP6P[ːzM&xA]D/[ 8w>/>idBƋUޥLVi$ǤԤS? H])ߥO?ߢ3+yĩ~ѿ_\&f8{ V5?G\&;uǰqJL6N- F0% ΘOJ 1OOJgv{yQ1g)fo&w//!5R9yVgcٔ3o DoQ3(2/=;"m3#P t: ]Vxc@H g1p ޕ${WM%~;B+輻GЭxy|.XޚZcG#39ι"!OPg|9} A9*S7 +Щ{t  SzHUY">^֧Y_́2Z`V?}xƷԜIqIxء]8 Gp$*F}͓JV3̺ǟ^,<||=zgXbOQxgqz@LV/o~B¹xxJ޸pݷ'͂3ǞK_E{FgrRm3οaजnffKAvP@Fn*1Vv5/7;пwfafL4)髝$_HHe~t:24У2%N?.dqzd6iFd@BZ˜`ay{4#0=d]ŝ:-)dS͂gavX%Wgٝ WIj?#r?l'NFa޼Oh}c2hQ7դ^nʑ{P:ƫO3'sS 4oV:MƚU+Qp/Xvr )OM1Ao|[G[^:{b^Ωo;شZ࿢ 1R$M3Tɦhjhj8|Ev8TGTGC72cS2 \ĩʾ`zJ:~:.!/*s`mR }eNz5| L9_e'cŋ/f6#>ѳsKap[8d)eu۷F|ƂFo2X|r!eG!7/vvY}v=r-F3 >2Ye_b1l]oQ0\łkDP@XyP F=qԁ 9 "^6]yZ9>P\Rg*hm0JnƵ3\iTJiKrJB54TrLmĨ"Iwz>TRRyV!((q?E_J pQM D=w /HGvlډ! CĞepiƷFWo?tҳ?,_E(>KM`'`Koy+Huťs&|>>z ^Tg3۬5M`j|oaεxp-~T;Xehvԛ),G! 9WG+YW%^@ǶA G}))}y?=F8J[[bؘ=U.21zbYGUtؐ8~~aW:|aORR_|`lw08 QvW6YJ)9>mhh%Ő)+|߅=My`~U_;Ѭ1pi<&۩Hmۆc] Bs-u11FM}~e ڲcͣ}p8nW3Npx4W;֡ψ ()-Ws]5f=LƝx ^{~ t ܽx]0kJ~r '㼃”W_`s\6Ʋc5d Ke/S[O 8ʴ"گk^lXj' [z_ ןOϠCG fLрyn'>|:PܟBvǏG*[ym_ehߛ~4 k ݨԔ-zR|9w.:oG|t6B$`X{l7_|IGFR*DF S]1a<7E;RÌmccyZn,u#s$+{a ,Vë&u_~j~VeErsMZ-o V; U>DM5kjhjhjH8}9I'@f>Y)QBdZk3v15҃f#뵬xDK1Xr 2?Oa%Zzaء5`| 89Ko.m(HYUI QTRd7wtn.rAXJv Kʱ7qJ- ofQ1?suBr% =eZPQ;idt֮)%)ILx tńDJJ42")+e r}|^ Uu7QdhC&j ~pQ8b/Ō;>rJVFGi[[OϸPW&do)M#UzMul7o~AN {dj UU5t%ٰvg"8d X7 iȥH/JƅcnuUM6|Ȳv%]I)SNV8GgG'y%Xr|+Th F*Woٍ1 Z^@$+`ɳ0Pi}jҒ{<s 0"ui !Y>\l{5uwG1h޳ca O*IoGʄΞŖ;PjJWA1Jes {-yvl4Gab "OH"]~^v_W_VDEt.?oSJК( lBj}_uB!,{4xp ) >lm/7|/;T[bX_?>{7AŻkU]3fΘ!+'bnM{ĕ!d+^XreN'66U#C *$eB RA.ItW}離}mOuK{EsUN^=nu [3&hy7[P-nLK< ]K``F^%Va_"yj 2vtW7ۖ6H9gbRB2rM |eXܙSH x5o#OvKAdC7̐2gto!ˢ\و?$vg;AAa3>f̜ ???|2"7b/o 1撺JU$sC{vĆ`y\r[T%:(kT}QbIJI%Be{lo--{w^E hktٺ]?bVO579^ޒ[zEb,=YBr;yBbw,*Fa=sVb3~ md{"91Aw2d,,%+32Mv̓QMpp(m/(F m1sghg­ V Q,Q@Shz_IM|{MtSPrb^Т<;E|M61gzyCȤVғg?&fRE< } )H%-ﭵ4ttïAb|9@uK=*G7MMGn-xI ,л콘/JH8hĔi0,:r/jK `on A\V<%6NZ6~muGQy ^,VydNୗ;9 N~AAI7eͫC拏jrys\7wXJw>eo?w.lOXZ!ٙ5\&u0"[y о8u 6>EE`_bSc>k Nv2lĵe\ @7i?əu]+{:0`faq-ٕb!HIJsMlz 5o?5s/8=1anjfMaEܙ_SO=v>~}hT 'M{8T7b:TQʜuCݥ,|ބ/s>vSؿe_eMNZ։@?2c =%v̝/JWP}6q T/vًw7?3! hu){H `U](6g"75GueydBy5e;}u ~d3XFدWh4P@VWM`K*5'.$ %q`Q:x'b 2+Ǟ{a) iO[^A׳5}ט_oAoW=]hic0l^X026f;5S7k/DÛ9l)Ǥ#e [[5kV:ǜݮM+a䫰Aua}9_yx/T?Դ߲wa>z{<(7FA„ &&KqY|g sg(~,U`6*J) 11R~WUIYZ/"*XIj-SHjj9-\! # 6f3O!:: >V,./۹S:TlRWOۈecO::ĸSQw]1Wx\~-nY hJ$:%!$Lȁh7l]+GJwubƠȝKS:wYΌyM3v{WD9.=@'Et(OfN`X@̞<yYhԲBCs3e[%FURR٘ >/R:)nWYb - 5៚&ؚi3aL4G >'*z+,.Οݫ*|+}mdCy@N/#^DiHUׄ0AtQOWqq#(A0o"뱥q n?5ҫo a`ei!pq^_Q+Y8u. XXbQRŋ6fRņW /W4SϿߧTAl=J:JMV:LEJa"Se+ֿ+s;Xr}]LVi+|jii>rry{cOY[{f[0WJtpdYx_uׯ}1;$ZX/,)l5g{; sf +})+nQO_~qS 6'9!#ai- 5>FXzeu($w,B%b!,\nXEIՅ~Od&Oя_)(7>`PN<Gl!Z_͢NSGzՊaʟk}3"lXf47p-hng]$DFp{9&Ca?b;qTX#1\Qё?G.ä/2w&e̽3rajs[?02FDLIA4hz0`MAZy:FegKNr[y "}@ 2K֭~[\ܯ(fЎzwW'7an4P۬M6v>>sŽRW}r8z]~=,1u\ߠSA`0` *E̙ml9M v,4rtt.hn_#SqaJjSҷl%;BCAḙ j)>ۘoDw@q;ߘ0mZز2}U`PI!qb^i+K甂DDn ;;G `$[6_|h˖ؿ ab`,I\KJRvOr%x](s9EHI㦃<7=Sу8PW28)oh`g+nrsXC"+;i3:Jy50虋i Qha{߱N M~YH$i☫ ry[W>_.{!0d폷;]s[JүlZ]v8BЮg E  L~:R ]m =Gbx)+BW61׆V}gb/b\F5]hEO@vnr ~>SW<պP߳n**"jCv*gSJP-nV`d2VB1d=Fd'Gs柒Mi`NsS-K Sh^0+) OEhs5 4TH@nI+V̴pE:p!SO߄ZPիĉ"K-jb57 ܇tzqZɆh:8ѾN^o&d>mf0//Z { XWh$1_dDImm&Z2״5NBهÆH (*J0d`堝⛣2)afb{QPX=>љƶuf=]lm4'<NΕgn4̓{U!Sd3kj]tI-{2 {~݀ttEDmzI!rz&[ݠH7W1)khO7OĊ4ĤPMT0zQO^C!FoHC V\ L=)gXOF  DBi=XW$[ėtO2ji,$1Mi Țe/<$G -}#>OX%EFV^#oz}=(_P>c"ĽP!>l'[P|+"{ucר ,@S1NX2;` %ä SNqW3BǎǗ˾&0?:ϝ=G) _&{CLr>x{bR L" xf";3f2v;vlU%R՗15%YM[Vz/ۊk^1[,^$K`^ #}aHֲ㡭7!sk}0ςCa_ʼn ({xȩ%;:XeF#SYFh/ f `7GFHy\C~mG7/̞+E󾱿(mۯ`=y) PP-ś/TQg"1M+>C[u&<@h+.l_{c6-1)|.ƜL,}z30%q7~-G eŷl AahdN5K'Oy1ޝ֖̃;]zD wl|i&?#;W}Ii[ƍ > *o/@Wn}l/6=IX$C{ś iJL`E{[Xμh 46֣$uZ1TpurD;[W}eP J&Hka ;?o UI2VUŠk[j dNqѷ Ursx/멺 R(oݏvUU.Ϯ'ES*Yy]dzp#XLR2q|" }9n}.n+:1 xe[`'y1CJQcR%ϡXyUv%t;LD>`.T@Kz}GG 'HYvc=Zrx 0ЪN2HgssBBJ.VK^m {4'S¿?%fW\гU0lXRW_c؈R4:wmC)lLRw(%bq_8q\b9; 6zn\j2vcZZS~NtffKXy_xL7ɬTfψ~BɘMDmMn>+wߵh2Qr˯cZm~ [=O|jWU,M,V#z-̖}f u棭@+EɘlE`p !D`"iSh5yHͫm /0i*?zì|Ӯ|=}2GT萭i";B `whO⮔k&rA+C.8!dKY3oe3y᱉hKX?`D!CP*Lfv>ήΐ[M\U9Yf9x4)bl +R[dfZ/@RwF:D.(kއ-'pbu4*RO[oGaA!,_}FXt.!%Ф#ؘAMm)W8(;76K~IVhO#l[|Ž?}~Y_|p֎—fl#' @&ɳJkK2į⍏cة!C('oƑ=0`Ynu5*ߦ@%1̭`CUҡuX PEϼ"M.&}0ȡA. 8|a&2BnI9.+XrQY;^c'a7?]_7'Uؓpۄig혧@.Δ^eT BԬ BV]<0%2|F q|ũ "& 01zGg_9ym JrawXu~Z^w*92Ȧ:߲e nJsj_,@IDAT媵HMJfM6vg0**VS塕 g|>ؾSg΂-02)϶_~"`Y 32h`n/g zE*r\6:pDae!8ǘ@8Mv U)@˅s0NKH8,$UPLb H@@hol[L0lu;'#\Xlr,lA֦6TO6 *H!֚F4N=F]p@k(eU-:vcA58_X[ZZpת^']sZy=&ġn^snנE@sTWs Q>zH@kϥz^Z}c{rcB+S/YS^ -0nڭ#]J6ѬKLSeY c"P`1~a<{x57V6JJ6"P5_(;qi2xa$5(srz!1 ]*PX TUo+J}=X.f_LhABjS-GZ`ŷ?Œ,M1Z`Hxyymٍw7lތ6̫LxhOsX&6 [ޯWۓDRn\QSqa7G\s+BYU]KIKU-)WL&)=F1hSSԖM%y(/$-umېD~J 0>#x'g3F߂},B:z(4U*RP0ʊJ1sd4VCAYપ&T+J7 i`k(^rk,0Ӎ!<6lG!܅L֙5qa+Hc",֞qt<[)l߹+*Aiq!sO!wJ--%U:?!I4 qMҚu#Ž*J f÷O0YdZK:@YɛbP{=%(MUK\Gnڑ%%#R8=*Ig ;:IX'(?H*S8[ xvӚZ_|V!Rڽ.ƶwkawKq+lް v?K s!5 6oksz<7wp72ѶG_p׃|hyaf._ACI?Z>vhf-Ŀ>xK}X^qt˒TT$r)oOr}慽Oߣ{P%:G3k|QOIv& s'Fi坛aHJ3>*) e,,Ms$3gq*̽w'WO݇|iiU RӲx-O@h :~︧ӱOoGؤp4ꁍ?/|)\U^JE M-m{ͭ1 SjF&* ưc }3+[qq1ގRT 667;|qI6as-Fg㾇F.;L1f$eLl*~\5Ӑ \XY / }54X )#H`V(Ce;Qz ŹX7|7(J%&ӳ1r4)K_Nxu_g9)^}9}/oT/[qu(\}d'o+ x}}Q}u嗟c9ر+e6,Qcvh"Xϔ smSdSEap8ߏI`"i8GMYnŽ*҉h&8S_)9lqwft|^A\߭Qŋ S' `տ$?{ FcF*'s gr xI676֢cmYIg_}1.}(˖-ӧ1 ,$)1hUV[Czc5f6w ސ(I9CzF|;}PH} ?`#qX! jU1Δ@1֔Vqlye)}P]߅EFB۪2_ L>PY>S"ٛ ,3`G*)4.VRC-pSAֿiLn (Q!(ʗa?H{S-P:mMyɩ;w@2 _.M##CYSq tS|L,{y[b:8S 8[lFFLk>c/A9Uelؿ^&K ;ȟ +m| ~2kr^'S e7<:* 9 Sk[`+۲LTV89 uTLocI3ϽHPO_zVjfdfKJ~^ٞ)̹^Y¤; T%*2p %9 EfYo/7e+7KN X_1/[j2cV%l $V/]s};ڽV$n=8\2TŘL8=c,'V&ή#E(匲;A0|P5l1&jE&@O .mB'R.!(؇FR11Ԁ62` Êͻ7- `Tc|÷JKY6>y!8p>_/3Q[Z+T[aA?Rz}$G.E /O 0d2A!mgH!.AJXU}苮 F8Jpn"6-9˨h٧n-TSOuW"3GZ; &|.9 mXYY`έ-x_ױ41٪qߺbVFԐɚ$n]L$WIW Ъju}bRT̙2@X>MV+zZAACjýBKVk-$*(y5ڑSTfi 1u^5/wHJS@Fz|sc*"b= u5Ujsp4E َFv(H<3±V~w͗Ņ,BJ2۠%p?( } [ϟ΂2cna1ֺlGfFҲ9(Cgp~3)x~֢}]$mEYSZmGP~71Zd 3U,0A#Nec܈RuQ8FǔFBPUm/T< .W-AjûпdlBcNϼERG\b`cދ 1БVts͖}l@;ٽ}܉gW'+xʤqD*&kegڤI(a!ֺ_yN+fZq}i3OZ0XO%*37ND>Go!㻒!r7bg"FL7zM?._r>ۈ| 7InGWb;~5h(4uR7N٥ nGkWb,q߄RWnt Aϧ>N 28fYs#U.-FAr*0`Gg7/,Eʜ<FZhjdv(,.C-e|{`NGƪo#W}o[JE%Y_tD2/e|/Zt!v״ p ceG2AsR/Zd3Ĝ}8{7:`deņd.F'w1}-<썋dE;bNt[ꪘfZ!d8VQ)#9B(VEDD^YEIvX \t%L y:[_3㒎S/*pqW:,6JmN&- I;A B̙}֧D]} .uqLjjrql G;k\Ldʬ+V1Ӊ}AUz\!ԖQN6{U)eLZom⏔ oia5 cBmS:ut Qj o!]DA8ԗ6_"zܪf}L̈́^r(A=1nu)Np>ܡJYkKΜ< LGms2+[44[L6pq4G\r.>=&8ΜOF1n`(2JQZ7e;A}<_tтu8bl-'"(?%G&w3cU. `>מ B);sgil*@y09 fՀ8yJ9^qЂ2vn@L>T`;*L(t/Ζ }o߈VQ Lbs)w0:𫥩s'Ub>5;I+u}}Ɍj+N&uwf9!Qބ;`OoL? !a`ԋ[}D)U|[1﹆&(E|ycӨ2PqI;>cTPx$ge!bEYF9f0^NcS2n `88K)(!RG9LshMf2CiO<~$-382'_|})L0zf߾2Ul7sO\v$[hA A@]8XaĐR ߶mF(s!gEqaPG*VS^xd1=TP_IUbê\*;+6A)_d"CF_<5)RC./eNN7q| dz ~R_'+H)ǽ$('Qѩq([4+*%@?ǓVqF_<D_-~֧o&X0MPH O0m JƝ1-AV\ `uN?'Ώ=8؟Bp ^ez2[k :ϲs֘QӱOaK;39yt= kpΔ]* ›ZWih5^WS€~^ވG|f`nI&d=}~LQ|84׶q Rj? q pR<ۄ1<\%U,K׭OX֋cz·vHҼri C"J Wڅ(*֫**]zW[@ S[@<^[<`azgP?[9W,dȀ4CL'L 2M1| 75 % b޹ s,ANU3jۘ{:G^8wAʷgK׭)?+L;RE (L)|jHw()-'U4W#5dlX!!<"HYM dFœz :pUkΔQI Л ["dZ葝Q&3G9PʾآĻ!=}?Y>`z)ąhhh;w{US$|4)q t^ <߭=SK=.ڭ<f2hor1̈QQCvM*3)g{E;s[{+n;NJEFsNą3arp#rJ)coUhs+Pl3ˇm›G7Vyz3jXK#)%9W(MkI2yd0/_oף^8DL33(8r#(#V:R~@Fwߏ'I 4-_RyS%M2CMHKK'w^O*Va $ՊӍ03:3 t 5v` T˯-Ł1`[4(} bXwR?)鵘`o g|IUp$5 桰{g 8B/̯ȭ$[T B^$Ig+ ,2ȎqprCȀҷ\ ؿX||&vMU\θe\]ں:i#*s^" Cx?5Nkw`aڇ)||T5a;\>V2HWHL+!up褁&Nka`}RV> pHr UTw͍,P/ɿWA~.s 3gֵ(xӲBL>2lWX {ߐ1rюog>JNsKұgدFYq^.QُOwTp]ծ`sUUPZ'svsgtfTCC%smK` YWo9ic)%ν JdnexOIWYmXg\B{n `nwo!/OBƔ q@N.\TZ!?B_/K pjQƾ[<b5 >nl]}.+{ /$ЙD3a=mGMY1N9^>8z,)䲿st%IFȱcBmmŐ:TX!0+^*.X^G]`A]N^{1gc%֘scؑAIek~P 1߅E{Kf]?_aҷ܂̍sF&^9Vba BJ@1G޵#c˟?>RYo /6XBN}&<yc9}f<*Ȏ *ep`zU{&n6 `T4 JIw76u m&RiőGY@3TRe Y7=\/XEw2c舑R:u{lmz6eepRYk^[ѯw;XU8֑M4GFT')Xx%7P>Xj~XU3Q."bR-pPWnd茛!% oB)&pR7y[#Va DG9 A*U)Z>>ndN"E(94a$QT x{1~8^% v/ٱ{w cg9턓Qgb\e8yAtr1? Aᛕ)Ni:Ğ?Ipdiѡg.z VaDu6g4.r2\+(bCȱ/H&:hv*\aS&8I 7鬅2q4k2Df&4H71RgΝp lg=BpA_f5NfXɐz7G0g£/c]rX8\}$bOΞ-\5?}02G i166{ヌ?@F.&L,toP>dS܁`"€RV۶߶Kc:d'? aNp=䅷a=2iٓp2PVR8: ﵰ E?n#/@8DD:BVdhU2dEБur+Ec踩rJL|Tbyՠ n5CKacnG] `$ #{A ', 9SF;6 FXzpԈ#.!.UeArSpbng}f6Ml̈r.07~M-jHg2Qw3嗕LOT]V| +vYw=1L>d|ϩnPVdx"sKXrj,݇+f-d'A% o#wͧt"Y&`g|ꦈ&}-r9JsӡІP_ 7܍,sd2/c3Peo@,ƪ|V$$BTr;yܨx'zjd"Pc1N~ ~X؇##]v|X@S4Rl+_ UpiXyy82@ Vז#3/6n˗#Y~=fΙ cJ[1J2*vُ)b|=Q'0e\ru,?M`Bd]#A00w!VK2蹢 \tfsӃҹUA/. o(+Uc_DEAva#Q?lCRA3رcRN[xiu2sFJ4A}acaQ6`;1H"Cs"qĺrrwXww!>2C7}ԇ^RI74#M\=8`\ޙU_cE+ɍYL)HbNJrMYeXa@h?滽&J+l Mf@ay=2\*=\[6m`܆vJ`C$AnT_⺭d(%qy}p#SL cZp kA ?[^8g 8p |0rDs<6dVttT6Gm4Ν܇ 4=,ۼi=( .=cxJ<֐[r"c&U=ǞyH1npHXF&!+ ]x]Q..$ږ-u}W}u7ԠJw@ ! ąp=7WF̜{̙Z˖%8ȝ;v:BWk(/*)9~&\vRҺSEݦVސN (҆2w.)NX\k ,Uw% "j+.nEƙLa}+MUdbÖ m4#&hIؼb-;r]@6'2\eI楜'- 56@dYux^m𞩓f;LgSZs/SUW@J$sFA5 TXin&v]UW6+V^=I@ VFZ֩^wXY1z21/mWB};`N6܀ ms=3\jEn]/moW`m! n *ytŸI9hĉ>? x͗FXؙc=O0) EpTwǘ[+7*={HΥ"%-]ʯg;@ZSrRz&[y?5xrNaƜ0Rhl>ՒI@W*C]H>a#lτ}~l#̙6OP:7:`˯ /ډxtDIy1?{cִ xg׈?˻V}r~ޣ )ExA }ARlJvnX3 2399 e3jN=T1aJ|[2( ZGz3AEf"Rncb a!^>n:.ڳWodQ+}r1qR̿"ʜ !@cIpy}ˢe9Yd7D-L'&Ţ2Y #ځoH__Fܙ(8{bptK ξJ=cԘqpՐi4 ^g&k/wQ:@}Gi#,4ظi}M/N[gWe 0nGOD!n~B9̴S[_~ odH2r2eɬ򆹵I &E_",d[ѝv L`-)72L0ӳ}M_d!,_  ʲ=$U٫W2)QMU37'l?{1k:}v`pߑ-%їR;{+ p1h5gу1oL)Xƪ %epf9P-An9ki1x,܋jKǎӲoþmk1[@Vŗwc{Ǯd~/[ΐ@2U.VD*wPnv)8!uWrYv" mGJ7ڬp0y3Ȫ2nk1SǶl؄ *3.)[֕}HF&@nHVD :L'dRD?6ur)dWס{FFX 5CCL4$#۠bJ־%`H0?z73j=OÃ6 !!n4meMA&1T(# 5dZhZ9~jxVV"qϝ8J{#πVUO<m߲_Ok&ь=mP>yU ww.aZ /dyݿ IJ +5cN2i&zA2I u,ﵯer8654KW9T|j91u pߌ$|dbߚr5T.7 4cd΢>3oX@ߒ{KJ 2)bY7΃AxDC)ǝyQO}z{)nZ?(w<} gl,nFwT9%ț$*VDFpPHvE@-jZEElwx'L7O:㓇o`]e։,WmZivҟ Ǥ<ɤ/ҥr0je%SN;T*DǩXWZ~A 6=~6җAvFuc'Kb$($" |lK]E"\,B/(q~g>?Jx-[69b^Dmړb"L+,RU>q낭%^z"} Odܘ*E%kEdEз-{Z`A nu۱{ =!vqm ś7nLEa=|D05+ An,qt >[?)f@l;"c{LF 7s;c۸1 0g6QM)Α9I:x1nÀIz0`{Lz(O}UYݍR^{5 Z `"c.mlN|-m1>0ol SS'ޞl6mCi`;bؼn9zt J^Ȉ `]86ʑtD9ރ;6 ̟s zm ΘQSu# Ⱦdcw"CϽݻ/<8kݙ5t|e;.%Ks[lp69C2=C q/mؚGEں(݇~k^ qIH}.0pRAP'U}seXǜ=d#@h\Pks6G ?W^D8|H#)*0w62/%zl-m zRhd1=uD¤fzt6${ϖ~܅C#%)0UGOCAN*B{ ͿghLi2_VZ-;];#cOg"% cnA(eZ6_'X'yg17 [J`?8~-څ$H2]>]q! YkaFqkd"l!@O9 Po%=ۚ pC?e5?<}&:w&!L*L^i!Cݕ MK~E͜1 L2gbW%?ÔN:bE)ϫcܺ~+^S".*!wRG/&xxŽI]Lͩ|.! 09eJ=^3(S>Z3Q v(; >8-\pܓ \.psU&XZr=Em[*=EXe\תoou*8X(/cYIu-M3-|GȎ[Tر|\<ҋvwNzC 7e3l0N8j&f%&\_>A녍mbnLebܗ_o}wgG-8R݉V!cz&0<]V]GƜ(~lnܨ'~F@wL.unUю >k*12W^`r|-} [@II'60$Ҫ~з-7k0?-"L_+g&!?));A&n1Rs=gG Ep$c2SFPXN9'Rء3 DBaAC$3 <AAN/e Ss[ʽbk9%s1x6(*L 1 ES3w&?K+?cD qs̝;2U=.#6mތ:^x*x6 eRRs`2621cݘ;NĞQ}%)=o AzeƔP@?##E6o/L&O V=L;2kCq Z>q%w#<0rX vbO] !Ed El ,p6);#Нg҂ :vxw;dn^jkutr~)_sPXk3gcH8C֒wAzFcE-U\Gdva6 gڲNSt?d[7̬ .6 G 5;G,X!@d;;xE?Af?&Vp!6f|eG2RmϿ;wOi‚622}Fw5g0̢Y+;h:/5/_ [!$YfV%x$_¦u된ƍ{y "{D 3ϣ!5"eq!7'FʹT;w& (QZNc̨ a`3 X忞Cf%j)ẛI4x!A~2/#bh8V%(!ѨєME6W**SwUCZyuV̸aFKU2֗"ѫ;ޗkN{|w c<>6 5kC;$30}r*W,vdZpcxo wk TuaR2{q.TDrlͅoRC hc/.\d&[h&zQvmdE^|a̿Ɗx'ӑSäg+dЮ8S?: ln"_TT{`J z|5?R߳s+NHLP1CSS,]чl gږQFa Ҏv-#"q?~ LPRZ@FG'"+=QLnI8eQ^?wmd"g~{ԱЋff2-D;_e:t̖c0#= gXMO=ࠣcb,CeLK_zW(fSZu`LrXwnYE&eݏb`*v-M G%nfWT?r rkҙ <oR7Eiy9(Z6LF+xͻ\h@ BXeŋ*uxܷTzmwu"ŠVk{K{EU]U֑>xctmk;?۽0&gRYb~>>ރQ2/cu^z]+4LnFpZ>:Ч-6oلg$IV~ЧZQшvFȽTMl?{4b!*;|l/]SEExL ej=&Y `,jf[ۑO[@~]Rw-j,{з-p9azG4w|5xao=2*Ά K-Ė,U$؍ӧI_ވ[A)Yؔ$0w8s=>,C Ȧj8JgRYe7E2Nġ3'E8n4`JŜNDl)<_EeϿw%RuEy6]9_O!}gض~g墫O0)38}x; AFrn{J?&@u;']'l+od`܄`K@N]~yo&{RkaPAU-2N!wW{DzVL" ;WX3)@vz10YZۄWlqsnYܾO6,g*b ]'hSZWc-@sA7SK2s~{+ǘ($C[xX*w$CSP&aŵpro A?+(,;%_yn/6 rӐ2cpҕ֚RD_SgW+mєz5&KJJ'eruPyHv'QF7a2Yz`/3qxG2$Κ"z=SCC3}8CP)So#1,'S+QQ:[-g6ڶ䶽1a5Ƨ%qtprVG}9zL_W>@]?0ӗ}Yid/#7>B1cqR[n) s"Ԃ MQѲ^ IPWeM2-M hYCUс{Ȫ'hzA+IHWXu*6:xj4e ; ~뒸_U2,MЙC9A42l,ʙ|ڹa}kgӅغs#_y2 Ƿwj5-,5].ʡ(EXk;Yn3ig0܇n)}GU{{?c6*]n̙0!E[QdDRXW~VVYOI6#/è#?BxPYt #SyQWe`炀naʻ2W^P-W ?+(hGӾ 0)҆ziY*7w97LdBR&RO@AEߋ]aEؕrxvܹ ѧSɔ, *X񾑄/!5VN:cAҵ%an`=hjkd)[Z'_ wSVĈA *C1lXܾmtKH3=\ØQXoukUV;|p.r9v>kTE_}U2q:ycTJA[n}Cډ7aFFRbΧ&äYwbhL>_c׊ґ#LɁgxǵJX2i`$kL3᡽dӖRHՏWxZ9vd֬Xp55aT?%R.ړm(PZUM Br^ac;QUN}.fxT`z>zSF9#53S$;”rϫ~\CXWĝGˊ֯R# )z<&r7=< O'xv JEyʤꆮ Z"Ig"+ȶjbRA);)b=N*޼v'b`˜%fL [wƬYspwο^Ow^~ZTBy |Bu"2[Ņׯ/ 6:?`v_|f&G!%1ϻ4z$Ėەkcw,GGwFDJQ7<==pmS( ѧ/ 0xe}O0xM}sv@^ WWq6EvA1 -4w3 Ng/Sy_;W&t2܅u/:O&2IPIQAIDZ䙠~3\ 8gi-Hԭ22]5#ސdrǫ ^;nqB@Uiy]mkԃJ溦}vw0< \adh7K"3UeG >d }>}vހ<Ʃ3m{NdM#>ث{Y"$SÒ+WdwsuP5,kYi˦&HJ@$ T-t& ;IoD{%;+t}p@ZD[~ `ݾc-rؖ mulJ9|0| O|{K#tp8"o?\B0f\Ӄ5l,jꨘ TtJK)\ "S'C=IňQ95;h PP k܄>* JR&Kd#\Đ30Kj9L$1Z!}.8&'}#5>H'g#wϼ ;x2R]y~hTqL i f@%+'(zy8kQꉨA۶\YSQأG'aŒKlYu[f^\:bdZlغ?DKD-?[ Xy P^1o n-vCƦfxÏ0̨Uleb *&ϸw^oY 9.ʗ"}c/rЂ\& `ZV w d#[׮"I#rOR{0sK*?‚lyٛT)/_ *MsO)ÏO={c'/0ifOeXw FJ0K_ʔ߂7!ȿ\ `խKalU?mbUe(\Du+6XoNbD7"m/<$Ru]Gx3\}RE5<ڱ R#HV}e-2ɒ޸؟} [@dK*E]S_*(^ [TVrRLWi\} [@\ UXX_})Ի@dF5;ʜ\|on|K-YE ԗw0%-+ol1vDt!#n䠲ZAtRN:ć_OcHɳdԌRdi= 7LW9axe+VS꾖\/d֪ O(F}ʺ|WH4b`7K22M,|JZׯlQmCOO8{GiY4q%S|+ꋸ曑v\̆fLnA&%Z ڼfĴXu\yVXTB/ S"Oを@ lrLܯ+}d&Ȉs'=C)t E ~"":*o.n{2ܼя"Wz^d$ca6q4 Qb} E+~H9 ^x:sC{}XI&XeXg\YׄO<#Bɧ6)dFxٽA2] \=^uO? d$@CP=b(][m W~FgToj1ND+H>{:KڠNQǷ*k~K@J*?KM`!a3GG&E+0{k wxZ%8Y嗽k⨱63eB2 Lş"޴u6@W?p3fp@ LtaսRȤpJC/8W=BagWr|]$C2%,3Sѣ"mkBF6RX{MOބ>ڀ$,]3fÞQ$%|EjkkOѾUST p{n߭Q 끬trBTJҗÚǐ3|v/F0}F wܱ+KJD'^(aRF SOgHf4x-1UeڈVǧ~ټa1 eĬ%I{zz],8WQ -+gw wJws/jcG,6!v֝GՇ_^R"@t[KKclo{I2\+ gk,hh^=`_tYbޟee@ic~Ͼ]egqK8iYSHe}XD53 N5)R*Ș TpQ*zdRZR9񫱱:Wwm𼉀|/yM `2LHbL[L8zVNE궢sXPg[l+&UkqUPeQ|l/^6N9/U3ᅫAc^kIg/1cM&2^V\ZO;lyxI@z鋥ظpLDu|.Iev0Ta62&Վq9ڢ:!"ߛ$OM0Ŷeʎ&ce)WԷߦ _SI)Y&.tH_.ԠIeqPo8/?} [@*"Iv/%Yd@Jujo+4l)xU_}]jDsm?yYH۱[cеp$%CuQ.L=y,bs3$:RR LHg7<P7*8 j0@ѽDZJ$eeR ȺSGY)',a=CWػ"4&MT̿m.WS@ȎaY^!>5ylaR6 }㉍>7ވ5W\);)%gMڏyCFGYֺywdhD䓰mj(;K>E Z[v2]wލ GѲo GFREEquuA w̞+`,6Mz޾/(;y; ̢dt8GXO y+ aM~:'09'd]k$Φh@ˁV(OKMチG#F>v] P]:J:DM$&̿x6L= -r\IcN|sկ=xj^-u\ `dcGLnYw} [5dca 6~d9+fP{kTQE;t$>{ RBF*|12o>!/|`)5tƮc)(MT2FJv>>d!^zQؒY&,jJu牟.C)#{zv587J aXX `1H8jy%gXOֵC 766k6LLAdf)+^RǠ!X O,*@L .M``}mG(R_.=ze[{'cQ`T]IK{TVF].uV/j% *`5#;5[VzR͛" 9h2s MSo}uD yёz!Ov@H!֥Gp9]pmMYFP%+]Uγb^ZR&ںڑaq ܛC4mVYTT~GNH\R>%qc+L:pY5ӧ?Hy=Ɠmx[o&8C LħJvRjK~ ;~L9j7ʧł׶2}bac&("O#-n+o'W`%U8-1eJo\#B([?zHZ;k4q 1i8&3x^/JjS~@U-֘ 'g_-洄T?#+'aΎJhXAJ+++R;0oҍԽL"ń}zd&娷wF0'(`qSWODڟ~@UpՀc3sVMA׊~O>D WhAB9CpU$aa~uڛ',E[ϘWY߇Yvשo} 3v̽drl8t-!w*~;|k|HLa8R0Fq5DŇ`Z $9 /ev8Z*+K OXXt?Hg"KpZ.>ܴGʾ^j mLPZR Kk6+uj ~ᘟdspyrϒȑoչ*˦LY|PWr8ć/mAaƀE2djm1l۾w{쉮lFJ*%A:RRwRQ\o܈^}MXuSOoyR3"̤l1~Pٟ~,qa^kyL⋺kYȯ]^GYGwyοLD7 l߰IńၔW'i5) "ȔIJM͂lHWOfHzU{_:E݉nS3Ε?R)ש.*;q(A__M/ƛϞ`U"GƵ(8~,{Z9.֍`[ `Ⱦ %SYp, M52ӾrknMN:F?6# ==ppoMu;R,9A'ʩ{( 3Q {?وux|.k)7wr|ꋴzx?TUz|Qͬ)9Rԩ*=g>y da˚jW9S)=peZ凅Sh=~,hБ2rz6gÝ]SU%2Kp+Dq*Kעx =SS&K7EN֓ ֬ D=P>\.[=ǟ8zүވ,YC7]?/>τ3K4V&$x܇-`tAVK&DgVُ <ȃdAyl_"XUXvA>Ǐ ֯ڟ` [ۍLooKX.Nr)vʷixXR)JMH©9qlxpLvrǗ< 5",`QkP؁~Ɲغg;ns~-9vb'މs|Ħ^ AM{DPh[GyFQ, oq*ͭwN{e9 t-}<{3:~l{f$ɑ* Kmk0a񰼫}>9񋔴+ܜVXEaɩ7rr} [Ѓ:We{ae5#A? HFJ/oY{OSQ5v|X9()z%Z*A* GKA{¾[# Hx$rT46Xg^~7̞C Ί@6g7N+K2'kKxt)w/e|`G. I'_n_FU+}5;'Kd5b(yh }G;a@'W$즇h1zf?c+sCY9g1lTu҄񺋕gb#)?lQG(gg^LN,[|_sSR`2i|T>|R N}@=-l8S s3 0S ӻ?'MduJX$`S >55;b/P/dff`KKFfgu1/ed^kȺ%L7K32́qs`9}E2i7/LI+;zt9rD`Uxt`>s}0ش}+.g _}Ha>#GVKuj̳7dQu#h[VQ{̷"K"Y ^{ :A+I_%`EВ qױ0kHtظ~ ɸ$t aZl,cfF:&DU>|mv+%;oqv5qO@JJ]gfv;L&ogw'+6.$DČ7Љyyg1ʒ;VپcC)= xc3QLоriUuIOA2 H+݆dj-7bPG! |.%8>~ h_+vs#Qt&" iAzy&uny{FwZ'hFL1FJon†!heJ]/Jʈ3[xxD nJ|Ji?k};Z]}ү Ө1qʎLp&0_ 0 cŲ~B^i{7_g&erCQ\Vg)/ cFnt)E C=+&O8YL0=6,$9lLwG?K[L8ٳm⫅>݂ae)'vrd ^oZRW"1z7>m J$gv3&gbDkg/t#K$#td?{\aynGϤ4) VX Μ"g̤@@O\JgDB^;,N *󶄄Tz`u#p Ego2x2:F]ֲOʋ5q!M&6SaƤ޴pRV]fuY2[Sـ[¡Hx{S1߯ہ~E~Fl/>rRA޽{R]5*X!Ep:ՊU1~@r%,&k1 LD$Qk-eTg"OgFv6=0׋iѓ.J1XWlXZzj߫;~lKh;]3Ld>Ғ&WGSWWdd$ыg5y2iZίJӳZ[z A%+X%Gee?-oZ`d?+2kOdVbu"#`c3^0C`U?sZ IHXM #6P|@IDATWߢ;h@h~2[#ғZ֓ F(dk"Nz.faǖ h]¿ŭ߭؃qQkHD: A>e3gar(i 瀋:=WR l^m胓M4K >&:*;;5A9aC~˰s%&rM[8Bi3Uw='Xy%ٴUQu2OU g]Y5p&t&ѽVj̒r L4{^EuWMsH$$tBUR]+CłHJN%^I_g./}ܹ3gfΔs^{mg;z ;=HcD!+LTkLI.R S¤3yJ>6"il=|&eG?1U[RSN Xv41*eگ/Ԡ^ sCO2%T-73Ub\8ӖBaQ܎Cw^$kמf~/;;5 <*1k5Њ)\\.hڻWde󟂾y wO4~ 4uzg1B=sY ^';GJϸcҿ^=){VՕP6 XWq&vbnH:yH'KD6˫%_}sbF@4 SZ:0Н`~[52X,%8w H,2YA5%ځs٫c?w2G0y֛o }zv ѵ9bLGD姟] 'i; {RZX2֯=(o`?jg)a@+QT2RKsp֞N^:(Z?:t* Zz}w7p߬Lmt)9>CQWJtDg |) k V 5`ѯѳ6yA >0Fo?ظ8DiT#J:92uy1ѩdNG~@:wR*߬X̜l/Sa_~q6^Z%#0u ޻t!v^nd;Rygh‡1gq>||z(D;K# PPQܒZ }ڀt IS@iA!Gl(~K'(շp,Lg&}[`hf}lr4 ?\ڧ>%IϪl ]B\s@vaY1&FU0ZJm~ O/f& fhu[kp@ gDZ3Hkq9 laAUl.d0%e0fR5VW>&1Ly~sq4.Mn[ӈb[< [ځE94!#O`ʔ{ }1Ϥ))b+e9qv8$icdgMq ߵ'LAi ,uP޻ϯAU[ >nȉĀєZOr؄idCߜx'fQ->l{ue*8>־"A.3g.o>'tk{aGпʙKGA\J]ģ>~~{(KeAv*P,0m"Jpg?'mCat1';%]a\ʏ`c.%njc:->.~^s")d6tO3I:y`X9 `Eua}b31-VلYؑr!Y}nw܆_}  i.%[B?36~5T;mMG7? 妢\Gt L%)g^eA3#HHUK9 Lg< MЛBè7~ƶ'`vIc\{dnj+\VW| #/53H mS SuV+KDeD72=;Da6^/;-+g2];hL4ja3k. [2K54+hj(5H@%Ia@W) @`HKB!g~.]8}EOR>'K膌cXf7 2x픲)f"!9u('kTv(.}3{ΜU;)|RL9cƎ\po,Vxw. sF:udTHls){ĜPD(Y[A<[e em͂BuD#9#qiջ;O#\҇2`M?']ݍh: M6WBna!~-P\X=lS^zuWS,Nj| nFo=:ZO-JpW ԯ^Vl[}gs횛"v?ܢ* pz ƒ'7$# #&NǕLJ CLZ2护c#B8:3?%W1Ͱ[,/ 3 0Om7c-G#>$Nw+E}+`z*R?-)W}MFmT%rXj6AeyvdwI-i3pcţԾS!&;$E~NBtxvoo#AĐ曯OG~0¾sh)E߽Nd S;K; [s#4Rb^pTw>sS=mHN~5UT؏sT 8}8 RoU%)Æ2M]9xy#(>y8GV:wn 5:{FkB@>Sj\([|dۂ%lw˘'TBV)az5K9xd>dG٥ңoǍaҥ;ekMRIdIb#x'FpLרakXt&Um/` |t4̮7NZrPʰrEKliu<+Uu$ޝ-;oZZ[)=}{2'Ѯ im[HpUXU)(,ŲO>Jh8΁j9 9;;۫(bY/|a䇙ÆV ?z O"[rRREUW7wբ:EߤaNx2kSQ1N`' #/*[/DO닏(G ?jxi0gϠʊ0q|c!L=>kGL9GMOrl?Zz{u =j@ =,nDcfeWS׀25qk֮Ʃ"Yܷ?tEE_,)Gcݏ3~J2=%W)bYjg9h'1gwmJ].0w.ndm<7ȂLĹD%C~` +m۶2[ /c3hpl#sXB޾W۹u=.\ND}rUT!sSdM כ:x665U>oy3@ǽMd8OI,}90s+)9nofs_X.m3IɅ6]fOdk5Ow`Jby(>DBk"HGbRPE0hEaCC-peRZ-{ ~`Ȑaͭv0?.K.)W5C,s L{5 Ŋ/d|HǧX-|laz)Z(ewn'+K_ӯ&&>rFeѳh.a8#X8xÜZK4anЄ f_t (OF%ޣ&pZD2Jaࠡظ;tCdc>4Ey/LKat<ϟmdv V50y*2P6ծcޡ +68|7>Dlݹ OŌ{fH%Y1 ˬDid[Ǔcb fX~Z3+]Hڷ?v(&θ^t+z]0JpH0Qbx+AVr|ŧHKuɓ.:>a7GcHrJ .yp5T0`קe\_yxswW’ ~X@&X/deFYJ#9A9&FV =yVX.HN|SG7NPn5eP= ikn47VJ2!f{^!mX }+Y,)dLe9[wK?bb#Nc!OߩrJrOIRȋ&2Hq\TȱH̴]cP~XwuJ3SЍٸ45$kI9{ ?c'$ 8Dx!#soVTPUQ׀5u⹨Y]vxuÐaC\],MbС؍LD1ع7nYSZdfx2)‚Zq|- R'kʘaS÷,%3R")cA%e)O',>ejq/sZiiiȫkjX֯N+{}&P+sLr{Sx Ll޲ d:Qݲ0}8h)B,s%gejOg!3Iv뾹 `?mJ*gՙ@U/*4lL)}FC MF⥳ho,&xֵ@:c*R{m#ʜDLx7_DC4-2߬=hZ KKcimmC' ^gk`!5VƸ0yIBvn6& s 5ܦy#\%b;s=;8#-1!=|)>"h<aCG251ed?[".DHEӱj5jqxN|姨ihA4B РĠN & 19{Nxۚ1r 8-w BvRʖ@)-FJ9G}c9`=N˘Gg!egݻxWֶv_WwS+q(s$d1E?!Zp%GN_@\+_ .%XcjLV++#S$_c@<" ٘>}6 |ZiŕdF]nÔQ5_<ugFY" Kݭac&`ƃK4 X8؉N|"&xqǞGIOK";-R گ (ݼVi3MOyu Z?M؉Rǖ 0կ7(PUOK` h"cQ3W@PMqCR #0=8eaڽw|!3/ &s@/8=}w筇 `c0YK~RM=p]!~TSwy =UO6U@+baZ| ZcfwӵD*H LXKU(f\[Q9:~9OD3(@D9۳[Pmȣ76.Cs\tX4 iJ;p9< PӀLvsSk2kJl~sϣ -K+K  saE4̚~?ێ%.g.f`PBJe$wlQ@q Cٙ 1fDJ libAfi0Yvg3oX=0!L^.>z!*'h%Љ]B jH*.ېLI?Z1f>$´w|:e G[‮53Fwћ0YQ4tz;)7GPB^Bq >^b~/tRFEA,a cy#{ aDF=d@{G"uquCV:ESѳwd$b!d(yRQ3f>..S9VD|PdUb|"#}In[cPU #6u6\\"lA6+eU썠04A %VsvA;{1Yuu*Ρo>NGFU+K%BL5g\\o+^p pjIVhVbYd/䫽"oYLpC^8SQX0ɠI/OOaۖ(#hL,VoCIiPA8w(Ν GCWA~.^c!<(a(O{xxxu=z0WlO,\;~6z Ev6YB-]FFZ+>s0a VEAo/CP; a^2qߨI^D(Af FV̀oP_^M *_w .UϿ6^zm<қ&|+q1"ub6r PZ.~U ai~ IR\\y=?թ寝nw ѩZ5M%3\1v˜,ޔk[dW[Dg@>9'44?깗Гjʯ8ܜr!\Z*NeeͲz3)][ۄ##*rE`1s[1f|{(\J'qRNόFqBU6GfOOėx{0>] ۲"EE˓;ytk? YyGT@[W#VPA~jߨo,w +&f'9nKx{;7zWә?/ sz+;;8PEu;')7}."&~+٪JV_P~ gۤ8.9;*&mR_Nb~ 7# f K2xޟJanB<4Ac'Cm_vr]]wD#dek%juk(elkyzu o׀YIϺ+)־F?y\ZNe%ljSEb/S.k=C6f39ڷNhbBc: (A۟ }_K); VdZ&ǟG!UT ^]q^pȾP=LVHLّ̈́J?Q_|-b¹˸ᄍG&LnŘyOz%^]Qy5fWW_~ #K+H`IO'@zH#Ӳ S":Mбpj qR* #JR7%y+T^mz 'mA^^  QATއL]~|xt$+n =}> v/ٽ6̽OyT+g ~.%88w gc^z<+n@X?PL٩aCc{QV^"FN,sEK-{7;o@n6CH(|Fܵaބ#cp4V! ajǮ:bG0عIђ8 Bä Ž8+KR2K&(?}?IǐUڿx64o[nhh/k38 )}`vץKܝ~x>Ξ"W..=+".Td?Ģf]ːK>kQR$NœCiH(s8\玼\tvǥmp\mCH4}AekMo7$ )z2qFj:MƌCvp({IJF A|AMM9Q4|=d<Ҡ Q1dD=O}ѓu[vūj2taMNcc3Fb.X'm0vfB^^|E8tH>X@ޯ|ldOE@ df_mӂ؀>z9vdM{Ȟxds R0Yg}r]IHI@5`.(n×asƓЪA8< شqΔ~? @l"[;b tbAXG@ՒSF`x_C|9AXm;Kʀܷ[NQ]mc4=@:`.X7D92 1L:!Z5?RpmXJZ-6e<20k ػ,Aȋ6>=@YU۲JB0̰_y16EnV&¾ƫh@V 9182CGSCԜ2A5b SO D8 ]b] 1BtI={6Գ&}\δ0Jfa$wqROK+VH f܃#sg.\j(] J6teZJ]!7#.!'u?!.1 EU8rꍊ OO' 3E8|yOL9yr62o嗻 >& =Ocr!6dlz28Ql{0+Ϸ V0Z.'">% QgA| Qvj x K;NF?w J/ Ք+uܫ8>Gfs9J*LyRG3vQ ыb!/LԵCK<:Q^Sե7xj|~0}x7m=i.Ɍ!84L ~4pRր)\驓G#_r픞Li_z{)羔I_Bb^G &EnMJ?z:u2w>.MG41+4pe}>my|,/L||ن3'xʏwqCؘI1n 1詭V&ZV;rD֮tvݖ < 1']w.i͎=^SNvn{>35e%L|pE^` s*د1Fmm+j+%=zceX_7Bb0oZ6tYccш M2\&C3DŽH-9eܙ=+'/6}UY釖mxF}a/J6g_S)Cؖ}ߴPրd^T)gk@n]?z$Y(4aʆU8j9n̙[9D"5Gz{u k@ԀowşAMU3/Lj(Ō*Õ,vgL#2St;+%t$NmFE/hڭȳ>i.VyZ/< M+l!HA&Lɜ41GJ^) nc01w?l @V)M Z%ǝ]EALd4um`܋~x7ޮ oN8)\`/'m(YI aOz7y9"Fȣsf[=j[#=] !Pz!<{WDˮ-rtV"I1 GO[e2Vڅ#G"E%%d0IL7 &'Ģ' /&LgV'LF57bh?~c&NBzN!sj梌8|0'|VX.2{ڴi,: #vcAq(%G@1Rx +PVQBdj: "Cv/e˖aܸqҭQۤ6Z)^䤖V@8ּ4ߎiB*χA=|}&9 dO=(j 4'\6&J${ΖkQ|:uhЇoW1kkD SA @HOC;Y}}QmFy&>\Y ѣ{mÏ?|!0 Q!_02k"6&c Y_JZn ZVv &סĄ3B eJ FQ|8s(5VZUdkSU[ׁ ArL̐?Ka=3VڀtLQF"cdg>w M[Vz8sN^ [Sٳ#6[{v deY0ep5hbyzDBn3j{Y7;Ĝ3T:vE^|uE8{T<:IM,eؾ{/ҽg{f{CFgoq wusX5doũ?GMĈ#B J܁ R+2Ȟc U~ SCZz/q:;Ľ"N_| u`OWoYcpswt}/DG]B+[α>;F3n* ~ S_ա$oNJPcGyݼ,ݺwZkz+ǵ-i0)gp`mkKs3AlR:Wc06C!|c| *VOUW>0Eu3$ݖ )GmP׀jNVqcawk+NGv{?6nDˀh1QjmoA6)8T]mji)aϨ]ZXO Nwʹ$s; qퟧzMu ]k@ ]c5 -1;  (K|D#)ٻK ^_%_P JuXtv^-)ž]{ FEd [Iet۴l=SyY?}.{n1Aj 6RY]oʮ NAF !mu ?S&DRrp֟ T@alA(` \| Ѝg.i6z)|M?(sbdy| dXu(/c 1{r;At:V~{Yi]2tF.zm]8##V@d9@-<?ımW(> }37Ե#ƒ͹vFdV^J,6o}[{sMzd ݩ,z>A֭\l3MMAtE 0u8#/ d> Yz l%XYq,2kA7&5)6}zug WM/i؂LuM iq> ?^ S{f[НCzݵ@{ֱ+`YŅxwdzh}\ɑb^*/%I8do1S:zRZ,)R2FnV7t|Ղn1_/A,zd,,cl<')O7r`C!!lL6蓏;U{̙2'M:g<m|iV䣥ucPrf h5.Cz~3rO`ġZLfM@VP0m4U=>y I hZ\]̱aU2p0@R9b)~(,bnZ/xztEzy^{p/X{baB ``lK]M16d SM)G37 `UXUz|4<˸GJ~m_~I%-8q3s绪 |Q `JMN`\؍dk%#?I9LKCjs(#dJqNq l~ ,C1Ve²۝Ԣ,k*`rWuUr \m&l"ۘ31<5wK_hYY٩zFg\+Max dKy(egw?@01V9s!=~XHS'F yYyIcb#b#bPע#[\c'_v-rz|[8VtO`}ؼ͍V;Uۮl)rӝ>4+L*Ni%p8͝Ŝ?;bӯHG6> cǏGUJFV uo692OP9SA.hQߘX=_yWejֲz;u k࿭z.*yEG{[8#nۃ@`9 H,z-4PQ達s+PQ(J5/$3r_Tprr1@XO9<{1ߘ+~D2<'fΗUAՙhE9R >ZmPk@ NK5pG5 FJvzu k@]ӧOLeļr̛?r0h-O k(%ɗz`7G4?c!={`-kG؉7~ez:Z3GmPSW.]pB 8Bܚhܴ*7LX,v| _G-`:<n1f2>Yۻٳj@^@={̝;3wCowy| AxwΊƩrډ{30m9V'P 15I.o:b0e{QpϾu"ck`E7HZ#*045ڻ8^Ȍ;Gʒr `.;cU#J)pґ!;}e(wUŸJOLf!I7˗*;hkNExLoܴi;V6:O'*xrϑu+11 I֯3D!KA ԛ` kH"3--~(Эf boQȀ#zwQOE^I!b٭ Ч8%S&,l/8(SfCJq8٘ҋT%J3=utl2~Zwܾx (,`N& uQF: pl&L3!):ꔨ An00` ƿxI{ gFsI0 ES7``KJbz__>WqsC/eE`O| *+OGY'~%;Mq+.qOܼM{o`Ii߭Rhm.AܸT>DL [2Q] f`XQU^` ,C}>ؘVO/HtݼNnnh66J}AZJU_X߯T+u_ܶ8B1A~}k?6mľxcV3%w(P4>VVг2]bW `ڊ[=P^!`U.kD{Ea=m"=8_0N !+׮HnkF^>dzzqs`^^ &AkO?r\!\ kQ|vy-GO૗_Ƌ[QA~ИtlF|q,{E:O:(**CVL<&[ e+p1l8,Ȣ՟2^ `kPxj,8N4MiR&[6Y376VVFYR зwg yZ,_0|aB AYV7$E;*Ny޼Hv%0=;Sv͈6Aksu S6eOYA9AE--L!sU2x8!ɒs6x;c<'bEdŎDɷVZ9 6zwr)Z(t43 oL9j:kP9+@!¢GG2F5v($hkcס-m- t89X” cgŽ&,Udn^ڼD)מ@ItWFFy:b)~+}R#=% ԅϽd\sKo"p҇IE3y`7K̴D`1c~>]XMUŘ_-֤j8rgB f"դDm{{Rցg&E SJLw̛!UK_WX;29ypp af5BqK+Q^Y!L3 1(jOMVÕLGo#lH3%@Uzc@*'*m*Gba⇊qcG*֫Z*vU{[@YO0Mo~}cP7b*P_l&攱j+w=)h 0 ^Xl'jwȕ}hjlyhFzv`O9F߽j硢Zjhi91p #q!-f{G4 >gOI071DCp ^yZ[6`i 9 ښ[ΚjXlNRjН,LcqqkA.K_c'O"I^H]nfP'k&(*:2 Úps#^j& aƣ+ F5"V Ȥ{ٗBP?,J |G9Kq[\`*/X۶F^,>ͺUQ0qc>do0Yx@$Vь9d ?3)' Zkӑ!8}*%P׀bbG=6Ràgb"iFc$2=qPDfh L; xr vT+Nfo#'uч/}aFl+M%s35+'[ȼ|*,!__?u0.A[$롧F38'Ҁ_Q.kV,STQZ!@XHHsJJKeAojr=Nݹ{L>IN?fW54t@L8R2ߗm"ӎk7Y8cdm~XwCFk)%ޭ-B BNHA=M@Y^_\׊nc=LɔSQۻ•}2*}d:ww)>^6+▾&䐁MxA+ ɍ@(R̢MN{b4,B1n4C+c/E7g*ЪcEiK*}-*mMچrk#dh{bWv엒hZ:{)v-^M;j&\ 6v҃}V~PJys$rq)OE?_O<̻￷{N6)b(6ֿ ^wnI7W{.6,u+-k`b)"sc޼κ;d&͋7WWYT jƮ(—FUn\ ׶5Zj{s#4w1M[_ EJ8.^'pSꫯ A܉cصvlIdW fd63h*@ٳIրĴJ^Ԛw~!6dw kjPnTV,=Ĭ`;gO챟@%vgyyZ[>~ڛR׊urv !s߽h#X/qrD|DlltWuBıӵQPJD]Q!Æ#|X ǎGU%-)9"Cċn](I:r} rœLB>},oQc 'u6n};0v̄s{.LJ;x'(H1 Ĉpad/'st;Fdg”)S挣n!מdK#v-(Bo 3aTLZ[)6'/ z\D[[@(cgebǫPL?^wR0Rz&煍}2߳XX!e\%]MT {yց̎lؾ\br6X87D}Kbi㣰7%ir$;Mi/ѷm3)/DZb^ggamqVۤ'sse@(nٲmDҀƊ7|'y V(k,;).?s8%k1uN+c+Y9Tw\2cS5veu"QF! wm7 /&Lή>uEeXS4>e:O>X˃ KyصfϞ 6=KItO'^1E[cDЅɳnBN^K J24,mpD3J ύ u#V3`2Ce~yV؊.X1P77߼>CQG =5VQ;Ȭ~vWLy7|#x~i)~=Жu `% h,@?oJ+… Y8w&l߮4յQܞN=6A UQ]nعxR MMLY_ =&Ei.–竢(;b&{T. )-Wucg[3G>}Q5ńt`4!5  %!$i:@ iZ u'S>퓏E n%+֗+7J 5i GKg7eɔLTq(?^[iGb?_B@PJS1,_Wd}% 3xonNL_#?+5yGTJmAgmLC YfboY*TyVA3At%RV6U͚Mo/H+-?plxdxD z[Jrqqx)iToe Xp {`b5Ҵ ~GGM㚋X EǞvOSqkw3TEMchX4gD<|HmîCG1yQQU@o):AV3~4ңv}|֏OJAiE%}]I7Jgz1^QЪ#a?XMjc*rt~+ƛb%P7-zbh>yc/͉/ߺXQIPcA t<j7jjU Z࿪to6ܶN{Nne},f>Yc%6L -Ю·)%aGUN'&,ss)9{kkf懏7?Oѓu©3py2[s5L5l߾zԉ? c[lXaڴو`S`GLMe]dV#Osh10Cie ):`#79l,XOC=23sq Zuz1{)v"r6^`{ⰰ 8hFyfMvrԱ1O ud45Z1YZ$P"UH;:c!h @ViKLg^6): +6`FhjiEss3lXȥoe44gOBYI.dˉׯ|:Ȍ Q_ f`0enVs`OvW9^R.K=Xr217^㉃x H]:8A0Вi5\1@I(<{.bVA!%>;Kdy8 F֍8}^He5lǩ|i<<*I,YΠs9Aŀ\*"-VT1(D n>peYي߈rš,g6640(B޺Re`OEwqrÁ&szZ۾gE8VL QQbRq%5Rk@ZV.""SGd={Kֈ؉LEk[`nզ$p.l\8vF!B&C_HKMFq7JLú "=a2A`E>?NTv#d>d;#Јh~Q6-8gb&ghlTQG~ݵ dN!8t1TQv▛f!7' H#PQ(&gMYjqn==)lZMu!Fo@0Q/%oZY~/UYx{ _bF]j䔳JvVٵ{'vفIT|1CҐQjjJ3!N=mU2z{(㜅t73bzQW3Qm_3߱zq^WWf8XV~LMXb1%kT`U(n׍7$ve&6̈]s7|.}i" Tj+QF _hswdƌf+ӓ?v> C*N1a QhW WmxML1חV1=lF1Ғ$ţc QFL$%,&K<CzXxS7J 1`۞h!v3f~Vt}>(o}g*1/HU!%=Yblٶ&:a$B5~U3A&2+w[U|I 9%'1e@]K!*`ֱ͍fqe+W--MX챎+d~E Xܻ=LJd6ʥRsQYlhD 40SF"CW%OLAĸ9x%Y C$~m-/u*^2J+*JQi{-wC˒@> JaLԀ:Y#)~=0ºMX.77Qf^ŖGՁK~׫."fHD}Q3  4RNiLViLD"r[,S=1gzT-g dZZD!0 >vJPnxȴ]Q\Pyq ~e7|@O҄ A-udQv"lAZ٧XZ̘4_ uxWXe쬍0CGK-AAƤq)ysgbŰ$p6mrjt?5#oB = \V1 f˖eOYA]J~F2QiDFf*Zq>^~TPZpZLMQ()Yn Dܾd3ReUhee0"f&pds;2,!'EAtZXZB FkU%+De3訝} L ĄĐ;_@l5K|h'[76z3x K/^MD%cmA!䰽#Jx-00'9F'\ `ɤ'%ÚP#V7'Mj r+![`Ǽ H?ɶ`;Mǥz1nÂ^cS[[g?|dK=ww4Ì@:sQIaqXP:wӣ>8NU9e} ygyVx0_o;2͝v|lstʿQzAԖ!$jϖQWWS1M~ Qoh?e#xmXTsi EQd>ⷊҮӗyZq*!$@M_EvrY`cX F=_7r;c0vΒFZ|󰞕uSbko_d&ӏv3}oiik+39kPc\MeHǝ6ƍ /BA~xڴ)F7ANfGI,ʰs0#K>cTtǷΎXt(9Ozu0`a)-=sCqӘ\Azd.`Bvv#ٰLWjJQ{o~aۻ%yzxMM%3;c?`cOb.Y!qc@,2Wb)5$E2Aڶ~Xػę0|$]O>d2ō4 Ж wuSfLfG-=ں^d01d}@ljSR&DqǦ IghΠ\\>N98~{Q>4t1,znZ Y+7QjG`z51qH:z =~bd@k' )Yn HJ_OOq}(Ldƹ AaΑ cJJ*މnih p<2j=L?w.3$?|RzMm yP2d-hl۾3OQSg$f/.%RU#`xH?,+&Ys-:ܓOBW gⅹPG2?O_pOvMU[xO>I V$` 3gc(v_gO&GyN0XXqX+Gn;Gy^bYV VUw_ f=ME̗(WX|n=rGΕ XYΖRĭW.`Gل$*~*ovZ쮮YKy/IA@V0/7 F<Ø<$L%ɮz@IDAT >Iiqr{iâfZM*Ȉer1A<,ƚ5*uysge1I" "@Ɩ!DF,UD*HWckJz滲>R}3/ uwKl_=T-?*vƷJ&Ʒj -3+@fq/A(Ϝƿw-ϧXϤੵ=0a[W 331* [@1VO@44x\x>]!GJ@P66v*u& #pOӌFWW?3My0㢇H|.eji)dcueUq j3L۱Ch딓Ujot\%7 qX]{G/Muy62"t ژM+PAsKSwv ܯw0<ܟˡ2EGC!Ɲ~O4{Z@Љ S; H ""oA֪XFf4 1v0{} B(y:%x`f> )rnuԖW.bF̀'3гfQk2p'[oj־> h]GB'ĊWH`JFLJ!v .lsmdb) zTrX[ɩXoK ζ>47աm&8Hm/YA[u6FNyF{ܛ `…pWAێc̰en2+ iA]ͼ)a[sZ 3g30Nf[>^,-lMWHGGAdjS V[39$? 7؎>$`0A0&|,;XJ6 [8,mGȩfRfXIH<*1f;k.KbZ!o&+_y1i߲UejF֤yDRPjj]f ^ uzZƭBJf,CgURLGHd[hNUPJ+-ǐ~~_z~R|PP)T~NVR=ǟgV2n}>]l/TN:DJ@-DPZū03҇*[ d[ Fii1R7+"sG0y%fM]U O]睨#`yr1[-~*<-q'%ULfVh!&hK;)c7 ͝hm[Vgg-²e1q<%0kLMpLbEE #)oҭ {{ @C4z3?'؇.WWh44(%26AYSaSt 2 a2ϾԴlzU3(m(( F`N'P~WN󔗤Hwgx0 6̛w+);lmara"t03*+V&,Ruh:ںٷ(/-Jݶ5U"vlJ$@x g NA?||e#؆-|6.瓓ɓ'L?.䜋?w7\ ZPB]}<,8@! NnQJpU 8u l2rZIK s\}Qֹ-$z`XGmΜ&1 3_5h74~^ޮapK [7@?O  qTb*h_^<߈Όu~"IuzS9 jzlkL胚FIt>bO_Z\zci8x<˖/%;܈ʋd_Nq`}𡥒4ރ5A4kؙr>aM_2O69ŕ8NI`=" = } &Y@YalΘ1j6bA˞`H>el<֖V-n$m:m |<^LhmES> tQN]MuuK>%jIN2~<KsNC`ŠަIs>T1y n_Fb2( :AqXW0aQz51AS<8q Ef|k!!%&נu'j" 0]*~Q3fv./l3jTgC@[G=3C/jo6}687r~uޖwce rsve͋gj'h-=)Pʊ)3}]=,\|'qt,h;SShhYQ>ʮژFW&͘a8|2k!&ܻ0rpԶ.*lT]=/^VQH9and@\k?UU-/ɺLSwV)D0=Km j3%>!QCF(OEHI"Ab$r{w.% 6׾+j0Q3ĶE b b,'Iew` #*DlV&<"xn-YnGXja *7|2_1/2DQܐP_=r~~׽{i 'qgϞD櫘0R`"*~#xp).d$(몃b"Cf"RGTߘˆ#)J Z;"ADw)S&2hM9Ax1`Ƚa`G=ܴ UTE(& Z;pX餾uucbm 9Zow@@֣ ޞ>tv#=qS7 O<%_`Ri蓝9"<?p.t 1VFObseV68@|gJ^YHS/rL$) _`O 7U}/_OXCpĒ )X~،̌;MɔbI= &&d6ADO̧@fj">c|s2ed&L1P*rI)1k`L97/SQUi*6$&3gk#8=iEOD م~]Z(ʼnNނY-Y{A5Y|xl<|w.YH̵I0b`^ U|hÞaFOF dTF$h]?yb U j`XX,U^M!Hik{%ӊytSX+FӉA]^06nC#9 _\ɔh"jzUԵ" M_($>͛b ȢZIa1~9DFF|09 ,f$^Pz\+09't T,|u…3p"7V!dḺ{.X.2Fp1dt5 $Csp/%i$"eIسbiAyQɨO=Vz ..8yٹd6̽u ܁ (8ELVw5i#<-!3wCrFe33D!?lI#4D]3>1fT,YV;Fc=C55xҷq(a[oJ)Yj2d0$g`j/‡|;41,v_q}8$rIGBj:67^Qp|3I8ww.d?ى> &պXB"`i瀠XʚcJًR\Ro(=eTyQuGrD%PږSP褯ΥbȦ}5llO\1:Nԑ[ArM& t5M^$Przw?DlEeS7<۪FeFז½}wMB&/7uQULpԄi h4+~ahF3GD:RKO`q㗑LZ%Ey{q|q7r!ںF7u~~&oH۳ 1eEp"OuAC;A2ṽ7Ϙ"IecŨ*/.7rıhJ_*OQYEQV䚙x xѻBPt@L;beY?{,k{ރ 8}p7N˧uÙL{QF )'.S*(tpH~ ^b>W_z)~uWXŏ=ap mvWW p_ϾZFk*VLg(*"P*YT(G\ky|U Z?Uu7׍/*$(Iԟ;n˫ d_<$ƐHc}8OfWUEUӿ[ @ֿ+Ut4/Q~O[T-z@WK },l1M!~KȄ L믕jzeY0pOۺw*ȡw=-(K< sɯm}D.gN|Q `o@5%%!48+JH.c 8QzT&~@^9dl°t0N\5BldoŬ Tc2~3ƍ?_+f27mk!K`W~Xp"tp}ԧ" 8|&F'Ԇ6F o0V d&Rw}~c+ ψ7 ]g?jp^ 6Ȑu.Ayǝ46)l.8o0~p0s a1cmnJGlhöʗ"Z᳚BoC 1~>OЈކ'QVDv[[*-?SZ% dCBZbBzc TO ?Jjl֗TNn|@ZlOdZqVoZ'‚M[DwB5|#ZDp/ǰ(|sJ7˜ ?7ghv#cO*\؂.?X:GF3<Dd41=[ dePRy^ d,!(p.-_ CV6aɔ7wA#e%84N|s_{dz2hhZSzߍ'}پ\SWKG31 auFx;➕S.fe?>LIL5}(Og3{eL_uL7l}¥]mMhl큞JeYie_5Х|XYO7)9x0 ^Q_F|1ns7ϓF+tyJrnN?Ucyx,(3s780d5z =O; etB\ua#hdUAa,蠇w(Tj4f#}@={_\(ov TSj^R& SB7#bFHO)#ރ{LF܉8P6֕>6fdb,Y~9RAu'_wXk j;QB6ϥ|fΜyEoȼB>Wo֎2!edON^&GP DOVwmց] VxPU Z࿳}=;+vZ)3Xp^(}5ptqk`5Ul-"t(Lm.b3*Xb`؊{ݣ_ XzK_܀ꍪB-YBZUm>yD |)X"*7!q΍ٲjPZ #:2tp]bˌUc`[YY.&'ï ygKcbn!UC+/l??dرkzxwkoVfN$#FxbE/["(oS/1/~H/f՗Â,bJ4j2q. X^0svEUY7z#SXAv)̭,@ZƅL45$0IRƲ7eD %gf1)i="YWŠK=Dc0Q~r*^U;rh .$'#FN!,};gbp4Aх3naz"Ix0G6 zy$֧ؑ%/ǎÅo{s3Q%o0> ٌddqKC7kԹPM@+35 r0z=]Ȫ< ‹ ܍%ClVQ()4d(ldOeXV ?.vރ%[E y+rb=a"KV$ŶDX3. űןDW:=~*%۫$|#wq#d~4[Zxշqp(Wc]C-%7ѧU[Lwr`d'(~!dKѝ,~?m0 }X`1lvP%کsur!h.[ zA[L_ Xv-0DcNĢ%˰pus',Sx套  gn}c,xw.Շ X&zd{a=5-`8JuLv:’J3XGLEzٝoc&pײBМ2n00QU-&r8*g`Ҙɸ뾻I+vy!z!C&Yח}h0v=RFhmxs%I_?q@<ML}S52SOP#1~8P:l߳S?Jpە2jMʠi9E쐒^J/nSgYCQ'}7|I&-d׺ZyԀKV$rqo>&Snl4Ƞ֑ج2&W lNGGc!:m_ FwU\ks@BEZZ@K5j*uJR $$ĉ?gæ -}-7w췻gf]ᄚ>.LFA W3㊆s' GM4OVSV0gr c, M,4* :f'x[+2q[#L%: 6(E|ZޅDZ+5O *Ry- 0rurD}Y! 5)A:ºrvARr֠PHfW* \W< ==]넔%,^t;b<4,,,.JLZ^2θXeUbιE֎023 .d2ѡtPK?]BF)I(\>p3[!(-)AeJQVW?OQ>LE+/Ŷ͟s r~*rBz"Mu0y%=}KACGBX5p7L}Q!?Ĝ93..?fW!'IP&;M$JNU L=?#'1V -k61=Wx&&%jNTYŒ2&Ommc»sPځ8|W_|X*mƁlAA!~m:m6em@6#ReK`ɤJ{F=~/Nk%KlԉꕧDRكLEwX7 ~'Oaa#V'SY,́[D&R"?Ue6Wn-U (ۂ2Z_ }˳\em 3tnf8E?k_ ;:s$^OW6@6۫,>gx!D$&ݠM.!;yd} uo{=BL5=h0<<0$4ٶb#P&]0ȡ0l 256;Pd{Mc^~<KF7?c [Yj*WO>v6'IV3oK\SĘbNH }}㲵" SUz35Z,ߜ1a1ČڊFO@Vyqt^?}("*?ow'Z:T|nwYNN{dRL=zӚFr̛oyJuϹ^A"("'D@H q_o*F$DRݕ6T-QE P,E&鎅p' Ĵ]L濶_y3oXE}*>"w>5~3~'ÝCA =^9; \;wY]":D1464&G;#ҏwJɀlE>|L ܱ p deOA֎ 4bbbFӀrJ;ҋP]nka"Il^%`w("EM[Pz0!goҵ5:XiZyWrҗ1QM--Р4r _LHmhkFJQ5eDQ̚)SL9ke MxeJ[PTBְ4g!Ç*\Ǐ9of[ I s܂PL֭!;=G0({d hW iE#G'wP #+3$g^t)<F Mχ>;_iL_H|*=UM橒RYoSi0n @[2E23Rl4Y[힦Nd`9ּM;}b("L9~{/O& U$Hׯ 2{ x KQJ=aGevay1׭x`ex2r7w:;R674c5GaDzss!m}L'~MCGcgqH39rv=sLHΜ9Gh322$3,(k4Ѐ.%RCLG2e:-))DԕhdSً c~%傸D/xYIelǶD.V덺+H?T(bkkFPǰjd[[%ѣG]$@Ԉ=#.{ vnp2na(M!Fݑd^D/`G'1zow=,p5`b^cFR ^lR2}KD&%p]9riVo®`壏"_nfPH'!&A,K[f;wL0Op'Nu`"1Y':?={.{JJFMdϊ0s=G(&zDbNczeU`a/u@!LM`a\ReܡMTq5|#*Jx?4BP/w%G 'z!EPWUetbCȣu#-#h<\Ou9h_J%ɲGvAAF^gB u@/gwÆZd7³ >~"X˯EUz 0u22Pw:ѣ[.Am}|'p%ZPX%!d! ը$?yK)@F**NOAql 5UJ3R|D7>tBg U̴5s׌]N+u;YI <8J݀._NTVQ1#%EM %5Q׀{{!=xΘQfEKX:ᄁas9y=|O]EM)U[YGxD$&̜Kw7 8HR\W|ff|{vSD@EȢAl*!+%+ZkU($Rs/HRTpt<ܣӟhN:v?  o~.B ݧG/9zf'?(38bԵ kpnKLFgL){}|[- J۽{~)Y]˂2Cvnvur`azzJdՖW[ &% ^Xs*g*/pT0p&8Z_z!j#6&ɩ5v ^zv?{v1(ymMЧD=t sUI Y/FXF&fODRX[bsއQW7{ ӫϐTHjGbdyT4h_q2A2A| d0M8117ЕY-XpBi[,cʒ `P$1+vz7wT:Y*B:w&u'7]s} NRqJΛ} t5`Es]0!Y'K+W4giO#sp ?,z |Ι8r&p~w qQUe'o&KdN4c,tD_+ҵk:k  K۟30ȺuGsN)+[*iXZvQ:XIz}F+!ܹ~?t *h֫x̺gz_zlEDk465W9ߍ$z-hh"bFݕKoDq9Qa^O\ ڦ6̴:YQavJaƌ1ޥP&T2aPG RUdam(1 gJJ߻x1_H)hQ߃gz~EI  x6rY68}>4tM)TfYLYb{àbޕ+Ae|B81$6*TJصk']VEAajjޢr1,zwf9wA\@spa|{>\ֆ۶o/K#F˗baHPO=4RAB5h)5`,HXء _OGVի(FLJ2`s<3 y󟀾>~s$ Ւ~'#>NjУF6O)ޔ6!*;D3!`!ן~B`چ k9Vy]-m]%X%b)իK5 ew!C/(h`v$ّy\h"}&ljv}B~fFLDFXam2۾ΥrE0foU Kyεn5/!\ư]ٱb-T2\)r᧛4jį5ё6nIn[W&XJshVOIϿ!,LIk&rՋN%u&SX=UXد"G_E.CGJeU *+K  W%xVE[X-hrc UbdY/"sײay[,Vʓ;cvPD`Vq`M"(:uA;n qWHX drI41m켌 [hb e@SCN&Z+sL[0b}wWGщ/-1C 4dO47 `EF^F?J8Ir2ՁoMA $CY*0+?eZY,X?덈MKCuqY6 xhhSxEJ !TTҳT)<r u-LpVK%1w{/)P[jM2%VQqՐZ1"0׸,xI2客~~&wY@ƭGwL#Kl&ңME聩M_N[G<+HD/CIσG&%U٭mEd^`\TJ0(37,c-$*1h~Z17$vŸ`];T|?AF%e/_BDt80 ->5lLvc&eE7bC'p^dd3>$CΓw?R淔'OGZJ =>U{zO&(f͛UWl޸As!˫ʼn7@pԾ򍿂ԇP<Уd`wsZORbzc∿){M<3h':~}=r s*0ݷ>42t72}}F@GOm&) T-5uXlik4#Fqט ' `ϟ{_ "A?kd>Fה.!1%1R kdX k|YO5CvZl~ b(>_%2jW^e<<ԪU|z5y)mD Z[Ua-MQńbz?sb2r߹ŵPҶ$kU )̃4TS׈fJd;o:.N Ex"@Ь(+}@9B(k>LdR`g2N1Ϊ$ql[x4vmrL1'Sݷ `)|hVU ]5sC+`NmZheȺIFJ2i-YwSքKN~N;OW_'-ʓO>!XWDE/_祏"QD&*ߛyl[kpjz"龷v{vN: DƮS.2^?//穯X;`=}.~sKё^ss)k4yaY&-xA|kܩï?N}Q;@M  wZ;3`o*XGEF%}|U1ЧJ߆'cb#XN\D{tŸ"(@ֿYj#QT]EgFGI`*HU%Ě@S}p>|*,~%IVuoa⴩ZOnA>N9IB/R>Y0_K]9FJA–-p^ z% ` ;o幢$;'VQaL%Y_|AVy݆y3 "ĕVз:Z8xcaMո,9A5Jg /&:R UJ(!(stF4bZql#G|)ѨOFr4ؑ[Wm dK#RdՓPC9*\݌[>ĹNIvY:>Բ:6mޅ!ACSTzXΉ,+C |p˰N54;SȾ=aP%(VT"]vxK:={>ۺϑA]3| '_yYZLG#{GٙݓPfP2B |47 cSby(\?|K؆]?Yߋ.PVk9̙]!9uGwK?#($~#"Mo]w{tm@+/ YWaՔANQSWC_*t40z(XLPFcq_š`jOg'gzE@V掄|^PϿXǴA `FlܲWiKj#PGޛR٩YL &*#mk9HUR<>x/tn'eNԽ4Z~%+[m-΁QKL{1jD8&6?/WbfKRadXѧ(#\SsAI+ZsK<QRDC G}`iHC2,n;v.̮\AvzR ЌF p7Wط }Ma5r3'}î`4uH =e[Lnem%1֚ыu ;YJLwA#. ןYb8V=RLPQQzL F(:Lk_)S$@S =9T!Jrb rWU)˟XE f3)Z.l`\OE%zy7ʥBZW  SJ ElkN)`<< ] qƍ ˝+"< 2}yjѦx{icJ2~ŹAf6Ȫ=$ʞ}{O5^S7%EӥfJ `nm^w./]W--ɡm@jRrBlAÛ;eU#d7ƣƋukK/ߋX?|-&u[qFj'&7'ރAN ?' Xb\= o|`U㳨"#Pnܻ_|h%3U E;f'';u$K*~=",JJp.XҫPZq`#qOۯOZp`@/<ɢgqJgk+`'W"/U5homLNE>bn=]z7>a#FJv pSi6ЃKH'3Fـl4m-*A̬l݃2&XN֑e-XeVeF^#]>vڷ,fp3A =Dgw.ܶ&/)iK)Q٬ SCzf\J.5arEzGme`ehRA`ˆ Ì0RvH #hN, mG5ٜ4Ad+1JY{Gx)I%J˪ bR fJ^: 5ͼz‚?G+Zz檫Èrr\zE9Y ;h*q~\ٙ^(MKDnz";Ӥ$֑W -w^3k`s?zX94D &+gº`8u2v9iN1?) ( ӭ fx?R2kHF#m {% d[#e`~!G"0fHTVA]S|).5("ΈV>Qs[/ĐmGaɰ0FnId 5gϡ3Ylϟ4P5Q$G?-]#ƑJTSXmvz|)ͫͰn69j(d t7E$eˊΦDo&xmU~s!uŃ˧)aDְ(\@N޻@*M &w-` (Eӏ3 0Lt<||稣? m[bYeU7nw#h@%D6T '#׹2..ז9Es 1iǭ ;a]k$X$) #ggoPZTΦtإ(!߀\9Fq?D-xŚXEIS/WV塇V"!>LЃiyՔ!KXŚLdʁZP_UYNtLIðd7fMkUNe]jm>#gK V^Um(8)9\IhZn&?!WqjJ+Myy-z@_[H1a1\j>Tz¯ia6 1=z|h8O){[*P_2̜>+ôakGZ KߋVJ{w݅,fcYmmU>9~*9hTz1=*싄'%=={Z@r2+Ķ/չPLZ,ѿ?ʦ Su1\UYŰ7r 0VXWoPQA)aLP`*,G&svʝ҂[X{PD@Etڔ1#G]oso[1*ދED|YN+"ode!wXQuEPDEWW@6找d ӤweZm vKb w22(0& 1m i`+Y~u|czbR[&J]iROZ7@l`Ea.K.3PU _v檷6dàDyy讚\U^WqFs7Heꉕdg2^8g/z}$Utq틋0"9BO=rB)?~l27]Ov5!B9D6U.j={ z(M%|UB̤"z6ֳwzN!%cddzXJp&`B 9]HjS69Pb^*՛Vq,CO|dFHJލsEoK@ٛoЁdw/ÀC*):(]J]q: UU"a8v| ]B]h0[ Pcǿ5j 6*KZLXH5{u=ʒ`SIk,xV`۷lDSO4TBR;zBtY4 )|q9TJYY+a'T5’Iɔ(e7;N@XA<(/P\,,o¥ 4,Fd577xUU0yF k1d5X[Gẇ'l љe4{bq/ӏ[r||p 3\f'ghlBOZmX!&fQj$ZWeل' FhGWz榦e%!2H a3Ydۑ%f*A:nѓۗ ܎i^i_Jd=wJ\ "]3K+SޫK/|N:K̵Ux~s>!h`T/3`pnf&̚E+ƥ$>B]lalw %P~\񓍘lQ4#ZoR/mUu?~;o482ĔZcbyܖv乌1R^V!6}7_xK LĘgG>X1nX&Уr@~J]guھN jb.3cmNi[ -pv޻ X_ks_u *LDMc42!>"gךC9 wnP]ho0]{چװX5(I2<6y+U1O^{j 'X}M5AHB DICix;v}.|ҧВƝG.ѧ3PF(j"芳'3ibb/}c@[oիwG(acv iǂԱsa('_"^dDҗ6"=1lpAěـ/:^|f%%6ǠE pn4AQI[bl":&? #BO3θcPL`u%ܔ\G cT\x9Z!>$ewdCUع@ l DI%vi\#X-J\0kdFp|SraVU28#E )[ٹXa=s֓lhAۡS8JEM-u%KQGg?Y]7{@۬4Mc8O/GGEJæ 0ҝqcǾh,L l%Es}{7V݇#&AiY)a$8L!p. g~Kp${|#'3{aÆWPT #c޽zEEE%R:Ԗ@uUpv--W" MEZF. άJS-|GU&oHb<4:(yMģ(? Ͼ&gc/eW~]C.[:JC'woEr -Ɨl?~k=,^{a &ςZ7>hldF ںLP0DjV[Y'/t(ʸH|Y?~#CIȾd̥Q3iw6p"OMB 9w֦%;TTҋ,۷3d.ɓ"OBv&x*d^}u ppYθJfU@GU1IhljĹv #Gd06d:ӷԉr|J1зCRJֽlް˰xC|!vT+ɸ$3YEjՇ33d.YPaaHp+Yn 0`'1h!+S2oht;5ˉAID o\HW O$$-ZVo0aNEH|!)ZOo+hXwnӇ8n ߅+;3$m%'wgA = ̦F= $]-'AVSC#) 44X1"X6Fl";үi!!)*(.F e{w^ %s9P2kaNQ۽yDGEeU$/[}SKxƠrԔ'gtk63G !k TAu2 uTkx|UA N2}@=A:,ʐťc1,?~N;\ow=KQTnx /o~; lLgFM *(_oxK~d${ֿc+]fc]u#-y_ P/ѓO2PU[kFVrO;s-">+="A]g3ã'eWTl #؊lmCX1%%`kE2Q՝,_[dN`ֿ }֭p.lU%Kշާ[] mz\W52Dut0m,>Rz u<m{ \fGcjj&4J,&>_.\%erK%me&-Hkk6lD sUNUǙ\$"676g $;Ax^ ?wc'9['1byQv݅L3*[[{~oE^Nb[kʚW`XF.m0􂉚Y(Nv|JܛO0$; a>s08::¬ ZOûLX!$memL_PUs>}&Ttd,4b["YH(qQ?q,dط`:,s':S 2 ~w4ҵ#K̋=j5wgغ"w&`I}Ilwf*"pg#ZEՄ. N_ۺxiWjW -}>յo*/+~;tf$27No!ܾ Pk +[EPDoF^)_JYB84iOv:%>*TufyzFG/jհ+vΞLH[,[y?|=wei2duԱH#V ~zޑ^^ċӰ[%P1M2(\,$Yiq-Ces'' WLɌ\v; w>pz8_q$NЖࡥ+ec0Ūʎos2ļ8 OGeToRZ{'O@)=HGˌW(8~883Q*>BjQrc-}SW-թd1h|`C0[3zw 4Wf5PGRfiJ4y 2LP{mGL %m)! LuiY߅iSfSqtt2ln^1dDŕ¸W"2~=]! =:5f<پEDbr&.DƠGOw*2J^J LM&ڻ7V>ZެCϞtl±S{(/|6?|6ew"4+ ڙO#kw~K Pӆ .`_qmF&fCM( GJהKQCxץJʡQ#,Ʀr2eK*J(gMK-W̤ Sl9{$\VQ;kWeAa'/&L#FÚ?M1yH㨫Ĺ31a??;."Sl35^c/ tG#WuPW'PBM%<@b>3d< $ !!3(,,f"J_ns9XpB6*!@ߴ䭿>SFNĉ`I@5.>خ'bb-FY^x\e{mdl68{&-9kB!b^pwrRiִ⾗ 1u6AL^2rJͲb]9)C!:7E!%M b;Y~E=*9wrm?BJ~oc\Z<*ktLlkGĻJg*Y:Քڕ>%0ה Xx~6dwdU^'yb"P+AEPD_1_9oo62g3؀aTYsq/>C(Y*{ ly`'}WD^VثdiTooM7lj$=N ݧO ®;@ (L9$dy\H)ؠ{ݏ4: X3YDPE*ecm8rzڡ/ gq%C+:.^u5 k?1:^x{Y~&$8p">!˞o[O*ycޒz^H4=*3gfHK,zIyi¾o7QV8JS 0f(PS=()yJ .:гřKwM +MBrrܔM+ BԲv2[TM}"#P34[h<y]6:/si~=K?8m݊lxz߁Qu9)glӧB(%<q;s_CF"XIhR5)5ރ O_0եDh0CPU6S/GiXsG [2ӹ'x匞< r^K^?`7*v.Lt;҅Ǣ̂"=Xd_f⻝c젾$CRG&"Z*ҵ "5:\(6r,jHxٰk&"## 7y$T ؎9[Zy%7}1W^D+lx&d6媇!c ewT``%D6 E%xݷ`j p篾:.7b/a0nX ?ﮃW9٦a|C7#]:nWABŵoPPƅXЛ2}$k ƶs,¬b*{o?ͺ9Q8^Ov 2cFVoאcOy3]eJb)wF 8=]}u&ڃ΀P* ]]̛HE~rg6xN<Nt-#PրZ`l{iDű+"wH ,b(O `@;]wacj 65n" Uq ("""OX(HJg ?+/G ='NV*1PZZZ'gP!& 8-!;sI.S7)'m<ɬvtV/_ZJye2ŧY+ӗLBQ^F:P)7oF6A˛ƙG$Ջ7!)Xo~\ߪ z3YUy40cG M lDM*AP>}& Iw9JO֐͆r=3KEv>\(QGʞmRzZ엷&ف;h ӯTX\`VvxyTG6S"fU0_32Z5@pS/,xv ~UUNd)CI9!# 9.Iɉ#(2ǂ$nU3w4QvѸpt0{^?0v  !R&3}WA':@fnCfNv0ٮBZHߛ.KM P1]#T %5:4c?2DW=vcϠ!3*ꐷua,G-6 [6H8IIl3 \, DyU5J6c 0 뻍@vېDyF~d$# <]EOCso -.D ~/GŜ +צaV,716%ؕBFL4O\J 6d! ̈́#e|_>`Gכdkz=˺9;Hͭ)l +~%zHr'O!.VPRBt>y+\׈ѓ#'!}#qהq2c5Lp" 5sfL#v ,mƺ``*,P:5%'yWT`ZeI]I1}\#h[ RU k{uDO&488_lGOlZXI H!Iqd•ZQZ%o&%kt%ʝ .&Xsvhe0֗,ܠm;w]RrJT4wgC /*ާg`ɉs ?nAVk̍G}>41p@sZn4GÛd< H9(2ƍs\MUeIZǟl'٘ ^;e:ɴHƴI?k6}Ǯ.qs:L>Spk5gs.+ TY#Eȶ1AnI$[v};HE{+[D{c,nqr!{X%.Go@k[;4Sm`OA仐GHWcX<92a-־ T{ WٲS}וO# @O &# <#'"POSʟUud]CWpʋN>4#nY"KJ נ,#Iu@c|9 Yc[ke2E,++~mHA3}z-6 wjXIuKLGZj&)=@ '\-@'٦H hMy=5LժZEXXL_.綎vhI,Jjf\8}abvvcGgx}}Ic;":{JfΥrА5 P#8C7 |Y[=ҫjC#e:7Ç/!l$lI@Q_Mu6(jJF A&3 "H]iӦÀ2з񅽕9}Ip1^@[eBg5Lyy C!~БnǁЦ:2ѵM-4!tE!.< ǐSk_kQE/>%sCm)iEN~VVNq}#HbO؞c$F!":Eٰ6*/GnVjj‹o޻gd:Q֔޾:(m XۑEK7=PQ݀"2}9{HXzNXłȈr(~ dϡ[$<Y ֬xJKNsٌ~C=`AL}]]l߷$ы֊^—x׏k:d_p#x0m{GgL2:7߄1&da2n[Xwo]T&yUP$; uyX%Ubc!VO!ȊgzGdRG8(F FIm3~N-`C&@IDAT֛T~<8և|vϜ6 s*z!Xh{ Ʀ攰+Y ,$r11q(f;wޝɉ.LD&s V`RZK"6(oPBCM%,Ar>|ޫamnF(қԦ6cԑQ?uwu?½ z)L^l0VYLV%4 }VQUvv$WXkX^ u C7u@`}F_Mp klCuB]eipsEي%`fNh2i3s|23AGwȡh۫+JŔBYYYq31w}{ʍl{1QŠDZzӱh_D`_]q?~GZ.wnVL 0zX)OCv?5`pV#ܼbBVZg$t"~X,~OT>-wT;gUvrU K /%}{xBwG?y);?t}%0s_<E@CGWe/AC)'%&N9Xspn~פSyYؽu>%j7ݏҚENș/mE>a3p#eF7)(*QҨFSPD\KeU1noSUB_}2Jzz x>8g)oΎS-^siQ. 6d*h‘jE?ڊ|2\jŽd2nje24q^۰}'8[K6A*gϝCVv%v:u'c4~W+SN,: 0qw YV!{ ЈeQF,,52ظxaPUϞ&KOd )5ǡlMEuxMaef An=u)oyE 7CnP&#~LfVScǾ+ Ά<ԩ(\cX"wU+V\z/=!g+//Q\dŒ3'Rj3PN!W-u [ qCl\"NT" K^+}ď6"1/s{i4K^SsjwwqhGum=vEwxb F"AVU}公?s FDlJ"n0ւ`.=)dCBCC#0t =>\;{'2-d ?EХ?Kuk=}[8lr+qpvc[ Q`I9dZ\K^AuUgP{jo,8]0%=#;//0L\zv ?,Ϲ5doÁ#gm㛱/%Z7khBZ YQ8 dP<蚯WcvQĎi^ > }5\8>!(Ql&TTp7= \:?w`Xu~bYsLPV*@+X\S}_qGJ3v Ͼ ylJJkȆ'sC]q%16UPfn/@ۑC_LH Y|R{pP0*rϬXŲ|YvR} |Q;0$~crMUl !@q5Vζjءx0/2QTTq6)ARr"s[ŔCmnE2-ӓ@ ;^xGj?%R1/dr %"ul7=vSqJi6#*ՃY{9h%~;0ɄUrV| {gcCC]Osji.AcL( 7YgS]=Su>#<E\:Zɞ3f=iؐRoJ--$OYZCj䨋1޼m=}{})*0lT(oA&=+sh a{wl%`G#1pT8L=!KFʅ (cqL8J.>`\MOfVOػJV%@Y!{QvW$j ŬE9s,`۬Hyg}_ ?YJx"$F/mtx6߽OU cIm;ivJ6[ H\>up֬Y$^&6~@,z-x%#õ;f^äS1kh|=tPWWSRºLhAtG"39j #}0fÆ~S mKKoPT~u,? ݣb}^ WeE7܄|7Hy:2 Dy4]*֛k)…&Š IU8}4UWGfM;Juڍ^{ YeM4Nq&M͵2ydMRPP@}ȏl\Uejk3AՊR&4dz )-OoϳOb3eG9edoc"T;,$"'9QHG^~ b(%bm.NgҒc)-}q_Ï=FO7zZe؍0jwsTut)3L2ӏ?7zN$,~)>d9ˋAלR? *̊mo݁:bp_(S.Q JCSj0tΔ V2;'hkL,4SӡD'rf"@(uŎwEMYl)1->ƴG3{MYZSh4K|v`\!q.6dB!CQ3~iiDt$,-+~㕟Wɒ2*TI?D4%[PnTW⢮j70DuNbx 7 "^Gc0z$!4RCU;cTPC$4_"SёZ3LJx.m`Z)mΣo.t,`K9]w3MmO_Rz`K[v/ :NNБc(jxki85M5sp߭5vVM]9 qQ=2Sc W& 81ʐcmu|pd?f(r3p1۴ꪪlڱϡY_[6/.<M ut7^ " 4 [VEQ`H$_bRCG܊ c 7^&cٮz"Y-?Wub6h0mMk~\G|x,zIG.!69(m&zxAAʷ6^YBY\Y`;Р `mZO0sǶxEb_*? /=}d4/'kz)$aAa%lxDzqy?s_z`k O'\]3vU dr1,zC U\eߋ|^ =_<# m,Sc kIAv;dصuycʍUbpO2?X!~dA ]<oCcSQ?j*޾^*tc(ŻD*[D"S4 F#UDmoEeE9ʫo{=p!/.@Q{vMiLC'΄2fM ?H6]3ZjT;`ZJpj\ z?qrȎ*(F]-eo):mƜ/S_Ss S-{L8߁ȴ\ԕJLچf5IEx`J&O/~k md%'STV"3ᨧiI~:\. [Nt_w7":N)9oeF;ѿ a,/\ 4zR>x lE(.d`y /Od`$+4552:x_4"BR[Н+a1h sc=Lw_DCdJi܅/% \#*& jcʬq$/*5C` k HܔB+) 3# u3#T7y+ ZR5tڊ7.†;M.58HI{2|<(kNob9+j"|k7Mԧ2$'^ =n RPCph& FUe~Ӹ CYh!HSᑈLʁ&O +ȧTx\c&uaLl2%0 ME[ʕ^SلҬ68N b]`QVR42@~3H|6+ n[Ο%`R|zZ׷Z;kJҖں|2!S/'eGEOI#Zvs:&mO f[gW5`M GqkaD6(;t5`o"m'#SIͻSo mEƵ \`wX6O-NCULFӡ@ɳ 6ArzLHHKNF}-xY$Ɯ?r&_GYTH8"Кkr]ΞF)Z9nb{V'l~<#\8eC{7('ELپu;j zN5&jxx [ 0vP& YN՚'ѩHcJSTb0Ǹ(4nd-$RN~~=ޭFl}Q{#Ϡz0%aJޢD`iBnL0Cs5BAUmd! uEm训"McKS^|ظc/:0Rf&ذe?2#OζoG|GD% +3s,-wI]yyPۄⲒc2^r)Wݔ%Qd,A`YY3qt{mr4;%o'I8t)F5V_:OV~1س{;`BAbVοg]CؓI$U -[1@R-,] wOã-mx(!wK%_I>PRTX̾nBT]9"|Mwy R%U2=ƛ&M ͭ<# ?&56$ͬ!ƿVz%d棝,°$U+X>bY s.g NϻV$bVU0p8ٰh*᝟1r +pb V"_-65k2n*vM:w|*w"mꔳ`=* -%9E.p5j6~#''J$'("Hhxivih^?{Z$J'&&"2QщdvR֐5&IכCR}TNF :|{-X;c%X?"|)`PX8,3K>lex1$,ɸ+&KPՄ \k 5Tں!]OI!NQBMC?Q疢xxle|W0~J|ryhocWW3ΜW=8s~ziK=SfSuq(E]@/9)Iekq49(Y /_2kh}<*JZ{km}j (f *@uk2 URtZ: 3wo(Qf5;;hm_ [Xݺ& t%UJӃ>$y.(jZ][VtrsȚ."PDC-L3PR$ Ѕ3TTƖGQD'`g|$akmի##F$UXS)mQH11W^(rT&(_\\+W ȷeZ:NUSccEQn:.R2ׂrؠPe#=9 eu)?nM4Kҫ ZU UVE+Ԗ/EPӄn((KgCK F/S7s[JX"ۍٓHۙYqx]"GהIoX_LuHO$V[,X..JAD\XqgNB>JbⰞĕ_VxWPDYW-kaKaN aް";O$2U2SdbCV9c}_?| s3QDcfJ1yhi#ا6;7l3IIJ*ӕ(`"kq.֕f4Rzl_:2OK=.HH~D9c NA9wli=F#DbAv.hM' ͠_xkc5 0{Ђ3iR~GU SQXccc X+e{p`GŋaMXi7P^M?zNvp%6!hI&>(Rl읡HYF&0\J΅6cDus%ݶU|M{`F~5SP}Cf[svcyN1 =A-$ؚnX!>ПI IXiӎzl/T L*u_Vǣ 7A)h%{۔n+=*6Աe'  tw?^s-x͓־>x|&?|C}gFroﰒm 6vD;3U^JUygDJ?9da2d'{m-)E"pjv6mxO[?de `j"#'" YD# <#W"p3 yI0$#i N2=Lnf"=LBYfHFwAV~zN=dyVf_WY9Vya'{&}VwZ xx"YJ;ԙw!%)6 aljwp߬%kFx!=ZNڊlI,.”Ȃ3"+R&˦@D~z^L.쭌`O&Udjjh“@YgȳЊxƏJRRU;;;0dマnA8 2 ^r?V6dbQp|\nXg``7=6+G|xFf\fK%&VE- & 'lni´Ak$>+?vdZghI`oI/QE=u/g`M5eMdl݆RT㳱S#A_}Ĉ̵`hS)/u龲@VaZuMf\!Pd[*HWs#== 4^ؔ?< GĹQS fCEx%JfbԔ x m ]CI6XM20\cx_ޒGFMDstİa4a<<Ȟ/*PX\;[#ɯ#rC fONF9mZ,;Ts % )[ E&&k*f@uk*O=QGf!P}0u*eu.OŒBF\.>YƆʎ ]]dԍD =R(KKiE_&ҴCU\Ԃ1'0SN8`m'<5mHʁD+Owf(mHQCdɄ?Ͼ@6k9RҎK؝u #BF L&ݏ,~#.>>ñg?qNҫ@t~1%Ҟ~b/Rb[ 6*ע3 KCmXo-$//[skːL0? ۄXX@ޫK߅c'ϝ`L7 }Z<7VT|IAq d|8*_t#YڔF?!Y U$V/W {Zw.KJ;tahvaB.T:P\Q,a0P0 $'Í /!+8455̔ /*/oaLF,q2{-A0eEDļnd UN W$E!o/J}fm _sSO0c.G=pU$!5+QϹ1(ޅ 7!^LƙKѸ{ӫE™cDZI CBՔ^(/=fd~ǎ_?H |՗j"ɤwq 9 mLH A; G=.梁^>z֭!]UgлZh? r=ZW/L60‰Pj6ax<J6#P4B U\h'rv]GvE_z)}'/s#0etw-~B].6:9IAȼak"Na__ y~'rw$_,<##xUډVOG_^}i3e:MosWΪ woAslnyWWNU+xl=.zl(H@0M4Q.4x^[_V>TbZf5~v^y]>ʙݴU24 )WZRN6_< NE Je0u.YJ s ;ojc@S)x@`&DX #' "tVGQq1t#Sp#~Sѯ53<̳2d e3> IE1Nurq+&kU+ޒFHT#coTe#\CQ_ЗTG_,C_ACGR#.pKb89:FUQ3nXB@g{`d,K G9Ӕ;!RO#yRP-0 8@v(X]u#*`(7Ԕ.VB'T1Y #>@;tuZmǏWwj, z<Ef 3|UT"k5T({;Sg=,#C&2{q ;?pvAci:^u <xӓՖ-U RZRFq̥SHLI#MV+8|:?AIGK :;X8‹ dV#@uMS>cGo_3 uwm9*PH^+G_+Ȁk6ytG}} A}GP ;#6W?#JeJ TtNJ '=z"x.J"7+X`XJj%C}{|$ᬙ $8 !CG7t2.FR$Pg:]); I[?\>F,DnǩMG)YʔTҏvxO($#0t8"xEw]z_ YE%aR2E` yg`=cʕ|[@ ވMxK7I\9CWr2 5eEϥjCSiń'qZ^- RBٚ dMJ)" 0 FӇZL6%-PYQ&[U=+ҧZLI з CF`-/CΕ 8 Wbv4㷶Co6zQOSH}Vb_VMb2M0ib=S3k G &܎2.Np2פrҠNprA\5U陮(e"CF LzLJJ.@iYx0k7/5;: FjlwU!n @ۋ@oo7|ImTm%.]GpSXU_"%q<awݚoqA̟$^^JO-!6I 1xbg_|SGp*] 5upeUn,˖ _t?#)%q~6fW( d" 凬vt(RfrG^ 2KzM߶(iC{ѽ} i vumDFew  $j2!ݘN:|s2AlXx'HُBOzҷQdS@JthR~v,&Y\B2Z@OԔxsz^,bd5QJv@F|W2襸{d?ssq5|:ee†;91Esk'=#\{;hhP꒠[JJ2A<h>Z-X%eR dPׁ)Y"]"+LpSJ:u3VVSƚLW0Õ QZۆSE;Х SD9zx_%dt$4E_oo<)@`Z޶o؆adc_C ?DN`I\WgQI%GUuCX٠8/ f˳l6zب1SZ:zLưXN5W>vb1Iu4HfՔr#.mu'_bɚj"eE115-~AbJ_}D3q⭷^ƼD#|o(2HQ 5וLΰb.F%mpQa @?WԵk}ömd߇zRJ"th!޼yXzⵗ_BeV(uM0BLX{-_yY:wO =l,٪h1xp oooż8V`ekTVx iL_#ÞCxa|)Jz0[k[~ذJ{4[>}gF 癞 S*~' i$wZy =wqa&+dϋ|Q$(U&9z˷]97Q yG@ߎWi⿃7r\Ԯf/dudԕA *j4FfYRgYJ԰X!08ᇰa&l`H@̒j_#;Ы2|\+时 Yy>H&ӴW3^ 6Uȑ8~0zh2BM9^kۼ /$cmj4&j U*F[ȴTԯYŅ[-jj`K.\}1an:AE_MS4Y?y&}G?@7em pw= {-(JFsY^K6!ɬxZD NC#2t͊O)ElDɢ{,ʖ&PMϢd3Z;VPYSJkt!%Sh"iHLN: F[sGLb2zAK p=yWȦFNV"m!ek[뛑]D+2 D萉\!3 I~8J p22BW2-*m2r SZB]>k %5+OW3сpp-NVХ7O("z ѣo } `={U*eg@@5@/͌a?p1\IcW0.^ >^<*3pC@`BHH _<7fV wz&(6c?.Veȿ1z|d@^d2߻=;漧vf"o@Z|{O<`RJ%(޷kYYR"ͫݩd-B:36LΉK,@.#!]E6u3c=wҋPRB^*%Sɞ/yݯ(L.0g={vÚҿ/pH46u`cPHg6n$ 8+AjSy)+lp#z)2u~7geۿ[(.*`g U gqPumWGVG |,1ТAm2`?Eq+)G_ic03.3Cc/O?B NcPQM҆LDFF^F ]}l\v]2s`1!KQU=۾ AzZ* a,Y~d oloڸIACo*,VYS-A{= +3&cքUku5iήpl,L"}$)s+Ѻ3v`*Ub>@H }[ʗ# <pUi{olX tz a#dS?< ?>ʯBy#Cʢ;TE 5!JYl)o(Z]W4Xw؄];7q7q_?v$iMgf8|0Aذi# 82\1#bٲ=fs'-۶w!~eiz0UĐwGZztȋvXwx/)8%I\˞}qll C[I׷g^/2ľ<}PI/cGUy%ٶ9k6eȶnAK5U8K ɚCUYgMU73)M!奬gc'N`[DGŬ YFz:$`|"]K;}OP3CJ*Xq1zP @0j3ZEfnO.ٰi(iPB*3k ̜2AҢL Sڎ> p I`޵kvއӯ0iu>ZGu1Y{Aֵ 7--TIx| nvؾ(+4Fh(SAm8)2]0 ˦Tυ٣ [VA>٢CO>>PPR ꜩ23k*.i ISR؄0`#x"N2QȂG%mmPSr96{qzVȚVZr2>[Ka(+KltτVߐ/\}#rON[KE[)U%k;u7ҝ # <#l߿aP6r@;6#|_G@Y=X>SLI*՟YD硃~Z :93INg;MCjz￷ `zѻݏWa}D';bO-[SQvsnA$PF~)Uiˎ.C3Datq>=Q>xs7uv@_2v#1 >szOv휘I @FV2LOƛ! ,ʯu6fpϖ/kI:c{aĹ-MkKKtUlC ]X|LJgW7A#F`Рo}2IDV';3=D=6j411j.?#F_~tmYnmy)XDĶ!j ^HDIHfjg~9<4u@YffL!GfC_0;{y;y .?bB:dg!4x8.\8 'WP*0-Z\MCQ5!3uSFZ&xk/#-1y<,aH'1-"7Z-2eRmh3` <ݝPF߫'+RrC{Zq[WJZ0I5-H<< fcAhU[ϿY7{73PzV\&3Kz|8qvn[Ο#I GrJ.\+>@V=2n'aѩ潌KK^AxTD\ CC̜0` }4]@{!jE&G~4[&TY*b,1O Wzjެ}ɊlU1Cs'_X6Is=VL?wȣ Y¦(kEVnuzKN& 7v1ل:x7@db^y-kdz%nZ5=W&,Ɩ͛y2X##)~ .~8M4'(..h-|Y:gdey){mozQ/Ix|!_?l%L&T01%#9.8g+._#f}mU8tP7삻d*gS 1yPH_a/p4Ġ_HKdO&FP:cϕڎTBrǗ,|aE`:jhO̕aCfLe_/cl[ dˆ^[*&ܫ,&AZUG}m}-T?{EG?:g߻}N9${r{r# G#Z\xL'\K9YU:cψrE2UNꑙl9CrbGq55'[OL A1L̍al(E F-3iMJIQ;se1D֕c 4uS7#= 1Rd(ǎ5pdXӫ0+r`)nhQ>ҶDeY1z %?o%3(inq E_ER};S+Hm\̈i_i <>* XbjZғn0:1ϧ370Ȃۧ'@FPTlRr|c Zݰ&.5BQ_Έ')Gf>,3)Y!''XdbIO1=`CE-"#: %J&ڪ4LkEI[|\72A1Pl%U.,UJ9vݤt(!; 7o$pп>{p&Sṵ)FN!pZ!TL`kcm̝7s澃Ƿcw_1^/_.t1_'[k:.]BkJWɫLBS|:ܔLwSsؒdt#[6}G)de wPEH j/oYiCV7jZ$4U8}r\bڒmZgeW`pH0} ua`n޶3(-ĝ+غGQ0id`p>d$vu& ^ ]MQTR/@yUn`ݮb1`̘ R܄#1zǎE@qHw(:|$Zʏ<Vzt_y嶭d4ec+BLȉ%p-ꋶ^KE[BD]C!UzƠg˦ه*_ p.#<ޙ=@ R^sšך-fgy `QGH 7n<9=&b!-!~UR+'ACmkr~{@)#XE KVQ~{r#w@&>Y9IjWz2&7m+D@Y gI9Xx A-%% uϰq `+K$&Rr6iS4  5*vۄ|PZYXu kg1i`;,a~{{X5 gB{WX(.}&-98Jݻ'[IS _C3;$fZO{ַSf;:F.z\i CM us@ˣv ~XT8p%-!(@VQ*J%t`Prw dǘC[ 89Q*U=(ߡr۰n#V}Tpذ?zcND2eJ #@ 'ʡp:u\jiX9oncS7X6cR*5|c}r.\LlX%>'iPQn}W{TXG%M_ f0Vť[PҊ (mjCSu j)CKj*j%c@=\ьՠ!c1$R$Q ?bɫ:u!yD:]*d8>!Phi@Ђ{}&7J`ݕ[pt.&P9'+ҤdV)VQRn`aZ|#Xw ti&։Mgd9AGۍ[R\\;5d-G$U0T  fVǻm!gu҄IҢxd;N 3d־l2zC֎  FRUIuKQW_b°QHLCi-D2AaM\igC64uQZVs 1.LB fcx9SώWxlz_6??q9^^^Od ؁mGO )圚|twCYM)EdEaa1)1AX6-Ǐ N h7y!\Jv 8.ʭ7|S72 jq;d܎]ڷA{;#9|Ɣ00+fYjn1r2{~DD#61=a)}W(IeS^`ڪo:cjxuOg.SYqZH`8.̤ع{&JIȋaCǠoDe7pu۵;>&> ۸rgxZ 9Pg$dR^A,(ϣ3vB{Sc |< K*XI5S豚Cy󻸕QB6f8y*R*gW7wx8MFLJ4Ukq'%acPW oZ=KÇ֎Yq<`Q3U|%e ?P|JSVr+FU#^hQXEX  bb6 gs# G@?][JWsN1cr#O β|r$CC[,7l&f+Z| ֖k*KFdaբT,U#C!:(sŜ EWbpI|z6 r@\ }z[/ѡ];̛?L-[ `.ɉ`kHn-݆F{MyPc8jCJjȬӈؤl݀:wVH] 2ޓ!a+'šmAc[2o_y3QvϿ6V=*k[hjbJ:^3OfmYLxʫԦ-<:yKwDYY)prՌ* X[}WYDotpH,2,uvټ،7efu?%4 i2"E100 Iۅ$ɻRwq~isqEh3jV 50> {Dk6#cvH#IXiiP6,k߇mh&%cxb0:wǐ!c/`m?~*^gCAh>ub0Y.#{{?Cd]'q7PO0 փ lw,~5o#~X) *u7sUv*A"AZMV]eW(zQ ">nH#3ZIM{й9jB?O%,TTexqp\q'۷8MMaFuryt!hN,+?y/<*}c=|R~G%dނͭTT UkGG>}}(g"PISinaE^OΈ޸2|$,܉@DX99XAE[~܎Z#^A'MKv._gΜDk-f~Md@%dӬT(!Go\$u?<^9 :̭rN NLs=Xd|-^񱗨

x%yd>Z8<2=zG.R3Ġw~}T`};wRU0.--- zI0@!?3oc%@u!я.3;d%Ɔ#$/߃ %u{?ڼ}v%e:i8dNlO"#tm/>`'T piw4PhH0Ntx8tYp [-m5ć|EoNYY40@,K4zVVFA^k} >ߺm3HOF &3:Su+CzJq+ IuޗD{++}$Nܹs$Y>ઢ њ,0;UPoa,S#Mzѻ OFRb"\ɚ6&λoKY{lCaA>ZZHe(i!Ȏh6'ӺIfߴgEzPhT !d{z`0A0z^n}qm۶.=v !ܠwGϏ|D@U ooa ${5 AXyL}q?= [6FZf4Y۵iOK3O=8sއIr= mY~~ ABEKKϭ=LPWĒIbnFmYacDָ ͙XP-1<[SHNbRvxi{a A9s5ID ^-Kph(aIy046ٳdۄ=E)mju_`-/G"hi#|z8OۆC[~ Ÿ #<G2nP 䬫FEm&"I)Ϭ 'sm*d5M6i&=/ fDan"OcgMʸUdS;&|VB 0k!8꺼 U"sE''l;,1p LZP*Bg`Cr\ #}q}S/aρ訫"L2 )ELЩAS9=[eZSyjK5LܪeG~/.-{DGVW&Y=|t0ߺ?r;ͭ(Ba:)G@9rEuyZR1UrVAֿF3*/[ F6g_D 5&9H,JruD({5)e5b:Y: 02o.S˿T`\ԸY[-$ϯ9ѴޝdJ(&\j55_ ss32b򛕔3t2:LNITFT"f(Bdߵiqsms:8:Xrdz刦՟x^l5&Q0DRWOfQ5^d2rim]A}DF#Ț@GAu)Քش~BbHdj@Ԙ Ci8>ZZzv&N` @! HD]4>u>p:z8u0>_D&Z=zaμzs`T?[ʣJkzxҗ嗣.!42.E. V5v5މ^d'ܩp6j**OocUAz3n߳(ICm)kdjkHҰu`\5[=]]gD\W޻ xҭ0d<3e2, $vRB==Yɶ=+WD]uh i~*=;Y9L%{T'<"70x$[_ᓏH&Uժ^CXb… p^G02,9={SG/e0z1jgNËȸˠI ݏ1lㄤ0ѷ¦[0p=& &%SjU0om8kL@ڝ#$*ʔgK&)AjZY'КR8K)ho\Dݠ\+{+:y^)?~64ݍ^=Ê%'+M+#ce3>({ {-|{HՄjAm+ FkNH'C/FVtzcK^;= HڰuSxWܯ@W&%j\$F]Fzv% Jˡfz:AVy%H j%3#X \HLW;L+ )1rS6BΜACVӾy֭kWhSg_(Mo$L1jȣ Y]$`/ag7)ܽ{o\t@6%fzჇ0m Y9  1?:*غ};ƏyMVq*d6SG2ia#VQQE =?  FWY2 Opr9ro"ou~%?JRUl#Pޔ?9{F@Yxʭ# GYϝ!ȚKEf/rZF@[[KBSS5',TTBik@_7P|L?3}hKVžu\Е5-?rtp8v1ِMU*-ѯwO` `g6duՓ畸E9wamo+WoP4E%hb"anf K['2|ʵ-GmT$^c{蓉[C*/^GK`pjX`OHܺC'lT1d1ݳ#,o8mt uPYJେT- ̾V4t e?^:oANm2_Ҫ:W_-ǨQc$pu*F6Yg__ ⎝{|..IW Oqr e@M,e8e_Bg:hӾ#%d˱wFy:ғb)3+S^F2FF/Šgsp&cLxaV@%c!ڑ;sluj\|>WNeT1H!%] :fg`ɍ!QMF ՟Xx!jXΤH^dnV)AJtw҂5ܯ6lJYPW_!@9؇$X`ŗש'?ItJ)AOߵ߬#a:XtϦ%@SWSзG_\ԔdH +t[b[ƹdR 31a\ ?d|D ]ӧOKұZNo<٠k%]  Yx#5vk1F` 8l[=C~Lћ}PYo!׉w'߼qQUPq |2dyvE8K/ܻEX姸 E%{絳sk$IC1:ΣW3)%Ю9NF ALMJϓr7ueem$ܾ]Xꯗ0y  i[!k,VQCOZ|֭PP 'N"XjD/tIJ-OFrL@qh#k^_lG:fFՋad KU$%{!r4:vۙEsAjtM[w_z1 OOO^C%ssSJֻd ]w5(Qܿ[|aazyn*L=Sv51IDnm`P1C9R\PI|DAYfdBą H.+ԧ7v $m @QUU &QRn]Ct=WˤŶlCQ`Uԗ?4@׀,*x[т@_mzfy9r+ki"7;E@xo54Xlg^9r Oҟ{XE 5X2*׫Y* 5Օ45O;RZLalkr?Z V*7B+-_P/i04N&D-OibYqZ%'ͱeسwl͌$Prq-Q&--3H#}MVQ~DG2Vu~2&Kà 5*&QĠ]َ +sovOV"TPK~6T co&TU֝ {2BIbGb(7~sh. ς| e@uWIբopaLxޚ WbHڿ8īDM_hS:7;wopCn=N[qz۳t#O&;35⬗Δ lF,4u v;ܦ4aǯkz ZndՖ47ؤcM@BJ smi^HGٽR](M'0GV3΄=*aL|,qS'I0}Œ3Ɍw}yZR=aC`έ;^uX9AKG_bЏQ$"d#LV`m 6 Û2:xzJռ\_"G)uPVlJ.=y#o^/_}W/2Q!f4~ܺyYYKG/Q+"h(܊dë!?BDC)݉IT(.44XrhkI"HNG$ ޱÀ1N%sdݻsڅSK,r-&%hA[C7o#٩ҳLxn۲<\COJ .Th3/HUPFi^Oڷ'kS-؃nxZPޔB=&0i- JBa-现 }H`,x~bʜ8G^}GdRz"qShXoXS;&,~mU{|"e8Sai2B|rTr}i~NAm_dvUJ_E ȈmUq}~ }5V ܸ- k&3d.| dgW^%vy_1H,`񯏝9r~kDn \oL߷0koݏ1ߘBXxE?=2O# G@?Un߼JY惹 ZkVRZh,I+_ ffƿjU0%gᕕ` =G薥AS|t|JvnM_,E%s.0@Xv'#[5gb2"M QD5dš{\!XH+6RI77'o¿U')W 6y\'g'8XA0j΍L#mu5ٹ_F[[s8R% Kdcoi}~.^y/G0Xbp6]rrX$Zd>RJ\m"*+++S1+pl:JzQ =&_u`7'Ι`Sgz [bﶯAb*BלL7z>3PTZ1L,l8&3z5*YE Eoٺu{E5?eEUo#͂O?Vo: Ȇ54wu~N\HK$hdػ6xvK!y>8w }QX~y4&;ɦ3>5_bİ1!?f$Ib7 C{P_;ʈvpqѣGwJ:$ kdnɓNѰ08k-.\JfNٻw'[ Y.p7v<(l|Jn}!N5<-UD^!MÅKqM$g$8w?U/z ?Ыw)M7ܸy. h.,z&C9=7l{NZ!,PYY58w4G\]6`DooMgȾԿJ=0RwNMՁEU_#ƌ]FIWSJNĠ!C0\ D"A:%e&\1O8i 8wĵ,}+1c5Q8|q.AYμo3!{?+3 ]05J^=@M,jz0;|PQ#ہ n|~%w]&wZ[JS*&| 5+mD5QTXao)I^V|=ץ\sTQ26l '{;ĆA V*صL.,+!Ż`Cph=xLF7.kQ5+GG ^[z>8?OMnH ɰkF=VTCOSd& eI0Y9r#?@Urr(!dwo#utiP Op-|R1' ~9!U=9LO*/Y(?Y `6M@}mzrAֿYI9r_%6l2]2kvTq8#|H?mpʪI|LdH݃bV*U}vJQvDPsp؀`Ian U @W-jOҤyi `L-$5::r8B!nP~xWRj!#flk0kv f4}c P@)l~ @|r{~ӣ֯ZƶHJMQWW[T`S $jz59e+m[;(6V#]zCGu[ZX B-+70Xy@,šUk$OۉiNf#77gϝƲ% )[#!j} wb%ZBKKЮ}7i)⧀Oٺm{D?v˝1ؼe5aN C۸컶jjfkOZݹe&L*%FṊq 0/.*5ƦDp}挗x~EДGZ2II/}L{jq]ߜ3q,lW?GlH\^d O)q@5EJ S}J=Xs䕔ҋS fPq =N6{>|~TEWÁMe_~=R62[R6;-5%pUkXh0*knnfP"ɸr5(ALHzHߥew(O] U^c ƕF}5S%䇾#;@IDAT,ѩ'KRV4#Ֆҥ2U}ؽ?y)ЭG~QYT桓+doQKkpʷ@%שTTW2e{K%`ZK UdJ0<ژ`GZ偙ؐ_T4*@YuEV-JpXX=L^Ptv5|w@= /tlUk'_l<+!FРJc1`S3Xo(+Sm?Ըp*vev9v6=uw)SpN&[Ùd>LOp|d* v]Y ++U9{ek<b;]3jXfS˭F0} :62dyTuߢkH ~.NN=t 4|^BzD>~ *U`}D Er# GSgCq\(z]S& S)0|*qa)ܽ/"l(āwŖehBDqlr}v~r%9P>>9r#=m98k5IB\ )p5S6):l R/!\!{їbq?e}70i=9.]1rp〻(¡gh 6hée^*"|? 3 !(.m+Džsp2UT }ew$CFbWN?// pQ.eWHvasyܻi89܏LܯIJe_!2,tdIil` O_"XqQ# ۹#-*\]z耉$b|TSƣ_xƄ<#$f>"3'J߫ ],{ ?dҼbJ> 'PmX>}Ty\-`g*M Vv]}I_4v!GNms{mܺmc{{5TF>t1υ\=G 2IA3=A.dЉp"t#ڊWC: ?CB֌?l$`% s87/7qY LW¾'CHzxy{GG^A%V݀Wm$dÞgL4qNB!qwnu} ":Us\)/_CEKdO?A^׹3~ܸdeKqo~Ԧ⏡my 9ژ(xP\Q.Zp6XU̷ꆼa5{6,y̚{duWBT ^JJOLvlrƀ{6@!bbU( 2G7RZrrP USN+1GէPfxY.0'z٦-hW$3Gd =yBL8tx.Ο7Gv; Mz Y.رuDƯ*F D&۵A~v'JdSS ӛ,%0\LZ1to׆$f7ܹ?ko3[i9%[iSeJy6Z~p"\^Z|,X%> DYD>C**^^&#*ց|nZuo%O݂+<&.P\B ,YD R9I{GUB)'_صf m>yG^q< .mZX5.9r# GoIiW3\Um=in1AZbT U=܄;=6F?ZbiH|c IdC}c]طk|J7^n޺IpVR})J4 ̇mrdz*9rB(@o.azM#~V}?}v!i9֦^OTb,LFjL'-Gw(U5j]埠 .}t 5ȴ#3NJ2%EԮ 2ŗ GQЦvc.G.}-,]IqԦG\Xd$ڡR:֜E9U4KjDߦ猨/ DҎdeio }1T\(`{LҴy+zUS܏>YK??0~|Mh >*SXYD6%;xPٜg<\PMQ_q:{(jJv*Z`Ҍٍ݉t9< s]<`Z BP6&:1ط;T>a߫~5􌊊"+R~px6hPsFνj,b%=Hhܵ&Ĥҙ{Hl`&)/5$ߗa$%+[l޴ƏkB OܜސLxHIE*NHC,#ׂވۋ{;{4g+s!ؽ{ƺ'~-j*ᕹoݵ3Jk`g9JwR)|cbҤR[ h!*9u?Nd` jpX(Sag^|VcCӥQ#a eX-,>](lCƲ/_Nvi-}O!'GNrm~3b1ж[ͷAXc3S3}qQ6 F n$ϏEzv)H]V :x _.R2cL_f&S,b4+oMFްiU/oxM 6 >V\(4pA\ XM!}#π@3ۻ38tx?O{^V+)I^V|. c1=y'{=MfSԩÈ錜<Zelݬ2 9PQӆ =`u=̝RυEGIԻxiL? (Lq* Iy /nZ2\L/CF~ :#7 w+`)pF|{uheUؚ2QȀ1A` \~iэ! ?PGeGxuɵ9t:cOPMUzg }kI~o:Ty_;97޿3`)|Ⱦ.?mU? MzI))hwa]5cfʵ+̘rlL6b-\$v0;,"b)Lh(J)ďT3'{9{=+3'ҩ4k)MxO04n8'W4~L;OH>%u* ѿKr#rGJlw3E~ n|qv;L43R6!**4r`,1lX=y~.ֻmGw`ڳoVb\A_,9r(d]+'Y(e6 찋dd̓ ZYSVlǎrJp*"5-˿]w/ȷb'ʀ^b_\*vA( ~ *;F 0YH&k'䒅p5>)}~Iݷc)A?[9zzɥ(`]x7$7$詳Ԧa'EUSϷ}XҔNhi *֋OsLO+ Ы!O/zNb*l ֊ٰёM7k6oF63ffsZ\v^1( ? vx WKR5D +̃X_Fزi=U;PRB>A%S=edVG.QƑ%{U^Id GbKsP[YDY G64uM. ]~!ظ#z92sq'=VfXf]#̱plӮ}T)'v–/@7Yw9D:z>s׋&(=*Xzyn! ,4O=` ƌ K ko?R9-o;Ey__o^âf{2gPWطq~8WU#N*}?#6 :yza J [5Ve 8Af e#m׎Hs(/oΟ kbf !xntEahlLTgg$vXƥXSJze_>zr>j*001Euh1STAnvFJj ;JD"&4*w;B0|@CK\XZ01#ڐ(_Ic`Ȟ,([cee>]F֠>['NĈ߽'(ڲ_8K9@|oXmԖe z&@O) l#ઘnЏfj&({ ߯1'զ!o}&<%uQ^K==7'{Zgј8~L!hK>e!c&!;)@(i9g ?AFJjKU͸雷c$`-]30e<d<^ev?WG:ޝK+**RczA ;yh>}D{jWO.]9̸/oo~4Y UѸ@+C{َ_JZ*zpW.Ā@c0dﵺKќjrصrң) A~&}͝Oq.ظa3ϡj"?j| l߸/KlO,-q_RV`G@V[<lYOÕ"8󚱖>wC{w#P LPze! Ū?+3}b0eK :ڳ reqM} Z|`6ՉC=|Re45 //pd6ٗIr]{wkß̿d  =Tǎ 庺rJTo&At|cL/j&R &!3w^+( ;n.Xr>zv+1xvlJww\T܄Imo2ܷ)"XqKi+X|5{ ][H2.FP oTPҗ7fP69uQ$cVݻGVp-L5V((,DyI4 ֟:]&I$54uZu*ojq 2ak#XZ|#4Mc x_ALTӅ"±Q_ro l|&;;Iӧ~az[ܼ *+̅ ]XnI%&&Ҵ߉)L0B'b͟'Հ xo11# ]P~<`d ՚縈LOǶm6~\wW&iDof~Ø@;xKFƮ= ª`>w8A:x::"/%c[!U&8<$wxhois#%&՛ cNv:E4:۹uEq}C{/bA{$hbM4$&1&$[,Qk%AA  |ͺHF{ ;;ٙw9?Gq9EtG+E3+/'1t#)>U=!7mކ  ʶȞf%9EH.[+Mt3!CLxo Kl.2EbN)*U$jQ6"GVe_6 d@gJ@ݳٖt+X9Rl܊ݗsS"3 OO{%wnVFbÃ`N௙AO\ܬ ʫDOi6S A X}f͌;ύ,*%Sj,~skHLMr)rik jJd&ęPOooS.5c$xz5]DĤLBJ96_C?kϝnz5=;aJXYd:Kj*V`)bmчɢz@ VsLoՉv؅ I#{Yʹ0F;۸CpSYEdݺ zu"0>q;)&M#<x!AQa Q7YPuUKNDȱ>2dL/]N p{~\c*= XXh= c"R=^qS?e3 (2Ȁ" d?"<5Ox-kOhxyhs'޽wYF󂍋rxӓ@)\ue2k j!.ށr}")Sr FsIc€Vp gD94DbJed$`?1O$J~~>jI~[^:!rId At,",1ބ! ( 580Uy;~  ,"97jC(,̭fd@ƒ>Wp;2oL= N3)s|x,ȤO˶5v"@ֹ G6Oѷ%89-S[A.]p#TK0J4qrR ֖pt/aaht{W:y?A;# {>| oۛRd\y~Rc`h@ߗ|%u5qgGOxO>;FEXrt(}0Ыc{I˥?|O ~5[Գ4D 3A.  1 ?G%=7 | Pуҭ"5"9;ĩShgϞGn]mf-8NŐwǡ(+ hhkp1VCQ~:l{mբHrzl} ~;$̙ K4nOVl0ϓhJb6 (é5 NN><^mz o8uo"c6 B @/fa%hoFHz$~lѼ C/vMiݺ5X.HLr4a[y&ЂS WTV.YNO3Ķ O_S{h%u4av7i:Ӷ f͒qt M( ȿmHE^[]ёi '_F峔lS}`lZ&;%ٛX2o#J X܇U…4]9D~aQ~_$6RI;q Lz.ρ.\(3ڶFs~-4Җ; V(IDԴ0iX((.f ` KFb[O\?Eˋ,yN҄+V!tm\nWE>ev~ QGwaB-'6OQ>]j\޶ Aٰ5W56?澾Hydg 4C_e~^"Rb9p1ie,Aia96ۅ gczb~Ԉ>_\Շ;uui֊~ua΍XPn&칳,Ԥ[>?%6S  []^O*:Ud@E@U-Ԩm']Utq]ZZ6QM zwKLAQ~f6i̽)RhyOv-wӿ3,(Q\s˜+5!/TD( פ4(Po%2 NBQL"تqF_Bl]֖QYadYl=;; 4@v\:y 'TT쏍PQR5 8S⼧iS;`Zq4mJ!v`?}{8#Syz,ԣ?$C},ݠߜ5eR,U:zK+K2|#hJCc&:0AZB9死A28D0Ď2z]5f"J ҪHTQ#= ŏdJuhAz9wXw] +"#jkNjVIYg)CidG p5#`֌ٔt,,") d%% s/j^yoc߼00% (^G(_*#J x[8AM)h2p8s)IUVl;* :rh 2)yZ[XgaնNGېAԶ^,@f ;c{$/צ6<%%={RʡF@5sD́KKR~=IJƩ Ζ>뺦'X pR|dAQr8?Q2'#WnD|<'~GaG{6˯|-Ѧ?+Իxo?[-pEl碣cc4'sNh ;ְsy&k(٪Ukcď`V`OŋSo[)5:بFdQbJX >7 fPӇcάn- GgRZdp{{{{i:qbQ4#aN f.EwTm`yCT,عknĤPֶbSyތF6Al4(g %~G|ZdjJ r*7.\ Ĝ#0gh‚sJ:M}[ӷTU5;c'}Z3d| >}6u@ME2t.X>mڳmk<;ݷŖdYz`6m k?_ ȱd(G)o4Hc}0w)*$߽6*  FF)7oL@ڍ8_PBd7 KU w?JԙI;:<&;n`qVA(햆_+ A#+yѺk-]_=3ظs-TӍ=)2Ȫ2rU ;ymBF ;#bhd ?O!kյ$A7cT9pLGݣ *_)Og'GK]mi24Y/-1315E QC櫭il c,}aW_~1(3&!ƦfUWTP? lӦmv\}ht> mJЪx9*:KKK5qm">0"J.%WH_F4Cl%(uukF  (Vuw&te9 PJ:%%^hcD@R7D*2 Gbb"m}W;2*o— HeUAhh5LNɋdu'1 ;pts UHQSvyFF6Hxiyܼ='V@x<qPtJ]L~N?L //222解] !q+[ I%2i-_ɽr-΃%o }avVt.R͛˻|d%FȖIq|`oXXbҢ*wʂf!44#$7fiݏ@ oHWX1}ǔZ!rsobm|;{|4CUop{tlۙO::u6(ǿʕg|}#Uْ9ؾ{;0o<)֡ "Ḥxs6\IhײVΆ#s $(aFjJHF]=[du0Z zp76Wv lm#ϧWH6,C^!b1VT @MO+M6;;fLVyچl`oWٚF<&"j?d6jmz$ 繜7#xiOGGYJyr; Bcynvh#ֱ`eS?-3'4 D'e6J޸lBzEbz.\ݽϬdwJhgQKy鉨sJzMC];NKoţx9Lf=<yd3"8 bJs:P/;@Ym(y{x@VѦW*V6^^TJPv?]z~X:=v:&n^(ϫ?$4TjM`'֬M5#L^0a'UĂKbHIJnK j G^ zfR湍Osf}Nu-pj}o=Y`#=S4i{؄fd}2 gU'-ـs~W `;viTh%}]rފ"JIb~XTo9Dy]|6{vA]G[QISrWb jٝ,P3 AMn?8R<↦氠oK^<\qJ JofLM[A@~ÆHhs,L&F umc`(%>z:PbIt4rRٕP3nȂ D_BHiY`K`۽f~e"|6lՓ4[a-O6o\lߺrmc-^ޢi&!F,?VzwtvnbnSq.Z穌{&L2f%@aM-cԯo 3* #.G/CvP[)WݺC}(+vępxV=O2,B1|xg)gA֊3 #+2Lj:0b"^|~]._ֿ)Ȁ" JC=]ԗ) 0V,oS1l8Y{%dsrG*,FBbn$E%ťhbhĢwE<{FMVWoS~qһ iF[t]8h^]8_M4L x|֔dOLŇ<)O#=Ue) Mu3` ?-ҏo_R,C*X&V:UxLVVNxVU2؂B&? `]b HTJ-Lz{O6Us~PCRҗ(U5VM*Ξ9W(G#L,$0&X#zl;LlCʜOWX'є H/?7C&Up>(!qf* bqO9ݒRdӘ\fXrO7s7O9C@Uvx*VdaAeU,Zi4xT.d.BUVW1}׹GZDEX'YkCC,a#ύ'B\1hCV ))޵c{J2F9d%Q ;n+U9OOOέXr)GX+ғprZSlxRb<"îS:m4l `w?bMWˠZ{ضs/-&!<hYY@倢ROQJxnuAܹSt%_bQQJJkQM TRB?3YY?m {c0.9I7&M|@lMm۷ eK!܌>u63pțkŁAR_ߌKW.+rVZ*WGVb[oxvcG.\оs׬ɯBN\>}91XXN}++fn/wzV!!k!3 B+2tz;r"ƾ;a˟"!׮b/b ESEx $AL[VC#%{HZ`:!I"#q\}86  :jՅ!ztv}轰b2gY1ʓ4yۇ==._ IaE(2_ʀ+U2 NBQ 2X2J)Z@IDATm};!H{(SЄjϼۅUd O>~-(f3JӊI62Y/\έEDfƢc#(”ΚPKԀ=(IY#}9R=%3xp2'g#! crai[@SR6d&w յt'B0;'pG|qa;>1'E]ۈ0g|dFOZAִZ%&ôWrޫJڞRfW[V=xo$yy5kjW+) JsV9<>~)&),2j'^1a GUھe tth-y@W8v$~^NM@C&pÎ56=zڈzZPբbpj2 +45楑f7"(3Æ|q>YZW4<qދ7Gصpb9s?FIa61qT܈/µ hHa7fg>ۧ+zxK>+U0Xt My.dl^hck_iV-[jjS{'yXNAII1<%TX U$2kk6~Rs1qD+N}7UKbkƘE)%"ܺs7غ7$܊NrKnuyr| zݠM Sc'țWK66rz-+ˑy&oD)kdJzcs>Ve͚4@T\PBlݣV=0¼R\\=j?uqdh^1EC׉j3`ElE %s4)zPd@Z3p/Ã{rA&ނzjmIfy˔p `һ{{lc-yCD]-)߁*d}$~Zx&n^*x )KV/d_qA>uy?[1! Ÿb\&ޮzw7xQ~3oⅾ (@֗n^T~.ێ](Ud@ Iq;:d|P``emhd%O}69ML;߅9Y~oQ gK $[L]ۈv)@̄;[wp>"R Gl0#5w{gac&J d;r ҋ,B^Nn%ߔ@ֲr%.pCڊ wԈ҆7&;+V(I# x`RZx<6$-̙ ;|6 ,A#SSj"1:>_OyFy|8}z UKEћסtq+$H+br{|G:8h4d|WÇ+2xxgL,g`8D! ]p!93u5@xx8%u3"99媪p!\č30| o[8ohu`מXe%XVH{a`ŨO6??Sױsݾ݄z \D_d  H/IkSc>|-(? J>6vq-Zw:Yaaذu% Ơކ?==ݐ3dv.f8G.وu$yW ǽ sk ʔ:lۦҚ4o9lzI[ﺹ`=gn<#./vȺﯭbJO 8g!MA[K^ı ߏ5s@HxPTO۞~RS_fgf<[7Gk>T9fUuS.\L+ָXлs[srw^Okk+Kқ؈^DݡD;)W)uWWVR#E9]@hd2蟬/F+" V9yEرs?膾=K,ߓ0OgrUjrw_SBeKǵ| iW"}|3몕Yեshp3[*Ţ>C6# _Kɮg_+^˫ S3jW/@=Pƺon~pڭB_U,EgD\PV86qlNsNn>R9lSFLz{.7$VZiZ" } :=VÁYN Jp.,(kV΀R!Qң`ID=S>Tѣg/z¹O)mZCr |޻U)vªcw 쪺*Щ[0$ӧ'Ss+\xk.W?CUY'n2Ps.ҪۈMǦͫG6Nͪ<BFT]x*1R]ǍK E+o_!no6@y}h Ok}FLʻ~)gNC'J/h>~*""L&X>2r<A}Xa@7,#JW/"lTetڸ!Yy%P_`_lGU ĸb|>Si, :|yqaucS lY瓧ҥk^WӽJ Ռnqa-壄?=AOkiݻο9 MZ(u= ~ )R[?/٧ı ^$@M;V>!!^#PX=voYȗ^^O_`mcMպɈ=w^'=O=7K9ovzR޽WVGϞcm ּǥsPLs8[=cJ07nkx2^D 7ѡCGAUC66Tk0@DmC禒wA%`܈25Q__Gj:eBQÂoz?+d43w.Ο[K@262 :i]-#-37sQrx*mOz6D39|{]4'L1!VLO'W4/S(͇>WR ӠLA, 98f V/[cG!*9Eז<_nq.6֟~j~@8A6у;*)fxu"PdeE]fwCTt,REeN{nܸ*xR۹/TΒ= ?'|D`if[Ч);ϳM~%1{LuH֯5YԐ]C:^YMHbݶMņ]K1"+s vk1֊ (_\n=+2xl9K|  t1+ECX\RZ=d ccȧd(oI=gkB|h L uդoely2zPW^:+5]pL4IOµ,Ӄ[]$G?ЌLJEN!Q01ŝB2HcaRYlgi+.WYэxҕp%o{ 4+dV!i+b -zԙ~Y3=^LtLuPfy=p.+ѹ{*iQm k<լES๼SkzrzXW7ϣ,[04榦V0aNt_|ɥn΄񰤏02`SyhBᾃT 8o7Zn_eURJ*TcKpisR7f"=XIBxdڷ UOšM\ftN^wBBL5Tpr6%%uWS5!N )yiE6%lD2zvP=F^gzQ:!qgEuXa^.Iu/]CslGж]7Jv@VUd O~p 㽘RyEmU=>?"Z1QgDI s{+cA8!o+^Zл&l,Cc dx؍\zI32eߧ%ʝ&ZVfEIz Kus޽ bcЉRBڬ+i 7`:d$}]xaÆMKOV,;{Ȍ{<V. 5^=.OSwFK_b'AmNѱ oE5kdNw*̎.NNCztM̽>S:whH{u'$ ٟ^,*d:{#1wLQ} d[ܑXfU".&M}.[SNIÖ_g}vF,\d 5-5}("E^عK‹"5Ǥ/fy (͚{}wn)3aq)#,Vn",(/&Vi Ide9wܪEKmd YsOPMThdKc*ozCO[R,_5O3OJ[s-M5>^vն?.-XE^VTl"Pdg@+/8)0uHOJ{nض*/VdCGjNlx2 $}!Y`7ȪřEgXѿ" (2Sg`cMj`"GZC`}a'Ou ߞ5',a C](1r~ :2d قfm*}IMz\ 8>-]潨 mZ@=Zߒ@IS~Jx9Ծǹ-_;ai=1yg>].^#;]׊{~u J H?7JPފq OBf^JM7A4v>I9ڄ9tIVboo< _TUgcPhҾ=ths_x=#SQ&}s$i^T2vyK %, ȦsU$ƦGR2RN "FhEpuJԛV)G z6%8[KF_#-%V+(ALn+>dGҿ(z|~g1jdrMxIu $ܽ{ښ-d oao֞sxc@ Əz{7#ZVd⸱R? }tdD$Giw FX@r'f-:BRc6bx.gz U%?`\be̞1 Kyc߳31ZVcu'NC.Suc_KΟPWBn c_GIr 6lG2%ջuB "x/#&LPY-]0lp07l)Ю};DE";\Vߍ4<]ܦ *Pˑ1ش~->5>ѯWZ;XMܥ81e%;wz<۱Vb4sj]mAGE̚a\- - w#:͕' !Q. @zLi GG??.")5vB8{ W*7߾q|WU水4O /W37ƏDln4XPF]3zyfEX> UBO@Sq!k՚7ƍIv0ZxQ;τKMؙhTb^ݺ.GφE5>W"pz>Ag,PSQqIog԰: z*̀T(ج{*U{GGEgt v1R9ٛh#.5k뙥0ђU1X' JH.Ο=kVN:)W~m,-@V'#A<ݒR<>HHLFaQ>s蝶FZ߄5?~ ܡc-P'72 }Ⱦ5ʳ:[`KE KOMy}`Щ.-欈}Ԝ!tf.5 d ZkBFL=>zZtGiJ}_ũS%aia:`u]<~Tj̳U֔:%%}]Ȏ@RRzUڋ'i&I3el RUb!2AZįa'gI)k>H.xàG0d+K{pPaưD' 0;VZFqedϙCބ|oZѪ}@OKCVT&Mf5.J9hB&]ރ=1&MCSSΰ z*ZLbefa#pvrDLtT 5'ޝ!PI01a5#bСaMgǯZ*_DIʾ@u>*||Cyy,dGZfdbJin G¬JVSgIEG|vQAN)zrԼ8A#sO0sظn#ychب!<=ԏ*!!bNEn:{/؟#Gܺk7h$ٵv:vV 眄DRWnM]>q-_ \<(k}lO&v`3ޔZ :wc#)Blwi 򳡭GRS|XO8 3?NF3dT(Cc ؟Y"aOɜk՝.8ga}:r^d^L6ߵr~lR'FFx-b-nh`Pj瞃ص BPAN|:u  ZCR]gAKF8y$E66Vi]5cs1S3pf-ĨA=%뤑b'ÒI 33.VˑE7> !/&GNOSnT?ƅd$L吚9a.ʝoݽ(_9 йcʋOF0]=],W؊Pd@hdd7nͩ{wT7i8?FE0x]X[6>&4rCmM2sZ/uj)+|,oP]-d`)_ =bׅ^Wxu (@;8nEPd΀9_lQZ+*^k)@IYΤ+j,>k-] p;.wC#t >[`| ; tnEWu}#Mܚ 9G$E^0ԟ=%MΞ&[c"{y*EB\!xJm/̆F9\y uCKJ|$ygCAԿ!:E=} -d^hpF o1gW̅R<f-;Cn^r9K 7}o-b{Nz,((x~B-d7_1Y_}uʪxs[3U¦g )>k 7եM֯__ZYINp-!@X/8kGATU8KҀwqOe[ZYƵغU*bH!XDe@2;ӧNb*{1v:ujMVzQEѪ]'I9N_Vӿ J)W +m9r } 4ؾm *oyjoת%x{yc4v3wG^=q;;Vشy;f},_n ;,ưk㧐&HMĎnSz5µ>{ۊcɓ[ptypf(c:8APKG) {=ލFMO1)XbL4{]AzW HR{G 7owggfg۷owΜs 0uTܼxwF 1c4^xiL]&=NM[/ 1Ћb11'jkRH>ӤXlhAO`vt\œE||غ/=qB@~y+͟ ҒV+ױwٙ|yh~ܻw$0KtS6Ab}IH60o0tUp-\4ѷ@8;ׂk[8H Z7[/Dӱ;pkZ_ [2ua8W.]q8{|d%cT2$e5ըvFB_.k 0sJ$8/k|Ѣ8ƾ Ǥ}VodM'VoXMr;>˜QA_=mOǸL'kjl;>[Zp \$KP5n)S jKWh* ޽m";t!tMLjL_1~͈D # gⰍ?vnXn?oܐ`ŏCrf.y?S_&\> wW_MWO|=o">xLqž+]p߰dEtdKq+_9X]Ū*3@uVD;)5P5J\7z3 p5~HY# e)竏IQáA2ܒ$PK,tvhlanXh ל絆8U{#6E>Uua B߻ٳ!(2 {cjPh"<}F e^nn{JaWY;={1zg6oxpx8I 'G$b 1vT~4`s ZSQjffI7wYo&kvz2?jێcީ/ dgYqQ8|20b)yaY$:2=C'kv(|8:xd&ATʋ EcݳHO[O`tw2'U+Hf |ڤ Űֿ:W/`?9IPM61kMe!¤.?:D2 `0b:X&ٱcd⃁X7/B{e-k"  un">$,Ɛ|T7#{}3AU3jg.yV8z#hJl^vۼr96)枥e~W^f&څQcIU < wL9t$&d /|i#~8{ j5?bub1S$ I1g3]؉#O"I2JZZZ\[#sXpKzU!98퓈vj-&n$H=?3I)S&ת&\صߗn#Ic%; `eՒ7hf܀UFCixO_ Y}*&nuXRm7oن6lى.e*_t*hW=f%8@5$%R\cBDEqd\8֥;jc+s$T`b̈_لWd_<%AR+d0_7UsL$NJI!߲ У tR{[g#eZ"|FxZtsuwg1f(bxϞ;6m݀żSPH&X$zڔa;j9cDA"VEs%yC+nY|,Ԭ$ك#ժA0P$b$e%Mݨ1| |xxg}&unhPv@o= -ߧV֦(VsD=NҊDOIMÕ{h׿ G^=(v\}W,6];SQDҲK^FWEary${x.4';5Uh`MIzgO<Т<23R J*L5^Q V=bZPj{)bEģXxE@=f峍'\ `|ke&Gmac br;|D$@YV2"Ȉ|殭z=< X ڴ7!{_qw.&fWE1mow"eWl;qIe.] u8 #YѬ5y }6Ҫr9Fѭ&MY]<+6mIJN}j?Fl]q|n'ٙHgZЋ$]L'c+%r:2(M.1\Hբ|2"mHB./XvX1Hy ضe]Inf*;c>rqAG[^K)kH cEoJr>#B5*dosÆbJۏgAOF=|$uMkŢKx4qH+M1q=O1DFMCm5\Ŭ|xM'1+ee/{_xNtre>Cq2r/ܵЅ)0 ,Ue.BCj*CʕXl[5mH.@Bst'<3g"0M.?r"\p͉_:uLL=kS-<&?ᶋ+zUhn.ӹ&,& !LcT=\> K^l3ywlܼ)<Ι #8/F; ;3t`u耿;̣' miZ<U_ Vؽ4˽ hgLbamsTdxT.G2B 3cf4MQp={d׿xK-[.÷=Vj 5TdYLAd|rKq8l=n; bGMLH26:+nMb^h)P-"IY eD1^5*1)uU"@qi$>i{tu0_ڐ@.KV_TCzmXӰfUU#ZHտnAN$@' eAL‚"wo_Z;#;~ ́>MksZb@,M:b4Sq9H槡,FB+t|2AJJ<^b?`F,X$gV^*? }''9ioĐw de֬k+# 9h i!~;w׾=;C௲7u~alP<@C{@xkCo-<-ZZBFF\'6lِ=kBrGTz2m@IDATefggDXD@'A?`kU0WiJߕ.l«0\3<GCC#WR YQ0sD$ِmmȷVw)a5NMCSyL|vGBrާIGѱ+lK*H(@FVQT@ޕPlJ^$2)L"|FCNN1fFEb_f2()u]af1X^$[KYnTX,[iTRM F r󐣥WH'̴%I @Fca2!)\aҬL=䧝9 }H浬([y4L]637 nx*}rE\\ 5*#C"Ze!E~UԆ޾BS$Sc:?)haGҙMc OПXLzW=~/Е$9~IU҅Xc 0qRmy_},F#@0!C[GO vFӓ␑ \Qd5"eԨХ:yxr0n<nOt7Et}YXrRPTADD$ b$QAk]U?FgpujA2MH>۶ Jt/Tiv.Z]ꅳغu bpq um_PڐTYtL,{4Qj7}t$bАh$ SNd?$} tp(f~~~$i[3S3s ? ʒ4Y$gmL2 :`׮УX)(L CRAKwcװUp2 QhMbrJCUh˶3f*sRB>k[2? 滵xt YYg֐A8ޖk!sĞ}WyGRܽ '`~4ɖmweOS'vٕwQ,t0)'& _z-|GSm ORml&MTT™s7ؽ1f(sMo/`].-W0ֳv")10/TqŊZiG-2v G`YMGN$H<Ťvq\ r҈K,.62SsiQsGvH:\hfZû!yhkl2-Xh@]U$/&b/^8 ZRGx#KģR `~Q_}%\+pyݻv_.o?Ac]_Լ)yn ;¼~!lI:W:PezVt9W. ts4%x(А@QUv7;g\Je78ȸJ/pyqS@JO5o{}U2KB$%k7RZf *xk`Y}c==3Y?o?$oiBju2==䑚/ mG@r BKKjjJ VMu5hPm#9Vs,kq_Jľ郻pr $`dQ DƼ"dL3/bU; Fj3t<)ARIlvЬ2dk7CňXǘExڵӅN;Ct0  Ğ[ѥ[OY t }oYT1UXc Z+ݮR9H"K$ V)H򹪪ugkoXQ[Y>@=wIm0kbEVO+|&('^xnVź#$M2ݻw %AXު+B,kuarM:vvv|mmA$Uֿty1W_?> )`]D<1cQ,?¾Q2OhFAv[Ќn{TFVFwQobeC]Q\V--:&DE)^uƒBVΞĤW8o'? ö/ ?@IA>14;™'&bc⡤ ދqL""CS/W@[^9W ӵcŝN40CGb܅0'F1eߥw~sm_a캊cVّZ:ujyk[{;q1 I2ԙT-[)b|aƦj7X_6.ӢUYF$g(:g YDWWBtj߄'b@0 R֪#d,R2I(q~xw0[pR-ZC;x= WKySzk8c)-- ĤRP,>r*:r +W?t}BpU H VV>62H.Q2d,䒼c`l-i0]Y GHbաk7̘7h8@Wh& /y4%D 0RɐTS%#1sɴ(D2T=ĵ34u#5.[CN=JL-YCcHP엊1/85/&QgXz[Y⏪}0ؓf4U ѓaVa$<#s^g-(;[ն߂@%F#&X+el2f41 4 2ߔ˹lCI}199`+ܺnVO3{gAAYm H*HS9MYZ‹&7aʤ9HH|B<3Wߍ442I]&.ֺ: ` F]!-ՔYY>XVՉZ6YGvgD=@4Bѭ,̄*4lG'vL6bw7͡*?a*K4.#M3x={Wy>D0p"#8(HA1rKW:= v@X+*OC:&Q زw94mL*.HƗ)[LEכ&l[ݡJ8U:m'UM6"ms6X>ox)I tap!.2ն[-:*;۷W} k+bцa}.¨C:$;G}~~ ];VVnر$ۙ1Oщb%W4ǩ\cRirƄ7ؐ);*NzSP!VȉsSQ`e4 5ߣHDs\{Nz6$` )oi0kCkn}aE%9?K(.BqQߧdU`dj bƷ|_')JNN*֊Uu.BEl!4IEp<jUab{ENeFC`2Fu$ڵ&h EroVVW~Olm$"J`NLU3Cǽ[nPGXD$N:4acS@8_35@u0w1~EU(dj. g7t^ڕhҘcgRRD(: U\HKzY%u>Omu*aZU3*EjqVv`?Q|ӹcg$#7/ڡ:u۞DZӧJSH*m_{ٮ&ν>m`Pux_={}i͐!'`q=Squ$͜]0!~*#*4>c>XPYLDhd:\m}*ܙE쫜W)pypzP#/{dȀ9H ~}\]ܒeKB۳`ʕ'yӛpS(,j3sR26kgPP*>VXӧp8i4lȭy < ]4ƬsI>t4n:;#3'Ot] y"Îc`JޟwY?> zl,:lP8?|jbwc(Ǒc[0$Ձ$KYb~V=[g 0F23` iťfb݆-ڬmowg4Ql 0#&7?ڍDjd)63\V6rCn(n6_m$f4$eZaE3)"<lނ7I[Sצ)!=cqa,L&",&ς#H7kDT,\Ya%tj~~u!4OtSxƥ%FIڽWq6L̟=kv֜8ue1g͙ϖ/ir&<~ &!,#hYZxcpXIR2-@nA!d$ O乺 vc` ֪Xc IiA]EDF텻eV-)urx|0` > ͛5?˯Vӳ9 1! \HUM<=]2k$H 69Ym}-S±z+\?K 3$, ?2d;v˾zn_ӧpn4Dί[1i=:d}Ky={@Es=(f}56m r(=ْO?g ?g1V)cd fR^b  RGT1|< }vj;}o/ :d h3_e`@9{x1GϚA2DjZS҈H>cb@Tl c4a/ivaq}Veant꼏CJՙM70c{KlgU\=I/ C zFP+{% pi >X YkCcҽ¥5~"Zw}wܣ%YHKOF!ErroM1N5k=\"@V0,%l{<b^'kqƖk+_aɗ`\?Z˵q}',VrEJJ MeQЄ$_~ 5ͪ׶N87ܩS݆(BV7,f (TsEr-ʇ6M] -ѿ@04~Pl3dR[ˈ '&֓cZAv֘9{ob̞99!be '9@R{7߆qǶ6|<Cn"@_d`Pߚzs6:ε[4;{aja^Ff͸ݱB£q1\}O=@[68Pj|l˱XYU +o``+.7}?xY-,xNr-Ld%Q SqB3]$HI@In:^FZ3(7K76mށ{wo'QW=uNq Xtj%22d1v HSi L,-б"$% +## <9swoS'saJ3{wn$}e-!Hg9x '%9 )#oHI I/(05/=LOۓdYHoݹz  _͎UT%F Z6ᤒ5f!/nm d)K QD1:-:Y6MtuŽ߷$$I'I5Z?|XwCzZ^FMsaOU-lQ:޲'Ʃ okQQ/0y]&lK7SKw\o]m%Řq% dX:tFXbo W77 <XSl|=Ѻ6maKE<޻$We Ԅ&q{@>Ex={@X]o߃EhB9!ٺ-%g#M'gDF ;_}"=wC:ȃhBdءrJqHE]P$Fp?'aj Y5c\]aҡL$VcŚj\&մBߪ>w$R\A*Xkm_K d=v|ibΏh<q 8{fΜA$tFWeu{&0:$gbđѱijn;y-_?81bwZ|F<}GcY oSfa%$cAIkID=A@0[yoCR҅<G#X٢Z4liY},<:&8C-1{\,*gN%(ƎG71f hb 9 _bڸ1~^2g5&o]Fl }#i醻5ړwaaG{1'\%5 d,kAדkjk`@1t7oZnߥGaddCJ]t9uIz ii!ho`'A!@1aŧ"Φhе?M9~cP!(J+˗l*f2mELyX;G.\/BfܦtfDz-NЪW|%u,]3hyf;Аk}TUٽTb;XO:G`ȊU8 @VYtzwX{u'h׃py&J#6kS?u$nr-7:N]aeґb\4H[nɩ\OF'ortCWhaca mFdP_D{4ٲYPX0xU5|4+-PwY݉ry{>n\laߓ^$@VMeWDq>H^̃gc[]r\.LJj7B^3 ݮ:驉DkO[5'=i5bbBb9AdssR`lC~]Vl ի*>b|X Zo"#?U_İ\7/jPNEq[).cx)V?D#(ⴲ>%'c 唠I['Ig e|юR꿫V&+KLdZM-m^ҋG`ni$Sنd"_:yTb~A97j""2Ő9YuYil$ᤣ˥u_hS|j hu=~}߻LΕp2VMD2h$]z}jc\}V/E2ڣEljQ TFFSTew\} Wn#%/~= L}:ZӃ$+L 9`K1%Ri F: }1?ϓ0_+>+$Ab.5zFv%6&od&b@@ٷ1JJбk@zݿoE7TenJC#mD,] 6jbŗ^_j}3$P_bIbۯ `[BfГV+ɮ9 ;X_+5/fX;`eݜ:e:K1vlP;ckY>xxbxp"B r-8V!d$ñ[?,#JXXؖmOmi!Cf暘0i WNqf::ڕ[WX&q~]YPVl : յe˷4a&c`jWy~Mu+ӧi͙ =<([6c4ZPBRxUU$ɴTfǃ'v2`KN3pk e@tA \у{k&acLU㒰NuKt1*Wc^pEE7g*`t c$)^JSbaq܄m2*Xٶ` V/]6leUmzFZz ^W&iJlN9yʹOޝu5,b|6jh °ۍAŃ<Uo Vkۀչ@{}2˞w*0Y]y\Lt9 re45@SSnbe!<*ۇn@E-JxDqwuttp%.mrk*saRtl---b.C^É{Æ /5c>j?5$]wC+&X]"W(WA]v@1`HFYns߷͈n#:~bkWGC̝3n>~Ėh3F OcФ*U-7} DGRF9φ) zr?j&P33FIYnB1Re dեeAV z(b嗼x=az+\ )˳0TŅ xϗ7OJIEGځΞA8ZGvPIVXLL:г2.=I*h=~߅sR~R 1KzF/zd2.x"~ڵk@!y#s!^eaM$)f?:3's۶?&1W9ižRiC[-?_0,@'b|fie aE ;z\ӽ!\oɣP]t47$uibM:NR2vhg wæ?cg#08?.o[/[F#(n˝ÀV&M W:`yŅhJhOӺK?Vl]uqx={qzaT_@V_iu~\hB,p ׷N'| L6N?I)`Ӝ(BC7׌6ӳ1 0BA-QPC[8C"&?nSűCnÏ߯+5eg6kJ,6&z^nq~>b_7'm~ & WlΤS2:~\wK[.ς_ß" ڠ={c6oR7S,~J~}W| hJ֝[W!xLSZ`qHH@'R/99 ČC ſAO&[Y*<`ef<_Rz[sM28 8X-'E䁅$V a,WE\1)1Z)Pl~(`d'CezY1}VT/O`Ъԭ'N9M]LgAB;_,2ۗE nÒ}V~DzEz?F$\֔dyzʕ `NZӒZ*i-l6$'SCXWceg]⬐&^Cܽ OI<E3dęaGkhZFMeuw2 ;ở~ *_|9ijeJ3 }r,VV̙aQ*KH|7mǤɓfuLTI̪ؑ&GffLZwzΞ:.W!ÑH3\cV$cӷС%MQ`%hKqjMiE{ 4:bm`A }I="l%RY+{D[#1k~_={ RV!5dzUիX]OҾѭENǽѺUK&'CkvÕ5 mHc-dH)^%AGCEt1qX88WrDš ;;@W+u!f# ._ ZkŇc0k ! w\`a^L}PW+Ş=\z4K0TDĂzhVW 5)iE_!Yb õڷ7ϳ` ;3,~ùc]w}stgQOnt|OCƥϡԦ-MtdKftAHLex4 4Fx={RuK2+}#3b[6,* ?HK7G[|I@nΎyAضv ҳrɶ~/#^ڵ7/ |OjrFTl,99ӿ֗FM ܊O}7Iӵ* {6x-TT9!P`@R'wVdz "e>_;a$c:[ k|<|̬L,7P;B&7wzlsHJf:>@W3nMu n؍;Ù4ùbZY'7(5MM"ߗq8SxqhӺlaٸ=l.^xx*]paɏ####P;}bY N,MnbGW5/3z? +]{Z\+܊={wjB.X Ҁ+BL TxΛs?7s,㨛GNvM GQ 8:wGm6_dɔ1u)sWԨ(Nhq(Pn}grZ+ca_%YȲ::VyF8es]u -IX78X_ٹ[([$m@XSS?b|yrU{}6;!^$< ҕ%ǔ5@=;)Fs],,,Rib\HQE +;Gаa#DGU@z=jU`a-˸R#cc۷-{,":uA}QS%%ey 4De1fMFRQ`عm3UQa?emoPRT J"YGfDtsz.O˲h^j 7%b=p)fcrl.英r Μ8=IZ7"\H.|qP_Sdb њڴB{ cM?+;b s9jLɝ13,_.$~R62K)?d2ɪ4i. Ʌ}Mٹx:J`Ϲk5Gjz:,nHǻCzFe@Ou-&012(Orr_#"*g.Aγ].3[ 'qIH!Ek}}^m[`L #D:wE&z8p:뀘8ls}#y:PpOR95x0&OGY56I f %0%42JT /s&gW/Q0pXbe֫[W-X& -G hʽ7*V2!7 h)35W낥ޞkbM$j8n-b"a%k>POpw O"p<]1kRaE\Dܺ%֡ fG|ѳx/rAJv֮ ޼ŻEo ":]m2vQ#[aF+`Umo t?XV K% beIQڟ'E!#q]CILC%:E[ ]tEKE6o~<<@TϷبRnڈ6nJիYWCԢʇ<#Gr3N.8Xh\ԡ!,X2uUx9phE=}Cz`ǜ3K|ޢO80$~*He߾ë/L⢩+:e.p_S,W&̺lV./Э/CѴ!tYmj7KBa֜GFviO Vrc jZʼȒϚUvxfr i#óMĄ>kGYV&bie]Hs -DݍdȊ{w օJ=Mm&t1FJ up\"3K-[`ApvñcG`5UL DCOWvh@۸9cHa Vnx"M y¿$==D -YEJVҞ$pss!׵ϰ&xkqS\⇽'/*7wr; _YdJn{. .8j=`$~c#v,NTTT*UeL5yعs?[)_|CyclH>rn@BB8Wp89r( s3?NEMm(?`v߆dž&h)jW3'K$SX4+80ѦCp'!tt 7uQUY`G&> \]p'V=C=sb\Ab]"i J0$ bwgwtnw"xԴu`FVpd9HɃGi># s7: w?q DVuWƽ;' u%X\ۻuFW -FXK \bm߾[b)S'bYb,Ŝ3U朑N={q뱭`C.^-I’g\q],܄;hc+:X҉\jba?VlƨH=&O1Rݱ`ؐ۵w*zh3/s>t; "g7o|3~őf}/ *V7sVg!&C1׳k_O֠w/`gc%m؂:Zs0Ǝm.4KNNXj-ƌO!Wg9hJ&/#YJ6)VU_t) Ӧ-kh'[:~<FA=޷a!2M {OW05h{GG Y?mJqwd*{>U9E#4U}DQ +FZKu;MytlakVc  pS/B7/wo":;.^ )pm&:Ϥ={GE\ C@=GGG"-Vcن-puwInmqhRx8VxrC^jHҊ.NpMk+:P ퟕ[f}<Z&2<*=7ln^ 2_% 7 1b}3<0ڽ vvQ`Y&>k++0?z\*\WAF ReDo~,"s nIGHz  B0ohK>Y$~d8l|-b 3rDp/.K)RW\#X;Äxp~([2oo& J4a$2o/ҚV;B؂.ٯߠ^ .#",;)qeRSAha}V{y@}^{ ȝ!(8{+f(sX =Ua_vj{%Kb#`$kxH8&ޙSF # 1$*,{.̉ޣڏS\ `DkKCؑ|-<<<<)c̰QH&9p9,Lp?:@ȈV+&@BWV,[pUYcV8 #Yl؏&!I"Ku%u9`|[DQ'\+0_ 2\CSls g Y`cH!G#F_!}-;|&S.\]"CWIq23+͇ICm*].6>/ $'Wq˹3GKOxx)_U3?*@uD`?ptu kz\T&+[[0rEV23ĵ %`xtc -tStmWd!lєh6U1tED ֋WVC,D;w ,1IM厲d=OK {bVZ҄Yn G*PVFfVqs_/=fΜJnM.0"W/ pJIDǩUTփb$:phK9Q9Hɉwq\zzx3wW0n WT 59(.|bP|ޥA]C:D񉏐&@6o%w䞲oGTȝ7ɗ=zCWWF8ü(3a,?|;V.D^Y݇5992j4cɫ@]x*v&ޅkW $%%gsps 7&HMʔt0g.ɂ擋p=ɋ}:F~Y-) zZS߻.zE1}::KBd4ۑhX`,{S Wn_b\q v.h(ft=uxsQ1HM{h )gH_ q=~}͐;; ˖P2(4'^ܲkX>####P\&e+>ֻh;P-;"ԉtMVܾUKӭOqX)m^}qt^]?DpfEVabbACQ)˞3΂,hR=d ٳ0f:~֎b]:آLDnӥWvūWx"jjg[BƹʹjcweL==$8+x5k(g.(-@qk/% aT%z9GïGGGL0II2 ^ƌ`:\ߵ=kfsF$/@+zL]UU Y٧oOڌ0mz é󗡦xFx]W jq՛&-=/hlU0Ҝ,TemH*>ЀEW䇀@?&,Eh DmX&Hn7l(ׯe7`Kx7)g7 )p& om=ÿb$z;l:4AW~E%M"4F'{Im?de#,,\u 6X 5v~we=&"?K0)1q#Y˻:Am`(1̎KѺ]7"_%ɢeˠwWg8>y̛a;w& `pn [Rʻ}MIA^LC0#ߘGGGG )B;{ 箅^nުâ.KT. b2Wډ2Zҟu_hG"%ֳl@D$l00{zE$F`7ߔܹ kު^fQ[O}=R0; U"V2rOO##Pn)|G#Wʮɪ GFŃ.̂,k<'Q0I6i `?sZfujA8q2 "3m`UE_KB|l)py<+nX*ݲ Aek -&L5Y73qtr)Brzh+WV5hF{,=<9C$rnha 1]&HzDBOɘدcSoSN_|% naإ"+kˬXra hL%ڱ/WwGlLr7|.U Vk["+6++SPT5"+۾zG'or`em"~A:eRb[G{`-!GGG#G,1ٰa )ɗ@n0@#:6/%SĄc <$Ri?7x"544?yHk&2iz&450tH2 " ƏH dzH}BZtD@<&y6 W:aј?,OE!W!y5|+ܜEK\##P$kˏ##PHL|PV["CzuXDnrʼnVraRu>Ec4q%0l F!EUuix c,2FqI.)1X;faZVܘ`⪊'!PqI+:U\zZEFWUˇM6>R,tATes%O,`[Uo8#Z'=X42咝Kkt*,s+PlEPڽCUY9?[}'Yz #3U[[ F W;8Yؾ _ [I컖7||*;LNs;Ek ͹={sWE&88]] ,Gnc垎u,\4_ٌIS{V`*"oH`ةeŝ|L0+fჸ⚉=S,Պ)"wBٶ0\b53<<<<EX -* (ڻG[wr.77E}u(w7L2PnYE@:r*K56pU<'J0\,z(Ch޾]Z5<)45(p3):7d$x a!g<$ė|!,X:$XޓXz]yxxʁ.]xxj`ݹc~iV*7^ue$+zU4;p%d۱nKE]!F}eS{ PWS;zA@0T60Y/_CSY\knV/[fK,G@,6UqY|z뱣,UF&+ٔU d+6P"tW$,F4wZJhԁ *OS'p[KփXbyS'Lp yzؔaZjGnQTn#Eڴ űxMqʒ>GuE=.^X~+TUp̚#GD~vK~0\tQ)eCqܑ c]pXӫ)c9XLkKlM)xECⵢc43g|O Ƅ>>F.<1i S팕 3h]z޼ W|3e&Wm6D`$)8 cGCr6 'K!?Ȟ&##ZY XFaD\[! *ua<<<< Y:X<4n ]#{Q}Th?L@b8r }1gڹ%o9tqXSɊ[N\E/ZsTP"xq.~fΥς*N9fM╚L{Ln؇1>:>##3/_AmD 2LjT De#Ѫ nA1!>diU<穉P3+i/)^ǪRԂAQI,UX6Aer\i3 *[,Dn]M'@4+ `۞u ^ OҢф̗'0ROU4hԠ;▤7pdwGƯ)oA ׂ3/,_ Thiadh[/΂M⮙GIۿSZɴ)QV;k1nU"ltsg97v0[ش˪)WMU6~^eGb &Łd$;!==5d564KNШc#kARӇ܂&odˎ-0d`Қ@ "*^=N#:L7G}-ŗ<>2lV._^ڴi~-ōպ2>tڵk ڀZpRp2dg? qE@k7Wʪ{FL5Ap\" ♲_)Tg#)9eV/iعl{=?Tx:r򈈎GJj& u؈[>L6܋SC _Mp3Ӂ$N"AA])=͓ät?"P-h![NNT(E6-8nֈk./Jru,STݧh'|[UYbg #toVu3H9nl.J[#ۡ-JT%kaɏ6ɳ}q8M*vBZPP N{Z&L7`d]!>x_TPlbÒܶ #> z`XvLNr #Y!M2(<"cogߑ'y9I?C#}ԟ0n,RLR?mlVlX40n[A%\akLߎE KT_GqsX}5ߏmRh2d⮳_/ 6mm7vu) a&f&W8xndzm}7x͆RFEF;5V._ &ݻزq -,_JQ7n@-D !lk?7{|\DM`e[a͐Wì¶0 oM7Chŵ'ӕkVam%*}×T: _ fBKW xxxxʄ8ɺv^K)h3QVP({{Kgh,)vE@P V~E2ڴH7ϔ;1z" ,Y5Hx5M^n{P_rmdKD>]ZsC6zhApy8T8N%w[P*,{9@%#[V2pBo"'GK\AU5@MG`Pnzظuj[KZe#IP^.d hkbc)i#C=0K۸ʪc&7//YaQxE|LcVYY֯Ņs'0z(1ĪMkq@?LU5LufM5rYk` %99._ꭇ ;((I&`\6lE0,T(6,3p>m6~x_T҃re0 Vf:'GY~SպDUѢdp6%1YYOԥ¡(g}QOvk,JC]S(OVں Ê+^]v:"QPy7Yk7{ΚǐlɥCɲZaÆ ORQMp$ VG.RA^R ?#Cq\B=Mqn4D=-UYݣ[Dy>)?5jڐW=+g;5)@@=/]#uhG^Q׻UGV5.E}o4@SK XC"{EFZ#08F>cF Ŕ\ާX(OR9x3g!$nHaXzqJf3s~xy }~ʤe~w"9|#фgl΋x5.cس{76lt45lخ"ۀ_iH|>###!o~]~_}K\"H늹e]yhzF2:L1z L3`}"{\Fm[`\ۊlnFċtfϝG.u@T/tdo/`eY *}YZӮCKΣK0k=o(TkrX<w^vT7x&qthQ*Z{xIv*GG$kE@ˊvԳ8oц| X B8F]f}ٔ6N:]֠{^\F~,L͇+*|3 ~;n訩b%O7{;de+KBYu CoG>*,³;yU*r;{hhe`uq(>c525>>gϟЁ82Vx6=_Q} dFd=V#`ily5:]3+5f*$Zy"++#gm`K1 Iê:xbS*ZU$X:e="FX{v庻lFgJVĪa}=$`'EؙnËW! thnM1w\㩑uڶi.;HƟ vG~[[mFJբ֢Te4,3S`T0uҬ1v ɽ DwbBiɴq&|GnUunh5h`HE? Ef^ Mx dNxÓF6Rhaf|kp\~ sѯG栴ud=5Uӣ{z+3 iPlyt|4kEDz,2Rsq.]s }`uX#B࿁&uΘ/M&ȓ|EnU }1W+ݡ;;*pbx ǩ(m>/ڹBP.maԤprs+iyǤ&%]_`Νk+KӳպN|4L7hnf+F[S@'"Q>.#Bʕ66IDATB'w#PTnveF"U&~@¹\ɓ'04K_X(u,źY *fZV+V#f(®:Òq#Ye zTXQ/0{ L3+$?swlL~gb֯Ֆ#K01Ǣj$ tM0rhx'OpCo䰘Y35v/U~ܹ#VNبL'HKz';mi믬=qUYUqN; ĺoZ%qΟ|\& BVc6r/-[`o}54quu%RU@¢YC : GF%^*$WΟEpP@:ۈ\zB51&oS1f_d,. xkǷϿO26n\ 2b0X= vl8m;zQ1w7֯[+w(U YFESև(ԄD####_ҳ_PE0aD֞em.s; ?TYr5ٓyX~hzVB)eg_o|=yl^#2MbSr{+SC0_Q.[ͤ(*XgRwO"X!69v|%@c7FlLߎ\ j~?)"xcEȭʘ1S^ A #5)t+ 5Sk`Ū%عm?nEVT">LW Jޢ`9?R=g"`Sox[>####P6~f>]b (<ʶz۞y8ih@!Nod\jxs#aLVvaH=>GwPyHzr;8v j:Dq˵{k~}&k'|jUg^]wQȆ+J4|0Yrfrt0zrDZ V 2`=tL?/`ݲsD|Up- "凐۷>;1u9n dm ^o^Hdz^qk˗XsF}7 7n\Ƣ%6}* (2~L ׈=%} bqazr}^枥W@@/@ ? "6~aS"gYz xbĽ(mh4}P?q-訩pW>UiѢX `DDäT=9u`.YWUat8P77Qs4h q\ӻȐ(=wKNn;|,\lV{lCJ+-<%X٘W^w:L}%XņkK*ɋXX.ia1h(/ ];ċ'J"+lF^: ).XbbV7Y߲T^0/E# ޒU:.|i9`VFL@#Pع$VZ\L |fzZR{b݌Ȣ٤vŅfqWתfrþp R2Nn>vцנZoqaR1u' r Oww̘>Ek.~#KzVO<4AbuA!px"eZCnf ̚:TL`?:W>=j0L4%eZGum,=YiOɁT%-k\]bO"dQD[ϐ; _C3\ylj z !lR+֬c¤ɵz> #KOڜ8lĢnw Yw⢖GCpv4qذn֯߈UAKS u7-[xQZ:gt~噊ìT1_qlWcYwx/M1~hQgϸF=>V>###PEy(_0_HępкԙZXcaټd ĔqJ]+kP%k]H\L>9o9%}Ue3epsvZ_ $rKjbڨإQV=㴧\zE26LH _##P^xK"@@<>VP!U{*GړXesE~ +ħ;j<|>y yJ )@=ȹqu<#W+ VVnem9IA#3avbk/Iۿ tѢ]{Z*N"loI2` 5N]:OFSPl&ݲc܈{ ̉_8X>x(2CBtml rS(/{y죤ղ o;ha CJUA!%Xw?#1&#### F(J<jksAH%+B9L̒%qakn~%r>/5yp)+ӗ!66sq˸ uUf3ܽ' GS'%ʅ',題f<! WU؅cqr Y'Buvhܸ Yڱ -qxRoj:qǑjK ľ[&*ВMWuMg8`Ԑ]j {uR=MKnK@J׈Jle *$^'.":>\vʈ[qlKV00kfg!=h^iM@lz<1s[+[ޞqXoL=icLb(*{׵{YKwok؂bT$g@KE*7726 3< Ρ7)?$ABBF'1󰑣8׿b'}}#xzxJߋ36//`si}l\]XQ1шVDw΄EDC w"0ֶ}쬬0wve=aW'8;qc>N,cb1]Kj#+deUh2r",X)/l}v֔l$*mٍP;L%y Ѩ$ĨN *JXT%+ʕ+`Ѵ1?ga{wa#W#P!xw`bv*@1C&i v7bX/$H*XR!Ў+qcˆVYIf.X%NEzEݮةTFK&+\28ŭֹE7{>86\+"~+ #9>zu˗#ONC-JLӧX=U94mnV$O$󇞁1t+?$92VYDV8<<18rlɻEhެ,׭ǃ{wП6tmȥ4 0/С]"Շē,L0ETwNBn/O3eyQ]y2d Ͽbm,p8e;Wzt?~V5G޺9k?J~>uw., mqH<@!"s^@_=V@F 80z?4Ք*؃*D eʷC 9ݼn=9֍ !ٺKv@ɨqjI+IKŨa?5=Ujܚ#0A ѽG'Z5/S4L04񞷄5kkY4.4ŢEmj ɚֽ03gQ=ʄlaҭ|NNID@t-%+WHV#=ŲҾ;]BK[[[ 4-i *kfs X"52[,s+;/6Z07|xOm5֪rUe  WzkNLnbAN{iB~F ٜgKKEI!Q2AMSjf6ۄĄGMNjA|*.c:Aj VF6=?|n ڍk1qo%;H_LIcC,&Sԙu֌#5kŞ}"-#BrP۠I(]_ !Q+zSL;  w/Ӧ_aq1hg6gQ|b+横'S"Z-ajB Z&`1v31HMSOI{QV/( ;bǷ;qֈ𦨣oRvOŖGe##lŪ9E>ЄpRT4S8?_oCCж7,g` XE H_IWSs\zzV#Gkst,,<#Pg*d9Ũz(¼Wzug dثuO?%/sU7spE U')bL; M@/{׭?LMU]C 9= _ $_^Y '')4),sb)8w9WnCxu'ďÃyv_',Xي\0#:v6b!o٩xɓjL put Xۺ wn4E׬يUcagDI7㚍]! #<n)`xC1t( ߾Z^v6Ç[>WahԵ'BODTF*Eb{{"kez\"Wh3,Db.M4ڵ U]t9uUL.ABAʺ4L"X@.]_ Î;S蒘Oeߛܣ ^.=7$R)YaA#Iw蹬~oZ[]0,l*.1~h]Zn5'j* O$bxơ$kk#1#H,IOr S#n! vOj(Nz0GkH,zeT>Rz^q[ַU۩VFý/^}m!{J.-O|c;oJ~;#|8l L-`J'V_'rKŰяBXš,dAZ$rSFS#;Dp"{A_Õ%p !έs"q] Ї=#Yz!rUDfBB;;,Ktї6Um >0x`leEdyjSa+Vu"Uek>9Yx8¬GÁV_0"tR+._-[oǶTfF`=MqlvKؼy&<|"1-%ўno %g/#ߍΆCt^t2ظdK==ͫ8QG`ҍǞIukL8]G7zei\;=&Z[񥒙Ѵ<#0@XzDza  Rg݊ƒ{Aj3+(A"6t10ҋ+^Dw S|K,i3U.|Y{wa?$2 *sM;E+ڼm|; *SbӑغE| 4b Uќdd1Nb $քÿ7%&MD0@oj |X' !axLd}  P?}0 V|lzmѪJ@ԗ甲rUMkߣH H$'; {VT .!>Ri͂9O>!ꭅ[S`F}">k&XQ%5C0kTxq'T{KJMH.sfʈd.IݭೃqC#caD<---p@^^D Eמw~e;%+WTWb9Ӳ%kˣ3#8R''>]׮!,1hn\2qx,1E !]Xi#Β5GL!qD.ml; }TVAaUMkj]'RO'@O[Y1(arz&f<2#tn!' 4" xs7!WO݌zp/ȵSʱtD~1ewڸ{Lȥ9Y w9|NKޝeziim5Ǩ"Js$ŠUH ŐWXyx71}-Ȣ\)r_;lHX*#jɏNح2+VUmT rVRW)v|UXs5 (ECp>qg=4Y:O !il=)+g/_WMņ錯<|\Z8yŭ¬Yp) >lyYa(~rmHuX*S ?)OmݠΖb$_F>Y( 6a{Z<˩3ۭI1&Y;ҫka W`eedT`=E1% ;*'%%7Zhkڰ[#oV)(`ɢ̛=޾B:RPU2$"?,%&e dlſ0q܃HLLMb̧/; 0MƫYnfG(Y=$X<\UFAIYT$dlHjGRRI2ןL*0n홫gVA:-#sΩ 鿶 YgΜf{HҒR`rq֖n9Pf\! #3>!Q=|-.DVQOZYx&C]\Ǻ/^IBgF`:"7hQ=:\ɓ'O0B꒑{}Tv߰n5b nz}{δ?32eEGt"y27OSHϝmW7xGHNLƚMk80#z< ~^wԓt( b>[n͊ tzWٿ.ս7瓹rQÂ}=wVT!ZY4Ygc2_GD5) ڙf #$bV8eW e-xif)9>8=yg052f$:ls+_C+F͟YSKŸ}GQǣ>L(mrܹ?Ǯߚg|dgˮ?'gڱ#4,QWLP2QG}}63B}뚐|_\'>1k7UЍM%XVE"XEbmEIJŦou{*oo|9B(kn/jwߑVJU/b D{CŘ=wtϿF@sRS!\!XEU$l$&?yLΓ=Fnխu?ʿVIŹ%%~V&j"jqy-VE F@d^J69jseD_S V1G``>pȲЊ`*k7)^kVaW;g,lCZjQGL*IVp!#0Yz7vf0bHw0DjI i{ Y̝k@Ŧvk_EY; 5~r*쯹ؗ5_ymtqĄIi~\",X|>۲ڒuG!R@p`u=gszRYWcǒOjViO^)AV]w43v}&XλTo8عWޜiÝxΞOO.}i!~ܵ]jfonwU[ǟҶ[l5ֺ%*Ff "E}ϮD PA"',EsEO m[ ,- nyyF`F@)z:l-s!#+MJ5֏1hH$ ;3^Gj`Un0iDns F"wh we u߃?^y9yҫ X=KJLW9~<^^Rx;c|̘cAR]d6N[a#sjM̚4|ćWc蠈겦d}1||t:N)AʻEɼ,z2\v mRORv6k18t 21˜`ei!%emeVVؾyOA~X[ I֍=oOȒ4͗6`0qP4ϟNj.P}={"^kcӪ453jh 1fG՟wVA$cC" K*ߤGwWƠpܺX"Z7 w Gi"NA_tN}5ە&Ru(Sgٵ F`:yan]T U.v9>ٶ ځ*j7: PuvRx+9 YN\/`FhjګgVckZuV\GX.uиY=Mzsb`gDzjT]΋Z|0ݡ#R`>.\LCWR AfF:,%& IH|,յD +֥-EZzT'XJ*b:rf9d.d/ID=6FJuuN!cj&n%+9 1DbaF`E@imA$erɥlbR"<=ʚq"{n\i&1ztt)Bŧ ̙%n"֌#0@dUߵc4BVRXx׮]E^>w[Lx%v3nuv-t̏`gk N~}r} >nF'[!‚Esѷo_|f zPow| R}brݥ  ^uk`na!%1)]t0'a#Gh4v9juc͚F (LַزsJ/b`rs9jF:F7ah`)LA]-zwӦO?Yۑ:qip =_^{bU1#0 !p\busPB>Ų2)kO-\mQ\xgKwE+8{6|:™u¶]r:ՙ,^0!2Ifqdtv zGqrPȍ{5P9F@gȤxMgKuFgVTsjhd#Cjf4QQ[呤dعvU&?{N.p$Op EݔHX.Zӟx|}.\l[ ~Zm;l:|p$=DJsSGyfQ*IV|J묤hXٻJi/?!r*g snN귿gj&$$"88HeChrU&ʬKTM{A—VV6,%mr{F`F)TqAFfETٝTl޺Cxrt2ٯ_g]>`d?F`j `M(ޖt<ȪK뾃{Qz,ڿK’ҋ4]YX`%oёkCL-, N+LxeiQ\]]^5 ||ae8Uף"0 !?߽~&fpWz\ -._'0s_`ep{Ytp7on߮Ľ{wLrI$ H #ڟ>c)ޕ(ޫC:8*,XmgѰڛkF`F-`U=]P_7~d̝5~%=?/عݕ8A=x\0t"4C~LOIENDB`colmap-3.9.1/doc/index.rst000077500000000000000000000064511454702036400154230ustar00rootroot00000000000000COLMAP ====== .. figure:: images/sparse.png :alt: Sparse reconstruction of central Rome. :figclass: align-center Sparse model of central Rome using 21K photos produced by COLMAP's SfM pipeline. .. figure:: images/dense.png :alt: Dense reconstruction of landmarks. :figclass: align-center Dense models of several landmarks produced by COLMAP's MVS pipeline. About ----- COLMAP is a general-purpose Structure-from-Motion (SfM) and Multi-View Stereo (MVS) pipeline with a graphical and command-line interface. It offers a wide range of features for reconstruction of ordered and unordered image collections. The software is licensed under the new BSD license. If you use this project for your research, please cite:: @inproceedings{schoenberger2016sfm, author={Sch\"{o}nberger, Johannes Lutz and Frahm, Jan-Michael}, title={Structure-from-Motion Revisited}, booktitle={Conference on Computer Vision and Pattern Recognition (CVPR)}, year={2016}, } @inproceedings{schoenberger2016mvs, author={Sch\"{o}nberger, Johannes Lutz and Zheng, Enliang and Pollefeys, Marc and Frahm, Jan-Michael}, title={Pixelwise View Selection for Unstructured Multi-View Stereo}, booktitle={European Conference on Computer Vision (ECCV)}, year={2016}, } If you use the image retrieval / vocabulary tree engine, please also cite:: @inproceedings{schoenberger2016vote, author={Sch\"{o}nberger, Johannes Lutz and Price, True and Sattler, Torsten and Frahm, Jan-Michael and Pollefeys, Marc}, title={A Vote-and-Verify Strategy for Fast Spatial Verification in Image Retrieval}, booktitle={Asian Conference on Computer Vision (ACCV)}, year={2016}, } The latest source code is available at `GitHub `_. COLMAP builds on top of existing works and when using specific algorithms within COLMAP, please also cite the original authors, as specified in the source code. Download -------- Executables and other resources can be downloaded from https://demuc.de/colmap/. Getting Started --------------- 1. Download the `pre-built binaries `_ or build the library manually from `source `_ (see :ref:`Installation `). 2. Download one of the provided datasets (see :ref:`Datasets `) or use your own images. 3. Use the **automatic reconstruction** to easily build models with a single click (see :ref:`Quickstart `). Support ------- Please, use `GitHub Discussions `_ for questions and the `GitHub issue tracker `_ for bug reports, feature requests/additions, etc. Acknowledgments --------------- The library was originally written by `Johannes L. Schönberger `_ with funding provided by his PhD advisors Jan-Michael Frahm and Marc Pollefeys. Since then the project has benefited from countless community contributions, including bug fixes, improvements, new features, third-party tooling, and community support. .. toctree:: :hidden: :maxdepth: 2 install tutorial database cameras format datasets gui cli faq changelog contribution license bibliography colmap-3.9.1/doc/install.rst000077500000000000000000000230041454702036400157530ustar00rootroot00000000000000.. _installation: Installation ============ You can either download one of the pre-built binaries or build the source code manually. Executables for Windows and Mac and other resources can be downloaded from https://demuc.de/colmap/. Executables for Linux/Unix/BSD are available at https://repology.org/metapackage/colmap/versions. Note that the COLMAP packages in the default repositories for Linux/Unix/BSD do not come with CUDA support, which requires manual compilation but is relatively easy on these platforms. COLMAP can be used as an independent application through the command-line or graphical user interface. Alternatively, COLMAP is also built as a reusable library, i.e., you can include and link COLMAP against your own source code, as described further below. ------------------ Pre-built Binaries ------------------ Windows ------- For convenience, the pre-built binaries for Windows contain both the graphical and command-line interface executables. To start the COLMAP GUI, you can simply double-click the ``COLMAP.bat`` batch script or alternatively run it from the Windows command shell or Powershell. The command-line interface is also accessible through this batch script, which automatically sets the necessary library paths. To list the available COLMAP commands, run ``COLMAP.bat -h`` in the command shell ``cmd.exe`` or in Powershell. Mac --- The pre-built application package for Mac contains both the GUI and command-line version of COLMAP. To open the GUI, simply open the application and note that COLMAP is shipped as an unsigned application, i.e., when your first open the application, you have to right-click the application and select *Open* and then accept to trust the application. In the future, you can then simply double-click the application to open COLMAP. The command-line interface is accessible by running the packaged binary ``COLMAP.app/Contents/MacOS/colmap``. To list the available COLMAP commands, run ``COLMAP.app/Contents/MacOS/colmap -h``. ----------------- Build from Source ----------------- COLMAP builds on all major platforms (Linux, Mac, Windows) with little effort. First, checkout the latest source code:: git clone https://github.com/colmap/colmap On Linux and Mac it is generally recommended to follow the installation instructions below, which use the system package managers to install the required dependencies. Alternatively, there is a Python build script that builds COLMAP and its dependencies locally. This script is useful under Windows and on a (cluster) system if you do not have root access under Linux or Mac. Linux ----- *Recommended dependencies:* CUDA (at least version 7.X) Dependencies from the default Ubuntu repositories:: sudo apt-get install \ git \ cmake \ ninja-build \ build-essential \ libboost-program-options-dev \ libboost-filesystem-dev \ libboost-graph-dev \ libboost-system-dev \ libeigen3-dev \ libflann-dev \ libfreeimage-dev \ libmetis-dev \ libgoogle-glog-dev \ libgtest-dev \ libsqlite3-dev \ libglew-dev \ qtbase5-dev \ libqt5opengl5-dev \ libcgal-dev \ libceres-dev Configure and compile COLMAP:: git clone https://github.com/colmap/colmap.git cd colmap mkdir build cd build cmake .. -GNinja ninja sudo ninja install Run COLMAP:: colmap -h colmap gui To compile with **CUDA support**, also install Ubuntu's default CUDA package:: sudo apt-get install -y \ nvidia-cuda-toolkit \ nvidia-cuda-toolkit-gcc Or, manually install latest CUDA from NVIDIA's homepage. During CMake configuration specify `CMAKE_CUDA_ARCHITECTURES` as "native", if you want to run COLMAP on your current machine only, "all"/"all-major" to be able to distribute to other machines, or a specific CUDA architecture like "75", etc. Under **Ubuntu 16.04/18.04**, the CMake configuration scripts of CGAL are broken and you must also install the CGAL Qt5 package:: sudo apt-get install libcgal-qt5-dev Under **Ubuntu 22.04**, there is a problem when compiling with Ubuntu's default CUDA package and GCC, and you must compile against GCC 10:: sudo apt-get install gcc-10 g++-10 export CC=/usr/bin/gcc-10 export CXX=/usr/bin/g++-10 export CUDAHOSTCXX=/usr/bin/g++-10 # ... and then run CMake against COLMAP's sources. Mac --- Dependencies from `Homebrew `_:: brew install \ cmake \ ninja \ boost \ eigen \ flann \ freeimage \ metis \ glog \ googletest \ ceres-solver \ qt5 \ glew \ cgal \ sqlite3 Configure and compile COLMAP:: git clone https://github.com/colmap/colmap.git cd colmap export PATH="/usr/local/opt/qt@5/bin:$PATH" mkdir build cd build cmake .. -GNinja -DQt5_DIR=/usr/local/opt/qt/lib/cmake/Qt5 ninja sudo ninja install If you have Qt 6 installed on your system as well, you might have to temporarily link your Qt 5 installation while configuring CMake:: brew link qt5 cmake configuration (from previous code block) brew unlink qt5 Run COLMAP:: colmap -h colmap gui Windows ------- *Recommended dependencies:* CUDA (at least version 7.X), Visual Studio 2019 On Windows, the recommended way is to build COLMAP using vcpkg:: git clone https://github.com/microsoft/vcpkg cd vcpkg .\bootstrap-vcpkg.bat .\vcpkg install colmap[cuda,tests]:x64-windows To compile CUDA for multiple compute architectures, please use:: .\vcpkg install colmap[cuda-redist]:x64-windows Please refer to the next section for more details. **Visual Studio 2022** has some known compiler bugs that crash when compiling COLMAP's source code. VCPKG ----- COLMAP ships as part of the vcpkg distribution. This enables to conveniently build COLMAP and all of its dependencies from scratch under different platforms. Note that VCPKG requires you to install CUDA manually in the standard way on your platform. To compile COLMAP using VCPKG, you run:: git clone https://github.com/microsoft/vcpkg cd vcpkg ./bootstrap-vcpkg.sh ./vcpkg install colmap:x64-linux VCPKG ships with support for various other platforms (e.g., x64-osx, x64-windows, etc.). To compile with CUDA support and to build all tests:: ./vcpkg install colmap[cuda,tests]:x64-linux The above commands will build the latest release version of COLMAP. To compile the latest commit in the dev branch, you can use the following options:: ./vcpkg install colmap:x64-linux --head To modify the source code, you can further add ``--editable --no-downloads``. Or, if you want to build from another folder and use the dependencies from vcpkg, first run `./vcpkg integrate install` and then configure COLMAP as:: cd path/to/colmap mkdir build cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=path/to/vcpkg/scripts/buildsystems/vcpkg.cmake cmake --build . --config release --target colmap_main --parallel 24 .. _installation-library: ------- Library ------- If you want to include and link COLMAP against your own library, the easiest way is to use CMake as a build configuration tool. After configuring the COLMAP build and running `ninja/make install`, COLMAP automatically installs all headers to ``${CMAKE_INSTALL_PREFIX}/include/colmap``, all libraries to ``${CMAKE_INSTALL_PREFIX}/lib/colmap``, and the CMake configuration to ``${CMAKE_INSTALL_PREFIX}/share/colmap``. For example, compiling your own source code against COLMAP is as simple as using the following ``CMakeLists.txt``:: cmake_minimum_required(VERSION 3.10) project(SampleProject) find_package(colmap REQUIRED) # or to require a specific version: find_package(colmap 3.4 REQUIRED) add_executable(hello_world hello_world.cc) target_link_libraries(hello_world colmap::colmap) with the source code ``hello_world.cc``:: #include #include #include #include int main(int argc, char** argv) { colmap::InitializeGlog(argv); std::string message; colmap::OptionManager options; options.AddRequiredOption("message", &message); options.Parse(argc, argv); std::cout << colmap::StringPrintf("Hello %s!", message.c_str()) << std::endl; return EXIT_SUCCESS; } Then compile and run your code as:: mkdir build cd build export colmap_DIR=${CMAKE_INSTALL_PREFIX}/share/colmap cmake .. -GNinja ninja ./hello_world --message "world" The sources of this example are stored under ``doc/sample-project``. ---------------- AddressSanitizer ---------------- If you want to build COLMAP with address sanitizer flags enabled, you need to use a recent compiler with ASan support. For example, you can manually install a recent clang version on your Ubuntu machine and invoke CMake as follows:: CC=/usr/bin/clang CXX=/usr/bin/clang++ cmake .. \ -DASAN_ENABLED=ON \ -DTESTS_ENABLED=ON \ -DCMAKE_BUILD_TYPE=RelWithDebInfo Note that it is generally useful to combine ASan with debug symbols to get meaningful traces for reported issues. ------------- Documentation ------------- You need Python and Sphinx to build the HTML documentation:: cd path/to/colmap/doc sudo apt-get install python pip install sphinx make html open _build/html/index.html Alternatively, you can build the documentation as PDF, EPUB, etc.:: make latexpdf open _build/pdf/COLMAP.pdf colmap-3.9.1/doc/license.rst000066400000000000000000000036541454702036400157350ustar00rootroot00000000000000License ======= The COLMAP library is licensed under the new BSD license. Note that this text refers only to the license for COLMAP itself, independent of its thirdparty dependencies, which are separately licensed. Building COLMAP with these dependencies may affect the resulting COLMAP license. .. code-block:: text 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. colmap-3.9.1/doc/make.bat000077500000000000000000000144731454702036400151720ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\COLMAP.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\COLMAP.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end colmap-3.9.1/doc/sample-project/000077500000000000000000000000001454702036400164765ustar00rootroot00000000000000colmap-3.9.1/doc/sample-project/CMakeLists.txt000066400000000000000000000004001454702036400212300ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.10) project(SampleProject) find_package(colmap REQUIRED) # or to require a specific version: find_package(colmap 3.4 REQUIRED) add_executable(hello_world hello_world.cc) target_link_libraries(hello_world colmap::colmap) colmap-3.9.1/doc/sample-project/hello_world.cc000066400000000000000000000006551454702036400213250ustar00rootroot00000000000000#include #include #include #include int main(int argc, char** argv) { colmap::InitializeGlog(argv); std::string message; colmap::OptionManager options; options.AddRequiredOption("message", &message); options.Parse(argc, argv); std::cout << colmap::StringPrintf("Hello %s!", message.c_str()) << std::endl; return EXIT_SUCCESS; } colmap-3.9.1/doc/tutorial.rst000077500000000000000000000637471454702036400161720ustar00rootroot00000000000000.. _tutorial: Tutorial ======== This tutorial covers the topic of image-based 3D reconstruction by demonstrating the individual processing steps in COLMAP. If you are interested in a more general and mathematical introduction to the topic of image-based 3D reconstruction, please also refer to the `CVPR 2017 Tutorial on Large-scale 3D Modeling from Crowdsourced Data `_ and [schoenberger_thesis]_. Image-based 3D reconstruction from images traditionally first recovers a sparse representation of the scene and the camera poses of the input images using Structure-from-Motion. This output then serves as the input to Multi-View Stereo to recover a dense representation of the scene. .. _quick-start: Quickstart ---------- First, start the graphical user interface of COLMAP, as described :ref:`here `. COLMAP provides an automatic reconstruction tool that simply takes a folder of input images and produces a sparse and dense reconstruction in a workspace folder. Click ``Reconstruction > Automatic Reconstruction`` in the GUI and specify the relevant options. The output is written to the workspace folder. For example, if your images are located in ``path/to/project/images``, you could select ``path/to/project`` as a workspace folder and after running the automatic reconstruction tool, the folder would look similar to this:: +── images │   +── image1.jpg │   +── image2.jpg │   +── ... +── sparse │   +── 0 │   │ +── cameras.bin │   │ +── images.bin │   │ +── points3D.bin │ +── ... +── dense │   +── 0 │   │ +── images │   │ +── sparse │   │ +── stereo │   │ +── fused.ply │   │ +── meshed-poisson.ply │   │ +── meshed-delaunay.ply │ +── ... +── database.db Here, the ``path/to/project/sparse`` contains the sparse models for all reconstructed components, while ``path/to/project/dense`` contains their corresponding dense models. The dense point cloud ``fused.ply`` can be imported in COLMAP using ``File > Import model from ...``, while the dense mesh must be visualized with an external viewer such as Meshlab. The following sections give general recommendations and describe the reconstruction process in more detail, if you need more control over the reconstruction process/parameters or if you are interested in the underlying technology in COLMAP. Structure-from-Motion --------------------- .. figure:: images/incremental-sfm.png :alt: Incremental Structure-from-Motion pipeline :figclass: align-center COLMAP's incremental Structure-from-Motion pipeline. Structure-from-Motion (SfM) is the process of reconstructing 3D structure from its projections into a series of images. The input is a set of overlapping images of the same object, taken from different viewpoints. The output is a 3-D reconstruction of the object, and the reconstructed intrinsic and extrinsic camera parameters of all images. Typically, Structure-from-Motion systems divide this process into three stages: 1) Feature detection and extraction 2) Feature matching and geometric verification 3) Structure and motion reconstruction COLMAP reflects these stages in different modules, that can be combined depending on the application. More information on Structure-from-Motion in general and the algorithms in COLMAP can be found in [schoenberger16sfm]_ and [schoenberger16mvs]_. If you have control over the picture capture process, please follow these guidelines for optimal reconstruction results: - Capture images with **good texture**. Avoid completely texture-less images (e.g., a white wall or empty desk). If the scene does not contain enough texture itself, you could place additional background objects, such as posters, etc. - Capture images at **similar illumination** conditions. Avoid high dynamic range scenes (e.g., pictures against the sun with shadows or pictures through doors/windows). Avoid specularities on shiny surfaces. - Capture images with **high visual overlap**. Make sure that each object is seen in at least 3 images -- the more images the better. - Capture images from **different viewpoints**. Do not take images from the same location by only rotating the camera, e.g., make a few steps after each shot. At the same time, try to have enough images from a relatively similar viewpoint. Note that more images is not necessarily better and might lead to a slow reconstruction process. If you use a video as input, consider down-sampling the frame rate. Multi-View Stereo ----------------- Multi-View Stereo (MVS) takes the output of SfM to compute depth and/or normal information for every pixel in an image. Fusion of the depth and normal maps of multiple images in 3D then produces a dense point cloud of the scene. Using the depth and normal information of the fused point cloud, algorithms such as the (screened) Poisson surface reconstruction [kazhdan2013]_ can then recover the 3D surface geometry of the scene. More information on Multi-View Stereo in general and the algorithms in COLMAP can be found in [schoenberger16mvs]_. Preface ------- COLMAP requires only few steps to do a standard reconstruction for a general user. For more experienced users, the program exposes many different parameters, only some of which are intuitive to a beginner. The program should usually work without the need to modify any parameters. The defaults are chosen as a trade- off between reconstruction robustness/quality and speed. You can set "optimal" options for different reconstruction scenarios by choosing ``Extras > Set options for ... data``. If in doubt what settings to choose, stick to the defaults. The source code contains more documentation about all parameters. COLMAP is research software and in rare cases it may exit ungracefully if some constraints are not fulfilled. In this case, the program prints a traceback to stdout. To see this traceback or more debug information, it is recommended to run the executables (including the GUI) from the command-line, where you can define various levels of logging verbosity. Terminology ----------- The term **camera** is associated with the physical object of a camera using the same zoom-factor and lens. A camera defines the intrinsic projection model in COLMAP. A single camera can take multiple images with the same resolution, intrinsic parameters, and distortion characteristics. The term **image** is associated with a bitmap file, e.g., a JPEG or PNG file on disk. COLMAP detects **keypoints** in each image whose appearance is described by numerical **descriptors**. Pure appearance-based correspondences between keypoints/descriptors are defined by **matches**, while **inlier matches** are geometrically verified and used for the reconstruction procedure. Data Structure -------------- COLMAP assumes that all input images are in one input directory with potentially nested sub-directories. It recursively considers all images stored in this directory, and it supports various different image formats (see `FreeImage `_). Other files are automatically ignored. If high performance is a requirement, then you should separate any files that are not images. Images are identified uniquely by their relative file path. For later processing, such as image undistortion or dense reconstruction, the relative folder structure should be preserved. COLMAP does not modify the input images or directory and all extracted data is stored in a single, self-contained SQLite database file (see :doc:`database`). The first step is to start the graphical user interface of COLMAP by running the pre-built binaries (Windows: `COLMAP.bat`, Mac: `COLMAP.app`) or by executing ``./src/colmap/exe/colmap gui`` from the CMake build folder. Next, create a new project by choosing ``File > New project``. In this dialog, you must select where to store the database and the folder that contains the input images. For convenience, you can save the entire project settings to a configuration file by choosing ``File > Save project``. The project configuration stores the absolute path information of the database and image folder in addition to any other parameter settings. If you decide to move the database or image folder, you must change the paths accordingly by creating a new project. Alternatively, the resulting `.ini` configuration file can be directly modified in a text editor of your choice. To reopen an existing project, you can simply open the configuration file by choosing ``File > Open project`` and all parameter settings should be recovered. Note that all COLMAP executables can be started from the command-line by either specifying individual settings as command-line arguments or by providing the path to the project configuration file (see :ref:`Interface `). An example folder structure could look like this:: /path/to/project/... +── images │   +── image1.jpg │   +── image2.jpg │   +── ... │   +── imageN.jpg +── database.db +── project.ini In this example, you would select `/path/to/project/images` as the image folder path, `/path/to/project/database.db` as the database file path, and save the project configuration to `/path/to/project/project.ini`. Feature Detection and Extraction -------------------------------- In the first step, feature detection/extraction finds sparse feature points in the image and describes their appearance using a numerical descriptor. COLMAP imports images and performs feature detection/extraction in one step in order to only load images from disk once. Next, choose ``Processing > Extract features``. In this dialog, you must first decide on the employed intrinsic camera model. You can either automatically extract focal length information from the embedded EXIF information or manually specify intrinsic parameters, e.g., as obtained in a lab calibration. If an image has partial EXIF information, COLMAP tries to find the missing camera specifications in a large database of camera models automatically. If all your images were captured by the same physical camera with identical zoom factor, it is recommended to share intrinsics between all images. Note that the program will exit ungracefully if the same camera model is shared among all images but not all images have the same size or EXIF focal length. If you have several groups of images that share the same intrinsic camera parameters, you can easily modify the camera models at a later point as well (see :ref:`Database Management `). If in doubt what to choose in this step, simply stick to the default parameters. You can either detect and extract new features from the images or import existing features from text files. COLMAP extracts SIFT [lowe04]_ features either on the GPU or the CPU. The GPU version requires an attached display, while the CPU version is recommended for use on a server. In general, the GPU version is favorable as it has a customized feature detection mode that often produces higher quality features in the case of high contrast images. If you import existing features, every image must have a text file next to it (e.g., `/path/to/image1.jpg` and `/path/to/image1.jpg.txt`) in the following format:: NUM_FEATURES 128 X Y SCALE ORIENTATION D_1 D_2 D_3 ... D_128 ... X Y SCALE ORIENTATION D_1 D_2 D_3 ... D_128 where `X, Y, SCALE, ORIENTATION` are floating point numbers and `D_1...D_128` values in the range `0...255`. The file should have `NUM_FEATURES` lines with one line per feature. For example, if an image has 4 features, then the text file should look something like this:: 4 128 1.2 2.3 0.1 0.3 1 2 3 4 ... 21 2.2 3.3 1.1 0.3 3 2 3 2 ... 32 0.2 1.3 1.1 0.3 3 2 3 2 ... 2 1.2 2.3 1.1 0.3 3 2 3 2 ... 3 Note that by convention the upper left corner of an image has coordinate `(0, 0)` and the center of the upper left most pixel has coordinate `(0.5, 0.5)`. If you must import features for large image collections, it is much more efficient to directly access the database with your favorite scripting language (see :ref:`Database Format `). If you are done setting all options, choose ``Extract`` and wait for the extraction to finish or cancel. If you cancel during the extraction process, the next time you start extracting images for the same project, COLMAP automatically continues where it left off. This also allows you to add images to an existing project/reconstruction. In this case, be sure to verify the camera parameters when using shared intrinsics. All extracted data will be stored in the database file and can be reviewed/managed in the database management tool (see :ref:`Database Management `) or, for experts, directly modified using SQLite (see :ref:`Database Format `). Feature Matching and Geometric Verification ------------------------------------------- In the second step, feature matching and geometric verification finds correspondences between the feature points in different images. Please, choose ``Processing > Match features`` and select one of the provided matching modes, that are intended for different input scenarios: - **Exhaustive Matching**: If the number of images in your dataset is relatively low (up to several hundreds), this matching mode should be fast enough and leads to the best reconstruction results. Here, every image is matched against every other image, while the block size determines how many images are loaded from disk into memory at the same time. - **Sequential Matching**: This mode is useful if the images are acquired in sequential order, e.g., by a video camera. In this case, consecutive frames have visual overlap and there is no need to match all image pairs exhaustively. Instead, consecutively captured images are matched against each other. This matching mode has built-in loop detection based on a vocabulary tree, where every N-th image (`loop_detection_period`) is matched against its visually most similar images (`loop_detection_num_images`). Note that image file names must be ordered sequentially (e.g., `image0001.jpg`, `image0002.jpg`, etc.). The order in the database is not relevant, since the images are explicitly ordered according to their file names. Note that loop detection requires a pre-trained vocabulary tree, that can be downloaded from https://demuc.de/colmap/. - **Vocabulary Tree Matching**: In this matching mode [schoenberger16vote]_, every image is matched against its visual nearest neighbors using a vocabulary tree with spatial re-ranking. This is the recommended matching mode for large image collections (several thousands). This requires a pre-trained vocabulary tree, that can be downloaded from https://demuc.de/colmap/. - **Spatial Matching**: This matching mode matches every image against its spatial nearest neighbors. Spatial locations can be manually set in the database management. By default, COLMAP also extracts GPS information from EXIF and uses it for spatial nearest neighbor search. If accurate prior location information is available, this is the recommended matching mode. - **Transitive Matching**: This matching mode uses the transitive relations of already existing feature matches to produce a more complete matching graph. If an image A matches to an image B and B matches to C, then this matcher attempts to match A to C directly. - **Custom Matching**: This mode allows to specify individual image pairs for matching or to import individual feature matches. To specify image pairs, you have to provide a text file with one image pair per line:: image1.jpg image2.jpg image1.jpg image3.jpg ... where `image1.jpg` is the relative path in the image folder. You have two options to import individual feature matches. Either raw feature matches, which are not geometrically verified or already geometrically verified feature matches. In both cases, the expected format is:: image1.jpg image2.jpg 0 1 1 2 3 4 image1.jpg image3.jpg 0 1 1 2 3 4 4 5 ... where `image1.jpg` is the relative path in the image folder and the pairs of numbers are zero-based feature indices in the respective images. If you must import many matches for large image collections, it is more efficient to directly access the database with a scripting language of your choice. If you are done setting all options, choose ``Match`` and wait for the matching to finish or cancel in between. Note that this step can take a significant amount of time depending on the number of images, the number of features per image, and the chosen matching mode. Expected times for exhaustive matching are from a few minutes for tens of images to a few hours for hundreds of images to days or weeks for thousands of images. If you cancel the matching process or import new images after matching, COLMAP only matches image pairs that have not been matched previously. The overhead of skipping already matched image pairs is low. This also enables to match additional images imported after an initial matching and it enables to combine different matching modes for the same dataset. All extracted data will be stored in the database file and can be reviewed/managed in the database management tool (see :ref:`Database Management `) or, for experts, directly modified using SQLite (see :ref:`Database Format `). Note that feature matching requires a GPU and that the display performance of your computer might degrade significantly during the matching process. If your system has multiple CUDA-enabled GPUs, you can select specific GPUs with the `gpu_index` option. Sparse Reconstruction --------------------- After producing the scene graph in the previous two steps, you can start the incremental reconstruction process by choosing ``Reconstruction > Start``. COLMAP first loads all extracted data from the database into memory and seeds the reconstruction from an initial image pair. Then, the scene is incrementally extended by registering new images and triangulating new points. The results are visualized in "real-time" during this reconstruction process. Refer to the :ref:`Graphical User Interface ` section for more details about the available controls. COLMAP attempts to reconstruct multiple models if not all images are registered into the same model. The different models can be selected from the drop-down menu in the toolbar. If the different models have common registered images, you can use the ``model_converter`` executable to merge them into a single reconstruction (see :ref:`FAQ ` for details). Ideally, the reconstruction works fine and all images are registered. If this is not the case, it is recommended to: - Perform additional matching. For best results, use exhaustive matching, enable guided matching, increase the number of nearest neighbors in vocabulary tree matching, or increase the overlap in sequential matching, etc. - Manually choose an initial image pair, if COLMAP fails to initialize. Choose ``Reconstruction > Reconstruction options > Init`` and set images from the database management tool that have enough matches from different viewpoints. Importing and Exporting ----------------------- COLMAP provides several export options for further processing. For full flexibility, it is recommended to export the reconstruction in COLMAP's data format by choosing ``File > Export`` to export the currently viewed model or ``File > Export all`` to export all reconstructed models. The model is exported in the selected folder using separate text files for the reconstructed cameras, images, and points. When exporting in COLMAP's data format, you can re- import the reconstruction for later visualization, image undistortion, or to continue an existing reconstruction from where it left off (e.g., after importing and matching new images). To import a model, choose ``File > Import`` and select the export folder path. Alternatively, you can also export the model in various other formats, such as Bundler, VisualSfM [#f1]_, PLY, or VRML by choosing ``File > Export as...``. COLMAP can visualize plain PLY point cloud files with RGB information by choosing ``File > Import From...``. Further information about the format of the exported models can be found :ref:`here `. .. _dense-reconstruction: Dense Reconstruction -------------------- After reconstructing a sparse representation of the scene and the camera poses of the input images, MVS can now recover denser scene geometry. COLMAP has an integrated dense reconstruction pipeline to produce depth and normal maps for all registered images, to fuse the depth and normal maps into a dense point cloud with normal information, and to finally estimate a dense surface from the fused point cloud using Poisson [kazhdan2013]_ or Delaunay reconstruction. To get started, import your sparse 3D model into COLMAP (or select the reconstructed model after finishing the previous sparse reconstruction steps). Then, choose ``Reconstruction > Multi-view stereo`` and select an empty or existing workspace folder, which is used for the output and of all dense reconstruction results. The first step is to ``undistort`` the images, second to compute the depth and normal maps using ``stereo``, third to ``fuse`` the depth and normals maps to a point cloud, followed by a final, optional point cloud ``meshing`` step. During the stereo reconstruction process, the display might freeze due to heavy compute load and, if your GPU does not have enough memory, the reconstruction process might ungracefully crash. Please, refer to the FAQ (:ref:`freeze ` and :ref:`memory `) for information on how to avoid these problems. Note that the reconstructed normals of the point cloud cannot be directly visualized in COLMAP, but e.g. in Meshlab by enabling ``Render > Show Normal/Curvature``. Similarly, the reconstructed dense surface mesh model must be visualized with external software. In addition to the internal dense reconstruction functionality, COLMAP exports to several other dense reconstruction libraries, such as CMVS/PMVS [furukawa10]_ or CMP-MVS [jancosek11]_. Please choose ``Extras > Undistort images`` and select the appropriate format. The output folders contain the reconstruction and the undistorted images. In addition, the folders contain sample shell scripts to perform the dense reconstruction. To run PMVS2, execute the following commands: ./path/to/pmvs2 /path/to/undistortion/folder/pmvs/ option-all where `/path/to/undistortion/folder` is the folder selected in the undistortion dialog. Make sure not to forget the trailing slash in `/path/to/undistortion/folder/pmvs/` in the above command-line arguments. For large datasets, you probably want to first run CMVS to cluster the scene into more manageable parts and then run COLMAP or PMVS2. Please, refer to the sample shell scripts in the undistortion output folder on how to run CMVS in combination with COLMAP or PMVS2. Moreover, there are a number of external libraries that support COLMAP's output: - `CMVS/PMVS `_ [furukawa10]_ - `CMP-MVS `_ [jancosek11]_ - `Line3D++ `_ [hofer16]_. .. _database-management: Database Management ------------------- You can review and manage the imported cameras, images, and feature matches in the database management tool. Choose ``Processing > Manage database``. In the opening dialog, you can see the list of imported images and cameras. You can view the features and matches for each image by clicking ``Show image`` and ``Overlapping images``. Individual entries in the database tables can be modified by double clicking specific cells. Note that any changes to the database are only effective after clicking ``Save``. To share intrinsic camera parameters between arbitrary groups of images, select a single or multiple images, choose ``Set camera`` and set the `camera_id`, which corresponds to the unique `camera_id` column in the cameras table. You can also add new cameras with specific parameters. By setting the `prior_focal_length` flag to 0 or 1, you can give a hint whether the reconstruction algorithm should trust the focal length value. In case of a prior lab calibration, you want to set this value to 1. Without prior knowledge about the focal length, it is recommended to set this value to `1.25 * max(width_in_px, height_in_px)`. The database management tool has only limited functionality and, for full control over the data, you must directly modify the SQLite database (see :ref:`Database Format `). By accessing the database directly, you can use COLMAP only for feature extraction and matching or you can import your own features and matches to only use COLMAP's incremental reconstruction algorithm. .. _interface: Graphical and Command-line Interface ------------------------------------ Most of COLMAP's features are accessible from both the graphical and the command-line interface, which are both embedded in the same executable. You can provide the options directly as command-line arguments or you can provide a `.ini` project configuration file containing the options using the ``--project_path path/to/project.ini`` argument. To start the GUI application, please execute ``colmap gui`` or directly specify a project configuration as ``colmap gui --project_path path/to/project.ini`` to avoid tedious selection in the GUI. To list the different commands available from the command-line, execute ``colmap help``. For example, to run feature extraction from the command-line, you must execute ``colmap feature_extractor``. The :ref:`graphical user interface ` and :ref:`command-line Interface ` sections provide more details about the available commands. .. rubric:: Footnotes .. [#f1] VisualSfM's [wu13]_ projection model applies the distortion to the measurements and COLMAP to the projection, hence the exported NVM file is not fully compatible with VisualSfM. colmap-3.9.1/docker/000077500000000000000000000000001454702036400142535ustar00rootroot00000000000000colmap-3.9.1/docker/Dockerfile000066400000000000000000000021401454702036400162420ustar00rootroot00000000000000FROM nvidia/cuda:12.2.2-devel-ubuntu22.04 ARG COLMAP_GIT_COMMIT=main ARG CUDA_ARCHITECTURES=native ENV QT_XCB_GL_INTEGRATION=xcb_egl # Prevent stop building ubuntu at time zone selection. ENV DEBIAN_FRONTEND=noninteractive # Prepare and empty machine for building. RUN apt-get update && apt-get install -y \ git \ cmake \ ninja-build \ build-essential \ libboost-program-options-dev \ libboost-filesystem-dev \ libboost-graph-dev \ libboost-system-dev \ libeigen3-dev \ libflann-dev \ libfreeimage-dev \ libmetis-dev \ libgoogle-glog-dev \ libgtest-dev \ libsqlite3-dev \ libglew-dev \ qtbase5-dev \ libqt5opengl5-dev \ libcgal-dev \ libceres-dev # Build and install COLMAP. RUN git clone https://github.com/colmap/colmap.git RUN cd colmap && \ git fetch https://github.com/colmap/colmap.git ${COLMAP_GIT_COMMIT} && \ git checkout FETCH_HEAD && \ mkdir build && \ cd build && \ cmake .. -GNinja -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHITECTURES} && \ ninja && \ ninja install && \ cd .. && rm -rf colmap colmap-3.9.1/docker/README.md000066400000000000000000000031431454702036400155330ustar00rootroot00000000000000# How to build COLMAP using Docker ## Requirements - Host machine with at least one NVIDIA GPU/CUDA support and installed drivers (to support dense reconstruction). - Docker (for CUDA support 19.03+). ## Quick Start 1. Check that Docker >=19.03 installed on your host machine: ``` docker --version ``` 2. Check that you have an NVIDIA driver installed on your host machine: ``` nvidia-smi ``` 3. Setup the nvidia-toolkit on your host machine: For Ubuntu host machines: `./setup-ubuntu.sh` For CentOS host machines: `./setup-centos.sh` 4. Run the *run* script, using the *full local path* to your preferred local working directory (a folder with your input files/images, etc.): ``` ./run.sh /path/where/your/working/folder/is ``` This will put you in a directory (inside the Docker container) mounted to the local path you specified. Now you can run COLMAP binaries on your own inputs like this: ``` colmap automatic_reconstructor --image_path ./images --workspace_path . ``` ## Build from Scratch After completing steps 1-3, you can alternatively build the docker image from scratch based on the **Dockerfile** (e.g., with your own modifications) using: ``` ./build.sh ./run.sh /path/where/your/working/folder/is ``` ## Troubleshooting Install an NVIDIA driver and NVIDIA container runtime: ``` sudo apt install ubuntu-drivers-common sudo ubuntu-drivers autoinstall ``` If you failed to install the above, check the appropriate NVIDIA driver by yourself and install it: ``` ubuntu-drivers devices e.g. sudo apt install nvidia-driver-455 ``` colmap-3.9.1/docker/build.sh000077500000000000000000000002701454702036400157100ustar00rootroot00000000000000docker build -t="colmap:latest" . # In some cases, you may have to explicitly specify the compute architecture: # docker build -t="colmap:latest" --build-arg CUDA_ARCHITECTURES=75 . colmap-3.9.1/docker/run-gui.sh000077500000000000000000000003421454702036400161770ustar00rootroot00000000000000docker pull colmap/colmap:latest docker run \ -e QT_XCB_GL_INTEGRATION=xcb_egl \ -e DISPLAY=:0 \ -v /tmp/.X11-unix:/tmp/.X11-unix \ --gpus all \ --privileged \ -it colmap/colmap:latest \ colmap gui colmap-3.9.1/docker/run.sh000077500000000000000000000001531454702036400154150ustar00rootroot00000000000000docker pull colmap/colmap:latest docker run --gpus all -w /working -v $1:/working -it colmap/colmap:latest colmap-3.9.1/docker/setup-centos.sh000077500000000000000000000006261454702036400172470ustar00rootroot00000000000000# Add the package repositories distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.repo | sudo tee /etc/yum.repos.d/nvidia-docker.repo # Install nvidia-container-toolkit sudo yum install -y nvidia-container-toolkit sudo systemctl restart docker # Check that it worked! docker run --gpus all nvidia/cuda:10.2-base nvidia-smi colmap-3.9.1/docker/setup-ubuntu.sh000077500000000000000000000010061454702036400172670ustar00rootroot00000000000000# Add the package repositories distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list # Install nvidia-container-toolkit sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit sudo systemctl restart docker # Check that it worked! docker run --gpus all nvidia/cuda:10.2-base nvidia-smi colmap-3.9.1/scripts/000077500000000000000000000000001454702036400144735ustar00rootroot00000000000000colmap-3.9.1/scripts/format/000077500000000000000000000000001454702036400157635ustar00rootroot00000000000000colmap-3.9.1/scripts/format/black.sh000077500000000000000000000015001454702036400173720ustar00rootroot00000000000000#!/usr/bin/env bash # This script runs the black Python formatter on the whole repository. # Check version version_string=$(black --version | sed -E 's/^.*(\d+\.\d+-.*).*$/\1/') expected_version_string='21.12' if [[ "$version_string" =~ "$expected_version_string" ]]; then echo "black version '$version_string' matches '$expected_version_string'" else echo "black version '$version_string' doesn't match '$expected_version_string'" exit 1 fi # Get all C++ files checked into the repo, excluding submodules root_folder=$(git rev-parse --show-toplevel) all_files=$( \ git ls-tree --full-tree -r --name-only HEAD . \ | grep "^.*\(\.py\)$" \ | sed "s~^~$root_folder/~") num_files=$(echo $all_files | wc -w) echo "Formatting ${num_files} files" # shellcheck disable=SC2086 black --line-length 80 ${all_files} colmap-3.9.1/scripts/format/clang_format.sh000077500000000000000000000022731454702036400207620ustar00rootroot00000000000000#!/usr/bin/env bash # This script applies clang-format to the whole repository. # Find clang-format tools=' clang-format-8 clang-format ' clang_format='' for tool in ${tools}; do if type -p "${tool}" > /dev/null; then clang_format=$tool break fi done if [ -z "$clang_format" ]; then echo "Could not locate clang-format" exit 1 fi echo "Found clang-format: $(which ${clang_format})" # Check version version_string=$($clang_format --version | sed -E 's/^.*(\d+\.\d+\.\d+-.*).*$/\1/') expected_version_string='14.0.0' if [[ "$version_string" =~ "$expected_version_string" ]]; then echo "clang-format version '$version_string' matches '$expected_version_string'" else echo "clang-format version '$version_string' doesn't match '$expected_version_string'" exit 1 fi # Get all C++ files checked into the repo, excluding submodules root_folder=$(git rev-parse --show-toplevel) all_files=$( \ git ls-tree --full-tree -r --name-only HEAD . \ | grep "^src/colmap.*\(\.cc\|\.h\|\.hpp\|\.cpp\|\.cu\)$" \ | sed "s~^~$root_folder/~") num_files=$(echo $all_files | wc -w) echo "Formatting ${num_files} files" # Run clang-format ${clang_format} -i $all_files colmap-3.9.1/scripts/matlab/000077500000000000000000000000001454702036400157335ustar00rootroot00000000000000colmap-3.9.1/scripts/matlab/cmap2rgb.m000066400000000000000000000036371454702036400176170ustar00rootroot00000000000000% 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. function rgb = cmap2rgb(image, cmap, varargin) if length(varargin) == 1 clim = varargin{1}; image(image <= clim(1)) = clim(1); image(image > clim(2)) = clim(2); end image_min = min(image(:)); image_max = max(image(:)); image = (image - image_min) / (image_max - image_min) * size(cmap, 1); rgb = ind2rgb(uint32(image), cmap); end colmap-3.9.1/scripts/matlab/plot_model.m000077500000000000000000000045761454702036400202660ustar00rootroot00000000000000% 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. function plot_model(cameras, images, points) % Visualize COLMAP model. keys = images.keys; camera_centers = zeros(images.length, 3); view_dirs = zeros(3 * images.length, 3); for i = 1:images.length image_id = keys{i}; image = images(image_id); camera_centers(i,:) = -image.R' * image.t; view_dirs(3 * i - 2,:) = camera_centers(i,:); view_dirs(3 * i - 1,:) = camera_centers(i,:)' + image.R' * [0; 0; 0.3]; view_dirs(3 * i,:) = nan; end keys = points.keys; xyz = zeros(points.length, 3); for i = 1:points.length point_id = keys{i}; point = points(point_id); xyz(i,:) = point.xyz; end hold on; plot3(camera_centers(:,1), camera_centers(:,2), camera_centers(:,3), 'xr'); plot3(view_dirs(:,1), view_dirs(:,2), view_dirs(:,3), '-b'); plot3(xyz(:,1), xyz(:,2), xyz(:,3), '.k'); hold off; end colmap-3.9.1/scripts/matlab/quat2rotmat.m000077500000000000000000000041651454702036400204050ustar00rootroot00000000000000% 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. function rotmat = quat2rotmat(qvec) rotmat = [1 - 2 * qvec(3).^2 - 2 * qvec(4).^2, ... 2 * qvec(2) * qvec(3) - 2 * qvec(1) * qvec(4), ... 2 * qvec(4) * qvec(2) + 2 * qvec(1) * qvec(3); ... 2 * qvec(2) * qvec(3) + 2 * qvec(1) * qvec(4), ... 1 - 2 * qvec(2).^2 - 2 * qvec(4).^2, ... 2 * qvec(3) * qvec(4) - 2 * qvec(1) * qvec(2); ... 2 * qvec(4) * qvec(2) - 2 * qvec(1) * qvec(3), ... 2 * qvec(3) * qvec(4) + 2 * qvec(1) * qvec(2), ... 1 - 2 * qvec(2).^2 - 2 * qvec(3).^2]; end colmap-3.9.1/scripts/matlab/read_array.m000066400000000000000000000037761454702036400202370ustar00rootroot00000000000000% 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. function array = read_array(path, varargin) if length(varargin) == 1 dtype = varargin{1}; else dtype = 'single'; end fid = fopen(path); line = fscanf(fid, '%d&%d&%d&', [1, 3]); width = line(1); height = line(2); channels = line(3); num = width * height * channels; array = fread(fid, num, dtype); array = reshape(array, [width, height, channels]); array = permute(array, [2 1 3]); array = cast(array, dtype); fclose(fid); end colmap-3.9.1/scripts/matlab/read_depth_map.m000066400000000000000000000035101454702036400210440ustar00rootroot00000000000000% 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. function [depth_map, depth_map_rgb] = read_depth_map(path) depth_map = read_array(path); depth_range = prctile(depth_map(depth_map(:) > 0), [2, 98]); depth_map(depth_map<=0) = nan; depth_map_rgb = cmap2rgb(depth_map, [0 0 0; jet(2^15)], depth_range); end colmap-3.9.1/scripts/matlab/read_model.m000077500000000000000000000111611454702036400202070ustar00rootroot00000000000000% 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. function [cameras, images, points3D] = read_model(path) % Read COLMAP model from folder, which contains a % cameras.txt, images.txt, and points3D.txt. if numel(path) > 0 && path(end) ~= '/' path = [path '/']; end cameras = read_cameras([path 'cameras.txt']); images = read_images([path 'images.txt']); points3D = read_points3D([path 'points3D.txt']); end function cameras = read_cameras(path) cameras = containers.Map('KeyType', 'int64', 'ValueType', 'any'); fid = fopen(path); tline = fgets(fid); while ischar(tline) elems = strsplit(tline); if numel(elems) < 4 || strcmp(elems(1), '#') tline = fgets(fid); continue end if mod(cameras.Count, 10) == 0 fprintf('Reading camera %d\n', cameras.length); end camera = struct; camera.camera_id = str2num(elems{1}); camera.model = elems{2}; camera.width = str2num(elems{3}); camera.height = str2num(elems{4}); camera.params = zeros(numel(elems) - 5, 1); for i = 5:numel(elems) - 1 camera.params(i - 4) = str2double(elems{i}); end cameras(camera.camera_id) = camera; tline = fgets(fid); end fclose(fid); end function images = read_images(path) images = containers.Map('KeyType', 'int64', 'ValueType', 'any'); fid = fopen(path); tline = fgets(fid); while ischar(tline) elems = strsplit(tline); if numel(elems) < 4 || strcmp(elems(1), '#') tline = fgets(fid); continue end if mod(images.Count, 10) == 0 fprintf('Reading image %d\n', images.length); end image = struct; image.image_id = str2num(elems{1}); qw = str2double(elems{2}); qx = str2double(elems{3}); qy = str2double(elems{4}); qz = str2double(elems{5}); image.R = quat2rotmat([qw, qx, qy, qz]); tx = str2double(elems{6}); ty = str2double(elems{7}); tz = str2double(elems{8}); image.t = [tx; ty; tz]; image.camera_id = str2num(elems{9}); image.name = elems{10}; tline = fgets(fid); elems = sscanf(tline, '%f'); elems = reshape(elems, [3, numel(elems) / 3]); image.xys = elems(1:2,:)'; image.point3D_ids = elems(3,:)'; images(image.image_id) = image; tline = fgets(fid); end fclose(fid); end function points3D = read_points3D(path) points3D = containers.Map('KeyType', 'int64', 'ValueType', 'any'); fid = fopen(path); tline = fgets(fid); while ischar(tline) if numel(tline) == 0 || strcmp(tline(1), '#') tline = fgets(fid); continue; end elems = sscanf(tline, '%f'); if numel(elems) == 0 tline = fgets(fid); continue; end if mod(points3D.Count, 1000) == 0 fprintf('Reading point %d\n', points3D.length); end point = struct; point.point3D_id = int64(elems(1)); point.xyz = elems(2:4); point.rgb = uint8(elems(5:7)); point.error = elems(8); point.track = int64(elems(9:end)); point.track = reshape(point.track, [2, numel(point.track) / 2])'; point.track(:,2) = point.track(:,2) + 1; points3D(point.point3D_id) = point; tline = fgets(fid); end fclose(fid); end colmap-3.9.1/scripts/matlab/read_normal_map.m000066400000000000000000000041031454702036400212270ustar00rootroot00000000000000% 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. function [normal_map, normal_map_rgb] = read_normal_map(path, varargin) normal_map = read_array(path); normal_map_rgb = -[normal_map(:,:,1) normal_map(:,:,2) ... 2 * normal_map(:,:,3) + 1]; normal_map_rgb = reshape(normal_map_rgb, ... size(normal_map,1), size(normal_map,2), 3); normal_map_rgb = uint8(((normal_map_rgb + 1) ./ 2) .* 255); if length(varargin) == 1 depth_map = varargin{1}; normal_map_rgb(repmat(isnan(depth_map), [1, 1, 3])) = 0; end end colmap-3.9.1/scripts/matlab/read_ply.m000077500000000000000000000044231454702036400177160ustar00rootroot00000000000000% 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. function [xyz, normals, rgb] = read_ply(path) % Read point cloud from PLY text file. file = fopen(path, 'r'); type = fscanf(file, '%s', 1); format = fscanf(file, '%s', 3); data = fscanf(file, '%s', 2); num_points = fscanf(file, '%d', 1); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 3); fscanf(file, '%s', 1); points_data = textscan(file, '%f %f %f %f %f %f %f %f %f', num_points); xyz = [points_data{1}, points_data{2}, points_data{3}]; rgb = [points_data{7}, points_data{8}, points_data{9}]; normals = [points_data{4}, points_data{5}, points_data{6}]; end colmap-3.9.1/scripts/matlab/write_array.m000066400000000000000000000034401454702036400204420ustar00rootroot00000000000000% 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. function write_array(path, array) fid = fopen(path, 'w'); fprintf(fid, '%d&%d&%d&', size(array, 2), size(array, 1), size(array, 3)); array = permute(array, [2 1 3]); fwrite(fid, array, class(array)); fclose(fid); end colmap-3.9.1/scripts/matlab/write_ply.m000077500000000000000000000046461454702036400201440ustar00rootroot00000000000000% 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. function write_ply(path, xyz, normals, rgb) % Write point cloud to PLY text file. file = fopen(path, 'W'); fprintf(file,'ply\n'); fprintf(file,'format ascii 1.0\n'); fprintf(file,'element vertex %d\n',size(xyz,1)); fprintf(file,'property float x\n'); fprintf(file,'property float y\n'); fprintf(file,'property float z\n'); fprintf(file,'property float nx\n'); fprintf(file,'property float ny\n'); fprintf(file,'property float nz\n'); fprintf(file,'property uchar diffuse_red\n'); fprintf(file,'property uchar diffuse_green\n'); fprintf(file,'property uchar diffuse_blue\n'); fprintf(file,'end_header\n'); for i = 1:size(xyz, 1) fprintf(file, '%f %f %f %f %f %f %d %d %d\n', ... xyz(i,1), xyz(i,2), xyz(i,3), ... normals(i,1), normals(i,2), normals(i,3), ... uint8(rgb(i,1)), uint8(rgb(i,2)), uint8(rgb(i,3))); end fclose(file); end colmap-3.9.1/scripts/python/000077500000000000000000000000001454702036400160145ustar00rootroot00000000000000colmap-3.9.1/scripts/python/benchmark_eth3d.py000066400000000000000000000126561454702036400214210ustar00rootroot00000000000000import os import sys import argparse import urllib.request import subprocess def download_file(url, file_path, max_retries=3): if os.path.exists(file_path): return print(f"Downloading {url} to {file_path}") for retry in range(max_retries): try: urllib.request.urlretrieve(url, file_path) return except Exception as exc: print( f"Failed to download {url} (trial={retry+1}) to {file_path} due to {exc}" ) def check_small_errors_or_exit( dataset_name, max_rotation_error, max_proj_center_error, expected_num_images, errors_csv_path, ): print(f"Evaluating errors for {dataset_name}") error = False with open(errors_csv_path, "r") as fid: num_images = 0 for line in fid: line = line.strip() if len(line) == 0 or line.startswith("#"): continue rotation_error, proj_center_error = map(float, line.split(",")) num_images += 1 if rotation_error > max_rotation_error: print("Exceeded rotation error threshold:", rotation_error) error = True if proj_center_error > max_proj_center_error: print( "Exceeded projection center error threshold:", proj_center_error, ) error = True if num_images != expected_num_images: print("Unexpected number of images:", num_images) error = True if error: sys.exit(1) def process_dataset(args, dataset_name): print("Processing dataset:", dataset_name) workspace_path = os.path.join( os.path.realpath(args.workspace_path), dataset_name ) os.makedirs(workspace_path, exist_ok=True) dataset_archive_path = os.path.join(workspace_path, f"{dataset_name}.7z") download_file( f"https://www.eth3d.net/data/{dataset_name}_dslr_undistorted.7z", dataset_archive_path, ) subprocess.check_call( ["7zz", "x", "-y", f"{dataset_name}.7z"], cwd=workspace_path ) # Find undistorted parameters of first camera and initialize all images with it. with open( os.path.join( workspace_path, f"{dataset_name}/dslr_calibration_undistorted/cameras.txt", ), "r", ) as fid: for line in fid: if not line.startswith("#"): first_camera_data = line.split() camera_model = first_camera_data[1] assert camera_model == "PINHOLE" camera_params = first_camera_data[4:] assert len(camera_params) == 4 break # Count the number of expected images in the GT. expected_num_images = 0 with open( os.path.join( workspace_path, f"{dataset_name}/dslr_calibration_undistorted/images.txt", ), "r", ) as fid: for line in fid: if not line.startswith("#") and line.strip(): expected_num_images += 1 # Each image uses two consecutive lines. assert expected_num_images % 2 == 0 expected_num_images /= 2 # Run automatic reconstruction pipeline. subprocess.check_call( [ os.path.realpath(args.colmap_path), "automatic_reconstructor", "--image_path", f"{dataset_name}/images/", "--workspace_path", workspace_path, "--use_gpu", "1" if args.use_gpu else "0", "--num_threads", str(args.num_threads), "--quality", "low", "--camera_model", "PINHOLE", "--camera_params", ",".join(camera_params), ], cwd=workspace_path, ) # Compare reconstructed model to GT model. subprocess.check_call( [ os.path.realpath(args.colmap_path), "model_comparer", "--input_path1", "sparse/0", "--input_path2", f"{dataset_name}/dslr_calibration_undistorted/", "--output_path", ".", "--alignment_error", "proj_center", "--max_proj_center_error", str(args.max_proj_center_error), ], cwd=workspace_path, ) # Ensure discrepancy between reconstructed model and GT is small. check_small_errors_or_exit( dataset_name, args.max_rotation_error, args.max_proj_center_error, expected_num_images, os.path.join(workspace_path, "errors.csv"), ) def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--dataset_names", required=True) parser.add_argument("--workspace_path", required=True) parser.add_argument("--colmap_path", required=True) parser.add_argument("--use_gpu", default=True, action="store_true") parser.add_argument("--use_cpu", dest="use_gpu", action="store_false") parser.add_argument("--num_threads", type=int, default=-1) parser.add_argument("--quality", default="medium") parser.add_argument("--max_rotation_error", type=float, default=1.0) parser.add_argument("--max_proj_center_error", type=float, default=0.1) return parser.parse_args() def main(): args = parse_args() for dataset_name in args.dataset_names.split(","): process_dataset(args, dataset_name.strip()) if __name__ == "__main__": main() colmap-3.9.1/scripts/python/build_windows_app.py000077500000000000000000000071341454702036400221070ustar00rootroot00000000000000# 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. import os import glob import shutil import argparse def parse_args(): parser = argparse.ArgumentParser() parser.add_argument( "--install_path", required=True, help="The installation prefix, e.g., build/__install__", ) parser.add_argument( "--app_path", required=True, help="The application path, e.g., " "build/COLMAP-dev-windows", ) args = parser.parse_args() return args def mkdir_if_not_exists(path): assert os.path.exists(os.path.dirname(os.path.abspath(path))) if not os.path.exists(path): os.makedirs(path) def main(): args = parse_args() mkdir_if_not_exists(args.app_path) mkdir_if_not_exists(os.path.join(args.app_path, "bin")) mkdir_if_not_exists(os.path.join(args.app_path, "lib")) mkdir_if_not_exists(os.path.join(args.app_path, "lib/platforms")) # Copy batch scripts to app directory. shutil.copyfile( os.path.join(args.install_path, "COLMAP.bat"), os.path.join(args.app_path, "COLMAP.bat"), ) shutil.copyfile( os.path.join(args.install_path, "RUN_TESTS.bat"), os.path.join(args.app_path, "RUN_TESTS.bat"), ) # Copy executables to app directory. exe_files = glob.glob(os.path.join(args.install_path, "bin/*.exe")) for exe_file in exe_files: shutil.copyfile( exe_file, os.path.join(args.app_path, "bin", os.path.basename(exe_file)), ) # Copy shared libraries to app directory. dll_files = glob.glob(os.path.join(args.install_path, "lib/*.dll")) for dll_file in dll_files: shutil.copyfile( dll_file, os.path.join(args.app_path, "lib", os.path.basename(dll_file)), ) shutil.copyfile( os.path.join(args.install_path, "lib/platforms/qwindows.dll"), os.path.join(args.app_path, "lib/platforms/qwindows.dll"), ) # Create zip archive for deployment. shutil.make_archive(args.app_path, "zip", root_dir=args.app_path) if __name__ == "__main__": main() colmap-3.9.1/scripts/python/bundler_to_ply.py000077500000000000000000000113131454702036400214110ustar00rootroot00000000000000# 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. # This script converts a Bundler reconstruction file to a PLY point cloud. import argparse import numpy as np def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--bundler_path", required=True) parser.add_argument("--ply_path", required=True) parser.add_argument("--normalize", type=bool, default=True) parser.add_argument("--normalize_p0", type=float, default=0.2) parser.add_argument("--normalize_p1", type=float, default=0.8) parser.add_argument("--min_track_length", type=int, default=3) args = parser.parse_args() return args def main(): args = parse_args() with open(args.bundler_path, "r") as fid: line = fid.readline() line = fid.readline() num_images, num_points = map(int, line.split()) for i in range(5 * num_images): fid.readline() xyz = np.zeros((num_points, 3), dtype=np.float64) rgb = np.zeros((num_points, 3), dtype=np.uint16) track_lengths = np.zeros((num_points,), dtype=np.uint32) for i in range(num_points): if i % 1000 == 0: print("Reading point", i, "/", num_points) xyz[i] = map(float, fid.readline().split()) rgb[i] = map(int, fid.readline().split()) track_lengths[i] = int(fid.readline().split()[0]) mask = track_lengths >= args.min_track_length xyz = xyz[mask] rgb = rgb[mask] if args.normalize: sorted_x = np.sort(xyz[:, 0]) sorted_y = np.sort(xyz[:, 1]) sorted_z = np.sort(xyz[:, 2]) num_coords = sorted_x.size min_coord = int(args.normalize_p0 * num_coords) max_coord = int(args.normalize_p1 * num_coords) mean_coords = xyz.mean(0) bbox_min = np.array( [sorted_x[min_coord], sorted_y[min_coord], sorted_z[min_coord]] ) bbox_max = np.array( [sorted_x[max_coord], sorted_y[max_coord], sorted_z[max_coord]] ) extent = np.linalg.norm(bbox_max - bbox_min) scale = 10.0 / extent xyz -= mean_coords xyz *= scale xyz[:, 2] *= -1 with open(args.ply_path, "w") as fid: fid.write("ply\n") fid.write("format ascii 1.0\n") fid.write("element vertex %d\n" % xyz.shape[0]) fid.write("property float x\n") fid.write("property float y\n") fid.write("property float z\n") fid.write("property float nx\n") fid.write("property float ny\n") fid.write("property float nz\n") fid.write("property uchar diffuse_red\n") fid.write("property uchar diffuse_green\n") fid.write("property uchar diffuse_blue\n") fid.write("end_header\n") for i in range(xyz.shape[0]): if i % 1000 == 0: print("Writing point", i, "/", xyz.shape[0]) fid.write( "%f %f %f 0 0 0 %d %d %d\n" % ( xyz[i, 0], xyz[i, 1], xyz[i, 2], rgb[i, 0], rgb[i, 1], rgb[i, 2], ) ) if __name__ == "__main__": main() colmap-3.9.1/scripts/python/clang_format_code.py000077500000000000000000000050031454702036400220150ustar00rootroot00000000000000# 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. import os import string import argparse import subprocess def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--path", required=True) parser.add_argument("--exts", default=".h,.cc") parser.add_argument("--style", default="File") args = parser.parse_args() return args def main(): args = parse_args() exts = map(string.lower, args.exts.split(",")) for root, subdirs, files in os.walk(args.path): for f in files: name, ext = os.path.splitext(f) if ext.lower() in exts: file_path = os.path.join(root, f) proc = subprocess.Popen( ["clang-format", "--style", args.style, file_path], stdout=subprocess.PIPE, ) text = "".join(proc.stdout) with open(file_path, "w") as fd: fd.write(text) if __name__ == "__main__": main() colmap-3.9.1/scripts/python/crawl_camera_specs.py000077500000000000000000000130041454702036400222040ustar00rootroot00000000000000# 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. import re import argparse import requests from lxml.html import soupparser MAX_REQUEST_TRIALS = 10 def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--lib_path", required=True) args = parser.parse_args() return args def request_trial(func, *args, **kwargs): for i in range(MAX_REQUEST_TRIALS): try: response = func(*args, **kwargs) except: continue else: return response raise SystemError def main(): args = parse_args() ########################################################################## # Header file ########################################################################## with open(args.lib_path + ".h", "w") as f: f.write("#include \n") f.write("#include \n") f.write("#include \n\n") f.write("// { make1 : ({ model1 : sensor-width in mm }, ...), ... }\n") f.write( "typedef std::vector> make_specs_t;\n" ) f.write( "typedef std::unordered_map camera_specs_t;;\n\n" ) f.write("camera_specs_t InitializeCameraSpecs();\n\n") ########################################################################## # Source file ########################################################################## makes_response = requests.get("http://www.digicamdb.com") makes_tree = soupparser.fromstring(makes_response.text) makes_node = makes_tree.find('.//select[@id="select_brand"]') makes = [b.attrib["value"] for b in makes_node.iter("option")] with open(args.lib_path + ".cc", "w") as f: f.write("camera_specs_t InitializeCameraSpecs() {\n") f.write(" camera_specs_t specs;\n\n") for make in makes: f.write(" {\n") f.write( ' auto& make_specs = specs["%s"];\n' % make.lower().replace(" ", "") ) models_response = request_trial( requests.post, "http://www.digicamdb.com/inc/ajax.php", data={"b": make, "role": "header_search"}, ) models_tree = soupparser.fromstring(models_response.text) models_code = "" num_models = 0 for model_node in models_tree.iter("option"): model = model_node.attrib.get("value") model_name = model_node.text if model is None: continue url = "http://www.digicamdb.com/specs/{0}_{1}".format( make, model ) specs_response = request_trial(requests.get, url) specs_tree = soupparser.fromstring(specs_response.text) for spec in specs_tree.findall('.//td[@class="info_key"]'): if spec.text.strip() == "Sensor:": sensor_text = spec.find("..").find( './td[@class="bold"]' ) sensor_text = sensor_text.text.strip() m = re.match(".*?([\d.]+) x ([\d.]+).*?", sensor_text) sensor_width = m.group(1) data = ( model_name.lower().replace(" ", ""), float(sensor_width.replace(" ", "")), ) models_code += ( ' make_specs.emplace_back("%s", %.4ff);\n' % data ) print(make, model_name) print(" ", sensor_text) num_models += 1 f.write(" make_specs.reserve(%d);\n" % num_models) f.write(models_code) f.write(" }\n\n") f.write(" return specs;\n") f.write("}\n") if __name__ == "__main__": main() colmap-3.9.1/scripts/python/database.py000077500000000000000000000316261454702036400201450ustar00rootroot00000000000000# 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. # This script is based on an original implementation by True Price. import sys import sqlite3 import numpy as np IS_PYTHON3 = sys.version_info[0] >= 3 MAX_IMAGE_ID = 2 ** 31 - 1 CREATE_CAMERAS_TABLE = """CREATE TABLE IF NOT EXISTS cameras ( camera_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, model INTEGER NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL, params BLOB, prior_focal_length INTEGER NOT NULL)""" CREATE_DESCRIPTORS_TABLE = """CREATE TABLE IF NOT EXISTS descriptors ( image_id INTEGER PRIMARY KEY NOT NULL, rows INTEGER NOT NULL, cols INTEGER NOT NULL, data BLOB, FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE)""" CREATE_IMAGES_TABLE = """CREATE TABLE IF NOT EXISTS images ( image_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL UNIQUE, camera_id INTEGER NOT NULL, prior_qw REAL, prior_qx REAL, prior_qy REAL, prior_qz REAL, prior_tx REAL, prior_ty REAL, prior_tz REAL, CONSTRAINT image_id_check CHECK(image_id >= 0 and image_id < {}), FOREIGN KEY(camera_id) REFERENCES cameras(camera_id)) """.format( MAX_IMAGE_ID ) CREATE_TWO_VIEW_GEOMETRIES_TABLE = """ CREATE TABLE IF NOT EXISTS two_view_geometries ( pair_id INTEGER PRIMARY KEY NOT NULL, rows INTEGER NOT NULL, cols INTEGER NOT NULL, data BLOB, config INTEGER NOT NULL, F BLOB, E BLOB, H BLOB, qvec BLOB, tvec BLOB) """ CREATE_KEYPOINTS_TABLE = """CREATE TABLE IF NOT EXISTS keypoints ( image_id INTEGER PRIMARY KEY NOT NULL, rows INTEGER NOT NULL, cols INTEGER NOT NULL, data BLOB, FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE) """ CREATE_MATCHES_TABLE = """CREATE TABLE IF NOT EXISTS matches ( pair_id INTEGER PRIMARY KEY NOT NULL, rows INTEGER NOT NULL, cols INTEGER NOT NULL, data BLOB)""" CREATE_NAME_INDEX = ( "CREATE UNIQUE INDEX IF NOT EXISTS index_name ON images(name)" ) CREATE_ALL = "; ".join( [ CREATE_CAMERAS_TABLE, CREATE_IMAGES_TABLE, CREATE_KEYPOINTS_TABLE, CREATE_DESCRIPTORS_TABLE, CREATE_MATCHES_TABLE, CREATE_TWO_VIEW_GEOMETRIES_TABLE, CREATE_NAME_INDEX, ] ) def image_ids_to_pair_id(image_id1, image_id2): if image_id1 > image_id2: image_id1, image_id2 = image_id2, image_id1 return image_id1 * MAX_IMAGE_ID + image_id2 def pair_id_to_image_ids(pair_id): image_id2 = pair_id % MAX_IMAGE_ID image_id1 = (pair_id - image_id2) / MAX_IMAGE_ID return image_id1, image_id2 def array_to_blob(array): if IS_PYTHON3: return array.tostring() else: return np.getbuffer(array) def blob_to_array(blob, dtype, shape=(-1,)): if IS_PYTHON3: return np.fromstring(blob, dtype=dtype).reshape(*shape) else: return np.frombuffer(blob, dtype=dtype).reshape(*shape) class COLMAPDatabase(sqlite3.Connection): @staticmethod def connect(database_path): return sqlite3.connect(database_path, factory=COLMAPDatabase) def __init__(self, *args, **kwargs): super(COLMAPDatabase, self).__init__(*args, **kwargs) self.create_tables = lambda: self.executescript(CREATE_ALL) self.create_cameras_table = lambda: self.executescript( CREATE_CAMERAS_TABLE ) self.create_descriptors_table = lambda: self.executescript( CREATE_DESCRIPTORS_TABLE ) self.create_images_table = lambda: self.executescript( CREATE_IMAGES_TABLE ) self.create_two_view_geometries_table = lambda: self.executescript( CREATE_TWO_VIEW_GEOMETRIES_TABLE ) self.create_keypoints_table = lambda: self.executescript( CREATE_KEYPOINTS_TABLE ) self.create_matches_table = lambda: self.executescript( CREATE_MATCHES_TABLE ) self.create_name_index = lambda: self.executescript(CREATE_NAME_INDEX) def add_camera( self, model, width, height, params, prior_focal_length=False, camera_id=None, ): params = np.asarray(params, np.float64) cursor = self.execute( "INSERT INTO cameras VALUES (?, ?, ?, ?, ?, ?)", ( camera_id, model, width, height, array_to_blob(params), prior_focal_length, ), ) return cursor.lastrowid def add_image( self, name, camera_id, prior_q=np.full(4, np.NaN), prior_t=np.full(3, np.NaN), image_id=None, ): cursor = self.execute( "INSERT INTO images VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( image_id, name, camera_id, prior_q[0], prior_q[1], prior_q[2], prior_q[3], prior_t[0], prior_t[1], prior_t[2], ), ) return cursor.lastrowid def add_keypoints(self, image_id, keypoints): assert len(keypoints.shape) == 2 assert keypoints.shape[1] in [2, 4, 6] keypoints = np.asarray(keypoints, np.float32) self.execute( "INSERT INTO keypoints VALUES (?, ?, ?, ?)", (image_id,) + keypoints.shape + (array_to_blob(keypoints),), ) def add_descriptors(self, image_id, descriptors): descriptors = np.ascontiguousarray(descriptors, np.uint8) self.execute( "INSERT INTO descriptors VALUES (?, ?, ?, ?)", (image_id,) + descriptors.shape + (array_to_blob(descriptors),), ) def add_matches(self, image_id1, image_id2, matches): assert len(matches.shape) == 2 assert matches.shape[1] == 2 if image_id1 > image_id2: matches = matches[:, ::-1] pair_id = image_ids_to_pair_id(image_id1, image_id2) matches = np.asarray(matches, np.uint32) self.execute( "INSERT INTO matches VALUES (?, ?, ?, ?)", (pair_id,) + matches.shape + (array_to_blob(matches),), ) def add_two_view_geometry( self, image_id1, image_id2, matches, F=np.eye(3), E=np.eye(3), H=np.eye(3), qvec=np.array([1.0, 0.0, 0.0, 0.0]), tvec=np.zeros(3), config=2, ): assert len(matches.shape) == 2 assert matches.shape[1] == 2 if image_id1 > image_id2: matches = matches[:, ::-1] pair_id = image_ids_to_pair_id(image_id1, image_id2) matches = np.asarray(matches, np.uint32) F = np.asarray(F, dtype=np.float64) E = np.asarray(E, dtype=np.float64) H = np.asarray(H, dtype=np.float64) qvec = np.asarray(qvec, dtype=np.float64) tvec = np.asarray(tvec, dtype=np.float64) self.execute( "INSERT INTO two_view_geometries VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (pair_id,) + matches.shape + ( array_to_blob(matches), config, array_to_blob(F), array_to_blob(E), array_to_blob(H), array_to_blob(qvec), array_to_blob(tvec), ), ) def example_usage(): import os import argparse parser = argparse.ArgumentParser() parser.add_argument("--database_path", default="database.db") args = parser.parse_args() if os.path.exists(args.database_path): print("ERROR: database path already exists -- will not modify it.") return # Open the database. db = COLMAPDatabase.connect(args.database_path) # For convenience, try creating all the tables upfront. db.create_tables() # Create dummy cameras. model1, width1, height1, params1 = ( 0, 1024, 768, np.array((1024.0, 512.0, 384.0)), ) model2, width2, height2, params2 = ( 2, 1024, 768, np.array((1024.0, 512.0, 384.0, 0.1)), ) camera_id1 = db.add_camera(model1, width1, height1, params1) camera_id2 = db.add_camera(model2, width2, height2, params2) # Create dummy images. image_id1 = db.add_image("image1.png", camera_id1) image_id2 = db.add_image("image2.png", camera_id1) image_id3 = db.add_image("image3.png", camera_id2) image_id4 = db.add_image("image4.png", camera_id2) # Create dummy keypoints. # # Note that COLMAP supports: # - 2D keypoints: (x, y) # - 4D keypoints: (x, y, theta, scale) # - 6D affine keypoints: (x, y, a_11, a_12, a_21, a_22) num_keypoints = 1000 keypoints1 = np.random.rand(num_keypoints, 2) * (width1, height1) keypoints2 = np.random.rand(num_keypoints, 2) * (width1, height1) keypoints3 = np.random.rand(num_keypoints, 2) * (width2, height2) keypoints4 = np.random.rand(num_keypoints, 2) * (width2, height2) db.add_keypoints(image_id1, keypoints1) db.add_keypoints(image_id2, keypoints2) db.add_keypoints(image_id3, keypoints3) db.add_keypoints(image_id4, keypoints4) # Create dummy matches. M = 50 matches12 = np.random.randint(num_keypoints, size=(M, 2)) matches23 = np.random.randint(num_keypoints, size=(M, 2)) matches34 = np.random.randint(num_keypoints, size=(M, 2)) db.add_matches(image_id1, image_id2, matches12) db.add_matches(image_id2, image_id3, matches23) db.add_matches(image_id3, image_id4, matches34) # Commit the data to the file. db.commit() # Read and check cameras. rows = db.execute("SELECT * FROM cameras") camera_id, model, width, height, params, prior = next(rows) params = blob_to_array(params, np.float64) assert camera_id == camera_id1 assert model == model1 and width == width1 and height == height1 assert np.allclose(params, params1) camera_id, model, width, height, params, prior = next(rows) params = blob_to_array(params, np.float64) assert camera_id == camera_id2 assert model == model2 and width == width2 and height == height2 assert np.allclose(params, params2) # Read and check keypoints. keypoints = dict( (image_id, blob_to_array(data, np.float32, (-1, 2))) for image_id, data in db.execute("SELECT image_id, data FROM keypoints") ) assert np.allclose(keypoints[image_id1], keypoints1) assert np.allclose(keypoints[image_id2], keypoints2) assert np.allclose(keypoints[image_id3], keypoints3) assert np.allclose(keypoints[image_id4], keypoints4) # Read and check matches. pair_ids = [ image_ids_to_pair_id(*pair) for pair in ( (image_id1, image_id2), (image_id2, image_id3), (image_id3, image_id4), ) ] matches = dict( (pair_id_to_image_ids(pair_id), blob_to_array(data, np.uint32, (-1, 2))) for pair_id, data in db.execute("SELECT pair_id, data FROM matches") ) assert np.all(matches[(image_id1, image_id2)] == matches12) assert np.all(matches[(image_id2, image_id3)] == matches23) assert np.all(matches[(image_id3, image_id4)] == matches34) # Clean up. db.close() if os.path.exists(args.database_path): os.remove(args.database_path) if __name__ == "__main__": example_usage() colmap-3.9.1/scripts/python/export_inlier_matches.py000077500000000000000000000064601454702036400227660ustar00rootroot00000000000000# 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. # This script exports inlier matches from a COLMAP database to a text file. import os import argparse import sqlite3 import numpy as np def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--database_path", required=True) parser.add_argument("--output_path", required=True) parser.add_argument("--min_num_matches", type=int, default=15) args = parser.parse_args() return args def pair_id_to_image_ids(pair_id): image_id2 = pair_id % 2147483647 image_id1 = (pair_id - image_id2) / 2147483647 return image_id1, image_id2 def main(): args = parse_args() connection = sqlite3.connect(args.database_path) cursor = connection.cursor() images = {} cursor.execute("SELECT image_id, camera_id, name FROM images;") for row in cursor: image_id = row[0] image_name = row[2] images[image_id] = image_name with open(os.path.join(args.output_path), "w") as fid: cursor.execute( "SELECT pair_id, data FROM two_view_geometries WHERE rows>=?;", (args.min_num_matches,), ) for row in cursor: pair_id = row[0] inlier_matches = np.fromstring(row[1], dtype=np.uint32).reshape( -1, 2 ) image_id1, image_id2 = pair_id_to_image_ids(pair_id) image_name1 = images[image_id1] image_name2 = images[image_id2] fid.write( "%s %s %d\n" % (image_name1, image_name2, inlier_matches.shape[0]) ) for i in range(inlier_matches.shape[0]): fid.write("%d %d\n" % tuple(inlier_matches[i])) cursor.close() connection.close() if __name__ == "__main__": main() colmap-3.9.1/scripts/python/export_inlier_pairs.py000077500000000000000000000061521454702036400224560ustar00rootroot00000000000000# 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. # This script exports inlier image pairs from a COLMAP database to a text file. import sqlite3 import argparse def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--database_path", required=True) parser.add_argument("--match_list_path", required=True) parser.add_argument("--min_num_matches", type=int, default=15) args = parser.parse_args() return args def pair_id_to_image_ids(pair_id): image_id2 = pair_id % 2147483647 image_id1 = (pair_id - image_id2) / 2147483647 return image_id1, image_id2 def main(): args = parse_args() connection = sqlite3.connect(args.database_path) cursor = connection.cursor() # Get a mapping between image ids and image names image_id_to_name = dict() cursor.execute("SELECT image_id, name FROM images;") for row in cursor: image_id = row[0] name = row[1] image_id_to_name[image_id] = name # Iterate over entries in the two_view_geometries table output = open(args.match_list_path, "w") cursor.execute("SELECT pair_id, rows FROM two_view_geometries;") for row in cursor: pair_id = row[0] rows = row[1] if rows < args.min_num_matches: continue image_id1, image_id2 = pair_id_to_image_ids(pair_id) image_name1 = image_id_to_name[image_id1] image_name2 = image_id_to_name[image_id2] output.write("%s %s\n" % (image_name1, image_name2)) output.close() cursor.close() connection.close() if __name__ == "__main__": main() colmap-3.9.1/scripts/python/export_to_bundler.py000077500000000000000000000152271454702036400221360ustar00rootroot00000000000000# 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. # This script exports a COLMAP database to the file structure to run Bundler. import os import argparse import sqlite3 import shutil import gzip import numpy as np def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--database_path", required=True) parser.add_argument("--image_path", required=True) parser.add_argument("--output_path", required=True) parser.add_argument("--min_num_matches", type=int, default=15) args = parser.parse_args() return args def pair_id_to_image_ids(pair_id): image_id2 = pair_id % 2147483647 image_id1 = (pair_id - image_id2) / 2147483647 return image_id1, image_id2 def main(): args = parse_args() connection = sqlite3.connect(args.database_path) cursor = connection.cursor() try: os.makedirs(args.output_path) except: pass cameras = {} cursor.execute("SELECT camera_id, params FROM cameras;") for row in cursor: camera_id = row[0] params = np.fromstring(row[1], dtype=np.double) cameras[camera_id] = params images = {} with open(os.path.join(args.output_path, "list.txt"), "w") as fid: cursor.execute("SELECT image_id, camera_id, name FROM images;") for row in cursor: image_id = row[0] camera_id = row[1] image_name = row[2] print("Copying image", image_name) images[image_id] = (len(images), image_name) fid.write("./%s 0 %f\n" % (image_name, cameras[camera_id][0])) if not os.path.exists(os.path.join(args.output_path, image_name)): shutil.copyfile( os.path.join(args.image_path, image_name), os.path.join(args.output_path, image_name), ) for image_id, (image_idx, image_name) in images.iteritems(): print("Exporting key file for", image_name) base_name, ext = os.path.splitext(image_name) key_file_name = os.path.join(args.output_path, base_name + ".key") key_file_name_gz = key_file_name + ".gz" if os.path.exists(key_file_name_gz): continue cursor.execute( "SELECT data FROM keypoints WHERE image_id=?;", (image_id,) ) row = next(cursor) if row[0] is None: keypoints = np.zeros((0, 6), dtype=np.float32) descriptors = np.zeros((0, 128), dtype=np.uint8) else: keypoints = np.fromstring(row[0], dtype=np.float32).reshape(-1, 6) cursor.execute( "SELECT data FROM descriptors WHERE image_id=?;", (image_id,) ) row = next(cursor) descriptors = np.fromstring(row[0], dtype=np.uint8).reshape(-1, 128) with open(key_file_name, "w") as fid: fid.write("%d %d\n" % (keypoints.shape[0], descriptors.shape[1])) for r in range(keypoints.shape[0]): fid.write( "%f %f %f %f\n" % ( keypoints[r, 1], keypoints[r, 0], keypoints[r, 2], keypoints[r, 3], ) ) for i in range(0, 128, 20): desc_block = descriptors[r, i : i + 20] fid.write(" ".join(map(str, desc_block.ravel().tolist()))) fid.write("\n") with open(key_file_name, "rb") as fid_in: with gzip.open(key_file_name + ".gz", "wb") as fid_out: fid_out.writelines(fid_in) os.remove(key_file_name) with open(os.path.join(args.output_path, "matches.init.txt"), "w") as fid: cursor.execute( "SELECT pair_id, data FROM two_view_geometries " "WHERE rows>=?;", (args.min_num_matches,), ) for row in cursor: pair_id = row[0] inlier_matches = np.fromstring(row[1], dtype=np.uint32).reshape( -1, 2 ) image_id1, image_id2 = pair_id_to_image_ids(pair_id) image_idx1 = images[image_id1][0] image_idx2 = images[image_id2][0] fid.write( "%d %d\n%d\n" % (image_idx1, image_idx2, inlier_matches.shape[0]) ) for i in range(inlier_matches.shape[0]): fid.write( "%d %d\n" % (inlier_matches[i, 0], inlier_matches[i, 1]) ) with open(os.path.join(args.output_path, "run_bundler.sh"), "w") as fid: fid.write("bin/Bundler list.txt \\\n") fid.write("--run_bundle \\\n") fid.write("--use_focal_estimate \\\n") fid.write("--output_all bundle_ \\\n") fid.write("--constrain_focal \\\n") fid.write("--estimate_distortion \\\n") fid.write("--match_table matches.init.txt \\\n") fid.write("--variable_focal_length \\\n") fid.write("--output_dir bundle \\\n") fid.write("--output bundle.out \\\n") fid.write("--constrain_focal_weight 0.0001 \\\n") cursor.close() connection.close() if __name__ == "__main__": main() colmap-3.9.1/scripts/python/export_to_visualsfm.py000077500000000000000000000145371454702036400225170ustar00rootroot00000000000000# 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. # This script exports a COLMAP database to the file structure to run VisualSfM. import os import sys import argparse import sqlite3 import shutil import gzip import numpy as np def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--database_path", required=True) parser.add_argument("--image_path", required=True) parser.add_argument("--output_path", required=True) parser.add_argument("--min_num_matches", type=int, default=15) parser.add_argument("--binary_feature_files", type=bool, default=True) args = parser.parse_args() return args def pair_id_to_image_ids(pair_id): image_id2 = pair_id % 2147483647 image_id1 = (pair_id - image_id2) / 2147483647 return image_id1, image_id2 def main(): args = parse_args() connection = sqlite3.connect(args.database_path) cursor = connection.cursor() try: os.makedirs(args.output_path) except: pass cameras = {} cursor.execute("SELECT camera_id, params FROM cameras;") for row in cursor: camera_id = row[0] params = np.fromstring(row[1], dtype=np.double) cameras[camera_id] = params images = {} cursor.execute("SELECT image_id, camera_id, name FROM images;") for row in cursor: image_id = row[0] camera_id = row[1] image_name = row[2] print("Copying image", image_name) images[image_id] = (len(images), image_name) if not os.path.exists(os.path.join(args.output_path, image_name)): shutil.copyfile( os.path.join(args.image_path, image_name), os.path.join(args.output_path, image_name), ) # The magic numbers used in VisualSfM's binary file format for storing the # feature descriptors. sift_name = 1413892435 sift_version_v4 = 808334422 sift_eof_marker = 1179600383 for image_id, (image_idx, image_name) in images.iteritems(): print("Exporting key file for", image_name) base_name, ext = os.path.splitext(image_name) key_file_name = os.path.join(args.output_path, base_name + ".sift") if os.path.exists(key_file_name): continue cursor.execute( "SELECT data FROM keypoints WHERE image_id=?;", (image_id,) ) row = next(cursor) if row[0] is None: keypoints = np.zeros((0, 6), dtype=np.float32) descriptors = np.zeros((0, 128), dtype=np.uint8) else: keypoints = np.fromstring(row[0], dtype=np.float32).reshape(-1, 6) cursor.execute( "SELECT data FROM descriptors WHERE image_id=?;", (image_id,) ) row = next(cursor) descriptors = np.fromstring(row[0], dtype=np.uint8).reshape(-1, 128) if args.binary_feature_files: with open(key_file_name, "wb") as fid: fid.write(struct.pack("i", sift_name)) fid.write(struct.pack("i", sift_version_v4)) fid.write(struct.pack("i", keypoints.shape[0])) fid.write(struct.pack("i", 4)) fid.write(struct.pack("i", 128)) keypoints[:, :4].astype(np.float32).tofile(fid) descriptors.astype(np.uint8).tofile(fid) fid.write(struct.pack("i", sift_eof_marker)) else: with open(key_file_name, "w") as fid: fid.write( "%d %d\n" % (keypoints.shape[0], descriptors.shape[1]) ) for r in range(keypoints.shape[0]): fid.write("%f %f 0 0 " % (keypoints[r, 0], keypoints[r, 1])) fid.write( " ".join(map(str, descriptors[r].ravel().tolist())) ) fid.write("\n") with open(os.path.join(args.output_path, "matches.txt"), "w") as fid: cursor.execute( "SELECT pair_id, data FROM two_view_geometries " "WHERE rows>=?;", (args.min_num_matches,), ) for row in cursor: pair_id = row[0] inlier_matches = np.fromstring(row[1], dtype=np.uint32).reshape( -1, 2 ) image_id1, image_id2 = pair_id_to_image_ids(pair_id) image_name1 = images[image_id1][1] image_name2 = images[image_id2][1] fid.write( "%s %s %d\n" % (image_name1, image_name2, inlier_matches.shape[0]) ) line1 = "" line2 = "" for i in range(inlier_matches.shape[0]): line1 += "%d " % inlier_matches[i, 0] line2 += "%d " % inlier_matches[i, 1] fid.write(line1 + "\n") fid.write(line2 + "\n") cursor.close() connection.close() if __name__ == "__main__": main() colmap-3.9.1/scripts/python/flickr_downloader.py000077500000000000000000000143301454702036400220620ustar00rootroot00000000000000# 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. import os import time import datetime import urllib import urllib2 import urlparse import socket import argparse import multiprocessing import xml.etree.ElementTree as ElementTree PER_PAGE = 500 SORT = "date-posted-desc" URL = ( "https://api.flickr.com/services/rest/?method=flickr.photos.search&" "api_key=%s&text=%s&sort=%s&per_page=%d&page=%d&min_upload_date=%s&" "max_upload_date=%s&format=rest&extras=url_o,url_l,url_c,url_z,url_n" ) MAX_PAGE_REQUESTS = 5 MAX_PAGE_TIMEOUT = 20 MAX_IMAGE_REQUESTS = 3 TIME_SKIP = 24 * 60 * 60 MAX_DATE = time.time() MIN_DATE = MAX_DATE - TIME_SKIP def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--search_text", required=True) parser.add_argument("--api_key", required=True) parser.add_argument("--image_path", required=True) parser.add_argument("--num_procs", type=int, default=10) parser.add_argument("--max_days_without_image", type=int, default=365) args = parser.parse_args() return args def compose_url(page, api_key, text, min_date, max_date): return URL % ( api_key, text, SORT, PER_PAGE, page, str(min_date), str(max_date), ) def parse_page(page, api_key, text, min_date, max_date): f = None for _ in range(MAX_PAGE_REQUESTS): try: f = urllib2.urlopen( compose_url(page, api_key, text, min_date, max_date), timeout=MAX_PAGE_TIMEOUT, ) except socket.timeout: continue else: break if f is None: return { "pages": "0", "total": "0", "page": "0", "perpage": "0", }, tuple() response = f.read() root = ElementTree.fromstring(response) if root.attrib["stat"] != "ok": raise IOError photos = [] for photo in root.iter("photo"): photos.append(photo.attrib) return root.find("photos").attrib, photos class PhotoDownloader(object): def __init__(self, image_path): self.image_path = image_path def __call__(self, photo): # Find the URL corresponding to the highest image resolution. We will # need this URL here to determine the image extension (typically .jpg, # but could be .png, .gif, etc). url = None for url_suffix in ("o", "l", "k", "h", "b", "c", "z"): url_attr = "url_%s" % url_suffix if photo.get(url_attr) is not None: url = photo.get(url_attr) break if url is not None: # Note that the following statement may fail in Python 3. urlparse # may need to be replaced with urllib.parse. url_filename = urlparse.urlparse(url).path image_ext = os.path.splitext(url_filename)[1] image_name = "%s_%s%s" % (photo["id"], photo["secret"], image_ext) path = os.path.join(self.image_path, image_name) if not os.path.exists(path): print(url) for _ in range(MAX_IMAGE_REQUESTS): try: urllib.urlretrieve(url, path) except urllib.ContentTooShortError: continue else: break def main(): args = parse_args() downloader = PhotoDownloader(args.image_path) pool = multiprocessing.Pool(processes=args.num_procs) num_pages = float("inf") page = 0 min_date = MIN_DATE max_date = MAX_DATE days_in_row = 0 search_text = args.search_text.replace(" ", "-") while num_pages > page: page += 1 metadata, photos = parse_page( page, args.api_key, search_text, min_date, max_date ) num_pages = int(metadata["pages"]) print(78 * "=") print("Page:\t\t", page, "of", num_pages) print("Min-Date:\t", datetime.datetime.fromtimestamp(min_date)) print("Max-Date:\t", datetime.datetime.fromtimestamp(max_date)) print("Num-Photos:\t", len(photos)) print(78 * "=") try: pool.map_async(downloader, photos).get(1e10) except KeyboardInterrupt: pool.wait() break if page >= num_pages: max_date -= TIME_SKIP min_date -= TIME_SKIP page = 0 if num_pages == 0: days_in_row = days_in_row + 1 num_pages = float("inf") print(" No images in", days_in_row, "days in a row") if days_in_row == args.max_days_without_image: break else: days_in_row = 0 if __name__ == "__main__": main() colmap-3.9.1/scripts/python/merge_ply_files.py000077500000000000000000000051111454702036400215340ustar00rootroot00000000000000# 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. # This script merges multiple homogeneous PLY files into a single PLY file. import os import glob import argparse import numpy as np import plyfile def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--folder_path", required=True) parser.add_argument("--merged_path", required=True) args = parser.parse_args() return args def main(): args = parse_args() files = [] for file_name in os.listdir(args.folder_path): if len(file_name) < 4 or file_name[-4:].lower() != ".ply": continue print("Reading file", file_name) file = plyfile.PlyData.read(os.path.join(args.folder_path, file_name)) for element in file.elements: files.append(element.data) print("Merging files") merged_file = np.concatenate(files, -1) merged_el = plyfile.PlyElement.describe(merged_file, "vertex") print("Writing merged file") plyfile.PlyData([merged_el]).write(args.merged_path) if __name__ == "__main__": main() colmap-3.9.1/scripts/python/nvm_to_ply.py000077500000000000000000000113271454702036400205630ustar00rootroot00000000000000# 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. # This script converts a VisualSfM reconstruction file to a PLY point cloud. import os import argparse import numpy as np def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--nvm_path", required=True) parser.add_argument("--ply_path", required=True) parser.add_argument("--normalize", type=bool, default=True) parser.add_argument("--normalize_p0", type=float, default=0.2) parser.add_argument("--normalize_p1", type=float, default=0.8) parser.add_argument("--min_track_length", type=int, default=3) args = parser.parse_args() return args def main(): args = parse_args() with open(args.nvm_path, "r") as fid: line = fid.readline() line = fid.readline() num_images = int(fid.readline()) for i in range(num_images + 1): fid.readline() num_points = int(fid.readline()) xyz = np.zeros((num_points, 3), dtype=np.float64) rgb = np.zeros((num_points, 3), dtype=np.uint16) track_lengths = np.zeros((num_points,), dtype=np.uint32) for i in range(num_points): if i % 1000 == 0: print("Reading point", i, "/", num_points) elems = fid.readline().split() xyz[i] = map(float, elems[0:3]) rgb[i] = map(int, elems[3:6]) track_lengths[i] = int(elems[6]) mask = track_lengths >= args.min_track_length xyz = xyz[mask] rgb = rgb[mask] if args.normalize: sorted_x = np.sort(xyz[:, 0]) sorted_y = np.sort(xyz[:, 1]) sorted_z = np.sort(xyz[:, 2]) num_coords = sorted_x.size min_coord = int(args.normalize_p0 * num_coords) max_coord = int(args.normalize_p1 * num_coords) mean_coords = xyz.mean(0) bbox_min = np.array( [sorted_x[min_coord], sorted_y[min_coord], sorted_z[min_coord]] ) bbox_max = np.array( [sorted_x[max_coord], sorted_y[max_coord], sorted_z[max_coord]] ) extent = np.linalg.norm(bbox_max - bbox_min) scale = 10.0 / extent xyz -= mean_coords xyz *= scale with open(args.ply_path, "w") as fid: fid.write("ply\n") fid.write("format ascii 1.0\n") fid.write("element vertex %d\n" % xyz.shape[0]) fid.write("property float x\n") fid.write("property float y\n") fid.write("property float z\n") fid.write("property float nx\n") fid.write("property float ny\n") fid.write("property float nz\n") fid.write("property uchar diffuse_red\n") fid.write("property uchar diffuse_green\n") fid.write("property uchar diffuse_blue\n") fid.write("end_header\n") for i in range(xyz.shape[0]): if i % 1000 == 0: print("Writing point", i, "/", xyz.shape[0]) fid.write( "%f %f %f 0 0 0 %d %d %d\n" % ( xyz[i, 0], xyz[i, 1], xyz[i, 2], rgb[i, 0], rgb[i, 1], rgb[i, 2], ) ) if __name__ == "__main__": main() colmap-3.9.1/scripts/python/plyfile.py000077500000000000000000000624471454702036400200520ustar00rootroot00000000000000# Copyright 2014 Darsh Ranjan # # This file is part of python-plyfile. # # python-plyfile is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # python-plyfile is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with python-plyfile. If not, see # . from itertools import islice as _islice import numpy as _np from sys import byteorder as _byteorder try: _range = xrange except NameError: _range = range # Many-many relation _data_type_relation = [ ("int8", "i1"), ("char", "i1"), ("uint8", "u1"), ("uchar", "b1"), ("uchar", "u1"), ("int16", "i2"), ("short", "i2"), ("uint16", "u2"), ("ushort", "u2"), ("int32", "i4"), ("int", "i4"), ("uint32", "u4"), ("uint", "u4"), ("float32", "f4"), ("float", "f4"), ("float64", "f8"), ("double", "f8"), ] _data_types = dict(_data_type_relation) _data_type_reverse = dict((b, a) for (a, b) in _data_type_relation) _types_list = [] _types_set = set() for (_a, _b) in _data_type_relation: if _a not in _types_set: _types_list.append(_a) _types_set.add(_a) if _b not in _types_set: _types_list.append(_b) _types_set.add(_b) _byte_order_map = { "ascii": "=", "binary_little_endian": "<", "binary_big_endian": ">", } _byte_order_reverse = {"<": "binary_little_endian", ">": "binary_big_endian"} _native_byte_order = {"little": "<", "big": ">"}[_byteorder] def _lookup_type(type_str): if type_str not in _data_type_reverse: try: type_str = _data_types[type_str] except KeyError: raise ValueError( "field type %r not in %r" % (type_str, _types_list) ) return _data_type_reverse[type_str] def _split_line(line, n): fields = line.split(None, n) if len(fields) == n: fields.append("") assert len(fields) == n + 1 return fields def make2d(array, cols=None, dtype=None): """ Make a 2D array from an array of arrays. The `cols' and `dtype' arguments can be omitted if the array is not empty. """ if (cols is None or dtype is None) and not len(array): raise RuntimeError( "cols and dtype must be specified for empty " "array" ) if cols is None: cols = len(array[0]) if dtype is None: dtype = array[0].dtype return _np.fromiter(array, [("_", dtype, (cols,))], count=len(array))["_"] class PlyParseError(Exception): """ Raised when a PLY file cannot be parsed. The attributes `element', `row', `property', and `message' give additional information. """ def __init__(self, message, element=None, row=None, prop=None): self.message = message self.element = element self.row = row self.prop = prop s = "" if self.element: s += "element %r: " % self.element.name if self.row is not None: s += "row %d: " % self.row if self.prop: s += "property %r: " % self.prop.name s += self.message Exception.__init__(self, s) def __repr__(self): return ( "PlyParseError(%r, element=%r, row=%r, prop=%r)" % self.message, self.element, self.row, self.prop, ) class PlyData(object): """ PLY file header and data. A PlyData instance is created in one of two ways: by the static method PlyData.read (to read a PLY file), or directly from __init__ given a sequence of elements (which can then be written to a PLY file). """ def __init__( self, elements=[], text=False, byte_order="=", comments=[], obj_info=[] ): """ elements: sequence of PlyElement instances. text: whether the resulting PLY file will be text (True) or binary (False). byte_order: '<' for little-endian, '>' for big-endian, or '=' for native. This is only relevant if `text' is False. comments: sequence of strings that will be placed in the header between the 'ply' and 'format ...' lines. obj_info: like comments, but will be placed in the header with "obj_info ..." instead of "comment ...". """ if byte_order == "=" and not text: byte_order = _native_byte_order self.byte_order = byte_order self.text = text self.comments = list(comments) self.obj_info = list(obj_info) self.elements = elements def _get_elements(self): return self._elements def _set_elements(self, elements): self._elements = tuple(elements) self._index() elements = property(_get_elements, _set_elements) def _get_byte_order(self): return self._byte_order def _set_byte_order(self, byte_order): if byte_order not in ["<", ">", "="]: raise ValueError("byte order must be '<', '>', or '='") self._byte_order = byte_order byte_order = property(_get_byte_order, _set_byte_order) def _index(self): self._element_lookup = dict((elt.name, elt) for elt in self._elements) if len(self._element_lookup) != len(self._elements): raise ValueError("two elements with same name") @staticmethod def _parse_header(stream): """ Parse a PLY header from a readable file-like stream. """ lines = [] comments = {"comment": [], "obj_info": []} while True: line = stream.readline().decode("ascii").strip() fields = _split_line(line, 1) if fields[0] == "end_header": break elif fields[0] in comments.keys(): lines.append(fields) else: lines.append(line.split()) a = 0 if lines[a] != ["ply"]: raise PlyParseError("expected 'ply'") a += 1 while lines[a][0] in comments.keys(): comments[lines[a][0]].append(lines[a][1]) a += 1 if lines[a][0] != "format": raise PlyParseError("expected 'format'") if lines[a][2] != "1.0": raise PlyParseError("expected version '1.0'") if len(lines[a]) != 3: raise PlyParseError("too many fields after 'format'") fmt = lines[a][1] if fmt not in _byte_order_map: raise PlyParseError("don't understand format %r" % fmt) byte_order = _byte_order_map[fmt] text = fmt == "ascii" a += 1 while a < len(lines) and lines[a][0] in comments.keys(): comments[lines[a][0]].append(lines[a][1]) a += 1 return PlyData( PlyElement._parse_multi(lines[a:]), text, byte_order, comments["comment"], comments["obj_info"], ) @staticmethod def read(stream): """ Read PLY data from a readable file-like object or filename. """ must_close = False try: if isinstance(stream, str): stream = open(stream, "rb") must_close = True data = PlyData._parse_header(stream) for elt in data: elt._read(stream, data.text, data.byte_order) finally: if must_close: stream.close() return data def write(self, stream): """ Write PLY data to a writeable file-like object or filename. """ must_close = False try: if isinstance(stream, str): stream = open(stream, "wb") must_close = True stream.write(self.header.encode("ascii")) stream.write(b"\r\n") for elt in self: elt._write(stream, self.text, self.byte_order) finally: if must_close: stream.close() @property def header(self): """ Provide PLY-formatted metadata for the instance. """ lines = ["ply"] if self.text: lines.append("format ascii 1.0") else: lines.append( "format " + _byte_order_reverse[self.byte_order] + " 1.0" ) # Some information is lost here, since all comments are placed # between the 'format' line and the first element. for c in self.comments: lines.append("comment " + c) for c in self.obj_info: lines.append("obj_info " + c) lines.extend(elt.header for elt in self.elements) lines.append("end_header") return "\r\n".join(lines) def __iter__(self): return iter(self.elements) def __len__(self): return len(self.elements) def __contains__(self, name): return name in self._element_lookup def __getitem__(self, name): return self._element_lookup[name] def __str__(self): return self.header def __repr__(self): return ( "PlyData(%r, text=%r, byte_order=%r, " "comments=%r, obj_info=%r)" % ( self.elements, self.text, self.byte_order, self.comments, self.obj_info, ) ) class PlyElement(object): """ PLY file element. A client of this library doesn't normally need to instantiate this directly, so the following is only for the sake of documenting the internals. Creating a PlyElement instance is generally done in one of two ways: as a byproduct of PlyData.read (when reading a PLY file) and by PlyElement.describe (before writing a PLY file). """ def __init__(self, name, properties, count, comments=[]): """ This is not part of the public interface. The preferred methods of obtaining PlyElement instances are PlyData.read (to read from a file) and PlyElement.describe (to construct from a numpy array). """ self._name = str(name) self._check_name() self._count = count self._properties = tuple(properties) self._index() self.comments = list(comments) self._have_list = any( isinstance(p, PlyListProperty) for p in self.properties ) @property def count(self): return self._count def _get_data(self): return self._data def _set_data(self, data): self._data = data self._count = len(data) self._check_sanity() data = property(_get_data, _set_data) def _check_sanity(self): for prop in self.properties: if prop.name not in self._data.dtype.fields: raise ValueError("dangling property %r" % prop.name) def _get_properties(self): return self._properties def _set_properties(self, properties): self._properties = tuple(properties) self._check_sanity() self._index() properties = property(_get_properties, _set_properties) def _index(self): self._property_lookup = dict( (prop.name, prop) for prop in self._properties ) if len(self._property_lookup) != len(self._properties): raise ValueError("two properties with same name") def ply_property(self, name): return self._property_lookup[name] @property def name(self): return self._name def _check_name(self): if any(c.isspace() for c in self._name): msg = "element name %r contains spaces" % self._name raise ValueError(msg) def dtype(self, byte_order="="): """ Return the numpy dtype of the in-memory representation of the data. (If there are no list properties, and the PLY format is binary, then this also accurately describes the on-disk representation of the element.) """ return [(prop.name, prop.dtype(byte_order)) for prop in self.properties] @staticmethod def _parse_multi(header_lines): """ Parse a list of PLY element definitions. """ elements = [] while header_lines: (elt, header_lines) = PlyElement._parse_one(header_lines) elements.append(elt) return elements @staticmethod def _parse_one(lines): """ Consume one element definition. The unconsumed input is returned along with a PlyElement instance. """ a = 0 line = lines[a] if line[0] != "element": raise PlyParseError("expected 'element'") if len(line) > 3: raise PlyParseError("too many fields after 'element'") if len(line) < 3: raise PlyParseError("too few fields after 'element'") (name, count) = (line[1], int(line[2])) comments = [] properties = [] while True: a += 1 if a >= len(lines): break if lines[a][0] == "comment": comments.append(lines[a][1]) elif lines[a][0] == "property": properties.append(PlyProperty._parse_one(lines[a])) else: break return (PlyElement(name, properties, count, comments), lines[a:]) @staticmethod def describe(data, name, len_types={}, val_types={}, comments=[]): """ Construct a PlyElement from an array's metadata. len_types and val_types can be given as mappings from list property names to type strings (like 'u1', 'f4', etc., or 'int8', 'float32', etc.). These can be used to define the length and value types of list properties. List property lengths always default to type 'u1' (8-bit unsigned integer), and value types default to 'i4' (32-bit integer). """ if not isinstance(data, _np.ndarray): raise TypeError("only numpy arrays are supported") if len(data.shape) != 1: raise ValueError("only one-dimensional arrays are " "supported") count = len(data) properties = [] descr = data.dtype.descr for t in descr: if not isinstance(t[1], str): raise ValueError("nested records not supported") if not t[0]: raise ValueError("field with empty name") if len(t) != 2 or t[1][1] == "O": # non-scalar field, which corresponds to a list # property in PLY. if t[1][1] == "O": if len(t) != 2: raise ValueError( "non-scalar object fields not " "supported" ) len_str = _data_type_reverse[len_types.get(t[0], "u1")] if t[1][1] == "O": val_type = val_types.get(t[0], "i4") val_str = _lookup_type(val_type) else: val_str = _lookup_type(t[1][1:]) prop = PlyListProperty(t[0], len_str, val_str) else: val_str = _lookup_type(t[1][1:]) prop = PlyProperty(t[0], val_str) properties.append(prop) elt = PlyElement(name, properties, count, comments) elt.data = data return elt def _read(self, stream, text, byte_order): """ Read the actual data from a PLY file. """ if text: self._read_txt(stream) else: if self._have_list: # There are list properties, so a simple load is # impossible. self._read_bin(stream, byte_order) else: # There are no list properties, so loading the data is # much more straightforward. self._data = _np.fromfile( stream, self.dtype(byte_order), self.count ) if len(self._data) < self.count: k = len(self._data) del self._data raise PlyParseError("early end-of-file", self, k) self._check_sanity() def _write(self, stream, text, byte_order): """ Write the data to a PLY file. """ if text: self._write_txt(stream) else: if self._have_list: # There are list properties, so serialization is # slightly complicated. self._write_bin(stream, byte_order) else: # no list properties, so serialization is # straightforward. self.data.astype(self.dtype(byte_order), copy=False).tofile( stream ) def _read_txt(self, stream): """ Load a PLY element from an ASCII-format PLY file. The element may contain list properties. """ self._data = _np.empty(self.count, dtype=self.dtype()) k = 0 for line in _islice(iter(stream.readline, b""), self.count): fields = iter(line.strip().split()) for prop in self.properties: try: self._data[prop.name][k] = prop._from_fields(fields) except StopIteration: raise PlyParseError("early end-of-line", self, k, prop) except ValueError: raise PlyParseError("malformed input", self, k, prop) try: next(fields) except StopIteration: pass else: raise PlyParseError("expected end-of-line", self, k) k += 1 if k < self.count: del self._data raise PlyParseError("early end-of-file", self, k) def _write_txt(self, stream): """ Save a PLY element to an ASCII-format PLY file. The element may contain list properties. """ for rec in self.data: fields = [] for prop in self.properties: fields.extend(prop._to_fields(rec[prop.name])) _np.savetxt(stream, [fields], "%.18g", newline="\r\n") def _read_bin(self, stream, byte_order): """ Load a PLY element from a binary PLY file. The element may contain list properties. """ self._data = _np.empty(self.count, dtype=self.dtype(byte_order)) for k in _range(self.count): for prop in self.properties: try: self._data[prop.name][k] = prop._read_bin( stream, byte_order ) except StopIteration: raise PlyParseError("early end-of-file", self, k, prop) def _write_bin(self, stream, byte_order): """ Save a PLY element to a binary PLY file. The element may contain list properties. """ for rec in self.data: for prop in self.properties: prop._write_bin(rec[prop.name], stream, byte_order) @property def header(self): """ Format this element's metadata as it would appear in a PLY header. """ lines = ["element %s %d" % (self.name, self.count)] # Some information is lost here, since all comments are placed # between the 'element' line and the first property definition. for c in self.comments: lines.append("comment " + c) lines.extend(list(map(str, self.properties))) return "\r\n".join(lines) def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def __str__(self): return self.header def __repr__(self): return "PlyElement(%r, %r, count=%d, comments=%r)" % ( self.name, self.properties, self.count, self.comments, ) class PlyProperty(object): """ PLY property description. This class is pure metadata; the data itself is contained in PlyElement instances. """ def __init__(self, name, val_dtype): self._name = str(name) self._check_name() self.val_dtype = val_dtype def _get_val_dtype(self): return self._val_dtype def _set_val_dtype(self, val_dtype): self._val_dtype = _data_types[_lookup_type(val_dtype)] val_dtype = property(_get_val_dtype, _set_val_dtype) @property def name(self): return self._name def _check_name(self): if any(c.isspace() for c in self._name): msg = "Error: property name %r contains spaces" % self._name raise RuntimeError(msg) @staticmethod def _parse_one(line): assert line[0] == "property" if line[1] == "list": if len(line) > 5: raise PlyParseError("too many fields after " "'property list'") if len(line) < 5: raise PlyParseError("too few fields after " "'property list'") return PlyListProperty(line[4], line[2], line[3]) else: if len(line) > 3: raise PlyParseError("too many fields after " "'property'") if len(line) < 3: raise PlyParseError("too few fields after " "'property'") return PlyProperty(line[2], line[1]) def dtype(self, byte_order="="): """ Return the numpy dtype description for this property (as a tuple of strings). """ return byte_order + self.val_dtype def _from_fields(self, fields): """ Parse from generator. Raise StopIteration if the property could not be read. """ return _np.dtype(self.dtype()).type(next(fields)) def _to_fields(self, data): """ Return generator over one item. """ yield _np.dtype(self.dtype()).type(data) def _read_bin(self, stream, byte_order): """ Read data from a binary stream. Raise StopIteration if the property could not be read. """ try: return _np.fromfile(stream, self.dtype(byte_order), 1)[0] except IndexError: raise StopIteration def _write_bin(self, data, stream, byte_order): """ Write data to a binary stream. """ _np.dtype(self.dtype(byte_order)).type(data).tofile(stream) def __str__(self): val_str = _data_type_reverse[self.val_dtype] return "property %s %s" % (val_str, self.name) def __repr__(self): return "PlyProperty(%r, %r)" % (self.name, _lookup_type(self.val_dtype)) class PlyListProperty(PlyProperty): """ PLY list property description. """ def __init__(self, name, len_dtype, val_dtype): PlyProperty.__init__(self, name, val_dtype) self.len_dtype = len_dtype def _get_len_dtype(self): return self._len_dtype def _set_len_dtype(self, len_dtype): self._len_dtype = _data_types[_lookup_type(len_dtype)] len_dtype = property(_get_len_dtype, _set_len_dtype) def dtype(self, byte_order="="): """ List properties always have a numpy dtype of "object". """ return "|O" def list_dtype(self, byte_order="="): """ Return the pair (len_dtype, val_dtype) (both numpy-friendly strings). """ return (byte_order + self.len_dtype, byte_order + self.val_dtype) def _from_fields(self, fields): (len_t, val_t) = self.list_dtype() n = int(_np.dtype(len_t).type(next(fields))) data = _np.loadtxt(list(_islice(fields, n)), val_t, ndmin=1) if len(data) < n: raise StopIteration return data def _to_fields(self, data): """ Return generator over the (numerical) PLY representation of the list data (length followed by actual data). """ (len_t, val_t) = self.list_dtype() data = _np.asarray(data, dtype=val_t).ravel() yield _np.dtype(len_t).type(data.size) for x in data: yield x def _read_bin(self, stream, byte_order): (len_t, val_t) = self.list_dtype(byte_order) try: n = _np.fromfile(stream, len_t, 1)[0] except IndexError: raise StopIteration data = _np.fromfile(stream, val_t, n) if len(data) < n: raise StopIteration return data def _write_bin(self, data, stream, byte_order): """ Write data to a binary stream. """ (len_t, val_t) = self.list_dtype(byte_order) data = _np.asarray(data, dtype=val_t).ravel() _np.array(data.size, dtype=len_t).tofile(stream) data.tofile(stream) def __str__(self): len_str = _data_type_reverse[self.len_dtype] val_str = _data_type_reverse[self.val_dtype] return "property list %s %s %s" % (len_str, val_str, self.name) def __repr__(self): return "PlyListProperty(%r, %r, %r)" % ( self.name, _lookup_type(self.len_dtype), _lookup_type(self.val_dtype), ) colmap-3.9.1/scripts/python/read_write_dense.py000077500000000000000000000120171454702036400216750ustar00rootroot00000000000000#!/usr/bin/env python # 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. import argparse import numpy as np import os import struct def read_array(path): with open(path, "rb") as fid: width, height, channels = np.genfromtxt( fid, delimiter="&", max_rows=1, usecols=(0, 1, 2), dtype=int ) fid.seek(0) num_delimiter = 0 byte = fid.read(1) while True: if byte == b"&": num_delimiter += 1 if num_delimiter >= 3: break byte = fid.read(1) array = np.fromfile(fid, np.float32) array = array.reshape((width, height, channels), order="F") return np.transpose(array, (1, 0, 2)).squeeze() def write_array(array, path): """ see: src/mvs/mat.h void Mat::Write(const std::string& path) """ assert array.dtype == np.float32 if len(array.shape) == 2: height, width = array.shape channels = 1 elif len(array.shape) == 3: height, width, channels = array.shape else: assert False with open(path, "w") as fid: fid.write(str(width) + "&" + str(height) + "&" + str(channels) + "&") with open(path, "ab") as fid: if len(array.shape) == 2: array_trans = np.transpose(array, (1, 0)) elif len(array.shape) == 3: array_trans = np.transpose(array, (1, 0, 2)) else: assert False data_1d = array_trans.reshape(-1, order="F") data_list = data_1d.tolist() endian_character = "<" format_char_sequence = "".join(["f"] * len(data_list)) byte_data = struct.pack( endian_character + format_char_sequence, *data_list ) fid.write(byte_data) def parse_args(): parser = argparse.ArgumentParser() parser.add_argument( "-d", "--depth_map", help="path to depth map", type=str, required=True ) parser.add_argument( "-n", "--normal_map", help="path to normal map", type=str, required=True ) parser.add_argument( "--min_depth_percentile", help="minimum visualization depth percentile", type=float, default=5, ) parser.add_argument( "--max_depth_percentile", help="maximum visualization depth percentile", type=float, default=95, ) args = parser.parse_args() return args def main(): args = parse_args() if args.min_depth_percentile > args.max_depth_percentile: raise ValueError( "min_depth_percentile should be less than or equal " "to the max_depth_perceintile." ) # Read depth and normal maps corresponding to the same image. if not os.path.exists(args.depth_map): raise FileNotFoundError("File not found: {}".format(args.depth_map)) if not os.path.exists(args.normal_map): raise FileNotFoundError("File not found: {}".format(args.normal_map)) depth_map = read_array(args.depth_map) normal_map = read_array(args.normal_map) min_depth, max_depth = np.percentile( depth_map, [args.min_depth_percentile, args.max_depth_percentile] ) depth_map[depth_map < min_depth] = min_depth depth_map[depth_map > max_depth] = max_depth import pylab as plt # Visualize the depth map. plt.figure() plt.imshow(depth_map) plt.title("depth map") # Visualize the normal map. plt.figure() plt.imshow(normal_map) plt.title("normal map") plt.show() if __name__ == "__main__": main() colmap-3.9.1/scripts/python/read_write_fused_vis.py000077500000000000000000000121211454702036400225620ustar00rootroot00000000000000#!/usr/bin/env python # 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. import os import collections import numpy as np import pandas as pd from pyntcloud import PyntCloud from read_write_model import read_next_bytes, write_next_bytes MeshPoint = collections.namedtuple( "MeshingPoint", ["position", "color", "normal", "num_visible_images", "visible_image_idxs"], ) def read_fused(path_to_fused_ply, path_to_fused_ply_vis): """ see: src/mvs/meshing.cc void ReadDenseReconstruction(const std::string& path """ assert os.path.isfile(path_to_fused_ply) assert os.path.isfile(path_to_fused_ply_vis) point_cloud = PyntCloud.from_file(path_to_fused_ply) xyz_arr = point_cloud.points.loc[:, ["x", "y", "z"]].to_numpy() normal_arr = point_cloud.points.loc[:, ["nx", "ny", "nz"]].to_numpy() color_arr = point_cloud.points.loc[:, ["red", "green", "blue"]].to_numpy() with open(path_to_fused_ply_vis, "rb") as fid: num_points = read_next_bytes(fid, 8, "Q")[0] mesh_points = [0] * num_points for i in range(num_points): num_visible_images = read_next_bytes(fid, 4, "I")[0] visible_image_idxs = read_next_bytes( fid, num_bytes=4 * num_visible_images, format_char_sequence="I" * num_visible_images, ) visible_image_idxs = np.array(tuple(map(int, visible_image_idxs))) mesh_point = MeshPoint( position=xyz_arr[i], color=color_arr[i], normal=normal_arr[i], num_visible_images=num_visible_images, visible_image_idxs=visible_image_idxs, ) mesh_points[i] = mesh_point return mesh_points def write_fused_ply(mesh_points, path_to_fused_ply): columns = ["x", "y", "z", "nx", "ny", "nz", "red", "green", "blue"] points_data_frame = pd.DataFrame( np.zeros((len(mesh_points), len(columns))), columns=columns ) positions = np.asarray([point.position for point in mesh_points]) normals = np.asarray([point.normal for point in mesh_points]) colors = np.asarray([point.color for point in mesh_points]) points_data_frame.loc[:, ["x", "y", "z"]] = positions points_data_frame.loc[:, ["nx", "ny", "nz"]] = normals points_data_frame.loc[:, ["red", "green", "blue"]] = colors points_data_frame = points_data_frame.astype( { "x": positions.dtype, "y": positions.dtype, "z": positions.dtype, "red": colors.dtype, "green": colors.dtype, "blue": colors.dtype, "nx": normals.dtype, "ny": normals.dtype, "nz": normals.dtype, } ) point_cloud = PyntCloud(points_data_frame) point_cloud.to_file(path_to_fused_ply) def write_fused_ply_vis(mesh_points, path_to_fused_ply_vis): """ see: src/mvs/fusion.cc void WritePointsVisibility(const std::string& path, const std::vector>& points_visibility) """ with open(path_to_fused_ply_vis, "wb") as fid: write_next_bytes(fid, len(mesh_points), "Q") for point in mesh_points: write_next_bytes(fid, point.num_visible_images, "I") format_char_sequence = "I" * point.num_visible_images write_next_bytes( fid, [*point.visible_image_idxs], format_char_sequence ) def write_fused(points, path_to_fused_ply, path_to_fused_ply_vis): write_fused_ply(points, path_to_fused_ply) write_fused_ply_vis(points, path_to_fused_ply_vis) colmap-3.9.1/scripts/python/read_write_model.py000077500000000000000000000537201454702036400217050ustar00rootroot00000000000000# 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. import os import collections import numpy as np import struct import argparse CameraModel = collections.namedtuple( "CameraModel", ["model_id", "model_name", "num_params"] ) Camera = collections.namedtuple( "Camera", ["id", "model", "width", "height", "params"] ) BaseImage = collections.namedtuple( "Image", ["id", "qvec", "tvec", "camera_id", "name", "xys", "point3D_ids"] ) Point3D = collections.namedtuple( "Point3D", ["id", "xyz", "rgb", "error", "image_ids", "point2D_idxs"] ) class Image(BaseImage): def qvec2rotmat(self): return qvec2rotmat(self.qvec) CAMERA_MODELS = { CameraModel(model_id=0, model_name="SIMPLE_PINHOLE", num_params=3), CameraModel(model_id=1, model_name="PINHOLE", num_params=4), CameraModel(model_id=2, model_name="SIMPLE_RADIAL", num_params=4), CameraModel(model_id=3, model_name="RADIAL", num_params=5), CameraModel(model_id=4, model_name="OPENCV", num_params=8), CameraModel(model_id=5, model_name="OPENCV_FISHEYE", num_params=8), CameraModel(model_id=6, model_name="FULL_OPENCV", num_params=12), CameraModel(model_id=7, model_name="FOV", num_params=5), CameraModel(model_id=8, model_name="SIMPLE_RADIAL_FISHEYE", num_params=4), CameraModel(model_id=9, model_name="RADIAL_FISHEYE", num_params=5), CameraModel(model_id=10, model_name="THIN_PRISM_FISHEYE", num_params=12), } CAMERA_MODEL_IDS = dict( [(camera_model.model_id, camera_model) for camera_model in CAMERA_MODELS] ) CAMERA_MODEL_NAMES = dict( [(camera_model.model_name, camera_model) for camera_model in CAMERA_MODELS] ) def read_next_bytes(fid, num_bytes, format_char_sequence, endian_character="<"): """Read and unpack the next bytes from a binary file. :param fid: :param num_bytes: Sum of combination of {2, 4, 8}, e.g. 2, 6, 16, 30, etc. :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}. :param endian_character: Any of {@, =, <, >, !} :return: Tuple of read and unpacked values. """ data = fid.read(num_bytes) return struct.unpack(endian_character + format_char_sequence, data) def write_next_bytes(fid, data, format_char_sequence, endian_character="<"): """pack and write to a binary file. :param fid: :param data: data to send, if multiple elements are sent at the same time, they should be encapsuled either in a list or a tuple :param format_char_sequence: List of {c, e, f, d, h, H, i, I, l, L, q, Q}. should be the same length as the data list or tuple :param endian_character: Any of {@, =, <, >, !} """ if isinstance(data, (list, tuple)): bytes = struct.pack(endian_character + format_char_sequence, *data) else: bytes = struct.pack(endian_character + format_char_sequence, data) fid.write(bytes) def read_cameras_text(path): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::WriteCamerasText(const std::string& path) void Reconstruction::ReadCamerasText(const std::string& path) """ cameras = {} with open(path, "r") as fid: while True: line = fid.readline() if not line: break line = line.strip() if len(line) > 0 and line[0] != "#": elems = line.split() camera_id = int(elems[0]) model = elems[1] width = int(elems[2]) height = int(elems[3]) params = np.array(tuple(map(float, elems[4:]))) cameras[camera_id] = Camera( id=camera_id, model=model, width=width, height=height, params=params, ) return cameras def read_cameras_binary(path_to_model_file): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::WriteCamerasBinary(const std::string& path) void Reconstruction::ReadCamerasBinary(const std::string& path) """ cameras = {} with open(path_to_model_file, "rb") as fid: num_cameras = read_next_bytes(fid, 8, "Q")[0] for _ in range(num_cameras): camera_properties = read_next_bytes( fid, num_bytes=24, format_char_sequence="iiQQ" ) camera_id = camera_properties[0] model_id = camera_properties[1] model_name = CAMERA_MODEL_IDS[camera_properties[1]].model_name width = camera_properties[2] height = camera_properties[3] num_params = CAMERA_MODEL_IDS[model_id].num_params params = read_next_bytes( fid, num_bytes=8 * num_params, format_char_sequence="d" * num_params, ) cameras[camera_id] = Camera( id=camera_id, model=model_name, width=width, height=height, params=np.array(params), ) assert len(cameras) == num_cameras return cameras def write_cameras_text(cameras, path): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::WriteCamerasText(const std::string& path) void Reconstruction::ReadCamerasText(const std::string& path) """ HEADER = ( "# Camera list with one line of data per camera:\n" + "# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n" + "# Number of cameras: {}\n".format(len(cameras)) ) with open(path, "w") as fid: fid.write(HEADER) for _, cam in cameras.items(): to_write = [cam.id, cam.model, cam.width, cam.height, *cam.params] line = " ".join([str(elem) for elem in to_write]) fid.write(line + "\n") def write_cameras_binary(cameras, path_to_model_file): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::WriteCamerasBinary(const std::string& path) void Reconstruction::ReadCamerasBinary(const std::string& path) """ with open(path_to_model_file, "wb") as fid: write_next_bytes(fid, len(cameras), "Q") for _, cam in cameras.items(): model_id = CAMERA_MODEL_NAMES[cam.model].model_id camera_properties = [cam.id, model_id, cam.width, cam.height] write_next_bytes(fid, camera_properties, "iiQQ") for p in cam.params: write_next_bytes(fid, float(p), "d") return cameras def read_images_text(path): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadImagesText(const std::string& path) void Reconstruction::WriteImagesText(const std::string& path) """ images = {} with open(path, "r") as fid: while True: line = fid.readline() if not line: break line = line.strip() if len(line) > 0 and line[0] != "#": elems = line.split() image_id = int(elems[0]) qvec = np.array(tuple(map(float, elems[1:5]))) tvec = np.array(tuple(map(float, elems[5:8]))) camera_id = int(elems[8]) image_name = elems[9] elems = fid.readline().split() xys = np.column_stack( [ tuple(map(float, elems[0::3])), tuple(map(float, elems[1::3])), ] ) point3D_ids = np.array(tuple(map(int, elems[2::3]))) images[image_id] = Image( id=image_id, qvec=qvec, tvec=tvec, camera_id=camera_id, name=image_name, xys=xys, point3D_ids=point3D_ids, ) return images def read_images_binary(path_to_model_file): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadImagesBinary(const std::string& path) void Reconstruction::WriteImagesBinary(const std::string& path) """ images = {} with open(path_to_model_file, "rb") as fid: num_reg_images = read_next_bytes(fid, 8, "Q")[0] for _ in range(num_reg_images): binary_image_properties = read_next_bytes( fid, num_bytes=64, format_char_sequence="idddddddi" ) image_id = binary_image_properties[0] qvec = np.array(binary_image_properties[1:5]) tvec = np.array(binary_image_properties[5:8]) camera_id = binary_image_properties[8] image_name = "" current_char = read_next_bytes(fid, 1, "c")[0] while current_char != b"\x00": # look for the ASCII 0 entry image_name += current_char.decode("utf-8") current_char = read_next_bytes(fid, 1, "c")[0] num_points2D = read_next_bytes( fid, num_bytes=8, format_char_sequence="Q" )[0] x_y_id_s = read_next_bytes( fid, num_bytes=24 * num_points2D, format_char_sequence="ddq" * num_points2D, ) xys = np.column_stack( [ tuple(map(float, x_y_id_s[0::3])), tuple(map(float, x_y_id_s[1::3])), ] ) point3D_ids = np.array(tuple(map(int, x_y_id_s[2::3]))) images[image_id] = Image( id=image_id, qvec=qvec, tvec=tvec, camera_id=camera_id, name=image_name, xys=xys, point3D_ids=point3D_ids, ) return images def write_images_text(images, path): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadImagesText(const std::string& path) void Reconstruction::WriteImagesText(const std::string& path) """ if len(images) == 0: mean_observations = 0 else: mean_observations = sum( (len(img.point3D_ids) for _, img in images.items()) ) / len(images) HEADER = ( "# Image list with two lines of data per image:\n" + "# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n" + "# POINTS2D[] as (X, Y, POINT3D_ID)\n" + "# Number of images: {}, mean observations per image: {}\n".format( len(images), mean_observations ) ) with open(path, "w") as fid: fid.write(HEADER) for _, img in images.items(): image_header = [ img.id, *img.qvec, *img.tvec, img.camera_id, img.name, ] first_line = " ".join(map(str, image_header)) fid.write(first_line + "\n") points_strings = [] for xy, point3D_id in zip(img.xys, img.point3D_ids): points_strings.append(" ".join(map(str, [*xy, point3D_id]))) fid.write(" ".join(points_strings) + "\n") def write_images_binary(images, path_to_model_file): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadImagesBinary(const std::string& path) void Reconstruction::WriteImagesBinary(const std::string& path) """ with open(path_to_model_file, "wb") as fid: write_next_bytes(fid, len(images), "Q") for _, img in images.items(): write_next_bytes(fid, img.id, "i") write_next_bytes(fid, img.qvec.tolist(), "dddd") write_next_bytes(fid, img.tvec.tolist(), "ddd") write_next_bytes(fid, img.camera_id, "i") for char in img.name: write_next_bytes(fid, char.encode("utf-8"), "c") write_next_bytes(fid, b"\x00", "c") write_next_bytes(fid, len(img.point3D_ids), "Q") for xy, p3d_id in zip(img.xys, img.point3D_ids): write_next_bytes(fid, [*xy, p3d_id], "ddq") def read_points3D_text(path): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadPoints3DText(const std::string& path) void Reconstruction::WritePoints3DText(const std::string& path) """ points3D = {} with open(path, "r") as fid: while True: line = fid.readline() if not line: break line = line.strip() if len(line) > 0 and line[0] != "#": elems = line.split() point3D_id = int(elems[0]) xyz = np.array(tuple(map(float, elems[1:4]))) rgb = np.array(tuple(map(int, elems[4:7]))) error = float(elems[7]) image_ids = np.array(tuple(map(int, elems[8::2]))) point2D_idxs = np.array(tuple(map(int, elems[9::2]))) points3D[point3D_id] = Point3D( id=point3D_id, xyz=xyz, rgb=rgb, error=error, image_ids=image_ids, point2D_idxs=point2D_idxs, ) return points3D def read_points3D_binary(path_to_model_file): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadPoints3DBinary(const std::string& path) void Reconstruction::WritePoints3DBinary(const std::string& path) """ points3D = {} with open(path_to_model_file, "rb") as fid: num_points = read_next_bytes(fid, 8, "Q")[0] for _ in range(num_points): binary_point_line_properties = read_next_bytes( fid, num_bytes=43, format_char_sequence="QdddBBBd" ) point3D_id = binary_point_line_properties[0] xyz = np.array(binary_point_line_properties[1:4]) rgb = np.array(binary_point_line_properties[4:7]) error = np.array(binary_point_line_properties[7]) track_length = read_next_bytes( fid, num_bytes=8, format_char_sequence="Q" )[0] track_elems = read_next_bytes( fid, num_bytes=8 * track_length, format_char_sequence="ii" * track_length, ) image_ids = np.array(tuple(map(int, track_elems[0::2]))) point2D_idxs = np.array(tuple(map(int, track_elems[1::2]))) points3D[point3D_id] = Point3D( id=point3D_id, xyz=xyz, rgb=rgb, error=error, image_ids=image_ids, point2D_idxs=point2D_idxs, ) return points3D def write_points3D_text(points3D, path): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadPoints3DText(const std::string& path) void Reconstruction::WritePoints3DText(const std::string& path) """ if len(points3D) == 0: mean_track_length = 0 else: mean_track_length = sum( (len(pt.image_ids) for _, pt in points3D.items()) ) / len(points3D) HEADER = ( "# 3D point list with one line of data per point:\n" + "# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n" + "# Number of points: {}, mean track length: {}\n".format( len(points3D), mean_track_length ) ) with open(path, "w") as fid: fid.write(HEADER) for _, pt in points3D.items(): point_header = [pt.id, *pt.xyz, *pt.rgb, pt.error] fid.write(" ".join(map(str, point_header)) + " ") track_strings = [] for image_id, point2D in zip(pt.image_ids, pt.point2D_idxs): track_strings.append(" ".join(map(str, [image_id, point2D]))) fid.write(" ".join(track_strings) + "\n") def write_points3D_binary(points3D, path_to_model_file): """ see: src/colmap/scene/reconstruction.cc void Reconstruction::ReadPoints3DBinary(const std::string& path) void Reconstruction::WritePoints3DBinary(const std::string& path) """ with open(path_to_model_file, "wb") as fid: write_next_bytes(fid, len(points3D), "Q") for _, pt in points3D.items(): write_next_bytes(fid, pt.id, "Q") write_next_bytes(fid, pt.xyz.tolist(), "ddd") write_next_bytes(fid, pt.rgb.tolist(), "BBB") write_next_bytes(fid, pt.error, "d") track_length = pt.image_ids.shape[0] write_next_bytes(fid, track_length, "Q") for image_id, point2D_id in zip(pt.image_ids, pt.point2D_idxs): write_next_bytes(fid, [image_id, point2D_id], "ii") def detect_model_format(path, ext): if ( os.path.isfile(os.path.join(path, "cameras" + ext)) and os.path.isfile(os.path.join(path, "images" + ext)) and os.path.isfile(os.path.join(path, "points3D" + ext)) ): print("Detected model format: '" + ext + "'") return True return False def read_model(path, ext=""): # try to detect the extension automatically if ext == "": if detect_model_format(path, ".bin"): ext = ".bin" elif detect_model_format(path, ".txt"): ext = ".txt" else: print("Provide model format: '.bin' or '.txt'") return if ext == ".txt": cameras = read_cameras_text(os.path.join(path, "cameras" + ext)) images = read_images_text(os.path.join(path, "images" + ext)) points3D = read_points3D_text(os.path.join(path, "points3D") + ext) else: cameras = read_cameras_binary(os.path.join(path, "cameras" + ext)) images = read_images_binary(os.path.join(path, "images" + ext)) points3D = read_points3D_binary(os.path.join(path, "points3D") + ext) return cameras, images, points3D def write_model(cameras, images, points3D, path, ext=".bin"): if ext == ".txt": write_cameras_text(cameras, os.path.join(path, "cameras" + ext)) write_images_text(images, os.path.join(path, "images" + ext)) write_points3D_text(points3D, os.path.join(path, "points3D") + ext) else: write_cameras_binary(cameras, os.path.join(path, "cameras" + ext)) write_images_binary(images, os.path.join(path, "images" + ext)) write_points3D_binary(points3D, os.path.join(path, "points3D") + ext) return cameras, images, points3D def qvec2rotmat(qvec): return np.array( [ [ 1 - 2 * qvec[2] ** 2 - 2 * qvec[3] ** 2, 2 * qvec[1] * qvec[2] - 2 * qvec[0] * qvec[3], 2 * qvec[3] * qvec[1] + 2 * qvec[0] * qvec[2], ], [ 2 * qvec[1] * qvec[2] + 2 * qvec[0] * qvec[3], 1 - 2 * qvec[1] ** 2 - 2 * qvec[3] ** 2, 2 * qvec[2] * qvec[3] - 2 * qvec[0] * qvec[1], ], [ 2 * qvec[3] * qvec[1] - 2 * qvec[0] * qvec[2], 2 * qvec[2] * qvec[3] + 2 * qvec[0] * qvec[1], 1 - 2 * qvec[1] ** 2 - 2 * qvec[2] ** 2, ], ] ) def rotmat2qvec(R): Rxx, Ryx, Rzx, Rxy, Ryy, Rzy, Rxz, Ryz, Rzz = R.flat K = ( np.array( [ [Rxx - Ryy - Rzz, 0, 0, 0], [Ryx + Rxy, Ryy - Rxx - Rzz, 0, 0], [Rzx + Rxz, Rzy + Ryz, Rzz - Rxx - Ryy, 0], [Ryz - Rzy, Rzx - Rxz, Rxy - Ryx, Rxx + Ryy + Rzz], ] ) / 3.0 ) eigvals, eigvecs = np.linalg.eigh(K) qvec = eigvecs[[3, 0, 1, 2], np.argmax(eigvals)] if qvec[0] < 0: qvec *= -1 return qvec def main(): parser = argparse.ArgumentParser( description="Read and write COLMAP binary and text models" ) parser.add_argument("--input_model", help="path to input model folder") parser.add_argument( "--input_format", choices=[".bin", ".txt"], help="input model format", default="", ) parser.add_argument("--output_model", help="path to output model folder") parser.add_argument( "--output_format", choices=[".bin", ".txt"], help="outut model format", default=".txt", ) args = parser.parse_args() cameras, images, points3D = read_model( path=args.input_model, ext=args.input_format ) print("num_cameras:", len(cameras)) print("num_images:", len(images)) print("num_points3D:", len(points3D)) if args.output_model is not None: write_model( cameras, images, points3D, path=args.output_model, ext=args.output_format, ) if __name__ == "__main__": main() colmap-3.9.1/scripts/python/test_read_write_dense.py000077500000000000000000000046211454702036400227360ustar00rootroot00000000000000# 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. import numpy as np from read_write_dense import read_array, write_array def main(): import sys if len(sys.argv) != 3: print( "Usage: python test_read_write_dense.py " "path/to/dense/input.bin path/to/dense/output.bin" ) return print( "Checking consistency of reading and writing dense arrays " + "(depth maps / normal maps) ..." ) path_to_dense_input = sys.argv[1] path_to_dense_output = sys.argv[2] dense_input = read_array(path_to_dense_input) print("Input shape: " + str(dense_input.shape)) write_array(dense_input, path_to_dense_output) dense_output = read_array(path_to_dense_output) np.testing.assert_array_equal(dense_input, dense_output) print("... dense arrays are equal.") if __name__ == "__main__": main() colmap-3.9.1/scripts/python/test_read_write_fused_vis.py000077500000000000000000000052151454702036400236270ustar00rootroot00000000000000# 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. import filecmp from read_write_fused_vis import read_fused, write_fused def main(): import sys if len(sys.argv) != 5: print( "Usage: python test_read_write_fused_vis.py " "path/to/input_fused.ply path/to/input_fused.ply.vis " "path/to/output_fused.ply path/to/output_fused.ply.vis" ) return print( "Checking consistency of reading and writing fused.ply and fused.ply.vis files ..." ) path_to_fused_ply_input = sys.argv[1] path_to_fused_ply_vis_input = sys.argv[2] path_to_fused_ply_output = sys.argv[3] path_to_fused_ply_vis_output = sys.argv[4] mesh_points = read_fused( path_to_fused_ply_input, path_to_fused_ply_vis_input ) write_fused( mesh_points, path_to_fused_ply_output, path_to_fused_ply_vis_output ) assert filecmp.cmp(path_to_fused_ply_input, path_to_fused_ply_output) assert filecmp.cmp( path_to_fused_ply_vis_input, path_to_fused_ply_vis_output ) print("... Results are equal.") if __name__ == "__main__": main() colmap-3.9.1/scripts/python/test_read_write_model.py000077500000000000000000000112321454702036400227340ustar00rootroot00000000000000# 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. import numpy as np from read_write_model import read_model, write_model from tempfile import mkdtemp def compare_cameras(cameras1, cameras2): assert len(cameras1) == len(cameras2) for camera_id1 in cameras1: camera1 = cameras1[camera_id1] camera2 = cameras2[camera_id1] assert camera1.id == camera2.id assert camera1.width == camera2.width assert camera1.height == camera2.height assert np.allclose(camera1.params, camera2.params) def compare_images(images1, images2): assert len(images1) == len(images2) for image_id1 in images1: image1 = images1[image_id1] image2 = images2[image_id1] assert image1.id == image2.id assert np.allclose(image1.qvec, image2.qvec) assert np.allclose(image1.tvec, image2.tvec) assert image1.camera_id == image2.camera_id assert image1.name == image2.name assert np.allclose(image1.xys, image2.xys) assert np.array_equal(image1.point3D_ids, image2.point3D_ids) def compare_points(points3D1, points3D2): for point3D_id1 in points3D1: point3D1 = points3D1[point3D_id1] point3D2 = points3D2[point3D_id1] assert point3D1.id == point3D2.id assert np.allclose(point3D1.xyz, point3D2.xyz) assert np.array_equal(point3D1.rgb, point3D2.rgb) assert np.allclose(point3D1.error, point3D2.error) assert np.array_equal(point3D1.image_ids, point3D2.image_ids) assert np.array_equal(point3D1.point2D_idxs, point3D2.point2D_idxs) def main(): import sys if len(sys.argv) != 3: print( "Usage: python read_model.py " "path/to/model/folder/txt path/to/model/folder/bin" ) return print("Comparing text and binary models ...") path_to_model_txt_folder = sys.argv[1] path_to_model_bin_folder = sys.argv[2] cameras_txt, images_txt, points3D_txt = read_model( path_to_model_txt_folder, ext=".txt" ) cameras_bin, images_bin, points3D_bin = read_model( path_to_model_bin_folder, ext=".bin" ) compare_cameras(cameras_txt, cameras_bin) compare_images(images_txt, images_bin) compare_points(points3D_txt, points3D_bin) print("... text and binary models are equal.") print("Saving text model and reloading it ...") tmpdir = mkdtemp() write_model(cameras_bin, images_bin, points3D_bin, tmpdir, ext=".txt") cameras_txt, images_txt, points3D_txt = read_model(tmpdir, ext=".txt") compare_cameras(cameras_txt, cameras_bin) compare_images(images_txt, images_bin) compare_points(points3D_txt, points3D_bin) print("... saved text and loaded models are equal.") print("Saving binary model and reloading it ...") write_model(cameras_bin, images_bin, points3D_bin, tmpdir, ext=".bin") cameras_bin, images_bin, points3D_bin = read_model(tmpdir, ext=".bin") compare_cameras(cameras_txt, cameras_bin) compare_images(images_txt, images_bin) compare_points(points3D_txt, points3D_bin) print("... saved binary and loaded models are equal.") if __name__ == "__main__": main() colmap-3.9.1/scripts/python/visualize_model.py000077500000000000000000000160301454702036400215640ustar00rootroot00000000000000# 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. import argparse import numpy as np import open3d from read_write_model import read_model, write_model, qvec2rotmat, rotmat2qvec class Model: def __init__(self): self.cameras = [] self.images = [] self.points3D = [] self.__vis = None def read_model(self, path, ext=""): self.cameras, self.images, self.points3D = read_model(path, ext) def add_points(self, min_track_len=3, remove_statistical_outlier=True): pcd = open3d.geometry.PointCloud() xyz = [] rgb = [] for point3D in self.points3D.values(): track_len = len(point3D.point2D_idxs) if track_len < min_track_len: continue xyz.append(point3D.xyz) rgb.append(point3D.rgb / 255) pcd.points = open3d.utility.Vector3dVector(xyz) pcd.colors = open3d.utility.Vector3dVector(rgb) # remove obvious outliers if remove_statistical_outlier: [pcd, _] = pcd.remove_statistical_outlier( nb_neighbors=20, std_ratio=2.0 ) # open3d.visualization.draw_geometries([pcd]) self.__vis.add_geometry(pcd) self.__vis.poll_events() self.__vis.update_renderer() def add_cameras(self, scale=1): frames = [] for img in self.images.values(): # rotation R = qvec2rotmat(img.qvec) # translation t = img.tvec # invert t = -R.T @ t R = R.T # intrinsics cam = self.cameras[img.camera_id] if cam.model in ("SIMPLE_PINHOLE", "SIMPLE_RADIAL", "RADIAL"): fx = fy = cam.params[0] cx = cam.params[1] cy = cam.params[2] elif cam.model in ( "PINHOLE", "OPENCV", "OPENCV_FISHEYE", "FULL_OPENCV", ): fx = cam.params[0] fy = cam.params[1] cx = cam.params[2] cy = cam.params[3] else: raise Exception("Camera model not supported") # intrinsics K = np.identity(3) K[0, 0] = fx K[1, 1] = fy K[0, 2] = cx K[1, 2] = cy # create axis, plane and pyramed geometries that will be drawn cam_model = draw_camera(K, R, t, cam.width, cam.height, scale) frames.extend(cam_model) # add geometries to visualizer for i in frames: self.__vis.add_geometry(i) def create_window(self): self.__vis = open3d.visualization.Visualizer() self.__vis.create_window() def show(self): self.__vis.poll_events() self.__vis.update_renderer() self.__vis.run() self.__vis.destroy_window() def draw_camera(K, R, t, w, h, scale=1, color=[0.8, 0.2, 0.8]): """Create axis, plane and pyramed geometries in Open3D format. :param K: calibration matrix (camera intrinsics) :param R: rotation matrix :param t: translation :param w: image width :param h: image height :param scale: camera model scale :param color: color of the image plane and pyramid lines :return: camera model geometries (axis, plane and pyramid) """ # intrinsics K = K.copy() / scale Kinv = np.linalg.inv(K) # 4x4 transformation T = np.column_stack((R, t)) T = np.vstack((T, (0, 0, 0, 1))) # axis axis = open3d.geometry.TriangleMesh.create_coordinate_frame( size=0.5 * scale ) axis.transform(T) # points in pixel points_pixel = [ [0, 0, 0], [0, 0, 1], [w, 0, 1], [0, h, 1], [w, h, 1], ] # pixel to camera coordinate system points = [Kinv @ p for p in points_pixel] # image plane width = abs(points[1][0]) + abs(points[3][0]) height = abs(points[1][1]) + abs(points[3][1]) plane = open3d.geometry.TriangleMesh.create_box(width, height, depth=1e-6) plane.paint_uniform_color(color) plane.translate([points[1][0], points[1][1], scale]) plane.transform(T) # pyramid points_in_world = [(R @ p + t) for p in points] lines = [ [0, 1], [0, 2], [0, 3], [0, 4], ] colors = [color for i in range(len(lines))] line_set = open3d.geometry.LineSet( points=open3d.utility.Vector3dVector(points_in_world), lines=open3d.utility.Vector2iVector(lines), ) line_set.colors = open3d.utility.Vector3dVector(colors) # return as list in Open3D format return [axis, plane, line_set] def parse_args(): parser = argparse.ArgumentParser( description="Visualize COLMAP binary and text models" ) parser.add_argument( "--input_model", required=True, help="path to input model folder" ) parser.add_argument( "--input_format", choices=[".bin", ".txt"], help="input model format", default="", ) args = parser.parse_args() return args def main(): args = parse_args() # read COLMAP model model = Model() model.read_model(args.input_model, ext=args.input_format) print("num_cameras:", len(model.cameras)) print("num_images:", len(model.images)) print("num_points3D:", len(model.points3D)) # display using Open3D visualization tools model.create_window() model.add_points() model.add_cameras(scale=0.25) model.show() if __name__ == "__main__": main() colmap-3.9.1/scripts/shell/000077500000000000000000000000001454702036400156025ustar00rootroot00000000000000colmap-3.9.1/scripts/shell/build_mac_app.sh000077500000000000000000000064231454702036400207250ustar00rootroot00000000000000# 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. # This script creates a deployable package of COLMAP for Mac OS X. BASE_PATH=$(dirname $1) echo "Creating bundle directory" mkdir -p "$BASE_PATH/COLMAP.app/Contents/MacOS" echo "Copying binary" cp "$BASE_PATH/colmap" "$BASE_PATH/COLMAP.app/Contents/MacOS/colmap" echo "Writing Info.plist" cat <"$BASE_PATH/COLMAP.app/Contents/Info.plist" CFBundlePackageType APPL CFBundleExecutable colmap CFBundleIdentifier COLMAP CFBundleName COLMAP CFBundleDisplayName COLMAP NSHighResolutionCapable NSAppSleepDisabled EOM install_name_tool -change @rpath/libtbb.dylib /usr/local/lib/libtbb.dylib $BASE_PATH/COLMAP.app/Contents/MacOS/COLMAP install_name_tool -change @rpath/libtbbmalloc.dylib /usr/local/lib/libtbbmalloc.dylib $BASE_PATH/COLMAP.app/Contents/MacOS/COLMAP echo "Linking dynamic libraries" /usr/local/opt/qt5/bin/macdeployqt "$BASE_PATH/COLMAP.app" echo "Wrapping binary" cat <"$BASE_PATH/COLMAP.app/Contents/MacOS/colmap_gui.sh" #!/bin/bash script_path="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)" \$script_path/colmap gui EOM chmod +x $BASE_PATH/COLMAP.app/Contents/MacOS/colmap_gui.sh sed -i '' 's#colmap#colmap_gui.sh#g' $BASE_PATH/COLMAP.app/Contents/Info.plist echo "Compressing application" cd "$BASE_PATH" zip -r "COLMAP-mac.zip" "COLMAP.app" colmap-3.9.1/scripts/shell/colmap.bat000077500000000000000000000035271454702036400175570ustar00rootroot00000000000000@echo off rem Copyright (c) 2023, ETH Zurich and UNC Chapel Hill. rem All rights reserved. rem rem Redistribution and use in source and binary forms, with or without rem modification, are permitted provided that the following conditions are met: rem rem * Redistributions of source code must retain the above copyright rem notice, this list of conditions and the following disclaimer. rem rem * Redistributions in binary form must reproduce the above copyright rem notice, this list of conditions and the following disclaimer in the rem documentation and/or other materials provided with the distribution. rem rem * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of rem its contributors may be used to endorse or promote products derived rem from this software without specific prior written permission. rem rem THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" rem AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE rem IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE rem ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE rem LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR rem CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF rem SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS rem INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN rem CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) rem ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE rem POSSIBILITY OF SUCH DAMAGE. set SCRIPT_PATH=%~dp0 set PATH=%SCRIPT_PATH%\lib;%PATH% set QT_PLUGIN_PATH=%SCRIPT_PATH%\lib;%QT_PLUGIN_PATH% set ARGUMENTS=%* if "%ARGUMENTS%"=="" set ARGUMENTS=gui "%SCRIPT_PATH%\bin\colmap" %ARGUMENTS% colmap-3.9.1/scripts/shell/enter_vs_dev_shell.ps1000066400000000000000000000015031454702036400221000ustar00rootroot00000000000000if (!$env:VisualStudioDevShell) { $vswhere = "${Env:ProgramFiles(x86)}/Microsoft Visual Studio/Installer/vswhere.exe" if (!(Test-Path $vswhere)) { throw "Failed to find vswhere.exe" } & $vswhere -latest -format json $vsInstance = & $vswhere -latest -format json | ConvertFrom-Json if ($LASTEXITCODE) { throw "vswhere.exe returned exit code $LASTEXITCODE" } Import-Module "$($vsInstance.installationPath)/Common7/Tools/Microsoft.VisualStudio.DevShell.dll" $prevCwd = Get-Location try { Enter-VsDevShell $vsInstance.instanceId -DevCmdArguments "-no_logo -host_arch=amd64 -arch=amd64" } catch { Write-Host $_ Write-Error "Failed to enter Visual Studio Dev Shell" exit 1 } Set-Location $prevCwd $env:VisualStudioDevShell = $true } colmap-3.9.1/scripts/shell/images_to_video.sh000077500000000000000000000033161454702036400213010ustar00rootroot00000000000000# 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. # Command to produce video from images produced by COLMAP movie grabber tool. ffmpeg -i frame%06d.png -r 30 -vf scale=1680:1050 out.mp4 colmap-3.9.1/scripts/shell/restore_git_submodules.sh000077500000000000000000000035261454702036400227370ustar00rootroot00000000000000# 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. set -e git config -f .gitmodules --get-regexp '^submodule\..*\.path$' | while read path_key path do url_key=$(echo $path_key | sed 's/\.path/.url/') url=$(git config -f .gitmodules --get "$url_key") git submodule add -f $url $path done colmap-3.9.1/scripts/shell/run_tests.bat000077500000000000000000000035511454702036400203270ustar00rootroot00000000000000@echo off rem Copyright (c) 2023, ETH Zurich and UNC Chapel Hill. rem All rights reserved. rem rem Redistribution and use in source and binary forms, with or without rem modification, are permitted provided that the following conditions are met: rem rem * Redistributions of source code must retain the above copyright rem notice, this list of conditions and the following disclaimer. rem rem * Redistributions in binary form must reproduce the above copyright rem notice, this list of conditions and the following disclaimer in the rem documentation and/or other materials provided with the distribution. rem rem * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of rem its contributors may be used to endorse or promote products derived rem from this software without specific prior written permission. rem rem THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" rem AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE rem IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE rem ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE rem LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR rem CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF rem SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS rem INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN rem CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) rem ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE rem POSSIBILITY OF SUCH DAMAGE. set SCRIPT_PATH=%~dp0 set PATH=%SCRIPT_PATH%\lib;%PATH% set QT_PLUGIN_PATH=%SCRIPT_PATH%\lib;%QT_PLUGIN_PATH% @echo on for %%i in (%SCRIPT_PATH%\bin\*_test.exe) do ( %%i if %errorlevel% neq 0 goto end ) :end pause colmap-3.9.1/src/000077500000000000000000000000001454702036400135735ustar00rootroot00000000000000colmap-3.9.1/src/colmap/000077500000000000000000000000001454702036400150465ustar00rootroot00000000000000colmap-3.9.1/src/colmap/CMakeLists.txt000066400000000000000000000056151454702036400176150ustar00rootroot00000000000000# 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. if(IS_MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W3") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3") # Avoid pulling in too many header files through add_definitions("-DWIN32_LEAN_AND_MEAN") elseif(IS_GNU OR IS_CLANG) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") endif() if(CUDA_ENABLED) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --use_fast_math") # Use a separate stream per thread to allow for concurrent kernel execution # between multiple threads on the same device. set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --default-stream per-thread") # Suppress warnings: # ptxas warning : Stack size for entry function X cannot be statically determined. set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Xptxas=-suppress-stack-size-warning") # Fix for Ubuntu 16.04. add_definitions("-D_MWAITXINTRIN_H_INCLUDED") endif() add_subdirectory(controllers) add_subdirectory(estimators) add_subdirectory(exe) add_subdirectory(feature) add_subdirectory(geometry) add_subdirectory(image) add_subdirectory(math) add_subdirectory(mvs) add_subdirectory(optim) add_subdirectory(retrieval) add_subdirectory(scene) add_subdirectory(sensor) add_subdirectory(sfm) add_subdirectory(tools) add_subdirectory(util) if (GUI_ENABLED) add_subdirectory(ui) endif() colmap-3.9.1/src/colmap/controllers/000077500000000000000000000000001454702036400174145ustar00rootroot00000000000000colmap-3.9.1/src/colmap/controllers/CMakeLists.txt000066400000000000000000000053021454702036400221540ustar00rootroot00000000000000# 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. set(FOLDER_NAME "controllers") COLMAP_ADD_LIBRARY( NAME colmap_controllers SRCS automatic_reconstruction.h automatic_reconstruction.cc bundle_adjustment.h bundle_adjustment.cc hierarchical_mapper.h hierarchical_mapper.cc feature_extraction.h feature_extraction.cc feature_matching.h feature_matching.cc feature_matching_utils.h feature_matching_utils.cc image_reader.h image_reader.cc incremental_mapper.h incremental_mapper.cc option_manager.h option_manager.cc PUBLIC_LINK_LIBS colmap_scene colmap_util Eigen3::Eigen Boost::program_options PRIVATE_LINK_LIBS colmap_estimators colmap_feature colmap_image colmap_math colmap_mvs colmap_retrieval colmap_sfm Ceres::ceres Boost::filesystem Boost::boost ) COLMAP_ADD_TEST( NAME hierarchical_mapper_test SRCS hierarchical_mapper_test.cc LINK_LIBS colmap_controllers ) COLMAP_ADD_TEST( NAME incremental_mapper_test SRCS incremental_mapper_test.cc LINK_LIBS colmap_controllers ) colmap-3.9.1/src/colmap/controllers/automatic_reconstruction.cc000066400000000000000000000277451454702036400250710ustar00rootroot00000000000000// 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. #include "colmap/controllers/automatic_reconstruction.h" #include "colmap/controllers/feature_extraction.h" #include "colmap/controllers/feature_matching.h" #include "colmap/controllers/incremental_mapper.h" #include "colmap/controllers/option_manager.h" #include "colmap/image/undistortion.h" #include "colmap/mvs/fusion.h" #include "colmap/mvs/meshing.h" #include "colmap/mvs/patch_match.h" #include "colmap/util/misc.h" #include namespace colmap { AutomaticReconstructionController::AutomaticReconstructionController( const Options& options, std::shared_ptr reconstruction_manager) : options_(options), reconstruction_manager_(std::move(reconstruction_manager)), active_thread_(nullptr) { CHECK(ExistsDir(options_.workspace_path)); CHECK(ExistsDir(options_.image_path)); CHECK_NOTNULL(reconstruction_manager_); option_manager_.AddAllOptions(); *option_manager_.image_path = options_.image_path; *option_manager_.database_path = JoinPaths(options_.workspace_path, "database.db"); if (options_.data_type == DataType::VIDEO) { option_manager_.ModifyForVideoData(); } else if (options_.data_type == DataType::INDIVIDUAL) { option_manager_.ModifyForIndividualData(); } else if (options_.data_type == DataType::INTERNET) { option_manager_.ModifyForInternetData(); } else { LOG(FATAL) << "Data type not supported"; } CHECK(ExistsCameraModelWithName(options_.camera_model)); if (options_.quality == Quality::LOW) { option_manager_.ModifyForLowQuality(); } else if (options_.quality == Quality::MEDIUM) { option_manager_.ModifyForMediumQuality(); } else if (options_.quality == Quality::HIGH) { option_manager_.ModifyForHighQuality(); } else if (options_.quality == Quality::EXTREME) { option_manager_.ModifyForExtremeQuality(); } option_manager_.sift_extraction->num_threads = options_.num_threads; option_manager_.sift_matching->num_threads = options_.num_threads; option_manager_.mapper->num_threads = options_.num_threads; option_manager_.poisson_meshing->num_threads = options_.num_threads; ImageReaderOptions& reader_options = *option_manager_.image_reader; reader_options.database_path = *option_manager_.database_path; reader_options.image_path = *option_manager_.image_path; if (!options_.mask_path.empty()) { reader_options.mask_path = options_.mask_path; option_manager_.image_reader->mask_path = options_.mask_path; option_manager_.stereo_fusion->mask_path = options_.mask_path; } reader_options.single_camera = options_.single_camera; reader_options.single_camera_per_folder = options_.single_camera_per_folder; reader_options.camera_model = options_.camera_model; reader_options.camera_params = options_.camera_params; option_manager_.sift_extraction->use_gpu = options_.use_gpu; option_manager_.sift_matching->use_gpu = options_.use_gpu; option_manager_.sift_extraction->gpu_index = options_.gpu_index; option_manager_.sift_matching->gpu_index = options_.gpu_index; option_manager_.patch_match_stereo->gpu_index = options_.gpu_index; feature_extractor_ = CreateFeatureExtractorController( reader_options, *option_manager_.sift_extraction); exhaustive_matcher_ = CreateExhaustiveFeatureMatcher(*option_manager_.exhaustive_matching, *option_manager_.sift_matching, *option_manager_.two_view_geometry, *option_manager_.database_path); if (!options_.vocab_tree_path.empty()) { option_manager_.sequential_matching->loop_detection = true; option_manager_.sequential_matching->vocab_tree_path = options_.vocab_tree_path; } sequential_matcher_ = CreateSequentialFeatureMatcher(*option_manager_.sequential_matching, *option_manager_.sift_matching, *option_manager_.two_view_geometry, *option_manager_.database_path); if (!options_.vocab_tree_path.empty()) { option_manager_.vocab_tree_matching->vocab_tree_path = options_.vocab_tree_path; vocab_tree_matcher_ = CreateVocabTreeFeatureMatcher(*option_manager_.vocab_tree_matching, *option_manager_.sift_matching, *option_manager_.two_view_geometry, *option_manager_.database_path); } } void AutomaticReconstructionController::Stop() { if (active_thread_ != nullptr) { active_thread_->Stop(); } Thread::Stop(); } void AutomaticReconstructionController::Run() { if (IsStopped()) { return; } RunFeatureExtraction(); if (IsStopped()) { return; } RunFeatureMatching(); if (IsStopped()) { return; } if (options_.sparse) { RunSparseMapper(); } if (IsStopped()) { return; } if (options_.dense) { RunDenseMapper(); } } void AutomaticReconstructionController::RunFeatureExtraction() { CHECK(feature_extractor_); active_thread_ = feature_extractor_.get(); feature_extractor_->Start(); feature_extractor_->Wait(); feature_extractor_.reset(); active_thread_ = nullptr; } void AutomaticReconstructionController::RunFeatureMatching() { Thread* matcher = nullptr; if (options_.data_type == DataType::VIDEO) { matcher = sequential_matcher_.get(); } else if (options_.data_type == DataType::INDIVIDUAL || options_.data_type == DataType::INTERNET) { Database database(*option_manager_.database_path); const size_t num_images = database.NumImages(); if (options_.vocab_tree_path.empty() || num_images < 200) { matcher = exhaustive_matcher_.get(); } else { matcher = vocab_tree_matcher_.get(); } } CHECK(matcher); active_thread_ = matcher; matcher->Start(); matcher->Wait(); exhaustive_matcher_.reset(); sequential_matcher_.reset(); vocab_tree_matcher_.reset(); active_thread_ = nullptr; } void AutomaticReconstructionController::RunSparseMapper() { const auto sparse_path = JoinPaths(options_.workspace_path, "sparse"); if (ExistsDir(sparse_path)) { auto dir_list = GetDirList(sparse_path); std::sort(dir_list.begin(), dir_list.end()); if (dir_list.size() > 0) { LOG(WARNING) << "Skipping sparse reconstruction because it is already computed"; for (const auto& dir : dir_list) { reconstruction_manager_->Read(dir); } return; } } IncrementalMapperController mapper(option_manager_.mapper, *option_manager_.image_path, *option_manager_.database_path, reconstruction_manager_); active_thread_ = &mapper; mapper.Start(); mapper.Wait(); active_thread_ = nullptr; CreateDirIfNotExists(sparse_path); reconstruction_manager_->Write(sparse_path); option_manager_.Write(JoinPaths(sparse_path, "project.ini")); } void AutomaticReconstructionController::RunDenseMapper() { CreateDirIfNotExists(JoinPaths(options_.workspace_path, "dense")); for (size_t i = 0; i < reconstruction_manager_->Size(); ++i) { if (IsStopped()) { return; } const std::string dense_path = JoinPaths(options_.workspace_path, "dense", std::to_string(i)); const std::string fused_path = JoinPaths(dense_path, "fused.ply"); std::string meshing_path; if (options_.mesher == Mesher::POISSON) { meshing_path = JoinPaths(dense_path, "meshed-poisson.ply"); } else if (options_.mesher == Mesher::DELAUNAY) { meshing_path = JoinPaths(dense_path, "meshed-delaunay.ply"); } if (ExistsFile(fused_path) && ExistsFile(meshing_path)) { continue; } // Image undistortion. if (!ExistsDir(dense_path)) { CreateDirIfNotExists(dense_path); UndistortCameraOptions undistortion_options; undistortion_options.max_image_size = option_manager_.patch_match_stereo->max_image_size; COLMAPUndistorter undistorter(undistortion_options, *reconstruction_manager_->Get(i), *option_manager_.image_path, dense_path); active_thread_ = &undistorter; undistorter.Start(); undistorter.Wait(); active_thread_ = nullptr; } if (IsStopped()) { return; } // Patch match stereo. #if defined(COLMAP_CUDA_ENABLED) { mvs::PatchMatchController patch_match_controller( *option_manager_.patch_match_stereo, dense_path, "COLMAP", ""); active_thread_ = &patch_match_controller; patch_match_controller.Start(); patch_match_controller.Wait(); active_thread_ = nullptr; } #else // COLMAP_CUDA_ENABLED LOG(WARNING) << "Skipping patch match stereo because CUDA is not available"; return; #endif // COLMAP_CUDA_ENABLED if (IsStopped()) { return; } // Stereo fusion. if (!ExistsFile(fused_path)) { auto fusion_options = *option_manager_.stereo_fusion; const int num_reg_images = reconstruction_manager_->Get(i)->NumRegImages(); fusion_options.min_num_pixels = std::min(num_reg_images + 1, fusion_options.min_num_pixels); mvs::StereoFusion fuser( fusion_options, dense_path, "COLMAP", "", options_.quality == Quality::HIGH ? "geometric" : "photometric"); active_thread_ = &fuser; fuser.Start(); fuser.Wait(); active_thread_ = nullptr; LOG(INFO) << "Writing output: " << fused_path; WriteBinaryPlyPoints(fused_path, fuser.GetFusedPoints()); mvs::WritePointsVisibility(fused_path + ".vis", fuser.GetFusedPointsVisibility()); } if (IsStopped()) { return; } // Surface meshing. if (!ExistsFile(meshing_path)) { if (options_.mesher == Mesher::POISSON) { mvs::PoissonMeshing( *option_manager_.poisson_meshing, fused_path, meshing_path); } else if (options_.mesher == Mesher::DELAUNAY) { #if defined(COLMAP_CGAL_ENABLED) mvs::DenseDelaunayMeshing( *option_manager_.delaunay_meshing, dense_path, meshing_path); #else // COLMAP_CGAL_ENABLED LOG(WARNING) << "Skipping Delaunay meshing because CGAL is not available"; return; #endif // COLMAP_CGAL_ENABLED } } } } } // namespace colmap colmap-3.9.1/src/colmap/controllers/automatic_reconstruction.h000066400000000000000000000105071454702036400247170ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/reconstruction_manager.h" #include "colmap/util/threading.h" #include #include namespace colmap { class AutomaticReconstructionController : public Thread { public: enum class DataType { INDIVIDUAL, VIDEO, INTERNET }; enum class Quality { LOW, MEDIUM, HIGH, EXTREME }; enum class Mesher { POISSON, DELAUNAY }; struct Options { // The path to the workspace folder in which all results are stored. std::string workspace_path; // The path to the image folder which are used as input. std::string image_path; // The path to the mask folder which are used as input. std::string mask_path; // The path to the vocabulary tree for feature matching. std::string vocab_tree_path; // The type of input data used to choose optimal mapper settings. DataType data_type = DataType::INDIVIDUAL; // Whether to perform low- or high-quality reconstruction. Quality quality = Quality::HIGH; // Whether to use shared intrinsics or not. bool single_camera = false; // Whether to use shared intrinsics or not for all images in the same // sub-folder. bool single_camera_per_folder = false; // Which camera model to use for images. std::string camera_model = "SIMPLE_RADIAL"; // Initial camera params for all images. std::string camera_params; // Whether to perform sparse mapping. bool sparse = true; // Whether to perform dense mapping. #if defined(COLMAP_CUDA_ENABLED) bool dense = true; #else bool dense = false; #endif // The meshing algorithm to be used. Mesher mesher = Mesher::POISSON; // The number of threads to use in all stages. int num_threads = -1; // Whether to use the GPU in feature extraction and matching. bool use_gpu = true; // Index of the GPU used for GPU stages. For multi-GPU computation, // you should separate multiple GPU indices by comma, e.g., "0,1,2,3". // By default, all GPUs will be used in all stages. std::string gpu_index = "-1"; }; AutomaticReconstructionController( const Options& options, std::shared_ptr reconstruction_manager); void Stop() override; private: void Run() override; void RunFeatureExtraction(); void RunFeatureMatching(); void RunSparseMapper(); void RunDenseMapper(); const Options options_; OptionManager option_manager_; std::shared_ptr reconstruction_manager_; Thread* active_thread_; std::unique_ptr feature_extractor_; std::unique_ptr exhaustive_matcher_; std::unique_ptr sequential_matcher_; std::unique_ptr vocab_tree_matcher_; }; } // namespace colmap colmap-3.9.1/src/colmap/controllers/bundle_adjustment.cc000066400000000000000000000071461454702036400234420ustar00rootroot00000000000000// 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. #include "colmap/controllers/bundle_adjustment.h" #include "colmap/estimators/bundle_adjustment.h" #include "colmap/util/misc.h" #include namespace colmap { namespace { // Callback functor called after each bundle adjustment iteration. class BundleAdjustmentIterationCallback : public ceres::IterationCallback { public: explicit BundleAdjustmentIterationCallback(Thread* thread) : thread_(thread) {} virtual ceres::CallbackReturnType operator()( const ceres::IterationSummary& summary) { CHECK_NOTNULL(thread_); thread_->BlockIfPaused(); if (thread_->IsStopped()) { return ceres::SOLVER_TERMINATE_SUCCESSFULLY; } else { return ceres::SOLVER_CONTINUE; } } private: Thread* thread_; }; } // namespace BundleAdjustmentController::BundleAdjustmentController( const OptionManager& options, std::shared_ptr reconstruction) : options_(options), reconstruction_(std::move(reconstruction)) {} void BundleAdjustmentController::Run() { CHECK_NOTNULL(reconstruction_); PrintHeading1("Global bundle adjustment"); const std::vector& reg_image_ids = reconstruction_->RegImageIds(); if (reg_image_ids.size() < 2) { LOG(ERROR) << "Need at least two views."; return; } // Avoid degeneracies in bundle adjustment. reconstruction_->FilterObservationsWithNegativeDepth(); BundleAdjustmentOptions ba_options = *options_.bundle_adjustment; BundleAdjustmentIterationCallback iteration_callback(this); ba_options.solver_options.callbacks.push_back(&iteration_callback); // Configure bundle adjustment. BundleAdjustmentConfig ba_config; for (const image_t image_id : reg_image_ids) { ba_config.AddImage(image_id); } ba_config.SetConstantCamPose(reg_image_ids[0]); ba_config.SetConstantCamPositions(reg_image_ids[1], {0}); // Run bundle adjustment. BundleAdjuster bundle_adjuster(ba_options, ba_config); bundle_adjuster.Solve(reconstruction_.get()); GetTimer().PrintMinutes(); } } // namespace colmap colmap-3.9.1/src/colmap/controllers/bundle_adjustment.h000066400000000000000000000042121454702036400232730ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/threading.h" namespace colmap { // Class that controls the global bundle adjustment procedure. class BundleAdjustmentController : public Thread { public: BundleAdjustmentController(const OptionManager& options, std::shared_ptr reconstruction); private: void Run(); const OptionManager options_; std::shared_ptr reconstruction_; }; } // namespace colmap colmap-3.9.1/src/colmap/controllers/feature_extraction.cc000066400000000000000000000503331454702036400236220ustar00rootroot00000000000000// 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. #include "colmap/controllers/feature_extraction.h" #include "colmap/feature/sift.h" #include "colmap/scene/database.h" #include "colmap/util/cuda.h" #include "colmap/util/misc.h" #include "colmap/util/opengl_utils.h" #include namespace colmap { namespace { void ScaleKeypoints(const Bitmap& bitmap, const Camera& camera, FeatureKeypoints* keypoints) { if (static_cast(bitmap.Width()) != camera.width || static_cast(bitmap.Height()) != camera.height) { const float scale_x = static_cast(camera.width) / bitmap.Width(); const float scale_y = static_cast(camera.height) / bitmap.Height(); for (auto& keypoint : *keypoints) { keypoint.Rescale(scale_x, scale_y); } } } void MaskKeypoints(const Bitmap& mask, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors) { size_t out_index = 0; BitmapColor color; for (size_t i = 0; i < keypoints->size(); ++i) { if (!mask.GetPixel(static_cast(keypoints->at(i).x), static_cast(keypoints->at(i).y), &color) || color.r == 0) { // Delete this keypoint by not copying it to the output. } else { // Retain this keypoint by copying it to the output index (in case this // index differs from its current position). if (out_index != i) { keypoints->at(out_index) = keypoints->at(i); for (int col = 0; col < descriptors->cols(); ++col) { (*descriptors)(out_index, col) = (*descriptors)(i, col); } } out_index += 1; } } keypoints->resize(out_index); descriptors->conservativeResize(out_index, descriptors->cols()); } struct ImageData { ImageReader::Status status = ImageReader::Status::FAILURE; Camera camera; Image image; Bitmap bitmap; Bitmap mask; FeatureKeypoints keypoints; FeatureDescriptors descriptors; }; class ImageResizerThread : public Thread { public: ImageResizerThread(int max_image_size, JobQueue* input_queue, JobQueue* output_queue) : max_image_size_(max_image_size), input_queue_(input_queue), output_queue_(output_queue) {} private: void Run() override { while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& image_data = input_job.Data(); if (image_data.status == ImageReader::Status::SUCCESS) { if (static_cast(image_data.bitmap.Width()) > max_image_size_ || static_cast(image_data.bitmap.Height()) > max_image_size_) { // Fit the down-sampled version exactly into the max dimensions. const double scale = static_cast(max_image_size_) / std::max(image_data.bitmap.Width(), image_data.bitmap.Height()); const int new_width = static_cast(image_data.bitmap.Width() * scale); const int new_height = static_cast(image_data.bitmap.Height() * scale); image_data.bitmap.Rescale(new_width, new_height); } } output_queue_->Push(std::move(image_data)); } else { break; } } } const int max_image_size_; JobQueue* input_queue_; JobQueue* output_queue_; }; class SiftFeatureExtractorThread : public Thread { public: SiftFeatureExtractorThread(const SiftExtractionOptions& sift_options, const std::shared_ptr& camera_mask, JobQueue* input_queue, JobQueue* output_queue) : sift_options_(sift_options), camera_mask_(camera_mask), input_queue_(input_queue), output_queue_(output_queue) { CHECK(sift_options_.Check()); #if !defined(COLMAP_CUDA_ENABLED) if (sift_options_.use_gpu) { opengl_context_ = std::make_unique(); } #endif } private: void Run() override { if (sift_options_.use_gpu) { #if !defined(COLMAP_CUDA_ENABLED) CHECK(opengl_context_); CHECK(opengl_context_->MakeCurrent()); #endif } std::unique_ptr extractor = CreateSiftFeatureExtractor(sift_options_); if (extractor == nullptr) { LOG(ERROR) << "Failed to create feature extractor."; SignalInvalidSetup(); return; } SignalValidSetup(); while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& image_data = input_job.Data(); if (image_data.status == ImageReader::Status::SUCCESS) { if (extractor->Extract(image_data.bitmap, &image_data.keypoints, &image_data.descriptors)) { ScaleKeypoints( image_data.bitmap, image_data.camera, &image_data.keypoints); if (camera_mask_) { MaskKeypoints(*camera_mask_, &image_data.keypoints, &image_data.descriptors); } if (image_data.mask.Data()) { MaskKeypoints(image_data.mask, &image_data.keypoints, &image_data.descriptors); } } else { image_data.status = ImageReader::Status::FAILURE; } } image_data.bitmap.Deallocate(); output_queue_->Push(std::move(image_data)); } else { break; } } } const SiftExtractionOptions sift_options_; std::shared_ptr camera_mask_; std::unique_ptr opengl_context_; JobQueue* input_queue_; JobQueue* output_queue_; }; class FeatureWriterThread : public Thread { public: FeatureWriterThread(size_t num_images, Database* database, JobQueue* input_queue) : num_images_(num_images), database_(database), input_queue_(input_queue) {} private: void Run() override { size_t image_index = 0; while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& image_data = input_job.Data(); image_index += 1; LOG(INFO) << StringPrintf( "Processed file [%d/%d]", image_index, num_images_); LOG(INFO) << StringPrintf(" Name: %s", image_data.image.Name().c_str()); if (image_data.status == ImageReader::Status::IMAGE_EXISTS) { LOG(INFO) << " SKIP: Features for image already extracted."; } else if (image_data.status == ImageReader::Status::BITMAP_ERROR) { LOG(ERROR) << "Failed to read image file format."; } else if (image_data.status == ImageReader::Status::CAMERA_SINGLE_DIM_ERROR) { LOG(ERROR) << "Single camera specified, " "but images have different dimensions."; } else if (image_data.status == ImageReader::Status::CAMERA_EXIST_DIM_ERROR) { LOG(ERROR) << "Image previously processed, but current image " "has different dimensions."; } else if (image_data.status == ImageReader::Status::CAMERA_PARAM_ERROR) { LOG(ERROR) << "Camera has invalid parameters."; } else if (image_data.status == ImageReader::Status::FAILURE) { LOG(ERROR) << "Failed to extract features."; } if (image_data.status != ImageReader::Status::SUCCESS) { continue; } LOG(INFO) << StringPrintf(" Dimensions: %d x %d", image_data.camera.width, image_data.camera.height); LOG(INFO) << StringPrintf(" Camera: #%d - %s", image_data.camera.camera_id, image_data.camera.ModelName().c_str()); LOG(INFO) << StringPrintf( " Focal Length: %.2fpx%s", image_data.camera.MeanFocalLength(), image_data.camera.has_prior_focal_length ? " (Prior)" : ""); const Eigen::Vector3d& translation_prior = image_data.image.CamFromWorldPrior().translation; if (translation_prior.array().isFinite().any()) { LOG(INFO) << StringPrintf( " GPS: LAT=%.3f, LON=%.3f, ALT=%.3f", translation_prior.x(), translation_prior.y(), translation_prior.z()); } LOG(INFO) << StringPrintf(" Features: %d", image_data.keypoints.size()); DatabaseTransaction database_transaction(database_); if (image_data.image.ImageId() == kInvalidImageId) { image_data.image.SetImageId(database_->WriteImage(image_data.image)); } if (!database_->ExistsKeypoints(image_data.image.ImageId())) { database_->WriteKeypoints(image_data.image.ImageId(), image_data.keypoints); } if (!database_->ExistsDescriptors(image_data.image.ImageId())) { database_->WriteDescriptors(image_data.image.ImageId(), image_data.descriptors); } } else { break; } } } const size_t num_images_; Database* database_; JobQueue* input_queue_; }; // Feature extraction class to extract features for all images in a directory. class FeatureExtractorController : public Thread { public: FeatureExtractorController(const ImageReaderOptions& reader_options, const SiftExtractionOptions& sift_options) : reader_options_(reader_options), sift_options_(sift_options), database_(reader_options_.database_path), image_reader_(reader_options_, &database_) { CHECK(reader_options_.Check()); CHECK(sift_options_.Check()); std::shared_ptr camera_mask; if (!reader_options_.camera_mask_path.empty()) { camera_mask = std::make_shared(); if (!camera_mask->Read(reader_options_.camera_mask_path, /*as_rgb*/ false)) { LOG(ERROR) << "Cannot read camera mask file: " << reader_options_.camera_mask_path << ". No mask is going to be used."; camera_mask.reset(); } } const int num_threads = GetEffectiveNumThreads(sift_options_.num_threads); CHECK_GT(num_threads, 0); // Make sure that we only have limited number of objects in the queue to // avoid excess in memory usage since images and features take lots of // memory. const int kQueueSize = 1; resizer_queue_ = std::make_unique>(kQueueSize); extractor_queue_ = std::make_unique>(kQueueSize); writer_queue_ = std::make_unique>(kQueueSize); if (sift_options_.max_image_size > 0) { for (int i = 0; i < num_threads; ++i) { resizers_.emplace_back( std::make_unique(sift_options_.max_image_size, resizer_queue_.get(), extractor_queue_.get())); } } if (!sift_options_.domain_size_pooling && !sift_options_.estimate_affine_shape && sift_options_.use_gpu) { std::vector gpu_indices = CSVToVector(sift_options_.gpu_index); CHECK_GT(gpu_indices.size(), 0); #if defined(COLMAP_CUDA_ENABLED) if (gpu_indices.size() == 1 && gpu_indices[0] == -1) { const int num_cuda_devices = GetNumCudaDevices(); CHECK_GT(num_cuda_devices, 0); gpu_indices.resize(num_cuda_devices); std::iota(gpu_indices.begin(), gpu_indices.end(), 0); } #endif // COLMAP_CUDA_ENABLED auto sift_gpu_options = sift_options_; for (const auto& gpu_index : gpu_indices) { sift_gpu_options.gpu_index = std::to_string(gpu_index); extractors_.emplace_back( std::make_unique(sift_gpu_options, camera_mask, extractor_queue_.get(), writer_queue_.get())); } } else { if (sift_options_.num_threads == -1 && sift_options_.max_image_size == SiftExtractionOptions().max_image_size && sift_options_.first_octave == SiftExtractionOptions().first_octave) { LOG(WARNING) << "Your current options use the maximum number of " "threads on the machine to extract features. Extracting SIFT " "features on the CPU can consume a lot of RAM per thread for " "large images. Consider reducing the maximum image size and/or " "the first octave or manually limit the number of extraction " "threads. Ignore this warning, if your machine has sufficient " "memory for the current settings."; } auto custom_sift_options = sift_options_; custom_sift_options.use_gpu = false; for (int i = 0; i < num_threads; ++i) { extractors_.emplace_back( std::make_unique(custom_sift_options, camera_mask, extractor_queue_.get(), writer_queue_.get())); } } writer_ = std::make_unique( image_reader_.NumImages(), &database_, writer_queue_.get()); } private: void Run() override { PrintHeading1("Feature extraction"); for (auto& resizer : resizers_) { resizer->Start(); } for (auto& extractor : extractors_) { extractor->Start(); } writer_->Start(); for (auto& extractor : extractors_) { if (!extractor->CheckValidSetup()) { return; } } while (image_reader_.NextIndex() < image_reader_.NumImages()) { if (IsStopped()) { resizer_queue_->Stop(); extractor_queue_->Stop(); resizer_queue_->Clear(); extractor_queue_->Clear(); break; } ImageData image_data; image_data.status = image_reader_.Next(&image_data.camera, &image_data.image, &image_data.bitmap, &image_data.mask); if (image_data.status != ImageReader::Status::SUCCESS) { image_data.bitmap.Deallocate(); } if (sift_options_.max_image_size > 0) { CHECK(resizer_queue_->Push(std::move(image_data))); } else { CHECK(extractor_queue_->Push(std::move(image_data))); } } resizer_queue_->Wait(); resizer_queue_->Stop(); for (auto& resizer : resizers_) { resizer->Wait(); } extractor_queue_->Wait(); extractor_queue_->Stop(); for (auto& extractor : extractors_) { extractor->Wait(); } writer_queue_->Wait(); writer_queue_->Stop(); writer_->Wait(); GetTimer().PrintMinutes(); } const ImageReaderOptions reader_options_; const SiftExtractionOptions sift_options_; Database database_; ImageReader image_reader_; std::vector> resizers_; std::vector> extractors_; std::unique_ptr writer_; std::unique_ptr> resizer_queue_; std::unique_ptr> extractor_queue_; std::unique_ptr> writer_queue_; }; // Import features from text files. Each image must have a corresponding text // file with the same name and an additional ".txt" suffix. class FeatureImporterController : public Thread { public: FeatureImporterController(const ImageReaderOptions& reader_options, const std::string& import_path) : reader_options_(reader_options), import_path_(import_path) {} private: void Run() override { PrintHeading1("Feature import"); if (!ExistsDir(import_path_)) { LOG(ERROR) << "Import directory does not exist."; return; } Database database(reader_options_.database_path); ImageReader image_reader(reader_options_, &database); while (image_reader.NextIndex() < image_reader.NumImages()) { if (IsStopped()) { break; } LOG(INFO) << StringPrintf("Processing file [%d/%d]", image_reader.NextIndex() + 1, image_reader.NumImages()); // Load image data and possibly save camera to database. Camera camera; Image image; Bitmap bitmap; if (image_reader.Next(&camera, &image, &bitmap, nullptr) != ImageReader::Status::SUCCESS) { continue; } const std::string path = JoinPaths(import_path_, image.Name() + ".txt"); if (ExistsFile(path)) { FeatureKeypoints keypoints; FeatureDescriptors descriptors; LoadSiftFeaturesFromTextFile(path, &keypoints, &descriptors); LOG(INFO) << "Features: " << keypoints.size(); DatabaseTransaction database_transaction(&database); if (image.ImageId() == kInvalidImageId) { image.SetImageId(database.WriteImage(image)); } if (!database.ExistsKeypoints(image.ImageId())) { database.WriteKeypoints(image.ImageId(), keypoints); } if (!database.ExistsDescriptors(image.ImageId())) { database.WriteDescriptors(image.ImageId(), descriptors); } } else { LOG(INFO) << "SKIP: No features found at " << path; } } GetTimer().PrintMinutes(); } const ImageReaderOptions reader_options_; const std::string import_path_; }; } // namespace std::unique_ptr CreateFeatureExtractorController( const ImageReaderOptions& reader_options, const SiftExtractionOptions& sift_options) { return std::make_unique(reader_options, sift_options); } std::unique_ptr CreateFeatureImporterController( const ImageReaderOptions& reader_options, const std::string& import_path) { return std::make_unique(reader_options, import_path); } } // namespace colmap colmap-3.9.1/src/colmap/controllers/feature_extraction.h000066400000000000000000000044061454702036400234640ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/image_reader.h" #include "colmap/feature/sift.h" #include "colmap/util/threading.h" namespace colmap { // Reads images from a folder, extracts features, and writes them to database. std::unique_ptr CreateFeatureExtractorController( const ImageReaderOptions& reader_options, const SiftExtractionOptions& sift_options); // Import features from text files. Each image must have a corresponding text // file with the same name and an additional ".txt" suffix. std::unique_ptr CreateFeatureImporterController( const ImageReaderOptions& reader_options, const std::string& import_path); } // namespace colmap colmap-3.9.1/src/colmap/controllers/feature_matching.cc000066400000000000000000001202721454702036400232340ustar00rootroot00000000000000// 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. #include "colmap/controllers/feature_matching.h" #include "colmap/controllers/feature_matching_utils.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/utils.h" #include "colmap/geometry/gps.h" #include "colmap/retrieval/visual_index.h" #include "colmap/util/misc.h" #include #include namespace colmap { namespace { void PrintElapsedTime(const Timer& timer) { LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); } void IndexImagesInVisualIndex(const int num_threads, const int num_checks, const int max_num_features, const std::vector& image_ids, Thread* thread, FeatureMatcherCache* cache, retrieval::VisualIndex<>* visual_index) { retrieval::VisualIndex<>::IndexOptions index_options; index_options.num_threads = num_threads; index_options.num_checks = num_checks; for (size_t i = 0; i < image_ids.size(); ++i) { if (thread->IsStopped()) { return; } Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Indexing image [%d/%d]", i + 1, image_ids.size()) << std::flush; auto keypoints = *cache->GetKeypoints(image_ids[i]); auto descriptors = *cache->GetDescriptors(image_ids[i]); if (max_num_features > 0 && descriptors.rows() > max_num_features) { ExtractTopScaleFeatures(&keypoints, &descriptors, max_num_features); } visual_index->Add(index_options, image_ids[i], keypoints, descriptors); PrintElapsedTime(timer); } // Compute the TF-IDF weights, etc. visual_index->Prepare(); } void MatchNearestNeighborsInVisualIndex(const int num_threads, const int num_images, const int num_neighbors, const int num_checks, const int num_images_after_verification, const int max_num_features, const std::vector& image_ids, Thread* thread, FeatureMatcherCache* cache, retrieval::VisualIndex<>* visual_index, FeatureMatcherController* matcher) { struct Retrieval { image_t image_id = kInvalidImageId; std::vector image_scores; }; // Create a thread pool to retrieve the nearest neighbors. ThreadPool retrieval_thread_pool(num_threads); JobQueue retrieval_queue(num_threads); // The retrieval thread kernel function. Note that the descriptors should be // extracted outside of this function sequentially to avoid any concurrent // access to the database causing race conditions. retrieval::VisualIndex<>::QueryOptions query_options; query_options.max_num_images = num_images; query_options.num_neighbors = num_neighbors; query_options.num_checks = num_checks; query_options.num_images_after_verification = num_images_after_verification; auto QueryFunc = [&](const image_t image_id) { auto keypoints = *cache->GetKeypoints(image_id); auto descriptors = *cache->GetDescriptors(image_id); if (max_num_features > 0 && descriptors.rows() > max_num_features) { ExtractTopScaleFeatures(&keypoints, &descriptors, max_num_features); } Retrieval retrieval; retrieval.image_id = image_id; visual_index->Query( query_options, keypoints, descriptors, &retrieval.image_scores); CHECK(retrieval_queue.Push(std::move(retrieval))); }; // Initially, make all retrieval threads busy and continue with the matching. size_t image_idx = 0; const size_t init_num_tasks = std::min(image_ids.size(), 2 * retrieval_thread_pool.NumThreads()); for (; image_idx < init_num_tasks; ++image_idx) { retrieval_thread_pool.AddTask(QueryFunc, image_ids[image_idx]); } std::vector> image_pairs; // Pop the finished retrieval results and enqueue them for feature matching. for (size_t i = 0; i < image_ids.size(); ++i) { if (thread->IsStopped()) { retrieval_queue.Stop(); return; } Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Matching image [%d/%d]", i + 1, image_ids.size()) << std::flush; // Push the next image to the retrieval queue. if (image_idx < image_ids.size()) { retrieval_thread_pool.AddTask(QueryFunc, image_ids[image_idx]); image_idx += 1; } // Pop the next results from the retrieval queue. auto retrieval = retrieval_queue.Pop(); CHECK(retrieval.IsValid()); const auto& image_id = retrieval.Data().image_id; const auto& image_scores = retrieval.Data().image_scores; // Compose the image pairs from the scores. image_pairs.clear(); image_pairs.reserve(image_scores.size()); for (const auto image_score : image_scores) { image_pairs.emplace_back(image_id, image_score.image_id); } matcher->Match(image_pairs); PrintElapsedTime(timer); } } class ExhaustiveFeatureMatcher : public Thread { public: ExhaustiveFeatureMatcher(const ExhaustiveMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) : options_(options), matching_options_(matching_options), database_(database_path), cache_(5 * options_.block_size, &database_), matcher_(matching_options, geometry_options, &database_, &cache_) { CHECK(options.Check()); CHECK(matching_options.Check()); CHECK(geometry_options.Check()); } private: void Run() override { PrintHeading1("Exhaustive feature matching"); if (!matcher_.Setup()) { return; } cache_.Setup(); const std::vector image_ids = cache_.GetImageIds(); const size_t block_size = static_cast(options_.block_size); const size_t num_blocks = static_cast( std::ceil(static_cast(image_ids.size()) / block_size)); const size_t num_pairs_per_block = block_size * (block_size - 1) / 2; std::vector> image_pairs; image_pairs.reserve(num_pairs_per_block); for (size_t start_idx1 = 0; start_idx1 < image_ids.size(); start_idx1 += block_size) { const size_t end_idx1 = std::min(image_ids.size(), start_idx1 + block_size) - 1; for (size_t start_idx2 = 0; start_idx2 < image_ids.size(); start_idx2 += block_size) { const size_t end_idx2 = std::min(image_ids.size(), start_idx2 + block_size) - 1; if (IsStopped()) { GetTimer().PrintMinutes(); return; } Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Matching block [%d/%d, %d/%d]", start_idx1 / block_size + 1, num_blocks, start_idx2 / block_size + 1, num_blocks) << std::flush; image_pairs.clear(); for (size_t idx1 = start_idx1; idx1 <= end_idx1; ++idx1) { for (size_t idx2 = start_idx2; idx2 <= end_idx2; ++idx2) { const size_t block_id1 = idx1 % block_size; const size_t block_id2 = idx2 % block_size; if ((idx1 > idx2 && block_id1 <= block_id2) || (idx1 < idx2 && block_id1 < block_id2)) { // Avoid duplicate pairs image_pairs.emplace_back(image_ids[idx1], image_ids[idx2]); } } } DatabaseTransaction database_transaction(&database_); matcher_.Match(image_pairs); PrintElapsedTime(timer); } } GetTimer().PrintMinutes(); } const ExhaustiveMatchingOptions options_; const SiftMatchingOptions matching_options_; Database database_; FeatureMatcherCache cache_; FeatureMatcherController matcher_; }; } // namespace bool ExhaustiveMatchingOptions::Check() const { CHECK_OPTION_GT(block_size, 1); return true; } std::unique_ptr CreateExhaustiveFeatureMatcher( const ExhaustiveMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) { return std::make_unique( options, matching_options, geometry_options, database_path); } namespace { class SequentialFeatureMatcher : public Thread { public: SequentialFeatureMatcher(const SequentialMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) : options_(options), matching_options_(matching_options), database_(database_path), cache_(std::max(5 * options_.loop_detection_num_images, 5 * options_.overlap), &database_), matcher_(matching_options, geometry_options, &database_, &cache_) { CHECK(options.Check()); CHECK(matching_options.Check()); CHECK(geometry_options.Check()); } private: void Run() override { PrintHeading1("Sequential feature matching"); if (!matcher_.Setup()) { return; } cache_.Setup(); const std::vector ordered_image_ids = GetOrderedImageIds(); RunSequentialMatching(ordered_image_ids); if (options_.loop_detection) { RunLoopDetection(ordered_image_ids); } GetTimer().PrintMinutes(); } std::vector GetOrderedImageIds() const { const std::vector image_ids = cache_.GetImageIds(); std::vector ordered_images; ordered_images.reserve(image_ids.size()); for (const auto image_id : image_ids) { ordered_images.push_back(cache_.GetImage(image_id)); } std::sort(ordered_images.begin(), ordered_images.end(), [](const Image& image1, const Image& image2) { return image1.Name() < image2.Name(); }); std::vector ordered_image_ids; ordered_image_ids.reserve(image_ids.size()); for (const auto& image : ordered_images) { ordered_image_ids.push_back(image.ImageId()); } return ordered_image_ids; } void RunSequentialMatching(const std::vector& image_ids) { std::vector> image_pairs; image_pairs.reserve(options_.overlap); for (size_t image_idx1 = 0; image_idx1 < image_ids.size(); ++image_idx1) { if (IsStopped()) { return; } const auto image_id1 = image_ids.at(image_idx1); Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Matching image [%d/%d]", image_idx1 + 1, image_ids.size()) << std::flush; image_pairs.clear(); for (int i = 0; i < options_.overlap; ++i) { const size_t image_idx2 = image_idx1 + i; if (image_idx2 < image_ids.size()) { image_pairs.emplace_back(image_id1, image_ids.at(image_idx2)); if (options_.quadratic_overlap) { const size_t image_idx2_quadratic = image_idx1 + (1 << i); if (image_idx2_quadratic < image_ids.size()) { image_pairs.emplace_back(image_id1, image_ids.at(image_idx2_quadratic)); } } } else { break; } } DatabaseTransaction database_transaction(&database_); matcher_.Match(image_pairs); PrintElapsedTime(timer); } } void RunLoopDetection(const std::vector& image_ids) { // Read the pre-trained vocabulary tree from disk. retrieval::VisualIndex<> visual_index; visual_index.Read(options_.vocab_tree_path); // Index all images in the visual index. IndexImagesInVisualIndex(matching_options_.num_threads, options_.loop_detection_num_checks, options_.loop_detection_max_num_features, image_ids, this, &cache_, &visual_index); if (IsStopped()) { return; } // Only perform loop detection for every n-th image. std::vector match_image_ids; for (size_t i = 0; i < image_ids.size(); i += options_.loop_detection_period) { match_image_ids.push_back(image_ids[i]); } MatchNearestNeighborsInVisualIndex( matching_options_.num_threads, options_.loop_detection_num_images, options_.loop_detection_num_nearest_neighbors, options_.loop_detection_num_checks, options_.loop_detection_num_images_after_verification, options_.loop_detection_max_num_features, match_image_ids, this, &cache_, &visual_index, &matcher_); } const SequentialMatchingOptions options_; const SiftMatchingOptions matching_options_; Database database_; FeatureMatcherCache cache_; FeatureMatcherController matcher_; }; } // namespace bool SequentialMatchingOptions::Check() const { CHECK_OPTION_GT(overlap, 0); CHECK_OPTION_GT(loop_detection_period, 0); CHECK_OPTION_GT(loop_detection_num_images, 0); CHECK_OPTION_GT(loop_detection_num_nearest_neighbors, 0); CHECK_OPTION_GT(loop_detection_num_checks, 0); return true; } std::unique_ptr CreateSequentialFeatureMatcher( const SequentialMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) { return std::make_unique( options, matching_options, geometry_options, database_path); } namespace { class VocabTreeFeatureMatcher : public Thread { public: VocabTreeFeatureMatcher(const VocabTreeMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) : options_(options), matching_options_(matching_options), database_(database_path), cache_(5 * options_.num_images, &database_), matcher_(matching_options, geometry_options, &database_, &cache_) { CHECK(options.Check()); CHECK(matching_options.Check()); CHECK(geometry_options.Check()); } private: void Run() override { PrintHeading1("Vocabulary tree feature matching"); if (!matcher_.Setup()) { return; } cache_.Setup(); // Read the pre-trained vocabulary tree from disk. retrieval::VisualIndex<> visual_index; visual_index.Read(options_.vocab_tree_path); const std::vector all_image_ids = cache_.GetImageIds(); std::vector image_ids; if (options_.match_list_path == "") { image_ids = cache_.GetImageIds(); } else { // Map image names to image identifiers. std::unordered_map image_name_to_image_id; image_name_to_image_id.reserve(all_image_ids.size()); for (const auto image_id : all_image_ids) { const auto& image = cache_.GetImage(image_id); image_name_to_image_id.emplace(image.Name(), image_id); } // Read the match list path. std::ifstream file(options_.match_list_path); CHECK(file.is_open()) << options_.match_list_path; std::string line; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } if (image_name_to_image_id.count(line) == 0) { LOG(ERROR) << "Image " << line << " does not exist."; } else { image_ids.push_back(image_name_to_image_id.at(line)); } } } // Index all images in the visual index. IndexImagesInVisualIndex(matching_options_.num_threads, options_.num_checks, options_.max_num_features, all_image_ids, this, &cache_, &visual_index); if (IsStopped()) { GetTimer().PrintMinutes(); return; } // Match all images in the visual index. MatchNearestNeighborsInVisualIndex(matching_options_.num_threads, options_.num_images, options_.num_nearest_neighbors, options_.num_checks, options_.num_images_after_verification, options_.max_num_features, image_ids, this, &cache_, &visual_index, &matcher_); GetTimer().PrintMinutes(); } const VocabTreeMatchingOptions options_; const SiftMatchingOptions matching_options_; Database database_; FeatureMatcherCache cache_; FeatureMatcherController matcher_; }; } // namespace bool VocabTreeMatchingOptions::Check() const { CHECK_OPTION_GT(num_images, 0); CHECK_OPTION_GT(num_nearest_neighbors, 0); CHECK_OPTION_GT(num_checks, 0); return true; } std::unique_ptr CreateVocabTreeFeatureMatcher( const VocabTreeMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) { return std::make_unique( options, matching_options, geometry_options, database_path); } namespace { class SpatialFeatureMatcher : public Thread { public: SpatialFeatureMatcher(const SpatialMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) : options_(options), matching_options_(matching_options), database_(database_path), cache_(5 * options_.max_num_neighbors, &database_), matcher_(matching_options, geometry_options, &database_, &cache_) { CHECK(options.Check()); CHECK(matching_options.Check()); CHECK(geometry_options.Check()); } private: void Run() override { PrintHeading1("Spatial feature matching"); if (!matcher_.Setup()) { return; } cache_.Setup(); const std::vector image_ids = cache_.GetImageIds(); ////////////////////////////////////////////////////////////////////////////// // Spatial indexing ////////////////////////////////////////////////////////////////////////////// Timer timer; timer.Start(); LOG(INFO) << "Indexing images..." << std::flush; GPSTransform gps_transform; size_t num_locations = 0; Eigen::Matrix location_matrix( image_ids.size(), 3); std::vector location_idxs; location_idxs.reserve(image_ids.size()); std::vector ells(1); for (size_t i = 0; i < image_ids.size(); ++i) { const auto image_id = image_ids[i]; const auto& image = cache_.GetImage(image_id); const Eigen::Vector3d& translation_prior = image.CamFromWorldPrior().translation; if ((translation_prior(0) == 0 && translation_prior(1) == 0 && options_.ignore_z) || (translation_prior(0) == 0 && translation_prior(1) == 0 && translation_prior(2) == 0 && !options_.ignore_z)) { continue; } location_idxs.push_back(i); if (options_.is_gps) { ells[0](0) = translation_prior(0); ells[0](1) = translation_prior(1); ells[0](2) = options_.ignore_z ? 0 : translation_prior(2); const auto xyzs = gps_transform.EllToXYZ(ells); location_matrix(num_locations, 0) = static_cast(xyzs[0](0)); location_matrix(num_locations, 1) = static_cast(xyzs[0](1)); location_matrix(num_locations, 2) = static_cast(xyzs[0](2)); } else { location_matrix(num_locations, 0) = static_cast(translation_prior(0)); location_matrix(num_locations, 1) = static_cast(translation_prior(1)); location_matrix(num_locations, 2) = static_cast(options_.ignore_z ? 0 : translation_prior(2)); } num_locations += 1; } PrintElapsedTime(timer); if (num_locations == 0) { LOG(INFO) << "=> No images with location data."; GetTimer().PrintMinutes(); return; } ////////////////////////////////////////////////////////////////////////////// // Building spatial index ////////////////////////////////////////////////////////////////////////////// timer.Restart(); LOG(INFO) << "Building search index..." << std::flush; flann::Matrix locations( location_matrix.data(), num_locations, location_matrix.cols()); flann::LinearIndexParams index_params; flann::LinearIndex> search_index(index_params); search_index.buildIndex(locations); PrintElapsedTime(timer); ////////////////////////////////////////////////////////////////////////////// // Searching spatial index ////////////////////////////////////////////////////////////////////////////// timer.Restart(); LOG(INFO) << "Searching for nearest neighbors..." << std::flush; const int knn = std::min(options_.max_num_neighbors, num_locations); Eigen::Matrix index_matrix(num_locations, knn); flann::Matrix indices(index_matrix.data(), num_locations, knn); Eigen::Matrix distance_matrix(num_locations, knn); flann::Matrix distances(distance_matrix.data(), num_locations, knn); flann::SearchParams search_params(flann::FLANN_CHECKS_AUTOTUNED); if (matching_options_.num_threads == ThreadPool::kMaxNumThreads) { search_params.cores = std::thread::hardware_concurrency(); } else { search_params.cores = matching_options_.num_threads; } if (search_params.cores <= 0) { search_params.cores = 1; } search_index.knnSearch(locations, indices, distances, knn, search_params); PrintElapsedTime(timer); ////////////////////////////////////////////////////////////////////////////// // Matching ////////////////////////////////////////////////////////////////////////////// const float max_distance = static_cast(options_.max_distance * options_.max_distance); std::vector> image_pairs; image_pairs.reserve(knn); for (size_t i = 0; i < num_locations; ++i) { if (IsStopped()) { GetTimer().PrintMinutes(); return; } timer.Restart(); LOG(INFO) << StringPrintf("Matching image [%d/%d]", i + 1, num_locations) << std::flush; image_pairs.clear(); for (int j = 0; j < knn; ++j) { // Check if query equals result. if (index_matrix(i, j) == i) { continue; } // Since the nearest neighbors are sorted by distance, we can break. if (distance_matrix(i, j) > max_distance) { break; } const size_t idx = location_idxs[i]; const image_t image_id = image_ids.at(idx); const size_t nn_idx = location_idxs.at(index_matrix(i, j)); const image_t nn_image_id = image_ids.at(nn_idx); image_pairs.emplace_back(image_id, nn_image_id); } DatabaseTransaction database_transaction(&database_); matcher_.Match(image_pairs); PrintElapsedTime(timer); } GetTimer().PrintMinutes(); } const SpatialMatchingOptions options_; const SiftMatchingOptions matching_options_; Database database_; FeatureMatcherCache cache_; FeatureMatcherController matcher_; }; } // namespace bool SpatialMatchingOptions::Check() const { CHECK_OPTION_GT(max_num_neighbors, 0); CHECK_OPTION_GT(max_distance, 0.0); return true; } std::unique_ptr CreateSpatialFeatureMatcher( const SpatialMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) { return std::make_unique( options, matching_options, geometry_options, database_path); } namespace { class TransitiveFeatureMatcher : public Thread { public: TransitiveFeatureMatcher(const TransitiveMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) : options_(options), matching_options_(matching_options), database_(database_path), cache_(options_.batch_size, &database_), matcher_(matching_options, geometry_options, &database_, &cache_) { CHECK(options.Check()); CHECK(matching_options.Check()); CHECK(geometry_options.Check()); } private: void Run() override { PrintHeading1("Transitive feature matching"); if (!matcher_.Setup()) { return; } cache_.Setup(); const std::vector image_ids = cache_.GetImageIds(); std::vector> image_pairs; std::unordered_set image_pair_ids; for (int iteration = 0; iteration < options_.num_iterations; ++iteration) { if (IsStopped()) { GetTimer().PrintMinutes(); return; } Timer timer; timer.Start(); LOG(INFO) << StringPrintf( "Iteration [%d/%d]", iteration + 1, options_.num_iterations); std::vector> existing_image_pairs; std::vector existing_num_inliers; database_.ReadTwoViewGeometryNumInliers(&existing_image_pairs, &existing_num_inliers); CHECK_EQ(existing_image_pairs.size(), existing_num_inliers.size()); std::unordered_map> adjacency; for (const auto& image_pair : existing_image_pairs) { adjacency[image_pair.first].push_back(image_pair.second); adjacency[image_pair.second].push_back(image_pair.first); } const size_t batch_size = static_cast(options_.batch_size); size_t num_batches = 0; image_pairs.clear(); image_pair_ids.clear(); for (const auto& image : adjacency) { const auto image_id1 = image.first; for (const auto& image_id2 : image.second) { if (adjacency.count(image_id2) > 0) { for (const auto& image_id3 : adjacency.at(image_id2)) { const auto image_pair_id = Database::ImagePairToPairId(image_id1, image_id3); if (image_pair_ids.count(image_pair_id) == 0) { image_pairs.emplace_back(image_id1, image_id3); image_pair_ids.insert(image_pair_id); if (image_pairs.size() >= batch_size) { num_batches += 1; LOG(INFO) << StringPrintf(" Batch %d", num_batches) << std::flush; DatabaseTransaction database_transaction(&database_); matcher_.Match(image_pairs); image_pairs.clear(); PrintElapsedTime(timer); timer.Restart(); if (IsStopped()) { GetTimer().PrintMinutes(); return; } } } } } } } num_batches += 1; LOG(INFO) << StringPrintf(" Batch %d", num_batches) << std::flush; DatabaseTransaction database_transaction(&database_); matcher_.Match(image_pairs); PrintElapsedTime(timer); } GetTimer().PrintMinutes(); } const TransitiveMatchingOptions options_; const SiftMatchingOptions matching_options_; Database database_; FeatureMatcherCache cache_; FeatureMatcherController matcher_; }; } // namespace bool TransitiveMatchingOptions::Check() const { CHECK_OPTION_GT(batch_size, 0); CHECK_OPTION_GT(num_iterations, 0); return true; } std::unique_ptr CreateTransitiveFeatureMatcher( const TransitiveMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) { return std::make_unique( options, matching_options, geometry_options, database_path); } namespace { class ImagePairsFeatureMatcher : public Thread { public: ImagePairsFeatureMatcher(const ImagePairsMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) : options_(options), matching_options_(matching_options), database_(database_path), cache_(options.block_size, &database_), matcher_(matching_options, geometry_options, &database_, &cache_) { CHECK(options.Check()); CHECK(matching_options.Check()); CHECK(geometry_options.Check()); } private: void Run() override { PrintHeading1("Custom feature matching"); if (!matcher_.Setup()) { return; } cache_.Setup(); ////////////////////////////////////////////////////////////////////////////// // Reading image pairs list ////////////////////////////////////////////////////////////////////////////// std::unordered_map image_name_to_image_id; image_name_to_image_id.reserve(cache_.GetImageIds().size()); for (const auto image_id : cache_.GetImageIds()) { const auto& image = cache_.GetImage(image_id); image_name_to_image_id.emplace(image.Name(), image_id); } std::ifstream file(options_.match_list_path); CHECK(file.is_open()) << options_.match_list_path; std::string line; std::vector> image_pairs; std::unordered_set image_pairs_set; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } std::stringstream line_stream(line); std::string image_name1; std::string image_name2; std::getline(line_stream, image_name1, ' '); StringTrim(&image_name1); std::getline(line_stream, image_name2, ' '); StringTrim(&image_name2); if (image_name_to_image_id.count(image_name1) == 0) { LOG(ERROR) << "Image " << image_name1 << " does not exist."; continue; } if (image_name_to_image_id.count(image_name2) == 0) { LOG(ERROR) << "Image " << image_name2 << " does not exist."; continue; } const image_t image_id1 = image_name_to_image_id.at(image_name1); const image_t image_id2 = image_name_to_image_id.at(image_name2); const image_pair_t image_pair = Database::ImagePairToPairId(image_id1, image_id2); const bool image_pair_exists = image_pairs_set.insert(image_pair).second; if (image_pair_exists) { image_pairs.emplace_back(image_id1, image_id2); } } ////////////////////////////////////////////////////////////////////////////// // Feature matching ////////////////////////////////////////////////////////////////////////////// const size_t num_match_blocks = image_pairs.size() / options_.block_size + 1; std::vector> block_image_pairs; block_image_pairs.reserve(options_.block_size); for (size_t i = 0; i < image_pairs.size(); i += options_.block_size) { if (IsStopped()) { GetTimer().PrintMinutes(); return; } Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Matching block [%d/%d]", i / options_.block_size + 1, num_match_blocks) << std::flush; const size_t block_end = i + options_.block_size <= image_pairs.size() ? i + options_.block_size : image_pairs.size(); std::vector> block_image_pairs; block_image_pairs.reserve(options_.block_size); for (size_t j = i; j < block_end; ++j) { block_image_pairs.push_back(image_pairs[j]); } DatabaseTransaction database_transaction(&database_); matcher_.Match(block_image_pairs); PrintElapsedTime(timer); } GetTimer().PrintMinutes(); } const ImagePairsMatchingOptions options_; const SiftMatchingOptions matching_options_; Database database_; FeatureMatcherCache cache_; FeatureMatcherController matcher_; }; } // namespace bool ImagePairsMatchingOptions::Check() const { CHECK_OPTION_GT(block_size, 0); return true; } std::unique_ptr CreateImagePairsFeatureMatcher( const ImagePairsMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) { return std::make_unique( options, matching_options, geometry_options, database_path); } namespace { class FeaturePairsFeatureMatcher : public Thread { public: FeaturePairsFeatureMatcher(const FeaturePairsMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) : options_(options), matching_options_(matching_options), geometry_options_(geometry_options), database_(database_path), cache_(kCacheSize, &database_) { CHECK(options.Check()); CHECK(matching_options.Check()); CHECK(geometry_options.Check()); } private: const static size_t kCacheSize = 100; void Run() override { PrintHeading1("Importing matches"); cache_.Setup(); std::unordered_map image_name_to_image; image_name_to_image.reserve(cache_.GetImageIds().size()); for (const auto image_id : cache_.GetImageIds()) { const auto& image = cache_.GetImage(image_id); image_name_to_image.emplace(image.Name(), &image); } std::ifstream file(options_.match_list_path); CHECK(file.is_open()) << options_.match_list_path; std::string line; while (std::getline(file, line)) { if (IsStopped()) { GetTimer().PrintMinutes(); return; } StringTrim(&line); if (line.empty()) { continue; } std::istringstream line_stream(line); std::string image_name1, image_name2; try { line_stream >> image_name1 >> image_name2; } catch (...) { LOG(ERROR) << "Could not read image pair."; break; } LOG(INFO) << StringPrintf( "%s - %s", image_name1.c_str(), image_name2.c_str()); if (image_name_to_image.count(image_name1) == 0) { LOG(INFO) << StringPrintf("SKIP: Image %s not found in database.", image_name1.c_str()); break; } if (image_name_to_image.count(image_name2) == 0) { LOG(INFO) << StringPrintf("SKIP: Image %s not found in database.", image_name2.c_str()); break; } const Image& image1 = *image_name_to_image[image_name1]; const Image& image2 = *image_name_to_image[image_name2]; bool skip_pair = false; if (database_.ExistsInlierMatches(image1.ImageId(), image2.ImageId())) { LOG(INFO) << "SKIP: Matches for image pair already exist in database."; skip_pair = true; } FeatureMatches matches; while (std::getline(file, line)) { StringTrim(&line); if (line.empty()) { break; } std::istringstream line_stream(line); FeatureMatch match; try { line_stream >> match.point2D_idx1 >> match.point2D_idx2; } catch (...) { LOG(ERROR) << "Cannot read feature matches."; break; } matches.push_back(match); } if (skip_pair) { continue; } const Camera& camera1 = cache_.GetCamera(image1.CameraId()); const Camera& camera2 = cache_.GetCamera(image2.CameraId()); if (options_.verify_matches) { database_.WriteMatches(image1.ImageId(), image2.ImageId(), matches); const auto keypoints1 = cache_.GetKeypoints(image1.ImageId()); const auto keypoints2 = cache_.GetKeypoints(image2.ImageId()); TwoViewGeometry two_view_geometry = EstimateTwoViewGeometry(camera1, FeatureKeypointsToPointsVector(*keypoints1), camera2, FeatureKeypointsToPointsVector(*keypoints2), matches, geometry_options_); database_.WriteTwoViewGeometry( image1.ImageId(), image2.ImageId(), two_view_geometry); } else { TwoViewGeometry two_view_geometry; if (camera1.has_prior_focal_length && camera2.has_prior_focal_length) { two_view_geometry.config = TwoViewGeometry::CALIBRATED; } else { two_view_geometry.config = TwoViewGeometry::UNCALIBRATED; } two_view_geometry.inlier_matches = matches; database_.WriteTwoViewGeometry( image1.ImageId(), image2.ImageId(), two_view_geometry); } } GetTimer().PrintMinutes(); } const FeaturePairsMatchingOptions options_; const SiftMatchingOptions matching_options_; const TwoViewGeometryOptions geometry_options_; Database database_; FeatureMatcherCache cache_; }; } // namespace bool FeaturePairsMatchingOptions::Check() const { return true; } std::unique_ptr CreateFeaturePairsFeatureMatcher( const FeaturePairsMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path) { return std::make_unique( options, matching_options, geometry_options, database_path); } } // namespace colmap colmap-3.9.1/src/colmap/controllers/feature_matching.h000066400000000000000000000234421454702036400230770ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/sift.h" #include "colmap/util/threading.h" #include #include namespace colmap { struct ExhaustiveMatchingOptions { // Block size, i.e. number of images to simultaneously load into memory. int block_size = 50; bool Check() const; }; // Exhaustively match images by processing each block in the exhaustive match // matrix in one batch: // // +----+----+-----------------> images[i] // |#000|0000| // |1#00|1000| <- Above the main diagonal, the block diagonal is not matched // |11#0|1100| ^ // |111#|1110| | // +----+----+ | // |1000|#000|\ | // |1100|1#00| \ One block | // |1110|11#0| / of image pairs | // |1111|111#|/ | // +----+----+ | // | ^ | // | | | // | Below the main diagonal, the block diagonal is matched <--------------+ // | // v // images[i] // // Pairs will only be matched if 1, to avoid duplicate pairs. Pairs with # // are on the main diagonal and denote pairs of the same image. std::unique_ptr CreateExhaustiveFeatureMatcher( const ExhaustiveMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path); struct SequentialMatchingOptions { // Number of overlapping image pairs. int overlap = 10; // Whether to match images against their quadratic neighbors. bool quadratic_overlap = true; // Whether to enable vocabulary tree based loop detection. bool loop_detection = false; // Loop detection is invoked every `loop_detection_period` images. int loop_detection_period = 10; // The number of images to retrieve in loop detection. This number should // be significantly bigger than the sequential matching overlap. int loop_detection_num_images = 50; // Number of nearest neighbors to retrieve per query feature. int loop_detection_num_nearest_neighbors = 1; // Number of nearest-neighbor checks to use in retrieval. int loop_detection_num_checks = 256; // How many images to return after spatial verification. Set to 0 to turn off // spatial verification. int loop_detection_num_images_after_verification = 0; // The maximum number of features to use for indexing an image. If an // image has more features, only the largest-scale features will be indexed. int loop_detection_max_num_features = -1; // Path to the vocabulary tree. std::string vocab_tree_path = ""; bool Check() const; }; // Sequentially match images within neighborhood: // // +-------------------------------+-----------------------> images[i] // ^ | ^ // | Current image[i] | // | | | // +----------+-----------+ // | // Match image_i against // // image_[i - o, i + o] with o = [1 .. overlap] // image_[i - 2^o, i + 2^o] (for quadratic overlap) // // Sequential order is determined based on the image names in ascending order. // // Invoke loop detection if `(i mod loop_detection_period) == 0`, retrieve // most similar `loop_detection_num_images` images from vocabulary tree, // and perform matching and verification. std::unique_ptr CreateSequentialFeatureMatcher( const SequentialMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path); struct VocabTreeMatchingOptions { // Number of images to retrieve for each query image. int num_images = 100; // Number of nearest neighbors to retrieve per query feature. int num_nearest_neighbors = 5; // Number of nearest-neighbor checks to use in retrieval. int num_checks = 256; // How many images to return after spatial verification. Set to 0 to turn off // spatial verification. int num_images_after_verification = 0; // The maximum number of features to use for indexing an image. If an // image has more features, only the largest-scale features will be indexed. int max_num_features = -1; // Path to the vocabulary tree. std::string vocab_tree_path = ""; // Optional path to file with specific image names to match. std::string match_list_path = ""; bool Check() const; }; // Match each image against its nearest neighbors using a vocabulary tree. std::unique_ptr CreateVocabTreeFeatureMatcher( const VocabTreeMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path); struct SpatialMatchingOptions { // Whether the location priors in the database are GPS coordinates in // the form of longitude and latitude coordinates in degrees. bool is_gps = true; // Whether to ignore the Z-component of the location prior. bool ignore_z = true; // The maximum number of nearest neighbors to match. int max_num_neighbors = 50; // The maximum distance between the query and nearest neighbor. For GPS // coordinates the unit is Euclidean distance in meters. double max_distance = 100; bool Check() const; }; // Match images against spatial nearest neighbors using prior location // information, e.g. provided manually or extracted from EXIF. std::unique_ptr CreateSpatialFeatureMatcher( const SpatialMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path); struct TransitiveMatchingOptions { // The maximum number of image pairs to process in one batch. int batch_size = 1000; // The number of transitive closure iterations. int num_iterations = 3; bool Check() const; }; // Match transitive image pairs in a database with existing feature matches. // This matcher transitively closes loops. For example, if image pairs A-B and // B-C match but A-C has not been matched, then this matcher attempts to match // A-C. This procedure is performed for multiple iterations. std::unique_ptr CreateTransitiveFeatureMatcher( const TransitiveMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path); struct ImagePairsMatchingOptions { // Number of image pairs to match in one batch. int block_size = 1225; // Path to the file with the matches. std::string match_list_path = ""; bool Check() const; }; // Match images manually specified in a list of image pairs. // // Read matches file with the following format: // // image_name1 image_name2 // image_name1 image_name3 // image_name2 image_name3 // ... // std::unique_ptr CreateImagePairsFeatureMatcher( const ImagePairsMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path); struct FeaturePairsMatchingOptions { // Whether to geometrically verify the given matches. bool verify_matches = true; // Path to the file with the matches. std::string match_list_path = ""; bool Check() const; }; // Import feature matches from a text file. // // Read matches file with the following format: // // image_name1 image_name2 // 0 1 // 1 2 // 2 3 // // image_name1 image_name3 // 0 1 // 1 2 // 2 3 // ... // std::unique_ptr CreateFeaturePairsFeatureMatcher( const FeaturePairsMatchingOptions& options, const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, const std::string& database_path); } // namespace colmap colmap-3.9.1/src/colmap/controllers/feature_matching_utils.cc000066400000000000000000000476741454702036400244720ustar00rootroot00000000000000// 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. #include "colmap/controllers/feature_matching_utils.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/utils.h" #include "colmap/util/cuda.h" #include "colmap/util/misc.h" #include #include #include namespace colmap { FeatureMatcherCache::FeatureMatcherCache(const size_t cache_size, const Database* database) : cache_size_(cache_size), database_(database) { CHECK_NOTNULL(database_); } void FeatureMatcherCache::Setup() { std::vector cameras = database_->ReadAllCameras(); cameras_cache_.reserve(cameras.size()); for (Camera& camera : cameras) { cameras_cache_.emplace(camera.camera_id, std::move(camera)); } std::vector images = database_->ReadAllImages(); images_cache_.reserve(images.size()); for (Image& image : images) { images_cache_.emplace(image.ImageId(), std::move(image)); } keypoints_cache_ = std::make_unique>>( cache_size_, [this](const image_t image_id) { return std::make_shared( database_->ReadKeypoints(image_id)); }); descriptors_cache_ = std::make_unique>>( cache_size_, [this](const image_t image_id) { return std::make_shared( database_->ReadDescriptors(image_id)); }); keypoints_exists_cache_ = std::make_unique>( images_cache_.size(), [this](const image_t image_id) { return database_->ExistsKeypoints(image_id); }); descriptors_exists_cache_ = std::make_unique>( images_cache_.size(), [this](const image_t image_id) { return database_->ExistsDescriptors(image_id); }); } const Camera& FeatureMatcherCache::GetCamera(const camera_t camera_id) const { return cameras_cache_.at(camera_id); } const Image& FeatureMatcherCache::GetImage(const image_t image_id) const { return images_cache_.at(image_id); } std::shared_ptr FeatureMatcherCache::GetKeypoints( const image_t image_id) { std::lock_guard lock(database_mutex_); return keypoints_cache_->Get(image_id); } std::shared_ptr FeatureMatcherCache::GetDescriptors( const image_t image_id) { std::lock_guard lock(database_mutex_); return descriptors_cache_->Get(image_id); } FeatureMatches FeatureMatcherCache::GetMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); return database_->ReadMatches(image_id1, image_id2); } std::vector FeatureMatcherCache::GetImageIds() const { std::vector image_ids; image_ids.reserve(images_cache_.size()); for (const auto& image : images_cache_) { image_ids.push_back(image.first); } return image_ids; } bool FeatureMatcherCache::ExistsKeypoints(const image_t image_id) { std::lock_guard lock(database_mutex_); return keypoints_exists_cache_->Get(image_id); } bool FeatureMatcherCache::ExistsDescriptors(const image_t image_id) { std::lock_guard lock(database_mutex_); return descriptors_exists_cache_->Get(image_id); } bool FeatureMatcherCache::ExistsMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); return database_->ExistsMatches(image_id1, image_id2); } bool FeatureMatcherCache::ExistsInlierMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); return database_->ExistsInlierMatches(image_id1, image_id2); } void FeatureMatcherCache::WriteMatches(const image_t image_id1, const image_t image_id2, const FeatureMatches& matches) { std::lock_guard lock(database_mutex_); database_->WriteMatches(image_id1, image_id2, matches); } void FeatureMatcherCache::WriteTwoViewGeometry( const image_t image_id1, const image_t image_id2, const TwoViewGeometry& two_view_geometry) { std::lock_guard lock(database_mutex_); database_->WriteTwoViewGeometry(image_id1, image_id2, two_view_geometry); } void FeatureMatcherCache::DeleteMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); database_->DeleteMatches(image_id1, image_id2); } void FeatureMatcherCache::DeleteInlierMatches(const image_t image_id1, const image_t image_id2) { std::lock_guard lock(database_mutex_); database_->DeleteInlierMatches(image_id1, image_id2); } FeatureMatcherWorker::FeatureMatcherWorker( const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, FeatureMatcherCache* cache, JobQueue* input_queue, JobQueue* output_queue) : matching_options_(matching_options), geometry_options_(geometry_options), cache_(cache), input_queue_(input_queue), output_queue_(output_queue) { CHECK(matching_options_.Check()); prev_keypoints_image_ids_[0] = kInvalidImageId; prev_keypoints_image_ids_[1] = kInvalidImageId; prev_descriptors_image_ids_[0] = kInvalidImageId; prev_descriptors_image_ids_[1] = kInvalidImageId; if (matching_options_.use_gpu) { #if !defined(COLMAP_CUDA_ENABLED) opengl_context_ = std::make_unique(); #endif } } void FeatureMatcherWorker::SetMaxNumMatches(int max_num_matches) { matching_options_.max_num_matches = max_num_matches; } void FeatureMatcherWorker::Run() { if (matching_options_.use_gpu) { #if !defined(COLMAP_CUDA_ENABLED) CHECK(opengl_context_); CHECK(opengl_context_->MakeCurrent()); #endif } std::unique_ptr matcher = CreateSiftFeatureMatcher(matching_options_); if (matcher == nullptr) { LOG(ERROR) << "Failed to create feature matcher."; SignalInvalidSetup(); return; } SignalValidSetup(); while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& data = input_job.Data(); if (!cache_->ExistsDescriptors(data.image_id1) || !cache_->ExistsDescriptors(data.image_id2)) { CHECK(output_queue_->Push(std::move(data))); continue; } if (matching_options_.guided_matching) { matcher->MatchGuided(geometry_options_, GetKeypointsPtr(0, data.image_id1), GetKeypointsPtr(1, data.image_id2), GetDescriptorsPtr(0, data.image_id1), GetDescriptorsPtr(1, data.image_id2), &data.two_view_geometry); } else { matcher->Match(GetDescriptorsPtr(0, data.image_id1), GetDescriptorsPtr(1, data.image_id2), &data.matches); } CHECK(output_queue_->Push(std::move(data))); } } } std::shared_ptr FeatureMatcherWorker::GetKeypointsPtr( const int index, const image_t image_id) { CHECK_GE(index, 0); CHECK_LE(index, 1); if (prev_keypoints_image_ids_[index] == image_id) { return nullptr; } else { prev_keypoints_image_ids_[index] = image_id; prev_keypoints_[index] = cache_->GetKeypoints(image_id); return prev_keypoints_[index]; } } std::shared_ptr FeatureMatcherWorker::GetDescriptorsPtr( const int index, const image_t image_id) { CHECK_GE(index, 0); CHECK_LE(index, 1); if (prev_descriptors_image_ids_[index] == image_id) { return nullptr; } else { prev_descriptors_image_ids_[index] = image_id; prev_descriptors_[index] = cache_->GetDescriptors(image_id); return prev_descriptors_[index]; } } namespace { class VerifierWorker : public Thread { public: typedef FeatureMatcherData Input; typedef FeatureMatcherData Output; VerifierWorker(const TwoViewGeometryOptions& options, FeatureMatcherCache* cache, JobQueue* input_queue, JobQueue* output_queue) : options_(options), cache_(cache), input_queue_(input_queue), output_queue_(output_queue) { CHECK(options_.Check()); } protected: void Run() override { while (true) { if (IsStopped()) { break; } auto input_job = input_queue_->Pop(); if (input_job.IsValid()) { auto& data = input_job.Data(); if (data.matches.size() < static_cast(options_.min_num_inliers)) { CHECK(output_queue_->Push(std::move(data))); continue; } const auto& camera1 = cache_->GetCamera(cache_->GetImage(data.image_id1).CameraId()); const auto& camera2 = cache_->GetCamera(cache_->GetImage(data.image_id2).CameraId()); const auto keypoints1 = cache_->GetKeypoints(data.image_id1); const auto keypoints2 = cache_->GetKeypoints(data.image_id2); const std::vector points1 = FeatureKeypointsToPointsVector(*keypoints1); const std::vector points2 = FeatureKeypointsToPointsVector(*keypoints2); data.two_view_geometry = EstimateTwoViewGeometry( camera1, points1, camera2, points2, data.matches, options_); CHECK(output_queue_->Push(std::move(data))); } } } private: const TwoViewGeometryOptions options_; FeatureMatcherCache* cache_; JobQueue* input_queue_; JobQueue* output_queue_; }; } // namespace FeatureMatcherController::FeatureMatcherController( const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, Database* database, FeatureMatcherCache* cache) : matching_options_(matching_options), geometry_options_(geometry_options), database_(database), cache_(cache), is_setup_(false) { CHECK(matching_options_.Check()); CHECK(geometry_options_.Check()); const int num_threads = GetEffectiveNumThreads(matching_options_.num_threads); CHECK_GT(num_threads, 0); std::vector gpu_indices = CSVToVector(matching_options_.gpu_index); CHECK_GT(gpu_indices.size(), 0); #if defined(COLMAP_CUDA_ENABLED) if (matching_options_.use_gpu && gpu_indices.size() == 1 && gpu_indices[0] == -1) { const int num_cuda_devices = GetNumCudaDevices(); CHECK_GT(num_cuda_devices, 0); gpu_indices.resize(num_cuda_devices); std::iota(gpu_indices.begin(), gpu_indices.end(), 0); } #endif // COLMAP_CUDA_ENABLED if (matching_options_.use_gpu) { auto matching_options_copy = matching_options_; // The first matching is always without guided matching. matching_options_copy.guided_matching = false; matchers_.reserve(gpu_indices.size()); for (const auto& gpu_index : gpu_indices) { matching_options_copy.gpu_index = std::to_string(gpu_index); matchers_.emplace_back( std::make_unique(matching_options_copy, geometry_options_, cache, &matcher_queue_, &verifier_queue_)); } } else { auto matching_options_copy = matching_options_; // The first matching is always without guided matching. matching_options_copy.guided_matching = false; matchers_.reserve(num_threads); for (int i = 0; i < num_threads; ++i) { matchers_.emplace_back( std::make_unique(matching_options_copy, geometry_options_, cache, &matcher_queue_, &verifier_queue_)); } } verifiers_.reserve(num_threads); if (matching_options_.guided_matching) { // Redirect the verification output to final round of guided matching. for (int i = 0; i < num_threads; ++i) { verifiers_.emplace_back(std::make_unique( geometry_options_, cache, &verifier_queue_, &guided_matcher_queue_)); } if (matching_options_.use_gpu) { auto matching_options_copy = matching_options_; guided_matchers_.reserve(gpu_indices.size()); for (const auto& gpu_index : gpu_indices) { matching_options_copy.gpu_index = std::to_string(gpu_index); guided_matchers_.emplace_back( std::make_unique(matching_options_copy, geometry_options_, cache, &guided_matcher_queue_, &output_queue_)); } } else { guided_matchers_.reserve(num_threads); for (int i = 0; i < num_threads; ++i) { guided_matchers_.emplace_back( std::make_unique(matching_options_, geometry_options_, cache, &guided_matcher_queue_, &output_queue_)); } } } else { for (int i = 0; i < num_threads; ++i) { verifiers_.emplace_back(std::make_unique( geometry_options_, cache, &verifier_queue_, &output_queue_)); } } } FeatureMatcherController::~FeatureMatcherController() { matcher_queue_.Wait(); verifier_queue_.Wait(); guided_matcher_queue_.Wait(); output_queue_.Wait(); for (auto& matcher : matchers_) { matcher->Stop(); } for (auto& verifier : verifiers_) { verifier->Stop(); } for (auto& guided_matcher : guided_matchers_) { guided_matcher->Stop(); } matcher_queue_.Stop(); verifier_queue_.Stop(); guided_matcher_queue_.Stop(); output_queue_.Stop(); for (auto& matcher : matchers_) { matcher->Wait(); } for (auto& verifier : verifiers_) { verifier->Wait(); } for (auto& guided_matcher : guided_matchers_) { guided_matcher->Wait(); } } bool FeatureMatcherController::Setup() { // Minimize the amount of allocated GPU memory by computing the maximum number // of descriptors for any image over the whole database. const int max_num_features = CHECK_NOTNULL(database_)->MaxNumKeypoints(); matching_options_.max_num_matches = std::min(matching_options_.max_num_matches, max_num_features); for (auto& matcher : matchers_) { matcher->SetMaxNumMatches(matching_options_.max_num_matches); matcher->Start(); } for (auto& verifier : verifiers_) { verifier->Start(); } for (auto& guided_matcher : guided_matchers_) { guided_matcher->SetMaxNumMatches(matching_options_.max_num_matches); guided_matcher->Start(); } for (auto& matcher : matchers_) { if (!matcher->CheckValidSetup()) { return false; } } for (auto& guided_matcher : guided_matchers_) { if (!guided_matcher->CheckValidSetup()) { return false; } } is_setup_ = true; return true; } void FeatureMatcherController::Match( const std::vector>& image_pairs) { CHECK_NOTNULL(database_); CHECK_NOTNULL(cache_); CHECK(is_setup_); if (image_pairs.empty()) { return; } ////////////////////////////////////////////////////////////////////////////// // Match the image pairs ////////////////////////////////////////////////////////////////////////////// std::unordered_set image_pair_ids; image_pair_ids.reserve(image_pairs.size()); size_t num_outputs = 0; for (const auto& image_pair : image_pairs) { // Avoid self-matches. if (image_pair.first == image_pair.second) { continue; } // Avoid duplicate image pairs. const image_pair_t pair_id = Database::ImagePairToPairId(image_pair.first, image_pair.second); if (image_pair_ids.count(pair_id) > 0) { continue; } image_pair_ids.insert(pair_id); const bool exists_matches = cache_->ExistsMatches(image_pair.first, image_pair.second); const bool exists_inlier_matches = cache_->ExistsInlierMatches(image_pair.first, image_pair.second); if (exists_matches && exists_inlier_matches) { continue; } num_outputs += 1; // If only one of the matches or inlier matches exist, we recompute them // from scratch and delete the existing results. This must be done before // pushing the jobs to the queue, otherwise database constraints might fail // when writing an existing result into the database. if (exists_inlier_matches) { cache_->DeleteInlierMatches(image_pair.first, image_pair.second); } FeatureMatcherData data; data.image_id1 = image_pair.first; data.image_id2 = image_pair.second; if (exists_matches) { data.matches = cache_->GetMatches(image_pair.first, image_pair.second); cache_->DeleteMatches(image_pair.first, image_pair.second); CHECK(verifier_queue_.Push(std::move(data))); } else { CHECK(matcher_queue_.Push(std::move(data))); } } ////////////////////////////////////////////////////////////////////////////// // Write results to database ////////////////////////////////////////////////////////////////////////////// for (size_t i = 0; i < num_outputs; ++i) { auto output_job = output_queue_.Pop(); CHECK(output_job.IsValid()); auto& output = output_job.Data(); if (output.matches.size() < static_cast(geometry_options_.min_num_inliers)) { output.matches = {}; } if (output.two_view_geometry.inlier_matches.size() < static_cast(geometry_options_.min_num_inliers)) { output.two_view_geometry = TwoViewGeometry(); } cache_->WriteMatches(output.image_id1, output.image_id2, output.matches); cache_->WriteTwoViewGeometry( output.image_id1, output.image_id2, output.two_view_geometry); } CHECK_EQ(output_queue_.Size(), 0); } } // namespace colmap colmap-3.9.1/src/colmap/controllers/feature_matching_utils.h000066400000000000000000000147231454702036400243210ustar00rootroot00000000000000// 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. #pragma once #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/sift.h" #include "colmap/scene/database.h" #include "colmap/util/cache.h" #include "colmap/util/opengl_utils.h" #include "colmap/util/threading.h" #include #include #include #include namespace colmap { struct FeatureMatcherData { image_t image_id1 = kInvalidImageId; image_t image_id2 = kInvalidImageId; FeatureMatches matches; TwoViewGeometry two_view_geometry; }; // Cache for feature matching to minimize database access during matching. class FeatureMatcherCache { public: FeatureMatcherCache(size_t cache_size, const Database* database); void Setup(); const Camera& GetCamera(camera_t camera_id) const; const Image& GetImage(image_t image_id) const; std::shared_ptr GetKeypoints(image_t image_id); std::shared_ptr GetDescriptors(image_t image_id); FeatureMatches GetMatches(image_t image_id1, image_t image_id2); std::vector GetImageIds() const; bool ExistsKeypoints(image_t image_id); bool ExistsDescriptors(image_t image_id); bool ExistsMatches(image_t image_id1, image_t image_id2); bool ExistsInlierMatches(image_t image_id1, image_t image_id2); void WriteMatches(image_t image_id1, image_t image_id2, const FeatureMatches& matches); void WriteTwoViewGeometry(image_t image_id1, image_t image_id2, const TwoViewGeometry& two_view_geometry); void DeleteMatches(image_t image_id1, image_t image_id2); void DeleteInlierMatches(image_t image_id1, image_t image_id2); private: const size_t cache_size_; const Database* database_; std::mutex database_mutex_; std::unordered_map cameras_cache_; std::unordered_map images_cache_; std::unique_ptr>> keypoints_cache_; std::unique_ptr>> descriptors_cache_; std::unique_ptr> keypoints_exists_cache_; std::unique_ptr> descriptors_exists_cache_; }; class FeatureMatcherWorker : public Thread { public: typedef FeatureMatcherData Input; typedef FeatureMatcherData Output; FeatureMatcherWorker(const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& geometry_options, FeatureMatcherCache* cache, JobQueue* input_queue, JobQueue* output_queue); void SetMaxNumMatches(int max_num_matches); private: void Run() override; std::shared_ptr GetKeypointsPtr(int index, image_t image_id); std::shared_ptr GetDescriptorsPtr(int index, image_t image_id); SiftMatchingOptions matching_options_; TwoViewGeometryOptions geometry_options_; FeatureMatcherCache* cache_; JobQueue* input_queue_; JobQueue* output_queue_; std::unique_ptr opengl_context_; std::array prev_keypoints_image_ids_; std::array, 2> prev_keypoints_; std::array prev_descriptors_image_ids_; std::array, 2> prev_descriptors_; }; // Multi-threaded and multi-GPU SIFT feature matcher, which writes the computed // results to the database and skips already matched image pairs. To improve // performance of the matching by taking advantage of caching and database // transactions, pass multiple images to the `Match` function. Note that the // database should be in an active transaction while calling `Match`. class FeatureMatcherController { public: FeatureMatcherController( const SiftMatchingOptions& matching_options, const TwoViewGeometryOptions& two_view_geometry_options, Database* database, FeatureMatcherCache* cache); ~FeatureMatcherController(); // Setup the matchers and return if successful. bool Setup(); // Match one batch of multiple image pairs. void Match(const std::vector>& image_pairs); private: SiftMatchingOptions matching_options_; TwoViewGeometryOptions geometry_options_; Database* database_; FeatureMatcherCache* cache_; bool is_setup_; std::vector> matchers_; std::vector> guided_matchers_; std::vector> verifiers_; std::unique_ptr thread_pool_; JobQueue matcher_queue_; JobQueue verifier_queue_; JobQueue guided_matcher_queue_; JobQueue output_queue_; }; } // namespace colmap colmap-3.9.1/src/colmap/controllers/hierarchical_mapper.cc000066400000000000000000000222441454702036400237110ustar00rootroot00000000000000// 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. #include "colmap/controllers/hierarchical_mapper.h" #include "colmap/estimators/alignment.h" #include "colmap/scene/scene_clustering.h" #include "colmap/util/misc.h" namespace colmap { namespace { void MergeClusters(const SceneClustering::Cluster& cluster, std::unordered_map>* reconstruction_managers) { // Extract all reconstructions from all child clusters. std::vector> reconstructions; for (const auto& child_cluster : cluster.child_clusters) { if (!child_cluster.child_clusters.empty()) { MergeClusters(child_cluster, reconstruction_managers); } auto& reconstruction_manager = reconstruction_managers->at(&child_cluster); for (size_t i = 0; i < reconstruction_manager->Size(); ++i) { reconstructions.push_back(reconstruction_manager->Get(i)); } } // Try to merge all child cluster reconstruction. while (reconstructions.size() > 1) { bool merge_success = false; for (size_t i = 0; i < reconstructions.size(); ++i) { const int num_reg_images_i = reconstructions[i]->NumRegImages(); for (size_t j = 0; j < i; ++j) { const double kMaxReprojError = 8.0; const int num_reg_images_j = reconstructions[j]->NumRegImages(); if (MergeReconstructions(kMaxReprojError, *reconstructions[j], reconstructions[i].get())) { LOG(INFO) << StringPrintf( "=> Merged clusters with %d and %d images into %d images", num_reg_images_i, num_reg_images_j, reconstructions[i]->NumRegImages()); reconstructions.erase(reconstructions.begin() + j); merge_success = true; break; } } if (merge_success) { break; } } if (!merge_success) { break; } } // Insert a new reconstruction manager for merged cluster. auto& reconstruction_manager = (*reconstruction_managers)[&cluster]; reconstruction_manager = std::make_shared(); for (const auto& reconstruction : reconstructions) { reconstruction_manager->Get(reconstruction_manager->Add()) = reconstruction; } // Delete all merged child cluster reconstruction managers. for (const auto& child_cluster : cluster.child_clusters) { reconstruction_managers->erase(&child_cluster); } } } // namespace bool HierarchicalMapperController::Options::Check() const { CHECK_OPTION_GT(init_num_trials, -1); CHECK_OPTION_GE(num_workers, -1); clustering_options.Check(); CHECK_EQ(clustering_options.branching, 2); incremental_options.Check(); return true; } HierarchicalMapperController::HierarchicalMapperController( const Options& options, std::shared_ptr reconstruction_manager) : options_(options), reconstruction_manager_(std::move(reconstruction_manager)) { CHECK(options_.Check()); } void HierarchicalMapperController::Run() { PrintHeading1("Partitioning scene"); ////////////////////////////////////////////////////////////////////////////// // Cluster scene graph ////////////////////////////////////////////////////////////////////////////// const Database database(options_.database_path); LOG(INFO) << "Reading images..."; const auto images = database.ReadAllImages(); std::unordered_map image_id_to_name; image_id_to_name.reserve(images.size()); for (const auto& image : images) { image_id_to_name.emplace(image.ImageId(), image.Name()); } SceneClustering scene_clustering = SceneClustering::Create(options_.clustering_options, database); auto leaf_clusters = scene_clustering.GetLeafClusters(); size_t total_num_images = 0; for (size_t i = 0; i < leaf_clusters.size(); ++i) { total_num_images += leaf_clusters[i]->image_ids.size(); LOG(INFO) << StringPrintf(" Cluster %d with %d images", i + 1, leaf_clusters[i]->image_ids.size()); } LOG(INFO) << StringPrintf("Clusters have %d images", total_num_images); ////////////////////////////////////////////////////////////////////////////// // Reconstruct clusters ////////////////////////////////////////////////////////////////////////////// PrintHeading1("Reconstructing clusters"); // Determine the number of workers and threads per worker. const int kMaxNumThreads = -1; const int num_eff_threads = GetEffectiveNumThreads(kMaxNumThreads); const int kDefaultNumWorkers = 8; const int num_eff_workers = options_.num_workers < 1 ? std::min(static_cast(leaf_clusters.size()), std::min(kDefaultNumWorkers, num_eff_threads)) : options_.num_workers; const int num_threads_per_worker = std::max(1, num_eff_threads / num_eff_workers); // Function to reconstruct one cluster using incremental mapping. auto ReconstructCluster = [&, this](const SceneClustering::Cluster& cluster, std::shared_ptr reconstruction_manager) { if (cluster.image_ids.empty()) { return; } auto incremental_options = std::make_shared( options_.incremental_options); incremental_options->max_model_overlap = 3; incremental_options->init_num_trials = options_.init_num_trials; if (incremental_options->num_threads < 0) { incremental_options->num_threads = num_threads_per_worker; } for (const auto image_id : cluster.image_ids) { incremental_options->image_names.insert( image_id_to_name.at(image_id)); } IncrementalMapperController mapper(std::move(incremental_options), options_.image_path, options_.database_path, std::move(reconstruction_manager)); mapper.Start(); mapper.Wait(); }; // Start reconstructing the bigger clusters first for better resource usage. std::sort(leaf_clusters.begin(), leaf_clusters.end(), [](const SceneClustering::Cluster* cluster1, const SceneClustering::Cluster* cluster2) { return cluster1->image_ids.size() > cluster2->image_ids.size(); }); // Start the reconstruction workers. Use a separate reconstruction manager per // thread to avoid race conditions. std::unordered_map> reconstruction_managers; reconstruction_managers.reserve(leaf_clusters.size()); ThreadPool thread_pool(num_eff_workers); for (const auto& cluster : leaf_clusters) { reconstruction_managers[cluster] = std::make_shared(); thread_pool.AddTask( ReconstructCluster, *cluster, reconstruction_managers[cluster]); } thread_pool.Wait(); ////////////////////////////////////////////////////////////////////////////// // Merge clusters ////////////////////////////////////////////////////////////////////////////// PrintHeading1("Merging clusters"); MergeClusters(*scene_clustering.GetRootCluster(), &reconstruction_managers); CHECK_EQ(reconstruction_managers.size(), 1); CHECK_GT(reconstruction_managers.begin()->second->Get(0)->NumRegImages(), 0); *reconstruction_manager_ = *reconstruction_managers.begin()->second; GetTimer().PrintMinutes(); } } // namespace colmap colmap-3.9.1/src/colmap/controllers/hierarchical_mapper.h000066400000000000000000000062241454702036400235530ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/incremental_mapper.h" #include "colmap/scene/reconstruction_manager.h" #include "colmap/scene/scene_clustering.h" #include "colmap/util/threading.h" #include namespace colmap { // Hierarchical mapping first hierarchically partitions the scene into multiple // overlapping clusters, then reconstructs them separately using incremental // mapping, and finally merges them all into a globally consistent // reconstruction. This is especially useful for larger-scale scenes, since // incremental mapping becomes slow with an increasing number of images. class HierarchicalMapperController : public Thread { public: struct Options { // The path to the image folder which are used as input. std::string image_path; // The path to the database file which is used as input. std::string database_path; // The maximum number of trials to initialize a cluster. int init_num_trials = 10; // The number of workers used to reconstruct clusters in parallel. int num_workers = -1; // Options for clustering the scene graph. SceneClustering::Options clustering_options; // Options used to reconstruction each cluster individually. IncrementalMapperOptions incremental_options; bool Check() const; }; HierarchicalMapperController( const Options& options, std::shared_ptr reconstruction_manager); private: void Run() override; const Options options_; std::shared_ptr reconstruction_manager_; }; } // namespace colmap colmap-3.9.1/src/colmap/controllers/hierarchical_mapper_test.cc000066400000000000000000000145321454702036400247510ustar00rootroot00000000000000// 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. #include "colmap/controllers/hierarchical_mapper.h" #include "colmap/estimators/alignment.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { void ExpectEqualReconstructions(const Reconstruction& gt, const Reconstruction& computed, const double max_rotation_error_deg, const double max_proj_center_error, const double num_obs_tolerance) { EXPECT_EQ(computed.NumCameras(), gt.NumCameras()); EXPECT_EQ(computed.NumImages(), gt.NumImages()); EXPECT_EQ(computed.NumRegImages(), gt.NumRegImages()); EXPECT_GE(computed.ComputeNumObservations(), (1 - num_obs_tolerance) * gt.ComputeNumObservations()); Sim3d gtFromComputed; AlignReconstructionsViaProjCenters(computed, gt, /*max_proj_center_error=*/0.1, >FromComputed); const std::vector errors = ComputeImageAlignmentError(computed, gt, gtFromComputed); EXPECT_EQ(errors.size(), gt.NumImages()); for (const auto& error : errors) { EXPECT_LT(error.rotation_error_deg, max_rotation_error_deg); EXPECT_LT(error.proj_center_error, max_proj_center_error); } } TEST(HierarchicalMapperController, WithoutNoise) { const std::string database_path = CreateTestDir() + "/database.db"; Database database(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_cameras = 2; synthetic_dataset_options.num_images = 20; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.point2D_stddev = 0; SynthesizeDataset(synthetic_dataset_options, >_reconstruction, &database); auto reconstruction_manager = std::make_shared(); HierarchicalMapperController::Options mapper_options; mapper_options.database_path = database_path; mapper_options.clustering_options.leaf_max_num_images = 5; mapper_options.clustering_options.image_overlap = 3; HierarchicalMapperController mapper(mapper_options, reconstruction_manager); mapper.Start(); mapper.Wait(); ASSERT_EQ(reconstruction_manager->Size(), 1); ExpectEqualReconstructions(gt_reconstruction, *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); } TEST(HierarchicalMapperController, MultiReconstruction) { const std::string database_path = CreateTestDir() + "/database.db"; Database database(database_path); Reconstruction gt_reconstruction1; Reconstruction gt_reconstruction2; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_cameras = 1; synthetic_dataset_options.num_images = 5; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.point2D_stddev = 0; SynthesizeDataset(synthetic_dataset_options, >_reconstruction1, &database); synthetic_dataset_options.num_images = 4; SynthesizeDataset(synthetic_dataset_options, >_reconstruction2, &database); auto reconstruction_manager = std::make_shared(); HierarchicalMapperController::Options mapper_options; mapper_options.database_path = database_path; mapper_options.clustering_options.leaf_max_num_images = 5; mapper_options.clustering_options.image_overlap = 3; HierarchicalMapperController mapper(mapper_options, reconstruction_manager); mapper.Start(); mapper.Wait(); ASSERT_EQ(reconstruction_manager->Size(), 2); Reconstruction* computed_reconstruction1 = nullptr; Reconstruction* computed_reconstruction2 = nullptr; if (reconstruction_manager->Get(0)->NumRegImages() == 5) { computed_reconstruction1 = reconstruction_manager->Get(0).get(); computed_reconstruction2 = reconstruction_manager->Get(1).get(); } else { computed_reconstruction1 = reconstruction_manager->Get(1).get(); computed_reconstruction2 = reconstruction_manager->Get(0).get(); } ExpectEqualReconstructions(gt_reconstruction1, *computed_reconstruction1, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); ExpectEqualReconstructions(gt_reconstruction2, *computed_reconstruction2, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/controllers/image_reader.cc000066400000000000000000000255501454702036400223360ustar00rootroot00000000000000// 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. #include "colmap/controllers/image_reader.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" namespace colmap { bool ImageReaderOptions::Check() const { CHECK_OPTION_GT(default_focal_length_factor, 0.0); CHECK_OPTION(ExistsCameraModelWithName(camera_model)); const CameraModelId model_id = CameraModelNameToId(camera_model); if (!camera_params.empty()) { CHECK_OPTION( CameraModelVerifyParams(model_id, CSVToVector(camera_params))); } return true; } ImageReader::ImageReader(const ImageReaderOptions& options, Database* database) : options_(options), database_(database), image_index_(0) { CHECK(options_.Check()); // Ensure trailing slash, so that we can build the correct image name. options_.image_path = EnsureTrailingSlash(StringReplace(options_.image_path, "\\", "/")); options_.mask_path = EnsureTrailingSlash(StringReplace(options_.mask_path, "\\", "/")); // Get a list of all files in the image path, sorted by image name. if (options_.image_list.empty()) { options_.image_list = GetRecursiveFileList(options_.image_path); std::sort(options_.image_list.begin(), options_.image_list.end()); } else { if (!std::is_sorted(options_.image_list.begin(), options_.image_list.end())) { std::sort(options_.image_list.begin(), options_.image_list.end()); } for (auto& image_name : options_.image_list) { image_name = JoinPaths(options_.image_path, image_name); } } if (static_cast(options_.existing_camera_id) != kInvalidCameraId) { CHECK(database->ExistsCamera(options_.existing_camera_id)); prev_camera_ = database->ReadCamera(options_.existing_camera_id); } else { // Set the manually specified camera parameters. prev_camera_.camera_id = kInvalidCameraId; CHECK(ExistsCameraModelWithName(options_.camera_model)); prev_camera_.model_id = CameraModelNameToId(options_.camera_model); prev_camera_.params.resize(CameraModelNumParams(prev_camera_.model_id), 0.); if (!options_.camera_params.empty()) { CHECK(prev_camera_.SetParamsFromString(options_.camera_params)); prev_camera_.has_prior_focal_length = true; } } } ImageReader::Status ImageReader::Next(Camera* camera, Image* image, Bitmap* bitmap, Bitmap* mask) { CHECK_NOTNULL(camera); CHECK_NOTNULL(image); CHECK_NOTNULL(bitmap); image_index_ += 1; CHECK_LE(image_index_, options_.image_list.size()); const std::string image_path = options_.image_list.at(image_index_ - 1); DatabaseTransaction database_transaction(database_); ////////////////////////////////////////////////////////////////////////////// // Set the image name. ////////////////////////////////////////////////////////////////////////////// image->SetName(image_path); image->SetName(StringReplace(image->Name(), "\\", "/")); image->SetName( image->Name().substr(options_.image_path.size(), image->Name().size() - options_.image_path.size())); const std::string image_folder = GetParentDir(image->Name()); ////////////////////////////////////////////////////////////////////////////// // Check if image already read. ////////////////////////////////////////////////////////////////////////////// const bool exists_image = database_->ExistsImageWithName(image->Name()); if (exists_image) { *image = database_->ReadImageWithName(image->Name()); const bool exists_keypoints = database_->ExistsKeypoints(image->ImageId()); const bool exists_descriptors = database_->ExistsDescriptors(image->ImageId()); if (exists_keypoints && exists_descriptors) { return Status::IMAGE_EXISTS; } } ////////////////////////////////////////////////////////////////////////////// // Read image. ////////////////////////////////////////////////////////////////////////////// if (!bitmap->Read(image_path, false)) { return Status::BITMAP_ERROR; } ////////////////////////////////////////////////////////////////////////////// // Read mask. ////////////////////////////////////////////////////////////////////////////// if (mask && !options_.mask_path.empty()) { const std::string mask_path = JoinPaths(options_.mask_path, image->Name() + ".png"); if (ExistsFile(mask_path) && !mask->Read(mask_path, false)) { // NOTE: Maybe introduce a separate error type MASK_ERROR? return Status::BITMAP_ERROR; } } ////////////////////////////////////////////////////////////////////////////// // Check for well-formed data. ////////////////////////////////////////////////////////////////////////////// if (exists_image) { Camera current_camera = database_->ReadCamera(image->CameraId()); if (options_.single_camera && prev_camera_.camera_id != kInvalidCameraId && (current_camera.width != prev_camera_.width || current_camera.height != prev_camera_.height)) { return Status::CAMERA_SINGLE_DIM_ERROR; } if (static_cast(bitmap->Width()) != current_camera.width || static_cast(bitmap->Height()) != current_camera.height) { return Status::CAMERA_EXIST_DIM_ERROR; } prev_camera_ = std::move(current_camera); } else { ////////////////////////////////////////////////////////////////////////////// // Check image dimensions. ////////////////////////////////////////////////////////////////////////////// if (prev_camera_.camera_id != kInvalidCameraId && ((options_.single_camera && !options_.single_camera_per_folder) || (options_.single_camera_per_folder && image_folder == prev_image_folder_)) && (prev_camera_.width != static_cast(bitmap->Width()) || prev_camera_.height != static_cast(bitmap->Height()))) { return Status::CAMERA_SINGLE_DIM_ERROR; } ////////////////////////////////////////////////////////////////////////////// // Read camera model and check for consistency if it exists ////////////////////////////////////////////////////////////////////////////// std::string camera_model; const bool valid_camera_model = bitmap->ExifCameraModel(&camera_model); if (camera_model_to_id_.count(camera_model) > 0) { Camera camera = database_->ReadCamera(camera_model_to_id_.at(camera_model)); if (camera.width != static_cast(bitmap->Width()) || camera.height != static_cast(bitmap->Height())) { return Status::CAMERA_EXIST_DIM_ERROR; } prev_camera_ = std::move(camera); } ////////////////////////////////////////////////////////////////////////////// // Extract camera model and focal length ////////////////////////////////////////////////////////////////////////////// if (prev_camera_.camera_id == kInvalidCameraId || options_.single_camera_per_image || (!options_.single_camera && !options_.single_camera_per_folder && static_cast(options_.existing_camera_id) == kInvalidCameraId && camera_model_to_id_.count(camera_model) == 0) || (options_.single_camera_per_folder && image_folders_.count(image_folder) == 0)) { if (options_.camera_params.empty()) { // Extract focal length. double focal_length = 0.0; bool has_focal_length = false; if (bitmap->ExifFocalLength(&focal_length)) { has_focal_length = true; } else { focal_length = options_.default_focal_length_factor * std::max(bitmap->Width(), bitmap->Height()); } prev_camera_ = Camera::CreateFromModelId(prev_camera_.camera_id, prev_camera_.model_id, focal_length, bitmap->Width(), bitmap->Height()); prev_camera_.has_prior_focal_length = has_focal_length; } prev_camera_.width = static_cast(bitmap->Width()); prev_camera_.height = static_cast(bitmap->Height()); if (!prev_camera_.VerifyParams()) { return Status::CAMERA_PARAM_ERROR; } prev_camera_.camera_id = database_->WriteCamera(prev_camera_); if (valid_camera_model) { camera_model_to_id_[camera_model] = prev_camera_.camera_id; } } image->SetCameraId(prev_camera_.camera_id); ////////////////////////////////////////////////////////////////////////////// // Extract GPS data. ////////////////////////////////////////////////////////////////////////////// Eigen::Vector3d& translation_prior = image->CamFromWorldPrior().translation; if (!bitmap->ExifLatitude(&translation_prior.x()) || !bitmap->ExifLongitude(&translation_prior.y()) || !bitmap->ExifAltitude(&translation_prior.z())) { translation_prior.setConstant(std::numeric_limits::quiet_NaN()); } } *camera = prev_camera_; image_folders_.insert(image_folder); prev_image_folder_ = image_folder; return Status::SUCCESS; } size_t ImageReader::NextIndex() const { return image_index_; } size_t ImageReader::NumImages() const { return options_.image_list.size(); } } // namespace colmap colmap-3.9.1/src/colmap/controllers/image_reader.h000066400000000000000000000115601454702036400221740ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/database.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/threading.h" #include namespace colmap { struct ImageReaderOptions { // Path to database in which to store the extracted data. std::string database_path = ""; // Root path to folder which contains the images. std::string image_path = ""; // Optional root path to folder which contains image masks. For a given image, // the corresponding mask must have the same sub-path below this root as the // image has below image_path. The filename must be equal, aside from the // added extension .png. For example, for an image image_path/abc/012.jpg, the // mask would be mask_path/abc/012.jpg.png. No features will be extracted in // regions where the mask image is black (pixel intensity value 0 in // grayscale). std::string mask_path = ""; // Optional list of images to read. The list must contain the relative path // of the images with respect to the image_path. std::vector image_list; // Name of the camera model. std::string camera_model = "SIMPLE_RADIAL"; // Whether to use the same camera for all images. bool single_camera = false; // Whether to use the same camera for all images in the same sub-folder. bool single_camera_per_folder = false; // Whether to use a different camera for each image. bool single_camera_per_image = false; // Whether to explicitly use an existing camera for all images. Note that in // this case the specified camera model and parameters are ignored. int existing_camera_id = kInvalidCameraId; // Manual specification of camera parameters. If empty, camera parameters // will be extracted from EXIF, i.e. principal point and focal length. std::string camera_params = ""; // If camera parameters are not specified manually and the image does not // have focal length EXIF information, the focal length is set to the // value `default_focal_length_factor * max(width, height)`. double default_focal_length_factor = 1.2; // Optional path to an image file specifying a mask for all images. No // features will be extracted in regions where the mask is black (pixel // intensity value 0 in grayscale). std::string camera_mask_path = ""; bool Check() const; }; // Recursively iterate over the images in a directory. Skips an image if it // already exists in the database. Extracts the camera intrinsics from EXIF and // writes the camera information to the database. class ImageReader { public: enum class Status { FAILURE, SUCCESS, IMAGE_EXISTS, BITMAP_ERROR, CAMERA_SINGLE_DIM_ERROR, CAMERA_EXIST_DIM_ERROR, CAMERA_PARAM_ERROR }; ImageReader(const ImageReaderOptions& options, Database* database); Status Next(Camera* camera, Image* image, Bitmap* bitmap, Bitmap* mask); size_t NextIndex() const; size_t NumImages() const; private: // Image reader options. ImageReaderOptions options_; Database* database_; // Index of previously processed image. size_t image_index_; // Previously processed camera. Camera prev_camera_; std::unordered_map camera_model_to_id_; // Names of image sub-folders. std::string prev_image_folder_; std::unordered_set image_folders_; }; } // namespace colmap colmap-3.9.1/src/colmap/controllers/incremental_mapper.cc000066400000000000000000000575561454702036400236120ustar00rootroot00000000000000// 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. #include "colmap/controllers/incremental_mapper.h" #include "colmap/util/misc.h" namespace colmap { namespace { size_t TriangulateImage(const IncrementalMapperOptions& options, const Image& image, IncrementalMapper* mapper) { LOG(INFO) << "=> Continued observations: " << image.NumPoints3D(); const size_t num_tris = mapper->TriangulateImage(options.Triangulation(), image.ImageId()); LOG(INFO) << "=> Added observations: " << num_tris; return num_tris; } void AdjustGlobalBundle(const IncrementalMapperOptions& options, IncrementalMapper* mapper) { BundleAdjustmentOptions custom_ba_options = options.GlobalBundleAdjustment(); const size_t num_reg_images = mapper->GetReconstruction().NumRegImages(); // Use stricter convergence criteria for first registered images. const size_t kMinNumRegImagesForFastBA = 10; if (num_reg_images < kMinNumRegImagesForFastBA) { custom_ba_options.solver_options.function_tolerance /= 10; custom_ba_options.solver_options.gradient_tolerance /= 10; custom_ba_options.solver_options.parameter_tolerance /= 10; custom_ba_options.solver_options.max_num_iterations *= 2; custom_ba_options.solver_options.max_linear_solver_iterations = 200; } PrintHeading1("Global bundle adjustment"); mapper->AdjustGlobalBundle(options.Mapper(), custom_ba_options); } void IterativeLocalRefinement(const IncrementalMapperOptions& options, const image_t image_id, IncrementalMapper* mapper) { auto ba_options = options.LocalBundleAdjustment(); for (int i = 0; i < options.ba_local_max_refinements; ++i) { const auto report = mapper->AdjustLocalBundle(options.Mapper(), ba_options, options.Triangulation(), image_id, mapper->GetModifiedPoints3D()); LOG(INFO) << "=> Merged observations: " << report.num_merged_observations; LOG(INFO) << "=> Completed observations: " << report.num_completed_observations; LOG(INFO) << "=> Filtered observations: " << report.num_filtered_observations; const double changed = report.num_adjusted_observations == 0 ? 0 : (report.num_merged_observations + report.num_completed_observations + report.num_filtered_observations) / static_cast(report.num_adjusted_observations); LOG(INFO) << StringPrintf("=> Changed observations: %.6f", changed); if (changed < options.ba_local_max_refinement_change) { break; } // Only use robust cost function for first iteration. ba_options.loss_function_type = BundleAdjustmentOptions::LossFunctionType::TRIVIAL; } mapper->ClearModifiedPoints3D(); } void IterativeGlobalRefinement(const IncrementalMapperOptions& options, IncrementalMapper* mapper) { PrintHeading1("Retriangulation"); CompleteAndMergeTracks(options, mapper); LOG(INFO) << "=> Retriangulated observations: " << mapper->Retriangulate(options.Triangulation()); for (int i = 0; i < options.ba_global_max_refinements; ++i) { const size_t num_observations = mapper->GetReconstruction().ComputeNumObservations(); size_t num_changed_observations = 0; AdjustGlobalBundle(options, mapper); num_changed_observations += CompleteAndMergeTracks(options, mapper); num_changed_observations += FilterPoints(options, mapper); const double changed = num_observations == 0 ? 0 : static_cast(num_changed_observations) / num_observations; LOG(INFO) << StringPrintf("=> Changed observations: %.6f", changed); if (changed < options.ba_global_max_refinement_change) { break; } } FilterImages(options, mapper); } void ExtractColors(const std::string& image_path, const image_t image_id, Reconstruction* reconstruction) { if (!reconstruction->ExtractColorsForImage(image_id, image_path)) { LOG(WARNING) << StringPrintf("Could not read image %s at path %s.", reconstruction->Image(image_id).Name().c_str(), image_path.c_str()); } } void WriteSnapshot(const Reconstruction& reconstruction, const std::string& snapshot_path) { PrintHeading1("Creating snapshot"); // Get the current timestamp in milliseconds. const size_t timestamp = std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch()) .count(); // Write reconstruction to unique path with current timestamp. const std::string path = JoinPaths(snapshot_path, StringPrintf("%010d", timestamp)); CreateDirIfNotExists(path); LOG(INFO) << "=> Writing to " << path; reconstruction.Write(path); } } // namespace size_t FilterPoints(const IncrementalMapperOptions& options, IncrementalMapper* mapper) { const size_t num_filtered_observations = mapper->FilterPoints(options.Mapper()); LOG(INFO) << "=> Filtered observations: " << num_filtered_observations; return num_filtered_observations; } size_t FilterImages(const IncrementalMapperOptions& options, IncrementalMapper* mapper) { const size_t num_filtered_images = mapper->FilterImages(options.Mapper()); LOG(INFO) << "=> Filtered images: " << num_filtered_images; return num_filtered_images; } size_t CompleteAndMergeTracks(const IncrementalMapperOptions& options, IncrementalMapper* mapper) { const size_t num_completed_observations = mapper->CompleteTracks(options.Triangulation()); LOG(INFO) << "=> Completed observations: " << num_completed_observations; const size_t num_merged_observations = mapper->MergeTracks(options.Triangulation()); LOG(INFO) << "=> Merged observations: " << num_merged_observations; return num_completed_observations + num_merged_observations; } IncrementalMapper::Options IncrementalMapperOptions::Mapper() const { IncrementalMapper::Options options = mapper; options.abs_pose_refine_focal_length = ba_refine_focal_length; options.abs_pose_refine_extra_params = ba_refine_extra_params; options.min_focal_length_ratio = min_focal_length_ratio; options.max_focal_length_ratio = max_focal_length_ratio; options.max_extra_param = max_extra_param; options.num_threads = num_threads; options.local_ba_num_images = ba_local_num_images; options.fix_existing_images = fix_existing_images; return options; } IncrementalTriangulator::Options IncrementalMapperOptions::Triangulation() const { IncrementalTriangulator::Options options = triangulation; options.min_focal_length_ratio = min_focal_length_ratio; options.max_focal_length_ratio = max_focal_length_ratio; options.max_extra_param = max_extra_param; return options; } BundleAdjustmentOptions IncrementalMapperOptions::LocalBundleAdjustment() const { BundleAdjustmentOptions options; options.solver_options.function_tolerance = ba_local_function_tolerance; options.solver_options.gradient_tolerance = 10.0; options.solver_options.parameter_tolerance = 0.0; options.solver_options.max_num_iterations = ba_local_max_num_iterations; options.solver_options.max_linear_solver_iterations = 100; options.solver_options.logging_type = ceres::LoggingType::SILENT; options.solver_options.num_threads = num_threads; #if CERES_VERSION_MAJOR < 2 options.solver_options.num_linear_solver_threads = num_threads; #endif // CERES_VERSION_MAJOR options.print_summary = true; options.refine_focal_length = ba_refine_focal_length; options.refine_principal_point = ba_refine_principal_point; options.refine_extra_params = ba_refine_extra_params; options.min_num_residuals_for_multi_threading = ba_min_num_residuals_for_multi_threading; options.loss_function_scale = 1.0; options.loss_function_type = BundleAdjustmentOptions::LossFunctionType::SOFT_L1; return options; } BundleAdjustmentOptions IncrementalMapperOptions::GlobalBundleAdjustment() const { BundleAdjustmentOptions options; options.solver_options.function_tolerance = ba_global_function_tolerance; options.solver_options.gradient_tolerance = 1.0; options.solver_options.parameter_tolerance = 0.0; options.solver_options.max_num_iterations = ba_global_max_num_iterations; options.solver_options.max_linear_solver_iterations = 100; options.solver_options.logging_type = ceres::LoggingType::PER_MINIMIZER_ITERATION; options.solver_options.minimizer_progress_to_stdout = true; options.solver_options.num_threads = num_threads; #if CERES_VERSION_MAJOR < 2 options.solver_options.num_linear_solver_threads = num_threads; #endif // CERES_VERSION_MAJOR options.print_summary = true; options.refine_focal_length = ba_refine_focal_length; options.refine_principal_point = ba_refine_principal_point; options.refine_extra_params = ba_refine_extra_params; options.min_num_residuals_for_multi_threading = ba_min_num_residuals_for_multi_threading; options.loss_function_type = BundleAdjustmentOptions::LossFunctionType::TRIVIAL; return options; } bool IncrementalMapperOptions::Check() const { CHECK_OPTION_GT(min_num_matches, 0); CHECK_OPTION_GT(max_num_models, 0); CHECK_OPTION_GT(max_model_overlap, 0); CHECK_OPTION_GE(min_model_size, 0); CHECK_OPTION_GT(init_num_trials, 0); CHECK_OPTION_GT(min_focal_length_ratio, 0); CHECK_OPTION_GT(max_focal_length_ratio, 0); CHECK_OPTION_GE(max_extra_param, 0); CHECK_OPTION_GE(ba_local_num_images, 2); CHECK_OPTION_GE(ba_local_max_num_iterations, 0); CHECK_OPTION_GT(ba_global_images_ratio, 1.0); CHECK_OPTION_GT(ba_global_points_ratio, 1.0); CHECK_OPTION_GT(ba_global_images_freq, 0); CHECK_OPTION_GT(ba_global_points_freq, 0); CHECK_OPTION_GT(ba_global_max_num_iterations, 0); CHECK_OPTION_GT(ba_local_max_refinements, 0); CHECK_OPTION_GE(ba_local_max_refinement_change, 0); CHECK_OPTION_GT(ba_global_max_refinements, 0); CHECK_OPTION_GE(ba_global_max_refinement_change, 0); CHECK_OPTION_GE(snapshot_images_freq, 0); CHECK_OPTION(Mapper().Check()); CHECK_OPTION(Triangulation().Check()); return true; } IncrementalMapperController::IncrementalMapperController( std::shared_ptr options, const std::string& image_path, const std::string& database_path, std::shared_ptr reconstruction_manager) : options_(std::move(options)), image_path_(image_path), database_path_(database_path), reconstruction_manager_(std::move(reconstruction_manager)) { CHECK(options_->Check()); RegisterCallback(INITIAL_IMAGE_PAIR_REG_CALLBACK); RegisterCallback(NEXT_IMAGE_REG_CALLBACK); RegisterCallback(LAST_IMAGE_REG_CALLBACK); } void IncrementalMapperController::Run() { if (!LoadDatabase()) { return; } IncrementalMapper::Options init_mapper_options = options_->Mapper(); Reconstruct(init_mapper_options); const size_t kNumInitRelaxations = 2; for (size_t i = 0; i < kNumInitRelaxations; ++i) { if (reconstruction_manager_->Size() > 0 || IsStopped()) { break; } LOG(INFO) << "=> Relaxing the initialization constraints."; init_mapper_options.init_min_num_inliers /= 2; Reconstruct(init_mapper_options); if (reconstruction_manager_->Size() > 0 || IsStopped()) { break; } LOG(INFO) << "=> Relaxing the initialization constraints."; init_mapper_options.init_min_tri_angle /= 2; Reconstruct(init_mapper_options); } GetTimer().PrintMinutes(); } bool IncrementalMapperController::LoadDatabase() { PrintHeading1("Loading database"); // Make sure images of the given reconstruction are also included when // manually specifying images for the reconstrunstruction procedure. std::unordered_set image_names = options_->image_names; if (reconstruction_manager_->Size() == 1 && !options_->image_names.empty()) { const auto& reconstruction = reconstruction_manager_->Get(0); for (const image_t image_id : reconstruction->RegImageIds()) { const auto& image = reconstruction->Image(image_id); image_names.insert(image.Name()); } } Database database(database_path_); Timer timer; timer.Start(); const size_t min_num_matches = static_cast(options_->min_num_matches); database_cache_ = DatabaseCache::Create( database, min_num_matches, options_->ignore_watermarks, image_names); timer.PrintMinutes(); if (database_cache_->NumImages() == 0) { LOG(WARNING) << "No images with matches found in the database"; return false; } return true; } void IncrementalMapperController::Reconstruct( const IncrementalMapper::Options& init_mapper_options) { ////////////////////////////////////////////////////////////////////////////// // Main loop ////////////////////////////////////////////////////////////////////////////// IncrementalMapper mapper(database_cache_); // Is there a sub-model before we start the reconstruction? I.e. the user // has imported an existing reconstruction. const bool initial_reconstruction_given = reconstruction_manager_->Size() > 0; CHECK_LE(reconstruction_manager_->Size(), 1) << "Can only resume from a " "single reconstruction, but " "multiple are given."; for (int num_trials = 0; num_trials < options_->init_num_trials; ++num_trials) { BlockIfPaused(); if (IsStopped()) { break; } size_t reconstruction_idx; if (!initial_reconstruction_given || num_trials > 0) { reconstruction_idx = reconstruction_manager_->Add(); } else { reconstruction_idx = 0; } std::shared_ptr reconstruction = reconstruction_manager_->Get(reconstruction_idx); mapper.BeginReconstruction(reconstruction); //////////////////////////////////////////////////////////////////////////// // Register initial pair //////////////////////////////////////////////////////////////////////////// if (reconstruction->NumRegImages() == 0) { image_t image_id1 = static_cast(options_->init_image_id1); image_t image_id2 = static_cast(options_->init_image_id2); // Try to find good initial pair. if (options_->init_image_id1 == -1 || options_->init_image_id2 == -1) { PrintHeading1("Finding good initial image pair"); const bool find_init_success = mapper.FindInitialImagePair( init_mapper_options, &image_id1, &image_id2); if (!find_init_success) { LOG(INFO) << "=> No good initial image pair found."; mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); break; } } else { if (!reconstruction->ExistsImage(image_id1) || !reconstruction->ExistsImage(image_id2)) { LOG(INFO) << StringPrintf( "=> Initial image pair #%d and #%d do not exist.", image_id1, image_id2); mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); return; } } PrintHeading1(StringPrintf( "Initializing with image pair #%d and #%d", image_id1, image_id2)); const bool reg_init_success = mapper.RegisterInitialImagePair( init_mapper_options, image_id1, image_id2); if (!reg_init_success) { LOG(INFO) << "=> Initialization failed - possible solutions:" << std::endl << " - try to relax the initialization constraints" << std::endl << " - manually select an initial image pair"; mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); break; } AdjustGlobalBundle(*options_, &mapper); FilterPoints(*options_, &mapper); FilterImages(*options_, &mapper); // Initial image pair failed to register. if (reconstruction->NumRegImages() == 0 || reconstruction->NumPoints3D() == 0) { mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); // If both initial images are manually specified, there is no need for // further initialization trials. if (options_->init_image_id1 != -1 && options_->init_image_id2 != -1) { break; } else { continue; } } if (options_->extract_colors) { ExtractColors(image_path_, image_id1, reconstruction.get()); } } Callback(INITIAL_IMAGE_PAIR_REG_CALLBACK); //////////////////////////////////////////////////////////////////////////// // Incremental mapping //////////////////////////////////////////////////////////////////////////// size_t snapshot_prev_num_reg_images = reconstruction->NumRegImages(); size_t ba_prev_num_reg_images = reconstruction->NumRegImages(); size_t ba_prev_num_points = reconstruction->NumPoints3D(); bool reg_next_success = true; bool prev_reg_next_success = true; while (reg_next_success) { BlockIfPaused(); if (IsStopped()) { break; } reg_next_success = false; const std::vector next_images = mapper.FindNextImages(options_->Mapper()); if (next_images.empty()) { break; } for (size_t reg_trial = 0; reg_trial < next_images.size(); ++reg_trial) { const image_t next_image_id = next_images[reg_trial]; const Image& next_image = reconstruction->Image(next_image_id); PrintHeading1(StringPrintf("Registering image #%d (%d)", next_image_id, reconstruction->NumRegImages() + 1)); LOG(INFO) << StringPrintf("=> Image sees %d / %d points", next_image.NumVisiblePoints3D(), next_image.NumObservations()); reg_next_success = mapper.RegisterNextImage(options_->Mapper(), next_image_id); if (reg_next_success) { TriangulateImage(*options_, next_image, &mapper); IterativeLocalRefinement(*options_, next_image_id, &mapper); if (reconstruction->NumRegImages() >= options_->ba_global_images_ratio * ba_prev_num_reg_images || reconstruction->NumRegImages() >= options_->ba_global_images_freq + ba_prev_num_reg_images || reconstruction->NumPoints3D() >= options_->ba_global_points_ratio * ba_prev_num_points || reconstruction->NumPoints3D() >= options_->ba_global_points_freq + ba_prev_num_points) { IterativeGlobalRefinement(*options_, &mapper); ba_prev_num_points = reconstruction->NumPoints3D(); ba_prev_num_reg_images = reconstruction->NumRegImages(); } if (options_->extract_colors) { ExtractColors(image_path_, next_image_id, reconstruction.get()); } if (options_->snapshot_images_freq > 0 && reconstruction->NumRegImages() >= options_->snapshot_images_freq + snapshot_prev_num_reg_images) { snapshot_prev_num_reg_images = reconstruction->NumRegImages(); WriteSnapshot(*reconstruction, options_->snapshot_path); } Callback(NEXT_IMAGE_REG_CALLBACK); break; } else { LOG(INFO) << "=> Could not register, trying another image."; // If initial pair fails to continue for some time, // abort and try different initial pair. const size_t kMinNumInitialRegTrials = 30; if (reg_trial >= kMinNumInitialRegTrials && reconstruction->NumRegImages() < static_cast(options_->min_model_size)) { break; } } } const size_t max_model_overlap = static_cast(options_->max_model_overlap); if (mapper.NumSharedRegImages() >= max_model_overlap) { break; } // If no image could be registered, try a single final global iterative // bundle adjustment and try again to register one image. If this fails // once, then exit the incremental mapping. if (!reg_next_success && prev_reg_next_success) { reg_next_success = true; prev_reg_next_success = false; IterativeGlobalRefinement(*options_, &mapper); } else { prev_reg_next_success = reg_next_success; } } if (IsStopped()) { mapper.EndReconstruction(/*discard=*/false); break; } // Only run final global BA, if last incremental BA was not global. if (reconstruction->NumRegImages() >= 2 && reconstruction->NumRegImages() != ba_prev_num_reg_images && reconstruction->NumPoints3D() != ba_prev_num_points) { IterativeGlobalRefinement(*options_, &mapper); } // Remember the total number of registered images before potentially // discarding it below due to small size, so we can out of the main loop, // if all images were registered. const size_t total_num_reg_images = mapper.NumTotalRegImages(); // If the total number of images is small then do not enforce the minimum // model size so that we can reconstruct small image collections. // Always keep the first reconstruction, independent of size. const size_t min_model_size = std::min( 0.8 * database_cache_->NumImages(), options_->min_model_size); if ((options_->multiple_models && reconstruction_manager_->Size() > 1 && reconstruction->NumRegImages() < min_model_size) || reconstruction->NumRegImages() == 0) { mapper.EndReconstruction(/*discard=*/true); reconstruction_manager_->Delete(reconstruction_idx); } else { mapper.EndReconstruction(/*discard=*/false); } Callback(LAST_IMAGE_REG_CALLBACK); if (initial_reconstruction_given || !options_->multiple_models || reconstruction_manager_->Size() >= static_cast(options_->max_num_models) || total_num_reg_images >= database_cache_->NumImages() - 1) { break; } } } } // namespace colmap colmap-3.9.1/src/colmap/controllers/incremental_mapper.h000066400000000000000000000154101454702036400234330ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/reconstruction_manager.h" #include "colmap/sfm/incremental_mapper.h" #include "colmap/util/threading.h" namespace colmap { struct IncrementalMapperOptions { // The minimum number of matches for inlier matches to be considered. int min_num_matches = 15; // Whether to ignore the inlier matches of watermark image pairs. bool ignore_watermarks = false; // Whether to reconstruct multiple sub-models. bool multiple_models = true; // The number of sub-models to reconstruct. int max_num_models = 50; // The maximum number of overlapping images between sub-models. If the // current sub-models shares more than this number of images with another // model, then the reconstruction is stopped. int max_model_overlap = 20; // The minimum number of registered images of a sub-model, otherwise the // sub-model is discarded. Note that the first sub-model is always kept // independent of size. int min_model_size = 10; // The image identifiers used to initialize the reconstruction. Note that // only one or both image identifiers can be specified. In the former case, // the second image is automatically determined. int init_image_id1 = -1; int init_image_id2 = -1; // The number of trials to initialize the reconstruction. int init_num_trials = 200; // Whether to extract colors for reconstructed points. bool extract_colors = true; // The number of threads to use during reconstruction. int num_threads = -1; // Thresholds for filtering images with degenerate intrinsics. double min_focal_length_ratio = 0.1; double max_focal_length_ratio = 10.0; double max_extra_param = 1.0; // Which intrinsic parameters to optimize during the reconstruction. bool ba_refine_focal_length = true; bool ba_refine_principal_point = false; bool ba_refine_extra_params = true; // The minimum number of residuals per bundle adjustment problem to // enable multi-threading solving of the problems. int ba_min_num_residuals_for_multi_threading = 50000; // The number of images to optimize in local bundle adjustment. int ba_local_num_images = 6; // Ceres solver function tolerance for local bundle adjustment double ba_local_function_tolerance = 0.0; // The maximum number of local bundle adjustment iterations. int ba_local_max_num_iterations = 25; // The growth rates after which to perform global bundle adjustment. double ba_global_images_ratio = 1.1; double ba_global_points_ratio = 1.1; int ba_global_images_freq = 500; int ba_global_points_freq = 250000; // Ceres solver function tolerance for global bundle adjustment double ba_global_function_tolerance = 0.0; // The maximum number of global bundle adjustment iterations. int ba_global_max_num_iterations = 50; // The thresholds for iterative bundle adjustment refinements. int ba_local_max_refinements = 2; double ba_local_max_refinement_change = 0.001; int ba_global_max_refinements = 5; double ba_global_max_refinement_change = 0.0005; // Path to a folder with reconstruction snapshots during incremental // reconstruction. Snapshots will be saved according to the specified // frequency of registered images. std::string snapshot_path = ""; int snapshot_images_freq = 0; // Which images to reconstruct. If no images are specified, all images will // be reconstructed by default. std::unordered_set image_names; // If reconstruction is provided as input, fix the existing image poses. bool fix_existing_images = false; IncrementalMapper::Options mapper; IncrementalTriangulator::Options triangulation; IncrementalMapper::Options Mapper() const; IncrementalTriangulator::Options Triangulation() const; BundleAdjustmentOptions LocalBundleAdjustment() const; BundleAdjustmentOptions GlobalBundleAdjustment() const; bool Check() const; }; // Class that controls the incremental mapping procedure by iteratively // initializing reconstructions from the same scene graph. class IncrementalMapperController : public Thread { public: enum { INITIAL_IMAGE_PAIR_REG_CALLBACK, NEXT_IMAGE_REG_CALLBACK, LAST_IMAGE_REG_CALLBACK, }; IncrementalMapperController( std::shared_ptr options, const std::string& image_path, const std::string& database_path, std::shared_ptr reconstruction_manager); private: void Run(); bool LoadDatabase(); void Reconstruct(const IncrementalMapper::Options& init_mapper_options); const std::shared_ptr options_; const std::string image_path_; const std::string database_path_; std::shared_ptr reconstruction_manager_; std::shared_ptr database_cache_; }; // Globally filter points and images in mapper. size_t FilterPoints(const IncrementalMapperOptions& options, IncrementalMapper* mapper); size_t FilterImages(const IncrementalMapperOptions& options, IncrementalMapper* mapper); // Globally complete and merge tracks in mapper. size_t CompleteAndMergeTracks(const IncrementalMapperOptions& options, IncrementalMapper* mapper); } // namespace colmap colmap-3.9.1/src/colmap/controllers/incremental_mapper_test.cc000066400000000000000000000210461454702036400246320ustar00rootroot00000000000000// 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. #include "colmap/controllers/incremental_mapper.h" #include "colmap/estimators/alignment.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { void ExpectEqualReconstructions(const Reconstruction& gt, const Reconstruction& computed, const double max_rotation_error_deg, const double max_proj_center_error, const double num_obs_tolerance) { EXPECT_EQ(computed.NumCameras(), gt.NumCameras()); EXPECT_EQ(computed.NumImages(), gt.NumImages()); EXPECT_EQ(computed.NumRegImages(), gt.NumRegImages()); EXPECT_GE(computed.ComputeNumObservations(), (1 - num_obs_tolerance) * gt.ComputeNumObservations()); Sim3d gtFromComputed; AlignReconstructionsViaProjCenters(computed, gt, /*max_proj_center_error=*/0.1, >FromComputed); const std::vector errors = ComputeImageAlignmentError(computed, gt, gtFromComputed); EXPECT_EQ(errors.size(), gt.NumImages()); for (const auto& error : errors) { EXPECT_LT(error.rotation_error_deg, max_rotation_error_deg); EXPECT_LT(error.proj_center_error, max_proj_center_error); } } TEST(IncrementalMapperController, WithoutNoise) { const std::string database_path = CreateTestDir() + "/database.db"; Database database(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_cameras = 2; synthetic_dataset_options.num_images = 7; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.point2D_stddev = 0; SynthesizeDataset(synthetic_dataset_options, >_reconstruction, &database); auto reconstruction_manager = std::make_shared(); IncrementalMapperController mapper( std::make_shared(), /*image_path=*/"", database_path, reconstruction_manager); mapper.Start(); mapper.Wait(); ASSERT_EQ(reconstruction_manager->Size(), 1); ExpectEqualReconstructions(gt_reconstruction, *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); } TEST(IncrementalMapperController, WithNoise) { const std::string database_path = CreateTestDir() + "/database.db"; Database database(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_cameras = 2; synthetic_dataset_options.num_images = 7; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.point2D_stddev = 0.5; SynthesizeDataset(synthetic_dataset_options, >_reconstruction, &database); auto reconstruction_manager = std::make_shared(); IncrementalMapperController mapper( std::make_shared(), /*image_path=*/"", database_path, reconstruction_manager); mapper.Start(); mapper.Wait(); ASSERT_EQ(reconstruction_manager->Size(), 1); ExpectEqualReconstructions(gt_reconstruction, *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-1, /*max_proj_center_error=*/1e-1, /*num_obs_tolerance=*/0.02); } TEST(IncrementalMapperController, MultiReconstruction) { const std::string database_path = CreateTestDir() + "/database.db"; Database database(database_path); Reconstruction gt_reconstruction1; Reconstruction gt_reconstruction2; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_cameras = 1; synthetic_dataset_options.num_images = 5; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.point2D_stddev = 0; SynthesizeDataset(synthetic_dataset_options, >_reconstruction1, &database); synthetic_dataset_options.num_images = 4; SynthesizeDataset(synthetic_dataset_options, >_reconstruction2, &database); auto reconstruction_manager = std::make_shared(); auto mapper_options = std::make_shared(); mapper_options->min_model_size = 4; IncrementalMapperController mapper(mapper_options, /*image_path=*/"", database_path, reconstruction_manager); mapper.Start(); mapper.Wait(); ASSERT_EQ(reconstruction_manager->Size(), 2); Reconstruction* computed_reconstruction1 = nullptr; Reconstruction* computed_reconstruction2 = nullptr; if (reconstruction_manager->Get(0)->NumRegImages() == 5) { computed_reconstruction1 = reconstruction_manager->Get(0).get(); computed_reconstruction2 = reconstruction_manager->Get(1).get(); } else { computed_reconstruction1 = reconstruction_manager->Get(1).get(); computed_reconstruction2 = reconstruction_manager->Get(0).get(); } ExpectEqualReconstructions(gt_reconstruction1, *computed_reconstruction1, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); ExpectEqualReconstructions(gt_reconstruction2, *computed_reconstruction2, /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); } TEST(IncrementalMapperController, ChainedMatches) { const std::string database_path = CreateTestDir() + "/database.db"; Database database(database_path); Reconstruction gt_reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.match_config = SyntheticDatasetOptions::MatchConfig::CHAINED; synthetic_dataset_options.num_cameras = 1; synthetic_dataset_options.num_images = 4; synthetic_dataset_options.num_points3D = 100; synthetic_dataset_options.point2D_stddev = 0; SynthesizeDataset(synthetic_dataset_options, >_reconstruction, &database); auto reconstruction_manager = std::make_shared(); IncrementalMapperController mapper( std::make_shared(), /*image_path=*/"", database_path, reconstruction_manager); mapper.Start(); mapper.Wait(); ASSERT_EQ(reconstruction_manager->Size(), 1); ExpectEqualReconstructions(gt_reconstruction, *reconstruction_manager->Get(0), /*max_rotation_error_deg=*/1e-2, /*max_proj_center_error=*/1e-4, /*num_obs_tolerance=*/0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/controllers/option_manager.cc000066400000000000000000001217731454702036400227400ustar00rootroot00000000000000// 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. #include "colmap/controllers/option_manager.h" #include "colmap/controllers/feature_extraction.h" #include "colmap/controllers/feature_matching.h" #include "colmap/controllers/image_reader.h" #include "colmap/controllers/incremental_mapper.h" #include "colmap/estimators/bundle_adjustment.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/sift.h" #include "colmap/math/random.h" #include "colmap/mvs/fusion.h" #include "colmap/mvs/meshing.h" #include "colmap/mvs/patch_match.h" #include "colmap/ui/render_options.h" #include "colmap/util/misc.h" #include "colmap/util/version.h" #include #include namespace config = boost::program_options; namespace colmap { OptionManager::OptionManager(bool add_project_options) { project_path = std::make_shared(); database_path = std::make_shared(); image_path = std::make_shared(); image_reader = std::make_shared(); sift_extraction = std::make_shared(); sift_matching = std::make_shared(); two_view_geometry = std::make_shared(); exhaustive_matching = std::make_shared(); sequential_matching = std::make_shared(); vocab_tree_matching = std::make_shared(); spatial_matching = std::make_shared(); transitive_matching = std::make_shared(); image_pairs_matching = std::make_shared(); bundle_adjustment = std::make_shared(); mapper = std::make_shared(); patch_match_stereo = std::make_shared(); stereo_fusion = std::make_shared(); poisson_meshing = std::make_shared(); delaunay_meshing = std::make_shared(); render = std::make_shared(); Reset(); desc_->add_options()("help,h", ""); AddRandomOptions(); AddLogOptions(); if (add_project_options) { desc_->add_options()("project_path", config::value()); } } void OptionManager::ModifyForIndividualData() { mapper->min_focal_length_ratio = 0.1; mapper->max_focal_length_ratio = 10; mapper->max_extra_param = std::numeric_limits::max(); } void OptionManager::ModifyForVideoData() { const bool kResetPaths = false; ResetOptions(kResetPaths); mapper->mapper.init_min_tri_angle /= 2; mapper->ba_global_images_ratio = 1.4; mapper->ba_global_points_ratio = 1.4; mapper->min_focal_length_ratio = 0.1; mapper->max_focal_length_ratio = 10; mapper->max_extra_param = std::numeric_limits::max(); stereo_fusion->min_num_pixels = 15; } void OptionManager::ModifyForInternetData() { stereo_fusion->min_num_pixels = 10; } void OptionManager::ModifyForLowQuality() { sift_extraction->max_image_size = 1000; sift_extraction->max_num_features = 2048; sequential_matching->loop_detection_num_images /= 2; vocab_tree_matching->num_images /= 2; mapper->ba_local_max_num_iterations /= 2; mapper->ba_global_max_num_iterations /= 2; mapper->ba_global_images_ratio *= 1.2; mapper->ba_global_points_ratio *= 1.2; mapper->ba_global_max_refinements = 2; patch_match_stereo->max_image_size = 1000; patch_match_stereo->window_radius = 4; patch_match_stereo->window_step = 2; patch_match_stereo->num_samples /= 2; patch_match_stereo->num_iterations = 3; patch_match_stereo->geom_consistency = false; stereo_fusion->check_num_images /= 2; stereo_fusion->max_image_size = 1000; } void OptionManager::ModifyForMediumQuality() { sift_extraction->max_image_size = 1600; sift_extraction->max_num_features = 4096; sequential_matching->loop_detection_num_images /= 1.5; vocab_tree_matching->num_images /= 1.5; mapper->ba_local_max_num_iterations /= 1.5; mapper->ba_global_max_num_iterations /= 1.5; mapper->ba_global_images_ratio *= 1.1; mapper->ba_global_points_ratio *= 1.1; mapper->ba_global_max_refinements = 2; patch_match_stereo->max_image_size = 1600; patch_match_stereo->window_radius = 4; patch_match_stereo->window_step = 2; patch_match_stereo->num_samples /= 1.5; patch_match_stereo->num_iterations = 5; patch_match_stereo->geom_consistency = false; stereo_fusion->check_num_images /= 1.5; stereo_fusion->max_image_size = 1600; } void OptionManager::ModifyForHighQuality() { sift_extraction->estimate_affine_shape = true; sift_extraction->max_image_size = 2400; sift_extraction->max_num_features = 8192; sift_matching->guided_matching = true; mapper->ba_local_max_num_iterations = 30; mapper->ba_local_max_refinements = 3; mapper->ba_global_max_num_iterations = 75; patch_match_stereo->max_image_size = 2400; stereo_fusion->max_image_size = 2400; } void OptionManager::ModifyForExtremeQuality() { // Most of the options are set to extreme quality by default. sift_extraction->estimate_affine_shape = true; sift_extraction->domain_size_pooling = true; sift_matching->guided_matching = true; mapper->ba_local_max_num_iterations = 40; mapper->ba_local_max_refinements = 3; mapper->ba_global_max_num_iterations = 100; } void OptionManager::AddAllOptions() { AddLogOptions(); AddRandomOptions(); AddDatabaseOptions(); AddImageOptions(); AddExtractionOptions(); AddMatchingOptions(); AddExhaustiveMatchingOptions(); AddSequentialMatchingOptions(); AddVocabTreeMatchingOptions(); AddSpatialMatchingOptions(); AddTransitiveMatchingOptions(); AddImagePairsMatchingOptions(); AddBundleAdjustmentOptions(); AddMapperOptions(); AddPatchMatchStereoOptions(); AddStereoFusionOptions(); AddPoissonMeshingOptions(); AddDelaunayMeshingOptions(); AddRenderOptions(); } void OptionManager::AddLogOptions() { if (added_log_options_) { return; } added_log_options_ = true; AddAndRegisterDefaultOption("log_to_stderr", &FLAGS_logtostderr); AddAndRegisterDefaultOption("log_level", &FLAGS_v); } void OptionManager::AddRandomOptions() { if (added_random_options_) { return; } added_random_options_ = true; AddAndRegisterDefaultOption("random_seed", &kDefaultPRNGSeed); } void OptionManager::AddDatabaseOptions() { if (added_database_options_) { return; } added_database_options_ = true; AddAndRegisterRequiredOption("database_path", database_path.get()); } void OptionManager::AddImageOptions() { if (added_image_options_) { return; } added_image_options_ = true; AddAndRegisterRequiredOption("image_path", image_path.get()); } void OptionManager::AddExtractionOptions() { if (added_extraction_options_) { return; } added_extraction_options_ = true; AddAndRegisterDefaultOption("ImageReader.mask_path", &image_reader->mask_path); AddAndRegisterDefaultOption("ImageReader.camera_model", &image_reader->camera_model); AddAndRegisterDefaultOption("ImageReader.single_camera", &image_reader->single_camera); AddAndRegisterDefaultOption("ImageReader.single_camera_per_folder", &image_reader->single_camera_per_folder); AddAndRegisterDefaultOption("ImageReader.single_camera_per_image", &image_reader->single_camera_per_image); AddAndRegisterDefaultOption("ImageReader.existing_camera_id", &image_reader->existing_camera_id); AddAndRegisterDefaultOption("ImageReader.camera_params", &image_reader->camera_params); AddAndRegisterDefaultOption("ImageReader.default_focal_length_factor", &image_reader->default_focal_length_factor); AddAndRegisterDefaultOption("ImageReader.camera_mask_path", &image_reader->camera_mask_path); AddAndRegisterDefaultOption("SiftExtraction.num_threads", &sift_extraction->num_threads); AddAndRegisterDefaultOption("SiftExtraction.use_gpu", &sift_extraction->use_gpu); AddAndRegisterDefaultOption("SiftExtraction.gpu_index", &sift_extraction->gpu_index); AddAndRegisterDefaultOption("SiftExtraction.max_image_size", &sift_extraction->max_image_size); AddAndRegisterDefaultOption("SiftExtraction.max_num_features", &sift_extraction->max_num_features); AddAndRegisterDefaultOption("SiftExtraction.first_octave", &sift_extraction->first_octave); AddAndRegisterDefaultOption("SiftExtraction.num_octaves", &sift_extraction->num_octaves); AddAndRegisterDefaultOption("SiftExtraction.octave_resolution", &sift_extraction->octave_resolution); AddAndRegisterDefaultOption("SiftExtraction.peak_threshold", &sift_extraction->peak_threshold); AddAndRegisterDefaultOption("SiftExtraction.edge_threshold", &sift_extraction->edge_threshold); AddAndRegisterDefaultOption("SiftExtraction.estimate_affine_shape", &sift_extraction->estimate_affine_shape); AddAndRegisterDefaultOption("SiftExtraction.max_num_orientations", &sift_extraction->max_num_orientations); AddAndRegisterDefaultOption("SiftExtraction.upright", &sift_extraction->upright); AddAndRegisterDefaultOption("SiftExtraction.domain_size_pooling", &sift_extraction->domain_size_pooling); AddAndRegisterDefaultOption("SiftExtraction.dsp_min_scale", &sift_extraction->dsp_min_scale); AddAndRegisterDefaultOption("SiftExtraction.dsp_max_scale", &sift_extraction->dsp_max_scale); AddAndRegisterDefaultOption("SiftExtraction.dsp_num_scales", &sift_extraction->dsp_num_scales); } void OptionManager::AddMatchingOptions() { if (added_match_options_) { return; } added_match_options_ = true; AddAndRegisterDefaultOption("SiftMatching.num_threads", &sift_matching->num_threads); AddAndRegisterDefaultOption("SiftMatching.use_gpu", &sift_matching->use_gpu); AddAndRegisterDefaultOption("SiftMatching.gpu_index", &sift_matching->gpu_index); AddAndRegisterDefaultOption("SiftMatching.max_ratio", &sift_matching->max_ratio); AddAndRegisterDefaultOption("SiftMatching.max_distance", &sift_matching->max_distance); AddAndRegisterDefaultOption("SiftMatching.cross_check", &sift_matching->cross_check); AddAndRegisterDefaultOption("SiftMatching.guided_matching", &sift_matching->guided_matching); AddAndRegisterDefaultOption("SiftMatching.max_num_matches", &sift_matching->max_num_matches); AddAndRegisterDefaultOption("TwoViewGeometry.min_num_inliers", &two_view_geometry->min_num_inliers); AddAndRegisterDefaultOption("TwoViewGeometry.multiple_models", &two_view_geometry->multiple_models); AddAndRegisterDefaultOption("TwoViewGeometry.compute_relative_pose", &two_view_geometry->compute_relative_pose); AddAndRegisterDefaultOption("TwoViewGeometry.max_error", &two_view_geometry->ransac_options.max_error); AddAndRegisterDefaultOption("TwoViewGeometry.confidence", &two_view_geometry->ransac_options.confidence); AddAndRegisterDefaultOption( "TwoViewGeometry.max_num_trials", &two_view_geometry->ransac_options.max_num_trials); AddAndRegisterDefaultOption( "TwoViewGeometry.min_inlier_ratio", &two_view_geometry->ransac_options.min_inlier_ratio); } void OptionManager::AddExhaustiveMatchingOptions() { if (added_exhaustive_match_options_) { return; } added_exhaustive_match_options_ = true; AddMatchingOptions(); AddAndRegisterDefaultOption("ExhaustiveMatching.block_size", &exhaustive_matching->block_size); } void OptionManager::AddSequentialMatchingOptions() { if (added_sequential_match_options_) { return; } added_sequential_match_options_ = true; AddMatchingOptions(); AddAndRegisterDefaultOption("SequentialMatching.overlap", &sequential_matching->overlap); AddAndRegisterDefaultOption("SequentialMatching.quadratic_overlap", &sequential_matching->quadratic_overlap); AddAndRegisterDefaultOption("SequentialMatching.loop_detection", &sequential_matching->loop_detection); AddAndRegisterDefaultOption("SequentialMatching.loop_detection_period", &sequential_matching->loop_detection_period); AddAndRegisterDefaultOption("SequentialMatching.loop_detection_num_images", &sequential_matching->loop_detection_num_images); AddAndRegisterDefaultOption( "SequentialMatching.loop_detection_num_nearest_neighbors", &sequential_matching->loop_detection_num_nearest_neighbors); AddAndRegisterDefaultOption("SequentialMatching.loop_detection_num_checks", &sequential_matching->loop_detection_num_checks); AddAndRegisterDefaultOption( "SequentialMatching.loop_detection_num_images_after_verification", &sequential_matching->loop_detection_num_images_after_verification); AddAndRegisterDefaultOption( "SequentialMatching.loop_detection_max_num_features", &sequential_matching->loop_detection_max_num_features); AddAndRegisterDefaultOption("SequentialMatching.vocab_tree_path", &sequential_matching->vocab_tree_path); } void OptionManager::AddVocabTreeMatchingOptions() { if (added_vocab_tree_match_options_) { return; } added_vocab_tree_match_options_ = true; AddMatchingOptions(); AddAndRegisterDefaultOption("VocabTreeMatching.num_images", &vocab_tree_matching->num_images); AddAndRegisterDefaultOption("VocabTreeMatching.num_nearest_neighbors", &vocab_tree_matching->num_nearest_neighbors); AddAndRegisterDefaultOption("VocabTreeMatching.num_checks", &vocab_tree_matching->num_checks); AddAndRegisterDefaultOption( "VocabTreeMatching.num_images_after_verification", &vocab_tree_matching->num_images_after_verification); AddAndRegisterDefaultOption("VocabTreeMatching.max_num_features", &vocab_tree_matching->max_num_features); AddAndRegisterDefaultOption("VocabTreeMatching.vocab_tree_path", &vocab_tree_matching->vocab_tree_path); AddAndRegisterDefaultOption("VocabTreeMatching.match_list_path", &vocab_tree_matching->match_list_path); } void OptionManager::AddSpatialMatchingOptions() { if (added_spatial_match_options_) { return; } added_spatial_match_options_ = true; AddMatchingOptions(); AddAndRegisterDefaultOption("SpatialMatching.is_gps", &spatial_matching->is_gps); AddAndRegisterDefaultOption("SpatialMatching.ignore_z", &spatial_matching->ignore_z); AddAndRegisterDefaultOption("SpatialMatching.max_num_neighbors", &spatial_matching->max_num_neighbors); AddAndRegisterDefaultOption("SpatialMatching.max_distance", &spatial_matching->max_distance); } void OptionManager::AddTransitiveMatchingOptions() { if (added_transitive_match_options_) { return; } added_transitive_match_options_ = true; AddMatchingOptions(); AddAndRegisterDefaultOption("TransitiveMatching.batch_size", &transitive_matching->batch_size); AddAndRegisterDefaultOption("TransitiveMatching.num_iterations", &transitive_matching->num_iterations); } void OptionManager::AddImagePairsMatchingOptions() { if (added_image_pairs_match_options_) { return; } added_image_pairs_match_options_ = true; AddMatchingOptions(); AddAndRegisterDefaultOption("ImagePairsMatching.block_size", &image_pairs_matching->block_size); } void OptionManager::AddBundleAdjustmentOptions() { if (added_ba_options_) { return; } added_ba_options_ = true; AddAndRegisterDefaultOption( "BundleAdjustment.max_num_iterations", &bundle_adjustment->solver_options.max_num_iterations); AddAndRegisterDefaultOption( "BundleAdjustment.max_linear_solver_iterations", &bundle_adjustment->solver_options.max_linear_solver_iterations); AddAndRegisterDefaultOption( "BundleAdjustment.function_tolerance", &bundle_adjustment->solver_options.function_tolerance); AddAndRegisterDefaultOption( "BundleAdjustment.gradient_tolerance", &bundle_adjustment->solver_options.gradient_tolerance); AddAndRegisterDefaultOption( "BundleAdjustment.parameter_tolerance", &bundle_adjustment->solver_options.parameter_tolerance); AddAndRegisterDefaultOption("BundleAdjustment.refine_focal_length", &bundle_adjustment->refine_focal_length); AddAndRegisterDefaultOption("BundleAdjustment.refine_principal_point", &bundle_adjustment->refine_principal_point); AddAndRegisterDefaultOption("BundleAdjustment.refine_extra_params", &bundle_adjustment->refine_extra_params); AddAndRegisterDefaultOption("BundleAdjustment.refine_extrinsics", &bundle_adjustment->refine_extrinsics); } void OptionManager::AddMapperOptions() { if (added_mapper_options_) { return; } added_mapper_options_ = true; AddAndRegisterDefaultOption("Mapper.min_num_matches", &mapper->min_num_matches); AddAndRegisterDefaultOption("Mapper.ignore_watermarks", &mapper->ignore_watermarks); AddAndRegisterDefaultOption("Mapper.multiple_models", &mapper->multiple_models); AddAndRegisterDefaultOption("Mapper.max_num_models", &mapper->max_num_models); AddAndRegisterDefaultOption("Mapper.max_model_overlap", &mapper->max_model_overlap); AddAndRegisterDefaultOption("Mapper.min_model_size", &mapper->min_model_size); AddAndRegisterDefaultOption("Mapper.init_image_id1", &mapper->init_image_id1); AddAndRegisterDefaultOption("Mapper.init_image_id2", &mapper->init_image_id2); AddAndRegisterDefaultOption("Mapper.init_num_trials", &mapper->init_num_trials); AddAndRegisterDefaultOption("Mapper.extract_colors", &mapper->extract_colors); AddAndRegisterDefaultOption("Mapper.num_threads", &mapper->num_threads); AddAndRegisterDefaultOption("Mapper.min_focal_length_ratio", &mapper->min_focal_length_ratio); AddAndRegisterDefaultOption("Mapper.max_focal_length_ratio", &mapper->max_focal_length_ratio); AddAndRegisterDefaultOption("Mapper.max_extra_param", &mapper->max_extra_param); AddAndRegisterDefaultOption("Mapper.ba_refine_focal_length", &mapper->ba_refine_focal_length); AddAndRegisterDefaultOption("Mapper.ba_refine_principal_point", &mapper->ba_refine_principal_point); AddAndRegisterDefaultOption("Mapper.ba_refine_extra_params", &mapper->ba_refine_extra_params); AddAndRegisterDefaultOption( "Mapper.ba_min_num_residuals_for_multi_threading", &mapper->ba_min_num_residuals_for_multi_threading); AddAndRegisterDefaultOption("Mapper.ba_local_num_images", &mapper->ba_local_num_images); AddAndRegisterDefaultOption("Mapper.ba_local_function_tolerance", &mapper->ba_local_function_tolerance); AddAndRegisterDefaultOption("Mapper.ba_local_max_num_iterations", &mapper->ba_local_max_num_iterations); AddAndRegisterDefaultOption("Mapper.ba_global_images_ratio", &mapper->ba_global_images_ratio); AddAndRegisterDefaultOption("Mapper.ba_global_points_ratio", &mapper->ba_global_points_ratio); AddAndRegisterDefaultOption("Mapper.ba_global_images_freq", &mapper->ba_global_images_freq); AddAndRegisterDefaultOption("Mapper.ba_global_points_freq", &mapper->ba_global_points_freq); AddAndRegisterDefaultOption("Mapper.ba_global_function_tolerance", &mapper->ba_global_function_tolerance); AddAndRegisterDefaultOption("Mapper.ba_global_max_num_iterations", &mapper->ba_global_max_num_iterations); AddAndRegisterDefaultOption("Mapper.ba_global_max_refinements", &mapper->ba_global_max_refinements); AddAndRegisterDefaultOption("Mapper.ba_global_max_refinement_change", &mapper->ba_global_max_refinement_change); AddAndRegisterDefaultOption("Mapper.ba_local_max_refinements", &mapper->ba_local_max_refinements); AddAndRegisterDefaultOption("Mapper.ba_local_max_refinement_change", &mapper->ba_local_max_refinement_change); AddAndRegisterDefaultOption("Mapper.snapshot_path", &mapper->snapshot_path); AddAndRegisterDefaultOption("Mapper.snapshot_images_freq", &mapper->snapshot_images_freq); AddAndRegisterDefaultOption("Mapper.fix_existing_images", &mapper->fix_existing_images); // IncrementalMapper. AddAndRegisterDefaultOption("Mapper.init_min_num_inliers", &mapper->mapper.init_min_num_inliers); AddAndRegisterDefaultOption("Mapper.init_max_error", &mapper->mapper.init_max_error); AddAndRegisterDefaultOption("Mapper.init_max_forward_motion", &mapper->mapper.init_max_forward_motion); AddAndRegisterDefaultOption("Mapper.init_min_tri_angle", &mapper->mapper.init_min_tri_angle); AddAndRegisterDefaultOption("Mapper.init_max_reg_trials", &mapper->mapper.init_max_reg_trials); AddAndRegisterDefaultOption("Mapper.abs_pose_max_error", &mapper->mapper.abs_pose_max_error); AddAndRegisterDefaultOption("Mapper.abs_pose_min_num_inliers", &mapper->mapper.abs_pose_min_num_inliers); AddAndRegisterDefaultOption("Mapper.abs_pose_min_inlier_ratio", &mapper->mapper.abs_pose_min_inlier_ratio); AddAndRegisterDefaultOption("Mapper.filter_max_reproj_error", &mapper->mapper.filter_max_reproj_error); AddAndRegisterDefaultOption("Mapper.filter_min_tri_angle", &mapper->mapper.filter_min_tri_angle); AddAndRegisterDefaultOption("Mapper.max_reg_trials", &mapper->mapper.max_reg_trials); AddAndRegisterDefaultOption("Mapper.local_ba_min_tri_angle", &mapper->mapper.local_ba_min_tri_angle); // IncrementalTriangulator. AddAndRegisterDefaultOption("Mapper.tri_max_transitivity", &mapper->triangulation.max_transitivity); AddAndRegisterDefaultOption("Mapper.tri_create_max_angle_error", &mapper->triangulation.create_max_angle_error); AddAndRegisterDefaultOption("Mapper.tri_continue_max_angle_error", &mapper->triangulation.continue_max_angle_error); AddAndRegisterDefaultOption("Mapper.tri_merge_max_reproj_error", &mapper->triangulation.merge_max_reproj_error); AddAndRegisterDefaultOption("Mapper.tri_complete_max_reproj_error", &mapper->triangulation.complete_max_reproj_error); AddAndRegisterDefaultOption("Mapper.tri_complete_max_transitivity", &mapper->triangulation.complete_max_transitivity); AddAndRegisterDefaultOption("Mapper.tri_re_max_angle_error", &mapper->triangulation.re_max_angle_error); AddAndRegisterDefaultOption("Mapper.tri_re_min_ratio", &mapper->triangulation.re_min_ratio); AddAndRegisterDefaultOption("Mapper.tri_re_max_trials", &mapper->triangulation.re_max_trials); AddAndRegisterDefaultOption("Mapper.tri_min_angle", &mapper->triangulation.min_angle); AddAndRegisterDefaultOption("Mapper.tri_ignore_two_view_tracks", &mapper->triangulation.ignore_two_view_tracks); } void OptionManager::AddPatchMatchStereoOptions() { if (added_patch_match_stereo_options_) { return; } added_patch_match_stereo_options_ = true; AddAndRegisterDefaultOption("PatchMatchStereo.max_image_size", &patch_match_stereo->max_image_size); AddAndRegisterDefaultOption("PatchMatchStereo.gpu_index", &patch_match_stereo->gpu_index); AddAndRegisterDefaultOption("PatchMatchStereo.depth_min", &patch_match_stereo->depth_min); AddAndRegisterDefaultOption("PatchMatchStereo.depth_max", &patch_match_stereo->depth_max); AddAndRegisterDefaultOption("PatchMatchStereo.window_radius", &patch_match_stereo->window_radius); AddAndRegisterDefaultOption("PatchMatchStereo.window_step", &patch_match_stereo->window_step); AddAndRegisterDefaultOption("PatchMatchStereo.sigma_spatial", &patch_match_stereo->sigma_spatial); AddAndRegisterDefaultOption("PatchMatchStereo.sigma_color", &patch_match_stereo->sigma_color); AddAndRegisterDefaultOption("PatchMatchStereo.num_samples", &patch_match_stereo->num_samples); AddAndRegisterDefaultOption("PatchMatchStereo.ncc_sigma", &patch_match_stereo->ncc_sigma); AddAndRegisterDefaultOption("PatchMatchStereo.min_triangulation_angle", &patch_match_stereo->min_triangulation_angle); AddAndRegisterDefaultOption("PatchMatchStereo.incident_angle_sigma", &patch_match_stereo->incident_angle_sigma); AddAndRegisterDefaultOption("PatchMatchStereo.num_iterations", &patch_match_stereo->num_iterations); AddAndRegisterDefaultOption("PatchMatchStereo.geom_consistency", &patch_match_stereo->geom_consistency); AddAndRegisterDefaultOption( "PatchMatchStereo.geom_consistency_regularizer", &patch_match_stereo->geom_consistency_regularizer); AddAndRegisterDefaultOption("PatchMatchStereo.geom_consistency_max_cost", &patch_match_stereo->geom_consistency_max_cost); AddAndRegisterDefaultOption("PatchMatchStereo.filter", &patch_match_stereo->filter); AddAndRegisterDefaultOption("PatchMatchStereo.filter_min_ncc", &patch_match_stereo->filter_min_ncc); AddAndRegisterDefaultOption( "PatchMatchStereo.filter_min_triangulation_angle", &patch_match_stereo->filter_min_triangulation_angle); AddAndRegisterDefaultOption("PatchMatchStereo.filter_min_num_consistent", &patch_match_stereo->filter_min_num_consistent); AddAndRegisterDefaultOption( "PatchMatchStereo.filter_geom_consistency_max_cost", &patch_match_stereo->filter_geom_consistency_max_cost); AddAndRegisterDefaultOption("PatchMatchStereo.cache_size", &patch_match_stereo->cache_size); AddAndRegisterDefaultOption("PatchMatchStereo.allow_missing_files", &patch_match_stereo->allow_missing_files); AddAndRegisterDefaultOption("PatchMatchStereo.write_consistency_graph", &patch_match_stereo->write_consistency_graph); } void OptionManager::AddStereoFusionOptions() { if (added_stereo_fusion_options_) { return; } added_stereo_fusion_options_ = true; AddAndRegisterDefaultOption("StereoFusion.mask_path", &stereo_fusion->mask_path); AddAndRegisterDefaultOption("StereoFusion.num_threads", &stereo_fusion->num_threads); AddAndRegisterDefaultOption("StereoFusion.max_image_size", &stereo_fusion->max_image_size); AddAndRegisterDefaultOption("StereoFusion.min_num_pixels", &stereo_fusion->min_num_pixels); AddAndRegisterDefaultOption("StereoFusion.max_num_pixels", &stereo_fusion->max_num_pixels); AddAndRegisterDefaultOption("StereoFusion.max_traversal_depth", &stereo_fusion->max_traversal_depth); AddAndRegisterDefaultOption("StereoFusion.max_reproj_error", &stereo_fusion->max_reproj_error); AddAndRegisterDefaultOption("StereoFusion.max_depth_error", &stereo_fusion->max_depth_error); AddAndRegisterDefaultOption("StereoFusion.max_normal_error", &stereo_fusion->max_normal_error); AddAndRegisterDefaultOption("StereoFusion.check_num_images", &stereo_fusion->check_num_images); AddAndRegisterDefaultOption("StereoFusion.cache_size", &stereo_fusion->cache_size); AddAndRegisterDefaultOption("StereoFusion.use_cache", &stereo_fusion->use_cache); } void OptionManager::AddPoissonMeshingOptions() { if (added_poisson_meshing_options_) { return; } added_poisson_meshing_options_ = true; AddAndRegisterDefaultOption("PoissonMeshing.point_weight", &poisson_meshing->point_weight); AddAndRegisterDefaultOption("PoissonMeshing.depth", &poisson_meshing->depth); AddAndRegisterDefaultOption("PoissonMeshing.color", &poisson_meshing->color); AddAndRegisterDefaultOption("PoissonMeshing.trim", &poisson_meshing->trim); AddAndRegisterDefaultOption("PoissonMeshing.num_threads", &poisson_meshing->num_threads); } void OptionManager::AddDelaunayMeshingOptions() { if (added_delaunay_meshing_options_) { return; } added_delaunay_meshing_options_ = true; AddAndRegisterDefaultOption("DelaunayMeshing.max_proj_dist", &delaunay_meshing->max_proj_dist); AddAndRegisterDefaultOption("DelaunayMeshing.max_depth_dist", &delaunay_meshing->max_depth_dist); AddAndRegisterDefaultOption("DelaunayMeshing.visibility_sigma", &delaunay_meshing->visibility_sigma); AddAndRegisterDefaultOption("DelaunayMeshing.distance_sigma_factor", &delaunay_meshing->distance_sigma_factor); AddAndRegisterDefaultOption("DelaunayMeshing.quality_regularization", &delaunay_meshing->quality_regularization); AddAndRegisterDefaultOption("DelaunayMeshing.max_side_length_factor", &delaunay_meshing->max_side_length_factor); AddAndRegisterDefaultOption("DelaunayMeshing.max_side_length_percentile", &delaunay_meshing->max_side_length_percentile); AddAndRegisterDefaultOption("DelaunayMeshing.num_threads", &delaunay_meshing->num_threads); } void OptionManager::AddRenderOptions() { if (added_render_options_) { return; } added_render_options_ = true; AddAndRegisterDefaultOption("Render.min_track_len", &render->min_track_len); AddAndRegisterDefaultOption("Render.max_error", &render->max_error); AddAndRegisterDefaultOption("Render.refresh_rate", &render->refresh_rate); AddAndRegisterDefaultOption("Render.adapt_refresh_rate", &render->adapt_refresh_rate); AddAndRegisterDefaultOption("Render.image_connections", &render->image_connections); AddAndRegisterDefaultOption("Render.projection_type", &render->projection_type); } void OptionManager::Reset() { FLAGS_logtostderr = true; const bool kResetPaths = true; ResetOptions(kResetPaths); desc_ = std::make_shared(); options_bool_.clear(); options_int_.clear(); options_double_.clear(); options_string_.clear(); added_log_options_ = false; added_random_options_ = false; added_database_options_ = false; added_image_options_ = false; added_extraction_options_ = false; added_match_options_ = false; added_exhaustive_match_options_ = false; added_sequential_match_options_ = false; added_vocab_tree_match_options_ = false; added_spatial_match_options_ = false; added_transitive_match_options_ = false; added_image_pairs_match_options_ = false; added_ba_options_ = false; added_mapper_options_ = false; added_patch_match_stereo_options_ = false; added_stereo_fusion_options_ = false; added_poisson_meshing_options_ = false; added_delaunay_meshing_options_ = false; added_render_options_ = false; } void OptionManager::ResetOptions(const bool reset_paths) { if (reset_paths) { *project_path = ""; *database_path = ""; *image_path = ""; } *image_reader = ImageReaderOptions(); *sift_extraction = SiftExtractionOptions(); *sift_matching = SiftMatchingOptions(); *exhaustive_matching = ExhaustiveMatchingOptions(); *sequential_matching = SequentialMatchingOptions(); *vocab_tree_matching = VocabTreeMatchingOptions(); *spatial_matching = SpatialMatchingOptions(); *transitive_matching = TransitiveMatchingOptions(); *image_pairs_matching = ImagePairsMatchingOptions(); *bundle_adjustment = BundleAdjustmentOptions(); *mapper = IncrementalMapperOptions(); *patch_match_stereo = mvs::PatchMatchOptions(); *stereo_fusion = mvs::StereoFusionOptions(); *poisson_meshing = mvs::PoissonMeshingOptions(); *delaunay_meshing = mvs::DelaunayMeshingOptions(); *render = RenderOptions(); } bool OptionManager::Check() { bool success = true; if (added_database_options_) { const auto database_parent_path = GetParentDir(*database_path); success = success && CHECK_OPTION_IMPL(!ExistsDir(*database_path)) && CHECK_OPTION_IMPL(database_parent_path == "" || ExistsDir(database_parent_path)); } if (added_image_options_) success = success && CHECK_OPTION_IMPL(ExistsDir(*image_path)); if (image_reader) success = success && image_reader->Check(); if (sift_extraction) success = success && sift_extraction->Check(); if (sift_matching) success = success && sift_matching->Check(); if (two_view_geometry) success = success && two_view_geometry->Check(); if (exhaustive_matching) success = success && exhaustive_matching->Check(); if (sequential_matching) success = success && sequential_matching->Check(); if (vocab_tree_matching) success = success && vocab_tree_matching->Check(); if (spatial_matching) success = success && spatial_matching->Check(); if (transitive_matching) success = success && transitive_matching->Check(); if (image_pairs_matching) success = success && image_pairs_matching->Check(); if (bundle_adjustment) success = success && bundle_adjustment->Check(); if (mapper) success = success && mapper->Check(); if (patch_match_stereo) success = success && patch_match_stereo->Check(); if (stereo_fusion) success = success && stereo_fusion->Check(); if (poisson_meshing) success = success && poisson_meshing->Check(); if (delaunay_meshing) success = success && delaunay_meshing->Check(); #if defined(COLMAP_GUI_ENABLED) if (render) success = success && render->Check(); #endif return success; } void OptionManager::Parse(const int argc, char** argv) { config::variables_map vmap; try { config::store(config::parse_command_line(argc, argv, *desc_), vmap); if (vmap.count("help")) { LOG(INFO) << StringPrintf( "%s (%s)", GetVersionInfo().c_str(), GetBuildInfo().c_str()); LOG(INFO) << "Options can either be specified via command-line or by defining " "them in a .ini project file passed to `--project_path`.\n" << *desc_; // NOLINTNEXTLINE(concurrency-mt-unsafe) exit(EXIT_SUCCESS); } if (vmap.count("project_path")) { *project_path = vmap["project_path"].as(); if (!Read(*project_path)) { // NOLINTNEXTLINE(concurrency-mt-unsafe) exit(EXIT_FAILURE); } } else { vmap.notify(); } } catch (std::exception& exc) { LOG(ERROR) << "Failed to parse options - " << exc.what() << "."; // NOLINTNEXTLINE(concurrency-mt-unsafe) exit(EXIT_FAILURE); } catch (...) { LOG(ERROR) << "Failed to parse options for unknown reason."; // NOLINTNEXTLINE(concurrency-mt-unsafe) exit(EXIT_FAILURE); } if (!Check()) { LOG(ERROR) << "Invalid options provided."; // NOLINTNEXTLINE(concurrency-mt-unsafe) exit(EXIT_FAILURE); } } bool OptionManager::Read(const std::string& path) { config::variables_map vmap; if (!ExistsFile(path)) { LOG(ERROR) << "Configuration file does not exist."; return false; } try { std::ifstream file(path); CHECK(file.is_open()) << path; config::store(config::parse_config_file(file, *desc_), vmap); vmap.notify(); } catch (std::exception& e) { LOG(ERROR) << "Failed to parse options " << e.what() << "."; return false; } catch (...) { LOG(ERROR) << "Failed to parse options for unknown reason."; return false; } return Check(); } bool OptionManager::ReRead(const std::string& path) { Reset(); AddAllOptions(); return Read(path); } void OptionManager::Write(const std::string& path) const { boost::property_tree::ptree pt; // First, put all options without a section and then those with a section. // This is necessary as otherwise older Boost versions will write the // options without a section in between other sections and therefore // the errors will be assigned to the wrong section if read later. for (const auto& option : options_bool_) { if (!StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } for (const auto& option : options_int_) { if (!StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } for (const auto& option : options_double_) { if (!StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } for (const auto& option : options_string_) { if (!StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } for (const auto& option : options_bool_) { if (StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } for (const auto& option : options_int_) { if (StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } for (const auto& option : options_double_) { if (StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } for (const auto& option : options_string_) { if (StringContains(option.first, ".")) { pt.put(option.first, *option.second); } } boost::property_tree::write_ini(path, pt); } } // namespace colmap colmap-3.9.1/src/colmap/controllers/option_manager.h000066400000000000000000000222661454702036400225770ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include #include namespace colmap { struct ImageReaderOptions; struct SiftExtractionOptions; struct SiftMatchingOptions; struct TwoViewGeometryOptions; struct ExhaustiveMatchingOptions; struct SequentialMatchingOptions; struct VocabTreeMatchingOptions; struct SpatialMatchingOptions; struct TransitiveMatchingOptions; struct ImagePairsMatchingOptions; struct BundleAdjustmentOptions; struct IncrementalMapperOptions; struct RenderOptions; namespace mvs { struct PatchMatchOptions; struct StereoFusionOptions; struct PoissonMeshingOptions; struct DelaunayMeshingOptions; } // namespace mvs class OptionManager { public: explicit OptionManager(bool add_project_options = true); // Create "optimal" set of options for different reconstruction scenarios. // Note that the existing options are modified, so if your parameters are // already low quality, they will be further modified. void ModifyForIndividualData(); void ModifyForVideoData(); void ModifyForInternetData(); // Create "optimal" set of options for different quality settings. // Note that the existing options are modified, so if your parameters are // already low quality, they will be further degraded. void ModifyForLowQuality(); void ModifyForMediumQuality(); void ModifyForHighQuality(); void ModifyForExtremeQuality(); void AddAllOptions(); void AddLogOptions(); void AddRandomOptions(); void AddDatabaseOptions(); void AddImageOptions(); void AddExtractionOptions(); void AddMatchingOptions(); void AddExhaustiveMatchingOptions(); void AddSequentialMatchingOptions(); void AddVocabTreeMatchingOptions(); void AddSpatialMatchingOptions(); void AddTransitiveMatchingOptions(); void AddImagePairsMatchingOptions(); void AddBundleAdjustmentOptions(); void AddMapperOptions(); void AddPatchMatchStereoOptions(); void AddStereoFusionOptions(); void AddPoissonMeshingOptions(); void AddDelaunayMeshingOptions(); void AddRenderOptions(); template void AddRequiredOption(const std::string& name, T* option, const std::string& help_text = ""); template void AddDefaultOption(const std::string& name, T* option, const std::string& help_text = ""); void Reset(); void ResetOptions(bool reset_paths); bool Check(); void Parse(int argc, char** argv); bool Read(const std::string& path); bool ReRead(const std::string& path); void Write(const std::string& path) const; std::shared_ptr project_path; std::shared_ptr database_path; std::shared_ptr image_path; std::shared_ptr image_reader; std::shared_ptr sift_extraction; std::shared_ptr sift_matching; std::shared_ptr two_view_geometry; std::shared_ptr exhaustive_matching; std::shared_ptr sequential_matching; std::shared_ptr vocab_tree_matching; std::shared_ptr spatial_matching; std::shared_ptr transitive_matching; std::shared_ptr image_pairs_matching; std::shared_ptr bundle_adjustment; std::shared_ptr mapper; std::shared_ptr patch_match_stereo; std::shared_ptr stereo_fusion; std::shared_ptr poisson_meshing; std::shared_ptr delaunay_meshing; std::shared_ptr render; private: template void AddAndRegisterRequiredOption(const std::string& name, T* option, const std::string& help_text = ""); template void AddAndRegisterDefaultOption(const std::string& name, T* option, const std::string& help_text = ""); template void RegisterOption(const std::string& name, const T* option); std::shared_ptr desc_; std::vector> options_bool_; std::vector> options_int_; std::vector> options_double_; std::vector> options_string_; bool added_log_options_; bool added_random_options_; bool added_database_options_; bool added_image_options_; bool added_extraction_options_; bool added_match_options_; bool added_exhaustive_match_options_; bool added_sequential_match_options_; bool added_vocab_tree_match_options_; bool added_spatial_match_options_; bool added_transitive_match_options_; bool added_image_pairs_match_options_; bool added_ba_options_; bool added_mapper_options_; bool added_patch_match_stereo_options_; bool added_stereo_fusion_options_; bool added_poisson_meshing_options_; bool added_delaunay_meshing_options_; bool added_render_options_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template void OptionManager::AddRequiredOption(const std::string& name, T* option, const std::string& help_text) { desc_->add_options()(name.c_str(), boost::program_options::value(option)->required(), help_text.c_str()); } template void OptionManager::AddDefaultOption(const std::string& name, T* option, const std::string& help_text) { desc_->add_options()( name.c_str(), boost::program_options::value(option)->default_value(*option), help_text.c_str()); } template void OptionManager::AddAndRegisterRequiredOption(const std::string& name, T* option, const std::string& help_text) { desc_->add_options()(name.c_str(), boost::program_options::value(option)->required(), help_text.c_str()); RegisterOption(name, option); } template void OptionManager::AddAndRegisterDefaultOption(const std::string& name, T* option, const std::string& help_text) { desc_->add_options()( name.c_str(), boost::program_options::value(option)->default_value(*option), help_text.c_str()); RegisterOption(name, option); } template void OptionManager::RegisterOption(const std::string& name, const T* option) { if (std::is_same::value) { options_bool_.emplace_back(name, reinterpret_cast(option)); } else if (std::is_same::value) { options_int_.emplace_back(name, reinterpret_cast(option)); } else if (std::is_same::value) { options_double_.emplace_back(name, reinterpret_cast(option)); } else if (std::is_same::value) { options_string_.emplace_back(name, reinterpret_cast(option)); } else { LOG(FATAL) << "Unsupported option type"; } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/000077500000000000000000000000001454702036400172405ustar00rootroot00000000000000colmap-3.9.1/src/colmap/estimators/CMakeLists.txt000066400000000000000000000110721454702036400220010ustar00rootroot00000000000000# 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. set(FOLDER_NAME "estimators") COLMAP_ADD_LIBRARY( NAME colmap_estimators SRCS absolute_pose.h absolute_pose.cc affine_transform.h affine_transform.cc alignment.h alignment.cc bundle_adjustment.h bundle_adjustment.cc coordinate_frame.h coordinate_frame.cc cost_functions.h essential_matrix.h essential_matrix.cc euclidean_transform.h fundamental_matrix.h fundamental_matrix.cc generalized_absolute_pose.h generalized_absolute_pose.cc generalized_absolute_pose_coeffs.h generalized_absolute_pose_coeffs.cc generalized_relative_pose.h generalized_relative_pose.cc homography_matrix.h homography_matrix.cc pose.h pose.cc generalized_pose.h generalized_pose.cc similarity_transform.h translation_transform.h triangulation.h triangulation.cc two_view_geometry.h two_view_geometry.cc utils.h utils.cc PUBLIC_LINK_LIBS colmap_util colmap_math colmap_feature colmap_geometry colmap_sensor colmap_image colmap_scene colmap_optim Eigen3::Eigen Ceres::ceres ) COLMAP_ADD_TEST( NAME absolute_pose_test SRCS absolute_pose_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME affine_transform_test SRCS affine_transform_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME alignment_test SRCS alignment_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME bundle_adjustment_test SRCS bundle_adjustment_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME coordinate_frame_test SRCS coordinate_frame_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME cost_functions_test SRCS cost_functions_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME essential_matrix_test SRCS essential_matrix_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME estimators_utils_test SRCS utils_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME fundamental_matrix_test SRCS fundamental_matrix_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME generalized_absolute_pose_test SRCS generalized_absolute_pose_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME generalized_pose_test SRCS generalized_pose_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME generalized_relative_pose_test SRCS generalized_relative_pose_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME homography_matrix_test SRCS homography_matrix_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME pose_test SRCS pose_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME similarity_transform_test SRCS similarity_transform_test.cc LINK_LIBS colmap_estimators ) COLMAP_ADD_TEST( NAME translation_transform_test SRCS translation_transform_test.cc LINK_LIBS colmap_estimators ) colmap-3.9.1/src/colmap/estimators/absolute_pose.cc000066400000000000000000000507601454702036400224230ustar00rootroot00000000000000// 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. #include "colmap/estimators/absolute_pose.h" #include "colmap/estimators/utils.h" #include "colmap/math/polynomial.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include namespace colmap { void P3PEstimator::Estimate(const std::vector& points2D, const std::vector& points3D, std::vector* models) { CHECK_EQ(points2D.size(), 3); CHECK_EQ(points3D.size(), 3); CHECK(models != nullptr); models->clear(); Eigen::Matrix3d points3D_world; points3D_world.col(0) = points3D[0]; points3D_world.col(1) = points3D[1]; points3D_world.col(2) = points3D[2]; const Eigen::Vector3d u = points2D[0].homogeneous().normalized(); const Eigen::Vector3d v = points2D[1].homogeneous().normalized(); const Eigen::Vector3d w = points2D[2].homogeneous().normalized(); // Angles between 2D points. const double cos_uv = u.transpose() * v; const double cos_uw = u.transpose() * w; const double cos_vw = v.transpose() * w; // Distances between 2D points. const double dist_AB_2 = (points3D[0] - points3D[1]).squaredNorm(); const double dist_AC_2 = (points3D[0] - points3D[2]).squaredNorm(); const double dist_BC_2 = (points3D[1] - points3D[2]).squaredNorm(); const double dist_AB = std::sqrt(dist_AB_2); const double a = dist_BC_2 / dist_AB_2; const double b = dist_AC_2 / dist_AB_2; // Helper variables for calculation of coefficients. const double a2 = a * a; const double b2 = b * b; const double p = 2 * cos_vw; const double q = 2 * cos_uw; const double r = 2 * cos_uv; const double p2 = p * p; const double p3 = p2 * p; const double q2 = q * q; const double r2 = r * r; const double r3 = r2 * r; const double r4 = r3 * r; const double r5 = r4 * r; // Build polynomial coefficients: a4*x^4 + a3*x^3 + a2*x^2 + a1*x + a0 = 0. Eigen::Matrix coeffs; coeffs(0) = -2 * b + b2 + a2 + 1 + a * b * (2 - r2) - 2 * a; coeffs(1) = -2 * q * a2 - r * p * b2 + 4 * q * a + (2 * q + p * r) * b + (r2 * q - 2 * q + r * p) * a * b - 2 * q; coeffs(2) = (2 + q2) * a2 + (p2 + r2 - 2) * b2 - (4 + 2 * q2) * a - (p * q * r + p2) * b - (p * q * r + r2) * a * b + q2 + 2; coeffs(3) = -2 * q * a2 - r * p * b2 + 4 * q * a + (p * r + q * p2 - 2 * q) * b + (r * p + 2 * q) * a * b - 2 * q; coeffs(4) = a2 + b2 - 2 * a + (2 - p2) * b - 2 * a * b + 1; Eigen::VectorXd roots_real; Eigen::VectorXd roots_imag; if (!FindPolynomialRootsCompanionMatrix(coeffs, &roots_real, &roots_imag)) { return; } models->reserve(roots_real.size()); for (Eigen::VectorXd::Index i = 0; i < roots_real.size(); ++i) { const double kMaxRootImag = 1e-10; if (std::abs(roots_imag(i)) > kMaxRootImag) { continue; } const double x = roots_real(i); if (x < 0) { continue; } const double x2 = x * x; const double x3 = x2 * x; // Build polynomial coefficients: b1*y + b0 = 0. const double bb1 = (p2 - p * q * r + r2) * a + (p2 - r2) * b - p2 + p * q * r - r2; const double b1 = b * bb1 * bb1; const double b0 = ((1 - a - b) * x2 + (a - 1) * q * x - a + b + 1) * (r3 * (a2 + b2 - 2 * a - 2 * b + (2 - r2) * a * b + 1) * x3 + r2 * (p + p * a2 - 2 * r * q * a * b + 2 * r * q * b - 2 * r * q - 2 * p * a - 2 * p * b + p * r2 * b + 4 * r * q * a + q * r3 * a * b - 2 * r * q * a2 + 2 * p * a * b + p * b2 - r2 * p * b2) * x2 + (r5 * (b2 - a * b) - r4 * p * q * b + r3 * (q2 - 4 * a - 2 * q2 * a + q2 * a2 + 2 * a2 - 2 * b2 + 2) + r2 * (4 * p * q * a - 2 * p * q * a * b + 2 * p * q * b - 2 * p * q - 2 * p * q * a2) + r * (p2 * b2 - 2 * p2 * b + 2 * p2 * a * b - 2 * p2 * a + p2 + p2 * a2)) * x + (2 * p * r2 - 2 * r3 * q + p3 - 2 * p2 * q * r + p * q2 * r2) * a2 + (p3 - 2 * p * r2) * b2 + (4 * q * r3 - 4 * p * r2 - 2 * p3 + 4 * p2 * q * r - 2 * p * q2 * r2) * a + (-2 * q * r3 + p * r4 + 2 * p2 * q * r - 2 * p3) * b + (2 * p3 + 2 * q * r3 - 2 * p2 * q * r) * a * b + p * q2 * r2 - 2 * p2 * q * r + 2 * p * r2 + p3 - 2 * r3 * q); // Solve for y. const double y = b0 / b1; const double y2 = y * y; const double nu = x2 + y2 - 2 * x * y * cos_uv; const double dist_PC = dist_AB / std::sqrt(nu); const double dist_PB = y * dist_PC; const double dist_PA = x * dist_PC; Eigen::Matrix3d points3D_camera; points3D_camera.col(0) = u * dist_PA; // A' points3D_camera.col(1) = v * dist_PB; // B' points3D_camera.col(2) = w * dist_PC; // C' // Find transformation from the world to the camera system. const Eigen::Matrix4d transform = Eigen::umeyama(points3D_world, points3D_camera, false); models->push_back(transform.topLeftCorner<3, 4>()); } } void P3PEstimator::Residuals(const std::vector& points2D, const std::vector& points3D, const M_t& proj_matrix, std::vector* residuals) { ComputeSquaredReprojectionError(points2D, points3D, proj_matrix, residuals); } void EPNPEstimator::Estimate(const std::vector& points2D, const std::vector& points3D, std::vector* models) { CHECK_GE(points2D.size(), 4); CHECK_EQ(points2D.size(), points3D.size()); CHECK(models != nullptr); models->clear(); EPNPEstimator epnp; M_t proj_matrix; if (!epnp.ComputePose(points2D, points3D, &proj_matrix)) { return; } models->resize(1); (*models)[0] = proj_matrix; } void EPNPEstimator::Residuals(const std::vector& points2D, const std::vector& points3D, const M_t& proj_matrix, std::vector* residuals) { ComputeSquaredReprojectionError(points2D, points3D, proj_matrix, residuals); } bool EPNPEstimator::ComputePose(const std::vector& points2D, const std::vector& points3D, Eigen::Matrix3x4d* proj_matrix) { points2D_ = &points2D; points3D_ = &points3D; ChooseControlPoints(); if (!ComputeBarycentricCoordinates()) { return false; } const Eigen::Matrix M = ComputeM(); const Eigen::Matrix MtM = M.transpose() * M; Eigen::JacobiSVD> svd( MtM, Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Matrix Ut = svd.matrixU().transpose(); const Eigen::Matrix L6x10 = ComputeL6x10(Ut); const Eigen::Matrix rho = ComputeRho(); Eigen::Vector4d betas[4]; std::array reproj_errors; std::array Rs; std::array ts; FindBetasApprox1(L6x10, rho, &betas[1]); RunGaussNewton(L6x10, rho, &betas[1]); reproj_errors[1] = ComputeRT(Ut, betas[1], &Rs[1], &ts[1]); FindBetasApprox2(L6x10, rho, &betas[2]); RunGaussNewton(L6x10, rho, &betas[2]); reproj_errors[2] = ComputeRT(Ut, betas[2], &Rs[2], &ts[2]); FindBetasApprox3(L6x10, rho, &betas[3]); RunGaussNewton(L6x10, rho, &betas[3]); reproj_errors[3] = ComputeRT(Ut, betas[3], &Rs[3], &ts[3]); int best_idx = 1; if (reproj_errors[2] < reproj_errors[1]) { best_idx = 2; } if (reproj_errors[3] < reproj_errors[best_idx]) { best_idx = 3; } proj_matrix->leftCols<3>() = Rs[best_idx]; proj_matrix->rightCols<1>() = ts[best_idx]; return true; } void EPNPEstimator::ChooseControlPoints() { // Take C0 as the reference points centroid: cws_[0].setZero(); for (size_t i = 0; i < points3D_->size(); ++i) { cws_[0] += (*points3D_)[i]; } cws_[0] /= points3D_->size(); Eigen::Matrix PW0(points3D_->size(), 3); for (size_t i = 0; i < points3D_->size(); ++i) { PW0.row(i) = (*points3D_)[i] - cws_[0]; } const Eigen::Matrix3d PW0tPW0 = PW0.transpose() * PW0; Eigen::JacobiSVD svd( PW0tPW0, Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Vector3d& D = svd.singularValues(); const Eigen::Matrix3d Ut = svd.matrixU().transpose(); for (int i = 1; i < 4; ++i) { const double k = std::sqrt(D(i - 1) / points3D_->size()); cws_[i] = cws_[0] + k * Ut.row(i - 1).transpose(); } } bool EPNPEstimator::ComputeBarycentricCoordinates() { Eigen::Matrix3d CC; for (int i = 0; i < 3; ++i) { for (int j = 1; j < 4; ++j) { CC(i, j - 1) = cws_[j][i] - cws_[0][i]; } } if (CC.colPivHouseholderQr().rank() < 3) { return false; } const Eigen::Matrix3d CC_inv = CC.inverse(); alphas_.resize(points2D_->size()); for (size_t i = 0; i < points3D_->size(); ++i) { for (int j = 0; j < 3; ++j) { alphas_[i][1 + j] = CC_inv(j, 0) * ((*points3D_)[i][0] - cws_[0][0]) + CC_inv(j, 1) * ((*points3D_)[i][1] - cws_[0][1]) + CC_inv(j, 2) * ((*points3D_)[i][2] - cws_[0][2]); } alphas_[i][0] = 1.0 - alphas_[i][1] - alphas_[i][2] - alphas_[i][3]; } return true; } Eigen::Matrix EPNPEstimator::ComputeM() { Eigen::Matrix M(2 * points2D_->size(), 12); for (size_t i = 0; i < points3D_->size(); ++i) { for (size_t j = 0; j < 4; ++j) { M(2 * i, 3 * j) = alphas_[i][j]; M(2 * i, 3 * j + 1) = 0.0; M(2 * i, 3 * j + 2) = -alphas_[i][j] * (*points2D_)[i].x(); M(2 * i + 1, 3 * j) = 0.0; M(2 * i + 1, 3 * j + 1) = alphas_[i][j]; M(2 * i + 1, 3 * j + 2) = -alphas_[i][j] * (*points2D_)[i].y(); } } return M; } Eigen::Matrix EPNPEstimator::ComputeL6x10( const Eigen::Matrix& Ut) { Eigen::Matrix L6x10; std::array, 4> dv; for (int i = 0; i < 4; ++i) { int a = 0, b = 1; for (int j = 0; j < 6; ++j) { dv[i][j][0] = Ut(11 - i, 3 * a) - Ut(11 - i, 3 * b); dv[i][j][1] = Ut(11 - i, 3 * a + 1) - Ut(11 - i, 3 * b + 1); dv[i][j][2] = Ut(11 - i, 3 * a + 2) - Ut(11 - i, 3 * b + 2); b += 1; if (b > 3) { a += 1; b = a + 1; } } } for (int i = 0; i < 6; ++i) { L6x10(i, 0) = dv[0][i].transpose() * dv[0][i]; L6x10(i, 1) = 2.0 * dv[0][i].transpose() * dv[1][i]; L6x10(i, 2) = dv[1][i].transpose() * dv[1][i]; L6x10(i, 3) = 2.0 * dv[0][i].transpose() * dv[2][i]; L6x10(i, 4) = 2.0 * dv[1][i].transpose() * dv[2][i]; L6x10(i, 5) = dv[2][i].transpose() * dv[2][i]; L6x10(i, 6) = 2.0 * dv[0][i].transpose() * dv[3][i]; L6x10(i, 7) = 2.0 * dv[1][i].transpose() * dv[3][i]; L6x10(i, 8) = 2.0 * dv[2][i].transpose() * dv[3][i]; L6x10(i, 9) = dv[3][i].transpose() * dv[3][i]; } return L6x10; } Eigen::Matrix EPNPEstimator::ComputeRho() { Eigen::Matrix rho; rho[0] = (cws_[0] - cws_[1]).squaredNorm(); rho[1] = (cws_[0] - cws_[2]).squaredNorm(); rho[2] = (cws_[0] - cws_[3]).squaredNorm(); rho[3] = (cws_[1] - cws_[2]).squaredNorm(); rho[4] = (cws_[1] - cws_[3]).squaredNorm(); rho[5] = (cws_[2] - cws_[3]).squaredNorm(); return rho; } // betas10 = [B11 B12 B22 B13 B23 B33 B14 B24 B34 B44] // betas_approx_1 = [B11 B12 B13 B14] void EPNPEstimator::FindBetasApprox1(const Eigen::Matrix& L6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas) { Eigen::Matrix L_6x4; for (int i = 0; i < 6; ++i) { L_6x4(i, 0) = L6x10(i, 0); L_6x4(i, 1) = L6x10(i, 1); L_6x4(i, 2) = L6x10(i, 3); L_6x4(i, 3) = L6x10(i, 6); } Eigen::JacobiSVD> svd( L_6x4, Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Matrix b4 = svd.solve(rho); if (b4[0] < 0) { (*betas)[0] = std::sqrt(-b4[0]); (*betas)[1] = -b4[1] / (*betas)[0]; (*betas)[2] = -b4[2] / (*betas)[0]; (*betas)[3] = -b4[3] / (*betas)[0]; } else { (*betas)[0] = std::sqrt(b4[0]); (*betas)[1] = b4[1] / (*betas)[0]; (*betas)[2] = b4[2] / (*betas)[0]; (*betas)[3] = b4[3] / (*betas)[0]; } } // betas10 = [B11 B12 B22 B13 B23 B33 B14 B24 B34 B44] // betas_approx_2 = [B11 B12 B22 ] void EPNPEstimator::FindBetasApprox2(const Eigen::Matrix& L6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas) { Eigen::Matrix L_6x3(6, 3); for (int i = 0; i < 6; ++i) { L_6x3(i, 0) = L6x10(i, 0); L_6x3(i, 1) = L6x10(i, 1); L_6x3(i, 2) = L6x10(i, 2); } Eigen::JacobiSVD> svd( L_6x3, Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Matrix b3 = svd.solve(rho); if (b3[0] < 0) { (*betas)[0] = std::sqrt(-b3[0]); (*betas)[1] = (b3[2] < 0) ? std::sqrt(-b3[2]) : 0.0; } else { (*betas)[0] = std::sqrt(b3[0]); (*betas)[1] = (b3[2] > 0) ? std::sqrt(b3[2]) : 0.0; } if (b3[1] < 0) { (*betas)[0] = -(*betas)[0]; } (*betas)[2] = 0.0; (*betas)[3] = 0.0; } // betas10 = [B11 B12 B22 B13 B23 B33 B14 B24 B34 B44] // betas_approx_3 = [B11 B12 B22 B13 B23 ] void EPNPEstimator::FindBetasApprox3(const Eigen::Matrix& L6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas) { Eigen::JacobiSVD> svd( L6x10.leftCols<5>(), Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Matrix b5 = svd.solve(rho); if (b5[0] < 0) { (*betas)[0] = std::sqrt(-b5[0]); (*betas)[1] = (b5[2] < 0) ? std::sqrt(-b5[2]) : 0.0; } else { (*betas)[0] = std::sqrt(b5[0]); (*betas)[1] = (b5[2] > 0) ? std::sqrt(b5[2]) : 0.0; } if (b5[1] < 0) { (*betas)[0] = -(*betas)[0]; } (*betas)[2] = b5[3] / (*betas)[0]; (*betas)[3] = 0.0; } void EPNPEstimator::RunGaussNewton(const Eigen::Matrix& L6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas) { Eigen::Matrix A; Eigen::Matrix b; const int kNumIterations = 5; for (int k = 0; k < kNumIterations; ++k) { for (int i = 0; i < 6; ++i) { A(i, 0) = 2 * L6x10(i, 0) * (*betas)[0] + L6x10(i, 1) * (*betas)[1] + L6x10(i, 3) * (*betas)[2] + L6x10(i, 6) * (*betas)[3]; A(i, 1) = L6x10(i, 1) * (*betas)[0] + 2 * L6x10(i, 2) * (*betas)[1] + L6x10(i, 4) * (*betas)[2] + L6x10(i, 7) * (*betas)[3]; A(i, 2) = L6x10(i, 3) * (*betas)[0] + L6x10(i, 4) * (*betas)[1] + 2 * L6x10(i, 5) * (*betas)[2] + L6x10(i, 8) * (*betas)[3]; A(i, 3) = L6x10(i, 6) * (*betas)[0] + L6x10(i, 7) * (*betas)[1] + L6x10(i, 8) * (*betas)[2] + 2 * L6x10(i, 9) * (*betas)[3]; b(i) = rho[i] - (L6x10(i, 0) * (*betas)[0] * (*betas)[0] + L6x10(i, 1) * (*betas)[0] * (*betas)[1] + L6x10(i, 2) * (*betas)[1] * (*betas)[1] + L6x10(i, 3) * (*betas)[0] * (*betas)[2] + L6x10(i, 4) * (*betas)[1] * (*betas)[2] + L6x10(i, 5) * (*betas)[2] * (*betas)[2] + L6x10(i, 6) * (*betas)[0] * (*betas)[3] + L6x10(i, 7) * (*betas)[1] * (*betas)[3] + L6x10(i, 8) * (*betas)[2] * (*betas)[3] + L6x10(i, 9) * (*betas)[3] * (*betas)[3]); } const Eigen::Vector4d x = A.colPivHouseholderQr().solve(b); (*betas) += x; } } double EPNPEstimator::ComputeRT(const Eigen::Matrix& Ut, const Eigen::Vector4d& betas, Eigen::Matrix3d* R, Eigen::Vector3d* t) { ComputeCcs(betas, Ut); ComputePcs(); SolveForSign(); EstimateRT(R, t); return ComputeTotalReprojectionError(*R, *t); } void EPNPEstimator::ComputeCcs(const Eigen::Vector4d& betas, const Eigen::Matrix& Ut) { for (int i = 0; i < 4; ++i) { ccs_[i][0] = ccs_[i][1] = ccs_[i][2] = 0.0; } for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { for (int k = 0; k < 3; ++k) { ccs_[j][k] += betas[i] * Ut(11 - i, 3 * j + k); } } } } void EPNPEstimator::ComputePcs() { pcs_.resize(points2D_->size()); for (size_t i = 0; i < points3D_->size(); ++i) { for (int j = 0; j < 3; ++j) { pcs_[i][j] = alphas_[i][0] * ccs_[0][j] + alphas_[i][1] * ccs_[1][j] + alphas_[i][2] * ccs_[2][j] + alphas_[i][3] * ccs_[3][j]; } } } void EPNPEstimator::SolveForSign() { if (pcs_[0][2] < 0.0) { for (int i = 0; i < 4; ++i) { ccs_[i] = -ccs_[i]; } for (size_t i = 0; i < points3D_->size(); ++i) { pcs_[i] = -pcs_[i]; } } } void EPNPEstimator::EstimateRT(Eigen::Matrix3d* R, Eigen::Vector3d* t) { Eigen::Vector3d pc0 = Eigen::Vector3d::Zero(); Eigen::Vector3d pw0 = Eigen::Vector3d::Zero(); for (size_t i = 0; i < points3D_->size(); ++i) { pc0 += pcs_[i]; pw0 += (*points3D_)[i]; } pc0 /= points3D_->size(); pw0 /= points3D_->size(); Eigen::Matrix3d abt = Eigen::Matrix3d::Zero(); for (size_t i = 0; i < points3D_->size(); ++i) { for (int j = 0; j < 3; ++j) { abt(j, 0) += (pcs_[i][j] - pc0[j]) * ((*points3D_)[i][0] - pw0[0]); abt(j, 1) += (pcs_[i][j] - pc0[j]) * ((*points3D_)[i][1] - pw0[1]); abt(j, 2) += (pcs_[i][j] - pc0[j]) * ((*points3D_)[i][2] - pw0[2]); } } Eigen::JacobiSVD svd( abt, Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Matrix3d& abt_U = svd.matrixU(); const Eigen::Matrix3d& abt_V = svd.matrixV(); for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { (*R)(i, j) = abt_U.row(i) * abt_V.row(j).transpose(); } } if (R->determinant() < 0) { Eigen::Matrix3d Abt_v_prime = abt_V; Abt_v_prime.col(2) = -abt_V.col(2); for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { (*R)(i, j) = abt_U.row(i) * Abt_v_prime.row(j).transpose(); } } } *t = pc0 - *R * pw0; } double EPNPEstimator::ComputeTotalReprojectionError(const Eigen::Matrix3d& R, const Eigen::Vector3d& t) { Eigen::Matrix3x4d proj_matrix; proj_matrix.leftCols<3>() = R; proj_matrix.rightCols<1>() = t; std::vector residuals; ComputeSquaredReprojectionError( *points2D_, *points3D_, proj_matrix, &residuals); double reproj_error = 0.0; for (const double residual : residuals) { reproj_error += std::sqrt(residual); } return reproj_error; } } // namespace colmap colmap-3.9.1/src/colmap/estimators/absolute_pose.h000066400000000000000000000165301454702036400222620ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include namespace colmap { // Analytic solver for the P3P (Perspective-Three-Point) problem. // // The algorithm is based on the following paper: // // X.S. Gao, X.-R. Hou, J. Tang, H.-F. Chang. Complete Solution // Classification for the Perspective-Three-Point Problem. // http://www.mmrc.iss.ac.cn/~xgao/paper/ieee.pdf class P3PEstimator { public: // The 2D image feature observations. typedef Eigen::Vector2d X_t; // The observed 3D features in the world frame. typedef Eigen::Vector3d Y_t; // The transformation from the world to the camera frame. typedef Eigen::Matrix3x4d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 3; // Estimate the most probable solution of the P3P problem from a set of // three 2D-3D point correspondences. // // @param points2D Normalized 2D image points as 3x2 matrix. // @param points3D 3D world points as 3x3 matrix. // // @return Most probable pose as length-1 vector of a 3x4 matrix. static void Estimate(const std::vector& points2D, const std::vector& points3D, std::vector* models); // Calculate the squared reprojection error given a set of 2D-3D point // correspondences and a projection matrix. // // @param points2D Normalized 2D image points as Nx2 matrix. // @param points3D 3D world points as Nx3 matrix. // @param proj_matrix 3x4 projection matrix. // @param residuals Output vector of residuals. static void Residuals(const std::vector& points2D, const std::vector& points3D, const M_t& proj_matrix, std::vector* residuals); }; // EPNP solver for the PNP (Perspective-N-Point) problem. The solver needs a // minimum of 4 2D-3D correspondences. // // The algorithm is based on the following paper: // // Lepetit, Vincent, Francesc Moreno-Noguer, and Pascal Fua. // "Epnp: An accurate o (n) solution to the pnp problem." // International journal of computer vision 81.2 (2009): 155-166. // // The implementation is based on their original open-source release, but is // ported to Eigen and contains several improvements over the original code. class EPNPEstimator { public: // The 2D image feature observations. typedef Eigen::Vector2d X_t; // The observed 3D features in the world frame. typedef Eigen::Vector3d Y_t; // The transformation from the world to the camera frame. typedef Eigen::Matrix3x4d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 4; // Estimate the most probable solution of the P3P problem from a set of // three 2D-3D point correspondences. // // @param points2D Normalized 2D image points as 3x2 matrix. // @param points3D 3D world points as 3x3 matrix. // // @return Most probable pose as length-1 vector of a 3x4 matrix. static void Estimate(const std::vector& points2D, const std::vector& points3D, std::vector* models); // Calculate the squared reprojection error given a set of 2D-3D point // correspondences and a projection matrix. // // @param points2D Normalized 2D image points as Nx2 matrix. // @param points3D 3D world points as Nx3 matrix. // @param proj_matrix 3x4 projection matrix. // @param residuals Output vector of residuals. static void Residuals(const std::vector& points2D, const std::vector& points3D, const M_t& proj_matrix, std::vector* residuals); private: bool ComputePose(const std::vector& points2D, const std::vector& points3D, Eigen::Matrix3x4d* proj_matrix); void ChooseControlPoints(); bool ComputeBarycentricCoordinates(); Eigen::Matrix ComputeM(); Eigen::Matrix ComputeL6x10( const Eigen::Matrix& Ut); Eigen::Matrix ComputeRho(); void FindBetasApprox1(const Eigen::Matrix& L_6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas); void FindBetasApprox2(const Eigen::Matrix& L_6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas); void FindBetasApprox3(const Eigen::Matrix& L_6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas); void RunGaussNewton(const Eigen::Matrix& L_6x10, const Eigen::Matrix& rho, Eigen::Vector4d* betas); double ComputeRT(const Eigen::Matrix& Ut, const Eigen::Vector4d& betas, Eigen::Matrix3d* R, Eigen::Vector3d* t); void ComputeCcs(const Eigen::Vector4d& betas, const Eigen::Matrix& Ut); void ComputePcs(); void SolveForSign(); void EstimateRT(Eigen::Matrix3d* R, Eigen::Vector3d* t); double ComputeTotalReprojectionError(const Eigen::Matrix3d& R, const Eigen::Vector3d& t); const std::vector* points2D_ = nullptr; const std::vector* points3D_ = nullptr; std::vector pcs_; std::vector alphas_; std::array cws_; std::array ccs_; }; } // namespace colmap colmap-3.9.1/src/colmap/estimators/absolute_pose_test.cc000066400000000000000000000204161454702036400234550ustar00rootroot00000000000000// 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. #include "colmap/estimators/absolute_pose.h" #include "colmap/estimators/essential_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/rigid3.h" #include "colmap/math/random.h" #include "colmap/optim/ransac.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(AbsolutePose, P3P) { const std::vector points3D = { Eigen::Vector3d(1, 1, 1), Eigen::Vector3d(0, 1, 1), Eigen::Vector3d(3, 1.0, 4), Eigen::Vector3d(3, 1.1, 4), Eigen::Vector3d(3, 1.2, 4), Eigen::Vector3d(3, 1.3, 4), Eigen::Vector3d(3, 1.4, 4), Eigen::Vector3d(2, 1, 7), }; auto points3D_faulty = points3D; for (size_t i = 0; i < points3D.size(); ++i) { points3D_faulty[i](0) = 20; } // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double qx = 0; qx < 1; qx += 0.2) { // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double tx = 0; tx < 1; tx += 0.1) { const Rigid3d expected_cam_from_world( Eigen::Quaterniond(1, qx, 0, 0).normalized(), Eigen::Vector3d(tx, 0, 0)); // Project points to camera coordinate system. std::vector points2D; for (size_t i = 0; i < points3D.size(); ++i) { points2D.push_back( (expected_cam_from_world * points3D[i]).hnormalized()); } RANSACOptions options; options.max_error = 1e-5; RANSAC ransac(options); const auto report = ransac.Estimate(points2D, points3D); EXPECT_TRUE(report.success); EXPECT_LT((expected_cam_from_world.ToMatrix() - report.model).norm(), 1e-2); // Test residuals of exact points. std::vector residuals; P3PEstimator::Residuals(points2D, points3D, report.model, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_TRUE(residuals[i] < 1e-3); } // Test residuals of faulty points. P3PEstimator::Residuals( points2D, points3D_faulty, report.model, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_TRUE(residuals[i] > 0.1); } } } } TEST(AbsolutePose, EPNP) { const std::vector points3D = { Eigen::Vector3d(1, 1, 1), Eigen::Vector3d(0, 1, 1), Eigen::Vector3d(3, 1.0, 4), Eigen::Vector3d(3, 1.1, 4), Eigen::Vector3d(3, 1.2, 4), Eigen::Vector3d(3, 1.3, 4), Eigen::Vector3d(3, 1.4, 4), Eigen::Vector3d(2, 1, 7), }; auto points3D_faulty = points3D; for (size_t i = 0; i < points3D.size(); ++i) { points3D_faulty[i](0) = 20; } // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double qx = 0; qx < 1; qx += 0.2) { // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double tx = 0; tx < 1; tx += 0.1) { const Rigid3d expected_cam_from_world( Eigen::Quaterniond(1, qx, 0, 0).normalized(), Eigen::Vector3d(tx, 0, 0)); // Project points to camera coordinate system. std::vector points2D; for (size_t i = 0; i < points3D.size(); ++i) { points2D.push_back( (expected_cam_from_world * points3D[i]).hnormalized()); } RANSACOptions options; options.max_error = 1e-5; RANSAC ransac(options); const auto report = ransac.Estimate(points2D, points3D); EXPECT_TRUE(report.success); EXPECT_LT((expected_cam_from_world.ToMatrix() - report.model).norm(), 1e-4); // Test residuals of exact points. std::vector residuals; EPNPEstimator::Residuals(points2D, points3D, report.model, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_TRUE(residuals[i] < 1e-3); } // Test residuals of faulty points. EPNPEstimator::Residuals( points2D, points3D_faulty, report.model, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_TRUE(residuals[i] > 0.1); } } } } TEST(AbsolutePose, EPNP_BrokenSolveSignCase) { std::vector points2D; points2D.emplace_back(-2.6783007931074532e-01, 5.3457197430746251e-01); points2D.emplace_back(-4.2629907287470264e-01, 7.5623350319519789e-01); points2D.emplace_back(-1.6767413005963930e-01, -1.3387172544910089e-01); points2D.emplace_back(-5.6616329720373559e-02, 2.3621156497739373e-01); points2D.emplace_back(-1.7721225948969935e-01, 2.3395366792735982e-02); points2D.emplace_back(-5.1836259886632222e-02, -4.4380694271927049e-02); points2D.emplace_back(-3.5897765845560037e-01, 1.6252721078589397e-01); points2D.emplace_back(2.7057324473684058e-01, -1.4067450104631887e-01); points2D.emplace_back(-2.5811166424334520e-01, 8.0167171300227366e-02); points2D.emplace_back(2.0239567448222310e-02, -3.2845953375344145e-01); points2D.emplace_back(4.2571014715170657e-01, -2.8321173570154773e-01); points2D.emplace_back(-5.4597596412987237e-01, 9.1431935871671977e-02); std::vector points3D; points3D.emplace_back( 4.4276865308679305e+00, -1.3384364366019632e+00, -3.5997423085253892e+00); points3D.emplace_back( 2.7278555252512309e+00, -3.8152996187231392e-01, -2.6558518399902824e+00); points3D.emplace_back( 4.8548566083054894e+00, -1.4756197433631739e+00, -6.8274946022490501e-01); points3D.emplace_back( 3.1523013527998449e+00, -1.3377020437938025e+00, -1.6443269301929087e+00); points3D.emplace_back( 3.8551679771512073e+00, -1.0557700545885551e+00, -1.1695994508851486e+00); points3D.emplace_back( 5.9571373150353812e+00, -2.6120646101684555e+00, -1.0841441206050342e+00); points3D.emplace_back( 6.3287088499358894e+00, -1.1761274755817175e+00, -2.5951879774151583e+00); points3D.emplace_back( 2.3005305990121250e+00, -1.4019796626800123e+00, -4.4485464455072321e-01); points3D.emplace_back( 5.9816859934587354e+00, -1.4211814511691452e+00, -2.0285923889293449e+00); points3D.emplace_back( 5.2543344690665457e+00, -2.3389255564264144e+00, 4.3708173185524052e-01); points3D.emplace_back( 3.2181599245991688e+00, -2.8906671988445098e+00, 2.6825718150064348e-01); points3D.emplace_back( 4.4592895306946758e+00, -9.1235241641579902e-03, -1.6555237117970871e+00); std::vector models; EPNPEstimator::Estimate(points2D, points3D, &models); ASSERT_EQ(models.size(), 1); double reproj = 0.0; for (size_t i = 0; i < points3D.size(); ++i) { reproj += ((models[0] * points3D[i].homogeneous()).hnormalized() - points2D[i]) .norm(); } EXPECT_TRUE(reproj < 0.2); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/affine_transform.cc000066400000000000000000000077621454702036400231060ustar00rootroot00000000000000// 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. #include "colmap/estimators/affine_transform.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include namespace colmap { void AffineTransformEstimator::Estimate(const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_EQ(points1.size(), points2.size()); CHECK_GE(points1.size(), 3); CHECK(models != nullptr); models->clear(); // Sets up the linear system that we solve to obtain a least squared solution // for the affine transformation. Eigen::MatrixXd C(2 * points1.size(), 6); C.setZero(); Eigen::VectorXd b(2 * points1.size(), 1); for (size_t i = 0; i < points1.size(); ++i) { const Eigen::Vector2d& x1 = points1[i]; const Eigen::Vector2d& x2 = points2[i]; C(2 * i, 0) = x1(0); C(2 * i, 1) = x1(1); C(2 * i, 2) = 1.0f; b(2 * i) = x2(0); C(2 * i + 1, 3) = x1(0); C(2 * i + 1, 4) = x1(1); C(2 * i + 1, 5) = 1.0f; b(2 * i + 1) = x2(1); } const Eigen::VectorXd nullspace = C.jacobiSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b); Eigen::Map> A_t(nullspace.data()); models->resize(1); (*models)[0] = A_t.transpose(); } void AffineTransformEstimator::Residuals(const std::vector& points1, const std::vector& points2, const M_t& A, std::vector* residuals) { CHECK_EQ(points1.size(), points2.size()); residuals->resize(points1.size()); // Note that this code might not be as nice as Eigen expressions, // but it is significantly faster in various tests. const double A_00 = A(0, 0); const double A_01 = A(0, 1); const double A_02 = A(0, 2); const double A_10 = A(1, 0); const double A_11 = A(1, 1); const double A_12 = A(1, 2); for (size_t i = 0; i < points1.size(); ++i) { const double s_0 = points1[i](0); const double s_1 = points1[i](1); const double d_0 = points2[i](0); const double d_1 = points2[i](1); const double pd_0 = A_00 * s_0 + A_01 * s_1 + A_02; const double pd_1 = A_10 * s_0 + A_11 * s_1 + A_12; const double dd_0 = d_0 - pd_0; const double dd_1 = d_1 - pd_1; (*residuals)[i] = dd_0 * dd_0 + dd_1 * dd_1; } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/affine_transform.h000066400000000000000000000047671454702036400227520ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { class AffineTransformEstimator { public: typedef Eigen::Vector2d X_t; typedef Eigen::Vector2d Y_t; typedef Eigen::Matrix M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 3; // Estimate the affine transformation from at least 3 correspondences. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Compute the squared transformation error. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& E, std::vector* residuals); }; } // namespace colmap colmap-3.9.1/src/colmap/estimators/affine_transform_test.cc000066400000000000000000000051101454702036400241260ustar00rootroot00000000000000// 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. #include "colmap/estimators/affine_transform.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(AffineTransform, Nominal) { for (int x = 0; x < 10; ++x) { Eigen::Matrix A; A << x / 10.0, 0.2, 0.3, 30, 0.2, 0.1; std::vector src; src.emplace_back(x, 0); src.emplace_back(1, 0); src.emplace_back(2, 1); std::vector dst; for (size_t i = 0; i < 3; ++i) { dst.push_back(A * src[i].homogeneous()); } AffineTransformEstimator estimator; std::vector> models; estimator.Estimate(src, dst, &models); ASSERT_EQ(models.size(), 1); std::vector residuals; estimator.Residuals(src, dst, models[0], &residuals); EXPECT_EQ(residuals.size(), 3); for (size_t i = 0; i < 3; ++i) { EXPECT_LT(residuals[i], 1e-6); } } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/alignment.cc000066400000000000000000000447631454702036400215430ustar00rootroot00000000000000// 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. #include "colmap/estimators/alignment.h" #include "colmap/estimators/similarity_transform.h" #include "colmap/geometry/pose.h" #include "colmap/optim/loransac.h" #include "colmap/scene/projection.h" #include #include namespace colmap { namespace { struct ReconstructionAlignmentEstimator { static const int kMinNumSamples = 3; typedef const Image* X_t; typedef const Image* Y_t; typedef Sim3d M_t; void SetMaxReprojError(const double max_reproj_error) { max_squared_reproj_error_ = max_reproj_error * max_reproj_error; } void SetReconstructions(const Reconstruction* src_reconstruction, const Reconstruction* tgt_reconstruction) { CHECK_NOTNULL(src_reconstruction); CHECK_NOTNULL(tgt_reconstruction); src_reconstruction_ = src_reconstruction; tgt_reconstruction_ = tgt_reconstruction; } // Estimate 3D similarity transform from corresponding projection centers. void Estimate(const std::vector& src_images, const std::vector& tgt_images, std::vector* models) const { CHECK_GE(src_images.size(), 3); CHECK_GE(tgt_images.size(), 3); CHECK_EQ(src_images.size(), tgt_images.size()); CHECK(models != nullptr); models->clear(); std::vector proj_centers1(src_images.size()); std::vector proj_centers2(tgt_images.size()); for (size_t i = 0; i < src_images.size(); ++i) { CHECK_EQ(src_images[i]->ImageId(), tgt_images[i]->ImageId()); proj_centers1[i] = src_images[i]->ProjectionCenter(); proj_centers2[i] = tgt_images[i]->ProjectionCenter(); } Sim3d tgt_from_src; if (!EstimateSim3d(proj_centers1, proj_centers2, tgt_from_src)) { return; } models->resize(1); (*models)[0] = tgt_from_src; } // For each image, determine the ratio of 3D points that correctly project // from one image to the other image and vice versa for the given // tgt_from_src. The residual is then defined as 1 minus this ratio, i.e., an // error threshold of 0.3 means that 70% of the points for that image must // reproject within the given maximum reprojection error threshold. void Residuals(const std::vector& src_images, const std::vector& tgt_images, const M_t& tgt_from_src, std::vector* residuals) const { CHECK_EQ(src_images.size(), tgt_images.size()); CHECK_NOTNULL(src_reconstruction_); CHECK_NOTNULL(tgt_reconstruction_); const Sim3d srcFromTgt = Inverse(tgt_from_src); residuals->resize(src_images.size()); for (size_t i = 0; i < src_images.size(); ++i) { const auto& src_image = *src_images[i]; const auto& tgt_image = *tgt_images[i]; CHECK_EQ(src_image.ImageId(), tgt_image.ImageId()); const auto& src_camera = src_reconstruction_->Camera(src_image.CameraId()); const auto& tgt_camera = tgt_reconstruction_->Camera(tgt_image.CameraId()); const Eigen::Matrix3x4d src_cam_from_world = src_image.CamFromWorld().ToMatrix(); const Eigen::Matrix3x4d tgt_cam_from_world = tgt_image.CamFromWorld().ToMatrix(); CHECK_EQ(src_image.NumPoints2D(), tgt_image.NumPoints2D()); size_t num_inliers = 0; size_t num_common_points = 0; for (point2D_t point2D_idx = 0; point2D_idx < src_image.NumPoints2D(); ++point2D_idx) { // Check if both images have a 3D point. const auto& src_point2D = src_image.Point2D(point2D_idx); if (!src_point2D.HasPoint3D()) { continue; } const auto& tgt_point2D = tgt_image.Point2D(point2D_idx); if (!tgt_point2D.HasPoint3D()) { continue; } num_common_points += 1; const Eigen::Vector3d src_point_in_tgt = tgt_from_src * src_reconstruction_->Point3D(src_point2D.point3D_id).xyz; if (CalculateSquaredReprojectionError(tgt_point2D.xy, src_point_in_tgt, tgt_cam_from_world, tgt_camera) > max_squared_reproj_error_) { continue; } const Eigen::Vector3d tgt_point_in_src = srcFromTgt * tgt_reconstruction_->Point3D(tgt_point2D.point3D_id).xyz; if (CalculateSquaredReprojectionError(src_point2D.xy, tgt_point_in_src, src_cam_from_world, src_camera) > max_squared_reproj_error_) { continue; } num_inliers += 1; } if (num_common_points == 0) { (*residuals)[i] = 1.0; } else { const double negative_inlier_ratio = 1.0 - static_cast(num_inliers) / static_cast(num_common_points); (*residuals)[i] = negative_inlier_ratio * negative_inlier_ratio; } } } private: double max_squared_reproj_error_ = 0.0; const Reconstruction* src_reconstruction_ = nullptr; const Reconstruction* tgt_reconstruction_ = nullptr; }; } // namespace bool AlignReconstructionToLocations( const Reconstruction& src_reconstruction, const std::vector& tgt_image_names, const std::vector& tgt_image_locations, const int min_common_images, const RANSACOptions& ransac_options, Sim3d* tgt_from_src) { CHECK_GE(min_common_images, 3); CHECK_EQ(tgt_image_names.size(), tgt_image_locations.size()); // Find out which images are contained in the reconstruction and get the // positions of their camera centers. std::unordered_set common_image_ids; std::vector src; std::vector dst; for (size_t i = 0; i < tgt_image_names.size(); ++i) { const class Image* src_image = src_reconstruction.FindImageWithName(tgt_image_names[i]); if (src_image == nullptr) { continue; } if (!src_reconstruction.IsImageRegistered(src_image->ImageId())) { continue; } // Ignore duplicate images. if (common_image_ids.count(src_image->ImageId()) > 0) { continue; } common_image_ids.insert(src_image->ImageId()); src.push_back(src_image->ProjectionCenter()); dst.push_back(tgt_image_locations[i]); } // Only compute the alignment if there are enough correspondences. if (common_image_ids.size() < static_cast(min_common_images)) { return false; } LORANSAC, SimilarityTransformEstimator<3, true>> ransac(ransac_options); const auto report = ransac.Estimate(src, dst); if (report.support.num_inliers < static_cast(min_common_images)) { return false; } if (tgt_from_src != nullptr) { *tgt_from_src = Sim3d::FromMatrix(report.model); } return true; } bool AlignReconstructionsViaReprojections( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const double min_inlier_observations, const double max_reproj_error, Sim3d* tgt_from_src) { CHECK_GE(min_inlier_observations, 0.0); CHECK_LE(min_inlier_observations, 1.0); RANSACOptions ransac_options; ransac_options.max_error = 1.0 - min_inlier_observations; ransac_options.min_inlier_ratio = 0.2; LORANSAC ransac(ransac_options); ransac.estimator.SetMaxReprojError(max_reproj_error); ransac.estimator.SetReconstructions(&src_reconstruction, &tgt_reconstruction); ransac.local_estimator.SetMaxReprojError(max_reproj_error); ransac.local_estimator.SetReconstructions(&src_reconstruction, &tgt_reconstruction); const std::vector> common_image_ids = src_reconstruction.FindCommonRegImageIds(tgt_reconstruction); if (common_image_ids.size() < 3) { return false; } std::vector src_images(common_image_ids.size()); std::vector tgt_images(common_image_ids.size()); for (size_t i = 0; i < common_image_ids.size(); ++i) { src_images[i] = &src_reconstruction.Image(common_image_ids[i].first); tgt_images[i] = &tgt_reconstruction.Image(common_image_ids[i].second); } const auto report = ransac.Estimate(src_images, tgt_images); if (report.success) { *tgt_from_src = report.model; } return report.success; } bool AlignReconstructionsViaProjCenters( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const double max_proj_center_error, Sim3d* tgt_from_src) { CHECK_GT(max_proj_center_error, 0); std::vector ref_image_names; std::vector ref_proj_centers; for (const auto& image : tgt_reconstruction.Images()) { if (image.second.IsRegistered()) { ref_image_names.push_back(image.second.Name()); ref_proj_centers.push_back(image.second.ProjectionCenter()); } } Sim3d tform; RANSACOptions ransac_options; ransac_options.max_error = max_proj_center_error; return AlignReconstructionToLocations(src_reconstruction, ref_image_names, ref_proj_centers, /*min_common_images=*/3, ransac_options, tgt_from_src); } std::vector ComputeImageAlignmentError( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const Sim3d& tgt_from_src) { const std::vector> common_image_ids = src_reconstruction.FindCommonRegImageIds(tgt_reconstruction); const int num_common_images = common_image_ids.size(); std::vector errors; errors.reserve(num_common_images); for (const auto& image_ids : common_image_ids) { const auto& src_image = src_reconstruction.Image(image_ids.first); const Rigid3d tgt_world_from_src_cam = Inverse(TransformCameraWorld(tgt_from_src, src_image.CamFromWorld())); const Rigid3d tgt_world_from_tgt_cam = Inverse(tgt_reconstruction.Image(image_ids.second).CamFromWorld()); ImageAlignmentError error; error.image_name = src_image.Name(); error.rotation_error_deg = RadToDeg(tgt_world_from_src_cam.rotation.angularDistance( tgt_world_from_tgt_cam.rotation)); error.proj_center_error = (tgt_world_from_src_cam.translation - tgt_world_from_tgt_cam.translation) .norm(); errors.push_back(error); } return errors; } bool AlignReconstructionsViaPoints(const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const size_t min_common_observations, const double max_error, const double min_inlier_ratio, Sim3d* tgt_from_src) { CHECK_GT(min_common_observations, 0); CHECK_GT(max_error, 0.0); CHECK_GE(min_inlier_ratio, 0.0); CHECK_LE(min_inlier_ratio, 1.0); std::vector src_xyz; std::vector tgt_xyz; std::unordered_map counts; // Associate 3D points using point2D_idx for (const auto& src_point3D : src_reconstruction.Points3D()) { counts.clear(); // Count how often a 3D point in tgt is associated to this 3D point. for (const auto& track_el : src_point3D.second.track.Elements()) { if (!tgt_reconstruction.IsImageRegistered(track_el.image_id)) { continue; } const Point2D& tgt_point2D = tgt_reconstruction.Image(track_el.image_id) .Point2D(track_el.point2D_idx); if (tgt_point2D.HasPoint3D()) { if (counts.find(tgt_point2D.point3D_id) != counts.end()) { counts[tgt_point2D.point3D_id]++; } else { counts[tgt_point2D.point3D_id] = 0; } } } if (counts.empty()) { continue; } // The 3D point in tgt who is associated the most is selected auto best_p3D = std::max_element(counts.begin(), counts.end(), [](const std::pair& p1, const std::pair& p2) { return p1.second < p2.second; }); if (best_p3D->second >= min_common_observations) { src_xyz.push_back(src_point3D.second.xyz); tgt_xyz.push_back(tgt_reconstruction.Point3D(best_p3D->first).xyz); } } CHECK_EQ(src_xyz.size(), tgt_xyz.size()); LOG(INFO) << "Found " << src_xyz.size() << " / " << src_reconstruction.NumPoints3D() << " valid correspondences."; RANSACOptions ransac_options; ransac_options.max_error = max_error; ransac_options.min_inlier_ratio = min_inlier_ratio; LORANSAC, SimilarityTransformEstimator<3, true>> ransac(ransac_options); const auto report = ransac.Estimate(src_xyz, tgt_xyz); if (report.success) { *tgt_from_src = Sim3d::FromMatrix(report.model); } return report.success; } bool MergeReconstructions(const double max_reproj_error, const Reconstruction& src_reconstruction, Reconstruction* tgt_reconstruction) { Sim3d tgt_from_src; if (!AlignReconstructionsViaReprojections(src_reconstruction, *tgt_reconstruction, /*min_inlier_observations=*/0.3, max_reproj_error, &tgt_from_src)) { return false; } // Find common and missing images in the two reconstructions. std::unordered_set common_image_ids; common_image_ids.reserve(src_reconstruction.NumRegImages()); std::unordered_set missing_image_ids; missing_image_ids.reserve(src_reconstruction.NumRegImages()); for (const auto& image_id : src_reconstruction.RegImageIds()) { if (tgt_reconstruction->ExistsImage(image_id)) { common_image_ids.insert(image_id); } else { missing_image_ids.insert(image_id); } } // Register the missing images in this src_reconstruction. for (const auto image_id : missing_image_ids) { auto src_image = src_reconstruction.Image(image_id); src_image.SetRegistered(false); src_image.CamFromWorld() = TransformCameraWorld(tgt_from_src, src_image.CamFromWorld()); tgt_reconstruction->AddImage(src_image); tgt_reconstruction->RegisterImage(image_id); if (!tgt_reconstruction->ExistsCamera(src_image.CameraId())) { tgt_reconstruction->AddCamera( src_reconstruction.Camera(src_image.CameraId())); } } // Merge the two point clouds using the following two rules: // - copy points to this src_reconstruction with non-conflicting tracks, // i.e. points that do not have an already triangulated observation // in this src_reconstruction. // - merge tracks that are unambiguous, i.e. only merge points in the two // reconstructions if they have a one-to-one mapping. // Note that in both cases no cheirality or reprojection test is performed. for (const auto& point3D : src_reconstruction.Points3D()) { Track new_track; Track old_track; std::unordered_set old_point3D_ids; for (const auto& track_el : point3D.second.track.Elements()) { if (common_image_ids.count(track_el.image_id) > 0) { const auto& point2D = tgt_reconstruction->Image(track_el.image_id) .Point2D(track_el.point2D_idx); if (point2D.HasPoint3D()) { old_track.AddElement(track_el); old_point3D_ids.insert(point2D.point3D_id); } else { new_track.AddElement(track_el); } } else if (missing_image_ids.count(track_el.image_id) > 0) { tgt_reconstruction->Image(track_el.image_id) .ResetPoint3DForPoint2D(track_el.point2D_idx); new_track.AddElement(track_el); } } const bool create_new_point = new_track.Length() >= 2; const bool merge_new_and_old_point = (new_track.Length() + old_track.Length()) >= 2 && old_point3D_ids.size() == 1; if (create_new_point || merge_new_and_old_point) { const Eigen::Vector3d xyz = tgt_from_src * point3D.second.xyz; const auto point3D_id = tgt_reconstruction->AddPoint3D(xyz, new_track, point3D.second.color); if (old_point3D_ids.size() == 1) { tgt_reconstruction->MergePoints3D(point3D_id, *old_point3D_ids.begin()); } } } tgt_reconstruction->FilterAllPoints3D(max_reproj_error, /*min_tri_angle=*/0); return true; } } // namespace colmap colmap-3.9.1/src/colmap/estimators/alignment.h000066400000000000000000000105421454702036400213710ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/sim3.h" #include "colmap/scene/reconstruction.h" namespace colmap { bool AlignReconstructionToLocations( const Reconstruction& reconstruction, const std::vector& image_names, const std::vector& locations, int min_common_images, const RANSACOptions& ransac_options, Sim3d* tform); // Robustly compute alignment between reconstructions by finding images that // are registered in both reconstructions. The alignment is then estimated // robustly inside RANSAC from corresponding projection centers. An alignment // is verified by reprojecting common 3D point observations. // The min_inlier_observations threshold determines how many observations // in a common image must reproject within the given threshold. bool AlignReconstructionsViaReprojections( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, double min_inlier_observations, double max_reproj_error, Sim3d* tgt_from_src); // Robustly compute alignment between reconstructions by finding images that // are registered in both reconstructions. The alignment is then estimated // robustly inside RANSAC from corresponding projection centers and by // minimizing the Euclidean distance between them in world space. bool AlignReconstructionsViaProjCenters( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, double max_proj_center_error, Sim3d* tgt_from_src); // Robustly compute the alignment between reconstructions that share the // same 2D points. It is estimated by minimizing the 3D distance between // corresponding 3D points. bool AlignReconstructionsViaPoints(const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, size_t min_common_observations, double max_error, double min_inlier_ratio, Sim3d* tgt_from_src); // Compute image alignment errors in the target coordinate frame. struct ImageAlignmentError { std::string image_name; double rotation_error_deg = -1; double proj_center_error = -1; }; std::vector ComputeImageAlignmentError( const Reconstruction& src_reconstruction, const Reconstruction& tgt_reconstruction, const Sim3d& tgt_from_src); // Aligns the source to the target reconstruction and merges cameras, images, // points3D into the target using the alignment. Returns false on failure. bool MergeReconstructions(double max_reproj_error, const Reconstruction& src_reconstruction, Reconstruction* tgt_reconstruction); } // namespace colmap colmap-3.9.1/src/colmap/estimators/alignment_test.cc000066400000000000000000000116221454702036400225660ustar00rootroot00000000000000// 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. #include "colmap/estimators/alignment.h" #include "colmap/geometry/sim3.h" #include "colmap/math/random.h" #include "colmap/scene/database.h" #include "colmap/scene/reconstruction.h" #include "colmap/scene/synthetic.h" #include "colmap/util/testing.h" #include namespace colmap { Sim3d TestSim3d() { return Sim3d(RandomUniformReal(0.5, 2), Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); } void ExpectEqualSim3d(const Sim3d& gt_tgt_from_src, const Sim3d& tgt_from_src) { EXPECT_NEAR(gt_tgt_from_src.scale, tgt_from_src.scale, 1e-6); EXPECT_LT(gt_tgt_from_src.rotation.angularDistance(tgt_from_src.rotation), 1e-6); EXPECT_LT((gt_tgt_from_src.translation - tgt_from_src.translation).norm(), 1e-6); } Reconstruction GenerateReconstructionForAlignment() { // const std::string database_path = CreateTestDir() + "/database.db"; // Database database(database_path); Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_cameras = 2; synthetic_dataset_options.num_images = 20; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.point2D_stddev = 0; SynthesizeDataset(synthetic_dataset_options, &reconstruction); return reconstruction; } TEST(Alignment, AlignReconstructionsViaReprojections) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); Sim3d tgt_from_src; CHECK(AlignReconstructionsViaReprojections(src_reconstruction, tgt_reconstruction, /*min_inlier_observations=*/0.9, /*max_reproj_error=*/2, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } TEST(Alignment, AlignReconstructionsViaProjCenters) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); Sim3d tgt_from_src; CHECK(AlignReconstructionsViaProjCenters(src_reconstruction, tgt_reconstruction, /*max_proj_center_error=*/0.1, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } TEST(Alignment, AlignReconstructionsViaPoints) { Reconstruction src_reconstruction = GenerateReconstructionForAlignment(); Reconstruction tgt_reconstruction = src_reconstruction; Sim3d gt_tgt_from_src = TestSim3d(); tgt_reconstruction.Transform(gt_tgt_from_src); Sim3d tgt_from_src; CHECK(AlignReconstructionsViaPoints(src_reconstruction, tgt_reconstruction, /*min_common_observations=*/3, /*max_error=*/0.01, /*min_inlier_ratio=*/0.9, &tgt_from_src)); ExpectEqualSim3d(gt_tgt_from_src, tgt_from_src); } } // namespace colmap colmap-3.9.1/src/colmap/estimators/bundle_adjustment.cc000066400000000000000000001032571454702036400232660ustar00rootroot00000000000000// 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. #include "colmap/estimators/bundle_adjustment.h" #include "colmap/estimators/cost_functions.h" #include "colmap/scene/projection.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" #include "colmap/util/threading.h" #include "colmap/util/timer.h" #include namespace colmap { //////////////////////////////////////////////////////////////////////////////// // BundleAdjustmentOptions //////////////////////////////////////////////////////////////////////////////// ceres::LossFunction* BundleAdjustmentOptions::CreateLossFunction() const { ceres::LossFunction* loss_function = nullptr; switch (loss_function_type) { case LossFunctionType::TRIVIAL: loss_function = new ceres::TrivialLoss(); break; case LossFunctionType::SOFT_L1: loss_function = new ceres::SoftLOneLoss(loss_function_scale); break; case LossFunctionType::CAUCHY: loss_function = new ceres::CauchyLoss(loss_function_scale); break; } CHECK_NOTNULL(loss_function); return loss_function; } bool BundleAdjustmentOptions::Check() const { CHECK_OPTION_GE(loss_function_scale, 0); return true; } //////////////////////////////////////////////////////////////////////////////// // BundleAdjustmentConfig //////////////////////////////////////////////////////////////////////////////// BundleAdjustmentConfig::BundleAdjustmentConfig() {} size_t BundleAdjustmentConfig::NumImages() const { return image_ids_.size(); } size_t BundleAdjustmentConfig::NumPoints() const { return variable_point3D_ids_.size() + constant_point3D_ids_.size(); } size_t BundleAdjustmentConfig::NumConstantCamIntrinsics() const { return constant_intrinsics_.size(); } size_t BundleAdjustmentConfig::NumConstantCamPoses() const { return constant_cam_poses_.size(); } size_t BundleAdjustmentConfig::NumConstantCamPositions() const { return constant_cam_positions_.size(); } size_t BundleAdjustmentConfig::NumVariablePoints() const { return variable_point3D_ids_.size(); } size_t BundleAdjustmentConfig::NumConstantPoints() const { return constant_point3D_ids_.size(); } size_t BundleAdjustmentConfig::NumResiduals( const Reconstruction& reconstruction) const { // Count the number of observations for all added images. size_t num_observations = 0; for (const image_t image_id : image_ids_) { num_observations += reconstruction.Image(image_id).NumPoints3D(); } // Count the number of observations for all added 3D points that are not // already added as part of the images above. auto NumObservationsForPoint = [this, &reconstruction](const point3D_t point3D_id) { size_t num_observations_for_point = 0; const auto& point3D = reconstruction.Point3D(point3D_id); for (const auto& track_el : point3D.track.Elements()) { if (image_ids_.count(track_el.image_id) == 0) { num_observations_for_point += 1; } } return num_observations_for_point; }; for (const auto point3D_id : variable_point3D_ids_) { num_observations += NumObservationsForPoint(point3D_id); } for (const auto point3D_id : constant_point3D_ids_) { num_observations += NumObservationsForPoint(point3D_id); } return 2 * num_observations; } void BundleAdjustmentConfig::AddImage(const image_t image_id) { image_ids_.insert(image_id); } bool BundleAdjustmentConfig::HasImage(const image_t image_id) const { return image_ids_.find(image_id) != image_ids_.end(); } void BundleAdjustmentConfig::RemoveImage(const image_t image_id) { image_ids_.erase(image_id); } void BundleAdjustmentConfig::SetConstantCamIntrinsics( const camera_t camera_id) { constant_intrinsics_.insert(camera_id); } void BundleAdjustmentConfig::SetVariableCamIntrinsics( const camera_t camera_id) { constant_intrinsics_.erase(camera_id); } bool BundleAdjustmentConfig::HasConstantCamIntrinsics( const camera_t camera_id) const { return constant_intrinsics_.find(camera_id) != constant_intrinsics_.end(); } void BundleAdjustmentConfig::SetConstantCamPose(const image_t image_id) { CHECK(HasImage(image_id)); CHECK(!HasConstantCamPositions(image_id)); constant_cam_poses_.insert(image_id); } void BundleAdjustmentConfig::SetVariableCamPose(const image_t image_id) { constant_cam_poses_.erase(image_id); } bool BundleAdjustmentConfig::HasConstantCamPose(const image_t image_id) const { return constant_cam_poses_.find(image_id) != constant_cam_poses_.end(); } void BundleAdjustmentConfig::SetConstantCamPositions( const image_t image_id, const std::vector& idxs) { CHECK_GT(idxs.size(), 0); CHECK_LE(idxs.size(), 3); CHECK(HasImage(image_id)); CHECK(!HasConstantCamPose(image_id)); CHECK(!VectorContainsDuplicateValues(idxs)) << "Tvec indices must not contain duplicates"; constant_cam_positions_.emplace(image_id, idxs); } void BundleAdjustmentConfig::RemoveConstantCamPositions( const image_t image_id) { constant_cam_positions_.erase(image_id); } bool BundleAdjustmentConfig::HasConstantCamPositions( const image_t image_id) const { return constant_cam_positions_.find(image_id) != constant_cam_positions_.end(); } const std::unordered_set& BundleAdjustmentConfig::Images() const { return image_ids_; } const std::unordered_set& BundleAdjustmentConfig::VariablePoints() const { return variable_point3D_ids_; } const std::unordered_set& BundleAdjustmentConfig::ConstantPoints() const { return constant_point3D_ids_; } const std::vector& BundleAdjustmentConfig::ConstantCamPositions( const image_t image_id) const { return constant_cam_positions_.at(image_id); } void BundleAdjustmentConfig::AddVariablePoint(const point3D_t point3D_id) { CHECK(!HasConstantPoint(point3D_id)); variable_point3D_ids_.insert(point3D_id); } void BundleAdjustmentConfig::AddConstantPoint(const point3D_t point3D_id) { CHECK(!HasVariablePoint(point3D_id)); constant_point3D_ids_.insert(point3D_id); } bool BundleAdjustmentConfig::HasPoint(const point3D_t point3D_id) const { return HasVariablePoint(point3D_id) || HasConstantPoint(point3D_id); } bool BundleAdjustmentConfig::HasVariablePoint( const point3D_t point3D_id) const { return variable_point3D_ids_.find(point3D_id) != variable_point3D_ids_.end(); } bool BundleAdjustmentConfig::HasConstantPoint( const point3D_t point3D_id) const { return constant_point3D_ids_.find(point3D_id) != constant_point3D_ids_.end(); } void BundleAdjustmentConfig::RemoveVariablePoint(const point3D_t point3D_id) { variable_point3D_ids_.erase(point3D_id); } void BundleAdjustmentConfig::RemoveConstantPoint(const point3D_t point3D_id) { constant_point3D_ids_.erase(point3D_id); } //////////////////////////////////////////////////////////////////////////////// // BundleAdjuster //////////////////////////////////////////////////////////////////////////////// BundleAdjuster::BundleAdjuster(const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config) : options_(options), config_(config) { CHECK(options_.Check()); } bool BundleAdjuster::Solve(Reconstruction* reconstruction) { CHECK_NOTNULL(reconstruction); CHECK(!problem_) << "Cannot use the same BundleAdjuster multiple times"; ceres::Problem::Options problem_options; problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP; problem_ = std::make_unique(problem_options); const auto loss_function = std::unique_ptr(options_.CreateLossFunction()); SetUp(reconstruction, loss_function.get()); if (problem_->NumResiduals() == 0) { return false; } ceres::Solver::Options solver_options = options_.solver_options; const bool has_sparse = solver_options.sparse_linear_algebra_library_type != ceres::NO_SPARSE; // Empirical choice. const size_t kMaxNumImagesDirectDenseSolver = 50; const size_t kMaxNumImagesDirectSparseSolver = 1000; const size_t num_images = config_.NumImages(); if (num_images <= kMaxNumImagesDirectDenseSolver) { solver_options.linear_solver_type = ceres::DENSE_SCHUR; } else if (num_images <= kMaxNumImagesDirectSparseSolver && has_sparse) { solver_options.linear_solver_type = ceres::SPARSE_SCHUR; } else { // Indirect sparse (preconditioned CG) solver. solver_options.linear_solver_type = ceres::ITERATIVE_SCHUR; solver_options.preconditioner_type = ceres::SCHUR_JACOBI; } if (problem_->NumResiduals() < options_.min_num_residuals_for_multi_threading) { solver_options.num_threads = 1; #if CERES_VERSION_MAJOR < 2 solver_options.num_linear_solver_threads = 1; #endif // CERES_VERSION_MAJOR } else { solver_options.num_threads = GetEffectiveNumThreads(solver_options.num_threads); #if CERES_VERSION_MAJOR < 2 solver_options.num_linear_solver_threads = GetEffectiveNumThreads(solver_options.num_linear_solver_threads); #endif // CERES_VERSION_MAJOR } std::string solver_error; CHECK(solver_options.IsValid(&solver_error)) << solver_error; ceres::Solve(solver_options, problem_.get(), &summary_); if (options_.print_summary) { PrintHeading2("Bundle adjustment report"); PrintSolverSummary(summary_); } TearDown(reconstruction); return true; } const ceres::Solver::Summary& BundleAdjuster::Summary() const { return summary_; } void BundleAdjuster::SetUp(Reconstruction* reconstruction, ceres::LossFunction* loss_function) { // Warning: AddPointsToProblem assumes that AddImageToProblem is called first. // Do not change order of instructions! for (const image_t image_id : config_.Images()) { AddImageToProblem(image_id, reconstruction, loss_function); } for (const auto point3D_id : config_.VariablePoints()) { AddPointToProblem(point3D_id, reconstruction, loss_function); } for (const auto point3D_id : config_.ConstantPoints()) { AddPointToProblem(point3D_id, reconstruction, loss_function); } ParameterizeCameras(reconstruction); ParameterizePoints(reconstruction); } void BundleAdjuster::TearDown(Reconstruction*) { // Nothing to do } void BundleAdjuster::AddImageToProblem(const image_t image_id, Reconstruction* reconstruction, ceres::LossFunction* loss_function) { Image& image = reconstruction->Image(image_id); Camera& camera = reconstruction->Camera(image.CameraId()); // CostFunction assumes unit quaternions. image.CamFromWorld().rotation.normalize(); double* cam_from_world_rotation = image.CamFromWorld().rotation.coeffs().data(); double* cam_from_world_translation = image.CamFromWorld().translation.data(); double* camera_params = camera.params.data(); const bool constant_cam_pose = !options_.refine_extrinsics || config_.HasConstantCamPose(image_id); // Add residuals to bundle adjustment problem. size_t num_observations = 0; for (const Point2D& point2D : image.Points2D()) { if (!point2D.HasPoint3D()) { continue; } num_observations += 1; point3D_num_observations_[point2D.point3D_id] += 1; Point3D& point3D = reconstruction->Point3D(point2D.point3D_id); assert(point3D.track.Length() > 1); ceres::CostFunction* cost_function = nullptr; if (constant_cam_pose) { switch (camera.model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = ReprojErrorConstantPoseCostFunction::Create( \ image.CamFromWorld(), point2D.xy); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem_->AddResidualBlock( cost_function, loss_function, point3D.xyz.data(), camera_params); } else { switch (camera.model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = ReprojErrorCostFunction::Create(point2D.xy); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem_->AddResidualBlock(cost_function, loss_function, cam_from_world_rotation, cam_from_world_translation, point3D.xyz.data(), camera_params); } } if (num_observations > 0) { camera_ids_.insert(image.CameraId()); // Set pose parameterization. if (!constant_cam_pose) { SetQuaternionManifold(problem_.get(), cam_from_world_rotation); if (config_.HasConstantCamPositions(image_id)) { const std::vector& constant_position_idxs = config_.ConstantCamPositions(image_id); SetSubsetManifold(3, constant_position_idxs, problem_.get(), cam_from_world_translation); } } } } void BundleAdjuster::AddPointToProblem(const point3D_t point3D_id, Reconstruction* reconstruction, ceres::LossFunction* loss_function) { Point3D& point3D = reconstruction->Point3D(point3D_id); // Is 3D point already fully contained in the problem? I.e. its entire track // is contained in `variable_image_ids`, `constant_image_ids`, // `constant_x_image_ids`. if (point3D_num_observations_[point3D_id] == point3D.track.Length()) { return; } for (const auto& track_el : point3D.track.Elements()) { // Skip observations that were already added in `FillImages`. if (config_.HasImage(track_el.image_id)) { continue; } point3D_num_observations_[point3D_id] += 1; Image& image = reconstruction->Image(track_el.image_id); Camera& camera = reconstruction->Camera(image.CameraId()); const Point2D& point2D = image.Point2D(track_el.point2D_idx); // CostFunction assumes unit quaternions. image.CamFromWorld().rotation.normalize(); // We do not want to refine the camera of images that are not // part of `constant_image_ids_`, `constant_image_ids_`, // `constant_x_image_ids_`. if (camera_ids_.count(image.CameraId()) == 0) { camera_ids_.insert(image.CameraId()); config_.SetConstantCamIntrinsics(image.CameraId()); } ceres::CostFunction* cost_function = nullptr; switch (camera.model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = ReprojErrorConstantPoseCostFunction::Create( \ image.CamFromWorld(), point2D.xy); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem_->AddResidualBlock( cost_function, loss_function, point3D.xyz.data(), camera.params.data()); } } void BundleAdjuster::ParameterizeCameras(Reconstruction* reconstruction) { const bool constant_camera = !options_.refine_focal_length && !options_.refine_principal_point && !options_.refine_extra_params; for (const camera_t camera_id : camera_ids_) { Camera& camera = reconstruction->Camera(camera_id); if (constant_camera || config_.HasConstantCamIntrinsics(camera_id)) { problem_->SetParameterBlockConstant(camera.params.data()); continue; } else { std::vector const_camera_params; if (!options_.refine_focal_length) { const span params_idxs = camera.FocalLengthIdxs(); const_camera_params.insert( const_camera_params.end(), params_idxs.begin(), params_idxs.end()); } if (!options_.refine_principal_point) { const span params_idxs = camera.PrincipalPointIdxs(); const_camera_params.insert( const_camera_params.end(), params_idxs.begin(), params_idxs.end()); } if (!options_.refine_extra_params) { const span params_idxs = camera.ExtraParamsIdxs(); const_camera_params.insert( const_camera_params.end(), params_idxs.begin(), params_idxs.end()); } if (const_camera_params.size() > 0) { SetSubsetManifold(static_cast(camera.params.size()), const_camera_params, problem_.get(), camera.params.data()); } } } } void BundleAdjuster::ParameterizePoints(Reconstruction* reconstruction) { for (const auto elem : point3D_num_observations_) { Point3D& point3D = reconstruction->Point3D(elem.first); if (point3D.track.Length() > elem.second) { problem_->SetParameterBlockConstant(point3D.xyz.data()); } } for (const point3D_t point3D_id : config_.ConstantPoints()) { Point3D& point3D = reconstruction->Point3D(point3D_id); problem_->SetParameterBlockConstant(point3D.xyz.data()); } } //////////////////////////////////////////////////////////////////////////////// // RigBundleAdjuster //////////////////////////////////////////////////////////////////////////////// RigBundleAdjuster::RigBundleAdjuster(const BundleAdjustmentOptions& options, const Options& rig_options, const BundleAdjustmentConfig& config) : BundleAdjuster(options, config), rig_options_(rig_options) {} bool RigBundleAdjuster::Solve(Reconstruction* reconstruction, std::vector* camera_rigs) { CHECK_NOTNULL(reconstruction); CHECK_NOTNULL(camera_rigs); CHECK(!problem_) << "Cannot use the same BundleAdjuster multiple times"; // Check the validity of the provided camera rigs. std::unordered_set rig_camera_ids; for (auto& camera_rig : *camera_rigs) { camera_rig.Check(*reconstruction); for (const auto& camera_id : camera_rig.GetCameraIds()) { CHECK_EQ(rig_camera_ids.count(camera_id), 0) << "Camera must not be part of multiple camera rigs"; rig_camera_ids.insert(camera_id); } for (const auto& snapshot : camera_rig.Snapshots()) { for (const auto& image_id : snapshot) { CHECK_EQ(image_id_to_camera_rig_.count(image_id), 0) << "Image must not be part of multiple camera rigs"; image_id_to_camera_rig_.emplace(image_id, &camera_rig); } } } problem_ = std::make_unique(); ceres::Problem::Options problem_options; problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP; problem_ = std::make_unique(problem_options); const auto loss_function = std::unique_ptr(options_.CreateLossFunction()); SetUp(reconstruction, camera_rigs, loss_function.get()); if (problem_->NumResiduals() == 0) { return false; } ceres::Solver::Options solver_options = options_.solver_options; const bool has_sparse = solver_options.sparse_linear_algebra_library_type != ceres::NO_SPARSE; // Empirical choice. const size_t kMaxNumImagesDirectDenseSolver = 50; const size_t kMaxNumImagesDirectSparseSolver = 1000; const size_t num_images = config_.NumImages(); if (num_images <= kMaxNumImagesDirectDenseSolver) { solver_options.linear_solver_type = ceres::DENSE_SCHUR; } else if (num_images <= kMaxNumImagesDirectSparseSolver && has_sparse) { solver_options.linear_solver_type = ceres::SPARSE_SCHUR; } else { // Indirect sparse (preconditioned CG) solver. solver_options.linear_solver_type = ceres::ITERATIVE_SCHUR; solver_options.preconditioner_type = ceres::SCHUR_JACOBI; } solver_options.num_threads = GetEffectiveNumThreads(solver_options.num_threads); #if CERES_VERSION_MAJOR < 2 solver_options.num_linear_solver_threads = GetEffectiveNumThreads(solver_options.num_linear_solver_threads); #endif // CERES_VERSION_MAJOR std::string solver_error; CHECK(solver_options.IsValid(&solver_error)) << solver_error; ceres::Solve(solver_options, problem_.get(), &summary_); if (options_.print_summary) { PrintHeading2("Rig Bundle adjustment report"); PrintSolverSummary(summary_); } TearDown(reconstruction, *camera_rigs); return true; } void RigBundleAdjuster::SetUp(Reconstruction* reconstruction, std::vector* camera_rigs, ceres::LossFunction* loss_function) { ComputeCameraRigPoses(*reconstruction, *camera_rigs); for (const image_t image_id : config_.Images()) { AddImageToProblem(image_id, reconstruction, camera_rigs, loss_function); } for (const auto point3D_id : config_.VariablePoints()) { AddPointToProblem(point3D_id, reconstruction, loss_function); } for (const auto point3D_id : config_.ConstantPoints()) { AddPointToProblem(point3D_id, reconstruction, loss_function); } ParameterizeCameras(reconstruction); ParameterizePoints(reconstruction); ParameterizeCameraRigs(reconstruction); } void RigBundleAdjuster::TearDown(Reconstruction* reconstruction, const std::vector& camera_rigs) { for (const auto& elem : image_id_to_camera_rig_) { const auto image_id = elem.first; const auto& camera_rig = *elem.second; auto& image = reconstruction->Image(image_id); image.CamFromWorld() = camera_rig.CamFromRig(image.CameraId()) * (*image_id_to_rig_from_world_.at(image_id)); } } void RigBundleAdjuster::AddImageToProblem(const image_t image_id, Reconstruction* reconstruction, std::vector* camera_rigs, ceres::LossFunction* loss_function) { const double max_squared_reproj_error = rig_options_.max_reproj_error * rig_options_.max_reproj_error; Image& image = reconstruction->Image(image_id); Camera& camera = reconstruction->Camera(image.CameraId()); const bool constant_cam_pose = config_.HasConstantCamPose(image_id); const bool constant_cam_position = config_.HasConstantCamPositions(image_id); double* camera_params = camera.params.data(); double* cam_from_rig_rotation = nullptr; double* cam_from_rig_translation = nullptr; double* rig_from_world_rotation = nullptr; double* rig_from_world_translation = nullptr; CameraRig* camera_rig = nullptr; Eigen::Matrix3x4d cam_from_world_mat = Eigen::Matrix3x4d::Zero(); if (image_id_to_camera_rig_.count(image_id) > 0) { CHECK(!constant_cam_pose) << "Images contained in a camera rig must not have constant pose"; CHECK(!constant_cam_position) << "Images contained in a camera rig must not have constant tvec"; camera_rig = image_id_to_camera_rig_.at(image_id); Rigid3d& rig_from_world = *image_id_to_rig_from_world_.at(image_id); rig_from_world_rotation = rig_from_world.rotation.coeffs().data(); rig_from_world_translation = rig_from_world.translation.data(); Rigid3d& cam_from_rig = camera_rig->CamFromRig(image.CameraId()); cam_from_rig_rotation = cam_from_rig.rotation.coeffs().data(); cam_from_rig_translation = cam_from_rig.translation.data(); cam_from_world_mat = (cam_from_rig * rig_from_world).ToMatrix(); } else { // CostFunction assumes unit quaternions. image.CamFromWorld().rotation.normalize(); cam_from_rig_rotation = image.CamFromWorld().rotation.coeffs().data(); cam_from_rig_translation = image.CamFromWorld().translation.data(); } // Collect cameras for final parameterization. CHECK(image.HasCamera()); camera_ids_.insert(image.CameraId()); // The number of added observations for the current image. size_t num_observations = 0; // Add residuals to bundle adjustment problem. for (const Point2D& point2D : image.Points2D()) { if (!point2D.HasPoint3D()) { continue; } Point3D& point3D = reconstruction->Point3D(point2D.point3D_id); assert(point3D.track.Length() > 1); if (camera_rig != nullptr && CalculateSquaredReprojectionError( point2D.xy, point3D.xyz, cam_from_world_mat, camera) > max_squared_reproj_error) { continue; } num_observations += 1; point3D_num_observations_[point2D.point3D_id] += 1; ceres::CostFunction* cost_function = nullptr; if (camera_rig == nullptr) { if (constant_cam_pose) { switch (camera.model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = ReprojErrorConstantPoseCostFunction::Create( \ image.CamFromWorld(), point2D.xy); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem_->AddResidualBlock( cost_function, loss_function, point3D.xyz.data(), camera_params); } else { switch (camera.model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = ReprojErrorCostFunction::Create(point2D.xy); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem_->AddResidualBlock(cost_function, loss_function, cam_from_rig_rotation, // rig == world cam_from_rig_translation, // rig == world point3D.xyz.data(), camera_params); } } else { switch (camera.model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = \ RigReprojErrorCostFunction::Create(point2D.xy); \ \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem_->AddResidualBlock(cost_function, loss_function, cam_from_rig_rotation, cam_from_rig_translation, rig_from_world_rotation, rig_from_world_translation, point3D.xyz.data(), camera_params); } } if (num_observations > 0) { parameterized_quats_.insert(cam_from_rig_rotation); if (camera_rig != nullptr) { parameterized_quats_.insert(rig_from_world_rotation); // Set the relative pose of the camera constant if relative pose // refinement is disabled or if it is the reference camera to avoid over- // parameterization of the camera pose. if (!rig_options_.refine_relative_poses || image.CameraId() == camera_rig->RefCameraId()) { problem_->SetParameterBlockConstant(cam_from_rig_rotation); problem_->SetParameterBlockConstant(cam_from_rig_translation); } } // Set pose parameterization. if (!constant_cam_pose && constant_cam_position) { const std::vector& constant_position_idxs = config_.ConstantCamPositions(image_id); SetSubsetManifold( 3, constant_position_idxs, problem_.get(), cam_from_rig_translation); } } } void RigBundleAdjuster::AddPointToProblem(const point3D_t point3D_id, Reconstruction* reconstruction, ceres::LossFunction* loss_function) { Point3D& point3D = reconstruction->Point3D(point3D_id); // Is 3D point already fully contained in the problem? I.e. its entire track // is contained in `variable_image_ids`, `constant_image_ids`, // `constant_x_image_ids`. if (point3D_num_observations_[point3D_id] == point3D.track.Length()) { return; } for (const auto& track_el : point3D.track.Elements()) { // Skip observations that were already added in `AddImageToProblem`. if (config_.HasImage(track_el.image_id)) { continue; } point3D_num_observations_[point3D_id] += 1; Image& image = reconstruction->Image(track_el.image_id); Camera& camera = reconstruction->Camera(image.CameraId()); const Point2D& point2D = image.Point2D(track_el.point2D_idx); // We do not want to refine the camera of images that are not // part of `constant_image_ids_`, `constant_image_ids_`, // `constant_x_image_ids_`. if (camera_ids_.count(image.CameraId()) == 0) { camera_ids_.insert(image.CameraId()); config_.SetConstantCamIntrinsics(image.CameraId()); } ceres::CostFunction* cost_function = nullptr; switch (camera.model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = ReprojErrorConstantPoseCostFunction::Create( \ image.CamFromWorld(), point2D.xy); \ problem_->AddResidualBlock(cost_function, \ loss_function, \ point3D.xyz.data(), \ camera.params.data()); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } } } void RigBundleAdjuster::ComputeCameraRigPoses( const Reconstruction& reconstruction, const std::vector& camera_rigs) { rigs_from_world_.reserve(camera_rigs.size()); for (const auto& camera_rig : camera_rigs) { rigs_from_world_.emplace_back(); auto& rig_from_world = rigs_from_world_.back(); const size_t num_snapshots = camera_rig.NumSnapshots(); rig_from_world.resize(num_snapshots); for (size_t snapshot_idx = 0; snapshot_idx < num_snapshots; ++snapshot_idx) { rig_from_world[snapshot_idx] = camera_rig.ComputeRigFromWorld(snapshot_idx, reconstruction); for (const auto image_id : camera_rig.Snapshots()[snapshot_idx]) { image_id_to_rig_from_world_.emplace(image_id, &rig_from_world[snapshot_idx]); } } } } void RigBundleAdjuster::ParameterizeCameraRigs(Reconstruction* reconstruction) { for (double* cam_from_rig_rotation : parameterized_quats_) { SetQuaternionManifold(problem_.get(), cam_from_rig_rotation); } } void PrintSolverSummary(const ceres::Solver::Summary& summary) { std::ostringstream log; log << "\n"; log << std::right << std::setw(16) << "Residuals : "; log << std::left << summary.num_residuals_reduced << "\n"; log << std::right << std::setw(16) << "Parameters : "; log << std::left << summary.num_effective_parameters_reduced << "\n"; log << std::right << std::setw(16) << "Iterations : "; log << std::left << summary.num_successful_steps + summary.num_unsuccessful_steps << "\n"; log << std::right << std::setw(16) << "Time : "; log << std::left << summary.total_time_in_seconds << " [s]\n"; log << std::right << std::setw(16) << "Initial cost : "; log << std::right << std::setprecision(6) << std::sqrt(summary.initial_cost / summary.num_residuals_reduced) << " [px]\n"; log << std::right << std::setw(16) << "Final cost : "; log << std::right << std::setprecision(6) << std::sqrt(summary.final_cost / summary.num_residuals_reduced) << " [px]\n"; log << std::right << std::setw(16) << "Termination : "; std::string termination = ""; switch (summary.termination_type) { case ceres::CONVERGENCE: termination = "Convergence"; break; case ceres::NO_CONVERGENCE: termination = "No convergence"; break; case ceres::FAILURE: termination = "Failure"; break; case ceres::USER_SUCCESS: termination = "User success"; break; case ceres::USER_FAILURE: termination = "User failure"; break; default: termination = "Unknown"; break; } log << std::right << termination << "\n\n"; LOG(INFO) << log.str(); } } // namespace colmap colmap-3.9.1/src/colmap/estimators/bundle_adjustment.h000066400000000000000000000241741454702036400231300ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/camera_rig.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/eigen_alignment.h" #include #include #include #include namespace colmap { struct BundleAdjustmentOptions { // Loss function types: Trivial (non-robust) and Cauchy (robust) loss. enum class LossFunctionType { TRIVIAL, SOFT_L1, CAUCHY }; LossFunctionType loss_function_type = LossFunctionType::TRIVIAL; // Scaling factor determines residual at which robustification takes place. double loss_function_scale = 1.0; // Whether to refine the focal length parameter group. bool refine_focal_length = true; // Whether to refine the principal point parameter group. bool refine_principal_point = false; // Whether to refine the extra parameter group. bool refine_extra_params = true; // Whether to refine the extrinsic parameter group. bool refine_extrinsics = true; // Whether to print a final summary. bool print_summary = true; // Minimum number of residuals to enable multi-threading. Note that // single-threaded is typically better for small bundle adjustment problems // due to the overhead of threading. int min_num_residuals_for_multi_threading = 50000; // Ceres-Solver options. ceres::Solver::Options solver_options; BundleAdjustmentOptions() { solver_options.function_tolerance = 0.0; solver_options.gradient_tolerance = 0.0; solver_options.parameter_tolerance = 0.0; solver_options.logging_type = ceres::LoggingType::SILENT; solver_options.max_num_iterations = 100; solver_options.max_linear_solver_iterations = 200; solver_options.max_num_consecutive_invalid_steps = 10; solver_options.max_consecutive_nonmonotonic_steps = 10; solver_options.num_threads = -1; #if CERES_VERSION_MAJOR < 2 solver_options.num_linear_solver_threads = -1; #endif // CERES_VERSION_MAJOR } // Create a new loss function based on the specified options. The caller // takes ownership of the loss function. ceres::LossFunction* CreateLossFunction() const; bool Check() const; }; // Configuration container to setup bundle adjustment problems. class BundleAdjustmentConfig { public: BundleAdjustmentConfig(); size_t NumImages() const; size_t NumPoints() const; size_t NumConstantCamIntrinsics() const; size_t NumConstantCamPoses() const; size_t NumConstantCamPositions() const; size_t NumVariablePoints() const; size_t NumConstantPoints() const; // Determine the number of residuals for the given reconstruction. The number // of residuals equals the number of observations times two. size_t NumResiduals(const Reconstruction& reconstruction) const; // Add / remove images from the configuration. void AddImage(image_t image_id); bool HasImage(image_t image_id) const; void RemoveImage(image_t image_id); // Set cameras of added images as constant or variable. By default all // cameras of added images are variable. Note that the corresponding images // have to be added prior to calling these methods. void SetConstantCamIntrinsics(camera_t camera_id); void SetVariableCamIntrinsics(camera_t camera_id); bool HasConstantCamIntrinsics(camera_t camera_id) const; // Set the pose of added images as constant. The pose is defined as the // rotational and translational part of the projection matrix. void SetConstantCamPose(image_t image_id); void SetVariableCamPose(image_t image_id); bool HasConstantCamPose(image_t image_id) const; // Set the translational part of the pose, hence the constant pose // indices may be in [0, 1, 2] and must be unique. Note that the // corresponding images have to be added prior to calling these methods. void SetConstantCamPositions(image_t image_id, const std::vector& idxs); void RemoveConstantCamPositions(image_t image_id); bool HasConstantCamPositions(image_t image_id) const; // Add / remove points from the configuration. Note that points can either // be variable or constant but not both at the same time. void AddVariablePoint(point3D_t point3D_id); void AddConstantPoint(point3D_t point3D_id); bool HasPoint(point3D_t point3D_id) const; bool HasVariablePoint(point3D_t point3D_id) const; bool HasConstantPoint(point3D_t point3D_id) const; void RemoveVariablePoint(point3D_t point3D_id); void RemoveConstantPoint(point3D_t point3D_id); // Access configuration data. const std::unordered_set& Images() const; const std::unordered_set& VariablePoints() const; const std::unordered_set& ConstantPoints() const; const std::vector& ConstantCamPositions(image_t image_id) const; private: std::unordered_set constant_intrinsics_; std::unordered_set image_ids_; std::unordered_set variable_point3D_ids_; std::unordered_set constant_point3D_ids_; std::unordered_set constant_cam_poses_; std::unordered_map> constant_cam_positions_; }; // Bundle adjustment based on Ceres-Solver. Enables most flexible configurations // and provides best solution quality. class BundleAdjuster { public: BundleAdjuster(const BundleAdjustmentOptions& options, const BundleAdjustmentConfig& config); bool Solve(Reconstruction* reconstruction); // Get the Ceres solver summary for the last call to `Solve`. const ceres::Solver::Summary& Summary() const; private: void SetUp(Reconstruction* reconstruction, ceres::LossFunction* loss_function); void TearDown(Reconstruction* reconstruction); void AddImageToProblem(image_t image_id, Reconstruction* reconstruction, ceres::LossFunction* loss_function); void AddPointToProblem(point3D_t point3D_id, Reconstruction* reconstruction, ceres::LossFunction* loss_function); protected: void ParameterizeCameras(Reconstruction* reconstruction); void ParameterizePoints(Reconstruction* reconstruction); const BundleAdjustmentOptions options_; BundleAdjustmentConfig config_; std::unique_ptr problem_; ceres::Solver::Summary summary_; std::unordered_set camera_ids_; std::unordered_map point3D_num_observations_; }; class RigBundleAdjuster : public BundleAdjuster { public: struct Options { // Whether to optimize the relative poses of the camera rigs. bool refine_relative_poses = true; // The maximum allowed reprojection error for an observation to be // considered in the bundle adjustment. Some observations might have large // reprojection errors due to the concatenation of the absolute and relative // rig poses, which might be different from the absolute pose of the image // in the reconstruction. double max_reproj_error = 1000.0; }; RigBundleAdjuster(const BundleAdjustmentOptions& options, const Options& rig_options, const BundleAdjustmentConfig& config); bool Solve(Reconstruction* reconstruction, std::vector* camera_rigs); private: void SetUp(Reconstruction* reconstruction, std::vector* camera_rigs, ceres::LossFunction* loss_function); void TearDown(Reconstruction* reconstruction, const std::vector& camera_rigs); void AddImageToProblem(image_t image_id, Reconstruction* reconstruction, std::vector* camera_rigs, ceres::LossFunction* loss_function); void AddPointToProblem(point3D_t point3D_id, Reconstruction* reconstruction, ceres::LossFunction* loss_function); void ComputeCameraRigPoses(const Reconstruction& reconstruction, const std::vector& camera_rigs); void ParameterizeCameraRigs(Reconstruction* reconstruction); const Options rig_options_; // Mapping from images to camera rigs. std::unordered_map image_id_to_camera_rig_; // Mapping from images to the absolute camera rig poses. std::unordered_map image_id_to_rig_from_world_; // For each camera rig, the absolute camera rig poses for all snapshots. std::vector> rigs_from_world_; // The Quaternions added to the problem, used to set the local // parameterization once after setting up the problem. std::unordered_set parameterized_quats_; }; void PrintSolverSummary(const ceres::Solver::Summary& summary); } // namespace colmap colmap-3.9.1/src/colmap/estimators/bundle_adjustment_test.cc000066400000000000000000001000121454702036400243070ustar00rootroot00000000000000// 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. #include "colmap/estimators/bundle_adjustment.h" #include "colmap/math/random.h" #include "colmap/scene/correspondence_graph.h" #include "colmap/scene/projection.h" #include "colmap/sensor/models.h" #include #define CheckVariableCamera(camera, orig_camera) \ { \ const size_t focal_length_idx = \ SimpleRadialCameraModel::focal_length_idxs[0]; \ const size_t extra_param_idx = \ SimpleRadialCameraModel::extra_params_idxs[0]; \ EXPECT_NE((camera).params[focal_length_idx], \ (orig_camera).params[focal_length_idx]); \ EXPECT_NE((camera).params[extra_param_idx], \ (orig_camera).params[extra_param_idx]); \ } #define CheckConstantCamera(camera, orig_camera) \ { \ const size_t focal_length_idx = \ SimpleRadialCameraModel::focal_length_idxs[0]; \ const size_t extra_param_idx = \ SimpleRadialCameraModel::extra_params_idxs[0]; \ EXPECT_EQ((camera).params[focal_length_idx], \ (orig_camera).params[focal_length_idx]); \ EXPECT_EQ((camera).params[extra_param_idx], \ (orig_camera).params[extra_param_idx]); \ } #define CheckVariableImage(image, orig_image) \ { \ EXPECT_NE((image).CamFromWorld().rotation.coeffs(), \ (orig_image).CamFromWorld().rotation.coeffs()); \ EXPECT_NE((image).CamFromWorld().translation, \ (orig_image).CamFromWorld().translation); \ } #define CheckConstantImage(image, orig_image) \ { \ EXPECT_EQ((image).CamFromWorld().rotation.coeffs(), \ (orig_image).CamFromWorld().rotation.coeffs()); \ EXPECT_EQ((image).CamFromWorld().translation, \ (orig_image).CamFromWorld().translation); \ } #define CheckConstantXImage(image, orig_image) \ { \ CheckVariableImage(image, orig_image); \ EXPECT_EQ((image).CamFromWorld().translation.x(), \ (orig_image).CamFromWorld().translation.x()); \ } #define CheckConstantCameraRig(camera_rig, orig_camera_rig, camera_id) \ { \ EXPECT_EQ((camera_rig).CamFromRig(camera_id).rotation.coeffs(), \ (orig_camera_rig).CamFromRig(camera_id).rotation.coeffs()); \ EXPECT_EQ((camera_rig).CamFromRig(camera_id).translation, \ (orig_camera_rig).CamFromRig(camera_id).translation); \ } #define CheckVariableCameraRig(camera_rig, orig_camera_rig, camera_id) \ { \ if ((camera_rig).RefCameraId() == (camera_id)) { \ CheckConstantCameraRig(camera_rig, orig_camera_rig, camera_id); \ } else { \ EXPECT_NE((camera_rig).CamFromRig(camera_id).rotation.coeffs(), \ (orig_camera_rig).CamFromRig(camera_id).rotation.coeffs()); \ EXPECT_NE((camera_rig).CamFromRig(camera_id).translation, \ (orig_camera_rig).CamFromRig(camera_id).translation); \ } \ } #define CheckVariablePoint(point, orig_point) \ { EXPECT_NE((point).xyz, (orig_point).xyz); } #define CheckConstantPoint(point, orig_point) \ { EXPECT_EQ((point).xyz, (orig_point).xyz); } namespace colmap { namespace { void GeneratePointCloud(const size_t num_points, const Eigen::Vector3d& min, const Eigen::Vector3d& max, Reconstruction* reconstruction) { for (size_t i = 0; i < num_points; ++i) { Eigen::Vector3d xyz; xyz.x() = RandomUniformReal(min.x(), max.x()); xyz.y() = RandomUniformReal(min.y(), max.y()); xyz.z() = RandomUniformReal(min.z(), max.z()); reconstruction->AddPoint3D(xyz, Track()); } } void GenerateReconstruction(const size_t num_images, const size_t num_points, Reconstruction* reconstruction) { SetPRNGSeed(0); GeneratePointCloud(num_points, Eigen::Vector3d(-1, -1, -1), Eigen::Vector3d(1, 1, 1), reconstruction); const double kFocalLengthFactor = 1.2; const size_t kImageSize = 1000; for (size_t i = 0; i < num_images; ++i) { const camera_t camera_id = static_cast(i); const image_t image_id = static_cast(i); const Camera camera = Camera::CreateFromModelId(camera_id, SimpleRadialCameraModel::model_id, kFocalLengthFactor * kImageSize, kImageSize, kImageSize); reconstruction->AddCamera(camera); Image image; image.SetImageId(image_id); image.SetCameraId(camera_id); image.SetName(std::to_string(i)); image.CamFromWorld() = Rigid3d( Eigen::Quaterniond::Identity(), Eigen::Vector3d( RandomUniformReal(-1.0, 1.0), RandomUniformReal(-1.0, 1.0), 10)); image.SetRegistered(true); reconstruction->AddImage(image); const Eigen::Matrix3x4d cam_from_world_matrix = image.CamFromWorld().ToMatrix(); std::vector points2D; for (const auto& point3D : reconstruction->Points3D()) { EXPECT_TRUE( HasPointPositiveDepth(cam_from_world_matrix, point3D.second.xyz)); // Get exact projection of 3D point. Eigen::Vector2d point2D = camera.ImgFromCam( (image.CamFromWorld() * point3D.second.xyz).hnormalized()); // Add some uniform noise. point2D += Eigen::Vector2d(RandomUniformReal(-2.0, 2.0), RandomUniformReal(-2.0, 2.0)); points2D.push_back(point2D); } reconstruction->Image(image_id).SetPoints2D(points2D); } for (size_t i = 0; i < num_images; ++i) { const image_t image_id = static_cast(i); TrackElement track_el; track_el.image_id = image_id; track_el.point2D_idx = 0; for (const auto& point3D : reconstruction->Points3D()) { reconstruction->AddObservation(point3D.first, track_el); track_el.point2D_idx += 1; } } } TEST(BundleAdjustment, ConfigNumObservations) { Reconstruction reconstruction; GenerateReconstruction(4, 100, &reconstruction); BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); EXPECT_EQ(config.NumResiduals(reconstruction), 400); config.AddVariablePoint(1); EXPECT_EQ(config.NumResiduals(reconstruction), 404); config.AddConstantPoint(2); EXPECT_EQ(config.NumResiduals(reconstruction), 408); config.AddImage(2); EXPECT_EQ(config.NumResiduals(reconstruction), 604); config.AddImage(3); EXPECT_EQ(config.NumResiduals(reconstruction), 800); } TEST(BundleAdjustment, TwoView) { Reconstruction reconstruction; GenerateReconstruction(2, 100, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPositions(1, {0}); BundleAdjustmentOptions options; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 image parameters (pose of second image) // + 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 309); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantXImage(reconstruction.Image(1), orig_reconstruction.Image(1)); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, TwoViewConstantCamera) { Reconstruction reconstruction; GenerateReconstruction(2, 100, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPose(1); config.SetConstantCamIntrinsics(0); BundleAdjustmentOptions options; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 100 x 3 point parameters // + 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 302); CheckConstantCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantImage(reconstruction.Image(1), orig_reconstruction.Image(1)); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, PartiallyContainedTracks) { Reconstruction reconstruction; GenerateReconstruction(3, 100, &reconstruction); const auto variable_point3D_id = reconstruction.Image(2).Point2D(0).point3D_id; reconstruction.DeleteObservation(2, 0); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPose(1); BundleAdjustmentOptions options; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 1 x 3 point parameters // 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 7); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantImage(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckConstantCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantImage(reconstruction.Image(2), orig_reconstruction.Image(2)); for (const auto& point3D : reconstruction.Points3D()) { if (point3D.first == variable_point3D_id) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } else { CheckConstantPoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } } TEST(BundleAdjustment, PartiallyContainedTracksForceToOptimizePoint) { Reconstruction reconstruction; GenerateReconstruction(3, 100, &reconstruction); const point3D_t variable_point3D_id = reconstruction.Image(2).Point2D(0).point3D_id; const point3D_t add_variable_point3D_id = reconstruction.Image(2).Point2D(1).point3D_id; const point3D_t add_constant_point3D_id = reconstruction.Image(2).Point2D(2).point3D_id; reconstruction.DeleteObservation(2, 0); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPose(1); config.AddVariablePoint(add_variable_point3D_id); config.AddConstantPoint(add_constant_point3D_id); BundleAdjustmentOptions options; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image // + 2 residuals in 3rd image for added variable 3D point // (added constant point does not add residuals since the image/camera // is also constant). EXPECT_EQ(summary.num_residuals_reduced, 402); // 2 x 3 point parameters // 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 10); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantImage(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckConstantCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckConstantImage(reconstruction.Image(2), orig_reconstruction.Image(2)); for (const auto& point3D : reconstruction.Points3D()) { if (point3D.first == variable_point3D_id || point3D.first == add_variable_point3D_id) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } else { CheckConstantPoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } } TEST(BundleAdjustment, ConstantPoints) { Reconstruction reconstruction; GenerateReconstruction(2, 100, &reconstruction); const auto orig_reconstruction = reconstruction; const point3D_t constant_point3D_id1 = 1; const point3D_t constant_point3D_id2 = 2; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPose(1); config.AddConstantPoint(constant_point3D_id1); config.AddConstantPoint(constant_point3D_id2); BundleAdjustmentOptions options; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 98 x 3 point parameters // + 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 298); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantImage(reconstruction.Image(1), orig_reconstruction.Image(1)); for (const auto& point3D : reconstruction.Points3D()) { if (point3D.first == constant_point3D_id1 || point3D.first == constant_point3D_id2) { CheckConstantPoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } else { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } } TEST(BundleAdjustment, VariableImage) { Reconstruction reconstruction; GenerateReconstruction(3, 100, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.AddImage(2); config.SetConstantCamPose(0); config.SetConstantCamPositions(1, {0}); BundleAdjustmentOptions options; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 600); // 100 x 3 point parameters // + 5 image parameters (pose of second image) // + 6 image parameters (pose of third image) // + 3 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 317); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckConstantXImage(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCamera(reconstruction.Camera(2), orig_reconstruction.Camera(2)); CheckVariableImage(reconstruction.Image(2), orig_reconstruction.Image(2)); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, ConstantFocalLength) { Reconstruction reconstruction; GenerateReconstruction(2, 100, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPositions(1, {0}); BundleAdjustmentOptions options; options.refine_focal_length = false; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 image parameters (pose of second image) // + 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 307); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckConstantXImage(reconstruction.Image(1), orig_reconstruction.Image(1)); const size_t focal_length_idx = SimpleRadialCameraModel::focal_length_idxs[0]; const size_t extra_param_idx = SimpleRadialCameraModel::extra_params_idxs[0]; const auto& camera0 = reconstruction.Camera(0); const auto& orig_camera0 = orig_reconstruction.Camera(0); EXPECT_TRUE(camera0.params[focal_length_idx] == orig_camera0.params[focal_length_idx]); EXPECT_TRUE(camera0.params[extra_param_idx] != orig_camera0.params[extra_param_idx]); const auto& camera1 = reconstruction.Camera(1); const auto& orig_camera1 = orig_reconstruction.Camera(1); EXPECT_TRUE(camera1.params[focal_length_idx] == orig_camera1.params[focal_length_idx]); EXPECT_TRUE(camera1.params[extra_param_idx] != orig_camera1.params[extra_param_idx]); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, VariablePrincipalPoint) { Reconstruction reconstruction; GenerateReconstruction(2, 100, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPositions(1, {0}); BundleAdjustmentOptions options; options.refine_principal_point = true; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 image parameters (pose of second image) // + 8 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 313); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckConstantXImage(reconstruction.Image(1), orig_reconstruction.Image(1)); const size_t focal_length_idx = SimpleRadialCameraModel::focal_length_idxs[0]; const size_t principal_point_idx_x = SimpleRadialCameraModel::principal_point_idxs[0]; const size_t principal_point_idx_y = SimpleRadialCameraModel::principal_point_idxs[0]; const size_t extra_param_idx = SimpleRadialCameraModel::extra_params_idxs[0]; const auto& camera0 = reconstruction.Camera(0); const auto& orig_camera0 = orig_reconstruction.Camera(0); EXPECT_TRUE(camera0.params[focal_length_idx] != orig_camera0.params[focal_length_idx]); EXPECT_TRUE(camera0.params[principal_point_idx_x] != orig_camera0.params[principal_point_idx_x]); EXPECT_TRUE(camera0.params[principal_point_idx_y] != orig_camera0.params[principal_point_idx_y]); EXPECT_TRUE(camera0.params[extra_param_idx] != orig_camera0.params[extra_param_idx]); const auto& camera1 = reconstruction.Camera(1); const auto& orig_camera1 = orig_reconstruction.Camera(1); EXPECT_TRUE(camera1.params[focal_length_idx] != orig_camera1.params[focal_length_idx]); EXPECT_TRUE(camera1.params[principal_point_idx_x] != orig_camera1.params[principal_point_idx_x]); EXPECT_TRUE(camera1.params[principal_point_idx_y] != orig_camera1.params[principal_point_idx_y]); EXPECT_TRUE(camera1.params[extra_param_idx] != orig_camera1.params[extra_param_idx]); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, ConstantExtraParam) { Reconstruction reconstruction; GenerateReconstruction(2, 100, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.SetConstantCamPose(0); config.SetConstantCamPositions(1, {0}); BundleAdjustmentOptions options; options.refine_extra_params = false; BundleAdjuster bundle_adjuster(options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction)); const auto summary = bundle_adjuster.Summary(); // 100 points, 3 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 100 x 3 point parameters // + 5 image parameters (pose of second image) // + 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 307); CheckConstantImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckConstantXImage(reconstruction.Image(1), orig_reconstruction.Image(1)); const size_t focal_length_idx = SimpleRadialCameraModel::focal_length_idxs[0]; const size_t extra_param_idx = SimpleRadialCameraModel::extra_params_idxs[0]; const auto& camera0 = reconstruction.Camera(0); const auto& orig_camera0 = orig_reconstruction.Camera(0); EXPECT_TRUE(camera0.params[focal_length_idx] != orig_camera0.params[focal_length_idx]); EXPECT_TRUE(camera0.params[extra_param_idx] == orig_camera0.params[extra_param_idx]); const auto& camera1 = reconstruction.Camera(1); const auto& orig_camera1 = orig_reconstruction.Camera(1); EXPECT_TRUE(camera1.params[focal_length_idx] != orig_camera1.params[focal_length_idx]); EXPECT_TRUE(camera1.params[extra_param_idx] == orig_camera1.params[extra_param_idx]); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, RigTwoView) { Reconstruction reconstruction; GenerateReconstruction(2, 100, &reconstruction); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); std::vector camera_rigs; camera_rigs.emplace_back(); camera_rigs[0].AddCamera(0, Rigid3d()); camera_rigs[0].AddCamera(1, Rigid3d()); camera_rigs[0].AddSnapshot({0, 1}); camera_rigs[0].SetRefCameraId(0); const auto orig_camera_rigs = camera_rigs; BundleAdjustmentOptions options; RigBundleAdjuster::Options rig_options; RigBundleAdjuster bundle_adjuster(options, rig_options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction, &camera_rigs)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 400); // 100 x 3 point parameters // + 6 pose parameters for camera rig // + 1 x 6 relative pose parameters for camera rig // + 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 316); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckVariableImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckVariableImage(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCameraRig(camera_rigs[0], orig_camera_rigs[0], 0); CheckVariableCameraRig(camera_rigs[0], orig_camera_rigs[0], 1); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, RigFourView) { Reconstruction reconstruction; GenerateReconstruction(4, 100, &reconstruction); reconstruction.Image(2).SetCameraId(0); reconstruction.Image(3).SetCameraId(1); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.AddImage(2); config.AddImage(3); std::vector camera_rigs; camera_rigs.emplace_back(); camera_rigs[0].AddCamera(0, Rigid3d()); camera_rigs[0].AddCamera(1, Rigid3d()); camera_rigs[0].AddSnapshot({0, 1}); camera_rigs[0].AddSnapshot({2, 3}); camera_rigs[0].SetRefCameraId(0); const auto orig_camera_rigs = camera_rigs; BundleAdjustmentOptions options; RigBundleAdjuster::Options rig_options; RigBundleAdjuster bundle_adjuster(options, rig_options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction, &camera_rigs)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 800); // 100 x 3 point parameters // + 2 x 6 pose parameters for camera rig // + 1 x 6 relative pose parameters for camera rig // + 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 322); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckVariableImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckVariableImage(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCameraRig(camera_rigs[0], orig_camera_rigs[0], 0); CheckVariableCameraRig(camera_rigs[0], orig_camera_rigs[0], 1); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, ConstantRigFourView) { Reconstruction reconstruction; GenerateReconstruction(4, 100, &reconstruction); reconstruction.Image(2).SetCameraId(0); reconstruction.Image(3).SetCameraId(1); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.AddImage(2); config.AddImage(3); std::vector camera_rigs; camera_rigs.emplace_back(); camera_rigs[0].AddCamera(0, Rigid3d()); camera_rigs[0].AddCamera(1, Rigid3d()); camera_rigs[0].AddSnapshot({0, 1}); camera_rigs[0].AddSnapshot({2, 3}); camera_rigs[0].SetRefCameraId(0); const auto orig_camera_rigs = camera_rigs; BundleAdjustmentOptions options; RigBundleAdjuster::Options rig_options; rig_options.refine_relative_poses = false; RigBundleAdjuster bundle_adjuster(options, rig_options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction, &camera_rigs)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 800); // 100 x 3 point parameters // + 2 x 6 pose parameters for camera rig // + 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 316); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckVariableImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckVariableImage(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckConstantCameraRig(camera_rigs[0], orig_camera_rigs[0], 0); CheckConstantCameraRig(camera_rigs[0], orig_camera_rigs[0], 1); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } TEST(BundleAdjustment, RigFourViewPartial) { Reconstruction reconstruction; GenerateReconstruction(4, 100, &reconstruction); reconstruction.Image(2).SetCameraId(0); reconstruction.Image(3).SetCameraId(1); const auto orig_reconstruction = reconstruction; BundleAdjustmentConfig config; config.AddImage(0); config.AddImage(1); config.AddImage(2); config.AddImage(3); std::vector camera_rigs; camera_rigs.emplace_back(); camera_rigs[0].AddCamera(0, Rigid3d()); camera_rigs[0].AddCamera(1, Rigid3d()); camera_rigs[0].AddSnapshot({0, 1}); camera_rigs[0].AddSnapshot({2}); camera_rigs[0].SetRefCameraId(0); const auto orig_camera_rigs = camera_rigs; BundleAdjustmentOptions options; RigBundleAdjuster::Options rig_options; RigBundleAdjuster bundle_adjuster(options, rig_options, config); ASSERT_TRUE(bundle_adjuster.Solve(&reconstruction, &camera_rigs)); const auto summary = bundle_adjuster.Summary(); // 100 points, 2 images, 2 residuals per point per image EXPECT_EQ(summary.num_residuals_reduced, 800); // 100 x 3 point parameters // + 2 x 6 pose parameters for camera rig // + 1 x 6 relative pose parameters for camera rig // + 1 x 6 pose parameters for individual image // + 2 x 2 camera parameters EXPECT_EQ(summary.num_effective_parameters_reduced, 328); CheckVariableCamera(reconstruction.Camera(0), orig_reconstruction.Camera(0)); CheckVariableImage(reconstruction.Image(0), orig_reconstruction.Image(0)); CheckVariableCamera(reconstruction.Camera(1), orig_reconstruction.Camera(1)); CheckVariableImage(reconstruction.Image(1), orig_reconstruction.Image(1)); CheckVariableCameraRig(camera_rigs[0], orig_camera_rigs[0], 0); CheckVariableCameraRig(camera_rigs[0], orig_camera_rigs[0], 1); for (const auto& point3D : reconstruction.Points3D()) { CheckVariablePoint(point3D.second, orig_reconstruction.Point3D(point3D.first)); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/coordinate_frame.cc000066400000000000000000000332711454702036400230560ustar00rootroot00000000000000// 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. #include "colmap/estimators/coordinate_frame.h" #include "colmap/estimators/utils.h" #include "colmap/geometry/gps.h" #include "colmap/geometry/pose.h" #include "colmap/image/line.h" #include "colmap/image/undistortion.h" #include "colmap/optim/ransac.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" namespace colmap { namespace { struct VanishingPointEstimator { // The line segments. typedef LineSegment X_t; // The line representation of the segments. typedef Eigen::Vector3d Y_t; // The vanishing point. typedef Eigen::Vector3d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 2; // Estimate the vanishing point from at least two line segments. static void Estimate(const std::vector& line_segments, const std::vector& lines, std::vector* models) { CHECK_EQ(line_segments.size(), 2); CHECK_EQ(lines.size(), 2); CHECK(models != nullptr); models->resize(1); (*models)[0] = lines[0].cross(lines[1]); } // Calculate the squared distance of each line segment's end point to the line // connecting the vanishing point and the midpoint of the line segment. static void Residuals(const std::vector& line_segments, const std::vector& lines, const M_t& vanishing_point, std::vector* residuals) { residuals->resize(line_segments.size()); // Check if vanishing point is at infinity. if (vanishing_point[2] == 0) { std::fill(residuals->begin(), residuals->end(), std::numeric_limits::max()); return; } for (size_t i = 0; i < lines.size(); ++i) { const Eigen::Vector3d midpoint = (0.5 * (line_segments[i].start + line_segments[i].end)).homogeneous(); const Eigen::Vector3d connecting_line = midpoint.cross(vanishing_point); const double signed_distance = connecting_line.dot(line_segments[i].end.homogeneous()) / connecting_line.head<2>().norm(); (*residuals)[i] = signed_distance * signed_distance; } } }; Eigen::Vector3d FindBestConsensusAxis(const std::vector& axes, const double max_distance) { if (axes.empty()) { return Eigen::Vector3d::Zero(); } std::vector inlier_idxs; inlier_idxs.reserve(axes.size()); std::vector best_inlier_idxs; best_inlier_idxs.reserve(axes.size()); double best_inlier_distance_sum = std::numeric_limits::max(); for (size_t i = 0; i < axes.size(); ++i) { const Eigen::Vector3d& ref_axis = axes[i]; double inlier_distance_sum = 0; inlier_idxs.clear(); for (size_t j = 0; j < axes.size(); ++j) { if (i == j) { inlier_idxs.push_back(j); } else { const double distance = 1 - ref_axis.dot(axes[j]); if (distance <= max_distance) { inlier_distance_sum += distance; inlier_idxs.push_back(j); } } } if (inlier_idxs.size() > best_inlier_idxs.size() || (inlier_idxs.size() == best_inlier_idxs.size() && inlier_distance_sum < best_inlier_distance_sum)) { best_inlier_distance_sum = inlier_distance_sum; best_inlier_idxs = inlier_idxs; } } if (best_inlier_idxs.empty()) { return Eigen::Vector3d::Zero(); } Eigen::Vector3d best_axis(0, 0, 0); for (const auto idx : best_inlier_idxs) { best_axis += axes[idx]; } best_axis /= best_inlier_idxs.size(); return best_axis; } } // namespace Eigen::Vector3d EstimateGravityVectorFromImageOrientation( const Reconstruction& reconstruction, const double max_axis_distance) { std::vector downward_axes; downward_axes.reserve(reconstruction.NumRegImages()); for (const auto image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); downward_axes.push_back( image.CamFromWorld().rotation.toRotationMatrix().row(1)); } return FindBestConsensusAxis(downward_axes, max_axis_distance); } Eigen::Matrix3d EstimateManhattanWorldFrame( const ManhattanWorldFrameEstimationOptions& options, const Reconstruction& reconstruction, const std::string& image_path) { std::vector rightward_axes; std::vector downward_axes; for (size_t i = 0; i < reconstruction.NumRegImages(); ++i) { const auto image_id = reconstruction.RegImageIds()[i]; const auto& image = reconstruction.Image(image_id); const auto& camera = reconstruction.Camera(image.CameraId()); PrintHeading1(StringPrintf("Processing image %s (%d / %d)", image.Name().c_str(), i + 1, reconstruction.NumRegImages())); LOG(INFO) << "Reading image..."; colmap::Bitmap bitmap; CHECK(bitmap.Read(colmap::JoinPaths(image_path, image.Name()))); LOG(INFO) << "Undistorting image..."; UndistortCameraOptions undistortion_options; undistortion_options.max_image_size = options.max_image_size; Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(undistortion_options, bitmap, camera, &undistorted_bitmap, &undistorted_camera); LOG(INFO) << "Detecting lines..."; const std::vector line_segments = DetectLineSegments(undistorted_bitmap, options.min_line_length); const std::vector line_orientations = ClassifyLineSegmentOrientations(line_segments, options.line_orientation_tolerance); LOG(INFO) << StringPrintf(" %d", line_segments.size()); std::vector horizontal_line_segments; std::vector vertical_line_segments; std::vector horizontal_lines; std::vector vertical_lines; for (size_t i = 0; i < line_segments.size(); ++i) { const auto& line_segment = line_segments[i]; const Eigen::Vector3d line_segment_start = line_segment.start.homogeneous(); const Eigen::Vector3d line_segment_end = line_segment.end.homogeneous(); const Eigen::Vector3d line = line_segment_start.cross(line_segment_end); if (line_orientations[i] == LineSegmentOrientation::HORIZONTAL) { horizontal_line_segments.push_back(line_segment); horizontal_lines.push_back(line); } else if (line_orientations[i] == LineSegmentOrientation::VERTICAL) { vertical_line_segments.push_back(line_segment); vertical_lines.push_back(line); } } LOG(INFO) << StringPrintf(" (%d horizontal, %d vertical)", horizontal_lines.size(), vertical_lines.size()); LOG(INFO) << "Estimating vanishing points..."; RANSACOptions ransac_options; ransac_options.max_error = options.max_line_vp_distance; RANSAC ransac(ransac_options); const auto horizontal_report = ransac.Estimate(horizontal_line_segments, horizontal_lines); const auto vertical_report = ransac.Estimate(vertical_line_segments, vertical_lines); LOG(INFO) << StringPrintf(" (%d horizontal inliers, %d vertical inliers)", horizontal_report.support.num_inliers, vertical_report.support.num_inliers); LOG(INFO) << "Composing coordinate axes..."; const Eigen::Matrix3d inv_calib_matrix = undistorted_camera.CalibrationMatrix().inverse(); const Eigen::Quaterniond world_from_cam_rotation = image.CamFromWorld().rotation.inverse(); if (horizontal_report.success) { Eigen::Vector3d horizontal_axis_in_world = world_from_cam_rotation * (inv_calib_matrix * horizontal_report.model).normalized(); // Make sure all axes point into the same direction. if (rightward_axes.size() > 0 && rightward_axes[0].dot(horizontal_axis_in_world) < 0) { horizontal_axis_in_world = -horizontal_axis_in_world; } rightward_axes.push_back(horizontal_axis_in_world); LOG(INFO) << "Horizontal: " << horizontal_axis_in_world.transpose(); } if (vertical_report.success) { const Eigen::Vector3d vertical_axis_in_cam = (inv_calib_matrix * vertical_report.model).normalized(); Eigen::Vector3d vertical_axis_in_world = (world_from_cam_rotation * vertical_axis_in_cam).normalized(); // Make sure axis points downwards in the image, assuming that the image // was taken in upright orientation. if (vertical_axis_in_world.dot(Eigen::Vector3d(0, 1, 0)) < 0) { vertical_axis_in_world = -vertical_axis_in_world; } downward_axes.push_back(vertical_axis_in_world); LOG(INFO) << "Vertical: " << vertical_axis_in_world.transpose(); } } PrintHeading1("Computing coordinate frame"); Eigen::Matrix3d frame = Eigen::Matrix3d::Zero(); if (rightward_axes.size() > 0) { frame.col(0) = FindBestConsensusAxis(rightward_axes, options.max_axis_distance); } LOG(INFO) << "Found rightward axis: " << frame.col(0).transpose(); if (downward_axes.size() > 0) { frame.col(1) = FindBestConsensusAxis(downward_axes, options.max_axis_distance); } LOG(INFO) << "Found downward axis: " << frame.col(1).transpose(); if (rightward_axes.size() > 0 && downward_axes.size() > 0) { frame.col(2) = frame.col(0).cross(frame.col(1)); Eigen::JacobiSVD svd( frame, Eigen::ComputeFullV | Eigen::ComputeFullU); const Eigen::Matrix3d orthonormal_frame = svd.matrixU() * Eigen::Matrix3d::Identity() * svd.matrixV().transpose(); frame = orthonormal_frame; } LOG(INFO) << "Found orthonormal frame:\n" << frame; return frame; } void AlignToPrincipalPlane(Reconstruction* reconstruction, Sim3d* tform) { // Perform SVD on the 3D points to estimate the ground plane basis const Eigen::Vector3d centroid = reconstruction->ComputeCentroid(0.0, 1.0); Eigen::MatrixXd normalized_points3D(3, reconstruction->NumPoints3D()); int pidx = 0; for (const auto& point : reconstruction->Points3D()) { normalized_points3D.col(pidx++) = point.second.xyz - centroid; } const Eigen::Matrix3d basis = normalized_points3D.jacobiSvd(Eigen::ComputeThinU | Eigen::ComputeThinV) .matrixU(); Eigen::Matrix3d rot_mat; rot_mat << basis.col(0), basis.col(1), basis.col(0).cross(basis.col(1)); rot_mat.transposeInPlace(); *tform = Sim3d(1.0, Eigen::Quaterniond(rot_mat), -rot_mat * centroid); // If camera plane ends up below ground then flip basis vectors. const Rigid3d cam0_from_aligned_world = TransformCameraWorld( *tform, reconstruction->Image(reconstruction->RegImageIds()[0]).CamFromWorld()); if (Inverse(cam0_from_aligned_world).translation.z() < 0.0) { rot_mat << basis.col(0), -basis.col(1), basis.col(0).cross(-basis.col(1)); rot_mat.transposeInPlace(); *tform = Sim3d(1.0, Eigen::Quaterniond(rot_mat), -rot_mat * centroid); } reconstruction->Transform(*tform); } void AlignToENUPlane(Reconstruction* reconstruction, Sim3d* tform, bool unscaled) { const Eigen::Vector3d centroid = reconstruction->ComputeCentroid(0.0, 1.0); GPSTransform gps_tform; const Eigen::Vector3d ell_centroid = gps_tform.XYZToEll({centroid}).at(0); // Create rotation matrix from ECEF to ENU coordinates const double sin_lat = std::sin(DegToRad(ell_centroid(0))); const double sin_lon = std::sin(DegToRad(ell_centroid(1))); const double cos_lat = std::cos(DegToRad(ell_centroid(0))); const double cos_lon = std::cos(DegToRad(ell_centroid(1))); // Create ECEF to ENU rotation matrix Eigen::Matrix3d rot_mat; rot_mat << -sin_lon, cos_lon, 0, -cos_lon * sin_lat, -sin_lon * sin_lat, cos_lat, cos_lon * cos_lat, sin_lon * cos_lat, sin_lat; const double scale = unscaled ? 1.0 / tform->scale : 1.0; *tform = Sim3d(scale, Eigen::Quaterniond(rot_mat), -scale * rot_mat * centroid); reconstruction->Transform(*tform); } } // namespace colmap colmap-3.9.1/src/colmap/estimators/coordinate_frame.h000066400000000000000000000100051454702036400227060ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/reconstruction.h" #include "colmap/util/eigen_alignment.h" #include namespace colmap { struct ManhattanWorldFrameEstimationOptions { // The maximum image size for line detection. int max_image_size = 1024; // The minimum length of line segments in pixels. double min_line_length = 3; // The tolerance for classifying lines into horizontal/vertical. double line_orientation_tolerance = 0.2; // The maximum distance in pixels between lines and the vanishing points. double max_line_vp_distance = 0.5; // The maximum cosine distance between estimated axes to be inliers. double max_axis_distance = 0.05; }; // Estimate gravity vector by assuming gravity-aligned image orientation, i.e. // the majority of images is assumed to have the gravity vector aligned with an // upright image plane. Eigen::Vector3d EstimateGravityVectorFromImageOrientation( const Reconstruction& reconstruction, double max_axis_distance = 0.05); // Estimate the coordinate frame of the reconstruction assuming a Manhattan // world by finding the major vanishing points in each image. This function // assumes that the majority of images is taken in upright direction, i.e. // people are standing upright in the image. The orthonormal axes of the // estimated coordinate frame will be given in the columns of the returned // matrix. If one axis could not be determined, the respective column will be // zero. The axes are specified in the world coordinate system in the order // rightward, downward, forward. Eigen::Matrix3d EstimateManhattanWorldFrame( const ManhattanWorldFrameEstimationOptions& options, const Reconstruction& reconstruction, const std::string& image_path); // Aligns the reconstruction to the plane defined by running PCA on the 3D // points. The model centroid is at the origin of the new coordinate system // and the X axis is the first principal component with the Y axis being the // second principal component void AlignToPrincipalPlane(Reconstruction* recon, Sim3d* tform); // Aligns the reconstruction to the local ENU plane orientation. Rotates the // reconstruction such that the x-y plane aligns with the ENU tangent plane at // the point cloud centroid and translates the origin to the centroid. // If unscaled == true, then the original scale of the model remains unchanged. void AlignToENUPlane(Reconstruction* recon, Sim3d* tform, bool unscaled); } // namespace colmap colmap-3.9.1/src/colmap/estimators/coordinate_frame_test.cc000066400000000000000000000146471454702036400241230ustar00rootroot00000000000000// 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. #include "colmap/estimators/coordinate_frame.h" #include "colmap/geometry/gps.h" #include namespace colmap { namespace { TEST(CoordinateFrame, EstimateGravityVectorFromImageOrientation) { Reconstruction reconstruction; EXPECT_EQ(EstimateGravityVectorFromImageOrientation(reconstruction), Eigen::Vector3d::Zero()); } TEST(CoordinateFrame, EstimateManhattanWorldFrame) { Reconstruction reconstruction; std::string image_path; EXPECT_EQ( EstimateManhattanWorldFrame( ManhattanWorldFrameEstimationOptions(), reconstruction, image_path), Eigen::Matrix3d::Zero()); } TEST(CoordinateFrame, AlignToPrincipalPlane) { // Start with reconstruction containing points on the Y-Z plane and cameras // "above" the plane on the positive X axis. After alignment the points should // be on the X-Y plane and the cameras "above" the plane on the positive Z // axis. Sim3d tform; Reconstruction reconstruction; // Setup image with projection center at (1, 0, 0) Image image; image.SetImageId(1); image.SetRegistered(true); image.CamFromWorld() = Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(-1, 0, 0)); reconstruction.AddImage(image); // Setup 4 points on the Y-Z plane const point3D_t p1 = reconstruction.AddPoint3D(Eigen::Vector3d(0, -1, 0), Track()); const point3D_t p2 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 1, 0), Track()); const point3D_t p3 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, -1), Track()); const point3D_t p4 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, 1), Track()); AlignToPrincipalPlane(&reconstruction, &tform); // Note that the final X and Y axes may be inverted after alignment, so we // need to account for both cases when checking for correctness const bool inverted = tform.rotation.y() < 0; // Verify that points lie on the correct locations of the X-Y plane EXPECT_LE((reconstruction.Point3D(p1).xyz - Eigen::Vector3d(inverted ? 1 : -1, 0, 0)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(p2).xyz - Eigen::Vector3d(inverted ? -1 : 1, 0, 0)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(p3).xyz - Eigen::Vector3d(0, inverted ? 1 : -1, 0)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(p4).xyz - Eigen::Vector3d(0, inverted ? -1 : 1, 0)) .norm(), 1e-6); // Verify that projection center is at (0, 0, 1) EXPECT_LE( (reconstruction.Image(1).ProjectionCenter() - Eigen::Vector3d(0, 0, 1)) .norm(), 1e-6); // Verify that transform matrix does shuffling of axes Eigen::Matrix3x4d expected; if (inverted) { expected << 0, -1, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0; } else { expected << 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0; } EXPECT_LT((tform.ToMatrix() - expected).norm(), 1e-6); } TEST(CoordinateFrame, AlignToENUPlane) { // Create reconstruction with 4 points with known LLA coordinates. After the // ENU transform all 4 points should land approximately on the X-Y plane. GPSTransform gps; auto points = gps.EllToXYZ({Eigen::Vector3d(50, 10.1, 100), Eigen::Vector3d(50.1, 10, 100), Eigen::Vector3d(50.1, 10.1, 100), Eigen::Vector3d(50, 10, 100)}); Sim3d tform; Reconstruction reconstruction; std::vector point_ids; for (size_t i = 0; i < points.size(); ++i) { point_ids.push_back(reconstruction.AddPoint3D(points[i], Track())); LOG(INFO) << points[i].transpose(); } AlignToENUPlane(&reconstruction, &tform, false); // Verify final locations of points EXPECT_LE((reconstruction.Point3D(point_ids[0]).xyz - Eigen::Vector3d(3584.8565215, -5561.5336506, 0.0742643)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(point_ids[1]).xyz - Eigen::Vector3d(-3577.3888622, 5561.6397107, 0.0783761)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(point_ids[2]).xyz - Eigen::Vector3d(3577.4152111, 5561.6397283, 0.0783613)) .norm(), 1e-6); EXPECT_LE((reconstruction.Point3D(point_ids[3]).xyz - Eigen::Vector3d(-3584.8301178, -5561.5336683, 0.0742791)) .norm(), 1e-6); // Verify that straight line distance between points is preserved for (size_t i = 1; i < points.size(); ++i) { const double dist_orig = (points[i] - points[i - 1]).norm(); const double dist_tform = (reconstruction.Point3D(point_ids[i]).xyz - reconstruction.Point3D(point_ids[i - 1]).xyz) .norm(); EXPECT_LE(std::abs(dist_orig - dist_tform), 1e-6); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/cost_functions.h000066400000000000000000000314651454702036400224620ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { template using EigenVector3Map = Eigen::Map>; template using EigenQuaternionMap = Eigen::Map>; // Standard bundle adjustment cost function for variable // camera pose, calibration, and point parameters. template class ReprojErrorCostFunction { public: explicit ReprojErrorCostFunction(const Eigen::Vector2d& point2D) : observed_x_(point2D(0)), observed_y_(point2D(1)) {} static ceres::CostFunction* Create(const Eigen::Vector2d& point2D) { return ( new ceres::AutoDiffCostFunction, 2, 4, 3, 3, CameraModel::num_params>( new ReprojErrorCostFunction(point2D))); } template bool operator()(const T* const cam_from_world_rotation, const T* const cam_from_world_translation, const T* const point3D, const T* const camera_params, T* residuals) const { const Eigen::Matrix point3D_in_cam = EigenQuaternionMap(cam_from_world_rotation) * EigenVector3Map(point3D) + EigenVector3Map(cam_from_world_translation); CameraModel::ImgFromCam(camera_params, point3D_in_cam[0], point3D_in_cam[1], point3D_in_cam[2], &residuals[0], &residuals[1]); residuals[0] -= T(observed_x_); residuals[1] -= T(observed_y_); return true; } private: const double observed_x_; const double observed_y_; }; // Bundle adjustment cost function for variable // camera calibration and point parameters, and fixed camera pose. template class ReprojErrorConstantPoseCostFunction { public: ReprojErrorConstantPoseCostFunction(const Rigid3d& cam_from_world, const Eigen::Vector2d& point2D) : cam_from_world_(cam_from_world), observed_x_(point2D(0)), observed_y_(point2D(1)) {} static ceres::CostFunction* Create(const Rigid3d& cam_from_world, const Eigen::Vector2d& point2D) { return (new ceres::AutoDiffCostFunction< ReprojErrorConstantPoseCostFunction, 2, 3, CameraModel::num_params>( new ReprojErrorConstantPoseCostFunction(cam_from_world, point2D))); } template bool operator()(const T* const point3D, const T* const camera_params, T* residuals) const { const Eigen::Matrix point3D_in_cam = cam_from_world_.rotation.cast() * EigenVector3Map(point3D) + cam_from_world_.translation.cast(); CameraModel::ImgFromCam(camera_params, point3D_in_cam[0], point3D_in_cam[1], point3D_in_cam[2], &residuals[0], &residuals[1]); residuals[0] -= T(observed_x_); residuals[1] -= T(observed_y_); return true; } private: const Rigid3d& cam_from_world_; const double observed_x_; const double observed_y_; }; // Bundle adjustment cost function for variable // camera pose and calibration parameters, and fixed point. template class ReprojErrorConstantPoint3DCostFunction { public: ReprojErrorConstantPoint3DCostFunction(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D) : observed_x_(point2D(0)), observed_y_(point2D(1)), point3D_x_(point3D(0)), point3D_y_(point3D(1)), point3D_z_(point3D(2)) {} static ceres::CostFunction* Create(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D) { return (new ceres::AutoDiffCostFunction< ReprojErrorConstantPoint3DCostFunction, 2, 4, 3, CameraModel::num_params>( new ReprojErrorConstantPoint3DCostFunction(point2D, point3D))); } template bool operator()(const T* const cam_from_world_rotation, const T* const cam_from_world_translation, const T* const camera_params, T* residuals) const { Eigen::Matrix point3D; point3D[0] = T(point3D_x_); point3D[1] = T(point3D_y_); point3D[2] = T(point3D_z_); const Eigen::Matrix point3D_in_cam = EigenQuaternionMap(cam_from_world_rotation) * point3D + EigenVector3Map(cam_from_world_translation); CameraModel::ImgFromCam(camera_params, point3D_in_cam[0], point3D_in_cam[1], point3D_in_cam[2], &residuals[0], &residuals[1]); residuals[0] -= T(observed_x_); residuals[1] -= T(observed_y_); return true; } private: const double observed_x_; const double observed_y_; const double point3D_x_; const double point3D_y_; const double point3D_z_; }; // Rig bundle adjustment cost function for variable camera pose and calibration // and point parameters. Different from the standard bundle adjustment function, // this cost function is suitable for camera rigs with consistent relative poses // of the cameras within the rig. The cost function first projects points into // the local system of the camera rig and then into the local system of the // camera within the rig. template class RigReprojErrorCostFunction { public: explicit RigReprojErrorCostFunction(const Eigen::Vector2d& point2D) : observed_x_(point2D(0)), observed_y_(point2D(1)) {} static ceres::CostFunction* Create(const Eigen::Vector2d& point2D) { return ( new ceres::AutoDiffCostFunction, 2, 4, 3, 4, 3, 3, CameraModel::num_params>( new RigReprojErrorCostFunction(point2D))); } template bool operator()(const T* const cam_from_rig_rotation, const T* const cam_from_rig_translation, const T* const rig_from_world_rotation, const T* const rig_from_world_translation, const T* const point3D, const T* const camera_params, T* residuals) const { const Eigen::Matrix point3D_in_cam = EigenQuaternionMap(cam_from_rig_rotation) * (EigenQuaternionMap(rig_from_world_rotation) * EigenVector3Map(point3D) + EigenVector3Map(rig_from_world_translation)) + EigenVector3Map(cam_from_rig_translation); CameraModel::ImgFromCam(camera_params, point3D_in_cam[0], point3D_in_cam[1], point3D_in_cam[2], &residuals[0], &residuals[1]); residuals[0] -= T(observed_x_); residuals[1] -= T(observed_y_); return true; } private: const double observed_x_; const double observed_y_; }; // Cost function for refining two-view geometry based on the Sampson-Error. // // First pose is assumed to be located at the origin with 0 rotation. Second // pose is assumed to be on the unit sphere around the first pose, i.e. the // pose of the second camera is parameterized by a 3D rotation and a // 3D translation with unit norm. `tvec` is therefore over-parameterized as is // and should be down-projected using `SphereManifold`. class SampsonErrorCostFunction { public: SampsonErrorCostFunction(const Eigen::Vector2d& x1, const Eigen::Vector2d& x2) : x1_(x1(0)), y1_(x1(1)), x2_(x2(0)), y2_(x2(1)) {} static ceres::CostFunction* Create(const Eigen::Vector2d& x1, const Eigen::Vector2d& x2) { return (new ceres::AutoDiffCostFunction( new SampsonErrorCostFunction(x1, x2))); } template bool operator()(const T* const cam2_from_cam1_rotation, const T* const cam2_from_cam1_translation, T* residuals) const { const Eigen::Matrix R = EigenQuaternionMap(cam2_from_cam1_rotation).toRotationMatrix(); // Matrix representation of the cross product t x R. Eigen::Matrix t_x; t_x << T(0), -cam2_from_cam1_translation[2], cam2_from_cam1_translation[1], cam2_from_cam1_translation[2], T(0), -cam2_from_cam1_translation[0], -cam2_from_cam1_translation[1], cam2_from_cam1_translation[0], T(0); // Essential matrix. const Eigen::Matrix E = t_x * R; // Homogeneous image coordinates. const Eigen::Matrix x1_h(T(x1_), T(y1_), T(1)); const Eigen::Matrix x2_h(T(x2_), T(y2_), T(1)); // Squared sampson error. const Eigen::Matrix Ex1 = E * x1_h; const Eigen::Matrix Etx2 = E.transpose() * x2_h; const T x2tEx1 = x2_h.transpose() * Ex1; residuals[0] = x2tEx1 * x2tEx1 / (Ex1(0) * Ex1(0) + Ex1(1) * Ex1(1) + Etx2(0) * Etx2(0) + Etx2(1) * Etx2(1)); return true; } private: const double x1_; const double y1_; const double x2_; const double y2_; }; inline void SetQuaternionManifold(ceres::Problem* problem, double* quat_xyzw) { #if CERES_VERSION_MAJOR >= 3 || \ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 1) problem->SetManifold(quat_xyzw, new ceres::EigenQuaternionManifold); #else problem->SetParameterization(quat_xyzw, new ceres::EigenQuaternionParameterization); #endif } inline void SetSubsetManifold(int size, const std::vector& constant_params, ceres::Problem* problem, double* params) { #if CERES_VERSION_MAJOR >= 3 || \ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 1) problem->SetManifold(params, new ceres::SubsetManifold(size, constant_params)); #else problem->SetParameterization( params, new ceres::SubsetParameterization(size, constant_params)); #endif } template inline void SetSphereManifold(ceres::Problem* problem, double* params) { #if CERES_VERSION_MAJOR >= 3 || \ (CERES_VERSION_MAJOR == 2 && CERES_VERSION_MINOR >= 1) problem->SetManifold(params, new ceres::SphereManifold); #else problem->SetParameterization( params, new ceres::HomogeneousVectorParameterization(size)); #endif } } // namespace colmap colmap-3.9.1/src/colmap/estimators/cost_functions_test.cc000066400000000000000000000202151454702036400236460ustar00rootroot00000000000000// 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. #include "colmap/estimators/cost_functions.h" #include "colmap/geometry/pose.h" #include "colmap/sensor/models.h" #include namespace colmap { namespace { TEST(BundleAdjustment, AbsolutePose) { std::unique_ptr cost_function( ReprojErrorCostFunction::Create( Eigen::Vector2d::Zero())); double cam_from_world_rotation[4] = {0, 0, 0, 1}; double cam_from_world_translation[3] = {0, 0, 0}; double point3D[3] = {0, 0, 1}; double camera_params[3] = {1, 0, 0}; double residuals[2]; const double* parameters[4] = {cam_from_world_rotation, cam_from_world_translation, point3D, camera_params}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); point3D[1] = 1; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 1); camera_params[0] = 2; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 2); point3D[0] = -1; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], -2); EXPECT_EQ(residuals[1], 2); } TEST(BundleAdjustment, ConstantPoseAbsolutePose) { Rigid3d cam_from_world; std::unique_ptr cost_function( ReprojErrorConstantPoseCostFunction::Create( cam_from_world, Eigen::Vector2d::Zero())); double point3D[3] = {0, 0, 1}; double camera_params[3] = {1, 0, 0}; double residuals[2]; const double* parameters[2] = {point3D, camera_params}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); point3D[1] = 1; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 1); camera_params[0] = 2; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 2); point3D[0] = -1; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], -2); EXPECT_EQ(residuals[1], 2); } TEST(BundleAdjustment, ConstantPoint3DAbsolutePose) { Eigen::Vector2d point2D = Eigen::Vector2d::Zero(); Eigen::Vector3d point3D; point3D << 0, 0, 1; double cam_from_world_rotation[4] = {0, 0, 0, 1}; double cam_from_world_translation[3] = {0, 0, 0}; double camera_params[3] = {1, 0, 0}; double residuals[2]; const double* parameters[3] = { cam_from_world_rotation, cam_from_world_translation, camera_params}; { std::unique_ptr cost_function( ReprojErrorConstantPoint3DCostFunction< SimplePinholeCameraModel>::Create(point2D, point3D)); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); } { point3D[1] = 1; std::unique_ptr cost_function( ReprojErrorConstantPoint3DCostFunction< SimplePinholeCameraModel>::Create(point2D, point3D)); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 1); } { camera_params[0] = 2; std::unique_ptr cost_function( ReprojErrorConstantPoint3DCostFunction< SimplePinholeCameraModel>::Create(point2D, point3D)); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 2); } { point3D[0] = -1; std::unique_ptr cost_function( ReprojErrorConstantPoint3DCostFunction< SimplePinholeCameraModel>::Create(point2D, point3D)); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], -2); EXPECT_EQ(residuals[1], 2); } } TEST(BundleAdjustment, Rig) { std::unique_ptr cost_function( RigReprojErrorCostFunction::Create( Eigen::Vector2d::Zero())); double cam_from_rig_rotation[4] = {0, 0, 0, 1}; double cam_from_rig_translation[3] = {0, 0, -1}; double rig_from_world_rotation[4] = {0, 0, 0, 1}; double rig_from_world_translation[3] = {0, 0, 1}; double point3D[3] = {0, 0, 1}; double camera_params[3] = {1, 0, 0}; double residuals[2]; const double* parameters[6] = {cam_from_rig_rotation, cam_from_rig_translation, rig_from_world_rotation, rig_from_world_translation, point3D, camera_params}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0); point3D[1] = 1; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 1); camera_params[0] = 2; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 2); point3D[0] = -1; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], -2); EXPECT_EQ(residuals[1], 2); } TEST(BundleAdjustment, RelativePose) { std::unique_ptr cost_function( SampsonErrorCostFunction::Create(Eigen::Vector2d(0, 0), Eigen::Vector2d(0, 0))); double cam_from_world_rotation[4] = {1, 0, 0, 0}; double cam_from_world_translation[3] = {0, 1, 0}; double residuals[1]; const double* parameters[2] = {cam_from_world_rotation, cam_from_world_translation}; EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0); cost_function.reset(SampsonErrorCostFunction::Create(Eigen::Vector2d(0, 0), Eigen::Vector2d(1, 0))); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0.5); cost_function.reset(SampsonErrorCostFunction::Create(Eigen::Vector2d(0, 0), Eigen::Vector2d(1, 1))); EXPECT_TRUE(cost_function->Evaluate(parameters, residuals, nullptr)); EXPECT_EQ(residuals[0], 0.5); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/essential_matrix.cc000066400000000000000000000200241454702036400231200ustar00rootroot00000000000000// 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. #include "colmap/estimators/essential_matrix.h" #include "colmap/estimators/utils.h" #include "colmap/math/math.h" #include "colmap/math/polynomial.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include #include #include #include namespace colmap { void EssentialMatrixFivePointEstimator::Estimate( const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_EQ(points1.size(), points2.size()); CHECK(models != nullptr); models->clear(); // Step 1: Extraction of the nullspace x, y, z, w. Eigen::Matrix Q(points1.size(), 9); for (size_t i = 0; i < points1.size(); ++i) { const double x1_0 = points1[i](0); const double x1_1 = points1[i](1); const double x2_0 = points2[i](0); const double x2_1 = points2[i](1); Q(i, 0) = x1_0 * x2_0; Q(i, 1) = x1_1 * x2_0; Q(i, 2) = x2_0; Q(i, 3) = x1_0 * x2_1; Q(i, 4) = x1_1 * x2_1; Q(i, 5) = x2_1; Q(i, 6) = x1_0; Q(i, 7) = x1_1; Q(i, 8) = 1; } // Extract the 4 Eigen vectors corresponding to the smallest singular values. const Eigen::JacobiSVD> svd( Q, Eigen::ComputeFullV); const Eigen::Matrix E = svd.matrixV().block<9, 4>(0, 5); // Step 3: Gauss-Jordan elimination with partial pivoting on A. Eigen::Matrix A; #include "colmap/estimators/essential_matrix_poly.h" Eigen::Matrix AA = A.block<10, 10>(0, 0).partialPivLu().solve(A.block<10, 10>(0, 10)); // Step 4: Expansion of the determinant polynomial of the 3x3 polynomial // matrix B to obtain the tenth degree polynomial. Eigen::Matrix B; for (size_t i = 0; i < 3; ++i) { B(0, i) = 0; B(4, i) = 0; B(8, i) = 0; B.block<3, 1>(1, i) = AA.block<1, 3>(i * 2 + 4, 0); B.block<3, 1>(5, i) = AA.block<1, 3>(i * 2 + 4, 3); B.block<4, 1>(9, i) = AA.block<1, 4>(i * 2 + 4, 6); B.block<3, 1>(0, i) -= AA.block<1, 3>(i * 2 + 5, 0); B.block<3, 1>(4, i) -= AA.block<1, 3>(i * 2 + 5, 3); B.block<4, 1>(8, i) -= AA.block<1, 4>(i * 2 + 5, 6); } // Step 5: Extraction of roots from the degree 10 polynomial. Eigen::Matrix coeffs; #include "colmap/estimators/essential_matrix_coeffs.h" Eigen::VectorXd roots_real; Eigen::VectorXd roots_imag; if (!FindPolynomialRootsCompanionMatrix(coeffs, &roots_real, &roots_imag)) { return; } models->reserve(roots_real.size()); for (Eigen::VectorXd::Index i = 0; i < roots_imag.size(); ++i) { const double kMaxRootImag = 1e-10; if (std::abs(roots_imag(i)) > kMaxRootImag) { continue; } const double z1 = roots_real(i); const double z2 = z1 * z1; const double z3 = z2 * z1; const double z4 = z3 * z1; Eigen::Matrix3d Bz; for (size_t j = 0; j < 3; ++j) { Bz(j, 0) = B(0, j) * z3 + B(1, j) * z2 + B(2, j) * z1 + B(3, j); Bz(j, 1) = B(4, j) * z3 + B(5, j) * z2 + B(6, j) * z1 + B(7, j); Bz(j, 2) = B(8, j) * z4 + B(9, j) * z3 + B(10, j) * z2 + B(11, j) * z1 + B(12, j); } const Eigen::JacobiSVD svd(Bz, Eigen::ComputeFullV); const Eigen::Vector3d X = svd.matrixV().block<3, 1>(0, 2); const double kMaxX3 = 1e-10; if (std::abs(X(2)) < kMaxX3) { continue; } Eigen::MatrixXd essential_vec = E.col(0) * (X(0) / X(2)) + E.col(1) * (X(1) / X(2)) + E.col(2) * z1 + E.col(3); essential_vec /= essential_vec.norm(); const Eigen::Matrix3d essential_matrix = Eigen::Map>( essential_vec.data()); models->push_back(essential_matrix); } } void EssentialMatrixFivePointEstimator::Residuals( const std::vector& points1, const std::vector& points2, const M_t& E, std::vector* residuals) { ComputeSquaredSampsonError(points1, points2, E, residuals); } void EssentialMatrixEightPointEstimator::Estimate( const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_EQ(points1.size(), points2.size()); CHECK(models != nullptr); models->clear(); // Center and normalize image points for better numerical stability. std::vector normed_points1; std::vector normed_points2; Eigen::Matrix3d normed_from_orig1; Eigen::Matrix3d normed_from_orig2; CenterAndNormalizeImagePoints(points1, &normed_points1, &normed_from_orig1); CenterAndNormalizeImagePoints(points2, &normed_points2, &normed_from_orig2); // Setup homogeneous linear equation as x2' * F * x1 = 0. Eigen::Matrix cmatrix(points1.size(), 9); for (size_t i = 0; i < points1.size(); ++i) { cmatrix.block<1, 3>(i, 0) = normed_points1[i].homogeneous(); cmatrix.block<1, 3>(i, 0) *= normed_points2[i].x(); cmatrix.block<1, 3>(i, 3) = normed_points1[i].homogeneous(); cmatrix.block<1, 3>(i, 3) *= normed_points2[i].y(); cmatrix.block<1, 3>(i, 6) = normed_points1[i].homogeneous(); } // Solve for the nullspace of the constraint matrix. Eigen::JacobiSVD> cmatrix_svd( cmatrix, Eigen::ComputeFullV); const Eigen::VectorXd ematrix_nullspace = cmatrix_svd.matrixV().col(8); const Eigen::Map ematrix_t(ematrix_nullspace.data()); // De-normalize to image points. const Eigen::Matrix3d E_raw = normed_from_orig2.transpose() * ematrix_t.transpose() * normed_from_orig1; // Enforcing the internal constraint that two singular values must be equal // and one must be zero. Eigen::JacobiSVD E_raw_svd( E_raw, Eigen::ComputeFullU | Eigen::ComputeFullV); Eigen::Vector3d singular_values = E_raw_svd.singularValues(); singular_values(0) = (singular_values(0) + singular_values(1)) / 2.0; singular_values(1) = singular_values(0); singular_values(2) = 0.0; const Eigen::Matrix3d E = E_raw_svd.matrixU() * singular_values.asDiagonal() * E_raw_svd.matrixV().transpose(); models->resize(1); (*models)[0] = E; } void EssentialMatrixEightPointEstimator::Residuals( const std::vector& points1, const std::vector& points2, const M_t& E, std::vector* residuals) { ComputeSquaredSampsonError(points1, points2, E, residuals); } } // namespace colmap colmap-3.9.1/src/colmap/estimators/essential_matrix.h000066400000000000000000000116441454702036400227720ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include namespace colmap { // Essential matrix estimator from corresponding normalized point pairs. // // This algorithm solves the 5-Point problem based on the following paper: // // D. Nister, An efficient solution to the five-point relative pose problem, // IEEE-T-PAMI, 26(6), 2004. // http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.86.8769 class EssentialMatrixFivePointEstimator { public: typedef Eigen::Vector2d X_t; typedef Eigen::Vector2d Y_t; typedef Eigen::Matrix3d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 5; // Estimate up to 10 possible essential matrix solutions from a set of // corresponding points. // // The number of corresponding points must be at least 5. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // // @return Up to 10 solutions as a vector of 3x3 essential matrices. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Calculate the residuals of a set of corresponding points and a given // essential matrix. // // Residuals are defined as the squared Sampson error. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param E 3x3 essential matrix. // @param residuals Output vector of residuals. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& E, std::vector* residuals); }; // Essential matrix estimator from corresponding normalized point pairs. // // This algorithm solves the 8-Point problem based on the following paper: // // Hartley and Zisserman, Multiple View Geometry, algorithm 11.1, page 282. class EssentialMatrixEightPointEstimator { public: typedef Eigen::Vector2d X_t; typedef Eigen::Vector2d Y_t; typedef Eigen::Matrix3d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 8; // Estimate essential matrix solutions from set of corresponding points. // // The number of corresponding points must be at least 8. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Calculate the residuals of a set of corresponding points and a given // essential matrix. // // Residuals are defined as the squared Sampson error. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param E 3x3 essential matrix. // @param residuals Output vector of residuals. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& E, std::vector* residuals); }; } // namespace colmap colmap-3.9.1/src/colmap/estimators/essential_matrix_coeffs.h000066400000000000000000000331721454702036400243170ustar00rootroot00000000000000// 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. { const double* b = B.data(); coeffs(0) = b[0] * b[17] * b[34] + b[26] * b[4] * b[21] - b[26] * b[17] * b[8] - b[13] * b[4] * b[34] - b[0] * b[21] * b[30] + b[13] * b[30] * b[8]; coeffs(1) = b[26] * b[4] * b[22] + b[14] * b[30] * b[8] + b[13] * b[31] * b[8] + b[1] * b[17] * b[34] - b[13] * b[5] * b[34] + b[26] * b[5] * b[21] - b[0] * b[21] * b[31] - b[26] * b[17] * b[9] - b[1] * b[21] * b[30] + b[27] * b[4] * b[21] + b[0] * b[17] * b[35] - b[0] * b[22] * b[30] + b[13] * b[30] * b[9] + b[0] * b[18] * b[34] - b[27] * b[17] * b[8] - b[14] * b[4] * b[34] - b[13] * b[4] * b[35] - b[26] * b[18] * b[8]; coeffs(2) = b[14] * b[30] * b[9] + b[14] * b[31] * b[8] + b[13] * b[31] * b[9] - b[13] * b[4] * b[36] - b[13] * b[5] * b[35] + b[15] * b[30] * b[8] - b[13] * b[6] * b[34] + b[13] * b[30] * b[10] + b[13] * b[32] * b[8] - b[14] * b[4] * b[35] - b[14] * b[5] * b[34] + b[26] * b[4] * b[23] + b[26] * b[5] * b[22] + b[26] * b[6] * b[21] - b[26] * b[17] * b[10] - b[15] * b[4] * b[34] - b[26] * b[18] * b[9] - b[26] * b[19] * b[8] + b[27] * b[4] * b[22] + b[27] * b[5] * b[21] - b[27] * b[17] * b[9] - b[27] * b[18] * b[8] - b[1] * b[21] * b[31] - b[0] * b[23] * b[30] - b[0] * b[21] * b[32] + b[28] * b[4] * b[21] - b[28] * b[17] * b[8] + b[2] * b[17] * b[34] + b[0] * b[18] * b[35] - b[0] * b[22] * b[31] + b[0] * b[17] * b[36] + b[0] * b[19] * b[34] - b[1] * b[22] * b[30] + b[1] * b[18] * b[34] + b[1] * b[17] * b[35] - b[2] * b[21] * b[30]; coeffs(3) = b[14] * b[30] * b[10] + b[14] * b[32] * b[8] - b[3] * b[21] * b[30] + b[3] * b[17] * b[34] + b[13] * b[32] * b[9] + b[13] * b[33] * b[8] - b[13] * b[4] * b[37] - b[13] * b[5] * b[36] + b[15] * b[30] * b[9] + b[15] * b[31] * b[8] - b[16] * b[4] * b[34] - b[13] * b[6] * b[35] - b[13] * b[7] * b[34] + b[13] * b[30] * b[11] + b[13] * b[31] * b[10] + b[14] * b[31] * b[9] - b[14] * b[4] * b[36] - b[14] * b[5] * b[35] - b[14] * b[6] * b[34] + b[16] * b[30] * b[8] - b[26] * b[20] * b[8] + b[26] * b[4] * b[24] + b[26] * b[5] * b[23] + b[26] * b[6] * b[22] + b[26] * b[7] * b[21] - b[26] * b[17] * b[11] - b[15] * b[4] * b[35] - b[15] * b[5] * b[34] - b[26] * b[18] * b[10] - b[26] * b[19] * b[9] + b[27] * b[4] * b[23] + b[27] * b[5] * b[22] + b[27] * b[6] * b[21] - b[27] * b[17] * b[10] - b[27] * b[18] * b[9] - b[27] * b[19] * b[8] + b[0] * b[17] * b[37] - b[0] * b[23] * b[31] - b[0] * b[24] * b[30] - b[0] * b[21] * b[33] - b[29] * b[17] * b[8] + b[28] * b[4] * b[22] + b[28] * b[5] * b[21] - b[28] * b[17] * b[9] - b[28] * b[18] * b[8] + b[29] * b[4] * b[21] + b[1] * b[19] * b[34] - b[2] * b[21] * b[31] + b[0] * b[20] * b[34] + b[0] * b[19] * b[35] + b[0] * b[18] * b[36] - b[0] * b[22] * b[32] - b[1] * b[23] * b[30] - b[1] * b[21] * b[32] + b[1] * b[18] * b[35] - b[1] * b[22] * b[31] - b[2] * b[22] * b[30] + b[2] * b[17] * b[35] + b[1] * b[17] * b[36] + b[2] * b[18] * b[34]; coeffs(4) = -b[14] * b[6] * b[35] - b[14] * b[7] * b[34] - b[3] * b[22] * b[30] - b[3] * b[21] * b[31] + b[3] * b[17] * b[35] + b[3] * b[18] * b[34] + b[13] * b[32] * b[10] + b[13] * b[33] * b[9] - b[13] * b[4] * b[38] - b[13] * b[5] * b[37] - b[15] * b[6] * b[34] + b[15] * b[30] * b[10] + b[15] * b[32] * b[8] - b[16] * b[4] * b[35] - b[13] * b[6] * b[36] - b[13] * b[7] * b[35] + b[13] * b[31] * b[11] + b[13] * b[30] * b[12] + b[14] * b[32] * b[9] + b[14] * b[33] * b[8] - b[14] * b[4] * b[37] - b[14] * b[5] * b[36] + b[16] * b[30] * b[9] + b[16] * b[31] * b[8] - b[26] * b[20] * b[9] + b[26] * b[4] * b[25] + b[26] * b[5] * b[24] + b[26] * b[6] * b[23] + b[26] * b[7] * b[22] - b[26] * b[17] * b[12] + b[14] * b[30] * b[11] + b[14] * b[31] * b[10] + b[15] * b[31] * b[9] - b[15] * b[4] * b[36] - b[15] * b[5] * b[35] - b[26] * b[18] * b[11] - b[26] * b[19] * b[10] - b[27] * b[20] * b[8] + b[27] * b[4] * b[24] + b[27] * b[5] * b[23] + b[27] * b[6] * b[22] + b[27] * b[7] * b[21] - b[27] * b[17] * b[11] - b[27] * b[18] * b[10] - b[27] * b[19] * b[9] - b[16] * b[5] * b[34] - b[29] * b[17] * b[9] - b[29] * b[18] * b[8] + b[28] * b[4] * b[23] + b[28] * b[5] * b[22] + b[28] * b[6] * b[21] - b[28] * b[17] * b[10] - b[28] * b[18] * b[9] - b[28] * b[19] * b[8] + b[29] * b[4] * b[22] + b[29] * b[5] * b[21] - b[2] * b[23] * b[30] + b[2] * b[18] * b[35] - b[1] * b[22] * b[32] - b[2] * b[21] * b[32] + b[2] * b[19] * b[34] + b[0] * b[19] * b[36] - b[0] * b[22] * b[33] + b[0] * b[20] * b[35] - b[0] * b[23] * b[32] - b[0] * b[25] * b[30] + b[0] * b[17] * b[38] + b[0] * b[18] * b[37] - b[0] * b[24] * b[31] + b[1] * b[17] * b[37] - b[1] * b[23] * b[31] - b[1] * b[24] * b[30] - b[1] * b[21] * b[33] + b[1] * b[20] * b[34] + b[1] * b[19] * b[35] + b[1] * b[18] * b[36] + b[2] * b[17] * b[36] - b[2] * b[22] * b[31]; coeffs(5) = -b[14] * b[6] * b[36] - b[14] * b[7] * b[35] + b[14] * b[31] * b[11] - b[3] * b[23] * b[30] - b[3] * b[21] * b[32] + b[3] * b[18] * b[35] - b[3] * b[22] * b[31] + b[3] * b[17] * b[36] + b[3] * b[19] * b[34] + b[13] * b[32] * b[11] + b[13] * b[33] * b[10] - b[13] * b[5] * b[38] - b[15] * b[6] * b[35] - b[15] * b[7] * b[34] + b[15] * b[30] * b[11] + b[15] * b[31] * b[10] + b[16] * b[31] * b[9] - b[13] * b[6] * b[37] - b[13] * b[7] * b[36] + b[13] * b[31] * b[12] + b[14] * b[32] * b[10] + b[14] * b[33] * b[9] - b[14] * b[4] * b[38] - b[14] * b[5] * b[37] - b[16] * b[6] * b[34] + b[16] * b[30] * b[10] + b[16] * b[32] * b[8] - b[26] * b[20] * b[10] + b[26] * b[5] * b[25] + b[26] * b[6] * b[24] + b[26] * b[7] * b[23] + b[14] * b[30] * b[12] + b[15] * b[32] * b[9] + b[15] * b[33] * b[8] - b[15] * b[4] * b[37] - b[15] * b[5] * b[36] + b[29] * b[5] * b[22] + b[29] * b[6] * b[21] - b[26] * b[18] * b[12] - b[26] * b[19] * b[11] - b[27] * b[20] * b[9] + b[27] * b[4] * b[25] + b[27] * b[5] * b[24] + b[27] * b[6] * b[23] + b[27] * b[7] * b[22] - b[27] * b[17] * b[12] - b[27] * b[18] * b[11] - b[27] * b[19] * b[10] - b[28] * b[20] * b[8] - b[16] * b[4] * b[36] - b[16] * b[5] * b[35] - b[29] * b[17] * b[10] - b[29] * b[18] * b[9] - b[29] * b[19] * b[8] + b[28] * b[4] * b[24] + b[28] * b[5] * b[23] + b[28] * b[6] * b[22] + b[28] * b[7] * b[21] - b[28] * b[17] * b[11] - b[28] * b[18] * b[10] - b[28] * b[19] * b[9] + b[29] * b[4] * b[23] - b[2] * b[22] * b[32] - b[2] * b[21] * b[33] - b[1] * b[24] * b[31] + b[0] * b[18] * b[38] - b[0] * b[24] * b[32] + b[0] * b[19] * b[37] + b[0] * b[20] * b[36] - b[0] * b[25] * b[31] - b[0] * b[23] * b[33] + b[1] * b[19] * b[36] - b[1] * b[22] * b[33] + b[1] * b[20] * b[35] + b[2] * b[19] * b[35] - b[2] * b[24] * b[30] - b[2] * b[23] * b[31] + b[2] * b[20] * b[34] + b[2] * b[17] * b[37] - b[1] * b[25] * b[30] + b[1] * b[18] * b[37] + b[1] * b[17] * b[38] - b[1] * b[23] * b[32] + b[2] * b[18] * b[36]; coeffs(6) = -b[14] * b[6] * b[37] - b[14] * b[7] * b[36] + b[14] * b[31] * b[12] + b[3] * b[17] * b[37] - b[3] * b[23] * b[31] - b[3] * b[24] * b[30] - b[3] * b[21] * b[33] + b[3] * b[20] * b[34] + b[3] * b[19] * b[35] + b[3] * b[18] * b[36] - b[3] * b[22] * b[32] + b[13] * b[32] * b[12] + b[13] * b[33] * b[11] - b[15] * b[6] * b[36] - b[15] * b[7] * b[35] + b[15] * b[31] * b[11] + b[15] * b[30] * b[12] + b[16] * b[32] * b[9] + b[16] * b[33] * b[8] - b[13] * b[6] * b[38] - b[13] * b[7] * b[37] + b[14] * b[32] * b[11] + b[14] * b[33] * b[10] - b[14] * b[5] * b[38] - b[16] * b[6] * b[35] - b[16] * b[7] * b[34] + b[16] * b[30] * b[11] + b[16] * b[31] * b[10] - b[26] * b[19] * b[12] - b[26] * b[20] * b[11] + b[26] * b[6] * b[25] + b[26] * b[7] * b[24] + b[15] * b[32] * b[10] + b[15] * b[33] * b[9] - b[15] * b[4] * b[38] - b[15] * b[5] * b[37] + b[29] * b[5] * b[23] + b[29] * b[6] * b[22] + b[29] * b[7] * b[21] - b[27] * b[20] * b[10] + b[27] * b[5] * b[25] + b[27] * b[6] * b[24] + b[27] * b[7] * b[23] - b[27] * b[18] * b[12] - b[27] * b[19] * b[11] - b[28] * b[20] * b[9] - b[16] * b[4] * b[37] - b[16] * b[5] * b[36] + b[0] * b[19] * b[38] - b[0] * b[24] * b[33] + b[0] * b[20] * b[37] - b[29] * b[17] * b[11] - b[29] * b[18] * b[10] - b[29] * b[19] * b[9] + b[28] * b[4] * b[25] + b[28] * b[5] * b[24] + b[28] * b[6] * b[23] + b[28] * b[7] * b[22] - b[28] * b[17] * b[12] - b[28] * b[18] * b[11] - b[28] * b[19] * b[10] - b[29] * b[20] * b[8] + b[29] * b[4] * b[24] + b[2] * b[18] * b[37] - b[0] * b[25] * b[32] + b[1] * b[18] * b[38] - b[1] * b[24] * b[32] + b[1] * b[19] * b[37] + b[1] * b[20] * b[36] - b[1] * b[25] * b[31] + b[2] * b[17] * b[38] + b[2] * b[19] * b[36] - b[2] * b[24] * b[31] - b[2] * b[22] * b[33] - b[2] * b[23] * b[32] + b[2] * b[20] * b[35] - b[1] * b[23] * b[33] - b[2] * b[25] * b[30]; coeffs(7) = -b[14] * b[6] * b[38] - b[14] * b[7] * b[37] + b[3] * b[19] * b[36] - b[3] * b[22] * b[33] + b[3] * b[20] * b[35] - b[3] * b[23] * b[32] - b[3] * b[25] * b[30] + b[3] * b[17] * b[38] + b[3] * b[18] * b[37] - b[3] * b[24] * b[31] - b[15] * b[6] * b[37] - b[15] * b[7] * b[36] + b[15] * b[31] * b[12] + b[16] * b[32] * b[10] + b[16] * b[33] * b[9] + b[13] * b[33] * b[12] - b[13] * b[7] * b[38] + b[14] * b[32] * b[12] + b[14] * b[33] * b[11] - b[16] * b[6] * b[36] - b[16] * b[7] * b[35] + b[16] * b[31] * b[11] + b[16] * b[30] * b[12] + b[15] * b[32] * b[11] + b[15] * b[33] * b[10] - b[15] * b[5] * b[38] + b[29] * b[5] * b[24] + b[29] * b[6] * b[23] - b[26] * b[20] * b[12] + b[26] * b[7] * b[25] - b[27] * b[19] * b[12] - b[27] * b[20] * b[11] + b[27] * b[6] * b[25] + b[27] * b[7] * b[24] - b[28] * b[20] * b[10] - b[16] * b[4] * b[38] - b[16] * b[5] * b[37] + b[29] * b[7] * b[22] - b[29] * b[17] * b[12] - b[29] * b[18] * b[11] - b[29] * b[19] * b[10] + b[28] * b[5] * b[25] + b[28] * b[6] * b[24] + b[28] * b[7] * b[23] - b[28] * b[18] * b[12] - b[28] * b[19] * b[11] - b[29] * b[20] * b[9] + b[29] * b[4] * b[25] - b[2] * b[24] * b[32] + b[0] * b[20] * b[38] - b[0] * b[25] * b[33] + b[1] * b[19] * b[38] - b[1] * b[24] * b[33] + b[1] * b[20] * b[37] - b[2] * b[25] * b[31] + b[2] * b[20] * b[36] - b[1] * b[25] * b[32] + b[2] * b[19] * b[37] + b[2] * b[18] * b[38] - b[2] * b[23] * b[33]; coeffs(8) = b[3] * b[18] * b[38] - b[3] * b[24] * b[32] + b[3] * b[19] * b[37] + b[3] * b[20] * b[36] - b[3] * b[25] * b[31] - b[3] * b[23] * b[33] - b[15] * b[6] * b[38] - b[15] * b[7] * b[37] + b[16] * b[32] * b[11] + b[16] * b[33] * b[10] - b[16] * b[5] * b[38] - b[16] * b[6] * b[37] - b[16] * b[7] * b[36] + b[16] * b[31] * b[12] + b[14] * b[33] * b[12] - b[14] * b[7] * b[38] + b[15] * b[32] * b[12] + b[15] * b[33] * b[11] + b[29] * b[5] * b[25] + b[29] * b[6] * b[24] - b[27] * b[20] * b[12] + b[27] * b[7] * b[25] - b[28] * b[19] * b[12] - b[28] * b[20] * b[11] + b[29] * b[7] * b[23] - b[29] * b[18] * b[12] - b[29] * b[19] * b[11] + b[28] * b[6] * b[25] + b[28] * b[7] * b[24] - b[29] * b[20] * b[10] + b[2] * b[19] * b[38] - b[1] * b[25] * b[33] + b[2] * b[20] * b[37] - b[2] * b[24] * b[33] - b[2] * b[25] * b[32] + b[1] * b[20] * b[38]; coeffs(9) = b[29] * b[7] * b[24] - b[29] * b[20] * b[11] + b[2] * b[20] * b[38] - b[2] * b[25] * b[33] - b[28] * b[20] * b[12] + b[28] * b[7] * b[25] - b[29] * b[19] * b[12] - b[3] * b[24] * b[33] + b[15] * b[33] * b[12] + b[3] * b[19] * b[38] - b[16] * b[6] * b[38] + b[3] * b[20] * b[37] + b[16] * b[32] * b[12] + b[29] * b[6] * b[25] - b[16] * b[7] * b[37] - b[3] * b[25] * b[32] - b[15] * b[7] * b[38] + b[16] * b[33] * b[11]; coeffs(10) = -b[29] * b[20] * b[12] + b[29] * b[7] * b[25] + b[16] * b[33] * b[12] - b[16] * b[7] * b[38] + b[3] * b[20] * b[38] - b[3] * b[25] * b[33]; } colmap-3.9.1/src/colmap/estimators/essential_matrix_poly.h000066400000000000000000004514231454702036400240400ustar00rootroot00000000000000// 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. { double* a = A.data(); const double* e = E.data(); double e2[36]; double e3[36]; for (size_t i = 0; i < 36; ++i) { e2[i] = e[i] * e[i]; e3[i] = e2[i] * e[i]; } a[190] = e[33] * e[28] * e[32] - e[33] * e[31] * e[29] + e[30] * e[34] * e[29] - e[30] * e[28] * e[35] - e[27] * e[32] * e[34] + e[27] * e[31] * e[35]; a[7] = 0.5 * e[6] * e2[8] - 0.5 * e[6] * e2[5] + 0.5 * e3[6] + 0.5 * e[6] * e2[7] - 0.5 * e[6] * e2[4] + e[0] * e[2] * e[8] + e[3] * e[4] * e[7] + e[3] * e[5] * e[8] + e[0] * e[1] * e[7] - 0.5 * e[6] * e2[1] - 0.5 * e[6] * e2[2] + 0.5 * e2[0] * e[6] + 0.5 * e2[3] * e[6]; a[120] = e[30] * e[34] * e[2] + e[33] * e[1] * e[32] - e[3] * e[28] * e[35] + e[0] * e[31] * e[35] + e[3] * e[34] * e[29] - e[30] * e[1] * e[35] + e[27] * e[31] * e[8] - e[27] * e[32] * e[7] - e[30] * e[28] * e[8] - e[33] * e[31] * e[2] - e[0] * e[32] * e[34] + e[6] * e[28] * e[32] - e[33] * e[4] * e[29] + e[33] * e[28] * e[5] + e[30] * e[7] * e[29] + e[27] * e[4] * e[35] - e[27] * e[5] * e[34] - e[6] * e[31] * e[29]; a[77] = e[9] * e[27] * e[15] + e[9] * e[29] * e[17] + e[9] * e[11] * e[35] + e[9] * e[28] * e[16] + e[9] * e[10] * e[34] + e[27] * e[11] * e[17] + e[27] * e[10] * e[16] + e[12] * e[30] * e[15] + e[12] * e[32] * e[17] + e[12] * e[14] * e[35] + e[12] * e[31] * e[16] + e[12] * e[13] * e[34] + e[30] * e[14] * e[17] + e[30] * e[13] * e[16] + e[15] * e[35] * e[17] + e[15] * e[34] * e[16] - e[15] * e[28] * e[10] - e[15] * e[31] * e[13] - e[15] * e[32] * e[14] - e[15] * e[29] * e[11] + 0.5 * e2[9] * e[33] + 0.5 * e[33] * e2[16] - 0.5 * e[33] * e2[11] + 0.5 * e[33] * e2[12] + 1.5 * e[33] * e2[15] + 0.5 * e[33] * e2[17] - 0.5 * e[33] * e2[10] - 0.5 * e[33] * e2[14] - 0.5 * e[33] * e2[13]; a[180] = -e[33] * e[22] * e[29] - e[33] * e[31] * e[20] - e[27] * e[32] * e[25] + e[27] * e[22] * e[35] - e[27] * e[23] * e[34] + e[27] * e[31] * e[26] + e[33] * e[28] * e[23] - e[21] * e[28] * e[35] + e[30] * e[25] * e[29] + e[24] * e[28] * e[32] - e[24] * e[31] * e[29] + e[18] * e[31] * e[35] - e[30] * e[28] * e[26] - e[30] * e[19] * e[35] + e[21] * e[34] * e[29] + e[33] * e[19] * e[32] - e[18] * e[32] * e[34] + e[30] * e[34] * e[20]; a[87] = e[18] * e[2] * e[17] + e[3] * e[21] * e[15] + e[3] * e[12] * e[24] + e[3] * e[23] * e[17] + e[3] * e[14] * e[26] + e[3] * e[22] * e[16] + e[3] * e[13] * e[25] + 3. * e[6] * e[24] * e[15] + e[6] * e[26] * e[17] + e[6] * e[25] * e[16] + e[0] * e[20] * e[17] + e[0] * e[11] * e[26] + e[0] * e[19] * e[16] + e[0] * e[10] * e[25] + e[15] * e[26] * e[8] - e[15] * e[20] * e[2] - e[15] * e[19] * e[1] - e[15] * e[22] * e[4] + e[15] * e[25] * e[7] - e[15] * e[23] * e[5] + e[12] * e[21] * e[6] + e[12] * e[22] * e[7] + e[12] * e[4] * e[25] + e[12] * e[23] * e[8] + e[12] * e[5] * e[26] - e[24] * e[11] * e[2] - e[24] * e[10] * e[1] - e[24] * e[13] * e[4] + e[24] * e[16] * e[7] - e[24] * e[14] * e[5] + e[24] * e[17] * e[8] + e[21] * e[13] * e[7] + e[21] * e[4] * e[16] + e[21] * e[14] * e[8] + e[21] * e[5] * e[17] - e[6] * e[23] * e[14] - e[6] * e[20] * e[11] - e[6] * e[19] * e[10] - e[6] * e[22] * e[13] + e[9] * e[18] * e[6] + e[9] * e[0] * e[24] + e[9] * e[19] * e[7] + e[9] * e[1] * e[25] + e[9] * e[20] * e[8] + e[9] * e[2] * e[26] + e[18] * e[0] * e[15] + e[18] * e[10] * e[7] + e[18] * e[1] * e[16] + e[18] * e[11] * e[8]; a[150] = e[33] * e[10] * e[32] + e[33] * e[28] * e[14] - e[33] * e[13] * e[29] - e[33] * e[31] * e[11] + e[9] * e[31] * e[35] - e[9] * e[32] * e[34] + e[27] * e[13] * e[35] - e[27] * e[32] * e[16] + e[27] * e[31] * e[17] - e[27] * e[14] * e[34] + e[12] * e[34] * e[29] - e[12] * e[28] * e[35] + e[30] * e[34] * e[11] + e[30] * e[16] * e[29] - e[30] * e[10] * e[35] - e[30] * e[28] * e[17] + e[15] * e[28] * e[32] - e[15] * e[31] * e[29]; a[57] = e[0] * e[27] * e[6] + e[0] * e[28] * e[7] + e[0] * e[1] * e[34] + e[0] * e[29] * e[8] + e[0] * e[2] * e[35] + e[6] * e[34] * e[7] - e[6] * e[32] * e[5] + e[6] * e[30] * e[3] + e[6] * e[35] * e[8] - e[6] * e[29] * e[2] - e[6] * e[28] * e[1] - e[6] * e[31] * e[4] + e[27] * e[1] * e[7] + e[27] * e[2] * e[8] + e[3] * e[31] * e[7] + e[3] * e[4] * e[34] + e[3] * e[32] * e[8] + e[3] * e[5] * e[35] + e[30] * e[4] * e[7] + e[30] * e[5] * e[8] + 0.5 * e2[0] * e[33] + 1.5 * e[33] * e2[6] - 0.5 * e[33] * e2[4] - 0.5 * e[33] * e2[5] - 0.5 * e[33] * e2[1] + 0.5 * e[33] * e2[7] + 0.5 * e[33] * e2[3] - 0.5 * e[33] * e2[2] + 0.5 * e[33] * e2[8]; a[80] = -e[0] * e[23] * e[16] + e[9] * e[4] * e[26] + e[9] * e[22] * e[8] - e[9] * e[5] * e[25] - e[9] * e[23] * e[7] + e[18] * e[4] * e[17] + e[18] * e[13] * e[8] - e[18] * e[5] * e[16] - e[18] * e[14] * e[7] + e[3] * e[16] * e[20] + e[3] * e[25] * e[11] - e[3] * e[10] * e[26] - e[3] * e[19] * e[17] + e[12] * e[7] * e[20] + e[12] * e[25] * e[2] - e[12] * e[1] * e[26] - e[12] * e[19] * e[8] + e[21] * e[7] * e[11] + e[21] * e[16] * e[2] - e[21] * e[1] * e[17] - e[21] * e[10] * e[8] + e[6] * e[10] * e[23] + e[6] * e[19] * e[14] - e[6] * e[13] * e[20] - e[6] * e[22] * e[11] + e[15] * e[1] * e[23] + e[15] * e[19] * e[5] - e[15] * e[4] * e[20] - e[15] * e[22] * e[2] + e[24] * e[1] * e[14] + e[24] * e[10] * e[5] - e[24] * e[4] * e[11] - e[24] * e[13] * e[2] + e[0] * e[13] * e[26] + e[0] * e[22] * e[17] - e[0] * e[14] * e[25]; a[167] = e[18] * e[19] * e[25] + 0.5 * e3[24] - 0.5 * e[24] * e2[23] + e[18] * e[20] * e[26] + e[21] * e[22] * e[25] + e[21] * e[23] * e[26] - 0.5 * e[24] * e2[19] + 0.5 * e2[21] * e[24] + 0.5 * e[24] * e2[26] - 0.5 * e[24] * e2[20] + 0.5 * e2[18] * e[24] - 0.5 * e[24] * e2[22] + 0.5 * e[24] * e2[25]; a[50] = -e[3] * e[1] * e[35] - e[0] * e[32] * e[7] + e[27] * e[4] * e[8] + e[33] * e[1] * e[5] - e[33] * e[4] * e[2] + e[0] * e[4] * e[35] + e[3] * e[34] * e[2] - e[30] * e[1] * e[8] + e[30] * e[7] * e[2] - e[6] * e[4] * e[29] + e[3] * e[7] * e[29] + e[6] * e[1] * e[32] - e[0] * e[5] * e[34] - e[3] * e[28] * e[8] + e[0] * e[31] * e[8] + e[6] * e[28] * e[5] - e[6] * e[31] * e[2] - e[27] * e[5] * e[7]; a[97] = e[33] * e[16] * e[7] - e[33] * e[14] * e[5] + e[33] * e[17] * e[8] + e[30] * e[13] * e[7] + e[30] * e[4] * e[16] + e[30] * e[14] * e[8] + e[30] * e[5] * e[17] + e[6] * e[27] * e[9] - e[6] * e[28] * e[10] - e[6] * e[31] * e[13] - e[6] * e[32] * e[14] - e[6] * e[29] * e[11] + e[9] * e[28] * e[7] + e[9] * e[1] * e[34] + e[9] * e[29] * e[8] + e[9] * e[2] * e[35] + e[27] * e[10] * e[7] + e[27] * e[1] * e[16] + e[27] * e[11] * e[8] + e[27] * e[2] * e[17] + e[3] * e[30] * e[15] + e[3] * e[12] * e[33] + e[3] * e[32] * e[17] + e[3] * e[14] * e[35] + e[3] * e[31] * e[16] + e[3] * e[13] * e[34] + 3. * e[6] * e[33] * e[15] + e[6] * e[35] * e[17] + e[6] * e[34] * e[16] + e[0] * e[27] * e[15] + e[0] * e[9] * e[33] + e[0] * e[29] * e[17] + e[0] * e[11] * e[35] + e[0] * e[28] * e[16] + e[0] * e[10] * e[34] + e[15] * e[34] * e[7] - e[15] * e[32] * e[5] + e[15] * e[35] * e[8] - e[15] * e[29] * e[2] - e[15] * e[28] * e[1] - e[15] * e[31] * e[4] + e[12] * e[30] * e[6] + e[12] * e[31] * e[7] + e[12] * e[4] * e[34] + e[12] * e[32] * e[8] + e[12] * e[5] * e[35] - e[33] * e[11] * e[2] - e[33] * e[10] * e[1] - e[33] * e[13] * e[4]; a[0] = e[6] * e[1] * e[5] - e[6] * e[4] * e[2] + e[3] * e[7] * e[2] + e[0] * e[4] * e[8] - e[0] * e[5] * e[7] - e[3] * e[1] * e[8]; a[17] = 0.5 * e3[15] + e[9] * e[10] * e[16] - 0.5 * e[15] * e2[11] + e[9] * e[11] * e[17] + 0.5 * e2[12] * e[15] + 0.5 * e[15] * e2[16] + 0.5 * e[15] * e2[17] - 0.5 * e[15] * e2[13] + 0.5 * e2[9] * e[15] + e[12] * e[14] * e[17] - 0.5 * e[15] * e2[10] - 0.5 * e[15] * e2[14] + e[12] * e[13] * e[16]; a[70] = e[15] * e[28] * e[14] - e[15] * e[13] * e[29] - e[15] * e[31] * e[11] + e[33] * e[10] * e[14] - e[33] * e[13] * e[11] + e[9] * e[13] * e[35] - e[9] * e[32] * e[16] + e[9] * e[31] * e[17] - e[9] * e[14] * e[34] + e[27] * e[13] * e[17] - e[27] * e[14] * e[16] + e[12] * e[34] * e[11] + e[12] * e[16] * e[29] - e[12] * e[10] * e[35] - e[12] * e[28] * e[17] + e[30] * e[16] * e[11] - e[30] * e[10] * e[17] + e[15] * e[10] * e[32]; a[177] = e[18] * e[27] * e[24] + e[18] * e[28] * e[25] + e[18] * e[19] * e[34] + e[18] * e[29] * e[26] + e[18] * e[20] * e[35] + e[27] * e[19] * e[25] + e[27] * e[20] * e[26] + e[21] * e[30] * e[24] + e[21] * e[31] * e[25] + e[21] * e[22] * e[34] + e[21] * e[32] * e[26] + e[21] * e[23] * e[35] + e[30] * e[22] * e[25] + e[30] * e[23] * e[26] + e[24] * e[34] * e[25] + e[24] * e[35] * e[26] - e[24] * e[29] * e[20] - e[24] * e[31] * e[22] - e[24] * e[32] * e[23] - e[24] * e[28] * e[19] + 1.5 * e[33] * e2[24] + 0.5 * e[33] * e2[25] + 0.5 * e[33] * e2[26] - 0.5 * e[33] * e2[23] - 0.5 * e[33] * e2[19] - 0.5 * e[33] * e2[20] - 0.5 * e[33] * e2[22] + 0.5 * e2[18] * e[33] + 0.5 * e2[21] * e[33]; a[170] = e[21] * e[25] * e[29] - e[27] * e[23] * e[25] + e[24] * e[19] * e[32] - e[21] * e[28] * e[26] - e[21] * e[19] * e[35] + e[18] * e[31] * e[26] - e[30] * e[19] * e[26] - e[24] * e[31] * e[20] + e[24] * e[28] * e[23] + e[27] * e[22] * e[26] + e[30] * e[25] * e[20] - e[33] * e[22] * e[20] + e[33] * e[19] * e[23] + e[21] * e[34] * e[20] - e[18] * e[23] * e[34] - e[24] * e[22] * e[29] - e[18] * e[32] * e[25] + e[18] * e[22] * e[35]; a[37] = e[12] * e[14] * e[8] + e[12] * e[5] * e[17] + e[15] * e[16] * e[7] + e[15] * e[17] * e[8] + e[0] * e[11] * e[17] + e[0] * e[9] * e[15] + e[0] * e[10] * e[16] + e[3] * e[14] * e[17] + e[3] * e[13] * e[16] + e[9] * e[10] * e[7] + e[9] * e[1] * e[16] + e[9] * e[11] * e[8] + e[9] * e[2] * e[17] - e[15] * e[11] * e[2] - e[15] * e[10] * e[1] - e[15] * e[13] * e[4] - e[15] * e[14] * e[5] + e[12] * e[3] * e[15] + e[12] * e[13] * e[7] + e[12] * e[4] * e[16] + 0.5 * e2[12] * e[6] + 1.5 * e2[15] * e[6] + 0.5 * e[6] * e2[17] + 0.5 * e[6] * e2[16] + 0.5 * e[6] * e2[9] - 0.5 * e[6] * e2[11] - 0.5 * e[6] * e2[10] - 0.5 * e[6] * e2[14] - 0.5 * e[6] * e2[13]; a[10] = -e[9] * e[14] * e[16] - e[12] * e[10] * e[17] + e[9] * e[13] * e[17] - e[15] * e[13] * e[11] + e[15] * e[10] * e[14] + e[12] * e[16] * e[11]; a[67] = e[21] * e[14] * e[17] + e[21] * e[13] * e[16] + e[15] * e[26] * e[17] + e[15] * e[25] * e[16] - e[15] * e[23] * e[14] - e[15] * e[20] * e[11] - e[15] * e[19] * e[10] - e[15] * e[22] * e[13] + e[9] * e[20] * e[17] + e[9] * e[11] * e[26] + e[9] * e[19] * e[16] + e[9] * e[10] * e[25] + 0.5 * e2[12] * e[24] + 1.5 * e[24] * e2[15] + 0.5 * e[24] * e2[17] + 0.5 * e[24] * e2[16] + 0.5 * e2[9] * e[24] - 0.5 * e[24] * e2[11] - 0.5 * e[24] * e2[10] - 0.5 * e[24] * e2[14] - 0.5 * e[24] * e2[13] + e[18] * e[11] * e[17] + e[18] * e[9] * e[15] + e[18] * e[10] * e[16] + e[12] * e[21] * e[15] + e[12] * e[23] * e[17] + e[12] * e[14] * e[26] + e[12] * e[22] * e[16] + e[12] * e[13] * e[25]; a[90] = -e[9] * e[5] * e[34] + e[9] * e[31] * e[8] - e[9] * e[32] * e[7] + e[27] * e[4] * e[17] + e[27] * e[13] * e[8] - e[27] * e[5] * e[16] - e[27] * e[14] * e[7] + e[0] * e[13] * e[35] - e[0] * e[32] * e[16] + e[0] * e[31] * e[17] - e[0] * e[14] * e[34] + e[9] * e[4] * e[35] + e[6] * e[10] * e[32] + e[6] * e[28] * e[14] - e[6] * e[13] * e[29] - e[6] * e[31] * e[11] + e[15] * e[1] * e[32] + e[3] * e[34] * e[11] + e[3] * e[16] * e[29] - e[3] * e[10] * e[35] - e[3] * e[28] * e[17] - e[12] * e[1] * e[35] + e[12] * e[7] * e[29] + e[12] * e[34] * e[2] - e[12] * e[28] * e[8] + e[15] * e[28] * e[5] - e[15] * e[4] * e[29] - e[15] * e[31] * e[2] + e[33] * e[1] * e[14] + e[33] * e[10] * e[5] - e[33] * e[4] * e[11] - e[33] * e[13] * e[2] + e[30] * e[7] * e[11] + e[30] * e[16] * e[2] - e[30] * e[1] * e[17] - e[30] * e[10] * e[8]; a[117] = e[21] * e[31] * e[7] + e[21] * e[4] * e[34] + e[21] * e[32] * e[8] + e[21] * e[5] * e[35] + e[30] * e[22] * e[7] + e[30] * e[4] * e[25] + e[30] * e[23] * e[8] + e[30] * e[5] * e[26] + 3. * e[24] * e[33] * e[6] + e[24] * e[34] * e[7] + e[24] * e[35] * e[8] + e[33] * e[25] * e[7] + e[33] * e[26] * e[8] + e[0] * e[27] * e[24] + e[0] * e[18] * e[33] + e[0] * e[28] * e[25] + e[0] * e[19] * e[34] + e[0] * e[29] * e[26] + e[0] * e[20] * e[35] + e[18] * e[27] * e[6] + e[18] * e[28] * e[7] + e[18] * e[1] * e[34] + e[18] * e[29] * e[8] + e[18] * e[2] * e[35] + e[27] * e[19] * e[7] + e[27] * e[1] * e[25] + e[27] * e[20] * e[8] + e[27] * e[2] * e[26] + e[3] * e[30] * e[24] + e[3] * e[21] * e[33] + e[3] * e[31] * e[25] + e[3] * e[22] * e[34] + e[3] * e[32] * e[26] + e[3] * e[23] * e[35] + e[6] * e[30] * e[21] - e[6] * e[29] * e[20] + e[6] * e[35] * e[26] - e[6] * e[31] * e[22] - e[6] * e[32] * e[23] - e[6] * e[28] * e[19] + e[6] * e[34] * e[25] - e[24] * e[32] * e[5] - e[24] * e[29] * e[2] - e[24] * e[28] * e[1] - e[24] * e[31] * e[4] - e[33] * e[20] * e[2] - e[33] * e[19] * e[1] - e[33] * e[22] * e[4] - e[33] * e[23] * e[5]; a[160] = e[21] * e[25] * e[20] - e[21] * e[19] * e[26] + e[18] * e[22] * e[26] - e[18] * e[23] * e[25] - e[24] * e[22] * e[20] + e[24] * e[19] * e[23]; a[47] = e[3] * e[4] * e[25] + e[3] * e[23] * e[8] + e[3] * e[5] * e[26] + e[21] * e[4] * e[7] + e[21] * e[5] * e[8] + e[6] * e[25] * e[7] + e[6] * e[26] * e[8] + e[0] * e[19] * e[7] + e[0] * e[1] * e[25] + e[0] * e[20] * e[8] + e[0] * e[2] * e[26] - e[6] * e[20] * e[2] - e[6] * e[19] * e[1] - e[6] * e[22] * e[4] - e[6] * e[23] * e[5] + e[18] * e[1] * e[7] + e[18] * e[0] * e[6] + e[18] * e[2] * e[8] + e[3] * e[21] * e[6] + e[3] * e[22] * e[7] - 0.5 * e[24] * e2[4] + 0.5 * e[24] * e2[0] + 1.5 * e[24] * e2[6] - 0.5 * e[24] * e2[5] - 0.5 * e[24] * e2[1] + 0.5 * e[24] * e2[7] + 0.5 * e[24] * e2[3] - 0.5 * e[24] * e2[2] + 0.5 * e[24] * e2[8]; a[110] = e[6] * e[28] * e[23] - e[6] * e[22] * e[29] - e[6] * e[31] * e[20] - e[3] * e[19] * e[35] + e[3] * e[34] * e[20] + e[3] * e[25] * e[29] - e[21] * e[1] * e[35] + e[21] * e[7] * e[29] + e[21] * e[34] * e[2] + e[24] * e[1] * e[32] + e[24] * e[28] * e[5] - e[24] * e[4] * e[29] - e[24] * e[31] * e[2] + e[33] * e[1] * e[23] + e[33] * e[19] * e[5] - e[33] * e[4] * e[20] - e[33] * e[22] * e[2] - e[21] * e[28] * e[8] + e[30] * e[7] * e[20] + e[30] * e[25] * e[2] - e[30] * e[1] * e[26] + e[18] * e[4] * e[35] - e[18] * e[5] * e[34] + e[18] * e[31] * e[8] - e[18] * e[32] * e[7] + e[27] * e[4] * e[26] + e[27] * e[22] * e[8] - e[27] * e[5] * e[25] - e[27] * e[23] * e[7] - e[3] * e[28] * e[26] - e[0] * e[32] * e[25] + e[0] * e[22] * e[35] - e[0] * e[23] * e[34] + e[0] * e[31] * e[26] - e[30] * e[19] * e[8] + e[6] * e[19] * e[32]; a[107] = 0.5 * e2[18] * e[6] + 0.5 * e2[21] * e[6] + 1.5 * e2[24] * e[6] + 0.5 * e[6] * e2[26] - 0.5 * e[6] * e2[23] - 0.5 * e[6] * e2[19] - 0.5 * e[6] * e2[20] - 0.5 * e[6] * e2[22] + 0.5 * e[6] * e2[25] + e[21] * e[3] * e[24] + e[18] * e[20] * e[8] + e[21] * e[4] * e[25] + e[18] * e[19] * e[7] + e[18] * e[1] * e[25] + e[21] * e[22] * e[7] + e[21] * e[23] * e[8] + e[18] * e[0] * e[24] + e[18] * e[2] * e[26] + e[21] * e[5] * e[26] + e[24] * e[26] * e[8] - e[24] * e[20] * e[2] - e[24] * e[19] * e[1] - e[24] * e[22] * e[4] + e[24] * e[25] * e[7] - e[24] * e[23] * e[5] + e[0] * e[19] * e[25] + e[0] * e[20] * e[26] + e[3] * e[22] * e[25] + e[3] * e[23] * e[26]; a[40] = e[18] * e[4] * e[8] + e[3] * e[7] * e[20] + e[3] * e[25] * e[2] - e[3] * e[1] * e[26] - e[18] * e[5] * e[7] + e[6] * e[1] * e[23] + e[6] * e[19] * e[5] - e[6] * e[4] * e[20] - e[6] * e[22] * e[2] + e[21] * e[7] * e[2] - e[21] * e[1] * e[8] + e[24] * e[1] * e[5] - e[24] * e[4] * e[2] - e[3] * e[19] * e[8] + e[0] * e[4] * e[26] + e[0] * e[22] * e[8] - e[0] * e[5] * e[25] - e[0] * e[23] * e[7]; a[27] = e[9] * e[1] * e[7] + e[9] * e[0] * e[6] + e[9] * e[2] * e[8] + e[3] * e[12] * e[6] + e[3] * e[13] * e[7] + e[3] * e[4] * e[16] + e[3] * e[14] * e[8] + e[3] * e[5] * e[17] + e[12] * e[4] * e[7] + e[12] * e[5] * e[8] + e[6] * e[16] * e[7] + e[6] * e[17] * e[8] - e[6] * e[11] * e[2] - e[6] * e[10] * e[1] - e[6] * e[13] * e[4] - e[6] * e[14] * e[5] + e[0] * e[10] * e[7] + e[0] * e[1] * e[16] + e[0] * e[11] * e[8] + e[0] * e[2] * e[17] + 0.5 * e2[3] * e[15] + 1.5 * e[15] * e2[6] + 0.5 * e[15] * e2[7] + 0.5 * e[15] * e2[8] + 0.5 * e2[0] * e[15] - 0.5 * e[15] * e2[4] - 0.5 * e[15] * e2[5] - 0.5 * e[15] * e2[1] - 0.5 * e[15] * e2[2]; a[30] = -e[15] * e[13] * e[2] - e[6] * e[13] * e[11] - e[15] * e[4] * e[11] + e[12] * e[16] * e[2] - e[3] * e[10] * e[17] + e[3] * e[16] * e[11] + e[0] * e[13] * e[17] - e[0] * e[14] * e[16] + e[15] * e[1] * e[14] - e[12] * e[10] * e[8] + e[9] * e[4] * e[17] + e[9] * e[13] * e[8] - e[9] * e[5] * e[16] - e[9] * e[14] * e[7] + e[15] * e[10] * e[5] + e[12] * e[7] * e[11] + e[6] * e[10] * e[14] - e[12] * e[1] * e[17]; a[147] = e[12] * e[30] * e[24] + e[12] * e[21] * e[33] + e[12] * e[31] * e[25] + e[12] * e[22] * e[34] + e[12] * e[32] * e[26] + e[12] * e[23] * e[35] + e[9] * e[27] * e[24] + e[9] * e[18] * e[33] + e[9] * e[28] * e[25] + e[9] * e[19] * e[34] + e[9] * e[29] * e[26] + e[9] * e[20] * e[35] + e[21] * e[30] * e[15] + e[21] * e[32] * e[17] + e[21] * e[14] * e[35] + e[21] * e[31] * e[16] + e[21] * e[13] * e[34] + e[30] * e[23] * e[17] + e[30] * e[14] * e[26] + e[30] * e[22] * e[16] + e[30] * e[13] * e[25] + e[15] * e[27] * e[18] + 3. * e[15] * e[33] * e[24] - e[15] * e[29] * e[20] + e[15] * e[35] * e[26] - e[15] * e[31] * e[22] - e[15] * e[32] * e[23] - e[15] * e[28] * e[19] + e[15] * e[34] * e[25] + e[18] * e[29] * e[17] + e[18] * e[11] * e[35] + e[18] * e[28] * e[16] + e[18] * e[10] * e[34] + e[27] * e[20] * e[17] + e[27] * e[11] * e[26] + e[27] * e[19] * e[16] + e[27] * e[10] * e[25] - e[24] * e[28] * e[10] - e[24] * e[31] * e[13] - e[24] * e[32] * e[14] + e[24] * e[34] * e[16] + e[24] * e[35] * e[17] - e[24] * e[29] * e[11] - e[33] * e[23] * e[14] + e[33] * e[25] * e[16] + e[33] * e[26] * e[17] - e[33] * e[20] * e[11] - e[33] * e[19] * e[10] - e[33] * e[22] * e[13]; a[60] = e[18] * e[13] * e[17] + e[9] * e[13] * e[26] + e[9] * e[22] * e[17] - e[9] * e[14] * e[25] - e[18] * e[14] * e[16] - e[15] * e[13] * e[20] - e[15] * e[22] * e[11] + e[12] * e[16] * e[20] + e[12] * e[25] * e[11] - e[12] * e[10] * e[26] - e[12] * e[19] * e[17] + e[21] * e[16] * e[11] - e[21] * e[10] * e[17] - e[9] * e[23] * e[16] + e[24] * e[10] * e[14] - e[24] * e[13] * e[11] + e[15] * e[10] * e[23] + e[15] * e[19] * e[14]; a[137] = e[21] * e[12] * e[24] + e[21] * e[23] * e[17] + e[21] * e[14] * e[26] + e[21] * e[22] * e[16] + e[21] * e[13] * e[25] + e[24] * e[26] * e[17] + e[24] * e[25] * e[16] + e[9] * e[19] * e[25] + e[9] * e[18] * e[24] + e[9] * e[20] * e[26] + e[12] * e[22] * e[25] + e[12] * e[23] * e[26] + e[18] * e[20] * e[17] + e[18] * e[11] * e[26] + e[18] * e[19] * e[16] + e[18] * e[10] * e[25] - e[24] * e[23] * e[14] - e[24] * e[20] * e[11] - e[24] * e[19] * e[10] - e[24] * e[22] * e[13] + 0.5 * e2[21] * e[15] + 1.5 * e2[24] * e[15] + 0.5 * e[15] * e2[25] + 0.5 * e[15] * e2[26] + 0.5 * e[15] * e2[18] - 0.5 * e[15] * e2[23] - 0.5 * e[15] * e2[19] - 0.5 * e[15] * e2[20] - 0.5 * e[15] * e2[22]; a[20] = e[6] * e[1] * e[14] + e[15] * e[1] * e[5] - e[0] * e[5] * e[16] - e[0] * e[14] * e[7] + e[0] * e[13] * e[8] - e[15] * e[4] * e[2] + e[12] * e[7] * e[2] + e[6] * e[10] * e[5] + e[3] * e[7] * e[11] - e[6] * e[4] * e[11] + e[3] * e[16] * e[2] - e[6] * e[13] * e[2] - e[3] * e[1] * e[17] - e[9] * e[5] * e[7] - e[3] * e[10] * e[8] - e[12] * e[1] * e[8] + e[0] * e[4] * e[17] + e[9] * e[4] * e[8]; a[16] = -0.5 * e[14] * e2[16] - 0.5 * e[14] * e2[10] - 0.5 * e[14] * e2[9] + e[11] * e[9] * e[12] + 0.5 * e3[14] + e[17] * e[13] * e[16] + 0.5 * e[14] * e2[12] + e[11] * e[10] * e[13] - 0.5 * e[14] * e2[15] + 0.5 * e[14] * e2[17] + e[17] * e[12] * e[15] + 0.5 * e2[11] * e[14] + 0.5 * e[14] * e2[13]; a[100] = -e[21] * e[19] * e[8] + e[18] * e[4] * e[26] - e[18] * e[5] * e[25] - e[18] * e[23] * e[7] + e[21] * e[25] * e[2] - e[21] * e[1] * e[26] + e[6] * e[19] * e[23] + e[18] * e[22] * e[8] - e[0] * e[23] * e[25] - e[6] * e[22] * e[20] + e[24] * e[1] * e[23] + e[24] * e[19] * e[5] - e[24] * e[4] * e[20] - e[24] * e[22] * e[2] + e[3] * e[25] * e[20] - e[3] * e[19] * e[26] + e[0] * e[22] * e[26] + e[21] * e[7] * e[20]; a[176] = 0.5 * e2[20] * e[32] + 1.5 * e[32] * e2[23] + 0.5 * e[32] * e2[22] + 0.5 * e[32] * e2[21] + 0.5 * e[32] * e2[26] - 0.5 * e[32] * e2[18] - 0.5 * e[32] * e2[19] - 0.5 * e[32] * e2[24] - 0.5 * e[32] * e2[25] + e[20] * e[27] * e[21] + e[20] * e[18] * e[30] + e[20] * e[28] * e[22] + e[20] * e[19] * e[31] + e[20] * e[29] * e[23] + e[29] * e[19] * e[22] + e[29] * e[18] * e[21] + e[23] * e[30] * e[21] + e[23] * e[31] * e[22] + e[26] * e[30] * e[24] + e[26] * e[21] * e[33] + e[26] * e[31] * e[25] + e[26] * e[22] * e[34] + e[26] * e[23] * e[35] + e[35] * e[22] * e[25] + e[35] * e[21] * e[24] - e[23] * e[27] * e[18] - e[23] * e[33] * e[24] - e[23] * e[28] * e[19] - e[23] * e[34] * e[25]; a[130] = -e[9] * e[23] * e[25] - e[21] * e[10] * e[26] - e[21] * e[19] * e[17] - e[18] * e[23] * e[16] + e[18] * e[13] * e[26] + e[12] * e[25] * e[20] - e[12] * e[19] * e[26] - e[15] * e[22] * e[20] + e[21] * e[16] * e[20] + e[21] * e[25] * e[11] + e[24] * e[10] * e[23] + e[24] * e[19] * e[14] - e[24] * e[13] * e[20] - e[24] * e[22] * e[11] + e[18] * e[22] * e[17] - e[18] * e[14] * e[25] + e[9] * e[22] * e[26] + e[15] * e[19] * e[23]; a[166] = 0.5 * e[23] * e2[21] + e[20] * e[19] * e[22] + e[20] * e[18] * e[21] + 0.5 * e3[23] + e[26] * e[22] * e[25] + 0.5 * e[23] * e2[26] - 0.5 * e[23] * e2[18] + 0.5 * e[23] * e2[22] - 0.5 * e[23] * e2[19] + e[26] * e[21] * e[24] + 0.5 * e2[20] * e[23] - 0.5 * e[23] * e2[24] - 0.5 * e[23] * e2[25]; a[140] = e[18] * e[13] * e[35] - e[18] * e[32] * e[16] + e[18] * e[31] * e[17] - e[18] * e[14] * e[34] + e[27] * e[13] * e[26] + e[27] * e[22] * e[17] - e[27] * e[14] * e[25] - e[27] * e[23] * e[16] - e[9] * e[32] * e[25] + e[9] * e[22] * e[35] - e[9] * e[23] * e[34] + e[9] * e[31] * e[26] + e[15] * e[19] * e[32] + e[15] * e[28] * e[23] - e[15] * e[22] * e[29] - e[15] * e[31] * e[20] + e[24] * e[10] * e[32] + e[24] * e[28] * e[14] - e[24] * e[13] * e[29] - e[24] * e[31] * e[11] + e[33] * e[10] * e[23] + e[33] * e[19] * e[14] - e[33] * e[13] * e[20] - e[33] * e[22] * e[11] + e[21] * e[16] * e[29] - e[21] * e[10] * e[35] - e[21] * e[28] * e[17] + e[30] * e[16] * e[20] + e[30] * e[25] * e[11] - e[30] * e[10] * e[26] - e[30] * e[19] * e[17] - e[12] * e[28] * e[26] - e[12] * e[19] * e[35] + e[12] * e[34] * e[20] + e[12] * e[25] * e[29] + e[21] * e[34] * e[11]; a[96] = -e[32] * e[10] * e[1] + e[32] * e[13] * e[4] - e[32] * e[16] * e[7] - e[32] * e[15] * e[6] - e[32] * e[9] * e[0] + e[32] * e[12] * e[3] + e[17] * e[30] * e[6] + e[17] * e[3] * e[33] + e[17] * e[31] * e[7] + e[17] * e[4] * e[34] + e[17] * e[5] * e[35] - e[5] * e[27] * e[9] - e[5] * e[28] * e[10] - e[5] * e[33] * e[15] - e[5] * e[34] * e[16] + e[5] * e[29] * e[11] + e[35] * e[12] * e[6] + e[35] * e[3] * e[15] + e[35] * e[13] * e[7] + e[35] * e[4] * e[16] + e[11] * e[27] * e[3] + e[11] * e[0] * e[30] + e[11] * e[28] * e[4] + e[11] * e[1] * e[31] + e[29] * e[9] * e[3] + e[29] * e[0] * e[12] + e[29] * e[10] * e[4] + e[29] * e[1] * e[13] + e[5] * e[30] * e[12] + 3. * e[5] * e[32] * e[14] + e[5] * e[31] * e[13] + e[8] * e[30] * e[15] + e[8] * e[12] * e[33] + e[8] * e[32] * e[17] + e[8] * e[14] * e[35] + e[8] * e[31] * e[16] + e[8] * e[13] * e[34] + e[2] * e[27] * e[12] + e[2] * e[9] * e[30] + e[2] * e[29] * e[14] + e[2] * e[11] * e[32] + e[2] * e[28] * e[13] + e[2] * e[10] * e[31] - e[14] * e[27] * e[0] - e[14] * e[34] * e[7] - e[14] * e[33] * e[6] + e[14] * e[30] * e[3] - e[14] * e[28] * e[1] + e[14] * e[31] * e[4]; a[181] = 0.5 * e[18] * e2[29] + 0.5 * e[18] * e2[28] + 0.5 * e[18] * e2[30] + 0.5 * e[18] * e2[33] - 0.5 * e[18] * e2[32] - 0.5 * e[18] * e2[31] - 0.5 * e[18] * e2[34] - 0.5 * e[18] * e2[35] + 1.5 * e[18] * e2[27] + e[27] * e[28] * e[19] + e[27] * e[29] * e[20] + e[21] * e[27] * e[30] + e[21] * e[29] * e[32] + e[21] * e[28] * e[31] + e[30] * e[28] * e[22] + e[30] * e[19] * e[31] + e[30] * e[29] * e[23] + e[30] * e[20] * e[32] + e[24] * e[27] * e[33] + e[24] * e[29] * e[35] + e[24] * e[28] * e[34] + e[33] * e[28] * e[25] + e[33] * e[19] * e[34] + e[33] * e[29] * e[26] + e[33] * e[20] * e[35] - e[27] * e[35] * e[26] - e[27] * e[31] * e[22] - e[27] * e[32] * e[23] - e[27] * e[34] * e[25]; a[46] = e[20] * e[1] * e[4] + e[20] * e[0] * e[3] + e[20] * e[2] * e[5] + e[5] * e[21] * e[3] + e[5] * e[22] * e[4] + e[8] * e[21] * e[6] + e[8] * e[3] * e[24] + e[8] * e[22] * e[7] + e[8] * e[4] * e[25] + e[8] * e[5] * e[26] + e[26] * e[4] * e[7] + e[26] * e[3] * e[6] + e[2] * e[18] * e[3] + e[2] * e[0] * e[21] + e[2] * e[19] * e[4] + e[2] * e[1] * e[22] - e[5] * e[19] * e[1] - e[5] * e[18] * e[0] - e[5] * e[25] * e[7] - e[5] * e[24] * e[6] + 0.5 * e[23] * e2[4] - 0.5 * e[23] * e2[0] - 0.5 * e[23] * e2[6] + 1.5 * e[23] * e2[5] - 0.5 * e[23] * e2[1] - 0.5 * e[23] * e2[7] + 0.5 * e[23] * e2[3] + 0.5 * e[23] * e2[2] + 0.5 * e[23] * e2[8]; a[151] = 1.5 * e[9] * e2[27] + 0.5 * e[9] * e2[29] + 0.5 * e[9] * e2[28] - 0.5 * e[9] * e2[32] - 0.5 * e[9] * e2[31] + 0.5 * e[9] * e2[33] + 0.5 * e[9] * e2[30] - 0.5 * e[9] * e2[34] - 0.5 * e[9] * e2[35] + e[33] * e[27] * e[15] + e[33] * e[29] * e[17] + e[33] * e[11] * e[35] + e[33] * e[28] * e[16] + e[33] * e[10] * e[34] + e[27] * e[29] * e[11] + e[27] * e[28] * e[10] + e[27] * e[30] * e[12] - e[27] * e[31] * e[13] - e[27] * e[32] * e[14] - e[27] * e[34] * e[16] - e[27] * e[35] * e[17] + e[30] * e[29] * e[14] + e[30] * e[11] * e[32] + e[30] * e[28] * e[13] + e[30] * e[10] * e[31] + e[12] * e[29] * e[32] + e[12] * e[28] * e[31] + e[15] * e[29] * e[35] + e[15] * e[28] * e[34]; a[116] = -e[32] * e[24] * e[6] + e[8] * e[30] * e[24] + e[8] * e[21] * e[33] + e[8] * e[31] * e[25] + e[8] * e[22] * e[34] + e[26] * e[30] * e[6] + e[26] * e[3] * e[33] + e[26] * e[31] * e[7] + e[26] * e[4] * e[34] + e[26] * e[32] * e[8] + e[26] * e[5] * e[35] + e[35] * e[21] * e[6] + e[35] * e[3] * e[24] + e[35] * e[22] * e[7] + e[35] * e[4] * e[25] + e[35] * e[23] * e[8] + e[2] * e[27] * e[21] + e[2] * e[18] * e[30] + e[2] * e[28] * e[22] + e[2] * e[19] * e[31] + e[2] * e[29] * e[23] + e[2] * e[20] * e[32] + e[20] * e[27] * e[3] + e[20] * e[0] * e[30] + e[20] * e[28] * e[4] + e[20] * e[1] * e[31] + e[20] * e[29] * e[5] + e[29] * e[18] * e[3] + e[29] * e[0] * e[21] + e[29] * e[19] * e[4] + e[29] * e[1] * e[22] + e[5] * e[30] * e[21] + e[5] * e[31] * e[22] + 3. * e[5] * e[32] * e[23] - e[5] * e[27] * e[18] - e[5] * e[33] * e[24] - e[5] * e[28] * e[19] - e[5] * e[34] * e[25] - e[23] * e[27] * e[0] - e[23] * e[34] * e[7] - e[23] * e[33] * e[6] + e[23] * e[30] * e[3] - e[23] * e[28] * e[1] + e[23] * e[31] * e[4] + e[32] * e[21] * e[3] - e[32] * e[19] * e[1] + e[32] * e[22] * e[4] - e[32] * e[18] * e[0] - e[32] * e[25] * e[7]; a[191] = 0.5 * e[27] * e2[33] - 0.5 * e[27] * e2[32] - 0.5 * e[27] * e2[31] - 0.5 * e[27] * e2[34] - 0.5 * e[27] * e2[35] + e[33] * e[29] * e[35] + 0.5 * e[27] * e2[29] + e[30] * e[29] * e[32] + e[30] * e[28] * e[31] + e[33] * e[28] * e[34] + 0.5 * e[27] * e2[28] + 0.5 * e[27] * e2[30] + 0.5 * e3[27]; a[66] = e[14] * e[21] * e[12] + e[14] * e[22] * e[13] + e[17] * e[21] * e[15] + e[17] * e[12] * e[24] + e[17] * e[14] * e[26] + e[17] * e[22] * e[16] + e[17] * e[13] * e[25] + e[26] * e[12] * e[15] + e[26] * e[13] * e[16] - e[14] * e[24] * e[15] - e[14] * e[25] * e[16] - e[14] * e[18] * e[9] - e[14] * e[19] * e[10] + e[11] * e[18] * e[12] + e[11] * e[9] * e[21] + e[11] * e[19] * e[13] + e[11] * e[10] * e[22] + e[20] * e[11] * e[14] + e[20] * e[9] * e[12] + e[20] * e[10] * e[13] + 1.5 * e[23] * e2[14] + 0.5 * e[23] * e2[12] + 0.5 * e[23] * e2[13] + 0.5 * e[23] * e2[17] + 0.5 * e2[11] * e[23] - 0.5 * e[23] * e2[16] - 0.5 * e[23] * e2[9] - 0.5 * e[23] * e2[15] - 0.5 * e[23] * e2[10]; a[121] = 1.5 * e[0] * e2[27] + 0.5 * e[0] * e2[29] + 0.5 * e[0] * e2[28] + 0.5 * e[0] * e2[30] - 0.5 * e[0] * e2[32] - 0.5 * e[0] * e2[31] + 0.5 * e[0] * e2[33] - 0.5 * e[0] * e2[34] - 0.5 * e[0] * e2[35] - e[27] * e[31] * e[4] + e[3] * e[27] * e[30] + e[3] * e[29] * e[32] + e[3] * e[28] * e[31] + e[30] * e[28] * e[4] + e[30] * e[1] * e[31] + e[30] * e[29] * e[5] + e[30] * e[2] * e[32] + e[6] * e[27] * e[33] + e[6] * e[29] * e[35] + e[6] * e[28] * e[34] + e[27] * e[28] * e[1] + e[27] * e[29] * e[2] + e[33] * e[28] * e[7] + e[33] * e[1] * e[34] + e[33] * e[29] * e[8] + e[33] * e[2] * e[35] - e[27] * e[34] * e[7] - e[27] * e[32] * e[5] - e[27] * e[35] * e[8]; a[36] = e[14] * e[12] * e[3] + e[14] * e[13] * e[4] + e[17] * e[12] * e[6] + e[17] * e[3] * e[15] + e[17] * e[13] * e[7] + e[17] * e[4] * e[16] + e[17] * e[14] * e[8] + e[8] * e[12] * e[15] + e[8] * e[13] * e[16] + e[2] * e[11] * e[14] + e[2] * e[9] * e[12] + e[2] * e[10] * e[13] + e[11] * e[9] * e[3] + e[11] * e[0] * e[12] + e[11] * e[10] * e[4] + e[11] * e[1] * e[13] - e[14] * e[10] * e[1] - e[14] * e[16] * e[7] - e[14] * e[15] * e[6] - e[14] * e[9] * e[0] - 0.5 * e[5] * e2[16] - 0.5 * e[5] * e2[9] + 0.5 * e[5] * e2[11] + 0.5 * e[5] * e2[12] - 0.5 * e[5] * e2[15] - 0.5 * e[5] * e2[10] + 0.5 * e[5] * e2[13] + 1.5 * e2[14] * e[5] + 0.5 * e[5] * e2[17]; a[71] = 1.5 * e[27] * e2[9] - 0.5 * e[27] * e2[16] + 0.5 * e[27] * e2[11] + 0.5 * e[27] * e2[12] + 0.5 * e[27] * e2[15] - 0.5 * e[27] * e2[17] + 0.5 * e[27] * e2[10] - 0.5 * e[27] * e2[14] - 0.5 * e[27] * e2[13] + e[12] * e[10] * e[31] + e[30] * e[11] * e[14] + e[30] * e[10] * e[13] + e[15] * e[9] * e[33] + e[15] * e[29] * e[17] + e[15] * e[11] * e[35] + e[15] * e[28] * e[16] + e[15] * e[10] * e[34] + e[33] * e[11] * e[17] + e[33] * e[10] * e[16] - e[9] * e[31] * e[13] - e[9] * e[32] * e[14] - e[9] * e[34] * e[16] - e[9] * e[35] * e[17] + e[9] * e[29] * e[11] + e[9] * e[28] * e[10] + e[12] * e[9] * e[30] + e[12] * e[29] * e[14] + e[12] * e[11] * e[32] + e[12] * e[28] * e[13]; a[146] = e[29] * e[18] * e[12] + e[29] * e[9] * e[21] + e[29] * e[19] * e[13] + e[29] * e[10] * e[22] + e[17] * e[30] * e[24] + e[17] * e[21] * e[33] + e[17] * e[31] * e[25] + e[17] * e[22] * e[34] + e[17] * e[32] * e[26] + e[17] * e[23] * e[35] - e[23] * e[27] * e[9] - e[23] * e[28] * e[10] - e[23] * e[33] * e[15] - e[23] * e[34] * e[16] - e[32] * e[24] * e[15] - e[32] * e[25] * e[16] - e[32] * e[18] * e[9] - e[32] * e[19] * e[10] + e[26] * e[30] * e[15] + e[26] * e[12] * e[33] + e[26] * e[31] * e[16] + e[26] * e[13] * e[34] + e[35] * e[21] * e[15] + e[35] * e[12] * e[24] + e[35] * e[22] * e[16] + e[35] * e[13] * e[25] + e[14] * e[30] * e[21] + e[14] * e[31] * e[22] + 3. * e[14] * e[32] * e[23] + e[11] * e[27] * e[21] + e[11] * e[18] * e[30] + e[11] * e[28] * e[22] + e[11] * e[19] * e[31] + e[11] * e[29] * e[23] + e[11] * e[20] * e[32] + e[23] * e[30] * e[12] + e[23] * e[31] * e[13] + e[32] * e[21] * e[12] + e[32] * e[22] * e[13] - e[14] * e[27] * e[18] - e[14] * e[33] * e[24] + e[14] * e[29] * e[20] + e[14] * e[35] * e[26] - e[14] * e[28] * e[19] - e[14] * e[34] * e[25] + e[20] * e[27] * e[12] + e[20] * e[9] * e[30] + e[20] * e[28] * e[13] + e[20] * e[10] * e[31]; a[1] = 0.5 * e[0] * e2[1] + 0.5 * e[0] * e2[2] + e[6] * e[2] * e[8] + e[6] * e[1] * e[7] + 0.5 * e[0] * e2[3] + e[3] * e[1] * e[4] + 0.5 * e[0] * e2[6] + e[3] * e[2] * e[5] - 0.5 * e[0] * e2[5] - 0.5 * e[0] * e2[8] + 0.5 * e3[0] - 0.5 * e[0] * e2[7] - 0.5 * e[0] * e2[4]; a[136] = 1.5 * e2[23] * e[14] + 0.5 * e[14] * e2[26] - 0.5 * e[14] * e2[18] - 0.5 * e[14] * e2[19] + 0.5 * e[14] * e2[20] + 0.5 * e[14] * e2[22] - 0.5 * e[14] * e2[24] + 0.5 * e[14] * e2[21] - 0.5 * e[14] * e2[25] + e[23] * e[21] * e[12] + e[23] * e[22] * e[13] + e[26] * e[21] * e[15] + e[26] * e[12] * e[24] + e[26] * e[23] * e[17] + e[26] * e[22] * e[16] + e[26] * e[13] * e[25] + e[17] * e[22] * e[25] + e[17] * e[21] * e[24] + e[11] * e[19] * e[22] + e[11] * e[18] * e[21] + e[11] * e[20] * e[23] + e[20] * e[18] * e[12] + e[20] * e[9] * e[21] + e[20] * e[19] * e[13] + e[20] * e[10] * e[22] - e[23] * e[24] * e[15] - e[23] * e[25] * e[16] - e[23] * e[18] * e[9] - e[23] * e[19] * e[10]; a[51] = 1.5 * e[27] * e2[0] - 0.5 * e[27] * e2[4] + 0.5 * e[27] * e2[6] - 0.5 * e[27] * e2[5] + 0.5 * e[27] * e2[1] - 0.5 * e[27] * e2[7] + 0.5 * e[27] * e2[3] + 0.5 * e[27] * e2[2] - 0.5 * e[27] * e2[8] + e[0] * e[33] * e[6] + e[0] * e[30] * e[3] - e[0] * e[35] * e[8] - e[0] * e[31] * e[4] + e[3] * e[28] * e[4] + e[3] * e[1] * e[31] + e[3] * e[29] * e[5] + e[3] * e[2] * e[32] + e[30] * e[1] * e[4] + e[30] * e[2] * e[5] + e[6] * e[28] * e[7] + e[6] * e[1] * e[34] + e[6] * e[29] * e[8] + e[6] * e[2] * e[35] + e[33] * e[1] * e[7] + e[33] * e[2] * e[8] + e[0] * e[28] * e[1] + e[0] * e[29] * e[2] - e[0] * e[34] * e[7] - e[0] * e[32] * e[5]; a[106] = e[8] * e[22] * e[25] + e[8] * e[21] * e[24] + e[20] * e[18] * e[3] + e[20] * e[0] * e[21] + e[20] * e[19] * e[4] + e[20] * e[1] * e[22] + e[20] * e[2] * e[23] + e[23] * e[21] * e[3] + e[23] * e[22] * e[4] + e[23] * e[26] * e[8] - e[23] * e[19] * e[1] - e[23] * e[18] * e[0] - e[23] * e[25] * e[7] - e[23] * e[24] * e[6] + e[2] * e[19] * e[22] + e[2] * e[18] * e[21] + e[26] * e[21] * e[6] + e[26] * e[3] * e[24] + e[26] * e[22] * e[7] + e[26] * e[4] * e[25] + 0.5 * e2[20] * e[5] + 1.5 * e2[23] * e[5] + 0.5 * e[5] * e2[22] + 0.5 * e[5] * e2[21] + 0.5 * e[5] * e2[26] - 0.5 * e[5] * e2[18] - 0.5 * e[5] * e2[19] - 0.5 * e[5] * e2[24] - 0.5 * e[5] * e2[25]; a[81] = e[24] * e[11] * e[8] + e[24] * e[2] * e[17] + 3. * e[9] * e[18] * e[0] + e[9] * e[19] * e[1] + e[9] * e[20] * e[2] + e[18] * e[10] * e[1] + e[18] * e[11] * e[2] + e[3] * e[18] * e[12] + e[3] * e[9] * e[21] + e[3] * e[20] * e[14] + e[3] * e[11] * e[23] + e[3] * e[19] * e[13] + e[3] * e[10] * e[22] + e[6] * e[18] * e[15] + e[6] * e[9] * e[24] + e[6] * e[20] * e[17] + e[6] * e[11] * e[26] + e[6] * e[19] * e[16] + e[6] * e[10] * e[25] + e[0] * e[20] * e[11] + e[0] * e[19] * e[10] - e[9] * e[26] * e[8] - e[9] * e[22] * e[4] - e[9] * e[25] * e[7] - e[9] * e[23] * e[5] + e[12] * e[0] * e[21] + e[12] * e[19] * e[4] + e[12] * e[1] * e[22] + e[12] * e[20] * e[5] + e[12] * e[2] * e[23] - e[18] * e[13] * e[4] - e[18] * e[16] * e[7] - e[18] * e[14] * e[5] - e[18] * e[17] * e[8] + e[21] * e[10] * e[4] + e[21] * e[1] * e[13] + e[21] * e[11] * e[5] + e[21] * e[2] * e[14] + e[15] * e[0] * e[24] + e[15] * e[19] * e[7] + e[15] * e[1] * e[25] + e[15] * e[20] * e[8] + e[15] * e[2] * e[26] - e[0] * e[23] * e[14] - e[0] * e[25] * e[16] - e[0] * e[26] * e[17] - e[0] * e[22] * e[13] + e[24] * e[10] * e[7] + e[24] * e[1] * e[16]; a[26] = e[11] * e[1] * e[4] + e[11] * e[0] * e[3] + e[11] * e[2] * e[5] + e[5] * e[12] * e[3] + e[5] * e[13] * e[4] + e[8] * e[12] * e[6] + e[8] * e[3] * e[15] + e[8] * e[13] * e[7] + e[8] * e[4] * e[16] + e[8] * e[5] * e[17] + e[17] * e[4] * e[7] + e[17] * e[3] * e[6] - e[5] * e[10] * e[1] - e[5] * e[16] * e[7] - e[5] * e[15] * e[6] - e[5] * e[9] * e[0] + e[2] * e[9] * e[3] + e[2] * e[0] * e[12] + e[2] * e[10] * e[4] + e[2] * e[1] * e[13] + 0.5 * e2[2] * e[14] - 0.5 * e[14] * e2[0] - 0.5 * e[14] * e2[6] - 0.5 * e[14] * e2[1] - 0.5 * e[14] * e2[7] + 1.5 * e[14] * e2[5] + 0.5 * e[14] * e2[4] + 0.5 * e[14] * e2[3] + 0.5 * e[14] * e2[8]; a[91] = e[3] * e[27] * e[12] + e[3] * e[9] * e[30] + e[3] * e[29] * e[14] + e[3] * e[11] * e[32] + e[3] * e[28] * e[13] + e[3] * e[10] * e[31] + e[6] * e[27] * e[15] + e[6] * e[9] * e[33] + e[6] * e[29] * e[17] + e[6] * e[11] * e[35] + e[6] * e[28] * e[16] + e[6] * e[10] * e[34] + 3. * e[0] * e[27] * e[9] + e[0] * e[29] * e[11] + e[0] * e[28] * e[10] - e[9] * e[34] * e[7] - e[9] * e[32] * e[5] - e[9] * e[35] * e[8] + e[9] * e[29] * e[2] + e[9] * e[28] * e[1] - e[9] * e[31] * e[4] + e[12] * e[0] * e[30] + e[12] * e[28] * e[4] + e[12] * e[1] * e[31] + e[12] * e[29] * e[5] + e[12] * e[2] * e[32] + e[27] * e[11] * e[2] + e[27] * e[10] * e[1] - e[27] * e[13] * e[4] - e[27] * e[16] * e[7] - e[27] * e[14] * e[5] - e[27] * e[17] * e[8] + e[30] * e[10] * e[4] + e[30] * e[1] * e[13] + e[30] * e[11] * e[5] + e[30] * e[2] * e[14] + e[15] * e[0] * e[33] + e[15] * e[28] * e[7] + e[15] * e[1] * e[34] + e[15] * e[29] * e[8] + e[15] * e[2] * e[35] - e[0] * e[31] * e[13] - e[0] * e[32] * e[14] - e[0] * e[34] * e[16] - e[0] * e[35] * e[17] + e[33] * e[10] * e[7] + e[33] * e[1] * e[16] + e[33] * e[11] * e[8] + e[33] * e[2] * e[17]; a[127] = 0.5 * e2[30] * e[6] + 0.5 * e[6] * e2[27] - 0.5 * e[6] * e2[32] - 0.5 * e[6] * e2[28] - 0.5 * e[6] * e2[29] - 0.5 * e[6] * e2[31] + 1.5 * e[6] * e2[33] + 0.5 * e[6] * e2[34] + 0.5 * e[6] * e2[35] + e[0] * e[27] * e[33] + e[0] * e[29] * e[35] + e[0] * e[28] * e[34] + e[3] * e[30] * e[33] + e[3] * e[32] * e[35] + e[3] * e[31] * e[34] + e[30] * e[31] * e[7] + e[30] * e[4] * e[34] + e[30] * e[32] * e[8] + e[30] * e[5] * e[35] + e[27] * e[28] * e[7] + e[27] * e[1] * e[34] + e[27] * e[29] * e[8] + e[27] * e[2] * e[35] + e[33] * e[34] * e[7] + e[33] * e[35] * e[8] - e[33] * e[32] * e[5] - e[33] * e[29] * e[2] - e[33] * e[28] * e[1] - e[33] * e[31] * e[4]; a[161] = e[24] * e[20] * e[26] + e[21] * e[19] * e[22] - 0.5 * e[18] * e2[22] - 0.5 * e[18] * e2[25] + 0.5 * e3[18] + 0.5 * e[18] * e2[21] + e[21] * e[20] * e[23] + 0.5 * e[18] * e2[20] + 0.5 * e[18] * e2[19] + 0.5 * e[18] * e2[24] + e[24] * e[19] * e[25] - 0.5 * e[18] * e2[23] - 0.5 * e[18] * e2[26]; a[197] = 0.5 * e[33] * e2[35] + 0.5 * e3[33] + 0.5 * e2[27] * e[33] + 0.5 * e2[30] * e[33] - 0.5 * e[33] * e2[29] + 0.5 * e[33] * e2[34] - 0.5 * e[33] * e2[32] - 0.5 * e[33] * e2[28] + e[30] * e[32] * e[35] - 0.5 * e[33] * e2[31] + e[27] * e[29] * e[35] + e[27] * e[28] * e[34] + e[30] * e[31] * e[34]; a[171] = 1.5 * e[27] * e2[18] + 0.5 * e[27] * e2[19] + 0.5 * e[27] * e2[20] + 0.5 * e[27] * e2[21] + 0.5 * e[27] * e2[24] - 0.5 * e[27] * e2[26] - 0.5 * e[27] * e2[23] - 0.5 * e[27] * e2[22] - 0.5 * e[27] * e2[25] + e[33] * e[20] * e[26] - e[18] * e[35] * e[26] - e[18] * e[31] * e[22] - e[18] * e[32] * e[23] - e[18] * e[34] * e[25] + e[18] * e[28] * e[19] + e[18] * e[29] * e[20] + e[21] * e[18] * e[30] + e[21] * e[28] * e[22] + e[21] * e[19] * e[31] + e[21] * e[29] * e[23] + e[21] * e[20] * e[32] + e[30] * e[19] * e[22] + e[30] * e[20] * e[23] + e[24] * e[18] * e[33] + e[24] * e[28] * e[25] + e[24] * e[19] * e[34] + e[24] * e[29] * e[26] + e[24] * e[20] * e[35] + e[33] * e[19] * e[25]; a[157] = e[9] * e[27] * e[33] + e[9] * e[29] * e[35] + e[9] * e[28] * e[34] + e[33] * e[35] * e[17] + e[33] * e[34] * e[16] + e[27] * e[29] * e[17] + e[27] * e[11] * e[35] + e[27] * e[28] * e[16] + e[27] * e[10] * e[34] + e[33] * e[30] * e[12] - e[33] * e[28] * e[10] - e[33] * e[31] * e[13] - e[33] * e[32] * e[14] - e[33] * e[29] * e[11] + e[30] * e[32] * e[17] + e[30] * e[14] * e[35] + e[30] * e[31] * e[16] + e[30] * e[13] * e[34] + e[12] * e[32] * e[35] + e[12] * e[31] * e[34] + 0.5 * e[15] * e2[27] - 0.5 * e[15] * e2[32] - 0.5 * e[15] * e2[28] - 0.5 * e[15] * e2[29] - 0.5 * e[15] * e2[31] + 1.5 * e[15] * e2[33] + 0.5 * e[15] * e2[30] + 0.5 * e[15] * e2[34] + 0.5 * e[15] * e2[35]; a[11] = 0.5 * e[9] * e2[12] - 0.5 * e[9] * e2[16] + 0.5 * e[9] * e2[10] - 0.5 * e[9] * e2[17] - 0.5 * e[9] * e2[13] + e[15] * e[10] * e[16] + e[12] * e[11] * e[14] + 0.5 * e[9] * e2[11] + 0.5 * e[9] * e2[15] - 0.5 * e[9] * e2[14] + e[15] * e[11] * e[17] + 0.5 * e3[9] + e[12] * e[10] * e[13]; a[187] = e[18] * e[27] * e[33] + e[18] * e[29] * e[35] + e[18] * e[28] * e[34] + e[27] * e[28] * e[25] + e[27] * e[19] * e[34] + e[27] * e[29] * e[26] + e[27] * e[20] * e[35] + e[21] * e[30] * e[33] + e[21] * e[32] * e[35] + e[21] * e[31] * e[34] + e[30] * e[31] * e[25] + e[30] * e[22] * e[34] + e[30] * e[32] * e[26] + e[30] * e[23] * e[35] + e[33] * e[34] * e[25] + e[33] * e[35] * e[26] - e[33] * e[29] * e[20] - e[33] * e[31] * e[22] - e[33] * e[32] * e[23] - e[33] * e[28] * e[19] + 0.5 * e2[27] * e[24] + 0.5 * e2[30] * e[24] + 1.5 * e[24] * e2[33] + 0.5 * e[24] * e2[35] + 0.5 * e[24] * e2[34] - 0.5 * e[24] * e2[32] - 0.5 * e[24] * e2[28] - 0.5 * e[24] * e2[29] - 0.5 * e[24] * e2[31]; a[131] = 0.5 * e[9] * e2[21] + 0.5 * e[9] * e2[24] + 0.5 * e[9] * e2[19] + 1.5 * e[9] * e2[18] + 0.5 * e[9] * e2[20] - 0.5 * e[9] * e2[26] - 0.5 * e[9] * e2[23] - 0.5 * e[9] * e2[22] - 0.5 * e[9] * e2[25] + e[21] * e[18] * e[12] + e[21] * e[20] * e[14] + e[21] * e[11] * e[23] + e[21] * e[19] * e[13] + e[21] * e[10] * e[22] + e[24] * e[18] * e[15] + e[24] * e[20] * e[17] + e[24] * e[11] * e[26] + e[24] * e[19] * e[16] + e[24] * e[10] * e[25] + e[15] * e[19] * e[25] + e[15] * e[20] * e[26] + e[12] * e[19] * e[22] + e[12] * e[20] * e[23] + e[18] * e[20] * e[11] + e[18] * e[19] * e[10] - e[18] * e[23] * e[14] - e[18] * e[25] * e[16] - e[18] * e[26] * e[17] - e[18] * e[22] * e[13]; a[189] = 0.5 * e2[29] * e[26] + 0.5 * e2[32] * e[26] + 0.5 * e[26] * e2[33] + 1.5 * e[26] * e2[35] + 0.5 * e[26] * e2[34] - 0.5 * e[26] * e2[27] - 0.5 * e[26] * e2[28] - 0.5 * e[26] * e2[31] - 0.5 * e[26] * e2[30] + e[20] * e[27] * e[33] + e[20] * e[29] * e[35] + e[20] * e[28] * e[34] + e[29] * e[27] * e[24] + e[29] * e[18] * e[33] + e[29] * e[28] * e[25] + e[29] * e[19] * e[34] + e[23] * e[30] * e[33] + e[23] * e[32] * e[35] + e[23] * e[31] * e[34] + e[32] * e[30] * e[24] + e[32] * e[21] * e[33] + e[32] * e[31] * e[25] + e[32] * e[22] * e[34] + e[35] * e[33] * e[24] + e[35] * e[34] * e[25] - e[35] * e[27] * e[18] - e[35] * e[30] * e[21] - e[35] * e[31] * e[22] - e[35] * e[28] * e[19]; a[141] = e[12] * e[19] * e[31] + e[12] * e[29] * e[23] + e[12] * e[20] * e[32] + 3. * e[9] * e[27] * e[18] + e[9] * e[28] * e[19] + e[9] * e[29] * e[20] + e[21] * e[9] * e[30] + e[21] * e[29] * e[14] + e[21] * e[11] * e[32] + e[21] * e[28] * e[13] + e[21] * e[10] * e[31] + e[30] * e[20] * e[14] + e[30] * e[11] * e[23] + e[30] * e[19] * e[13] + e[30] * e[10] * e[22] + e[9] * e[33] * e[24] - e[9] * e[35] * e[26] - e[9] * e[31] * e[22] - e[9] * e[32] * e[23] - e[9] * e[34] * e[25] + e[18] * e[29] * e[11] + e[18] * e[28] * e[10] + e[27] * e[20] * e[11] + e[27] * e[19] * e[10] + e[15] * e[27] * e[24] + e[15] * e[18] * e[33] + e[15] * e[28] * e[25] + e[15] * e[19] * e[34] + e[15] * e[29] * e[26] + e[15] * e[20] * e[35] - e[18] * e[31] * e[13] - e[18] * e[32] * e[14] - e[18] * e[34] * e[16] - e[18] * e[35] * e[17] - e[27] * e[23] * e[14] - e[27] * e[25] * e[16] - e[27] * e[26] * e[17] - e[27] * e[22] * e[13] + e[24] * e[29] * e[17] + e[24] * e[11] * e[35] + e[24] * e[28] * e[16] + e[24] * e[10] * e[34] + e[33] * e[20] * e[17] + e[33] * e[11] * e[26] + e[33] * e[19] * e[16] + e[33] * e[10] * e[25] + e[12] * e[27] * e[21] + e[12] * e[18] * e[30] + e[12] * e[28] * e[22]; a[159] = -0.5 * e[17] * e2[27] + 0.5 * e[17] * e2[32] - 0.5 * e[17] * e2[28] + 0.5 * e[17] * e2[29] - 0.5 * e[17] * e2[31] + 0.5 * e[17] * e2[33] - 0.5 * e[17] * e2[30] + 0.5 * e[17] * e2[34] + 1.5 * e[17] * e2[35] + e[32] * e[30] * e[15] + e[32] * e[12] * e[33] + e[32] * e[31] * e[16] + e[32] * e[13] * e[34] + e[14] * e[30] * e[33] + e[14] * e[31] * e[34] + e[11] * e[27] * e[33] + e[11] * e[29] * e[35] + e[11] * e[28] * e[34] + e[35] * e[33] * e[15] + e[35] * e[34] * e[16] + e[29] * e[27] * e[15] + e[29] * e[9] * e[33] + e[29] * e[28] * e[16] + e[29] * e[10] * e[34] - e[35] * e[27] * e[9] - e[35] * e[30] * e[12] - e[35] * e[28] * e[10] - e[35] * e[31] * e[13] + e[35] * e[32] * e[14]; a[21] = 0.5 * e[9] * e2[1] + 1.5 * e[9] * e2[0] + 0.5 * e[9] * e2[2] + 0.5 * e[9] * e2[3] + 0.5 * e[9] * e2[6] - 0.5 * e[9] * e2[4] - 0.5 * e[9] * e2[5] - 0.5 * e[9] * e2[7] - 0.5 * e[9] * e2[8] + e[6] * e[0] * e[15] + e[6] * e[10] * e[7] + e[6] * e[1] * e[16] + e[6] * e[11] * e[8] + e[6] * e[2] * e[17] + e[15] * e[1] * e[7] + e[15] * e[2] * e[8] + e[0] * e[11] * e[2] + e[0] * e[10] * e[1] - e[0] * e[13] * e[4] - e[0] * e[16] * e[7] - e[0] * e[14] * e[5] - e[0] * e[17] * e[8] + e[3] * e[0] * e[12] + e[3] * e[10] * e[4] + e[3] * e[1] * e[13] + e[3] * e[11] * e[5] + e[3] * e[2] * e[14] + e[12] * e[1] * e[4] + e[12] * e[2] * e[5]; a[199] = 0.5 * e[35] * e2[33] + 0.5 * e[35] * e2[34] - 0.5 * e[35] * e2[27] - 0.5 * e[35] * e2[28] - 0.5 * e[35] * e2[31] - 0.5 * e[35] * e2[30] + e[32] * e[31] * e[34] + 0.5 * e2[29] * e[35] + 0.5 * e2[32] * e[35] + e[29] * e[28] * e[34] + e[32] * e[30] * e[33] + 0.5 * e3[35] + e[29] * e[27] * e[33]; a[101] = 0.5 * e[0] * e2[19] + 0.5 * e[0] * e2[20] + 0.5 * e[0] * e2[24] - 0.5 * e[0] * e2[26] - 0.5 * e[0] * e2[23] - 0.5 * e[0] * e2[22] - 0.5 * e[0] * e2[25] + 1.5 * e2[18] * e[0] + 0.5 * e[0] * e2[21] + e[18] * e[19] * e[1] + e[18] * e[20] * e[2] + e[21] * e[18] * e[3] + e[21] * e[19] * e[4] + e[21] * e[1] * e[22] + e[21] * e[20] * e[5] + e[21] * e[2] * e[23] - e[18] * e[26] * e[8] - e[18] * e[22] * e[4] - e[18] * e[25] * e[7] - e[18] * e[23] * e[5] + e[18] * e[24] * e[6] + e[3] * e[19] * e[22] + e[3] * e[20] * e[23] + e[24] * e[19] * e[7] + e[24] * e[1] * e[25] + e[24] * e[20] * e[8] + e[24] * e[2] * e[26] + e[6] * e[19] * e[25] + e[6] * e[20] * e[26]; a[129] = 0.5 * e2[32] * e[8] - 0.5 * e[8] * e2[27] - 0.5 * e[8] * e2[28] + 0.5 * e[8] * e2[29] - 0.5 * e[8] * e2[31] + 0.5 * e[8] * e2[33] - 0.5 * e[8] * e2[30] + 0.5 * e[8] * e2[34] + 1.5 * e[8] * e2[35] + e[2] * e[27] * e[33] + e[2] * e[29] * e[35] + e[2] * e[28] * e[34] + e[5] * e[30] * e[33] + e[5] * e[32] * e[35] + e[5] * e[31] * e[34] + e[32] * e[30] * e[6] + e[32] * e[3] * e[33] + e[32] * e[31] * e[7] + e[32] * e[4] * e[34] + e[29] * e[27] * e[6] + e[29] * e[0] * e[33] + e[29] * e[28] * e[7] + e[29] * e[1] * e[34] + e[35] * e[33] * e[6] + e[35] * e[34] * e[7] - e[35] * e[27] * e[0] - e[35] * e[30] * e[3] - e[35] * e[28] * e[1] - e[35] * e[31] * e[4]; a[41] = -0.5 * e[18] * e2[4] + 1.5 * e[18] * e2[0] + 0.5 * e[18] * e2[6] - 0.5 * e[18] * e2[5] + 0.5 * e[18] * e2[1] - 0.5 * e[18] * e2[7] + 0.5 * e[18] * e2[3] + 0.5 * e[18] * e2[2] - 0.5 * e[18] * e2[8] + e[3] * e[0] * e[21] + e[3] * e[19] * e[4] + e[3] * e[1] * e[22] + e[3] * e[20] * e[5] + e[3] * e[2] * e[23] + e[21] * e[1] * e[4] + e[21] * e[2] * e[5] + e[6] * e[0] * e[24] + e[6] * e[19] * e[7] + e[6] * e[1] * e[25] + e[6] * e[20] * e[8] + e[6] * e[2] * e[26] + e[24] * e[1] * e[7] + e[24] * e[2] * e[8] + e[0] * e[19] * e[1] + e[0] * e[20] * e[2] - e[0] * e[26] * e[8] - e[0] * e[22] * e[4] - e[0] * e[25] * e[7] - e[0] * e[23] * e[5]; a[28] = e[10] * e[1] * e[7] + e[10] * e[0] * e[6] + e[10] * e[2] * e[8] + e[4] * e[12] * e[6] + e[4] * e[3] * e[15] + e[4] * e[13] * e[7] + e[4] * e[14] * e[8] + e[4] * e[5] * e[17] + e[13] * e[3] * e[6] + e[13] * e[5] * e[8] + e[7] * e[15] * e[6] + e[7] * e[17] * e[8] - e[7] * e[11] * e[2] - e[7] * e[9] * e[0] - e[7] * e[14] * e[5] - e[7] * e[12] * e[3] + e[1] * e[9] * e[6] + e[1] * e[0] * e[15] + e[1] * e[11] * e[8] + e[1] * e[2] * e[17] + 1.5 * e[16] * e2[7] + 0.5 * e[16] * e2[6] + 0.5 * e[16] * e2[8] + 0.5 * e2[1] * e[16] - 0.5 * e[16] * e2[0] - 0.5 * e[16] * e2[5] - 0.5 * e[16] * e2[3] - 0.5 * e[16] * e2[2] + 0.5 * e2[4] * e[16]; a[111] = e[0] * e[30] * e[21] - e[0] * e[35] * e[26] - e[0] * e[31] * e[22] - e[0] * e[32] * e[23] - e[0] * e[34] * e[25] - e[18] * e[34] * e[7] - e[18] * e[32] * e[5] - e[18] * e[35] * e[8] - e[18] * e[31] * e[4] - e[27] * e[26] * e[8] - e[27] * e[22] * e[4] - e[27] * e[25] * e[7] - e[27] * e[23] * e[5] + e[6] * e[28] * e[25] + e[6] * e[19] * e[34] + e[6] * e[29] * e[26] + e[6] * e[20] * e[35] + e[21] * e[28] * e[4] + e[21] * e[1] * e[31] + e[21] * e[29] * e[5] + e[21] * e[2] * e[32] + e[30] * e[19] * e[4] + e[30] * e[1] * e[22] + e[30] * e[20] * e[5] + e[30] * e[2] * e[23] + e[24] * e[27] * e[6] + e[24] * e[0] * e[33] + e[24] * e[28] * e[7] + e[24] * e[1] * e[34] + e[24] * e[29] * e[8] + e[24] * e[2] * e[35] + e[33] * e[18] * e[6] + e[33] * e[19] * e[7] + e[33] * e[1] * e[25] + e[33] * e[20] * e[8] + e[33] * e[2] * e[26] + 3. * e[0] * e[27] * e[18] + e[0] * e[28] * e[19] + e[0] * e[29] * e[20] + e[18] * e[28] * e[1] + e[18] * e[29] * e[2] + e[27] * e[19] * e[1] + e[27] * e[20] * e[2] + e[3] * e[27] * e[21] + e[3] * e[18] * e[30] + e[3] * e[28] * e[22] + e[3] * e[19] * e[31] + e[3] * e[29] * e[23] + e[3] * e[20] * e[32]; a[108] = e[19] * e[18] * e[6] + e[19] * e[0] * e[24] + e[19] * e[1] * e[25] + e[19] * e[20] * e[8] + e[19] * e[2] * e[26] + e[22] * e[21] * e[6] + e[22] * e[3] * e[24] + e[22] * e[4] * e[25] + e[22] * e[23] * e[8] + e[22] * e[5] * e[26] - e[25] * e[21] * e[3] + e[25] * e[26] * e[8] - e[25] * e[20] * e[2] - e[25] * e[18] * e[0] - e[25] * e[23] * e[5] + e[25] * e[24] * e[6] + e[1] * e[18] * e[24] + e[1] * e[20] * e[26] + e[4] * e[21] * e[24] + e[4] * e[23] * e[26] + 0.5 * e2[19] * e[7] + 0.5 * e2[22] * e[7] + 1.5 * e2[25] * e[7] + 0.5 * e[7] * e2[26] - 0.5 * e[7] * e2[18] - 0.5 * e[7] * e2[23] - 0.5 * e[7] * e2[20] + 0.5 * e[7] * e2[24] - 0.5 * e[7] * e2[21]; a[61] = 0.5 * e[18] * e2[11] + 1.5 * e[18] * e2[9] + 0.5 * e[18] * e2[10] + 0.5 * e[18] * e2[12] + 0.5 * e[18] * e2[15] - 0.5 * e[18] * e2[16] - 0.5 * e[18] * e2[17] - 0.5 * e[18] * e2[14] - 0.5 * e[18] * e2[13] + e[12] * e[9] * e[21] + e[12] * e[20] * e[14] + e[12] * e[11] * e[23] + e[12] * e[19] * e[13] + e[12] * e[10] * e[22] + e[21] * e[11] * e[14] + e[21] * e[10] * e[13] + e[15] * e[9] * e[24] + e[15] * e[20] * e[17] + e[15] * e[11] * e[26] + e[15] * e[19] * e[16] + e[15] * e[10] * e[25] + e[24] * e[11] * e[17] + e[24] * e[10] * e[16] - e[9] * e[23] * e[14] - e[9] * e[25] * e[16] - e[9] * e[26] * e[17] + e[9] * e[20] * e[11] + e[9] * e[19] * e[10] - e[9] * e[22] * e[13]; a[138] = e[13] * e[21] * e[24] + e[13] * e[23] * e[26] + e[19] * e[18] * e[15] + e[19] * e[9] * e[24] + e[19] * e[20] * e[17] + e[19] * e[11] * e[26] - e[25] * e[23] * e[14] - e[25] * e[20] * e[11] - e[25] * e[18] * e[9] - e[25] * e[21] * e[12] + e[22] * e[21] * e[15] + e[22] * e[12] * e[24] + e[22] * e[23] * e[17] + e[22] * e[14] * e[26] + e[22] * e[13] * e[25] + e[25] * e[24] * e[15] + e[25] * e[26] * e[17] + e[10] * e[19] * e[25] + e[10] * e[18] * e[24] + e[10] * e[20] * e[26] - 0.5 * e[16] * e2[18] - 0.5 * e[16] * e2[23] + 0.5 * e[16] * e2[19] - 0.5 * e[16] * e2[20] - 0.5 * e[16] * e2[21] + 0.5 * e2[22] * e[16] + 1.5 * e2[25] * e[16] + 0.5 * e[16] * e2[24] + 0.5 * e[16] * e2[26]; a[31] = 0.5 * e[0] * e2[12] + 0.5 * e[0] * e2[15] + 0.5 * e[0] * e2[11] + 1.5 * e[0] * e2[9] + 0.5 * e[0] * e2[10] - 0.5 * e[0] * e2[16] - 0.5 * e[0] * e2[17] - 0.5 * e[0] * e2[14] - 0.5 * e[0] * e2[13] + e[12] * e[9] * e[3] + e[12] * e[10] * e[4] + e[12] * e[1] * e[13] + e[12] * e[11] * e[5] + e[12] * e[2] * e[14] + e[15] * e[9] * e[6] + e[15] * e[10] * e[7] + e[15] * e[1] * e[16] + e[15] * e[11] * e[8] + e[15] * e[2] * e[17] + e[6] * e[11] * e[17] + e[6] * e[10] * e[16] + e[3] * e[11] * e[14] + e[3] * e[10] * e[13] + e[9] * e[10] * e[1] + e[9] * e[11] * e[2] - e[9] * e[13] * e[4] - e[9] * e[16] * e[7] - e[9] * e[14] * e[5] - e[9] * e[17] * e[8]; a[148] = e[19] * e[11] * e[35] + e[28] * e[18] * e[15] + e[28] * e[9] * e[24] + e[28] * e[20] * e[17] + e[28] * e[11] * e[26] - e[25] * e[27] * e[9] - e[25] * e[30] * e[12] - e[25] * e[32] * e[14] + e[25] * e[33] * e[15] + e[25] * e[35] * e[17] - e[25] * e[29] * e[11] - e[34] * e[23] * e[14] + e[34] * e[24] * e[15] + e[34] * e[26] * e[17] - e[34] * e[20] * e[11] - e[34] * e[18] * e[9] - e[34] * e[21] * e[12] + e[13] * e[30] * e[24] + e[13] * e[21] * e[33] + e[13] * e[31] * e[25] + e[13] * e[22] * e[34] + e[13] * e[32] * e[26] + e[13] * e[23] * e[35] + e[10] * e[27] * e[24] + e[10] * e[18] * e[33] + e[10] * e[28] * e[25] + e[10] * e[19] * e[34] + e[10] * e[29] * e[26] + e[10] * e[20] * e[35] + e[22] * e[30] * e[15] + e[22] * e[12] * e[33] + e[22] * e[32] * e[17] + e[22] * e[14] * e[35] + e[22] * e[31] * e[16] + e[31] * e[21] * e[15] + e[31] * e[12] * e[24] + e[31] * e[23] * e[17] + e[31] * e[14] * e[26] - e[16] * e[27] * e[18] + e[16] * e[33] * e[24] - e[16] * e[30] * e[21] - e[16] * e[29] * e[20] + e[16] * e[35] * e[26] - e[16] * e[32] * e[23] + e[16] * e[28] * e[19] + 3. * e[16] * e[34] * e[25] + e[19] * e[27] * e[15] + e[19] * e[9] * e[33] + e[19] * e[29] * e[17]; a[52] = e[4] * e[27] * e[3] + e[4] * e[0] * e[30] + e[4] * e[29] * e[5] + e[4] * e[2] * e[32] + e[31] * e[0] * e[3] + e[31] * e[2] * e[5] + e[7] * e[27] * e[6] + e[7] * e[0] * e[33] + e[7] * e[29] * e[8] + e[7] * e[2] * e[35] + e[34] * e[0] * e[6] + e[34] * e[2] * e[8] + e[1] * e[27] * e[0] + e[1] * e[29] * e[2] + e[1] * e[34] * e[7] - e[1] * e[32] * e[5] - e[1] * e[33] * e[6] - e[1] * e[30] * e[3] - e[1] * e[35] * e[8] + e[1] * e[31] * e[4] + 1.5 * e[28] * e2[1] + 0.5 * e[28] * e2[4] + 0.5 * e[28] * e2[0] - 0.5 * e[28] * e2[6] - 0.5 * e[28] * e2[5] + 0.5 * e[28] * e2[7] - 0.5 * e[28] * e2[3] + 0.5 * e[28] * e2[2] - 0.5 * e[28] * e2[8]; a[99] = -e[35] * e[10] * e[1] - e[35] * e[13] * e[4] + e[35] * e[16] * e[7] + e[35] * e[15] * e[6] - e[35] * e[9] * e[0] - e[35] * e[12] * e[3] + e[32] * e[12] * e[6] + e[32] * e[3] * e[15] + e[32] * e[13] * e[7] + e[32] * e[4] * e[16] - e[8] * e[27] * e[9] - e[8] * e[30] * e[12] - e[8] * e[28] * e[10] - e[8] * e[31] * e[13] + e[8] * e[29] * e[11] + e[11] * e[27] * e[6] + e[11] * e[0] * e[33] + e[11] * e[28] * e[7] + e[11] * e[1] * e[34] + e[29] * e[9] * e[6] + e[29] * e[0] * e[15] + e[29] * e[10] * e[7] + e[29] * e[1] * e[16] + e[5] * e[30] * e[15] + e[5] * e[12] * e[33] + e[5] * e[32] * e[17] + e[5] * e[14] * e[35] + e[5] * e[31] * e[16] + e[5] * e[13] * e[34] + e[8] * e[33] * e[15] + 3. * e[8] * e[35] * e[17] + e[8] * e[34] * e[16] + e[2] * e[27] * e[15] + e[2] * e[9] * e[33] + e[2] * e[29] * e[17] + e[2] * e[11] * e[35] + e[2] * e[28] * e[16] + e[2] * e[10] * e[34] - e[17] * e[27] * e[0] + e[17] * e[34] * e[7] + e[17] * e[33] * e[6] - e[17] * e[30] * e[3] - e[17] * e[28] * e[1] - e[17] * e[31] * e[4] + e[14] * e[30] * e[6] + e[14] * e[3] * e[33] + e[14] * e[31] * e[7] + e[14] * e[4] * e[34] + e[14] * e[32] * e[8]; a[82] = e[19] * e[11] * e[2] + e[4] * e[18] * e[12] + e[4] * e[9] * e[21] + e[4] * e[20] * e[14] + e[4] * e[11] * e[23] + e[4] * e[19] * e[13] + e[4] * e[10] * e[22] + e[7] * e[18] * e[15] + e[7] * e[9] * e[24] + e[7] * e[20] * e[17] + e[7] * e[11] * e[26] + e[7] * e[19] * e[16] + e[7] * e[10] * e[25] + e[1] * e[18] * e[9] + e[1] * e[20] * e[11] - e[10] * e[21] * e[3] - e[10] * e[26] * e[8] - e[10] * e[23] * e[5] - e[10] * e[24] * e[6] + e[13] * e[18] * e[3] + e[13] * e[0] * e[21] + e[13] * e[1] * e[22] + e[13] * e[20] * e[5] + e[13] * e[2] * e[23] - e[19] * e[15] * e[6] - e[19] * e[14] * e[5] - e[19] * e[12] * e[3] - e[19] * e[17] * e[8] + e[22] * e[9] * e[3] + e[22] * e[0] * e[12] + e[22] * e[11] * e[5] + e[22] * e[2] * e[14] + e[16] * e[18] * e[6] + e[16] * e[0] * e[24] + e[16] * e[1] * e[25] + e[16] * e[20] * e[8] + e[16] * e[2] * e[26] - e[1] * e[23] * e[14] - e[1] * e[24] * e[15] - e[1] * e[26] * e[17] - e[1] * e[21] * e[12] + e[25] * e[9] * e[6] + e[25] * e[0] * e[15] + e[25] * e[11] * e[8] + e[25] * e[2] * e[17] + e[10] * e[18] * e[0] + 3. * e[10] * e[19] * e[1] + e[10] * e[20] * e[2] + e[19] * e[9] * e[0]; a[169] = 0.5 * e2[23] * e[26] + 0.5 * e[26] * e2[25] + 0.5 * e2[20] * e[26] - 0.5 * e[26] * e2[18] + 0.5 * e3[26] + 0.5 * e[26] * e2[24] + e[20] * e[19] * e[25] - 0.5 * e[26] * e2[19] - 0.5 * e[26] * e2[21] + e[20] * e[18] * e[24] - 0.5 * e[26] * e2[22] + e[23] * e[21] * e[24] + e[23] * e[22] * e[25]; a[72] = e[16] * e[9] * e[33] + e[16] * e[29] * e[17] + e[16] * e[11] * e[35] + e[16] * e[10] * e[34] + e[34] * e[11] * e[17] + e[34] * e[9] * e[15] - e[10] * e[30] * e[12] - e[10] * e[32] * e[14] - e[10] * e[33] * e[15] - e[10] * e[35] * e[17] + e[10] * e[27] * e[9] + e[10] * e[29] * e[11] + e[13] * e[27] * e[12] + e[13] * e[9] * e[30] + e[13] * e[29] * e[14] + e[13] * e[11] * e[32] + e[13] * e[10] * e[31] + e[31] * e[11] * e[14] + e[31] * e[9] * e[12] + e[16] * e[27] * e[15] + 1.5 * e[28] * e2[10] + 0.5 * e[28] * e2[16] + 0.5 * e[28] * e2[9] + 0.5 * e[28] * e2[11] - 0.5 * e[28] * e2[12] - 0.5 * e[28] * e2[15] - 0.5 * e[28] * e2[17] - 0.5 * e[28] * e2[14] + 0.5 * e[28] * e2[13]; a[179] = 0.5 * e2[20] * e[35] + 0.5 * e2[23] * e[35] + 1.5 * e[35] * e2[26] + 0.5 * e[35] * e2[25] + 0.5 * e[35] * e2[24] - 0.5 * e[35] * e2[18] - 0.5 * e[35] * e2[19] - 0.5 * e[35] * e2[22] - 0.5 * e[35] * e2[21] + e[20] * e[27] * e[24] + e[20] * e[18] * e[33] + e[20] * e[28] * e[25] + e[20] * e[19] * e[34] + e[20] * e[29] * e[26] + e[29] * e[19] * e[25] + e[29] * e[18] * e[24] + e[23] * e[30] * e[24] + e[23] * e[21] * e[33] + e[23] * e[31] * e[25] + e[23] * e[22] * e[34] + e[23] * e[32] * e[26] + e[32] * e[22] * e[25] + e[32] * e[21] * e[24] + e[26] * e[33] * e[24] + e[26] * e[34] * e[25] - e[26] * e[27] * e[18] - e[26] * e[30] * e[21] - e[26] * e[31] * e[22] - e[26] * e[28] * e[19]; a[2] = e[4] * e[2] * e[5] + 0.5 * e[1] * e2[0] - 0.5 * e[1] * e2[6] + e[7] * e[0] * e[6] + 0.5 * e[1] * e2[7] + 0.5 * e[1] * e2[4] - 0.5 * e[1] * e2[8] + 0.5 * e[1] * e2[2] - 0.5 * e[1] * e2[3] + 0.5 * e3[1] + e[7] * e[2] * e[8] - 0.5 * e[1] * e2[5] + e[4] * e[0] * e[3]; a[19] = -0.5 * e[17] * e2[13] - 0.5 * e[17] * e2[9] + 0.5 * e[17] * e2[16] + 0.5 * e[17] * e2[15] + 0.5 * e3[17] - 0.5 * e[17] * e2[10] + e[14] * e[13] * e[16] + e[14] * e[12] * e[15] + 0.5 * e2[14] * e[17] + e[11] * e[10] * e[16] - 0.5 * e[17] * e2[12] + 0.5 * e2[11] * e[17] + e[11] * e[9] * e[15]; a[122] = e[4] * e[27] * e[30] + e[4] * e[29] * e[32] + e[4] * e[28] * e[31] + e[31] * e[27] * e[3] + e[31] * e[0] * e[30] + e[31] * e[29] * e[5] + e[31] * e[2] * e[32] + e[7] * e[27] * e[33] + e[7] * e[29] * e[35] + e[7] * e[28] * e[34] + e[28] * e[27] * e[0] + e[28] * e[29] * e[2] + e[34] * e[27] * e[6] + e[34] * e[0] * e[33] + e[34] * e[29] * e[8] + e[34] * e[2] * e[35] - e[28] * e[32] * e[5] - e[28] * e[33] * e[6] - e[28] * e[30] * e[3] - e[28] * e[35] * e[8] + 0.5 * e[1] * e2[27] + 0.5 * e[1] * e2[29] + 1.5 * e[1] * e2[28] + 0.5 * e[1] * e2[31] - 0.5 * e[1] * e2[32] - 0.5 * e[1] * e2[33] - 0.5 * e[1] * e2[30] + 0.5 * e[1] * e2[34] - 0.5 * e[1] * e2[35]; a[79] = 0.5 * e2[11] * e[35] + 0.5 * e[35] * e2[16] - 0.5 * e[35] * e2[9] - 0.5 * e[35] * e2[12] + 0.5 * e[35] * e2[15] + 1.5 * e[35] * e2[17] - 0.5 * e[35] * e2[10] + 0.5 * e[35] * e2[14] - 0.5 * e[35] * e2[13] + e[11] * e[27] * e[15] + e[11] * e[9] * e[33] + e[11] * e[29] * e[17] + e[11] * e[28] * e[16] + e[11] * e[10] * e[34] + e[29] * e[9] * e[15] + e[29] * e[10] * e[16] + e[14] * e[30] * e[15] + e[14] * e[12] * e[33] + e[14] * e[32] * e[17] + e[14] * e[31] * e[16] + e[14] * e[13] * e[34] + e[32] * e[12] * e[15] + e[32] * e[13] * e[16] + e[17] * e[33] * e[15] + e[17] * e[34] * e[16] - e[17] * e[27] * e[9] - e[17] * e[30] * e[12] - e[17] * e[28] * e[10] - e[17] * e[31] * e[13]; a[192] = e[34] * e[27] * e[33] + e[34] * e[29] * e[35] - 0.5 * e[28] * e2[30] - 0.5 * e[28] * e2[35] + 0.5 * e3[28] + 0.5 * e[28] * e2[27] + 0.5 * e[28] * e2[29] + e[31] * e[27] * e[30] + e[31] * e[29] * e[32] - 0.5 * e[28] * e2[32] - 0.5 * e[28] * e2[33] + 0.5 * e[28] * e2[31] + 0.5 * e[28] * e2[34]; a[9] = 0.5 * e2[5] * e[8] + e[2] * e[0] * e[6] + 0.5 * e2[2] * e[8] + 0.5 * e3[8] - 0.5 * e[8] * e2[0] + e[5] * e[4] * e[7] + e[5] * e[3] * e[6] + 0.5 * e[8] * e2[7] + e[2] * e[1] * e[7] - 0.5 * e[8] * e2[1] - 0.5 * e[8] * e2[4] - 0.5 * e[8] * e2[3] + 0.5 * e[8] * e2[6]; a[152] = e[28] * e[27] * e[9] + e[28] * e[29] * e[11] - e[28] * e[30] * e[12] + e[28] * e[31] * e[13] - e[28] * e[32] * e[14] - e[28] * e[33] * e[15] - e[28] * e[35] * e[17] + e[31] * e[27] * e[12] + e[31] * e[9] * e[30] + e[31] * e[29] * e[14] + e[31] * e[11] * e[32] + e[13] * e[27] * e[30] + e[13] * e[29] * e[32] + e[16] * e[27] * e[33] + e[16] * e[29] * e[35] + e[34] * e[27] * e[15] + e[34] * e[9] * e[33] + e[34] * e[29] * e[17] + e[34] * e[11] * e[35] + e[34] * e[28] * e[16] + 0.5 * e[10] * e2[27] + 0.5 * e[10] * e2[29] + 1.5 * e[10] * e2[28] - 0.5 * e[10] * e2[32] + 0.5 * e[10] * e2[31] - 0.5 * e[10] * e2[33] - 0.5 * e[10] * e2[30] + 0.5 * e[10] * e2[34] - 0.5 * e[10] * e2[35]; a[59] = -0.5 * e[35] * e2[1] + 0.5 * e[35] * e2[7] - 0.5 * e[35] * e2[3] + 0.5 * e2[2] * e[35] + 1.5 * e[35] * e2[8] - 0.5 * e[35] * e2[4] - 0.5 * e[35] * e2[0] + 0.5 * e[35] * e2[6] + 0.5 * e[35] * e2[5] + e[2] * e[27] * e[6] + e[2] * e[0] * e[33] + e[2] * e[28] * e[7] + e[2] * e[1] * e[34] + e[2] * e[29] * e[8] - e[8] * e[27] * e[0] + e[8] * e[34] * e[7] + e[8] * e[32] * e[5] + e[8] * e[33] * e[6] - e[8] * e[30] * e[3] - e[8] * e[28] * e[1] - e[8] * e[31] * e[4] + e[29] * e[1] * e[7] + e[29] * e[0] * e[6] + e[5] * e[30] * e[6] + e[5] * e[3] * e[33] + e[5] * e[31] * e[7] + e[5] * e[4] * e[34] + e[32] * e[4] * e[7] + e[32] * e[3] * e[6]; a[182] = e[28] * e[27] * e[18] + e[28] * e[29] * e[20] + e[22] * e[27] * e[30] + e[22] * e[29] * e[32] + e[22] * e[28] * e[31] + e[31] * e[27] * e[21] + e[31] * e[18] * e[30] + e[31] * e[29] * e[23] + e[31] * e[20] * e[32] + e[25] * e[27] * e[33] + e[25] * e[29] * e[35] + e[25] * e[28] * e[34] + e[34] * e[27] * e[24] + e[34] * e[18] * e[33] + e[34] * e[29] * e[26] + e[34] * e[20] * e[35] - e[28] * e[33] * e[24] - e[28] * e[30] * e[21] - e[28] * e[35] * e[26] - e[28] * e[32] * e[23] - 0.5 * e[19] * e2[33] - 0.5 * e[19] * e2[30] - 0.5 * e[19] * e2[35] + 0.5 * e[19] * e2[27] + 0.5 * e[19] * e2[29] + 1.5 * e[19] * e2[28] + 0.5 * e[19] * e2[31] + 0.5 * e[19] * e2[34] - 0.5 * e[19] * e2[32]; a[89] = e[23] * e[3] * e[15] - e[17] * e[19] * e[1] - e[17] * e[22] * e[4] - e[17] * e[18] * e[0] + e[17] * e[25] * e[7] + e[17] * e[24] * e[6] + e[14] * e[21] * e[6] + e[14] * e[3] * e[24] + e[14] * e[22] * e[7] + e[14] * e[4] * e[25] + e[14] * e[23] * e[8] - e[26] * e[10] * e[1] - e[26] * e[13] * e[4] + e[26] * e[16] * e[7] + e[26] * e[15] * e[6] - e[26] * e[9] * e[0] - e[26] * e[12] * e[3] + e[23] * e[12] * e[6] + e[11] * e[18] * e[6] + e[11] * e[0] * e[24] + e[11] * e[19] * e[7] + e[11] * e[1] * e[25] + e[11] * e[20] * e[8] + e[11] * e[2] * e[26] + e[20] * e[9] * e[6] + e[20] * e[0] * e[15] + e[20] * e[10] * e[7] + e[20] * e[1] * e[16] + e[20] * e[2] * e[17] + e[5] * e[21] * e[15] + e[5] * e[12] * e[24] + e[5] * e[23] * e[17] + e[5] * e[14] * e[26] + e[5] * e[22] * e[16] + e[5] * e[13] * e[25] + e[8] * e[24] * e[15] + 3. * e[8] * e[26] * e[17] + e[8] * e[25] * e[16] + e[2] * e[18] * e[15] + e[2] * e[9] * e[24] + e[2] * e[19] * e[16] + e[2] * e[10] * e[25] - e[17] * e[21] * e[3] + e[23] * e[4] * e[16] + e[23] * e[13] * e[7] - e[8] * e[18] * e[9] - e[8] * e[21] * e[12] - e[8] * e[19] * e[10] - e[8] * e[22] * e[13]; a[62] = e[13] * e[18] * e[12] + e[13] * e[9] * e[21] + e[13] * e[20] * e[14] + e[13] * e[11] * e[23] + e[13] * e[10] * e[22] + e[22] * e[11] * e[14] + e[22] * e[9] * e[12] + e[16] * e[18] * e[15] + e[16] * e[9] * e[24] + e[16] * e[20] * e[17] + e[16] * e[11] * e[26] + e[16] * e[10] * e[25] + e[25] * e[11] * e[17] + e[25] * e[9] * e[15] - e[10] * e[23] * e[14] - e[10] * e[24] * e[15] - e[10] * e[26] * e[17] + e[10] * e[20] * e[11] + e[10] * e[18] * e[9] - e[10] * e[21] * e[12] + 0.5 * e[19] * e2[11] + 0.5 * e[19] * e2[9] + 1.5 * e[19] * e2[10] + 0.5 * e[19] * e2[13] + 0.5 * e[19] * e2[16] - 0.5 * e[19] * e2[12] - 0.5 * e[19] * e2[15] - 0.5 * e[19] * e2[17] - 0.5 * e[19] * e2[14]; a[88] = e[10] * e[18] * e[6] + e[10] * e[0] * e[24] + e[10] * e[19] * e[7] + e[10] * e[1] * e[25] + e[10] * e[20] * e[8] + e[10] * e[2] * e[26] + e[19] * e[9] * e[6] + e[19] * e[0] * e[15] + e[19] * e[1] * e[16] + e[19] * e[11] * e[8] + e[19] * e[2] * e[17] + e[4] * e[21] * e[15] + e[4] * e[12] * e[24] + e[4] * e[23] * e[17] + e[4] * e[14] * e[26] + e[4] * e[22] * e[16] + e[4] * e[13] * e[25] + e[7] * e[24] * e[15] + e[7] * e[26] * e[17] + 3. * e[7] * e[25] * e[16] + e[1] * e[18] * e[15] + e[1] * e[9] * e[24] + e[1] * e[20] * e[17] + e[1] * e[11] * e[26] - e[16] * e[21] * e[3] + e[16] * e[26] * e[8] - e[16] * e[20] * e[2] - e[16] * e[18] * e[0] - e[16] * e[23] * e[5] + e[16] * e[24] * e[6] + e[13] * e[21] * e[6] + e[13] * e[3] * e[24] + e[13] * e[22] * e[7] + e[13] * e[23] * e[8] + e[13] * e[5] * e[26] - e[25] * e[11] * e[2] + e[25] * e[15] * e[6] - e[25] * e[9] * e[0] - e[25] * e[14] * e[5] - e[25] * e[12] * e[3] + e[25] * e[17] * e[8] + e[22] * e[12] * e[6] + e[22] * e[3] * e[15] + e[22] * e[14] * e[8] + e[22] * e[5] * e[17] - e[7] * e[23] * e[14] - e[7] * e[20] * e[11] - e[7] * e[18] * e[9] - e[7] * e[21] * e[12]; a[32] = e[13] * e[9] * e[3] + e[13] * e[0] * e[12] + e[13] * e[10] * e[4] + e[13] * e[11] * e[5] + e[13] * e[2] * e[14] + e[16] * e[9] * e[6] + e[16] * e[0] * e[15] + e[16] * e[10] * e[7] + e[16] * e[11] * e[8] + e[16] * e[2] * e[17] + e[7] * e[11] * e[17] + e[7] * e[9] * e[15] + e[4] * e[11] * e[14] + e[4] * e[9] * e[12] + e[10] * e[9] * e[0] + e[10] * e[11] * e[2] - e[10] * e[15] * e[6] - e[10] * e[14] * e[5] - e[10] * e[12] * e[3] - e[10] * e[17] * e[8] + 0.5 * e[1] * e2[11] + 0.5 * e[1] * e2[9] + 1.5 * e[1] * e2[10] - 0.5 * e[1] * e2[12] - 0.5 * e[1] * e2[15] - 0.5 * e[1] * e2[17] - 0.5 * e[1] * e2[14] + 0.5 * e[1] * e2[13] + 0.5 * e[1] * e2[16]; a[58] = e[1] * e[27] * e[6] + e[1] * e[0] * e[33] + e[1] * e[28] * e[7] + e[1] * e[29] * e[8] + e[1] * e[2] * e[35] - e[7] * e[27] * e[0] - e[7] * e[32] * e[5] + e[7] * e[33] * e[6] - e[7] * e[30] * e[3] + e[7] * e[35] * e[8] - e[7] * e[29] * e[2] + e[7] * e[31] * e[4] + e[28] * e[0] * e[6] + e[28] * e[2] * e[8] + e[4] * e[30] * e[6] + e[4] * e[3] * e[33] + e[4] * e[32] * e[8] + e[4] * e[5] * e[35] + e[31] * e[3] * e[6] + e[31] * e[5] * e[8] + 0.5 * e2[1] * e[34] + 1.5 * e[34] * e2[7] + 0.5 * e[34] * e2[4] - 0.5 * e[34] * e2[0] + 0.5 * e[34] * e2[6] - 0.5 * e[34] * e2[5] - 0.5 * e[34] * e2[3] - 0.5 * e[34] * e2[2] + 0.5 * e[34] * e2[8]; a[42] = e[4] * e[18] * e[3] + e[4] * e[0] * e[21] + e[4] * e[1] * e[22] + e[4] * e[20] * e[5] + e[4] * e[2] * e[23] + e[22] * e[0] * e[3] + e[22] * e[2] * e[5] + e[7] * e[18] * e[6] + e[7] * e[0] * e[24] + e[7] * e[1] * e[25] + e[7] * e[20] * e[8] + e[7] * e[2] * e[26] + e[25] * e[0] * e[6] + e[25] * e[2] * e[8] + e[1] * e[18] * e[0] + e[1] * e[20] * e[2] - e[1] * e[21] * e[3] - e[1] * e[26] * e[8] - e[1] * e[23] * e[5] - e[1] * e[24] * e[6] + 0.5 * e[19] * e2[4] + 0.5 * e[19] * e2[0] - 0.5 * e[19] * e2[6] - 0.5 * e[19] * e2[5] + 1.5 * e[19] * e2[1] + 0.5 * e[19] * e2[7] - 0.5 * e[19] * e2[3] + 0.5 * e[19] * e2[2] - 0.5 * e[19] * e2[8]; a[8] = -0.5 * e[7] * e2[0] + e[4] * e[5] * e[8] + 0.5 * e2[4] * e[7] - 0.5 * e[7] * e2[2] + 0.5 * e[7] * e2[8] - 0.5 * e[7] * e2[5] + 0.5 * e[7] * e2[6] + e[1] * e[0] * e[6] + 0.5 * e3[7] + e[4] * e[3] * e[6] + e[1] * e[2] * e[8] - 0.5 * e[7] * e2[3] + 0.5 * e2[1] * e[7]; a[112] = -e[1] * e[32] * e[23] - e[19] * e[32] * e[5] - e[19] * e[33] * e[6] - e[19] * e[30] * e[3] - e[19] * e[35] * e[8] - e[28] * e[21] * e[3] - e[28] * e[26] * e[8] - e[28] * e[23] * e[5] - e[28] * e[24] * e[6] + e[7] * e[27] * e[24] + e[7] * e[18] * e[33] + e[7] * e[29] * e[26] + e[7] * e[20] * e[35] + e[22] * e[27] * e[3] + e[22] * e[0] * e[30] + e[22] * e[29] * e[5] + e[22] * e[2] * e[32] + e[31] * e[18] * e[3] + e[31] * e[0] * e[21] + e[31] * e[20] * e[5] + e[31] * e[2] * e[23] + e[25] * e[27] * e[6] + e[25] * e[0] * e[33] + e[25] * e[28] * e[7] + e[25] * e[1] * e[34] + e[25] * e[29] * e[8] + e[25] * e[2] * e[35] + e[34] * e[18] * e[6] + e[34] * e[0] * e[24] + e[34] * e[19] * e[7] + e[34] * e[20] * e[8] + e[34] * e[2] * e[26] + e[1] * e[27] * e[18] + 3. * e[1] * e[28] * e[19] + e[1] * e[29] * e[20] + e[19] * e[27] * e[0] + e[19] * e[29] * e[2] + e[28] * e[18] * e[0] + e[28] * e[20] * e[2] + e[4] * e[27] * e[21] + e[4] * e[18] * e[30] + e[4] * e[28] * e[22] + e[4] * e[19] * e[31] + e[4] * e[29] * e[23] + e[4] * e[20] * e[32] - e[1] * e[33] * e[24] - e[1] * e[30] * e[21] - e[1] * e[35] * e[26] + e[1] * e[31] * e[22]; a[78] = e[10] * e[27] * e[15] + e[10] * e[9] * e[33] + e[10] * e[29] * e[17] + e[10] * e[11] * e[35] + e[10] * e[28] * e[16] + e[28] * e[11] * e[17] + e[28] * e[9] * e[15] + e[13] * e[30] * e[15] + e[13] * e[12] * e[33] + e[13] * e[32] * e[17] + e[13] * e[14] * e[35] + e[13] * e[31] * e[16] + e[31] * e[14] * e[17] + e[31] * e[12] * e[15] + e[16] * e[33] * e[15] + e[16] * e[35] * e[17] - e[16] * e[27] * e[9] - e[16] * e[30] * e[12] - e[16] * e[32] * e[14] - e[16] * e[29] * e[11] + 0.5 * e2[10] * e[34] + 1.5 * e[34] * e2[16] - 0.5 * e[34] * e2[9] - 0.5 * e[34] * e2[11] - 0.5 * e[34] * e2[12] + 0.5 * e[34] * e2[15] + 0.5 * e[34] * e2[17] - 0.5 * e[34] * e2[14] + 0.5 * e[34] * e2[13]; a[162] = 0.5 * e[19] * e2[18] + 0.5 * e[19] * e2[25] + 0.5 * e[19] * e2[22] + e[25] * e[20] * e[26] - 0.5 * e[19] * e2[21] + 0.5 * e[19] * e2[20] - 0.5 * e[19] * e2[26] - 0.5 * e[19] * e2[23] - 0.5 * e[19] * e2[24] + 0.5 * e3[19] + e[22] * e[20] * e[23] + e[25] * e[18] * e[24] + e[22] * e[18] * e[21]; a[198] = 0.5 * e[34] * e2[33] + 0.5 * e[34] * e2[35] - 0.5 * e[34] * e2[27] - 0.5 * e[34] * e2[32] - 0.5 * e[34] * e2[29] - 0.5 * e[34] * e2[30] + 0.5 * e2[28] * e[34] + e[31] * e[30] * e[33] + e[31] * e[32] * e[35] + e[28] * e[27] * e[33] + 0.5 * e3[34] + e[28] * e[29] * e[35] + 0.5 * e2[31] * e[34]; a[92] = e[4] * e[28] * e[13] + e[4] * e[10] * e[31] + e[7] * e[27] * e[15] + e[7] * e[9] * e[33] + e[7] * e[29] * e[17] + e[7] * e[11] * e[35] + e[7] * e[28] * e[16] + e[7] * e[10] * e[34] + e[1] * e[27] * e[9] + e[1] * e[29] * e[11] + 3. * e[1] * e[28] * e[10] + e[10] * e[27] * e[0] - e[10] * e[32] * e[5] - e[10] * e[33] * e[6] - e[10] * e[30] * e[3] - e[10] * e[35] * e[8] + e[10] * e[29] * e[2] + e[13] * e[27] * e[3] + e[13] * e[0] * e[30] + e[13] * e[1] * e[31] + e[13] * e[29] * e[5] + e[13] * e[2] * e[32] + e[28] * e[11] * e[2] - e[28] * e[15] * e[6] + e[28] * e[9] * e[0] - e[28] * e[14] * e[5] - e[28] * e[12] * e[3] - e[28] * e[17] * e[8] + e[31] * e[9] * e[3] + e[31] * e[0] * e[12] + e[31] * e[11] * e[5] + e[31] * e[2] * e[14] + e[16] * e[27] * e[6] + e[16] * e[0] * e[33] + e[16] * e[1] * e[34] + e[16] * e[29] * e[8] + e[16] * e[2] * e[35] - e[1] * e[30] * e[12] - e[1] * e[32] * e[14] - e[1] * e[33] * e[15] - e[1] * e[35] * e[17] + e[34] * e[9] * e[6] + e[34] * e[0] * e[15] + e[34] * e[11] * e[8] + e[34] * e[2] * e[17] + e[4] * e[27] * e[12] + e[4] * e[9] * e[30] + e[4] * e[29] * e[14] + e[4] * e[11] * e[32]; a[128] = e[4] * e[30] * e[33] + e[4] * e[32] * e[35] + e[4] * e[31] * e[34] + e[31] * e[30] * e[6] + e[31] * e[3] * e[33] + e[31] * e[32] * e[8] + e[31] * e[5] * e[35] + e[28] * e[27] * e[6] + e[28] * e[0] * e[33] + e[28] * e[29] * e[8] + e[28] * e[2] * e[35] + e[34] * e[33] * e[6] + e[34] * e[35] * e[8] - e[34] * e[27] * e[0] - e[34] * e[32] * e[5] - e[34] * e[30] * e[3] - e[34] * e[29] * e[2] + e[1] * e[27] * e[33] + e[1] * e[29] * e[35] + e[1] * e[28] * e[34] + 0.5 * e2[31] * e[7] - 0.5 * e[7] * e2[27] - 0.5 * e[7] * e2[32] + 0.5 * e[7] * e2[28] - 0.5 * e[7] * e2[29] + 0.5 * e[7] * e2[33] - 0.5 * e[7] * e2[30] + 1.5 * e[7] * e2[34] + 0.5 * e[7] * e2[35]; a[12] = -0.5 * e[10] * e2[14] - 0.5 * e[10] * e2[17] - 0.5 * e[10] * e2[15] + e[13] * e[11] * e[14] + e[16] * e[11] * e[17] + 0.5 * e[10] * e2[13] + e[13] * e[9] * e[12] - 0.5 * e[10] * e2[12] + 0.5 * e3[10] + e[16] * e[9] * e[15] + 0.5 * e[10] * e2[16] + 0.5 * e[10] * e2[11] + 0.5 * e[10] * e2[9]; a[188] = e[22] * e[32] * e[35] + e[22] * e[31] * e[34] + e[31] * e[30] * e[24] + e[31] * e[21] * e[33] + e[31] * e[32] * e[26] + e[31] * e[23] * e[35] + e[34] * e[33] * e[24] + e[34] * e[35] * e[26] - e[34] * e[27] * e[18] - e[34] * e[30] * e[21] - e[34] * e[29] * e[20] - e[34] * e[32] * e[23] + e[19] * e[27] * e[33] + e[19] * e[29] * e[35] + e[19] * e[28] * e[34] + e[28] * e[27] * e[24] + e[28] * e[18] * e[33] + e[28] * e[29] * e[26] + e[28] * e[20] * e[35] + e[22] * e[30] * e[33] + 0.5 * e2[28] * e[25] + 0.5 * e2[31] * e[25] + 0.5 * e[25] * e2[33] + 0.5 * e[25] * e2[35] + 1.5 * e[25] * e2[34] - 0.5 * e[25] * e2[27] - 0.5 * e[25] * e2[32] - 0.5 * e[25] * e2[29] - 0.5 * e[25] * e2[30]; a[172] = -e[19] * e[35] * e[26] - e[19] * e[32] * e[23] + e[19] * e[27] * e[18] + e[19] * e[29] * e[20] + e[22] * e[27] * e[21] + e[22] * e[18] * e[30] + e[22] * e[19] * e[31] + e[22] * e[29] * e[23] + e[22] * e[20] * e[32] + e[31] * e[18] * e[21] + e[31] * e[20] * e[23] + e[25] * e[27] * e[24] + e[25] * e[18] * e[33] + e[25] * e[19] * e[34] + e[25] * e[29] * e[26] + e[25] * e[20] * e[35] + e[34] * e[18] * e[24] + e[34] * e[20] * e[26] - e[19] * e[33] * e[24] - e[19] * e[30] * e[21] + 1.5 * e[28] * e2[19] + 0.5 * e[28] * e2[18] + 0.5 * e[28] * e2[20] + 0.5 * e[28] * e2[22] + 0.5 * e[28] * e2[25] - 0.5 * e[28] * e2[26] - 0.5 * e[28] * e2[23] - 0.5 * e[28] * e2[24] - 0.5 * e[28] * e2[21]; a[158] = e[10] * e[27] * e[33] + e[10] * e[29] * e[35] + e[10] * e[28] * e[34] + e[34] * e[33] * e[15] + e[34] * e[35] * e[17] + e[28] * e[27] * e[15] + e[28] * e[9] * e[33] + e[28] * e[29] * e[17] + e[28] * e[11] * e[35] - e[34] * e[27] * e[9] - e[34] * e[30] * e[12] + e[34] * e[31] * e[13] - e[34] * e[32] * e[14] - e[34] * e[29] * e[11] + e[31] * e[30] * e[15] + e[31] * e[12] * e[33] + e[31] * e[32] * e[17] + e[31] * e[14] * e[35] + e[13] * e[30] * e[33] + e[13] * e[32] * e[35] - 0.5 * e[16] * e2[27] - 0.5 * e[16] * e2[32] + 0.5 * e[16] * e2[28] - 0.5 * e[16] * e2[29] + 0.5 * e[16] * e2[31] + 0.5 * e[16] * e2[33] - 0.5 * e[16] * e2[30] + 1.5 * e[16] * e2[34] + 0.5 * e[16] * e2[35]; a[153] = e[29] * e[32] * e[14] - e[29] * e[33] * e[15] - e[29] * e[34] * e[16] + e[32] * e[27] * e[12] + e[32] * e[9] * e[30] + e[32] * e[28] * e[13] + e[32] * e[10] * e[31] + e[14] * e[27] * e[30] + e[14] * e[28] * e[31] + e[17] * e[27] * e[33] + e[17] * e[28] * e[34] + e[35] * e[27] * e[15] + e[35] * e[9] * e[33] + e[35] * e[29] * e[17] + e[35] * e[28] * e[16] + e[35] * e[10] * e[34] + e[29] * e[27] * e[9] + e[29] * e[28] * e[10] - e[29] * e[30] * e[12] - e[29] * e[31] * e[13] + 0.5 * e[11] * e2[27] + 1.5 * e[11] * e2[29] + 0.5 * e[11] * e2[28] + 0.5 * e[11] * e2[32] - 0.5 * e[11] * e2[31] - 0.5 * e[11] * e2[33] - 0.5 * e[11] * e2[30] - 0.5 * e[11] * e2[34] + 0.5 * e[11] * e2[35]; a[118] = e[1] * e[20] * e[35] + e[19] * e[27] * e[6] + e[19] * e[0] * e[33] + e[19] * e[28] * e[7] + e[19] * e[29] * e[8] + e[19] * e[2] * e[35] + e[28] * e[18] * e[6] + e[28] * e[0] * e[24] + e[28] * e[20] * e[8] + e[28] * e[2] * e[26] + e[4] * e[30] * e[24] + e[4] * e[21] * e[33] + e[4] * e[31] * e[25] + e[4] * e[22] * e[34] + e[4] * e[32] * e[26] + e[4] * e[23] * e[35] - e[7] * e[27] * e[18] + e[7] * e[33] * e[24] - e[7] * e[30] * e[21] - e[7] * e[29] * e[20] + e[7] * e[35] * e[26] + e[7] * e[31] * e[22] - e[7] * e[32] * e[23] - e[25] * e[27] * e[0] - e[25] * e[32] * e[5] - e[25] * e[30] * e[3] - e[25] * e[29] * e[2] - e[34] * e[21] * e[3] - e[34] * e[20] * e[2] - e[34] * e[18] * e[0] - e[34] * e[23] * e[5] + e[22] * e[30] * e[6] + e[22] * e[3] * e[33] + e[22] * e[32] * e[8] + e[22] * e[5] * e[35] + e[31] * e[21] * e[6] + e[31] * e[3] * e[24] + e[31] * e[23] * e[8] + e[31] * e[5] * e[26] + e[34] * e[26] * e[8] + e[1] * e[27] * e[24] + e[1] * e[18] * e[33] + e[1] * e[28] * e[25] + e[1] * e[19] * e[34] + e[1] * e[29] * e[26] + e[34] * e[24] * e[6] + e[25] * e[33] * e[6] + 3. * e[25] * e[34] * e[7] + e[25] * e[35] * e[8]; a[183] = 0.5 * e[20] * e2[27] + 1.5 * e[20] * e2[29] + 0.5 * e[20] * e2[28] + 0.5 * e[20] * e2[32] + 0.5 * e[20] * e2[35] - 0.5 * e[20] * e2[31] - 0.5 * e[20] * e2[33] - 0.5 * e[20] * e2[30] - 0.5 * e[20] * e2[34] + e[29] * e[27] * e[18] + e[29] * e[28] * e[19] + e[23] * e[27] * e[30] + e[23] * e[29] * e[32] + e[23] * e[28] * e[31] + e[32] * e[27] * e[21] + e[32] * e[18] * e[30] + e[32] * e[28] * e[22] + e[32] * e[19] * e[31] + e[26] * e[27] * e[33] + e[26] * e[29] * e[35] + e[26] * e[28] * e[34] + e[35] * e[27] * e[24] + e[35] * e[18] * e[33] + e[35] * e[28] * e[25] + e[35] * e[19] * e[34] - e[29] * e[33] * e[24] - e[29] * e[30] * e[21] - e[29] * e[31] * e[22] - e[29] * e[34] * e[25]; a[48] = e[19] * e[1] * e[7] + e[19] * e[0] * e[6] + e[19] * e[2] * e[8] + e[4] * e[21] * e[6] + e[4] * e[3] * e[24] + e[4] * e[22] * e[7] + e[4] * e[23] * e[8] + e[4] * e[5] * e[26] + e[22] * e[3] * e[6] + e[22] * e[5] * e[8] + e[7] * e[24] * e[6] + e[7] * e[26] * e[8] + e[1] * e[18] * e[6] + e[1] * e[0] * e[24] + e[1] * e[20] * e[8] + e[1] * e[2] * e[26] - e[7] * e[21] * e[3] - e[7] * e[20] * e[2] - e[7] * e[18] * e[0] - e[7] * e[23] * e[5] + 0.5 * e[25] * e2[4] - 0.5 * e[25] * e2[0] + 0.5 * e[25] * e2[6] - 0.5 * e[25] * e2[5] + 0.5 * e[25] * e2[1] + 1.5 * e[25] * e2[7] - 0.5 * e[25] * e2[3] - 0.5 * e[25] * e2[2] + 0.5 * e[25] * e2[8]; a[123] = e[5] * e[27] * e[30] + e[5] * e[29] * e[32] + e[5] * e[28] * e[31] + e[32] * e[27] * e[3] + e[32] * e[0] * e[30] + e[32] * e[28] * e[4] + e[32] * e[1] * e[31] + e[8] * e[27] * e[33] + e[8] * e[29] * e[35] + e[8] * e[28] * e[34] + e[29] * e[27] * e[0] + e[29] * e[28] * e[1] + e[35] * e[27] * e[6] + e[35] * e[0] * e[33] + e[35] * e[28] * e[7] + e[35] * e[1] * e[34] - e[29] * e[34] * e[7] - e[29] * e[33] * e[6] - e[29] * e[30] * e[3] - e[29] * e[31] * e[4] + 0.5 * e[2] * e2[27] + 1.5 * e[2] * e2[29] + 0.5 * e[2] * e2[28] + 0.5 * e[2] * e2[32] - 0.5 * e[2] * e2[31] - 0.5 * e[2] * e2[33] - 0.5 * e[2] * e2[30] - 0.5 * e[2] * e2[34] + 0.5 * e[2] * e2[35]; a[38] = e[13] * e[12] * e[6] + e[13] * e[3] * e[15] + e[13] * e[4] * e[16] + e[13] * e[14] * e[8] + e[13] * e[5] * e[17] + e[16] * e[15] * e[6] + e[16] * e[17] * e[8] + e[1] * e[11] * e[17] + e[1] * e[9] * e[15] + e[1] * e[10] * e[16] + e[4] * e[14] * e[17] + e[4] * e[12] * e[15] + e[10] * e[9] * e[6] + e[10] * e[0] * e[15] + e[10] * e[11] * e[8] + e[10] * e[2] * e[17] - e[16] * e[11] * e[2] - e[16] * e[9] * e[0] - e[16] * e[14] * e[5] - e[16] * e[12] * e[3] + 0.5 * e2[13] * e[7] + 1.5 * e2[16] * e[7] + 0.5 * e[7] * e2[17] + 0.5 * e[7] * e2[15] - 0.5 * e[7] * e2[9] - 0.5 * e[7] * e2[11] - 0.5 * e[7] * e2[12] + 0.5 * e[7] * e2[10] - 0.5 * e[7] * e2[14]; a[193] = 0.5 * e[29] * e2[32] + 0.5 * e[29] * e2[35] - 0.5 * e[29] * e2[31] - 0.5 * e[29] * e2[33] - 0.5 * e[29] * e2[30] - 0.5 * e[29] * e2[34] + e[32] * e[27] * e[30] + 0.5 * e3[29] + 0.5 * e[29] * e2[28] + e[35] * e[28] * e[34] + 0.5 * e[29] * e2[27] + e[35] * e[27] * e[33] + e[32] * e[28] * e[31]; a[68] = -e[16] * e[21] * e[12] + e[10] * e[18] * e[15] + e[10] * e[9] * e[24] + e[10] * e[20] * e[17] + e[10] * e[11] * e[26] + e[19] * e[11] * e[17] + e[19] * e[9] * e[15] + e[19] * e[10] * e[16] + e[13] * e[21] * e[15] + e[13] * e[12] * e[24] + e[13] * e[23] * e[17] + e[13] * e[14] * e[26] + e[13] * e[22] * e[16] + e[22] * e[14] * e[17] + e[22] * e[12] * e[15] + e[16] * e[24] * e[15] + e[16] * e[26] * e[17] - e[16] * e[23] * e[14] - e[16] * e[20] * e[11] - e[16] * e[18] * e[9] + 0.5 * e2[13] * e[25] + 1.5 * e[25] * e2[16] + 0.5 * e[25] * e2[17] + 0.5 * e[25] * e2[15] + 0.5 * e2[10] * e[25] - 0.5 * e[25] * e2[9] - 0.5 * e[25] * e2[11] - 0.5 * e[25] * e2[12] - 0.5 * e[25] * e2[14]; a[102] = e[19] * e[20] * e[2] + e[22] * e[18] * e[3] + e[22] * e[0] * e[21] + e[22] * e[19] * e[4] + e[22] * e[20] * e[5] + e[22] * e[2] * e[23] - e[19] * e[21] * e[3] - e[19] * e[26] * e[8] + e[19] * e[25] * e[7] - e[19] * e[23] * e[5] - e[19] * e[24] * e[6] + e[4] * e[18] * e[21] + e[4] * e[20] * e[23] + e[25] * e[18] * e[6] + e[25] * e[0] * e[24] + e[25] * e[20] * e[8] + e[25] * e[2] * e[26] + e[7] * e[18] * e[24] + e[7] * e[20] * e[26] + e[19] * e[18] * e[0] + 1.5 * e2[19] * e[1] + 0.5 * e[1] * e2[22] + 0.5 * e[1] * e2[18] + 0.5 * e[1] * e2[20] + 0.5 * e[1] * e2[25] - 0.5 * e[1] * e2[26] - 0.5 * e[1] * e2[23] - 0.5 * e[1] * e2[24] - 0.5 * e[1] * e2[21]; a[178] = e[19] * e[27] * e[24] + e[19] * e[18] * e[33] + e[19] * e[28] * e[25] + e[19] * e[29] * e[26] + e[19] * e[20] * e[35] + e[28] * e[18] * e[24] + e[28] * e[20] * e[26] + e[22] * e[30] * e[24] + e[22] * e[21] * e[33] + e[22] * e[31] * e[25] + e[22] * e[32] * e[26] + e[22] * e[23] * e[35] + e[31] * e[21] * e[24] + e[31] * e[23] * e[26] + e[25] * e[33] * e[24] + e[25] * e[35] * e[26] - e[25] * e[27] * e[18] - e[25] * e[30] * e[21] - e[25] * e[29] * e[20] - e[25] * e[32] * e[23] - 0.5 * e[34] * e2[18] - 0.5 * e[34] * e2[23] - 0.5 * e[34] * e2[20] - 0.5 * e[34] * e2[21] + 0.5 * e2[19] * e[34] + 0.5 * e2[22] * e[34] + 1.5 * e[34] * e2[25] + 0.5 * e[34] * e2[24] + 0.5 * e[34] * e2[26]; a[22] = e[16] * e[0] * e[6] + e[16] * e[2] * e[8] + e[1] * e[11] * e[2] - e[1] * e[15] * e[6] + e[1] * e[9] * e[0] - e[1] * e[14] * e[5] - e[1] * e[12] * e[3] - e[1] * e[17] * e[8] + e[4] * e[9] * e[3] + e[4] * e[0] * e[12] + e[4] * e[1] * e[13] + e[4] * e[11] * e[5] + e[4] * e[2] * e[14] + e[13] * e[0] * e[3] + e[13] * e[2] * e[5] + e[7] * e[9] * e[6] + e[7] * e[0] * e[15] + e[7] * e[1] * e[16] + e[7] * e[11] * e[8] + e[7] * e[2] * e[17] - 0.5 * e[10] * e2[6] - 0.5 * e[10] * e2[5] - 0.5 * e[10] * e2[3] - 0.5 * e[10] * e2[8] + 1.5 * e[10] * e2[1] + 0.5 * e[10] * e2[0] + 0.5 * e[10] * e2[2] + 0.5 * e[10] * e2[4] + 0.5 * e[10] * e2[7]; a[18] = e[13] * e[14] * e[17] + e[13] * e[12] * e[15] + e[10] * e[9] * e[15] + 0.5 * e[16] * e2[15] - 0.5 * e[16] * e2[11] - 0.5 * e[16] * e2[12] - 0.5 * e[16] * e2[14] + e[10] * e[11] * e[17] + 0.5 * e2[10] * e[16] + 0.5 * e3[16] - 0.5 * e[16] * e2[9] + 0.5 * e[16] * e2[17] + 0.5 * e2[13] * e[16]; a[142] = e[10] * e[29] * e[20] + e[22] * e[27] * e[12] + e[22] * e[9] * e[30] + e[22] * e[29] * e[14] + e[22] * e[11] * e[32] + e[22] * e[10] * e[31] + e[31] * e[18] * e[12] + e[31] * e[9] * e[21] + e[31] * e[20] * e[14] + e[31] * e[11] * e[23] - e[10] * e[33] * e[24] - e[10] * e[30] * e[21] - e[10] * e[35] * e[26] - e[10] * e[32] * e[23] + e[10] * e[34] * e[25] + e[19] * e[27] * e[9] + e[19] * e[29] * e[11] + e[28] * e[18] * e[9] + e[28] * e[20] * e[11] + e[16] * e[27] * e[24] + e[16] * e[18] * e[33] + e[16] * e[28] * e[25] + e[16] * e[19] * e[34] + e[16] * e[29] * e[26] + e[16] * e[20] * e[35] - e[19] * e[30] * e[12] - e[19] * e[32] * e[14] - e[19] * e[33] * e[15] - e[19] * e[35] * e[17] - e[28] * e[23] * e[14] - e[28] * e[24] * e[15] - e[28] * e[26] * e[17] - e[28] * e[21] * e[12] + e[25] * e[27] * e[15] + e[25] * e[9] * e[33] + e[25] * e[29] * e[17] + e[25] * e[11] * e[35] + e[34] * e[18] * e[15] + e[34] * e[9] * e[24] + e[34] * e[20] * e[17] + e[34] * e[11] * e[26] + e[13] * e[27] * e[21] + e[13] * e[18] * e[30] + e[13] * e[28] * e[22] + e[13] * e[19] * e[31] + e[13] * e[29] * e[23] + e[13] * e[20] * e[32] + e[10] * e[27] * e[18] + 3. * e[10] * e[28] * e[19]; a[98] = e[4] * e[30] * e[15] + e[4] * e[12] * e[33] + e[4] * e[32] * e[17] + e[4] * e[14] * e[35] + e[4] * e[31] * e[16] + e[4] * e[13] * e[34] + e[7] * e[33] * e[15] + e[7] * e[35] * e[17] + 3. * e[7] * e[34] * e[16] + e[1] * e[27] * e[15] + e[1] * e[9] * e[33] + e[1] * e[29] * e[17] + e[1] * e[11] * e[35] + e[1] * e[28] * e[16] + e[1] * e[10] * e[34] - e[16] * e[27] * e[0] - e[16] * e[32] * e[5] + e[16] * e[33] * e[6] - e[16] * e[30] * e[3] + e[16] * e[35] * e[8] - e[16] * e[29] * e[2] + e[13] * e[30] * e[6] + e[13] * e[3] * e[33] + e[13] * e[31] * e[7] + e[13] * e[32] * e[8] + e[13] * e[5] * e[35] - e[34] * e[11] * e[2] + e[34] * e[15] * e[6] - e[34] * e[9] * e[0] - e[34] * e[14] * e[5] - e[34] * e[12] * e[3] + e[34] * e[17] * e[8] + e[31] * e[12] * e[6] + e[31] * e[3] * e[15] + e[31] * e[14] * e[8] + e[31] * e[5] * e[17] - e[7] * e[27] * e[9] - e[7] * e[30] * e[12] + e[7] * e[28] * e[10] - e[7] * e[32] * e[14] + e[10] * e[27] * e[6] + e[10] * e[0] * e[33] + e[10] * e[29] * e[8] + e[10] * e[2] * e[35] + e[28] * e[9] * e[6] + e[28] * e[0] * e[15] + e[28] * e[11] * e[8] + e[28] * e[2] * e[17] - e[7] * e[29] * e[11]; a[132] = e[22] * e[18] * e[12] + e[22] * e[9] * e[21] + e[22] * e[20] * e[14] + e[22] * e[11] * e[23] + e[22] * e[19] * e[13] + e[25] * e[18] * e[15] + e[25] * e[9] * e[24] + e[25] * e[20] * e[17] + e[25] * e[11] * e[26] + e[25] * e[19] * e[16] + e[16] * e[18] * e[24] + e[16] * e[20] * e[26] + e[13] * e[18] * e[21] + e[13] * e[20] * e[23] + e[19] * e[18] * e[9] + e[19] * e[20] * e[11] - e[19] * e[23] * e[14] - e[19] * e[24] * e[15] - e[19] * e[26] * e[17] - e[19] * e[21] * e[12] + 0.5 * e[10] * e2[22] + 0.5 * e[10] * e2[25] + 1.5 * e[10] * e2[19] + 0.5 * e[10] * e2[18] + 0.5 * e[10] * e2[20] - 0.5 * e[10] * e2[26] - 0.5 * e[10] * e2[23] - 0.5 * e[10] * e2[24] - 0.5 * e[10] * e2[21]; a[168] = e[19] * e[20] * e[26] - 0.5 * e[25] * e2[20] + e[22] * e[21] * e[24] + e[19] * e[18] * e[24] + 0.5 * e2[22] * e[25] - 0.5 * e[25] * e2[21] - 0.5 * e[25] * e2[23] + 0.5 * e2[19] * e[25] - 0.5 * e[25] * e2[18] + 0.5 * e[25] * e2[24] + 0.5 * e[25] * e2[26] + 0.5 * e3[25] + e[22] * e[23] * e[26]; a[113] = -e[20] * e[33] * e[6] - e[20] * e[30] * e[3] - e[20] * e[31] * e[4] - e[29] * e[21] * e[3] - e[29] * e[22] * e[4] - e[29] * e[25] * e[7] - e[29] * e[24] * e[6] + e[8] * e[27] * e[24] + e[8] * e[18] * e[33] + e[8] * e[28] * e[25] + e[8] * e[19] * e[34] + e[23] * e[27] * e[3] + e[23] * e[0] * e[30] + e[23] * e[28] * e[4] + e[23] * e[1] * e[31] + e[32] * e[18] * e[3] + e[32] * e[0] * e[21] + e[32] * e[19] * e[4] + e[32] * e[1] * e[22] + e[26] * e[27] * e[6] + e[26] * e[0] * e[33] + e[26] * e[28] * e[7] + e[26] * e[1] * e[34] + e[26] * e[29] * e[8] + e[26] * e[2] * e[35] + e[35] * e[18] * e[6] + e[35] * e[0] * e[24] + e[35] * e[19] * e[7] + e[35] * e[1] * e[25] + e[35] * e[20] * e[8] + e[2] * e[27] * e[18] + e[2] * e[28] * e[19] + 3. * e[2] * e[29] * e[20] + e[20] * e[27] * e[0] + e[20] * e[28] * e[1] + e[29] * e[18] * e[0] + e[29] * e[19] * e[1] + e[5] * e[27] * e[21] + e[5] * e[18] * e[30] + e[5] * e[28] * e[22] + e[5] * e[19] * e[31] + e[5] * e[29] * e[23] + e[5] * e[20] * e[32] - e[2] * e[33] * e[24] - e[2] * e[30] * e[21] - e[2] * e[31] * e[22] + e[2] * e[32] * e[23] - e[2] * e[34] * e[25] - e[20] * e[34] * e[7]; a[43] = e[5] * e[18] * e[3] + e[5] * e[0] * e[21] + e[5] * e[19] * e[4] + e[5] * e[1] * e[22] + e[5] * e[2] * e[23] + e[23] * e[1] * e[4] + e[23] * e[0] * e[3] + e[8] * e[18] * e[6] + e[8] * e[0] * e[24] + e[8] * e[19] * e[7] + e[8] * e[1] * e[25] + e[8] * e[2] * e[26] + e[26] * e[1] * e[7] + e[26] * e[0] * e[6] + e[2] * e[18] * e[0] + e[2] * e[19] * e[1] - e[2] * e[21] * e[3] - e[2] * e[22] * e[4] - e[2] * e[25] * e[7] - e[2] * e[24] * e[6] - 0.5 * e[20] * e2[4] + 0.5 * e[20] * e2[0] - 0.5 * e[20] * e2[6] + 0.5 * e[20] * e2[5] + 0.5 * e[20] * e2[1] - 0.5 * e[20] * e2[7] - 0.5 * e[20] * e2[3] + 1.5 * e[20] * e2[2] + 0.5 * e[20] * e2[8]; a[33] = e[14] * e[9] * e[3] + e[14] * e[0] * e[12] + e[14] * e[10] * e[4] + e[14] * e[1] * e[13] + e[14] * e[11] * e[5] + e[17] * e[9] * e[6] + e[17] * e[0] * e[15] + e[17] * e[10] * e[7] + e[17] * e[1] * e[16] + e[17] * e[11] * e[8] + e[8] * e[9] * e[15] + e[8] * e[10] * e[16] + e[5] * e[9] * e[12] + e[5] * e[10] * e[13] + e[11] * e[9] * e[0] + e[11] * e[10] * e[1] - e[11] * e[13] * e[4] - e[11] * e[16] * e[7] - e[11] * e[15] * e[6] - e[11] * e[12] * e[3] + 0.5 * e[2] * e2[14] + 0.5 * e[2] * e2[17] + 1.5 * e[2] * e2[11] + 0.5 * e[2] * e2[9] + 0.5 * e[2] * e2[10] - 0.5 * e[2] * e2[16] - 0.5 * e[2] * e2[12] - 0.5 * e[2] * e2[15] - 0.5 * e[2] * e2[13]; a[63] = e[14] * e[18] * e[12] + e[14] * e[9] * e[21] + e[14] * e[11] * e[23] + e[14] * e[19] * e[13] + e[14] * e[10] * e[22] + e[23] * e[9] * e[12] + e[23] * e[10] * e[13] + e[17] * e[18] * e[15] + e[17] * e[9] * e[24] + e[17] * e[11] * e[26] + e[17] * e[19] * e[16] + e[17] * e[10] * e[25] + e[26] * e[9] * e[15] + e[26] * e[10] * e[16] - e[11] * e[24] * e[15] - e[11] * e[25] * e[16] + e[11] * e[18] * e[9] - e[11] * e[21] * e[12] + e[11] * e[19] * e[10] - e[11] * e[22] * e[13] + 1.5 * e[20] * e2[11] + 0.5 * e[20] * e2[9] + 0.5 * e[20] * e2[10] + 0.5 * e[20] * e2[14] + 0.5 * e[20] * e2[17] - 0.5 * e[20] * e2[16] - 0.5 * e[20] * e2[12] - 0.5 * e[20] * e2[15] - 0.5 * e[20] * e2[13]; a[143] = e[23] * e[10] * e[31] + e[32] * e[18] * e[12] + e[32] * e[9] * e[21] + e[32] * e[19] * e[13] + e[32] * e[10] * e[22] - e[11] * e[33] * e[24] - e[11] * e[30] * e[21] + e[11] * e[35] * e[26] - e[11] * e[31] * e[22] - e[11] * e[34] * e[25] + e[20] * e[27] * e[9] + e[20] * e[28] * e[10] + e[29] * e[18] * e[9] + e[29] * e[19] * e[10] + e[17] * e[27] * e[24] + e[17] * e[18] * e[33] + e[17] * e[28] * e[25] + e[17] * e[19] * e[34] + e[17] * e[29] * e[26] + e[17] * e[20] * e[35] - e[20] * e[30] * e[12] - e[20] * e[31] * e[13] - e[20] * e[33] * e[15] - e[20] * e[34] * e[16] - e[29] * e[24] * e[15] - e[29] * e[25] * e[16] - e[29] * e[21] * e[12] - e[29] * e[22] * e[13] + e[26] * e[27] * e[15] + e[26] * e[9] * e[33] + e[26] * e[28] * e[16] + e[26] * e[10] * e[34] + e[35] * e[18] * e[15] + e[35] * e[9] * e[24] + e[35] * e[19] * e[16] + e[35] * e[10] * e[25] + e[14] * e[27] * e[21] + e[14] * e[18] * e[30] + e[14] * e[28] * e[22] + e[14] * e[19] * e[31] + e[14] * e[29] * e[23] + e[14] * e[20] * e[32] + e[11] * e[27] * e[18] + e[11] * e[28] * e[19] + 3. * e[11] * e[29] * e[20] + e[23] * e[27] * e[12] + e[23] * e[9] * e[30] + e[23] * e[11] * e[32] + e[23] * e[28] * e[13]; a[133] = e[23] * e[18] * e[12] + e[23] * e[9] * e[21] + e[23] * e[20] * e[14] + e[23] * e[19] * e[13] + e[23] * e[10] * e[22] + e[26] * e[18] * e[15] + e[26] * e[9] * e[24] + e[26] * e[20] * e[17] + e[26] * e[19] * e[16] + e[26] * e[10] * e[25] + e[17] * e[19] * e[25] + e[17] * e[18] * e[24] + e[14] * e[19] * e[22] + e[14] * e[18] * e[21] + e[20] * e[18] * e[9] + e[20] * e[19] * e[10] - e[20] * e[24] * e[15] - e[20] * e[25] * e[16] - e[20] * e[21] * e[12] - e[20] * e[22] * e[13] + 0.5 * e[11] * e2[23] + 0.5 * e[11] * e2[26] + 0.5 * e[11] * e2[19] + 0.5 * e[11] * e2[18] + 1.5 * e[11] * e2[20] - 0.5 * e[11] * e2[22] - 0.5 * e[11] * e2[24] - 0.5 * e[11] * e2[21] - 0.5 * e[11] * e2[25]; a[103] = -e[20] * e[21] * e[3] + e[20] * e[26] * e[8] - e[20] * e[22] * e[4] - e[20] * e[25] * e[7] - e[20] * e[24] * e[6] + e[5] * e[19] * e[22] + e[5] * e[18] * e[21] + e[26] * e[18] * e[6] + e[26] * e[0] * e[24] + e[26] * e[19] * e[7] + e[26] * e[1] * e[25] + e[8] * e[19] * e[25] + e[8] * e[18] * e[24] + e[20] * e[18] * e[0] + e[20] * e[19] * e[1] + e[23] * e[18] * e[3] + e[23] * e[0] * e[21] + e[23] * e[19] * e[4] + e[23] * e[1] * e[22] + e[23] * e[20] * e[5] + 1.5 * e2[20] * e[2] + 0.5 * e[2] * e2[23] + 0.5 * e[2] * e2[19] + 0.5 * e[2] * e2[18] + 0.5 * e[2] * e2[26] - 0.5 * e[2] * e2[22] - 0.5 * e[2] * e2[24] - 0.5 * e[2] * e2[21] - 0.5 * e[2] * e2[25]; a[23] = -e[2] * e[15] * e[6] + e[2] * e[9] * e[0] - e[2] * e[12] * e[3] + e[5] * e[9] * e[3] + e[5] * e[0] * e[12] + e[5] * e[10] * e[4] + e[5] * e[1] * e[13] + e[5] * e[2] * e[14] + e[14] * e[1] * e[4] + e[14] * e[0] * e[3] + e[8] * e[9] * e[6] + e[8] * e[0] * e[15] + e[8] * e[10] * e[7] + e[8] * e[1] * e[16] + e[8] * e[2] * e[17] + e[17] * e[1] * e[7] + e[17] * e[0] * e[6] + e[2] * e[10] * e[1] - e[2] * e[13] * e[4] - e[2] * e[16] * e[7] + 0.5 * e[11] * e2[1] + 0.5 * e[11] * e2[0] + 1.5 * e[11] * e2[2] + 0.5 * e[11] * e2[5] + 0.5 * e[11] * e2[8] - 0.5 * e[11] * e2[4] - 0.5 * e[11] * e2[6] - 0.5 * e[11] * e2[7] - 0.5 * e[11] * e2[3]; a[83] = e[5] * e[19] * e[13] + e[5] * e[10] * e[22] + e[8] * e[18] * e[15] + e[8] * e[9] * e[24] + e[8] * e[20] * e[17] + e[8] * e[11] * e[26] + e[8] * e[19] * e[16] + e[8] * e[10] * e[25] + e[2] * e[18] * e[9] + e[2] * e[19] * e[10] - e[11] * e[21] * e[3] - e[11] * e[22] * e[4] - e[11] * e[25] * e[7] - e[11] * e[24] * e[6] + e[14] * e[18] * e[3] + e[14] * e[0] * e[21] + e[14] * e[19] * e[4] + e[14] * e[1] * e[22] + e[14] * e[2] * e[23] - e[20] * e[13] * e[4] - e[20] * e[16] * e[7] - e[20] * e[15] * e[6] - e[20] * e[12] * e[3] + e[23] * e[9] * e[3] + e[23] * e[0] * e[12] + e[23] * e[10] * e[4] + e[23] * e[1] * e[13] + e[17] * e[18] * e[6] + e[17] * e[0] * e[24] + e[17] * e[19] * e[7] + e[17] * e[1] * e[25] + e[17] * e[2] * e[26] - e[2] * e[24] * e[15] - e[2] * e[25] * e[16] - e[2] * e[21] * e[12] - e[2] * e[22] * e[13] + e[26] * e[9] * e[6] + e[26] * e[0] * e[15] + e[26] * e[10] * e[7] + e[26] * e[1] * e[16] + e[11] * e[18] * e[0] + e[11] * e[19] * e[1] + 3. * e[11] * e[20] * e[2] + e[20] * e[9] * e[0] + e[20] * e[10] * e[1] + e[5] * e[18] * e[12] + e[5] * e[9] * e[21] + e[5] * e[20] * e[14] + e[5] * e[11] * e[23]; a[53] = e[32] * e[1] * e[4] + e[32] * e[0] * e[3] + e[8] * e[27] * e[6] + e[8] * e[0] * e[33] + e[8] * e[28] * e[7] + e[8] * e[1] * e[34] + e[35] * e[1] * e[7] + e[35] * e[0] * e[6] + e[2] * e[27] * e[0] + e[2] * e[28] * e[1] - e[2] * e[34] * e[7] + e[2] * e[32] * e[5] - e[2] * e[33] * e[6] - e[2] * e[30] * e[3] + e[2] * e[35] * e[8] - e[2] * e[31] * e[4] + e[5] * e[27] * e[3] + e[5] * e[0] * e[30] + e[5] * e[28] * e[4] + e[5] * e[1] * e[31] + 1.5 * e[29] * e2[2] - 0.5 * e[29] * e2[4] + 0.5 * e[29] * e2[0] - 0.5 * e[29] * e2[6] + 0.5 * e[29] * e2[5] + 0.5 * e[29] * e2[1] - 0.5 * e[29] * e2[7] - 0.5 * e[29] * e2[3] + 0.5 * e[29] * e2[8]; a[3] = e[5] * e[0] * e[3] + e[8] * e[1] * e[7] + e[8] * e[0] * e[6] + e[5] * e[1] * e[4] - 0.5 * e[2] * e2[4] + 0.5 * e3[2] + 0.5 * e[2] * e2[1] - 0.5 * e[2] * e2[3] + 0.5 * e[2] * e2[0] + 0.5 * e[2] * e2[8] + 0.5 * e[2] * e2[5] - 0.5 * e[2] * e2[6] - 0.5 * e[2] * e2[7]; a[73] = e[35] * e[9] * e[15] + e[35] * e[10] * e[16] - e[11] * e[30] * e[12] - e[11] * e[31] * e[13] - e[11] * e[33] * e[15] - e[11] * e[34] * e[16] + e[11] * e[27] * e[9] + e[11] * e[28] * e[10] + e[14] * e[27] * e[12] + e[14] * e[9] * e[30] + e[14] * e[11] * e[32] + e[14] * e[28] * e[13] + e[14] * e[10] * e[31] + e[32] * e[9] * e[12] + e[32] * e[10] * e[13] + e[17] * e[27] * e[15] + e[17] * e[9] * e[33] + e[17] * e[11] * e[35] + e[17] * e[28] * e[16] + e[17] * e[10] * e[34] + 1.5 * e[29] * e2[11] - 0.5 * e[29] * e2[16] + 0.5 * e[29] * e2[9] - 0.5 * e[29] * e2[12] - 0.5 * e[29] * e2[15] + 0.5 * e[29] * e2[17] + 0.5 * e[29] * e2[10] + 0.5 * e[29] * e2[14] - 0.5 * e[29] * e2[13]; a[13] = e[14] * e[9] * e[12] + e[17] * e[10] * e[16] + e[17] * e[9] * e[15] + 0.5 * e3[11] + e[14] * e[10] * e[13] + 0.5 * e[11] * e2[10] - 0.5 * e[11] * e2[15] + 0.5 * e[11] * e2[14] - 0.5 * e[11] * e2[13] - 0.5 * e[11] * e2[12] + 0.5 * e[11] * e2[9] - 0.5 * e[11] * e2[16] + 0.5 * e[11] * e2[17]; a[173] = e[20] * e[27] * e[18] + e[20] * e[28] * e[19] + e[23] * e[27] * e[21] + e[23] * e[18] * e[30] + e[23] * e[28] * e[22] + e[23] * e[19] * e[31] + e[23] * e[20] * e[32] + e[32] * e[19] * e[22] + e[32] * e[18] * e[21] + e[26] * e[27] * e[24] + e[26] * e[18] * e[33] + e[26] * e[28] * e[25] + e[26] * e[19] * e[34] + e[26] * e[20] * e[35] + e[35] * e[19] * e[25] + e[35] * e[18] * e[24] - e[20] * e[33] * e[24] - e[20] * e[30] * e[21] - e[20] * e[31] * e[22] - e[20] * e[34] * e[25] + 0.5 * e[29] * e2[23] + 0.5 * e[29] * e2[26] - 0.5 * e[29] * e2[22] - 0.5 * e[29] * e2[24] - 0.5 * e[29] * e2[21] - 0.5 * e[29] * e2[25] + 1.5 * e[29] * e2[20] + 0.5 * e[29] * e2[19] + 0.5 * e[29] * e2[18]; a[163] = 0.5 * e[20] * e2[26] + 0.5 * e[20] * e2[18] + 0.5 * e3[20] + 0.5 * e[20] * e2[19] + e[26] * e[18] * e[24] + 0.5 * e[20] * e2[23] - 0.5 * e[20] * e2[25] + e[23] * e[19] * e[22] - 0.5 * e[20] * e2[24] - 0.5 * e[20] * e2[21] - 0.5 * e[20] * e2[22] + e[23] * e[18] * e[21] + e[26] * e[19] * e[25]; a[93] = e[8] * e[28] * e[16] + e[8] * e[10] * e[34] + e[2] * e[27] * e[9] + 3. * e[2] * e[29] * e[11] + e[2] * e[28] * e[10] + e[11] * e[27] * e[0] - e[11] * e[34] * e[7] - e[11] * e[33] * e[6] - e[11] * e[30] * e[3] + e[11] * e[28] * e[1] - e[11] * e[31] * e[4] + e[14] * e[27] * e[3] + e[14] * e[0] * e[30] + e[14] * e[28] * e[4] + e[14] * e[1] * e[31] + e[14] * e[2] * e[32] + e[29] * e[10] * e[1] - e[29] * e[13] * e[4] - e[29] * e[16] * e[7] - e[29] * e[15] * e[6] + e[29] * e[9] * e[0] - e[29] * e[12] * e[3] + e[32] * e[9] * e[3] + e[32] * e[0] * e[12] + e[32] * e[10] * e[4] + e[32] * e[1] * e[13] + e[17] * e[27] * e[6] + e[17] * e[0] * e[33] + e[17] * e[28] * e[7] + e[17] * e[1] * e[34] + e[17] * e[2] * e[35] - e[2] * e[30] * e[12] - e[2] * e[31] * e[13] - e[2] * e[33] * e[15] - e[2] * e[34] * e[16] + e[35] * e[9] * e[6] + e[35] * e[0] * e[15] + e[35] * e[10] * e[7] + e[35] * e[1] * e[16] + e[5] * e[27] * e[12] + e[5] * e[9] * e[30] + e[5] * e[29] * e[14] + e[5] * e[11] * e[32] + e[5] * e[28] * e[13] + e[5] * e[10] * e[31] + e[8] * e[27] * e[15] + e[8] * e[9] * e[33] + e[8] * e[29] * e[17] + e[8] * e[11] * e[35]; a[94] = -e[12] * e[34] * e[7] + e[12] * e[32] * e[5] - e[12] * e[35] * e[8] - e[12] * e[29] * e[2] - e[12] * e[28] * e[1] + e[12] * e[31] * e[4] - e[30] * e[11] * e[2] - e[30] * e[10] * e[1] + e[30] * e[13] * e[4] - e[30] * e[16] * e[7] + e[30] * e[14] * e[5] - e[30] * e[17] * e[8] + e[15] * e[3] * e[33] + e[15] * e[31] * e[7] + e[15] * e[4] * e[34] + e[15] * e[32] * e[8] + e[15] * e[5] * e[35] + e[3] * e[27] * e[9] - e[3] * e[28] * e[10] - e[3] * e[34] * e[16] - e[3] * e[35] * e[17] - e[3] * e[29] * e[11] + e[33] * e[13] * e[7] + e[33] * e[4] * e[16] + e[33] * e[14] * e[8] + e[33] * e[5] * e[17] + e[9] * e[28] * e[4] + e[9] * e[1] * e[31] + e[9] * e[29] * e[5] + e[9] * e[2] * e[32] + e[27] * e[10] * e[4] + e[27] * e[1] * e[13] + e[27] * e[11] * e[5] + e[27] * e[2] * e[14] + 3. * e[3] * e[30] * e[12] + e[3] * e[32] * e[14] + e[3] * e[31] * e[13] + e[6] * e[30] * e[15] + e[6] * e[12] * e[33] + e[6] * e[32] * e[17] + e[6] * e[14] * e[35] + e[6] * e[31] * e[16] + e[6] * e[13] * e[34] + e[0] * e[27] * e[12] + e[0] * e[9] * e[30] + e[0] * e[29] * e[14] + e[0] * e[11] * e[32] + e[0] * e[28] * e[13] + e[0] * e[10] * e[31]; a[164] = 0.5 * e[21] * e2[24] - 0.5 * e[21] * e2[25] + 0.5 * e[21] * e2[23] - 0.5 * e[21] * e2[26] + 0.5 * e2[18] * e[21] + 0.5 * e[21] * e2[22] - 0.5 * e[21] * e2[20] + e[24] * e[22] * e[25] + e[24] * e[23] * e[26] - 0.5 * e[21] * e2[19] + e[18] * e[19] * e[22] + e[18] * e[20] * e[23] + 0.5 * e3[21]; a[174] = -0.5 * e[30] * e2[26] - 0.5 * e[30] * e2[19] - 0.5 * e[30] * e2[20] - 0.5 * e[30] * e2[25] + 0.5 * e2[18] * e[30] + 1.5 * e[30] * e2[21] + 0.5 * e[30] * e2[22] + 0.5 * e[30] * e2[23] + 0.5 * e[30] * e2[24] + e[18] * e[27] * e[21] + e[18] * e[28] * e[22] + e[18] * e[19] * e[31] + e[18] * e[29] * e[23] + e[18] * e[20] * e[32] + e[27] * e[19] * e[22] + e[27] * e[20] * e[23] + e[21] * e[31] * e[22] + e[21] * e[32] * e[23] + e[24] * e[21] * e[33] + e[24] * e[31] * e[25] + e[24] * e[22] * e[34] + e[24] * e[32] * e[26] + e[24] * e[23] * e[35] + e[33] * e[22] * e[25] + e[33] * e[23] * e[26] - e[21] * e[29] * e[20] - e[21] * e[35] * e[26] - e[21] * e[28] * e[19] - e[21] * e[34] * e[25]; a[14] = 0.5 * e[12] * e2[15] - 0.5 * e[12] * e2[17] + e[15] * e[13] * e[16] - 0.5 * e[12] * e2[10] + e[15] * e[14] * e[17] - 0.5 * e[12] * e2[16] - 0.5 * e[12] * e2[11] + e[9] * e[10] * e[13] + 0.5 * e[12] * e2[13] + 0.5 * e2[9] * e[12] + 0.5 * e3[12] + e[9] * e[11] * e[14] + 0.5 * e[12] * e2[14]; a[34] = e[12] * e[13] * e[4] + e[12] * e[14] * e[5] + e[15] * e[12] * e[6] + e[15] * e[13] * e[7] + e[15] * e[4] * e[16] + e[15] * e[14] * e[8] + e[15] * e[5] * e[17] + e[6] * e[14] * e[17] + e[6] * e[13] * e[16] + e[0] * e[11] * e[14] + e[0] * e[9] * e[12] + e[0] * e[10] * e[13] + e[9] * e[10] * e[4] + e[9] * e[1] * e[13] + e[9] * e[11] * e[5] + e[9] * e[2] * e[14] - e[12] * e[11] * e[2] - e[12] * e[10] * e[1] - e[12] * e[16] * e[7] - e[12] * e[17] * e[8] + 1.5 * e2[12] * e[3] + 0.5 * e[3] * e2[15] - 0.5 * e[3] * e2[16] + 0.5 * e[3] * e2[9] - 0.5 * e[3] * e2[11] - 0.5 * e[3] * e2[17] - 0.5 * e[3] * e2[10] + 0.5 * e[3] * e2[14] + 0.5 * e[3] * e2[13]; a[64] = e[18] * e[11] * e[14] + e[18] * e[9] * e[12] + e[18] * e[10] * e[13] + e[12] * e[23] * e[14] + e[12] * e[22] * e[13] + e[15] * e[12] * e[24] + e[15] * e[23] * e[17] + e[15] * e[14] * e[26] + e[15] * e[22] * e[16] + e[15] * e[13] * e[25] + e[24] * e[14] * e[17] + e[24] * e[13] * e[16] - e[12] * e[25] * e[16] - e[12] * e[26] * e[17] - e[12] * e[20] * e[11] - e[12] * e[19] * e[10] + e[9] * e[20] * e[14] + e[9] * e[11] * e[23] + e[9] * e[19] * e[13] + e[9] * e[10] * e[22] + 0.5 * e2[9] * e[21] - 0.5 * e[21] * e2[16] - 0.5 * e[21] * e2[11] - 0.5 * e[21] * e2[17] - 0.5 * e[21] * e2[10] + 1.5 * e[21] * e2[12] + 0.5 * e[21] * e2[14] + 0.5 * e[21] * e2[13] + 0.5 * e[21] * e2[15]; a[114] = -e[21] * e[35] * e[8] - e[21] * e[29] * e[2] - e[21] * e[28] * e[1] + e[21] * e[31] * e[4] - e[30] * e[26] * e[8] - e[30] * e[20] * e[2] - e[30] * e[19] * e[1] + e[30] * e[22] * e[4] - e[30] * e[25] * e[7] + e[30] * e[23] * e[5] + e[6] * e[31] * e[25] + e[6] * e[22] * e[34] + e[6] * e[32] * e[26] + e[6] * e[23] * e[35] + e[24] * e[30] * e[6] + e[24] * e[3] * e[33] + e[24] * e[31] * e[7] + e[24] * e[4] * e[34] + e[24] * e[32] * e[8] + e[24] * e[5] * e[35] + e[33] * e[21] * e[6] + e[33] * e[22] * e[7] + e[33] * e[4] * e[25] + e[33] * e[23] * e[8] + e[33] * e[5] * e[26] + e[0] * e[27] * e[21] + e[0] * e[18] * e[30] + e[0] * e[28] * e[22] + e[0] * e[19] * e[31] + e[0] * e[29] * e[23] + e[0] * e[20] * e[32] + e[18] * e[27] * e[3] + e[18] * e[28] * e[4] + e[18] * e[1] * e[31] + e[18] * e[29] * e[5] + e[18] * e[2] * e[32] + e[27] * e[19] * e[4] + e[27] * e[1] * e[22] + e[27] * e[20] * e[5] + e[27] * e[2] * e[23] + 3. * e[3] * e[30] * e[21] + e[3] * e[31] * e[22] + e[3] * e[32] * e[23] - e[3] * e[29] * e[20] - e[3] * e[35] * e[26] - e[3] * e[28] * e[19] - e[3] * e[34] * e[25] - e[21] * e[34] * e[7] + e[21] * e[32] * e[5]; a[44] = e[18] * e[1] * e[4] + e[18] * e[0] * e[3] + e[18] * e[2] * e[5] + e[3] * e[22] * e[4] + e[3] * e[23] * e[5] + e[6] * e[3] * e[24] + e[6] * e[22] * e[7] + e[6] * e[4] * e[25] + e[6] * e[23] * e[8] + e[6] * e[5] * e[26] + e[24] * e[4] * e[7] + e[24] * e[5] * e[8] + e[0] * e[19] * e[4] + e[0] * e[1] * e[22] + e[0] * e[20] * e[5] + e[0] * e[2] * e[23] - e[3] * e[26] * e[8] - e[3] * e[20] * e[2] - e[3] * e[19] * e[1] - e[3] * e[25] * e[7] + 0.5 * e[21] * e2[4] + 0.5 * e[21] * e2[0] + 0.5 * e[21] * e2[6] + 0.5 * e[21] * e2[5] - 0.5 * e[21] * e2[1] - 0.5 * e[21] * e2[7] + 1.5 * e[21] * e2[3] - 0.5 * e[21] * e2[2] - 0.5 * e[21] * e2[8]; a[184] = 0.5 * e2[27] * e[21] + 1.5 * e[21] * e2[30] + 0.5 * e[21] * e2[32] + 0.5 * e[21] * e2[31] + 0.5 * e[21] * e2[33] - 0.5 * e[21] * e2[28] - 0.5 * e[21] * e2[29] - 0.5 * e[21] * e2[34] - 0.5 * e[21] * e2[35] + e[18] * e[27] * e[30] + e[18] * e[29] * e[32] + e[18] * e[28] * e[31] + e[27] * e[28] * e[22] + e[27] * e[19] * e[31] + e[27] * e[29] * e[23] + e[27] * e[20] * e[32] + e[30] * e[31] * e[22] + e[30] * e[32] * e[23] + e[24] * e[30] * e[33] + e[24] * e[32] * e[35] + e[24] * e[31] * e[34] + e[33] * e[31] * e[25] + e[33] * e[22] * e[34] + e[33] * e[32] * e[26] + e[33] * e[23] * e[35] - e[30] * e[29] * e[20] - e[30] * e[35] * e[26] - e[30] * e[28] * e[19] - e[30] * e[34] * e[25]; a[49] = -0.5 * e[26] * e2[4] - 0.5 * e[26] * e2[0] + 0.5 * e[26] * e2[6] + 0.5 * e[26] * e2[5] - 0.5 * e[26] * e2[1] + 0.5 * e[26] * e2[7] - 0.5 * e[26] * e2[3] + 0.5 * e[26] * e2[2] + 1.5 * e[26] * e2[8] + e[20] * e[0] * e[6] + e[20] * e[2] * e[8] + e[5] * e[21] * e[6] + e[5] * e[3] * e[24] + e[5] * e[22] * e[7] + e[5] * e[4] * e[25] + e[5] * e[23] * e[8] + e[23] * e[4] * e[7] + e[23] * e[3] * e[6] + e[8] * e[24] * e[6] + e[8] * e[25] * e[7] + e[2] * e[18] * e[6] + e[2] * e[0] * e[24] + e[2] * e[19] * e[7] + e[2] * e[1] * e[25] - e[8] * e[21] * e[3] - e[8] * e[19] * e[1] - e[8] * e[22] * e[4] - e[8] * e[18] * e[0] + e[20] * e[1] * e[7]; a[154] = e[9] * e[27] * e[30] + e[9] * e[29] * e[32] + e[9] * e[28] * e[31] + e[33] * e[30] * e[15] + e[33] * e[32] * e[17] + e[33] * e[14] * e[35] + e[33] * e[31] * e[16] + e[33] * e[13] * e[34] + e[27] * e[29] * e[14] + e[27] * e[11] * e[32] + e[27] * e[28] * e[13] + e[27] * e[10] * e[31] - e[30] * e[28] * e[10] + e[30] * e[31] * e[13] + e[30] * e[32] * e[14] - e[30] * e[34] * e[16] - e[30] * e[35] * e[17] - e[30] * e[29] * e[11] + e[15] * e[32] * e[35] + e[15] * e[31] * e[34] - 0.5 * e[12] * e2[34] - 0.5 * e[12] * e2[35] + 0.5 * e[12] * e2[27] + 0.5 * e[12] * e2[32] - 0.5 * e[12] * e2[28] - 0.5 * e[12] * e2[29] + 0.5 * e[12] * e2[31] + 0.5 * e[12] * e2[33] + 1.5 * e[12] * e2[30]; a[119] = e[23] * e[30] * e[6] + e[23] * e[3] * e[33] + e[23] * e[31] * e[7] + e[23] * e[4] * e[34] + e[32] * e[21] * e[6] + e[32] * e[3] * e[24] + e[32] * e[22] * e[7] + e[32] * e[4] * e[25] + e[26] * e[33] * e[6] + e[26] * e[34] * e[7] + 3. * e[26] * e[35] * e[8] + e[35] * e[24] * e[6] + e[35] * e[25] * e[7] + e[2] * e[27] * e[24] + e[2] * e[18] * e[33] + e[2] * e[28] * e[25] + e[2] * e[19] * e[34] + e[2] * e[29] * e[26] + e[2] * e[20] * e[35] + e[20] * e[27] * e[6] + e[20] * e[0] * e[33] + e[20] * e[28] * e[7] + e[20] * e[1] * e[34] + e[20] * e[29] * e[8] + e[29] * e[18] * e[6] + e[29] * e[0] * e[24] + e[29] * e[19] * e[7] + e[29] * e[1] * e[25] + e[5] * e[30] * e[24] + e[5] * e[21] * e[33] + e[5] * e[31] * e[25] + e[5] * e[22] * e[34] + e[5] * e[32] * e[26] + e[5] * e[23] * e[35] - e[8] * e[27] * e[18] + e[8] * e[33] * e[24] - e[8] * e[30] * e[21] - e[8] * e[31] * e[22] + e[8] * e[32] * e[23] - e[8] * e[28] * e[19] + e[8] * e[34] * e[25] - e[26] * e[27] * e[0] - e[26] * e[30] * e[3] - e[26] * e[28] * e[1] - e[26] * e[31] * e[4] - e[35] * e[21] * e[3] - e[35] * e[19] * e[1] - e[35] * e[22] * e[4] - e[35] * e[18] * e[0]; a[194] = e[27] * e[29] * e[32] + e[27] * e[28] * e[31] + e[33] * e[32] * e[35] + e[33] * e[31] * e[34] + 0.5 * e3[30] - 0.5 * e[30] * e2[28] - 0.5 * e[30] * e2[29] - 0.5 * e[30] * e2[34] + 0.5 * e[30] * e2[33] + 0.5 * e2[27] * e[30] + 0.5 * e[30] * e2[32] + 0.5 * e[30] * e2[31] - 0.5 * e[30] * e2[35]; a[69] = 0.5 * e2[14] * e[26] + 1.5 * e[26] * e2[17] + 0.5 * e[26] * e2[15] + 0.5 * e[26] * e2[16] + 0.5 * e2[11] * e[26] - 0.5 * e[26] * e2[9] - 0.5 * e[26] * e2[12] - 0.5 * e[26] * e2[10] - 0.5 * e[26] * e2[13] + e[20] * e[11] * e[17] + e[20] * e[9] * e[15] + e[20] * e[10] * e[16] + e[14] * e[21] * e[15] + e[14] * e[12] * e[24] + e[14] * e[23] * e[17] + e[14] * e[22] * e[16] + e[14] * e[13] * e[25] + e[23] * e[12] * e[15] + e[23] * e[13] * e[16] + e[17] * e[24] * e[15] + e[17] * e[25] * e[16] - e[17] * e[18] * e[9] - e[17] * e[21] * e[12] - e[17] * e[19] * e[10] - e[17] * e[22] * e[13] + e[11] * e[18] * e[15] + e[11] * e[9] * e[24] + e[11] * e[19] * e[16] + e[11] * e[10] * e[25]; a[124] = e[0] * e[27] * e[30] + e[0] * e[29] * e[32] + e[0] * e[28] * e[31] + e[30] * e[31] * e[4] + e[30] * e[32] * e[5] + e[6] * e[30] * e[33] + e[6] * e[32] * e[35] + e[6] * e[31] * e[34] + e[27] * e[28] * e[4] + e[27] * e[1] * e[31] + e[27] * e[29] * e[5] + e[27] * e[2] * e[32] + e[33] * e[31] * e[7] + e[33] * e[4] * e[34] + e[33] * e[32] * e[8] + e[33] * e[5] * e[35] - e[30] * e[34] * e[7] - e[30] * e[35] * e[8] - e[30] * e[29] * e[2] - e[30] * e[28] * e[1] + 1.5 * e[3] * e2[30] + 0.5 * e[3] * e2[32] + 0.5 * e[3] * e2[31] + 0.5 * e[3] * e2[27] - 0.5 * e[3] * e2[28] - 0.5 * e[3] * e2[29] + 0.5 * e[3] * e2[33] - 0.5 * e[3] * e2[34] - 0.5 * e[3] * e2[35]; a[39] = 0.5 * e2[14] * e[8] + 1.5 * e2[17] * e[8] + 0.5 * e[8] * e2[15] + 0.5 * e[8] * e2[16] - 0.5 * e[8] * e2[9] + 0.5 * e[8] * e2[11] - 0.5 * e[8] * e2[12] - 0.5 * e[8] * e2[10] - 0.5 * e[8] * e2[13] + e[14] * e[12] * e[6] + e[14] * e[3] * e[15] + e[14] * e[13] * e[7] + e[14] * e[4] * e[16] + e[14] * e[5] * e[17] + e[17] * e[15] * e[6] + e[17] * e[16] * e[7] + e[2] * e[11] * e[17] + e[2] * e[9] * e[15] + e[2] * e[10] * e[16] + e[5] * e[12] * e[15] + e[5] * e[13] * e[16] + e[11] * e[9] * e[6] + e[11] * e[0] * e[15] + e[11] * e[10] * e[7] + e[11] * e[1] * e[16] - e[17] * e[10] * e[1] - e[17] * e[13] * e[4] - e[17] * e[9] * e[0] - e[17] * e[12] * e[3]; a[4] = -0.5 * e[3] * e2[1] - 0.5 * e[3] * e2[7] + 0.5 * e3[3] - 0.5 * e[3] * e2[8] + e[0] * e[2] * e[5] + 0.5 * e[3] * e2[6] + 0.5 * e[3] * e2[4] - 0.5 * e[3] * e2[2] + e[0] * e[1] * e[4] + e[6] * e[4] * e[7] + 0.5 * e2[0] * e[3] + 0.5 * e[3] * e2[5] + e[6] * e[5] * e[8]; a[139] = 0.5 * e2[23] * e[17] + 1.5 * e2[26] * e[17] + 0.5 * e[17] * e2[25] + 0.5 * e[17] * e2[24] - 0.5 * e[17] * e2[18] - 0.5 * e[17] * e2[19] + 0.5 * e[17] * e2[20] - 0.5 * e[17] * e2[22] - 0.5 * e[17] * e2[21] + e[23] * e[21] * e[15] + e[23] * e[12] * e[24] + e[23] * e[14] * e[26] + e[23] * e[22] * e[16] + e[23] * e[13] * e[25] + e[26] * e[24] * e[15] + e[26] * e[25] * e[16] + e[11] * e[19] * e[25] + e[11] * e[18] * e[24] + e[11] * e[20] * e[26] + e[14] * e[22] * e[25] + e[14] * e[21] * e[24] + e[20] * e[18] * e[15] + e[20] * e[9] * e[24] + e[20] * e[19] * e[16] + e[20] * e[10] * e[25] - e[26] * e[18] * e[9] - e[26] * e[21] * e[12] - e[26] * e[19] * e[10] - e[26] * e[22] * e[13]; a[74] = -e[12] * e[34] * e[16] - e[12] * e[35] * e[17] - e[12] * e[29] * e[11] + e[9] * e[27] * e[12] + e[9] * e[29] * e[14] + e[9] * e[11] * e[32] + e[9] * e[28] * e[13] + e[9] * e[10] * e[31] + e[27] * e[11] * e[14] + e[27] * e[10] * e[13] + e[12] * e[32] * e[14] + e[12] * e[31] * e[13] + e[15] * e[12] * e[33] + e[15] * e[32] * e[17] + e[15] * e[14] * e[35] + e[15] * e[31] * e[16] + e[15] * e[13] * e[34] + e[33] * e[14] * e[17] + e[33] * e[13] * e[16] - e[12] * e[28] * e[10] + 0.5 * e2[9] * e[30] - 0.5 * e[30] * e2[16] - 0.5 * e[30] * e2[11] + 1.5 * e[30] * e2[12] + 0.5 * e[30] * e2[15] - 0.5 * e[30] * e2[17] - 0.5 * e[30] * e2[10] + 0.5 * e[30] * e2[14] + 0.5 * e[30] * e2[13]; a[149] = e[32] * e[22] * e[16] + e[32] * e[13] * e[25] - e[17] * e[27] * e[18] + e[17] * e[33] * e[24] - e[17] * e[30] * e[21] + e[17] * e[29] * e[20] + 3. * e[17] * e[35] * e[26] - e[17] * e[31] * e[22] - e[17] * e[28] * e[19] + e[17] * e[34] * e[25] + e[20] * e[27] * e[15] + e[20] * e[9] * e[33] + e[20] * e[28] * e[16] + e[20] * e[10] * e[34] + e[29] * e[18] * e[15] + e[29] * e[9] * e[24] + e[29] * e[19] * e[16] + e[29] * e[10] * e[25] - e[26] * e[27] * e[9] - e[26] * e[30] * e[12] - e[26] * e[28] * e[10] - e[26] * e[31] * e[13] + e[26] * e[33] * e[15] + e[26] * e[34] * e[16] + e[35] * e[24] * e[15] + e[35] * e[25] * e[16] - e[35] * e[18] * e[9] - e[35] * e[21] * e[12] - e[35] * e[19] * e[10] - e[35] * e[22] * e[13] + e[14] * e[30] * e[24] + e[14] * e[21] * e[33] + e[14] * e[31] * e[25] + e[14] * e[22] * e[34] + e[14] * e[32] * e[26] + e[14] * e[23] * e[35] + e[11] * e[27] * e[24] + e[11] * e[18] * e[33] + e[11] * e[28] * e[25] + e[11] * e[19] * e[34] + e[11] * e[29] * e[26] + e[11] * e[20] * e[35] + e[23] * e[30] * e[15] + e[23] * e[12] * e[33] + e[23] * e[32] * e[17] + e[23] * e[31] * e[16] + e[23] * e[13] * e[34] + e[32] * e[21] * e[15] + e[32] * e[12] * e[24]; a[84] = e[6] * e[23] * e[17] + e[6] * e[14] * e[26] + e[6] * e[22] * e[16] + e[6] * e[13] * e[25] + e[0] * e[20] * e[14] + e[0] * e[11] * e[23] + e[0] * e[19] * e[13] + e[0] * e[10] * e[22] - e[12] * e[26] * e[8] - e[12] * e[20] * e[2] - e[12] * e[19] * e[1] + e[12] * e[22] * e[4] - e[12] * e[25] * e[7] + e[12] * e[23] * e[5] - e[21] * e[11] * e[2] - e[21] * e[10] * e[1] + e[21] * e[13] * e[4] - e[21] * e[16] * e[7] + e[21] * e[14] * e[5] - e[21] * e[17] * e[8] + e[15] * e[3] * e[24] + e[15] * e[22] * e[7] + e[15] * e[4] * e[25] + e[15] * e[23] * e[8] + e[15] * e[5] * e[26] - e[3] * e[25] * e[16] - e[3] * e[26] * e[17] - e[3] * e[20] * e[11] - e[3] * e[19] * e[10] + e[24] * e[13] * e[7] + e[24] * e[4] * e[16] + e[24] * e[14] * e[8] + e[24] * e[5] * e[17] + e[9] * e[18] * e[3] + e[9] * e[0] * e[21] + e[9] * e[19] * e[4] + e[9] * e[1] * e[22] + e[9] * e[20] * e[5] + e[9] * e[2] * e[23] + e[18] * e[0] * e[12] + e[18] * e[10] * e[4] + e[18] * e[1] * e[13] + e[18] * e[11] * e[5] + e[18] * e[2] * e[14] + 3. * e[3] * e[21] * e[12] + e[3] * e[23] * e[14] + e[3] * e[22] * e[13] + e[6] * e[21] * e[15] + e[6] * e[12] * e[24]; a[29] = 0.5 * e2[5] * e[17] + 1.5 * e[17] * e2[8] + 0.5 * e[17] * e2[7] + 0.5 * e[17] * e2[6] + 0.5 * e2[2] * e[17] - 0.5 * e[17] * e2[4] - 0.5 * e[17] * e2[0] - 0.5 * e[17] * e2[1] - 0.5 * e[17] * e2[3] + e[11] * e[1] * e[7] + e[11] * e[0] * e[6] + e[11] * e[2] * e[8] + e[5] * e[12] * e[6] + e[5] * e[3] * e[15] + e[5] * e[13] * e[7] + e[5] * e[4] * e[16] + e[5] * e[14] * e[8] + e[14] * e[4] * e[7] + e[14] * e[3] * e[6] + e[8] * e[15] * e[6] + e[8] * e[16] * e[7] - e[8] * e[10] * e[1] - e[8] * e[13] * e[4] - e[8] * e[9] * e[0] - e[8] * e[12] * e[3] + e[2] * e[9] * e[6] + e[2] * e[0] * e[15] + e[2] * e[10] * e[7] + e[2] * e[1] * e[16]; a[54] = e[6] * e[4] * e[34] + e[6] * e[32] * e[8] + e[6] * e[5] * e[35] + e[33] * e[4] * e[7] + e[33] * e[5] * e[8] + e[0] * e[27] * e[3] + e[0] * e[28] * e[4] + e[0] * e[1] * e[31] + e[0] * e[29] * e[5] + e[0] * e[2] * e[32] - e[3] * e[34] * e[7] + e[3] * e[32] * e[5] + e[3] * e[33] * e[6] - e[3] * e[35] * e[8] - e[3] * e[29] * e[2] - e[3] * e[28] * e[1] + e[3] * e[31] * e[4] + e[27] * e[1] * e[4] + e[27] * e[2] * e[5] + e[6] * e[31] * e[7] + 0.5 * e[30] * e2[4] + 0.5 * e[30] * e2[6] + 0.5 * e[30] * e2[5] - 0.5 * e[30] * e2[1] - 0.5 * e[30] * e2[7] - 0.5 * e[30] * e2[2] - 0.5 * e[30] * e2[8] + 0.5 * e2[0] * e[30] + 1.5 * e[30] * e2[3]; a[109] = 0.5 * e2[23] * e[8] + 1.5 * e2[26] * e[8] - 0.5 * e[8] * e2[18] - 0.5 * e[8] * e2[19] - 0.5 * e[8] * e2[22] + 0.5 * e[8] * e2[24] - 0.5 * e[8] * e2[21] + 0.5 * e[8] * e2[25] + 0.5 * e2[20] * e[8] + e[20] * e[18] * e[6] + e[20] * e[0] * e[24] + e[20] * e[19] * e[7] + e[20] * e[1] * e[25] + e[20] * e[2] * e[26] + e[23] * e[21] * e[6] + e[23] * e[3] * e[24] + e[23] * e[22] * e[7] + e[23] * e[4] * e[25] + e[23] * e[5] * e[26] - e[26] * e[21] * e[3] - e[26] * e[19] * e[1] - e[26] * e[22] * e[4] - e[26] * e[18] * e[0] + e[26] * e[25] * e[7] + e[26] * e[24] * e[6] + e[2] * e[19] * e[25] + e[2] * e[18] * e[24] + e[5] * e[22] * e[25] + e[5] * e[21] * e[24]; a[175] = e[19] * e[27] * e[21] + e[19] * e[18] * e[30] + e[19] * e[28] * e[22] + e[19] * e[29] * e[23] + e[19] * e[20] * e[32] + e[28] * e[18] * e[21] + e[28] * e[20] * e[23] + e[22] * e[30] * e[21] + e[22] * e[32] * e[23] + e[25] * e[30] * e[24] + e[25] * e[21] * e[33] + e[25] * e[22] * e[34] + e[25] * e[32] * e[26] + e[25] * e[23] * e[35] + e[34] * e[21] * e[24] + e[34] * e[23] * e[26] - e[22] * e[27] * e[18] - e[22] * e[33] * e[24] - e[22] * e[29] * e[20] - e[22] * e[35] * e[26] + 0.5 * e2[19] * e[31] + 1.5 * e[31] * e2[22] + 0.5 * e[31] * e2[21] + 0.5 * e[31] * e2[23] + 0.5 * e[31] * e2[25] - 0.5 * e[31] * e2[26] - 0.5 * e[31] * e2[18] - 0.5 * e[31] * e2[20] - 0.5 * e[31] * e2[24]; a[15] = -0.5 * e[13] * e2[15] + 0.5 * e[13] * e2[16] + 0.5 * e[13] * e2[12] + e[16] * e[12] * e[15] + 0.5 * e3[13] + e[10] * e[11] * e[14] + 0.5 * e[13] * e2[14] - 0.5 * e[13] * e2[17] - 0.5 * e[13] * e2[11] - 0.5 * e[13] * e2[9] + 0.5 * e2[10] * e[13] + e[10] * e[9] * e[12] + e[16] * e[14] * e[17]; a[95] = -e[13] * e[29] * e[2] - e[31] * e[11] * e[2] - e[31] * e[15] * e[6] - e[31] * e[9] * e[0] + e[31] * e[14] * e[5] + e[31] * e[12] * e[3] - e[31] * e[17] * e[8] + e[16] * e[30] * e[6] + e[16] * e[3] * e[33] + e[16] * e[4] * e[34] + e[16] * e[32] * e[8] + e[16] * e[5] * e[35] - e[4] * e[27] * e[9] + e[4] * e[28] * e[10] - e[4] * e[33] * e[15] - e[4] * e[35] * e[17] - e[4] * e[29] * e[11] + e[34] * e[12] * e[6] + e[34] * e[3] * e[15] + e[34] * e[14] * e[8] + e[34] * e[5] * e[17] + e[10] * e[27] * e[3] + e[10] * e[0] * e[30] + e[10] * e[29] * e[5] + e[10] * e[2] * e[32] + e[28] * e[9] * e[3] + e[28] * e[0] * e[12] + e[28] * e[11] * e[5] + e[28] * e[2] * e[14] + e[4] * e[30] * e[12] + e[4] * e[32] * e[14] + 3. * e[4] * e[31] * e[13] + e[7] * e[30] * e[15] + e[7] * e[12] * e[33] + e[7] * e[32] * e[17] + e[7] * e[14] * e[35] + e[7] * e[31] * e[16] + e[7] * e[13] * e[34] + e[1] * e[27] * e[12] + e[1] * e[9] * e[30] + e[1] * e[29] * e[14] + e[1] * e[11] * e[32] + e[1] * e[28] * e[13] + e[1] * e[10] * e[31] - e[13] * e[27] * e[0] + e[13] * e[32] * e[5] - e[13] * e[33] * e[6] + e[13] * e[30] * e[3] - e[13] * e[35] * e[8]; a[165] = e[25] * e[23] * e[26] + e[19] * e[20] * e[23] + e[19] * e[18] * e[21] + e[25] * e[21] * e[24] + 0.5 * e3[22] + 0.5 * e[22] * e2[23] + 0.5 * e2[19] * e[22] - 0.5 * e[22] * e2[18] - 0.5 * e[22] * e2[24] + 0.5 * e[22] * e2[21] + 0.5 * e[22] * e2[25] - 0.5 * e[22] * e2[20] - 0.5 * e[22] * e2[26]; a[55] = e[34] * e[5] * e[8] + e[1] * e[27] * e[3] + e[1] * e[0] * e[30] + e[1] * e[28] * e[4] + e[1] * e[29] * e[5] + e[1] * e[2] * e[32] - e[4] * e[27] * e[0] + e[4] * e[34] * e[7] + e[4] * e[32] * e[5] - e[4] * e[33] * e[6] + e[4] * e[30] * e[3] - e[4] * e[35] * e[8] - e[4] * e[29] * e[2] + e[28] * e[0] * e[3] + e[28] * e[2] * e[5] + e[7] * e[30] * e[6] + e[7] * e[3] * e[33] + e[7] * e[32] * e[8] + e[7] * e[5] * e[35] + e[34] * e[3] * e[6] + 0.5 * e2[1] * e[31] + 1.5 * e[31] * e2[4] - 0.5 * e[31] * e2[0] - 0.5 * e[31] * e2[6] + 0.5 * e[31] * e2[5] + 0.5 * e[31] * e2[7] + 0.5 * e[31] * e2[3] - 0.5 * e[31] * e2[2] - 0.5 * e[31] * e2[8]; a[85] = e[1] * e[20] * e[14] + e[1] * e[11] * e[23] + e[13] * e[21] * e[3] - e[13] * e[26] * e[8] - e[13] * e[20] * e[2] - e[13] * e[18] * e[0] + e[13] * e[23] * e[5] - e[13] * e[24] * e[6] - e[22] * e[11] * e[2] - e[22] * e[15] * e[6] - e[22] * e[9] * e[0] + e[22] * e[14] * e[5] + e[22] * e[12] * e[3] - e[22] * e[17] * e[8] + e[16] * e[21] * e[6] + e[16] * e[3] * e[24] + e[16] * e[4] * e[25] + e[16] * e[23] * e[8] + e[16] * e[5] * e[26] - e[4] * e[24] * e[15] - e[4] * e[26] * e[17] - e[4] * e[20] * e[11] - e[4] * e[18] * e[9] + e[25] * e[12] * e[6] + e[25] * e[3] * e[15] + e[25] * e[14] * e[8] + e[25] * e[5] * e[17] + e[10] * e[18] * e[3] + e[10] * e[0] * e[21] + e[10] * e[19] * e[4] + e[10] * e[1] * e[22] + e[10] * e[20] * e[5] + e[10] * e[2] * e[23] + e[19] * e[9] * e[3] + e[19] * e[0] * e[12] + e[19] * e[1] * e[13] + e[19] * e[11] * e[5] + e[19] * e[2] * e[14] + e[4] * e[21] * e[12] + e[4] * e[23] * e[14] + 3. * e[4] * e[22] * e[13] + e[7] * e[21] * e[15] + e[7] * e[12] * e[24] + e[7] * e[23] * e[17] + e[7] * e[14] * e[26] + e[7] * e[22] * e[16] + e[7] * e[13] * e[25] + e[1] * e[18] * e[12] + e[1] * e[9] * e[21]; a[75] = e[10] * e[27] * e[12] + e[10] * e[9] * e[30] + e[10] * e[29] * e[14] + e[10] * e[11] * e[32] + e[10] * e[28] * e[13] + e[28] * e[11] * e[14] + e[28] * e[9] * e[12] + e[13] * e[30] * e[12] + e[13] * e[32] * e[14] + e[16] * e[30] * e[15] + e[16] * e[12] * e[33] + e[16] * e[32] * e[17] + e[16] * e[14] * e[35] + e[16] * e[13] * e[34] + e[34] * e[14] * e[17] + e[34] * e[12] * e[15] - e[13] * e[27] * e[9] - e[13] * e[33] * e[15] - e[13] * e[35] * e[17] - e[13] * e[29] * e[11] + 0.5 * e2[10] * e[31] + 0.5 * e[31] * e2[16] - 0.5 * e[31] * e2[9] - 0.5 * e[31] * e2[11] + 0.5 * e[31] * e2[12] - 0.5 * e[31] * e2[15] - 0.5 * e[31] * e2[17] + 0.5 * e[31] * e2[14] + 1.5 * e[31] * e2[13]; a[5] = -0.5 * e[4] * e2[6] - 0.5 * e[4] * e2[0] + e[1] * e[2] * e[5] + 0.5 * e[4] * e2[7] + e[1] * e[0] * e[3] + e[7] * e[5] * e[8] - 0.5 * e[4] * e2[8] + 0.5 * e[4] * e2[3] + 0.5 * e[4] * e2[5] + e[7] * e[3] * e[6] - 0.5 * e[4] * e2[2] + 0.5 * e3[4] + 0.5 * e2[1] * e[4]; a[195] = e[34] * e[32] * e[35] - 0.5 * e[31] * e2[35] + 0.5 * e[31] * e2[34] + 0.5 * e2[28] * e[31] + 0.5 * e3[31] + 0.5 * e[31] * e2[32] + e[34] * e[30] * e[33] - 0.5 * e[31] * e2[27] + 0.5 * e[31] * e2[30] - 0.5 * e[31] * e2[33] - 0.5 * e[31] * e2[29] + e[28] * e[29] * e[32] + e[28] * e[27] * e[30]; a[125] = e[1] * e[27] * e[30] + e[1] * e[29] * e[32] + e[1] * e[28] * e[31] + e[31] * e[30] * e[3] + e[31] * e[32] * e[5] + e[7] * e[30] * e[33] + e[7] * e[32] * e[35] + e[7] * e[31] * e[34] + e[28] * e[27] * e[3] + e[28] * e[0] * e[30] + e[28] * e[29] * e[5] + e[28] * e[2] * e[32] + e[34] * e[30] * e[6] + e[34] * e[3] * e[33] + e[34] * e[32] * e[8] + e[34] * e[5] * e[35] - e[31] * e[27] * e[0] - e[31] * e[33] * e[6] - e[31] * e[35] * e[8] - e[31] * e[29] * e[2] + 0.5 * e[4] * e2[30] + 0.5 * e[4] * e2[32] + 1.5 * e[4] * e2[31] - 0.5 * e[4] * e2[27] + 0.5 * e[4] * e2[28] - 0.5 * e[4] * e2[29] - 0.5 * e[4] * e2[33] + 0.5 * e[4] * e2[34] - 0.5 * e[4] * e2[35]; a[185] = 0.5 * e[22] * e2[30] + 0.5 * e[22] * e2[32] + 1.5 * e[22] * e2[31] + 0.5 * e[22] * e2[34] - 0.5 * e[22] * e2[27] - 0.5 * e[22] * e2[29] - 0.5 * e[22] * e2[33] - 0.5 * e[22] * e2[35] + e[28] * e[18] * e[30] + e[28] * e[29] * e[23] + e[28] * e[20] * e[32] + e[31] * e[30] * e[21] + e[31] * e[32] * e[23] + e[25] * e[30] * e[33] + e[25] * e[32] * e[35] + e[25] * e[31] * e[34] + e[34] * e[30] * e[24] + e[34] * e[21] * e[33] + e[34] * e[32] * e[26] + e[34] * e[23] * e[35] - e[31] * e[27] * e[18] - e[31] * e[33] * e[24] - e[31] * e[29] * e[20] - e[31] * e[35] * e[26] + e[19] * e[27] * e[30] + e[19] * e[29] * e[32] + e[19] * e[28] * e[31] + e[28] * e[27] * e[21] + 0.5 * e2[28] * e[22]; a[155] = e[16] * e[30] * e[33] + e[16] * e[32] * e[35] + e[10] * e[27] * e[30] + e[10] * e[29] * e[32] + e[10] * e[28] * e[31] + e[34] * e[30] * e[15] + e[34] * e[12] * e[33] + e[34] * e[32] * e[17] + e[34] * e[14] * e[35] + e[34] * e[31] * e[16] + e[28] * e[27] * e[12] + e[28] * e[9] * e[30] + e[28] * e[29] * e[14] + e[28] * e[11] * e[32] - e[31] * e[27] * e[9] + e[31] * e[30] * e[12] + e[31] * e[32] * e[14] - e[31] * e[33] * e[15] - e[31] * e[35] * e[17] - e[31] * e[29] * e[11] - 0.5 * e[13] * e2[27] + 0.5 * e[13] * e2[32] + 0.5 * e[13] * e2[28] - 0.5 * e[13] * e2[29] + 1.5 * e[13] * e2[31] - 0.5 * e[13] * e2[33] + 0.5 * e[13] * e2[30] + 0.5 * e[13] * e2[34] - 0.5 * e[13] * e2[35]; a[134] = e[21] * e[23] * e[14] + e[21] * e[22] * e[13] + e[24] * e[21] * e[15] + e[24] * e[23] * e[17] + e[24] * e[14] * e[26] + e[24] * e[22] * e[16] + e[24] * e[13] * e[25] + e[15] * e[22] * e[25] + e[15] * e[23] * e[26] + e[9] * e[19] * e[22] + e[9] * e[18] * e[21] + e[9] * e[20] * e[23] + e[18] * e[20] * e[14] + e[18] * e[11] * e[23] + e[18] * e[19] * e[13] + e[18] * e[10] * e[22] - e[21] * e[25] * e[16] - e[21] * e[26] * e[17] - e[21] * e[20] * e[11] - e[21] * e[19] * e[10] + 1.5 * e2[21] * e[12] + 0.5 * e[12] * e2[24] - 0.5 * e[12] * e2[26] + 0.5 * e[12] * e2[18] + 0.5 * e[12] * e2[23] - 0.5 * e[12] * e2[19] - 0.5 * e[12] * e2[20] + 0.5 * e[12] * e2[22] - 0.5 * e[12] * e2[25]; a[144] = -e[12] * e[29] * e[20] - e[12] * e[35] * e[26] - e[12] * e[28] * e[19] - e[12] * e[34] * e[25] + e[18] * e[29] * e[14] + e[18] * e[11] * e[32] + e[18] * e[28] * e[13] + e[18] * e[10] * e[31] + e[27] * e[20] * e[14] + e[27] * e[11] * e[23] + e[27] * e[19] * e[13] + e[27] * e[10] * e[22] + e[15] * e[30] * e[24] + e[15] * e[21] * e[33] + e[15] * e[31] * e[25] + e[15] * e[22] * e[34] + e[15] * e[32] * e[26] + e[15] * e[23] * e[35] - e[21] * e[28] * e[10] - e[21] * e[34] * e[16] - e[21] * e[35] * e[17] - e[21] * e[29] * e[11] - e[30] * e[25] * e[16] - e[30] * e[26] * e[17] - e[30] * e[20] * e[11] - e[30] * e[19] * e[10] + e[24] * e[32] * e[17] + e[24] * e[14] * e[35] + e[24] * e[31] * e[16] + e[24] * e[13] * e[34] + e[33] * e[23] * e[17] + e[33] * e[14] * e[26] + e[33] * e[22] * e[16] + e[33] * e[13] * e[25] + 3. * e[12] * e[30] * e[21] + e[12] * e[31] * e[22] + e[12] * e[32] * e[23] + e[9] * e[27] * e[21] + e[9] * e[18] * e[30] + e[9] * e[28] * e[22] + e[9] * e[19] * e[31] + e[9] * e[29] * e[23] + e[9] * e[20] * e[32] + e[21] * e[32] * e[14] + e[21] * e[31] * e[13] + e[30] * e[23] * e[14] + e[30] * e[22] * e[13] + e[12] * e[27] * e[18] + e[12] * e[33] * e[24]; a[24] = e[0] * e[11] * e[5] + e[0] * e[2] * e[14] + e[9] * e[1] * e[4] + e[9] * e[0] * e[3] + e[9] * e[2] * e[5] + e[3] * e[13] * e[4] + e[3] * e[14] * e[5] + e[6] * e[3] * e[15] + e[6] * e[13] * e[7] + e[6] * e[4] * e[16] + e[6] * e[14] * e[8] + e[6] * e[5] * e[17] + e[15] * e[4] * e[7] + e[15] * e[5] * e[8] - e[3] * e[11] * e[2] - e[3] * e[10] * e[1] - e[3] * e[16] * e[7] - e[3] * e[17] * e[8] + e[0] * e[10] * e[4] + e[0] * e[1] * e[13] + 1.5 * e[12] * e2[3] + 0.5 * e[12] * e2[4] + 0.5 * e[12] * e2[5] + 0.5 * e[12] * e2[6] + 0.5 * e2[0] * e[12] - 0.5 * e[12] * e2[1] - 0.5 * e[12] * e2[7] - 0.5 * e[12] * e2[2] - 0.5 * e[12] * e2[8]; a[104] = e[21] * e[24] * e[6] + e[0] * e[19] * e[22] + e[0] * e[20] * e[23] + e[24] * e[22] * e[7] + e[24] * e[4] * e[25] + e[24] * e[23] * e[8] + e[24] * e[5] * e[26] + e[6] * e[22] * e[25] + e[6] * e[23] * e[26] + e[18] * e[0] * e[21] + e[18] * e[19] * e[4] + e[18] * e[1] * e[22] + e[18] * e[20] * e[5] + e[18] * e[2] * e[23] + e[21] * e[22] * e[4] + e[21] * e[23] * e[5] - e[21] * e[26] * e[8] - e[21] * e[20] * e[2] - e[21] * e[19] * e[1] - e[21] * e[25] * e[7] + 1.5 * e2[21] * e[3] + 0.5 * e[3] * e2[22] + 0.5 * e[3] * e2[23] + 0.5 * e[3] * e2[24] - 0.5 * e[3] * e2[26] - 0.5 * e[3] * e2[19] - 0.5 * e[3] * e2[20] - 0.5 * e[3] * e2[25] + 0.5 * e2[18] * e[3]; a[76] = e[11] * e[27] * e[12] + e[11] * e[9] * e[30] + e[11] * e[29] * e[14] + e[11] * e[28] * e[13] + e[11] * e[10] * e[31] + e[29] * e[9] * e[12] + e[29] * e[10] * e[13] + e[14] * e[30] * e[12] + e[14] * e[31] * e[13] + e[17] * e[30] * e[15] + e[17] * e[12] * e[33] + e[17] * e[14] * e[35] + e[17] * e[31] * e[16] + e[17] * e[13] * e[34] + e[35] * e[12] * e[15] + e[35] * e[13] * e[16] - e[14] * e[27] * e[9] - e[14] * e[28] * e[10] - e[14] * e[33] * e[15] - e[14] * e[34] * e[16] + 0.5 * e2[11] * e[32] - 0.5 * e[32] * e2[16] - 0.5 * e[32] * e2[9] + 0.5 * e[32] * e2[12] - 0.5 * e[32] * e2[15] + 0.5 * e[32] * e2[17] - 0.5 * e[32] * e2[10] + 1.5 * e[32] * e2[14] + 0.5 * e[32] * e2[13]; a[6] = e[8] * e[3] * e[6] + 0.5 * e2[2] * e[5] - 0.5 * e[5] * e2[0] + 0.5 * e[5] * e2[4] - 0.5 * e[5] * e2[6] + 0.5 * e[5] * e2[8] + e[8] * e[4] * e[7] + 0.5 * e3[5] + e[2] * e[0] * e[3] + 0.5 * e[5] * e2[3] - 0.5 * e[5] * e2[7] + e[2] * e[1] * e[4] - 0.5 * e[5] * e2[1]; a[56] = e[2] * e[27] * e[3] + e[2] * e[0] * e[30] + e[2] * e[28] * e[4] + e[2] * e[1] * e[31] + e[2] * e[29] * e[5] - e[5] * e[27] * e[0] - e[5] * e[34] * e[7] - e[5] * e[33] * e[6] + e[5] * e[30] * e[3] + e[5] * e[35] * e[8] - e[5] * e[28] * e[1] + e[5] * e[31] * e[4] + e[29] * e[1] * e[4] + e[29] * e[0] * e[3] + e[8] * e[30] * e[6] + e[8] * e[3] * e[33] + e[8] * e[31] * e[7] + e[8] * e[4] * e[34] + e[35] * e[4] * e[7] + e[35] * e[3] * e[6] + 0.5 * e2[2] * e[32] + 1.5 * e[32] * e2[5] + 0.5 * e[32] * e2[4] - 0.5 * e[32] * e2[0] - 0.5 * e[32] * e2[6] - 0.5 * e[32] * e2[1] - 0.5 * e[32] * e2[7] + 0.5 * e[32] * e2[3] + 0.5 * e[32] * e2[8]; a[86] = -e[14] * e[19] * e[1] + e[14] * e[22] * e[4] - e[14] * e[18] * e[0] - e[14] * e[25] * e[7] - e[14] * e[24] * e[6] - e[23] * e[10] * e[1] + e[23] * e[13] * e[4] - e[23] * e[16] * e[7] - e[23] * e[15] * e[6] - e[23] * e[9] * e[0] + e[23] * e[12] * e[3] + e[17] * e[21] * e[6] + e[17] * e[3] * e[24] + e[17] * e[22] * e[7] + e[17] * e[4] * e[25] + e[17] * e[5] * e[26] - e[5] * e[24] * e[15] - e[5] * e[25] * e[16] - e[5] * e[18] * e[9] - e[5] * e[19] * e[10] + e[26] * e[12] * e[6] + e[26] * e[3] * e[15] + e[26] * e[13] * e[7] + e[26] * e[4] * e[16] + e[11] * e[18] * e[3] + e[11] * e[0] * e[21] + e[11] * e[19] * e[4] + e[11] * e[1] * e[22] + e[11] * e[20] * e[5] + e[11] * e[2] * e[23] + e[20] * e[9] * e[3] + e[20] * e[0] * e[12] + e[20] * e[10] * e[4] + e[20] * e[1] * e[13] + e[20] * e[2] * e[14] + e[5] * e[21] * e[12] + 3. * e[5] * e[23] * e[14] + e[5] * e[22] * e[13] + e[8] * e[21] * e[15] + e[8] * e[12] * e[24] + e[8] * e[23] * e[17] + e[8] * e[14] * e[26] + e[8] * e[22] * e[16] + e[8] * e[13] * e[25] + e[2] * e[18] * e[12] + e[2] * e[9] * e[21] + e[2] * e[19] * e[13] + e[2] * e[10] * e[22] + e[14] * e[21] * e[3]; a[156] = -0.5 * e[14] * e2[27] + 1.5 * e[14] * e2[32] - 0.5 * e[14] * e2[28] + 0.5 * e[14] * e2[29] + 0.5 * e[14] * e2[31] - 0.5 * e[14] * e2[33] + 0.5 * e[14] * e2[30] - 0.5 * e[14] * e2[34] + 0.5 * e[14] * e2[35] + e[11] * e[27] * e[30] + e[11] * e[29] * e[32] + e[11] * e[28] * e[31] + e[35] * e[30] * e[15] + e[35] * e[12] * e[33] + e[35] * e[32] * e[17] + e[35] * e[31] * e[16] + e[35] * e[13] * e[34] + e[29] * e[27] * e[12] + e[29] * e[9] * e[30] + e[29] * e[28] * e[13] + e[29] * e[10] * e[31] - e[32] * e[27] * e[9] + e[32] * e[30] * e[12] - e[32] * e[28] * e[10] + e[32] * e[31] * e[13] - e[32] * e[33] * e[15] - e[32] * e[34] * e[16] + e[17] * e[30] * e[33] + e[17] * e[31] * e[34]; a[186] = -0.5 * e[23] * e2[33] - 0.5 * e[23] * e2[34] + 0.5 * e2[29] * e[23] + 0.5 * e[23] * e2[30] + 1.5 * e[23] * e2[32] + 0.5 * e[23] * e2[31] + 0.5 * e[23] * e2[35] - 0.5 * e[23] * e2[27] - 0.5 * e[23] * e2[28] + e[32] * e[30] * e[21] + e[32] * e[31] * e[22] + e[26] * e[30] * e[33] + e[26] * e[32] * e[35] + e[26] * e[31] * e[34] + e[35] * e[30] * e[24] + e[35] * e[21] * e[33] + e[35] * e[31] * e[25] + e[35] * e[22] * e[34] - e[32] * e[27] * e[18] - e[32] * e[33] * e[24] - e[32] * e[28] * e[19] - e[32] * e[34] * e[25] + e[20] * e[27] * e[30] + e[20] * e[29] * e[32] + e[20] * e[28] * e[31] + e[29] * e[27] * e[21] + e[29] * e[18] * e[30] + e[29] * e[28] * e[22] + e[29] * e[19] * e[31]; a[126] = e[2] * e[27] * e[30] + e[2] * e[29] * e[32] + e[2] * e[28] * e[31] + e[32] * e[30] * e[3] + e[32] * e[31] * e[4] + e[8] * e[30] * e[33] + e[8] * e[32] * e[35] + e[8] * e[31] * e[34] + e[29] * e[27] * e[3] + e[29] * e[0] * e[30] + e[29] * e[28] * e[4] + e[29] * e[1] * e[31] + e[35] * e[30] * e[6] + e[35] * e[3] * e[33] + e[35] * e[31] * e[7] + e[35] * e[4] * e[34] - e[32] * e[27] * e[0] - e[32] * e[34] * e[7] - e[32] * e[33] * e[6] - e[32] * e[28] * e[1] + 0.5 * e[5] * e2[30] + 1.5 * e[5] * e2[32] + 0.5 * e[5] * e2[31] - 0.5 * e[5] * e2[27] - 0.5 * e[5] * e2[28] + 0.5 * e[5] * e2[29] - 0.5 * e[5] * e2[33] - 0.5 * e[5] * e2[34] + 0.5 * e[5] * e2[35]; a[196] = 0.5 * e[32] * e2[31] + 0.5 * e[32] * e2[35] - 0.5 * e[32] * e2[27] + e[29] * e[27] * e[30] + e[29] * e[28] * e[31] + e[35] * e[30] * e[33] + e[35] * e[31] * e[34] + 0.5 * e2[29] * e[32] + 0.5 * e3[32] - 0.5 * e[32] * e2[33] - 0.5 * e[32] * e2[34] + 0.5 * e[32] * e2[30] - 0.5 * e[32] * e2[28]; a[25] = e[10] * e[1] * e[4] + e[10] * e[0] * e[3] + e[10] * e[2] * e[5] + e[4] * e[12] * e[3] + e[4] * e[14] * e[5] + e[7] * e[12] * e[6] + e[7] * e[3] * e[15] + e[7] * e[4] * e[16] + e[7] * e[14] * e[8] + e[7] * e[5] * e[17] + e[16] * e[3] * e[6] + e[16] * e[5] * e[8] - e[4] * e[11] * e[2] - e[4] * e[15] * e[6] - e[4] * e[9] * e[0] - e[4] * e[17] * e[8] + e[1] * e[9] * e[3] + e[1] * e[0] * e[12] + e[1] * e[11] * e[5] + e[1] * e[2] * e[14] + 1.5 * e[13] * e2[4] + 0.5 * e[13] * e2[3] + 0.5 * e[13] * e2[5] + 0.5 * e[13] * e2[7] + 0.5 * e2[1] * e[13] - 0.5 * e[13] * e2[0] - 0.5 * e[13] * e2[6] - 0.5 * e[13] * e2[2] - 0.5 * e[13] * e2[8]; a[105] = e[25] * e[21] * e[6] + e[25] * e[3] * e[24] + e[25] * e[23] * e[8] + e[25] * e[5] * e[26] + e[7] * e[21] * e[24] + e[7] * e[23] * e[26] + e[19] * e[18] * e[3] + e[19] * e[0] * e[21] + e[19] * e[1] * e[22] + e[19] * e[20] * e[5] + e[19] * e[2] * e[23] + e[22] * e[21] * e[3] + e[22] * e[23] * e[5] - e[22] * e[26] * e[8] - e[22] * e[20] * e[2] - e[22] * e[18] * e[0] + e[22] * e[25] * e[7] - e[22] * e[24] * e[6] + e[1] * e[18] * e[21] + e[1] * e[20] * e[23] + 0.5 * e[4] * e2[25] - 0.5 * e[4] * e2[26] - 0.5 * e[4] * e2[18] - 0.5 * e[4] * e2[20] - 0.5 * e[4] * e2[24] + 0.5 * e2[19] * e[4] + 1.5 * e2[22] * e[4] + 0.5 * e[4] * e2[21] + 0.5 * e[4] * e2[23]; a[135] = e[22] * e[21] * e[12] + e[22] * e[23] * e[14] + e[25] * e[21] * e[15] + e[25] * e[12] * e[24] + e[25] * e[23] * e[17] + e[25] * e[14] * e[26] + e[25] * e[22] * e[16] + e[16] * e[21] * e[24] + e[16] * e[23] * e[26] + e[10] * e[19] * e[22] + e[10] * e[18] * e[21] + e[10] * e[20] * e[23] + e[19] * e[18] * e[12] + e[19] * e[9] * e[21] + e[19] * e[20] * e[14] + e[19] * e[11] * e[23] - e[22] * e[24] * e[15] - e[22] * e[26] * e[17] - e[22] * e[20] * e[11] - e[22] * e[18] * e[9] - 0.5 * e[13] * e2[26] - 0.5 * e[13] * e2[18] + 0.5 * e[13] * e2[23] + 0.5 * e[13] * e2[19] - 0.5 * e[13] * e2[20] - 0.5 * e[13] * e2[24] + 0.5 * e[13] * e2[21] + 1.5 * e2[22] * e[13] + 0.5 * e[13] * e2[25]; a[145] = e[13] * e[30] * e[21] + 3. * e[13] * e[31] * e[22] + e[13] * e[32] * e[23] + e[10] * e[27] * e[21] + e[10] * e[18] * e[30] + e[10] * e[28] * e[22] + e[10] * e[19] * e[31] + e[10] * e[29] * e[23] + e[10] * e[20] * e[32] + e[22] * e[30] * e[12] + e[22] * e[32] * e[14] + e[31] * e[21] * e[12] + e[31] * e[23] * e[14] - e[13] * e[27] * e[18] - e[13] * e[33] * e[24] - e[13] * e[29] * e[20] - e[13] * e[35] * e[26] + e[13] * e[28] * e[19] + e[13] * e[34] * e[25] + e[19] * e[27] * e[12] + e[19] * e[9] * e[30] + e[19] * e[29] * e[14] + e[19] * e[11] * e[32] + e[28] * e[18] * e[12] + e[28] * e[9] * e[21] + e[28] * e[20] * e[14] + e[28] * e[11] * e[23] + e[16] * e[30] * e[24] + e[16] * e[21] * e[33] + e[16] * e[31] * e[25] + e[16] * e[22] * e[34] + e[16] * e[32] * e[26] + e[16] * e[23] * e[35] - e[22] * e[27] * e[9] - e[22] * e[33] * e[15] - e[22] * e[35] * e[17] - e[22] * e[29] * e[11] - e[31] * e[24] * e[15] - e[31] * e[26] * e[17] - e[31] * e[20] * e[11] - e[31] * e[18] * e[9] + e[25] * e[30] * e[15] + e[25] * e[12] * e[33] + e[25] * e[32] * e[17] + e[25] * e[14] * e[35] + e[34] * e[21] * e[15] + e[34] * e[12] * e[24] + e[34] * e[23] * e[17] + e[34] * e[14] * e[26]; a[65] = e[19] * e[11] * e[14] + e[19] * e[9] * e[12] + e[19] * e[10] * e[13] + e[13] * e[21] * e[12] + e[13] * e[23] * e[14] + e[16] * e[21] * e[15] + e[16] * e[12] * e[24] + e[16] * e[23] * e[17] + e[16] * e[14] * e[26] + e[16] * e[13] * e[25] + e[25] * e[14] * e[17] + e[25] * e[12] * e[15] - e[13] * e[24] * e[15] - e[13] * e[26] * e[17] - e[13] * e[20] * e[11] - e[13] * e[18] * e[9] + e[10] * e[18] * e[12] + e[10] * e[9] * e[21] + e[10] * e[20] * e[14] + e[10] * e[11] * e[23] + 1.5 * e[22] * e2[13] + 0.5 * e[22] * e2[14] + 0.5 * e[22] * e2[12] + 0.5 * e[22] * e2[16] + 0.5 * e2[10] * e[22] - 0.5 * e[22] * e2[9] - 0.5 * e[22] * e2[11] - 0.5 * e[22] * e2[15] - 0.5 * e[22] * e2[17]; a[35] = e[13] * e[12] * e[3] + e[13] * e[14] * e[5] + e[16] * e[12] * e[6] + e[16] * e[3] * e[15] + e[16] * e[13] * e[7] + e[16] * e[14] * e[8] + e[16] * e[5] * e[17] + e[7] * e[14] * e[17] + e[7] * e[12] * e[15] + e[1] * e[11] * e[14] + e[1] * e[9] * e[12] + e[1] * e[10] * e[13] + e[10] * e[9] * e[3] + e[10] * e[0] * e[12] + e[10] * e[11] * e[5] + e[10] * e[2] * e[14] - e[13] * e[11] * e[2] - e[13] * e[15] * e[6] - e[13] * e[9] * e[0] - e[13] * e[17] * e[8] + 1.5 * e2[13] * e[4] + 0.5 * e[4] * e2[16] - 0.5 * e[4] * e2[9] - 0.5 * e[4] * e2[11] + 0.5 * e[4] * e2[12] - 0.5 * e[4] * e2[15] - 0.5 * e[4] * e2[17] + 0.5 * e[4] * e2[10] + 0.5 * e[4] * e2[14]; a[45] = e[19] * e[1] * e[4] + e[19] * e[0] * e[3] + e[19] * e[2] * e[5] + e[4] * e[21] * e[3] + e[4] * e[23] * e[5] + e[7] * e[21] * e[6] + e[7] * e[3] * e[24] + e[7] * e[4] * e[25] + e[7] * e[23] * e[8] + e[7] * e[5] * e[26] + e[25] * e[3] * e[6] + e[25] * e[5] * e[8] + e[1] * e[18] * e[3] + e[1] * e[0] * e[21] + e[1] * e[20] * e[5] + e[1] * e[2] * e[23] - e[4] * e[26] * e[8] - e[4] * e[20] * e[2] - e[4] * e[18] * e[0] - e[4] * e[24] * e[6] + 1.5 * e[22] * e2[4] - 0.5 * e[22] * e2[0] - 0.5 * e[22] * e2[6] + 0.5 * e[22] * e2[5] + 0.5 * e[22] * e2[1] + 0.5 * e[22] * e2[7] + 0.5 * e[22] * e2[3] - 0.5 * e[22] * e2[2] - 0.5 * e[22] * e2[8]; a[115] = -e[31] * e[20] * e[2] - e[31] * e[18] * e[0] + e[31] * e[23] * e[5] - e[31] * e[24] * e[6] + e[7] * e[30] * e[24] + e[7] * e[21] * e[33] + e[7] * e[32] * e[26] + e[7] * e[23] * e[35] + e[25] * e[30] * e[6] + e[25] * e[3] * e[33] + e[25] * e[31] * e[7] + e[25] * e[4] * e[34] + e[25] * e[32] * e[8] + e[25] * e[5] * e[35] + e[34] * e[21] * e[6] + e[34] * e[3] * e[24] + e[34] * e[22] * e[7] + e[34] * e[23] * e[8] + e[34] * e[5] * e[26] + e[1] * e[27] * e[21] + e[1] * e[18] * e[30] + e[1] * e[28] * e[22] + e[1] * e[19] * e[31] + e[1] * e[29] * e[23] + e[1] * e[20] * e[32] + e[19] * e[27] * e[3] + e[19] * e[0] * e[30] + e[19] * e[28] * e[4] + e[19] * e[29] * e[5] + e[19] * e[2] * e[32] + e[28] * e[18] * e[3] + e[28] * e[0] * e[21] + e[28] * e[20] * e[5] + e[28] * e[2] * e[23] + e[4] * e[30] * e[21] + 3. * e[4] * e[31] * e[22] + e[4] * e[32] * e[23] - e[4] * e[27] * e[18] - e[4] * e[33] * e[24] - e[4] * e[29] * e[20] - e[4] * e[35] * e[26] - e[22] * e[27] * e[0] + e[22] * e[32] * e[5] - e[22] * e[33] * e[6] + e[22] * e[30] * e[3] - e[22] * e[35] * e[8] - e[22] * e[29] * e[2] + e[31] * e[21] * e[3] - e[31] * e[26] * e[8]; } colmap-3.9.1/src/colmap/estimators/essential_matrix_test.cc000066400000000000000000000133261454702036400241660ustar00rootroot00000000000000// 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. #include "colmap/geometry/essential_matrix.h" #include "colmap/estimators/essential_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/math/random.h" #include "colmap/optim/ransac.h" #include "colmap/sensor/models.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(EssentialMatrix, FivePoint) { const double points1_raw[] = { 0.4964, 1.0577, 0.3650, -0.0919, -0.5412, 0.0159, -0.5239, 0.9467, 0.3467, 0.5301, 0.2797, 0.0012, -0.1986, 0.0460, -0.1622, 0.5347, 0.0796, 0.2379, -0.3946, 0.7969, 0.2, 0.7, 0.6, 0.3}; const double points2_raw[] = { 0.7570, 2.7340, 0.3961, 0.6981, -0.6014, 0.7110, -0.7385, 2.2712, 0.4177, 1.2132, 0.3052, 0.4835, -0.2171, 0.5057, -0.2059, 1.1583, 0.0946, 0.7013, -0.6236, 3.0253, 0.5, 0.9, 0.9, 0.2}; const size_t num_points = 12; std::vector points1(num_points); std::vector points2(num_points); for (size_t i = 0; i < num_points; ++i) { points1[i] = Eigen::Vector2d(points1_raw[2 * i], points1_raw[2 * i + 1]); points2[i] = Eigen::Vector2d(points2_raw[2 * i], points2_raw[2 * i + 1]); } // Enforce repeatable tests SetPRNGSeed(0); RANSACOptions options; options.max_error = 0.02; options.confidence = 0.9999; options.min_inlier_ratio = 0.1; RANSAC ransac(options); const auto report = ransac.Estimate(points1, points2); std::vector residuals; EssentialMatrixFivePointEstimator::Residuals( points1, points2, report.model, &residuals); for (size_t i = 0; i < 10; ++i) { EXPECT_LE(residuals[i], options.max_error * options.max_error); } EXPECT_FALSE(report.inlier_mask[10]); EXPECT_FALSE(report.inlier_mask[11]); } TEST(EssentialMatrix, EightPoint) { const double points1_raw[] = {1.839035, 1.924743, 0.543582, 0.375221, 0.473240, 0.142522, 0.964910, 0.598376, 0.102388, 0.140092, 15.994343, 9.622164, 0.285901, 0.430055, 0.091150, 0.254594}; const double points2_raw[] = { 1.002114, 1.129644, 1.521742, 1.846002, 1.084332, 0.275134, 0.293328, 0.588992, 0.839509, 0.087290, 1.779735, 1.116857, 0.878616, 0.602447, 0.642616, 1.028681, }; const size_t kNumPoints = 8; std::vector points1(kNumPoints); std::vector points2(kNumPoints); for (size_t i = 0; i < kNumPoints; ++i) { points1[i] = Eigen::Vector2d(points1_raw[2 * i], points1_raw[2 * i + 1]); points2[i] = Eigen::Vector2d(points2_raw[2 * i], points2_raw[2 * i + 1]); } EssentialMatrixEightPointEstimator estimator; std::vector models; estimator.Estimate(points1, points2, &models); ASSERT_EQ(models.size(), 1); const Eigen::Matrix3d& E = models[0]; // Reference values. EXPECT_TRUE(std::abs(E(0, 0) - -0.0368602) < 1e-5); EXPECT_TRUE(std::abs(E(0, 1) - 0.265019) < 1e-5); EXPECT_TRUE(std::abs(E(0, 2) - -0.0625948) < 1e-5); EXPECT_TRUE(std::abs(E(1, 0) - -0.299679) < 1e-5); EXPECT_TRUE(std::abs(E(1, 1) - -0.110667) < 1e-5); EXPECT_TRUE(std::abs(E(1, 2) - 0.147114) < 1e-5); EXPECT_TRUE(std::abs(E(2, 0) - 0.169381) < 1e-5); EXPECT_TRUE(std::abs(E(2, 1) - -0.21072) < 1e-5); EXPECT_TRUE(std::abs(E(2, 2) - -0.00401306) < 1e-5); // Check that the internal constraint is satisfied (two singular values equal // and one zero). Eigen::JacobiSVD svd(E); Eigen::Vector3d s = svd.singularValues(); EXPECT_TRUE(std::abs(s(0) - s(1)) < 1e-5); EXPECT_TRUE(std::abs(s(2)) < 1e-5); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/euclidean_transform.h000066400000000000000000000044251454702036400234420ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/sim3.h" namespace colmap { // N-D Euclidean transform estimator from corresponding point pairs in the // source and destination coordinate systems. // // This algorithm is based on the following paper: // // S. Umeyama. Least-Squares Estimation of Transformation Parameters // Between Two Point Patterns. IEEE Transactions on Pattern Analysis and // Machine Intelligence, Volume 13 Issue 4, Page 376-380, 1991. // http://www.stanford.edu/class/cs273/refs/umeyama.pdf // // and uses the Eigen implementation. template using EuclideanTransformEstimator = SimilarityTransformEstimator; } // namespace colmap colmap-3.9.1/src/colmap/estimators/fundamental_matrix.cc000066400000000000000000000167151454702036400234430ustar00rootroot00000000000000// 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. #include "colmap/estimators/fundamental_matrix.h" #include "colmap/estimators/utils.h" #include "colmap/math/polynomial.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include #include #include #include #include #include namespace colmap { void FundamentalMatrixSevenPointEstimator::Estimate( const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_EQ(points1.size(), 7); CHECK_EQ(points2.size(), 7); CHECK(models != nullptr); models->clear(); // Note that no normalization of the points is necessary here. // Setup system of equations: [points2(i,:), 1]' * F * [points1(i,:), 1]'. Eigen::Matrix A; for (size_t i = 0; i < 7; ++i) { const double x0 = points1[i](0); const double y0 = points1[i](1); const double x1 = points2[i](0); const double y1 = points2[i](1); A(i, 0) = x1 * x0; A(i, 1) = x1 * y0; A(i, 2) = x1; A(i, 3) = y1 * x0; A(i, 4) = y1 * y0; A(i, 5) = y1; A(i, 6) = x0; A(i, 7) = y0; A(i, 8) = 1; } // 9 unknowns with 7 equations, so we have 2D null space. Eigen::JacobiSVD> svd(A, Eigen::ComputeFullV); const Eigen::Matrix& f = svd.matrixV(); Eigen::Matrix f1 = f.col(7); Eigen::Matrix f2 = f.col(8); f1 -= f2; // Normalize, such that lambda + mu = 1 // and add constraint det(F) = det(lambda * f1 + (1 - lambda) * f2). const double t0 = f1(4) * f1(8) - f1(5) * f1(7); const double t1 = f1(3) * f1(8) - f1(5) * f1(6); const double t2 = f1(3) * f1(7) - f1(4) * f1(6); const double t3 = f2(4) * f2(8) - f2(5) * f2(7); const double t4 = f2(3) * f2(8) - f2(5) * f2(6); const double t5 = f2(3) * f2(7) - f2(4) * f2(6); Eigen::Vector4d coeffs; coeffs(0) = f1(0) * t0 - f1(1) * t1 + f1(2) * t2; coeffs(1) = f2(0) * t0 - f2(1) * t1 + f2(2) * t2 - f2(3) * (f1(1) * f1(8) - f1(2) * f1(7)) + f2(4) * (f1(0) * f1(8) - f1(2) * f1(6)) - f2(5) * (f1(0) * f1(7) - f1(1) * f1(6)) + f2(6) * (f1(1) * f1(5) - f1(2) * f1(4)) - f2(7) * (f1(0) * f1(5) - f1(2) * f1(3)) + f2(8) * (f1(0) * f1(4) - f1(1) * f1(3)); coeffs(2) = f1(0) * t3 - f1(1) * t4 + f1(2) * t5 - f1(3) * (f2(1) * f2(8) - f2(2) * f2(7)) + f1(4) * (f2(0) * f2(8) - f2(2) * f2(6)) - f1(5) * (f2(0) * f2(7) - f2(1) * f2(6)) + f1(6) * (f2(1) * f2(5) - f2(2) * f2(4)) - f1(7) * (f2(0) * f2(5) - f2(2) * f2(3)) + f1(8) * (f2(0) * f2(4) - f2(1) * f2(3)); coeffs(3) = f2(0) * t3 - f2(1) * t4 + f2(2) * t5; Eigen::VectorXd roots_real; Eigen::VectorXd roots_imag; if (!FindPolynomialRootsCompanionMatrix(coeffs, &roots_real, &roots_imag)) { return; } models->reserve(roots_real.size()); for (Eigen::VectorXd::Index i = 0; i < roots_real.size(); ++i) { const double kMaxRootImag = 1e-10; if (std::abs(roots_imag(i)) > kMaxRootImag) { continue; } const double lambda = roots_real(i); const double mu = 1; Eigen::MatrixXd F = lambda * f1 + mu * f2; F.resize(3, 3); const double kEps = 1e-10; if (std::abs(F(2, 2)) < kEps) { continue; } F /= F(2, 2); models->push_back(F.transpose()); } } void FundamentalMatrixSevenPointEstimator::Residuals( const std::vector& points1, const std::vector& points2, const M_t& F, std::vector* residuals) { ComputeSquaredSampsonError(points1, points2, F, residuals); } void FundamentalMatrixEightPointEstimator::Estimate( const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_EQ(points1.size(), points2.size()); CHECK(models != nullptr); models->clear(); // Center and normalize image points for better numerical stability. std::vector normed_points1; std::vector normed_points2; Eigen::Matrix3d normed_from_orig1; Eigen::Matrix3d normed_from_orig2; CenterAndNormalizeImagePoints(points1, &normed_points1, &normed_from_orig1); CenterAndNormalizeImagePoints(points2, &normed_points2, &normed_from_orig2); // Setup homogeneous linear equation as x2' * F * x1 = 0. Eigen::Matrix cmatrix(points1.size(), 9); for (size_t i = 0; i < points1.size(); ++i) { cmatrix.block<1, 3>(i, 0) = normed_points1[i].homogeneous(); cmatrix.block<1, 3>(i, 0) *= normed_points2[i].x(); cmatrix.block<1, 3>(i, 3) = normed_points1[i].homogeneous(); cmatrix.block<1, 3>(i, 3) *= normed_points2[i].y(); cmatrix.block<1, 3>(i, 6) = normed_points1[i].homogeneous(); } // Solve for the nullspace of the constraint matrix. Eigen::JacobiSVD> cmatrix_svd( cmatrix, Eigen::ComputeFullV); const Eigen::VectorXd cmatrix_nullspace = cmatrix_svd.matrixV().col(8); const Eigen::Map ematrix_t(cmatrix_nullspace.data()); // Enforcing the internal constraint that two singular values must non-zero // and one must be zero. Eigen::JacobiSVD fmatrix_svd( ematrix_t.transpose(), Eigen::ComputeFullU | Eigen::ComputeFullV); Eigen::Vector3d singular_values = fmatrix_svd.singularValues(); singular_values(2) = 0.0; const Eigen::Matrix3d F = fmatrix_svd.matrixU() * singular_values.asDiagonal() * fmatrix_svd.matrixV().transpose(); models->resize(1); (*models)[0] = normed_from_orig2.transpose() * F * normed_from_orig1; } void FundamentalMatrixEightPointEstimator::Residuals( const std::vector& points1, const std::vector& points2, const M_t& E, std::vector* residuals) { ComputeSquaredSampsonError(points1, points2, E, residuals); } } // namespace colmap colmap-3.9.1/src/colmap/estimators/fundamental_matrix.h000066400000000000000000000121761454702036400233020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/estimators/homography_matrix.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Fundamental matrix estimator from corresponding point pairs. // // This algorithm solves the 7-Point problem and is based on the following // paper: // // Zhengyou Zhang and T. Kanade, Determining the Epipolar Geometry and its // Uncertainty: A Review, International Journal of Computer Vision, 1998. // http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.33.4540 class FundamentalMatrixSevenPointEstimator { public: typedef Eigen::Vector2d X_t; typedef Eigen::Vector2d Y_t; typedef Eigen::Matrix3d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 7; // Estimate either 1 or 3 possible fundamental matrix solutions from a set of // corresponding points. // // The number of corresponding points must be exactly 7. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points // // @return Up to 4 solutions as a vector of 3x3 fundamental matrices. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Calculate the residuals of a set of corresponding points and a given // fundamental matrix. // // Residuals are defined as the squared Sampson error. // // @param points1 First set of corresponding points as Nx2 matrix. // @param points2 Second set of corresponding points as Nx2 matrix. // @param F 3x3 fundamental matrix. // @param residuals Output vector of residuals. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& F, std::vector* residuals); }; // Fundamental matrix estimator from corresponding point pairs. // // This algorithm solves the 8-Point problem based on the following paper: // // Hartley and Zisserman, Multiple View Geometry, algorithm 11.1, page 282. class FundamentalMatrixEightPointEstimator { public: typedef Eigen::Vector2d X_t; typedef Eigen::Vector2d Y_t; typedef Eigen::Matrix3d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 8; // Estimate fundamental matrix solutions from a set of corresponding points. // // The number of corresponding points must be at least 8. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points // // @return Single solution as a vector of 3x3 fundamental matrices. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Calculate the residuals of a set of corresponding points and a given // fundamental matrix. // // Residuals are defined as the squared Sampson error. // // @param points1 First set of corresponding points as Nx2 matrix. // @param points2 Second set of corresponding points as Nx2 matrix. // @param F 3x3 fundamental matrix. // @param residuals Output vector of residuals. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& F, std::vector* residuals); }; } // namespace colmap colmap-3.9.1/src/colmap/estimators/fundamental_matrix_test.cc000066400000000000000000000135361454702036400245000ustar00rootroot00000000000000// 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. #include "colmap/estimators/fundamental_matrix.h" #include namespace colmap { namespace { TEST(FundamentalMatrix, SevenPoint) { const double points1_raw[] = {0.4964, 1.0577, 0.3650, -0.0919, -0.5412, 0.0159, -0.5239, 0.9467, 0.3467, 0.5301, 0.2797, 0.0012, -0.1986, 0.0460}; const double points2_raw[] = {0.7570, 2.7340, 0.3961, 0.6981, -0.6014, 0.7110, -0.7385, 2.2712, 0.4177, 1.2132, 0.3052, 0.4835, -0.2171, 0.5057}; const size_t kNumPoints = 7; std::vector points1(kNumPoints); std::vector points2(kNumPoints); for (size_t i = 0; i < kNumPoints; ++i) { points1[i] = Eigen::Vector2d(points1_raw[2 * i], points1_raw[2 * i + 1]); points2[i] = Eigen::Vector2d(points2_raw[2 * i], points2_raw[2 * i + 1]); } FundamentalMatrixSevenPointEstimator estimator; std::vector models; estimator.Estimate(points1, points2, &models); ASSERT_EQ(models.size(), 1); const auto& F = models[0]; // Reference values obtained from Matlab. EXPECT_NEAR(F(0, 0), 4.81441976, 1e-6); EXPECT_NEAR(F(0, 1), -8.16978909, 1e-6); EXPECT_NEAR(F(0, 2), 6.73133404, 1e-6); EXPECT_NEAR(F(1, 0), 5.16247992, 1e-6); EXPECT_NEAR(F(1, 1), 0.19325606, 1e-6); EXPECT_NEAR(F(1, 2), -2.87239381, 1e-6); EXPECT_NEAR(F(2, 0), -9.92570126, 1e-6); EXPECT_NEAR(F(2, 1), 3.64159554, 1e-6); EXPECT_NEAR(F(2, 2), 1., 1e-6); } TEST(FundamentalMatrix, EightPoint) { const double points1_raw[] = {1.839035, 1.924743, 0.543582, 0.375221, 0.473240, 0.142522, 0.964910, 0.598376, 0.102388, 0.140092, 15.994343, 9.622164, 0.285901, 0.430055, 0.091150, 0.254594}; const double points2_raw[] = { 1.002114, 1.129644, 1.521742, 1.846002, 1.084332, 0.275134, 0.293328, 0.588992, 0.839509, 0.087290, 1.779735, 1.116857, 0.878616, 0.602447, 0.642616, 1.028681, }; const size_t kNumPoints = 8; std::vector points1(kNumPoints); std::vector points2(kNumPoints); for (size_t i = 0; i < kNumPoints; ++i) { points1[i] = Eigen::Vector2d(points1_raw[2 * i], points1_raw[2 * i + 1]); points2[i] = Eigen::Vector2d(points2_raw[2 * i], points2_raw[2 * i + 1]); } FundamentalMatrixEightPointEstimator estimator; std::vector models; estimator.Estimate(points1, points2, &models); ASSERT_EQ(models.size(), 1); const auto& F = models[0]; // Reference values obtained from Matlab. EXPECT_TRUE(std::abs(F(0, 0) - -0.217859) < 1e-5); EXPECT_TRUE(std::abs(F(0, 1) - 0.419282) < 1e-5); EXPECT_TRUE(std::abs(F(0, 2) - -0.0343075) < 1e-5); EXPECT_TRUE(std::abs(F(1, 0) - -0.0717941) < 1e-5); EXPECT_TRUE(std::abs(F(1, 1) - 0.0451643) < 1e-5); EXPECT_TRUE(std::abs(F(1, 2) - 0.0216073) < 1e-5); EXPECT_TRUE(std::abs(F(2, 0) - 0.248062) < 1e-5); EXPECT_TRUE(std::abs(F(2, 1) - -0.429478) < 1e-5); EXPECT_TRUE(std::abs(F(2, 2) - 0.0221019) < 1e-5); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_absolute_pose.cc000066400000000000000000000263561454702036400250000ustar00rootroot00000000000000// 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. #include "colmap/estimators/generalized_absolute_pose.h" #include "colmap/estimators/generalized_absolute_pose_coeffs.h" #include "colmap/math/polynomial.h" #include "colmap/util/logging.h" #include namespace colmap { namespace { // Check whether the rays are close to parallel. bool CheckParallelRays(const Eigen::Vector3d& ray1, const Eigen::Vector3d& ray2, const Eigen::Vector3d& ray3) { const double kParallelThreshold = 1e-5; return ray1.cross(ray2).isApproxToConstant(0, kParallelThreshold) && ray1.cross(ray3).isApproxToConstant(0, kParallelThreshold); } // Check whether the points are close to collinear. bool CheckCollinearPoints(const Eigen::Vector3d& X1, const Eigen::Vector3d& X2, const Eigen::Vector3d& X3) { const double kMinNonCollinearity = 1e-5; const Eigen::Vector3d X12 = X2 - X1; const double non_collinearity_measure = X12.cross(X1 - X3).squaredNorm() / X12.squaredNorm(); return non_collinearity_measure < kMinNonCollinearity; } Eigen::Vector6d ComposePlueckerLine(const Rigid3d& rig_from_cam, const Eigen::Vector3d& ray_in_cam) { const Eigen::Vector3d ray_in_rig = (rig_from_cam.rotation * ray_in_cam).normalized(); Eigen::Vector6d pluecker; pluecker << ray_in_rig, rig_from_cam.translation.cross(ray_in_rig); return pluecker; } Eigen::Vector3d PointFromPlueckerLineAndDepth(const Eigen::Vector6d& pluecker, const double depth) { return pluecker.head<3>().cross(pluecker.tail<3>()) + depth * pluecker.head<3>(); } // Compute the coefficients from the system of 3 equations, nonlinear in the // depth of the points. Inputs are three Pluecker lines and the locations of // their corresponding points in 3D. The system of equations comes from the // distance constraints between 3D points: // // || f_i - f_j ||^2 = || (q_i x q_i' + lambda_i * q_i) - // (q_j x q_j' + lambda_j * q_j) ||^2 // // where [q_i; q_i'] is the Pluecker coordinate of bearing i and f_i is the // coordinate of the corresponding 3D point in the global coordinate system. A // 3D point in the local camera coordinate system along this line is // parameterized through the depth scalar lambda_i as: // // B_fi = q_i x q_i' + lambda_i * q_i. // Eigen::Matrix ComputePolynomialCoefficients( const std::vector& plueckers, const std::vector& points3D) { CHECK_EQ(plueckers.size(), 3); CHECK_EQ(points3D.size(), 3); Eigen::Matrix K; const std::array is = {{0, 0, 1}}; const std::array js = {{1, 2, 2}}; for (int k = 0; k < 3; ++k) { const int i = is[k]; const int j = js[k]; const Eigen::Vector3d moment_difference = plueckers[i].head<3>().cross(plueckers[i].tail<3>()) - plueckers[j].head<3>().cross(plueckers[j].tail<3>()); K(k, 0) = 1; K(k, 1) = -2 * plueckers[i].head<3>().dot(plueckers[j].head<3>()); K(k, 2) = 2 * moment_difference.dot(plueckers[i].head<3>()); K(k, 3) = 1; K(k, 4) = -2 * moment_difference.dot(plueckers[j].head<3>()); K(k, 5) = moment_difference.squaredNorm() - (points3D[i] - points3D[j]).squaredNorm(); } return K; } // Solve quadratics of the form: x^2 + bx + c = 0. int SolveQuadratic(const double b, const double c, double* roots) { const double delta = b * b - 4 * c; // Do not allow complex solutions. if (delta >= 0) { const double sqrt_delta = std::sqrt(delta); roots[0] = -0.5 * (b + sqrt_delta); roots[1] = -0.5 * (b - sqrt_delta); return 2; } else { return 0; } } // Given lambda_j, return the values for lambda_i, where: // k1 lambda_i^2 + (k2 lambda_j + k3) lambda_i // + k4 lambda_j^2 + k5 lambda_j + k6 = 0. void ComputeLambdaValues(const Eigen::Matrix::ConstRowXpr& k, const double lambda_j, std::vector* lambdas_i) { // Note that we solve x^2 + bx + c = 0, since k(0) is one. double roots[2]; const int num_solutions = SolveQuadratic(k(1) * lambda_j + k(2), lambda_j * (k(3) * lambda_j + k(4)) + k(5), roots); for (int i = 0; i < num_solutions; ++i) { if (roots[i] > 0) { lambdas_i->push_back(roots[i]); } } } // Given the coefficients of the polynomial system return the depths of the // points along the Pluecker lines. Use Sylvester resultant to get and 8th // degree polynomial for lambda_3 and back-substite in the original equations. std::vector ComputeDepthsSylvester( const Eigen::Matrix& K) { const Eigen::Matrix coeffs = ComputeDepthsSylvesterCoeffs(K); Eigen::VectorXd roots_real; Eigen::VectorXd roots_imag; if (!FindPolynomialRootsCompanionMatrix(coeffs, &roots_real, &roots_imag)) { return std::vector(); } // Back-substitute every lambda_3 to the system of equations. std::vector depths; depths.reserve(roots_real.size()); for (Eigen::VectorXd::Index i = 0; i < roots_real.size(); ++i) { const double kMaxRootImagRatio = 1e-3; if (std::abs(roots_imag(i)) > kMaxRootImagRatio * std::abs(roots_real(i))) { continue; } const double lambda_3 = roots_real(i); if (lambda_3 <= 0) { continue; } std::vector lambdas_2; ComputeLambdaValues(K.row(2), lambda_3, &lambdas_2); // Now we have two depths, lambda_2 and lambda_3. From the two remaining // equations, we must get the same lambda_1, otherwise the solution is // invalid. for (const double lambda_2 : lambdas_2) { std::vector lambdas_1_1; ComputeLambdaValues(K.row(0), lambda_2, &lambdas_1_1); std::vector lambdas_1_2; ComputeLambdaValues(K.row(1), lambda_3, &lambdas_1_2); for (const double lambda_1_1 : lambdas_1_1) { for (const double lambda_1_2 : lambdas_1_2) { const double kMaxLambdaRatio = 1e-2; if (std::abs(lambda_1_1 - lambda_1_2) < kMaxLambdaRatio * std::max(lambda_1_1, lambda_1_2)) { const double lambda_1 = (lambda_1_1 + lambda_1_2) / 2; depths.emplace_back(lambda_1, lambda_2, lambda_3); } } } } } return depths; } } // namespace void GP3PEstimator::Estimate(const std::vector& points2D, const std::vector& points3D, std::vector* models) { CHECK_EQ(points2D.size(), 3); CHECK_EQ(points3D.size(), 3); CHECK(models != nullptr); models->clear(); if (CheckCollinearPoints(points3D[0], points3D[1], points3D[2])) { return; } // Transform 2D points into compact Pluecker line representation. std::vector plueckers(3); for (size_t i = 0; i < 3; ++i) { plueckers[i] = ComposePlueckerLine(Inverse(points2D[i].cam_from_rig), points2D[i].ray_in_cam); } if (CheckParallelRays(plueckers[0].head<3>(), plueckers[1].head<3>(), plueckers[2].head<3>())) { return; } // Compute the coefficients k1, k2, k3 using Eq. 4. const Eigen::Matrix K = ComputePolynomialCoefficients(plueckers, points3D); // Compute the depths along the Pluecker lines of the observations. const std::vector depths = ComputeDepthsSylvester(K); if (depths.empty()) { return; } // For all valid depth values, compute the transformation between points in // the camera and the world frame. This uses Umeyama's method rather than the // algorithm proposed in the paper, since Umeyama's method is numerically more // stable and this part is not a bottleneck. Eigen::Matrix3d points3D_in_world; for (size_t i = 0; i < 3; ++i) { points3D_in_world.col(i) = points3D[i]; } models->resize(depths.size()); for (size_t i = 0; i < depths.size(); ++i) { Eigen::Matrix3d points3D_in_rig; for (size_t j = 0; j < 3; ++j) { points3D_in_rig.col(j) = PointFromPlueckerLineAndDepth(plueckers[j], depths[i][j]); } const Eigen::Matrix4d rig_from_world = Eigen::umeyama(points3D_in_world, points3D_in_rig, false); (*models)[i] = Rigid3d(Eigen::Quaterniond(rig_from_world.topLeftCorner<3, 3>()), rig_from_world.topRightCorner<3, 1>()); } } void GP3PEstimator::Residuals(const std::vector& points2D, const std::vector& points3D, const M_t& rig_from_world, std::vector* residuals) { CHECK_EQ(points2D.size(), points3D.size()); residuals->resize(points2D.size(), 0); for (size_t i = 0; i < points2D.size(); ++i) { const Eigen::Vector3d point3D_in_cam = points2D[i].cam_from_rig * (rig_from_world * points3D[i]); // Check if 3D point is in front of camera. if (point3D_in_cam.z() > std::numeric_limits::epsilon()) { if (residual_type == ResidualType::CosineDistance) { const double cosine_dist = 1 - point3D_in_cam.normalized().dot(points2D[i].ray_in_cam); (*residuals)[i] = cosine_dist * cosine_dist; } else if (residual_type == ResidualType::ReprojectionError) { (*residuals)[i] = (point3D_in_cam.hnormalized() - points2D[i].ray_in_cam.hnormalized()) .squaredNorm(); } else { LOG(FATAL) << "Invalid residual type"; } } else { (*residuals)[i] = std::numeric_limits::max(); } } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_absolute_pose.h000066400000000000000000000073171454702036400246360ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Solver for the Generalized P3P problem (NP3P or GP3P), based on: // // Lee, Gim Hee, et al. "Minimal solutions for pose estimation of a // multi-camera system." Robotics Research. Springer International // Publishing, 2016. 521-538. // // This class is based on an original implementation by Federico Camposeco. class GP3PEstimator { public: // The generalized image observations, which is composed of the relative pose // of a camera in the generalized camera and a ray in the camera frame. struct X_t { Rigid3d cam_from_rig; Eigen::Vector3d ray_in_cam; }; // The observed 3D feature points in the world frame. typedef Eigen::Vector3d Y_t; // The estimated rig_from_world pose of the generalized camera. typedef Rigid3d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 3; enum class ResidualType { CosineDistance, ReprojectionError, }; // Whether to compute the cosine similarity or the reprojection error. // [WARNING] The reprojection error being in normalized coordinates, // the unique error threshold of RANSAC corresponds to different pixel values // in the different cameras of the rig if they have different intrinsics. ResidualType residual_type = ResidualType::CosineDistance; // Estimate the most probable solution of the GP3P problem from a set of // three 2D-3D point correspondences. static void Estimate(const std::vector& points2D, const std::vector& points3D, std::vector* models); // Calculate the squared cosine distance error between the rays given a set of // 2D-3D point correspondences and the rig pose of the generalized camera. void Residuals(const std::vector& points2D, const std::vector& points3D, const M_t& rig_from_world, std::vector* residuals); }; } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_absolute_pose_coeffs.cc000066400000000000000000005456071454702036400263320ustar00rootroot00000000000000// 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. #include "colmap/estimators/generalized_absolute_pose_coeffs.h" namespace colmap { Eigen::Matrix ComputeDepthsSylvesterCoeffs( const Eigen::Matrix& K) { Eigen::Matrix coeffs; const double k12 = K(0, 1); const double k13 = K(0, 2); const double k15 = K(0, 4); const double k16 = K(0, 5); const double k22 = K(1, 1); const double k23 = K(1, 2); const double k25 = K(1, 4); const double k26 = K(1, 5); const double k32 = K(2, 1); const double k33 = K(2, 2); const double k35 = K(2, 4); const double k36 = K(2, 5); const double t2 = k12 * k12; const double t3 = k22 * k22; const double t4 = k32 * k32; const double t5 = t2 * t2; const double t6 = t4 * t4; const double t7 = t3 * t3; const double t8 = k13 * k13; const double t9 = k15 * k15; const double t10 = k23 * k23; const double t11 = k25 * k25; const double t12 = k33 * k33; const double t13 = k35 * k35; const double t14 = k16 * k16; const double t15 = k26 * k26; const double t16 = k36 * k36; const double t17 = t8 * t8; const double t18 = t9 * t9; const double t19 = t12 * t12; const double t20 = t10 * t10; coeffs(0) = t2 * -8.0 - t3 * 8.0 - t4 * 8.0 + t5 + t6 + t7 + t2 * t3 * 2.0 + t2 * t4 * 2.0 + t3 * t4 * 2.0 - k12 * k22 * k32 * 8.0 + t2 * t3 * t4 + k12 * k22 * k32 * t2 * 2.0 + k12 * k22 * k32 * t3 * 2.0 + k12 * k22 * k32 * t4 * 2.0 + 1.6E1; coeffs(1) = k25 * 3.2E1 + k35 * 3.2E1 - k22 * k23 * 1.6E1 - k32 * k33 * 1.6E1 - k25 * t2 * 1.6E1 - k25 * t3 * 8.0 - k25 * t4 * 1.6E1 + k25 * t5 * 2.0 + k25 * t6 * 2.0 - k35 * t2 * 1.6E1 - k35 * t3 * 1.6E1 - k35 * t4 * 8.0 + k35 * t5 * 2.0 + k35 * t7 * 2.0 + k12 * k15 * k22 * 8.0 + k12 * k13 * k32 * 8.0 - k12 * k22 * k33 * 8.0 - k12 * k23 * k32 * 8.0 + k13 * k22 * t4 * 4.0 - k13 * k22 * t6 + k22 * k23 * t2 * 4.0 + k22 * k23 * t3 * 4.0 + k22 * k23 * t4 * 4.0 + k15 * k32 * t3 * 4.0 - k15 * k32 * t7 + k32 * k33 * t2 * 4.0 + k32 * k33 * t3 * 4.0 + k32 * k33 * t4 * 4.0 + k25 * t2 * t3 * 2.0 + k25 * t2 * t4 * 4.0 + k25 * t3 * t4 * 2.0 + k35 * t2 * t3 * 4.0 + k35 * t2 * t4 * 2.0 + k35 * t3 * t4 * 2.0 - k12 * k22 * k25 * k32 * 1.2E1 - k12 * k22 * k32 * k35 * 1.2E1 - k12 * k15 * k22 * t2 * 2.0 - k12 * k15 * k22 * t3 * 2.0 - k12 * k15 * k22 * t4 * 2.0 - k12 * k13 * k32 * t2 * 2.0 - k12 * k13 * k32 * t3 * 2.0 - k12 * k13 * k32 * t4 * 2.0 + k12 * k22 * k33 * t2 * 2.0 + k12 * k23 * k32 * t2 * 2.0 + k12 * k22 * k33 * t3 * 2.0 + k12 * k23 * k32 * t3 * 6.0 + k12 * k22 * k33 * t4 * 6.0 + k12 * k23 * k32 * t4 * 2.0 - k13 * k22 * t2 * t4 * 3.0 - k13 * k22 * t3 * t4 + k22 * k23 * t2 * t4 * 2.0 - k15 * k32 * t2 * t3 * 3.0 - k15 * k32 * t3 * t4 + k32 * k33 * t2 * t3 * 2.0 + k25 * t2 * t3 * t4 + k35 * t2 * t3 * t4 + k12 * k22 * k25 * k32 * t2 * 3.0 + k12 * k22 * k25 * k32 * t3 + k12 * k22 * k25 * k32 * t4 * 3.0 + k12 * k22 * k32 * k35 * t2 * 3.0 + k12 * k22 * k32 * k35 * t3 * 3.0 + k12 * k22 * k32 * k35 * t4 - k12 * k15 * k22 * t3 * t4 - k12 * k13 * k32 * t3 * t4; coeffs(2) = k16 * -3.2E1 + k26 * 3.2E1 + k36 * 3.2E1 + t8 * 8.0 + t9 * 8.0 - t10 * 8.0 + t11 * 2.4E1 - t12 * 8.0 + t13 * 2.4E1 + k25 * k35 * 4.8E1 + k16 * t2 * 8.0 + k16 * t3 * 1.6E1 + k16 * t4 * 1.6E1 - k16 * t6 * 2.0 - k16 * t7 * 2.0 - k26 * t2 * 1.6E1 - k26 * t3 * 8.0 - k26 * t4 * 1.6E1 + k26 * t5 * 2.0 + k26 * t6 * 2.0 - k36 * t2 * 1.6E1 - k36 * t3 * 1.6E1 - k36 * t4 * 8.0 + k36 * t5 * 2.0 + k36 * t7 * 2.0 + t2 * t8 * 2.0 + t2 * t9 * 2.0 - t3 * t8 * 2.0 + t2 * t10 * 2.0 - t3 * t9 * 6.0 - t4 * t8 * 6.0 - t2 * t11 * 1.0E1 + t3 * t10 * 6.0 - t4 * t9 * 2.0 + t2 * t12 * 2.0 - t3 * t11 * 2.0 + t4 * t10 * 2.0 + t6 * t8 - t2 * t13 * 1.0E1 + t3 * t12 * 2.0 - t4 * t11 * 1.0E1 - t3 * t13 * 1.0E1 + t4 * t12 * 6.0 + t5 * t11 + t7 * t9 - t4 * t13 * 2.0 + t6 * t11 + t5 * t13 + t7 * t13 - k12 * k13 * k15 * 1.6E1 + k12 * k15 * k23 * 8.0 + k12 * k13 * k33 * 8.0 - k13 * k22 * k25 * 8.0 - k12 * k23 * k33 * 8.0 + k13 * k22 * k35 * 8.0 - k22 * k23 * k25 * 1.6E1 + k15 * k25 * k32 * 8.0 - k22 * k23 * k35 * 3.2E1 - k15 * k32 * k35 * 8.0 - k25 * k32 * k33 * 3.2E1 - k32 * k33 * k35 * 1.6E1 + k13 * k23 * t4 * 4.0 - k13 * k23 * t6 + k15 * k33 * t3 * 4.0 - k15 * k33 * t7 - k25 * k35 * t2 * 2.8E1 - k25 * k35 * t3 * 1.2E1 - k25 * k35 * t4 * 1.2E1 + k25 * k35 * t5 * 4.0 - k16 * t2 * t3 * 2.0 - k16 * t2 * t4 * 2.0 - k16 * t3 * t4 * 8.0 + k16 * t3 * t6 + k16 * t4 * t7 + k26 * t2 * t3 * 2.0 + k26 * t2 * t4 * 4.0 + k26 * t3 * t4 * 2.0 + k36 * t2 * t3 * 4.0 + k36 * t2 * t4 * 2.0 + k36 * t3 * t4 * 2.0 + t2 * t3 * t9 + t2 * t4 * t8 + t3 * t4 * t8 * 2.0 + t2 * t3 * t11 + t2 * t4 * t10 + t3 * t4 * t9 * 2.0 + t2 * t3 * t12 + t2 * t4 * t11 * 3.0 + t2 * t3 * t13 * 3.0 + t3 * t4 * t11 + t2 * t4 * t13 + t3 * t4 * t13 + k12 * k15 * k22 * k25 * 1.6E1 + k12 * k13 * k25 * k32 * 8.0 + k12 * k16 * k22 * k32 * 4.0 - k13 * k15 * k22 * k32 * 1.2E1 + k12 * k15 * k22 * k35 * 8.0 + k12 * k13 * k32 * k35 * 1.6E1 - k12 * k22 * k25 * k33 * 1.2E1 - k12 * k22 * k26 * k32 * 1.2E1 - k12 * k23 * k25 * k32 * 1.2E1 + k15 * k22 * k23 * k32 * 8.0 + k13 * k22 * k32 * k33 * 8.0 - k12 * k22 * k32 * k36 * 1.2E1 - k12 * k22 * k33 * k35 * 1.2E1 - k12 * k23 * k32 * k35 * 1.2E1 + k22 * k23 * k32 * k33 * 8.0 + k12 * k13 * k15 * t3 * 4.0 + k12 * k13 * k15 * t4 * 4.0 - k12 * k15 * k23 * t2 * 2.0 - k12 * k15 * k23 * t3 * 6.0 - k12 * k15 * k23 * t4 * 2.0 - k12 * k13 * k33 * t2 * 2.0 - k12 * k13 * k33 * t3 * 2.0 - k12 * k13 * k33 * t4 * 6.0 - k13 * k22 * k25 * t2 * 2.0 + k13 * k22 * k25 * t3 * 2.0 + k13 * k22 * k25 * t4 * 6.0 - k13 * k22 * k25 * t6 + k12 * k23 * k33 * t2 * 2.0 + k12 * k23 * k33 * t3 * 6.0 + k12 * k23 * k33 * t4 * 6.0 + k13 * k22 * k35 * t2 * 2.0 + k22 * k23 * k25 * t2 * 4.0 - k13 * k22 * k35 * t3 * 2.0 + k12 * k22 * k32 * t8 * 2.0 + k13 * k22 * k35 * t4 * 2.0 + k15 * k25 * k32 * t2 * 2.0 + k22 * k23 * k25 * t4 * 4.0 + k12 * k22 * k32 * t9 * 2.0 + k15 * k25 * k32 * t3 * 2.0 + k12 * k22 * k32 * t10 * 6.0 - k15 * k25 * k32 * t4 * 2.0 - k12 * k22 * k32 * t11 * 2.0 + k12 * k22 * k32 * t12 * 6.0 - k12 * k22 * k32 * t13 * 2.0 + k22 * k23 * k35 * t2 * 8.0 + k22 * k23 * k35 * t3 * 8.0 - k15 * k32 * k35 * t2 * 2.0 + k22 * k23 * k35 * t4 * 4.0; coeffs(2) += k15 * k32 * k35 * t3 * 6.0 + k15 * k32 * k35 * t4 * 2.0 - k15 * k32 * k35 * t7 + k25 * k32 * k33 * t2 * 8.0 + k25 * k32 * k33 * t3 * 4.0 + k25 * k32 * k33 * t4 * 8.0 + k32 * k33 * k35 * t2 * 4.0 + k32 * k33 * k35 * t3 * 4.0 - k13 * k23 * t2 * t4 * 3.0 - k13 * k23 * t3 * t4 * 3.0 - k15 * k33 * t2 * t3 * 3.0 - k15 * k33 * t3 * t4 * 3.0 + k25 * k35 * t2 * t3 * 2.0 + k25 * k35 * t2 * t4 * 2.0 + k16 * t2 * t3 * t4 * 2.0 + k26 * t2 * t3 * t4 + k36 * t2 * t3 * t4 - k12 * k13 * k22 * k23 * k32 * 4.0 - k12 * k15 * k22 * k32 * k33 * 4.0 - k12 * k22 * k25 * k32 * k35 * 2.0E1 - k12 * k15 * k22 * k25 * t2 * 2.0 - k12 * k15 * k22 * k25 * t3 * 2.0 - k12 * k15 * k22 * k25 * t4 * 2.0 - k12 * k13 * k25 * k32 * t2 * 4.0 + k12 * k16 * k22 * k32 * t2 + k13 * k15 * k22 * k32 * t2 * 3.0 - k12 * k13 * k25 * k32 * t3 * 2.0 - k12 * k16 * k22 * k32 * t3 + k13 * k15 * k22 * k32 * t3 * 3.0 - k12 * k13 * k25 * k32 * t4 * 2.0 - k12 * k15 * k22 * k35 * t2 * 4.0 - k12 * k16 * k22 * k32 * t4 + k13 * k15 * k22 * k32 * t4 * 3.0 - k12 * k15 * k22 * k35 * t3 * 2.0 - k12 * k15 * k22 * k35 * t4 * 2.0 - k12 * k13 * k32 * k35 * t2 * 2.0 + k12 * k22 * k25 * k33 * t2 * 3.0 + k12 * k22 * k26 * k32 * t2 * 3.0 + k12 * k23 * k25 * k32 * t2 * 3.0 - k15 * k22 * k23 * k32 * t2 * 6.0 - k12 * k13 * k32 * k35 * t3 * 2.0 + k12 * k22 * k25 * k33 * t3 + k12 * k22 * k26 * k32 * t3 + k12 * k23 * k25 * k32 * t3 * 3.0 - k15 * k22 * k23 * k32 * t3 * 4.0 - k12 * k13 * k32 * k35 * t4 * 2.0 + k12 * k22 * k25 * k33 * t4 * 9.0 + k12 * k22 * k26 * k32 * t4 * 3.0 + k12 * k23 * k25 * k32 * t4 * 3.0 - k15 * k22 * k23 * k32 * t4 * 2.0 - k13 * k22 * k32 * k33 * t2 * 6.0 - k13 * k22 * k32 * k33 * t3 * 2.0 + k12 * k22 * k32 * k36 * t2 * 3.0 + k12 * k22 * k33 * k35 * t2 * 3.0 + k12 * k23 * k32 * k35 * t2 * 3.0 - k13 * k22 * k32 * k33 * t4 * 4.0 + k12 * k22 * k32 * k36 * t3 * 3.0 + k12 * k22 * k33 * k35 * t3 * 3.0 + k12 * k23 * k32 * k35 * t3 * 9.0 + k12 * k22 * k32 * k36 * t4 + k12 * k22 * k33 * k35 * t4 * 3.0 + k12 * k23 * k32 * k35 * t4 + k22 * k23 * k32 * k33 * t2 * 4.0 + k12 * k13 * k15 * t3 * t4 * 2.0 - k12 * k15 * k23 * t3 * t4 * 3.0 - k12 * k13 * k33 * t3 * t4 * 3.0 - k13 * k22 * k25 * t2 * t4 * 4.0 - k13 * k22 * k25 * t3 * t4 - k13 * k22 * k35 * t2 * t4 * 2.0 + k22 * k23 * k25 * t2 * t4 * 2.0 - k15 * k25 * k32 * t2 * t3 * 2.0 + k12 * k22 * k32 * t3 * t9 + k12 * k22 * k32 * t4 * t8 + k12 * k22 * k32 * t2 * t11 + k12 * k22 * k32 * t2 * t13 + k12 * k22 * k32 * t4 * t11 + k12 * k22 * k32 * t3 * t13 + k22 * k23 * k35 * t2 * t4 * 2.0 - k15 * k32 * k35 * t2 * t3 * 4.0 - k15 * k32 * k35 * t3 * t4 + k25 * k32 * k33 * t2 * t3 * 2.0 + k32 * k33 * k35 * t2 * t3 * 2.0 + k25 * k35 * t2 * t3 * t4 - k12 * k13 * k22 * k23 * k32 * t4 * 2.0 - k12 * k15 * k22 * k32 * k33 * t3 * 2.0 + k12 * k22 * k25 * k32 * k35 * t2 * 4.0 + k12 * k22 * k25 * k32 * k35 * t3 + k12 * k22 * k25 * k32 * k35 * t4 - k12 * k13 * k25 * k32 * t3 * t4 + k12 * k16 * k22 * k32 * t3 * t4 - k12 * k15 * k22 * k35 * t3 * t4; coeffs(3) = k16 * k25 * -4.8E1 - k16 * k35 * 4.8E1 + k25 * k26 * 4.8E1 + k25 * k36 * 4.8E1 + k26 * k35 * 4.8E1 + k35 * k36 * 4.8E1 + k25 * t8 * 1.6E1 + k25 * t9 * 8.0 - k25 * t10 * 8.0 + k25 * t11 * 8.0 - k25 * t12 * 1.6E1 + k25 * t13 * 2.4E1 + k35 * t8 * 8.0 + k35 * t9 * 1.6E1 - k35 * t10 * 1.6E1 + k35 * t11 * 2.4E1 - k35 * t12 * 8.0 + k35 * t13 * 8.0 - k13 * k16 * k22 * 8.0 - k13 * k22 * k26 * 8.0 - k13 * k23 * k25 * 8.0 + k16 * k22 * k23 * 3.2E1 - k15 * k16 * k32 * 8.0 + k13 * k22 * k36 * 8.0 + k13 * k23 * k35 * 8.0 - k22 * k23 * k26 * 1.6E1 + k15 * k25 * k33 * 8.0 + k15 * k26 * k32 * 8.0 + k16 * k32 * k33 * 3.2E1 - k22 * k23 * k36 * 3.2E1 - k15 * k32 * k36 * 8.0 - k15 * k33 * k35 * 8.0 - k26 * k32 * k33 * 3.2E1 - k32 * k33 * k36 * 1.6E1 + k16 * k25 * t2 * 1.2E1 + k13 * k22 * t9 * 8.0 + k16 * k25 * t3 * 1.2E1 + k16 * k25 * t4 * 2.0E1 - k13 * k22 * t11 * 8.0 + k13 * k22 * t12 * 4.0 - k16 * k25 * t6 * 2.0 + k13 * k22 * t13 * 8.0 + k16 * k35 * t2 * 1.2E1 - k22 * k23 * t8 * 4.0 - k25 * k26 * t2 * 2.0E1 + k16 * k35 * t3 * 2.0E1 - k22 * k23 * t9 * 1.2E1 - k25 * k26 * t3 * 4.0 + k15 * k32 * t8 * 8.0 + k16 * k35 * t4 * 1.2E1 + k22 * k23 * t10 * 4.0 - k25 * k26 * t4 * 2.0E1 - k22 * k23 * t11 * 4.0 + k25 * k26 * t5 * 2.0 + k15 * k32 * t10 * 4.0 + k22 * k23 * t12 * 4.0 + k25 * k26 * t6 * 2.0 + k15 * k32 * t11 * 8.0 - k16 * k35 * t7 * 2.0 - k22 * k23 * t13 * 2.0E1 - k15 * k32 * t13 * 8.0 - k25 * k36 * t2 * 2.8E1 - k26 * k35 * t2 * 2.8E1 - k25 * k36 * t3 * 1.2E1 - k26 * k35 * t3 * 1.2E1 - k25 * k36 * t4 * 1.2E1 - k26 * k35 * t4 * 1.2E1 + k25 * k36 * t5 * 4.0 + k26 * k35 * t5 * 4.0 - k32 * k33 * t8 * 1.2E1 - k35 * k36 * t2 * 2.0E1 - k32 * k33 * t9 * 4.0 - k35 * k36 * t3 * 2.0E1 + k32 * k33 * t10 * 4.0 - k35 * k36 * t4 * 4.0 - k32 * k33 * t11 * 2.0E1 + k35 * k36 * t5 * 2.0 + k32 * k33 * t12 * 4.0 - k32 * k33 * t13 * 4.0 + k35 * k36 * t7 * 2.0 + k25 * t2 * t8 * 4.0 + k25 * t2 * t9 * 2.0 - k25 * t3 * t8 * 2.0 + k25 * t2 * t10 * 2.0 - k25 * t3 * t9 * 4.0 - k25 * t4 * t8 * 8.0 - k25 * t2 * t11 * 2.0 - k25 * t4 * t9 * 2.0 + k25 * t2 * t12 * 4.0 + k25 * t4 * t10 * 2.0 + k25 * t6 * t8 - k25 * t2 * t13 * 1.4E1 + k25 * t3 * t12 * 2.0 - k25 * t4 * t11 * 2.0 - k25 * t3 * t13 * 4.0 + k25 * t4 * t12 * 1.2E1 - k25 * t4 * t13 * 2.0 + k25 * t5 * t13 * 2.0 + k35 * t2 * t8 * 2.0 + k35 * t2 * t9 * 4.0 - k35 * t3 * t8 * 2.0 + k35 * t2 * t10 * 4.0 - k35 * t3 * t9 * 8.0 - k35 * t4 * t8 * 4.0 - k35 * t2 * t11 * 1.4E1 + k35 * t3 * t10 * 1.2E1 - k35 * t4 * t9 * 2.0 + k35 * t2 * t12 * 2.0 - k35 * t3 * t11 * 2.0 + k35 * t4 * t10 * 2.0 - k35 * t2 * t13 * 2.0 + k35 * t3 * t12 * 2.0 - k35 * t4 * t11 * 4.0 - k35 * t3 * t13 * 2.0 + k35 * t5 * t11 * 2.0 + k35 * t7 * t9 - k12 * k13 * k15 * k25 * 2.4E1 - k12 * k13 * k15 * k35 * 2.4E1 + k12 * k15 * k22 * k26 * 1.6E1 + k12 * k15 * k23 * k25 * 1.6E1 + k12 * k13 * k25 * k33 * 8.0 + k12 * k13 * k26 * k32 * 8.0 + k12 * k16 * k22 * k33 * 4.0 + k12 * k16 * k23 * k32 * 4.0 - k13 * k15 * k22 * k33 * 1.2E1 - k13 * k15 * k23 * k32 * 1.2E1 + k12 * k15 * k22 * k36 * 8.0 + k12 * k15 * k23 * k35 * 8.0 + k12 * k13 * k32 * k36 * 1.6E1 + k12 * k13 * k33 * k35 * 1.6E1 - k12 * k22 * k26 * k33 * 1.2E1 - k12 * k23 * k25 * k33 * 1.2E1 - k12 * k23 * k26 * k32 * 1.2E1 + k15 * k22 * k23 * k33 * 8.0 + k13 * k23 * k32 * k33 * 8.0 - k12 * k22 * k33 * k36 * 1.2E1 - k12 * k23 * k32 * k36 * 1.2E1 - k12 * k23 * k33 * k35 * 1.2E1 - k22 * k23 * k25 * k35 * 2.4E1 - k25 * k32 * k33 * k35 * 2.4E1 - k13 * k16 * k22 * t2 * 2.0 + k13 * k16 * k22 * t3 * 2.0 + k13 * k16 * k22 * t4 * 6.0 - k12 * k15 * k22 * t8 * 2.0 - k13 * k16 * k22 * t6 - k12 * k15 * k22 * t9 * 2.0 - k12 * k15 * k22 * t10 * 6.0 + k12 * k15 * k22 * t11 * 6.0 - k12 * k15 * k22 * t12 * 2.0 - k12 * k15 * k22 * t13 * 2.0 - k13 * k22 * k26 * t2 * 2.0 - k13 * k23 * k25 * t2 * 2.0 - k16 * k22 * k23 * t2 * 4.0 + k13 * k22 * k26 * t3 * 2.0 + k13 * k23 * k25 * t3 * 6.0 - k16 * k22 * k23 * t3 * 8.0 - k12 * k13 * k32 * t8 * 2.0 + k13 * k22 * k26 * t4 * 6.0 + k13 * k23 * k25 * t4 * 6.0 - k15 * k16 * k32 * t2 * 2.0 - k16 * k22 * k23 * t4 * 1.6E1 - k12 * k13 * k32 * t9 * 2.0 + k15 * k16 * k32 * t3 * 6.0 - k12 * k13 * k32 * t10 * 2.0 - k13 * k22 * k26 * t6 - k13 * k23 * k25 * t6 + k15 * k16 * k32 * t4 * 2.0 + k16 * k22 * k23 * t6 * 2.0 - k12 * k13 * k32 * t11 * 2.0 - k12 * k13 * k32 * t12 * 6.0 + k12 * k13 * k32 * t13 * 6.0 - k15 * k16 * k32 * t7 + k13 * k22 * k36 * t2 * 2.0 + k13 * k23 * k35 * t2 * 2.0 + k22 * k23 * k26 * t2 * 4.0 - k13 * k22 * k36 * t3 * 2.0 - k13 * k23 * k35 * t3 * 6.0 + k12 * k22 * k33 * t8 * 2.0 + k12 * k23 * k32 * t8 * 2.0 + k13 * k22 * k36 * t4 * 2.0 + k13 * k23 * k35 * t4 * 2.0 + k15 * k25 * k33 * t2 * 2.0 + k15 * k26 * k32 * t2 * 2.0 + k22 * k23 * k26 * t4 * 4.0 + k12 * k22 * k33 * t9 * 2.0 + k12 * k23 * k32 * t9 * 2.0 + k15 * k25 * k33 * t3 * 2.0 + k15 * k26 * k32 * t3 * 2.0 + k12 * k22 * k33 * t10 * 6.0 + k12 * k23 * k32 * t10 * 2.0 - k15 * k25 * k33 * t4 * 6.0 - k15 * k26 * k32 * t4 * 2.0 - k12 * k22 * k33 * t11 * 2.0 - k12 * k23 * k32 * t11 * 2.0 + k12 * k22 * k33 * t12 * 2.0 + k12 * k23 * k32 * t12 * 6.0 - k12 * k22 * k33 * t13 * 2.0 - k12 * k23 * k32 * t13 * 2.0 - k16 * k32 * k33 * t2 * 4.0 + k22 * k23 * k36 * t2 * 8.0 - k16 * k32 * k33 * t3 * 1.6E1 + k22 * k23 * k36 * t3 * 8.0 - k15 * k32 * k36 * t2 * 2.0 - k15 * k33 * k35 * t2 * 2.0 - k16 * k32 * k33 * t4 * 8.0 + k22 * k23 * k36 * t4 * 4.0 + k15 * k32 * k36 * t3 * 6.0 + k15 * k33 * k35 * t3 * 6.0 + k15 * k32 * k36 * t4 * 2.0 + k15 * k33 * k35 * t4 * 6.0 + k16 * k32 * k33 * t7 * 2.0 - k15 * k32 * k36 * t7 - k15 * k33 * k35 * t7 + k26 * k32 * k33 * t2 * 8.0 + k26 * k32 * k33 * t3 * 4.0 + k26 * k32 * k33 * t4 * 8.0 + k32 * k33 * k36 * t2 * 4.0 + k32 * k33 * k36 * t3 * 4.0 - k13 * k22 * t3 * t9 * 2.0 - k13 * k22 * t4 * t8 - k16 * k25 * t2 * t4 * 4.0 - k13 * k22 * t2 * t11 * 2.0 - k13 * k22 * t4 * t9 * 3.0 - k16 * k25 * t3 * t4 * 4.0 - k13 * k22 * t2 * t12 * 3.0 - k13 * k22 * t4 * t10 * 3.0 + k13 * k22 * t2 * t13 * 2.0 - k13 * k22 * t3 * t12 + k13 * k22 * t4 * t11 - k13 * k22 * t3 * t13 * 2.0 - k13 * k22 * t4 * t12 * 6.0 - k13 * k22 * t4 * t13 - k16 * k35 * t2 * t3 * 4.0 + k22 * k23 * t2 * t9 * 2.0 + k25 * k26 * t2 * t3 * 2.0 + k22 * k23 * t3 * t9 * 4.0 + k22 * k23 * t4 * t8 * 4.0 + k25 * k26 * t2 * t4 * 6.0 - k15 * k32 * t3 * t8 * 3.0 - k16 * k35 * t3 * t4 * 4.0 + k22 * k23 * t2 * t11 * 2.0 + k22 * k23 * t4 * t9 * 4.0 + k25 * k26 * t3 * t4 * 2.0 - k15 * k32 * t2 * t10 * 3.0 - k15 * k32 * t3 * t9 - k15 * k32 * t4 * t8 * 2.0 + k22 * k23 * t2 * t12 * 2.0 + k15 * k32 * t2 * t11 * 2.0 - k15 * k32 * t3 * t10 * 6.0 + k22 * k23 * t2 * t13 * 6.0 + k22 * k23 * t4 * t11 * 2.0 - k15 * k32 * t3 * t11 - k15 * k32 * t4 * t10 + k22 * k23 * t3 * t13 * 4.0 - k15 * k32 * t2 * t13 * 2.0 - k15 * k32 * t3 * t12 * 3.0 - k15 * k32 * t4 * t11 * 2.0 + k22 * k23 * t4 * t13 * 2.0 + k15 * k32 * t3 * t13 + k25 * k36 * t2 * t3 * 2.0 + k26 * k35 * t2 * t3 * 2.0 + k25 * k36 * t2 * t4 * 2.0 + k26 * k35 * t2 * t4 * 2.0 + k32 * k33 * t2 * t8 * 2.0 + k32 * k33 * t3 * t8 * 4.0 + k35 * k36 * t2 * t3 * 6.0 + k32 * k33 * t2 * t10 * 2.0 + k32 * k33 * t3 * t9 * 4.0 + k32 * k33 * t4 * t8 * 4.0 + k35 * k36 * t2 * t4 * 2.0 + k32 * k33 * t2 * t11 * 6.0 + k35 * k36 * t3 * t4 * 2.0 + k32 * k33 * t3 * t11 * 2.0 + k32 * k33 * t2 * t13 * 2.0 + k32 * k33 * t4 * t11 * 4.0 + k32 * k33 * t3 * t13 * 2.0 + k25 * t2 * t4 * t8 * 2.0 + k25 * t3 * t4 * t8 * 2.0 + k25 * t2 * t4 * t10 + k25 * t2 * t3 * t12 + k25 * t2 * t4 * t11 + k25 * t2 * t4 * t13 + k35 * t2 * t3 * t9 * 2.0 + k35 * t2 * t3 * t11 + k35 * t2 * t4 * t10 + k35 * t3 * t4 * t9 * 2.0 + k35 * t2 * t3 * t12; coeffs(3) += k35 * t2 * t3 * t13 + k12 * k13 * k15 * k22 * k23 * 8.0 - k12 * k13 * k22 * k23 * k33 * 4.0 + k12 * k13 * k15 * k32 * k33 * 8.0 + k12 * k16 * k22 * k25 * k32 * 4.0 - k13 * k15 * k22 * k25 * k32 * 1.2E1 + k12 * k15 * k22 * k25 * k35 * 2.0E1 - k12 * k15 * k23 * k32 * k33 * 4.0 + k12 * k13 * k25 * k32 * k35 * 2.0E1 + k12 * k16 * k22 * k32 * k35 * 4.0 - k12 * k22 * k25 * k26 * k32 * 4.0 - k13 * k15 * k22 * k32 * k35 * 1.2E1 + k15 * k22 * k23 * k25 * k32 * 4.0 + k13 * k22 * k25 * k32 * k33 * 1.2E1 - k12 * k22 * k25 * k32 * k36 * 2.0E1 - k12 * k22 * k25 * k33 * k35 * 2.0E1 - k12 * k22 * k26 * k32 * k35 * 2.0E1 - k12 * k23 * k25 * k32 * k35 * 2.0E1 + k15 * k22 * k23 * k32 * k35 * 1.2E1 + k13 * k22 * k32 * k33 * k35 * 4.0 + k22 * k23 * k25 * k32 * k33 * 8.0 - k12 * k22 * k32 * k35 * k36 * 4.0 + k22 * k23 * k32 * k33 * k35 * 8.0 + k12 * k13 * k15 * k25 * t3 * 4.0 + k12 * k13 * k15 * k25 * t4 * 4.0 - k12 * k15 * k16 * k22 * t4 * 2.0 - k12 * k13 * k16 * k32 * t3 * 2.0 - k12 * k15 * k22 * k26 * t2 * 2.0 - k12 * k15 * k23 * k25 * t2 * 2.0 + k12 * k13 * k15 * k35 * t3 * 4.0 - k12 * k15 * k22 * k26 * t3 * 2.0 - k12 * k15 * k23 * k25 * t3 * 6.0 + k12 * k13 * k15 * k35 * t4 * 4.0 - k12 * k15 * k22 * k26 * t4 * 2.0 - k12 * k15 * k23 * k25 * t4 * 2.0 - k12 * k13 * k25 * k33 * t2 * 4.0 - k12 * k13 * k26 * k32 * t2 * 4.0 + k12 * k16 * k22 * k33 * t2 + k12 * k16 * k23 * k32 * t2 + k13 * k15 * k22 * k33 * t2 * 3.0 + k13 * k15 * k23 * k32 * t2 * 3.0 - k12 * k13 * k25 * k33 * t3 * 2.0 - k12 * k13 * k26 * k32 * t3 * 2.0 - k12 * k16 * k22 * k33 * t3 - k12 * k16 * k23 * k32 * t3 * 3.0 + k13 * k15 * k22 * k33 * t3 * 3.0 + k13 * k15 * k23 * k32 * t3 * 9.0 - k12 * k13 * k25 * k33 * t4 * 6.0 - k12 * k13 * k26 * k32 * t4 * 2.0 - k12 * k15 * k22 * k36 * t2 * 4.0 - k12 * k15 * k23 * k35 * t2 * 4.0 - k12 * k16 * k22 * k33 * t4 * 3.0 - k12 * k16 * k23 * k32 * t4 + k13 * k15 * k22 * k33 * t4 * 9.0 + k13 * k15 * k23 * k32 * t4 * 3.0 - k12 * k15 * k22 * k36 * t3 * 2.0 - k12 * k15 * k23 * k35 * t3 * 6.0 - k12 * k15 * k22 * k36 * t4 * 2.0 - k12 * k15 * k23 * k35 * t4 * 2.0 - k12 * k13 * k32 * k36 * t2 * 2.0 - k12 * k13 * k33 * k35 * t2 * 2.0 + k12 * k22 * k26 * k33 * t2 * 3.0 + k12 * k23 * k25 * k33 * t2 * 3.0 + k12 * k23 * k26 * k32 * t2 * 3.0 - k15 * k22 * k23 * k33 * t2 * 6.0 - k12 * k13 * k32 * k36 * t3 * 2.0 - k12 * k13 * k33 * k35 * t3 * 2.0 + k12 * k22 * k26 * k33 * t3 + k12 * k23 * k25 * k33 * t3 * 3.0 + k12 * k23 * k26 * k32 * t3 * 3.0 - k15 * k22 * k23 * k33 * t3 * 4.0 - k12 * k13 * k32 * k36 * t4 * 2.0 - k12 * k13 * k33 * k35 * t4 * 6.0 + k12 * k22 * k26 * k33 * t4 * 9.0 + k12 * k23 * k25 * k33 * t4 * 9.0 + k12 * k23 * k26 * k32 * t4 * 3.0 - k15 * k22 * k23 * k33 * t4 * 6.0 + k13 * k22 * k25 * k35 * t3 * 2.0 + k12 * k22 * k25 * k32 * t8 * 5.0 + k13 * k22 * k25 * k35 * t4 * 4.0 - k12 * k22 * k25 * k32 * t9 + k12 * k22 * k25 * k32 * t10 * 3.0 + k12 * k22 * k25 * k32 * t11 + k12 * k22 * k25 * k32 * t12 * 9.0 - k13 * k23 * k32 * k33 * t2 * 6.0 - k12 * k22 * k25 * k32 * t13 * 5.0 - k13 * k23 * k32 * k33 * t3 * 6.0 + k12 * k22 * k33 * k36 * t2 * 3.0 + k12 * k23 * k32 * k36 * t2 * 3.0 + k12 * k23 * k33 * k35 * t2 * 3.0 - k13 * k23 * k32 * k33 * t4 * 4.0 + k12 * k22 * k33 * k36 * t3 * 3.0 + k12 * k23 * k32 * k36 * t3 * 9.0 + k12 * k23 * k33 * k35 * t3 * 9.0 + k12 * k22 * k33 * k36 * t4 * 3.0 + k12 * k23 * k32 * k36 * t4 + k12 * k23 * k33 * k35 * t4 * 3.0 + k22 * k23 * k25 * k35 * t2 * 4.0 - k12 * k22 * k32 * k35 * t8 + k12 * k22 * k32 * k35 * t9 * 5.0 + k15 * k25 * k32 * k35 * t3 * 4.0 + k12 * k22 * k32 * k35 * t10 * 9.0 + k15 * k25 * k32 * k35 * t4 * 2.0 - k12 * k22 * k32 * k35 * t11 * 5.0 + k12 * k22 * k32 * k35 * t12 * 3.0 + k12 * k22 * k32 * k35 * t13 + k25 * k32 * k33 * k35 * t2 * 4.0 - k13 * k16 * k22 * t2 * t4 - k13 * k16 * k22 * t3 * t4 * 2.0 - k12 * k15 * k22 * t4 * t8 - k12 * k15 * k22 * t4 * t10 * 3.0 - k12 * k15 * k22 * t2 * t13 * 2.0 - k12 * k15 * k22 * t3 * t12 - k12 * k15 * k22 * t4 * t11 - k12 * k15 * k22 * t4 * t13 - k13 * k22 * k26 * t2 * t4 * 4.0 - k13 * k23 * k25 * t2 * t4 * 4.0 + k16 * k22 * k23 * t2 * t4 * 4.0 - k13 * k22 * k26 * t3 * t4 - k13 * k23 * k25 * t3 * t4 * 3.0 - k15 * k16 * k32 * t2 * t3 + k16 * k22 * k23 * t3 * t4 * 4.0 - k12 * k13 * k32 * t3 * t9 - k12 * k13 * k32 * t2 * t11 * 2.0 - k15 * k16 * k32 * t3 * t4 * 2.0 - k12 * k13 * k32 * t3 * t11 - k12 * k13 * k32 * t4 * t10 - k12 * k13 * k32 * t3 * t12 * 3.0 - k12 * k13 * k32 * t3 * t13 - k13 * k22 * k36 * t2 * t4 * 2.0 - k13 * k23 * k35 * t2 * t4 * 2.0 + k22 * k23 * k26 * t2 * t4 * 2.0 - k15 * k25 * k33 * t2 * t3 * 2.0 - k15 * k26 * k32 * t2 * t3 * 2.0 + k12 * k22 * k33 * t3 * t9 + k12 * k22 * k33 * t4 * t8 * 3.0 + k12 * k23 * k32 * t3 * t9 * 3.0 + k12 * k23 * k32 * t4 * t8 + k12 * k22 * k33 * t2 * t11 + k12 * k23 * k32 * t2 * t11 + k12 * k22 * k33 * t2 * t13 + k12 * k22 * k33 * t4 * t11 * 3.0 + k12 * k23 * k32 * t2 * t13 + k12 * k23 * k32 * t4 * t11 + k12 * k22 * k33 * t3 * t13 + k12 * k23 * k32 * t3 * t13 * 3.0 + k16 * k32 * k33 * t2 * t3 * 4.0 + k22 * k23 * k36 * t2 * t4 * 2.0 - k15 * k32 * k36 * t2 * t3 * 4.0 - k15 * k33 * k35 * t2 * t3 * 4.0 + k16 * k32 * k33 * t3 * t4 * 4.0 - k15 * k32 * k36 * t3 * t4 - k15 * k33 * k35 * t3 * t4 * 3.0 + k26 * k32 * k33 * t2 * t3 * 2.0 + k32 * k33 * k36 * t2 * t3 * 2.0 + k16 * k25 * t2 * t3 * t4 - k13 * k22 * t2 * t4 * t11 + k16 * k35 * t2 * t3 * t4 - k15 * k32 * t2 * t3 * t13 + k25 * k36 * t2 * t3 * t4 + k26 * k35 * t2 * t3 * t4 - k12 * k13 * k22 * k23 * k25 * k32 * 4.0 - k12 * k13 * k22 * k23 * k32 * k35 * 4.0 - k12 * k15 * k22 * k25 * k32 * k33 * 4.0 - k12 * k15 * k22 * k32 * k33 * k35 * 4.0 + k12 * k13 * k15 * k22 * k23 * t4 * 4.0 - k12 * k13 * k22 * k23 * k33 * t4 * 6.0 + k12 * k13 * k15 * k32 * k33 * t3 * 4.0 + k12 * k16 * k22 * k25 * k32 * t2 + k13 * k15 * k22 * k25 * k32 * t2 * 3.0 + k12 * k16 * k22 * k25 * k32 * t3 + k13 * k15 * k22 * k25 * k32 * t3 - k12 * k15 * k22 * k25 * k35 * t2 * 4.0 - k12 * k16 * k22 * k25 * k32 * t4 * 2.0 + k13 * k15 * k22 * k25 * k32 * t4 * 2.0 - k12 * k15 * k22 * k25 * k35 * t3 * 2.0 - k12 * k15 * k23 * k32 * k33 * t3 * 6.0 - k12 * k13 * k25 * k32 * k35 * t2 * 4.0 + k12 * k16 * k22 * k32 * k35 * t2 + k12 * k22 * k25 * k26 * k32 * t2 * 2.0 + k13 * k15 * k22 * k32 * k35 * t2 * 3.0 - k15 * k22 * k23 * k25 * k32 * t2 * 4.0 - k12 * k16 * k22 * k32 * k35 * t3 * 2.0 + k13 * k15 * k22 * k32 * k35 * t3 * 2.0 - k12 * k13 * k25 * k32 * k35 * t4 * 2.0 + k12 * k16 * k22 * k32 * k35 * t4 + k12 * k22 * k25 * k26 * k32 * t4 * 2.0 + k13 * k15 * k22 * k32 * k35 * t4 - k13 * k22 * k25 * k32 * k33 * t2 * 8.0 - k13 * k22 * k25 * k32 * k33 * t3 * 2.0 + k12 * k22 * k25 * k32 * k36 * t2 * 4.0 + k12 * k22 * k25 * k33 * k35 * t2 * 4.0 + k12 * k22 * k26 * k32 * k35 * t2 * 4.0 + k12 * k23 * k25 * k32 * k35 * t2 * 4.0 - k13 * k22 * k25 * k32 * k33 * t4 * 4.0 - k15 * k22 * k23 * k32 * k35 * t2 * 8.0 + k12 * k22 * k25 * k32 * k36 * t3 + k12 * k22 * k25 * k33 * k35 * t3 + k12 * k22 * k26 * k32 * k35 * t3 + k12 * k23 * k25 * k32 * k35 * t3 * 3.0 - k15 * k22 * k23 * k32 * k35 * t3 * 4.0 + k12 * k22 * k25 * k32 * k36 * t4 + k12 * k22 * k25 * k33 * k35 * t4 * 3.0 + k12 * k22 * k26 * k32 * k35 * t4 + k12 * k23 * k25 * k32 * k35 * t4 - k15 * k22 * k23 * k32 * k35 * t4 * 2.0 - k13 * k22 * k32 * k33 * k35 * t2 * 4.0 + k22 * k23 * k25 * k32 * k33 * t2 * 4.0 + k12 * k22 * k32 * k35 * k36 * t2 * 2.0 + k12 * k22 * k32 * k35 * k36 * t3 * 2.0 + k22 * k23 * k32 * k33 * k35 * t2 * 4.0 + k12 * k13 * k15 * k25 * t3 * t4 - k12 * k15 * k16 * k22 * t3 * t4 - k12 * k13 * k16 * k32 * t3 * t4 + k12 * k13 * k15 * k35 * t3 * t4 - k12 * k13 * k25 * k33 * t3 * t4 * 3.0 - k12 * k13 * k26 * k32 * t3 * t4 + k12 * k16 * k22 * k33 * t3 * t4 * 3.0 + k12 * k16 * k23 * k32 * t3 * t4 * 3.0 - k12 * k15 * k22 * k36 * t3 * t4 - k12 * k15 * k23 * k35 * t3 * t4 * 3.0 - k13 * k22 * k25 * k35 * t2 * t4 * 2.0 + k12 * k22 * k25 * k32 * t4 * t8 + k12 * k22 * k25 * k32 * t2 * t13 + k22 * k23 * k25 * k35 * t2 * t4 * 2.0 - k15 * k25 * k32 * k35 * t2 * t3 * 2.0 + k12 * k22 * k32 * k35 * t3 * t9 + k12 * k22 * k32 * k35 * t2 * t11 + k25 * k32 * k33 * k35 * t2 * t3 * 2.0 - k12 * k13 * k22 * k23 * k25 * k32 * t4 * 2.0 - k12 * k15 * k22 * k32 * k33 * k35 * t3 * 2.0; coeffs(4) = t14 * 2.4E1 + t15 * 2.4E1 + t16 * 2.4E1 + t17 + t18 + t19 + t20 - k16 * k26 * 4.8E1 - k16 * k36 * 4.8E1 + k26 * k36 * 4.8E1 - k16 * t8 * 8.0 - k16 * t9 * 8.0 + k16 * t10 * 1.6E1 - k16 * t11 * 2.4E1 + k16 * t12 * 1.6E1 - k16 * t13 * 2.4E1 + k26 * t8 * 1.6E1 + k26 * t9 * 8.0 - k26 * t10 * 8.0 + k26 * t11 * 2.4E1 - k26 * t12 * 1.6E1 + k26 * t13 * 2.4E1 + k36 * t8 * 8.0 + k36 * t9 * 1.6E1 - k36 * t10 * 1.6E1 + k36 * t11 * 2.4E1 - k36 * t12 * 8.0 + k36 * t13 * 2.4E1 - t2 * t14 * 2.0 - t2 * t15 * 1.0E1 - t3 * t14 * 1.0E1 - t8 * t9 * 2.0 - t2 * t16 * 1.0E1 - t3 * t15 * 2.0 - t4 * t14 * 1.0E1 - t8 * t10 * 2.0 - t3 * t16 * 1.0E1 - t4 * t15 * 1.0E1 + t8 * t11 * 1.0E1 - t9 * t10 * 6.0 - t4 * t16 * 2.0 + t5 * t15 + t6 * t14 - t8 * t12 * 6.0 + t9 * t11 * 2.0 + t5 * t16 + t6 * t15 + t7 * t14 + t8 * t13 * 2.0 - t9 * t12 * 2.0 - t10 * t11 * 2.0 + t9 * t13 * 1.0E1 + t10 * t12 * 2.0 + t7 * t16 - t10 * t13 * 1.0E1 - t11 * t12 * 1.0E1 + t11 * t13 * 6.0 - t12 * t13 * 2.0 + t11 * t11 + t13 * t13 - k13 * k16 * k23 * 8.0 - k13 * k23 * k26 * 8.0 - k15 * k16 * k33 * 8.0 + k13 * k23 * k36 * 8.0 + k15 * k26 * k33 * 8.0 - k16 * k25 * k35 * 4.8E1 - k15 * k33 * k36 * 8.0 + k25 * k26 * k35 * 4.8E1 + k25 * k35 * k36 * 4.8E1 + k16 * k26 * t2 * 1.2E1 + k13 * k23 * t9 * 8.0 + k16 * k26 * t3 * 1.2E1 + k16 * k26 * t4 * 2.0E1 - k13 * k23 * t11 * 8.0 + k13 * k23 * t12 * 4.0 - k16 * k26 * t6 * 2.0 + k13 * k23 * t13 * 8.0 + k16 * k36 * t2 * 1.2E1 + k16 * k36 * t3 * 2.0E1 + k15 * k33 * t8 * 8.0 + k16 * k36 * t4 * 1.2E1 + k15 * k33 * t10 * 4.0 + k15 * k33 * t11 * 8.0 - k16 * k36 * t7 * 2.0 - k15 * k33 * t13 * 8.0 - k26 * k36 * t2 * 2.8E1 - k26 * k36 * t3 * 1.2E1 - k26 * k36 * t4 * 1.2E1 + k26 * k36 * t5 * 4.0 + k25 * k35 * t8 * 1.2E1 + k25 * k35 * t9 * 1.2E1 - k25 * k35 * t10 * 1.2E1 + k25 * k35 * t11 * 4.0 - k25 * k35 * t12 * 1.2E1 + k25 * k35 * t13 * 4.0 + k16 * t3 * t8 * 2.0 - k16 * t2 * t10 * 2.0 + k16 * t3 * t9 * 2.0 + k16 * t4 * t8 * 2.0 + k16 * t2 * t11 * 4.0 - k16 * t3 * t10 * 1.2E1 + k16 * t4 * t9 * 2.0 - k16 * t2 * t12 * 2.0 + k16 * t3 * t11 * 2.0 - k16 * t4 * t10 * 8.0 + k16 * t2 * t13 * 4.0 - k16 * t3 * t12 * 8.0 + k16 * t4 * t11 * 6.0 + k16 * t3 * t13 * 6.0 - k16 * t4 * t12 * 1.2E1 + k16 * t6 * t10 + k16 * t4 * t13 * 2.0 + k16 * t7 * t12 + k26 * t2 * t8 * 4.0 + k26 * t2 * t9 * 2.0 - k26 * t3 * t8 * 2.0 + k26 * t2 * t10 * 2.0 - k26 * t3 * t9 * 4.0 - k26 * t4 * t8 * 8.0 - k26 * t2 * t11 * 6.0 - k26 * t4 * t9 * 2.0 + k26 * t2 * t12 * 4.0 + k26 * t4 * t10 * 2.0 + k26 * t6 * t8 - k26 * t2 * t13 * 1.4E1 + k26 * t3 * t12 * 2.0 - k26 * t4 * t11 * 6.0 - k26 * t3 * t13 * 4.0 + k26 * t4 * t12 * 1.2E1 - k26 * t4 * t13 * 2.0 + k26 * t5 * t13 * 2.0 + k36 * t2 * t8 * 2.0 + k36 * t2 * t9 * 4.0 - k36 * t3 * t8 * 2.0 + k36 * t2 * t10 * 4.0 - k36 * t3 * t9 * 8.0 - k36 * t4 * t8 * 4.0 - k36 * t2 * t11 * 1.4E1 + k36 * t3 * t10 * 1.2E1 - k36 * t4 * t9 * 2.0 + k36 * t2 * t12 * 2.0 - k36 * t3 * t11 * 2.0 + k36 * t4 * t10 * 2.0 - k36 * t2 * t13 * 6.0 + k36 * t3 * t12 * 2.0 - k36 * t4 * t11 * 4.0 - k36 * t3 * t13 * 6.0 + k36 * t5 * t11 * 2.0 + k36 * t7 * t9 + t2 * t3 * t14 + t2 * t3 * t15 + t2 * t4 * t14 + t3 * t8 * t9 + t2 * t3 * t16 * 3.0 + t2 * t4 * t15 * 3.0 + t2 * t8 * t11 * 2.0 + t2 * t9 * t10 + t3 * t4 * t14 * 3.0 + t4 * t8 * t9 + t2 * t4 * t16 + t2 * t8 * t12 + t3 * t4 * t15 + t3 * t8 * t11 + t3 * t9 * t10 * 6.0 + t4 * t8 * t10 * 2.0 + t2 * t10 * t11 + t3 * t4 * t16 + t3 * t8 * t12 * 2.0 - t4 * t8 * t11 * 2.0 + t4 * t9 * t10 * 2.0 + t2 * t9 * t13 * 2.0 + t2 * t10 * t12 + t3 * t8 * t13 + t3 * t9 * t12 * 2.0 + t4 * t8 * t12 * 6.0 + t4 * t9 * t11 + t2 * t10 * t13 * 3.0 + t2 * t11 * t12 * 3.0 - t3 * t9 * t13 * 2.0 + t4 * t10 * t11 - t2 * t11 * t13 * 4.0 + t3 * t10 * t13 * 6.0 + t3 * t11 * t12 + t4 * t9 * t13 + t2 * t12 * t13 + t4 * t10 * t13 + t4 * t11 * t12 * 6.0 + t3 * t12 * t13 + t5 * t11 * t13 + k12 * k13 * k15 * k16 * 8.0 - k12 * k13 * k15 * k26 * 2.4E1 - k12 * k13 * k15 * k36 * 2.4E1 + k12 * k15 * k23 * k26 * 1.6E1 + k12 * k13 * k26 * k33 * 8.0 + k12 * k16 * k23 * k33 * 4.0 - k13 * k15 * k23 * k33 * 1.2E1 + k12 * k15 * k23 * k36 * 8.0 - k13 * k16 * k22 * k35 * 1.6E1 - k13 * k22 * k25 * k26 * 1.6E1 + k16 * k22 * k23 * k25 * 2.4E1 - k15 * k16 * k25 * k32 * 1.6E1 + k12 * k13 * k33 * k36 * 1.6E1 - k12 * k23 * k26 * k33 * 1.2E1 + k16 * k22 * k23 * k35 * 4.0E1 - k22 * k23 * k25 * k26 * 8.0 + k15 * k25 * k26 * k32 * 1.6E1 - k12 * k23 * k33 * k36 * 1.2E1 + k13 * k22 * k35 * k36 * 1.6E1 + k16 * k25 * k32 * k33 * 4.0E1 - k22 * k23 * k25 * k36 * 2.4E1 - k22 * k23 * k26 * k35 * 2.4E1 + k16 * k32 * k33 * k35 * 2.4E1 - k22 * k23 * k35 * k36 * 4.0E1 - k25 * k26 * k32 * k33 * 4.0E1 - k15 * k32 * k35 * k36 * 1.6E1 - k25 * k32 * k33 * k36 * 2.4E1 - k26 * k32 * k33 * k35 * 2.4E1 - k32 * k33 * k35 * k36 * 8.0 + k12 * k13 * k15 * t10 * 4.0 - k12 * k13 * k15 * t11 * 8.0 + k12 * k13 * k15 * t12 * 4.0 - k12 * k13 * k15 * t13 * 8.0 - k13 * k16 * k23 * t2 * 2.0 + k13 * k16 * k23 * t3 * 6.0 + k13 * k16 * k23 * t4 * 6.0 - k12 * k15 * k23 * t8 * 2.0 - k13 * k16 * k23 * t6 - k12 * k15 * k23 * t9 * 2.0 - k12 * k15 * k23 * t10 * 2.0 + k12 * k15 * k23 * t11 * 6.0 - k12 * k15 * k23 * t12 * 2.0 - k12 * k15 * k23 * t13 * 2.0 - k13 * k23 * k26 * t2 * 2.0 + k13 * k23 * k26 * t3 * 6.0 - k12 * k13 * k33 * t8 * 2.0 + k13 * k23 * k26 * t4 * 6.0 - k15 * k16 * k33 * t2 * 2.0 - k12 * k13 * k33 * t9 * 2.0 + k15 * k16 * k33 * t3 * 6.0 - k12 * k13 * k33 * t10 * 2.0 - k13 * k22 * k25 * t8 * 2.0 - k13 * k23 * k26 * t6 + k15 * k16 * k33 * t4 * 6.0 - k12 * k13 * k33 * t11 * 2.0 + k13 * k22 * k25 * t9 * 6.0 - k12 * k13 * k33 * t12 * 2.0 + k13 * k22 * k25 * t10 * 6.0 + k12 * k13 * k33 * t13 * 6.0 - k13 * k22 * k25 * t11 * 2.0 - k15 * k16 * k33 * t7 + k13 * k22 * k25 * t12 * 6.0 + k13 * k22 * k25 * t13 * 2.0 + k13 * k23 * k36 * t2 * 2.0 - k13 * k23 * k36 * t3 * 6.0 + k12 * k23 * k33 * t8 * 2.0 + k13 * k23 * k36 * t4 * 2.0 + k15 * k26 * k33 * t2 * 2.0 + k12 * k23 * k33 * t9 * 2.0 + k15 * k26 * k33 * t3 * 2.0 + k12 * k23 * k33 * t10 * 2.0 + k13 * k22 * k35 * t8 * 2.0 - k15 * k26 * k33 * t4 * 6.0 + k16 * k25 * k35 * t2 * 1.6E1 - k22 * k23 * k25 * t8 * 4.0 - k12 * k23 * k33 * t11 * 2.0 + k13 * k22 * k35 * t9 * 1.0E1 + k16 * k25 * k35 * t3 * 8.0 - k22 * k23 * k25 * t9 * 8.0 - k12 * k22 * k32 * t14 * 2.0 + k12 * k23 * k33 * t12 * 2.0 - k13 * k22 * k35 * t10 * 6.0 + k15 * k25 * k32 * t8 * 1.0E1 + k16 * k25 * k35 * t4 * 8.0 - k12 * k22 * k32 * t15 * 2.0 - k12 * k23 * k33 * t13 * 2.0 - k13 * k22 * k35 * t11 * 2.0 + k15 * k25 * k32 * t9 * 2.0 - k12 * k22 * k32 * t16 * 2.0 + k13 * k22 * k35 * t12 * 2.0 + k15 * k25 * k32 * t10 * 2.0 + k22 * k23 * k25 * t12 * 4.0 + k13 * k22 * k35 * t13 * 2.0 + k15 * k25 * k32 * t11 * 2.0 - k22 * k23 * k25 * t13 * 8.0 - k15 * k25 * k32 * t12 * 6.0 - k15 * k25 * k32 * t13 * 2.0 - k15 * k33 * k36 * t2 * 2.0 + k15 * k33 * k36 * t3 * 6.0 + k15 * k33 * k36 * t4 * 6.0 - k22 * k23 * k35 * t8 * 4.0 - k25 * k26 * k35 * t2 * 2.8E1 - k22 * k23 * k35 * t9 * 1.6E1 - k25 * k26 * k35 * t3 * 4.0 + k15 * k32 * k35 * t8 * 6.0 + k22 * k23 * k35 * t10 * 8.0 - k25 * k26 * k35 * t4 * 8.0 - k15 * k32 * k35 * t9 * 2.0 - k15 * k33 * k36 * t7 - k22 * k23 * k35 * t11 * 4.0 + k25 * k26 * k35 * t5 * 4.0 + k15 * k32 * k35 * t10 * 6.0 + k22 * k23 * k35 * t12 * 4.0 + k15 * k32 * k35 * t11 * 2.0 - k22 * k23 * k35 * t13 * 4.0 + k15 * k32 * k35 * t12 * 6.0 - k15 * k32 * k35 * t13 * 2.0 - k25 * k32 * k33 * t8 * 1.6E1 - k25 * k35 * k36 * t2 * 2.8E1 - k25 * k32 * k33 * t9 * 4.0 - k25 * k35 * k36 * t3 * 8.0 + k25 * k32 * k33 * t10 * 4.0 - k25 * k35 * k36 * t4 * 4.0 - k25 * k32 * k33 * t11 * 4.0 + k25 * k35 * k36 * t5 * 4.0 + k25 * k32 * k33 * t12 * 8.0 - k25 * k32 * k33 * t13 * 4.0 - k32 * k33 * k35 * t8 * 8.0 - k32 * k33 * k35 * t9 * 4.0 + k32 * k33 * k35 * t10 * 4.0 - k32 * k33 * k35 * t11 * 8.0 - k13 * k23 * t3 * t9 * 6.0 - k13 * k23 * t4 * t8 - k16 * k26 * t2 * t4 * 4.0 - k13 * k23 * t2 * t11 * 2.0 - k13 * k23 * t4 * t9 * 3.0 - k16 * k26 * t3 * t4 * 4.0 - k13 * k23 * t2 * t12 * 3.0 - k13 * k23 * t4 * t10 + k13 * k23 * t2 * t13 * 2.0 - k13 * k23 * t3 * t12 * 3.0 + k13 * k23 * t4 * t11 - k13 * k23 * t3 * t13 * 6.0 - k13 * k23 * t4 * t12 * 6.0 - k13 * k23 * t4 * t13 - k16 * k36 * t2 * t3 * 4.0 - k15 * k33 * t3 * t8 * 3.0 - k16 * k36 * t3 * t4 * 4.0 - k15 * k33 * t2 * t10 * 3.0 - k15 * k33 * t3 * t9 - k15 * k33 * t4 * t8 * 6.0 + k15 * k33 * t2 * t11 * 2.0 - k15 * k33 * t3 * t10 * 6.0 - k15 * k33 * t3 * t11 - k15 * k33 * t4 * t10 * 3.0 - k15 * k33 * t2 * t13 * 2.0 - k15 * k33 * t3 * t12 - k15 * k33 * t4 * t11 * 6.0 + k15 * k33 * t3 * t13 + k26 * k36 * t2 * t3 * 2.0 + k26 * k36 * t2 * t4 * 2.0 + k25 * k35 * t2 * t8 * 4.0 + k25 * k35 * t2 * t9 * 4.0 - k25 * k35 * t3 * t8 * 4.0 + k25 * k35 * t2 * t10 * 2.0 - k25 * k35 * t3 * t9 * 4.0 - k25 * k35 * t4 * t8 * 4.0 - k25 * k35 * t2 * t11 * 2.0 - k25 * k35 * t4 * t9 * 4.0 + k25 * k35 * t2 * t12 * 2.0 - k25 * k35 * t2 * t13 * 2.0 + k16 * t3 * t4 * t8 + k16 * t2 * t4 * t10 * 2.0 + k16 * t3 * t4 * t9 + k16 * t2 * t3 * t12 * 2.0 - k16 * t2 * t4 * t11 * 2.0 + k16 * t3 * t4 * t10 * 6.0 - k16 * t2 * t3 * t13 * 2.0 + k16 * t3 * t4 * t12 * 6.0 + k26 * t2 * t4 * t8 * 2.0 + k26 * t3 * t4 * t8 * 2.0 + k26 * t2 * t4 * t10 + k26 * t2 * t3 * t12 + k26 * t2 * t4 * t11 * 3.0 + k26 * t2 * t4 * t13 + k36 * t2 * t3 * t9 * 2.0 + k36 * t2 * t3 * t11 + k36 * t2 * t4 * t10 + k36 * t3 * t4 * t9 * 2.0 + k36 * t2 * t3 * t12 + k36 * t2 * t3 * t13 * 3.0 + t2 * t4 * t8 * t11 + t2 * t3 * t9 * t13 - k12 * k15 * k16 * k22 * k25 * 4.0 + k12 * k13 * k16 * k25 * k32 * 4.0 - k13 * k15 * k16 * k22 * k32 * 4.0 - k12 * k13 * k15 * k25 * k35 * 3.2E1 + k12 * k15 * k16 * k22 * k35 * 4.0 + k12 * k15 * k22 * k25 * k26 * 1.2E1 - k12 * k13 * k16 * k32 * k35 * 4.0 - k12 * k13 * k25 * k26 * k32 * 4.0 + k12 * k16 * k22 * k25 * k33 * 4.0 + k12 * k16 * k22 * k26 * k32 * 4.0 + k12 * k16 * k23 * k25 * k32 * 4.0 - k13 * k15 * k22 * k25 * k33 * 1.2E1 - k13 * k15 * k22 * k26 * k32 * 1.2E1 - k13 * k15 * k23 * k25 * k32 * 1.2E1 + k15 * k16 * k22 * k23 * k32 * 1.2E1 + k12 * k15 * k22 * k25 * k36 * 2.0E1 + k12 * k15 * k22 * k26 * k35 * 2.0E1 + k12 * k15 * k23 * k25 * k35 * 2.0E1 + k13 * k16 * k22 * k32 * k33 * 1.2E1 + k12 * k13 * k25 * k32 * k36 * 2.0E1 + k12 * k13 * k25 * k33 * k35 * 2.0E1 + k12 * k13 * k26 * k32 * k35 * 2.0E1 + k12 * k16 * k22 * k32 * k36 * 4.0 + k12 * k16 * k22 * k33 * k35 * 4.0 + k12 * k16 * k23 * k32 * k35 * 4.0 - k12 * k22 * k25 * k26 * k33 * 4.0 - k12 * k23 * k25 * k26 * k32 * 4.0 - k13 * k15 * k22 * k32 * k36 * 1.2E1 - k13 * k15 * k22 * k33 * k35 * 1.2E1 - k13 * k15 * k23 * k32 * k35 * 1.2E1 + k15 * k22 * k23 * k25 * k33 * 4.0 + k15 * k22 * k23 * k26 * k32 * 4.0 - k12 * k15 * k22 * k35 * k36 * 4.0 + k13 * k22 * k26 * k32 * k33 * 1.2E1 + k13 * k23 * k25 * k32 * k33 * 1.2E1 - k16 * k22 * k23 * k32 * k33 * 3.2E1 + k12 * k13 * k32 * k35 * k36 * 1.2E1 - k12 * k22 * k25 * k33 * k36 * 2.0E1 - k12 * k22 * k26 * k32 * k36 * 2.0E1 - k12 * k22 * k26 * k33 * k35 * 2.0E1 - k12 * k23 * k25 * k32 * k36 * 2.0E1 - k12 * k23 * k25 * k33 * k35 * 2.0E1 - k12 * k23 * k26 * k32 * k35 * 2.0E1 + k15 * k22 * k23 * k32 * k36 * 1.2E1 + k15 * k22 * k23 * k33 * k35 * 1.2E1 + k13 * k22 * k32 * k33 * k36 * 4.0 + k13 * k23 * k32 * k33 * k35 * 4.0 + k22 * k23 * k26 * k32 * k33 * 8.0 - k12 * k22 * k33 * k35 * k36 * 4.0 - k12 * k23 * k32 * k35 * k36 * 4.0 + k22 * k23 * k32 * k33 * k36 * 8.0 + k12 * k13 * k15 * k26 * t3 * 4.0 + k12 * k13 * k15 * k26 * t4 * 4.0 - k12 * k15 * k16 * k23 * t4 * 2.0 - k12 * k13 * k16 * k33 * t3 * 2.0 - k12 * k15 * k23 * k26 * t2 * 2.0 - k13 * k16 * k22 * k25 * t2 * 2.0 + k12 * k13 * k15 * k36 * t3 * 4.0 - k12 * k15 * k23 * k26 * t3 * 6.0 - k13 * k16 * k22 * k25 * t3 * 2.0 + k12 * k13 * k15 * k36 * t4 * 4.0 - k12 * k15 * k23 * k26 * t4 * 2.0 + k13 * k16 * k22 * k25 * t4 * 2.0 - k12 * k15 * k22 * k25 * t8 * 2.0 - k12 * k15 * k22 * k25 * t10 * 6.0 - k12 * k13 * k26 * k33 * t2 * 4.0 - k12 * k15 * k22 * k25 * t12 * 2.0 + k12 * k16 * k23 * k33 * t2 + k13 * k15 * k23 * k33 * t2 * 3.0 - k12 * k13 * k26 * k33 * t3 * 2.0 + k12 * k15 * k22 * k25 * t13 * 4.0 - k12 * k16 * k23 * k33 * t3 * 3.0 + k13 * k15 * k23 * k33 * t3 * 9.0 - k12 * k13 * k26 * k33 * t4 * 6.0 - k12 * k15 * k23 * k36 * t2 * 4.0 - k12 * k16 * k23 * k33 * t4 * 3.0 + k13 * k15 * k23 * k33 * t4 * 9.0 - k13 * k16 * k22 * k35 * t2 * 2.0 - k13 * k22 * k25 * k26 * t2 * 4.0 - k12 * k15 * k23 * k36 * t3 * 6.0 + k13 * k16 * k22 * k35 * t3 * 4.0 - k12 * k13 * k25 * k32 * t8 * 4.0 - k12 * k15 * k23 * k36 * t4 * 2.0 + k12 * k16 * k22 * k32 * t8 * 3.0 + k13 * k15 * k22 * k32 * t8 + k13 * k16 * k22 * k35 * t4 * 4.0 + k13 * k22 * k25 * k26 * t4 * 2.0 - k15 * k16 * k25 * k32 * t2 * 2.0 - k16 * k22 * k23 * k25 * t4 * 8.0 - k12 * k13 * k25 * k32 * t9 * 2.0 + k12 * k16 * k22 * k32 * t9 * 3.0 + k13 * k15 * k22 * k32 * t9 + k15 * k16 * k25 * k32 * t3 * 4.0 - k12 * k13 * k25 * k32 * t10 * 2.0 - k12 * k15 * k22 * k35 * t8 * 2.0 - k12 * k16 * k22 * k32 * t10 * 3.0 + k13 * k15 * k22 * k32 * t10 * 9.0 + k15 * k16 * k25 * k32 * t4 * 4.0 - k12 * k13 * k25 * k32 * t11 * 2.0 - k12 * k15 * k22 * k35 * t9 * 4.0 - k12 * k16 * k22 * k32 * t11 - k13 * k15 * k22 * k32 * t11 - k12 * k13 * k25 * k32 * t12 * 6.0 - k12 * k15 * k22 * k35 * t10 * 6.0 - k12 * k16 * k22 * k32 * t12 * 3.0 + k13 * k15 * k22 * k32 * t12 * 9.0; coeffs(4) += k12 * k13 * k25 * k32 * t13 * 6.0 + k12 * k15 * k22 * k35 * t11 * 6.0 - k12 * k16 * k22 * k32 * t13 - k13 * k15 * k22 * k32 * t13 - k12 * k13 * k33 * k36 * t2 * 2.0 - k12 * k15 * k22 * k35 * t12 * 2.0 + k12 * k23 * k26 * k33 * t2 * 3.0 - k12 * k13 * k33 * k36 * t3 * 2.0 - k12 * k15 * k22 * k35 * t13 * 2.0 + k12 * k23 * k26 * k33 * t3 * 3.0 - k12 * k13 * k33 * k36 * t4 * 6.0 + k12 * k23 * k26 * k33 * t4 * 9.0 - k16 * k22 * k23 * k35 * t2 * 8.0 + k22 * k23 * k25 * k26 * t2 * 4.0 + k13 * k22 * k25 * k36 * t3 * 2.0 + k13 * k22 * k26 * k35 * t3 * 2.0 + k13 * k23 * k25 * k35 * t3 * 6.0 - k16 * k22 * k23 * k35 * t3 * 8.0 + k12 * k22 * k25 * k33 * t8 * 5.0 + k12 * k22 * k26 * k32 * t8 * 5.0 + k12 * k23 * k25 * k32 * t8 * 5.0 + k13 * k22 * k25 * k36 * t4 * 4.0 + k13 * k22 * k26 * k35 * t4 * 4.0 + k13 * k23 * k25 * k35 * t4 * 4.0 - k15 * k16 * k32 * k35 * t2 * 2.0 - k15 * k22 * k23 * k32 * t8 * 6.0 + k15 * k25 * k26 * k32 * t2 * 4.0 - k16 * k22 * k23 * k35 * t4 * 8.0 + k22 * k23 * k25 * k26 * t4 * 4.0 - k12 * k13 * k32 * k35 * t9 * 2.0 - k12 * k22 * k25 * k33 * t9 - k12 * k22 * k26 * k32 * t9 - k12 * k23 * k25 * k32 * t9 + k15 * k16 * k32 * k35 * t3 * 2.0 - k15 * k22 * k23 * k32 * t9 * 2.0 - k15 * k25 * k26 * k32 * t3 * 2.0 - k12 * k13 * k32 * k35 * t10 * 2.0 + k12 * k22 * k25 * k33 * t10 * 3.0 + k12 * k22 * k26 * k32 * t10 * 3.0 + k12 * k23 * k25 * k32 * t10 - k15 * k16 * k32 * k35 * t4 * 2.0 - k15 * k22 * k23 * k32 * t10 * 4.0 - k15 * k25 * k26 * k32 * t4 * 4.0 + k12 * k13 * k32 * k35 * t11 * 4.0 + k12 * k22 * k25 * k33 * t11 + k12 * k22 * k26 * k32 * t11 * 3.0 + k12 * k23 * k25 * k32 * t11 - k15 * k22 * k23 * k32 * t11 * 2.0 - k12 * k13 * k32 * k35 * t12 * 6.0 + k12 * k22 * k25 * k33 * t12 * 3.0 + k12 * k22 * k26 * k32 * t12 * 9.0 + k12 * k23 * k25 * k32 * t12 * 9.0 - k15 * k22 * k23 * k32 * t12 * 6.0 - k12 * k22 * k25 * k33 * t13 * 5.0 - k12 * k22 * k26 * k32 * t13 * 5.0 - k12 * k23 * k25 * k32 * t13 * 5.0 + k15 * k22 * k23 * k32 * t13 * 2.0 + k12 * k23 * k33 * k36 * t2 * 3.0 + k12 * k23 * k33 * k36 * t3 * 9.0 + k12 * k23 * k33 * k36 * t4 * 3.0 - k13 * k22 * k32 * k33 * t8 * 2.0 + k13 * k22 * k35 * k36 * t2 * 4.0 - k16 * k25 * k32 * k33 * t2 * 8.0 + k22 * k23 * k25 * k36 * t2 * 4.0 + k22 * k23 * k26 * k35 * t2 * 4.0 - k13 * k22 * k32 * k33 * t9 * 6.0 - k13 * k22 * k35 * k36 * t3 * 4.0 - k16 * k25 * k32 * k33 * t3 * 8.0 - k12 * k22 * k32 * k36 * t8 - k12 * k22 * k33 * k35 * t8 - k12 * k23 * k32 * k35 * t8 - k13 * k22 * k32 * k33 * t10 * 6.0 - k13 * k22 * k35 * k36 * t4 * 2.0 - k16 * k25 * k32 * k33 * t4 * 8.0 + k12 * k22 * k32 * k36 * t9 * 5.0 + k12 * k22 * k33 * k35 * t9 * 5.0 + k12 * k23 * k32 * k35 * t9 * 5.0 + k13 * k22 * k32 * k33 * t11 * 2.0 + k15 * k25 * k32 * k36 * t3 * 4.0 + k15 * k25 * k33 * k35 * t3 * 4.0 + k15 * k26 * k32 * k35 * t3 * 4.0 + k12 * k22 * k32 * k36 * t10 * 9.0 + k12 * k22 * k33 * k35 * t10 * 9.0 + k12 * k23 * k32 * k35 * t10 * 3.0 - k13 * k22 * k32 * k33 * t12 * 4.0 + k15 * k25 * k32 * k36 * t4 * 2.0 + k15 * k25 * k33 * k35 * t4 * 6.0 + k15 * k26 * k32 * k35 * t4 * 2.0 - k12 * k22 * k32 * k36 * t11 * 5.0 - k12 * k22 * k33 * k35 * t11 * 5.0 - k12 * k23 * k32 * k35 * t11 * 5.0 - k13 * k22 * k32 * k33 * t13 * 2.0 + k12 * k22 * k32 * k36 * t12 * 3.0 + k12 * k22 * k33 * k35 * t12 + k12 * k23 * k32 * k35 * t12 * 3.0 + k12 * k22 * k32 * k36 * t13 * 3.0 + k12 * k22 * k33 * k35 * t13 + k12 * k23 * k32 * k35 * t13 + k22 * k23 * k32 * k33 * t8 * 8.0 + k22 * k23 * k35 * k36 * t2 * 1.2E1 + k25 * k26 * k32 * k33 * t2 * 1.2E1 - k16 * k32 * k33 * k35 * t3 * 8.0 + k22 * k23 * k32 * k33 * t9 * 8.0 + k22 * k23 * k35 * k36 * t3 * 8.0 + k25 * k26 * k32 * k33 * t3 * 4.0 - k15 * k32 * k35 * k36 * t2 * 4.0 + k22 * k23 * k35 * k36 * t4 * 4.0 + k25 * k26 * k32 * k33 * t4 * 8.0 + k15 * k32 * k35 * k36 * t3 * 2.0 + k22 * k23 * k32 * k33 * t11 * 4.0 + k22 * k23 * k32 * k33 * t13 * 4.0 + k25 * k32 * k33 * k36 * t2 * 4.0 + k26 * k32 * k33 * k35 * t2 * 4.0 + k32 * k33 * k35 * k36 * t2 * 4.0 + k32 * k33 * k35 * k36 * t3 * 4.0 + k12 * k13 * k15 * t4 * t10 * 2.0 + k12 * k13 * k15 * t3 * t12 * 2.0 - k13 * k16 * k23 * t2 * t4 - k13 * k16 * k23 * t3 * t4 * 6.0 - k12 * k15 * k23 * t4 * t8 - k12 * k15 * k23 * t4 * t10 - k12 * k15 * k23 * t2 * t13 * 2.0 - k12 * k15 * k23 * t3 * t12 * 3.0 - k12 * k15 * k23 * t4 * t11 - k12 * k15 * k23 * t4 * t13 - k13 * k23 * k26 * t2 * t4 * 4.0 - k13 * k23 * k26 * t3 * t4 * 3.0 - k15 * k16 * k33 * t2 * t3 - k12 * k13 * k33 * t3 * t9 - k12 * k13 * k33 * t2 * t11 * 2.0 - k15 * k16 * k33 * t3 * t4 * 6.0 - k12 * k13 * k33 * t3 * t11 - k12 * k13 * k33 * t4 * t10 * 3.0 - k13 * k22 * k25 * t4 * t8 - k12 * k13 * k33 * t3 * t12 - k13 * k22 * k25 * t4 * t9 - k12 * k13 * k33 * t3 * t13 - k13 * k22 * k25 * t2 * t12 * 4.0 - k13 * k22 * k25 * t4 * t10 * 3.0 + k13 * k22 * k25 * t2 * t13 * 2.0 - k13 * k22 * k25 * t3 * t12 - k13 * k22 * k25 * t4 * t12 * 6.0 - k13 * k23 * k36 * t2 * t4 * 2.0 - k15 * k26 * k33 * t2 * t3 * 2.0 + k12 * k23 * k33 * t3 * t9 * 3.0 + k12 * k23 * k33 * t4 * t8 * 3.0 + k12 * k23 * k33 * t2 * t11 - k13 * k22 * k35 * t3 * t9 * 2.0 + k22 * k23 * k25 * t4 * t8 * 4.0 + k12 * k22 * k32 * t2 * t15 + k12 * k22 * k32 * t3 * t14 + k12 * k23 * k33 * t2 * t13 + k12 * k23 * k33 * t4 * t11 * 3.0 - k13 * k22 * k35 * t2 * t11 * 2.0 - k13 * k22 * k35 * t4 * t9 * 2.0 - k15 * k25 * k32 * t3 * t8 * 2.0 + k12 * k22 * k32 * t2 * t16 + k12 * k22 * k32 * t4 * t14 + k12 * k23 * k33 * t3 * t13 * 3.0 - k13 * k22 * k35 * t2 * t12 * 2.0 - k15 * k25 * k32 * t2 * t10 * 2.0 - k15 * k25 * k32 * t4 * t8 * 2.0 + k22 * k23 * k25 * t2 * t12 * 2.0 + k12 * k22 * k32 * t3 * t16 + k12 * k22 * k32 * t4 * t15 + k12 * k22 * k32 * t8 * t11 * 3.0 + k12 * k22 * k32 * t9 * t10 * 3.0 + k12 * k22 * k32 * t8 * t12 * 3.0 - k15 * k25 * k32 * t2 * t13 * 2.0 + k12 * k22 * k32 * t9 * t13 * 3.0 + k12 * k22 * k32 * t10 * t13 * 3.0 + k12 * k22 * k32 * t11 * t12 * 3.0 - k15 * k33 * k36 * t2 * t3 * 4.0 - k15 * k33 * k36 * t3 * t4 * 3.0 + k22 * k23 * k35 * t2 * t9 * 4.0 + k25 * k26 * k35 * t2 * t3 * 2.0 + k22 * k23 * k35 * t3 * t9 * 4.0 - k15 * k32 * k35 * t3 * t8 + k22 * k23 * k35 * t2 * t11 * 2.0 + k22 * k23 * k35 * t4 * t9 * 4.0 - k15 * k32 * k35 * t2 * t10 * 4.0 - k15 * k32 * k35 * t3 * t9 + k22 * k23 * k35 * t2 * t12 * 2.0 + k15 * k32 * k35 * t2 * t11 * 2.0 - k15 * k32 * k35 * t3 * t10 * 6.0 + k22 * k23 * k35 * t2 * t13 * 2.0 - k15 * k32 * k35 * t4 * t10 - k15 * k32 * k35 * t3 * t12 * 3.0 + k25 * k32 * k33 * t2 * t8 * 4.0 + k25 * k32 * k33 * t3 * t8 * 4.0 + k25 * k32 * k33 * t2 * t10 * 2.0 + k25 * k32 * k33 * t4 * t8 * 4.0 + k25 * k35 * k36 * t2 * t4 * 2.0 + k25 * k32 * k33 * t2 * t11 * 2.0 + k25 * k32 * k33 * t2 * t13 * 2.0 + k32 * k33 * k35 * t2 * t10 * 2.0 + k32 * k33 * k35 * t3 * t9 * 4.0 + k16 * k26 * t2 * t3 * t4 - k13 * k23 * t2 * t4 * t11 + k16 * k36 * t2 * t3 * t4 - k15 * k33 * t2 * t3 * t13 + k26 * k36 * t2 * t3 * t4 + k25 * k35 * t2 * t4 * t10 + k25 * k35 * t2 * t3 * t12; coeffs(4) += k12 * k13 * k15 * k22 * k23 * k25 * 8.0 - k12 * k13 * k16 * k22 * k23 * k32 * 4.0 + k12 * k13 * k15 * k22 * k23 * k35 * 8.0 - k12 * k13 * k22 * k23 * k25 * k33 * 4.0 - k12 * k13 * k22 * k23 * k26 * k32 * 4.0 + k12 * k13 * k15 * k25 * k32 * k33 * 8.0 - k12 * k15 * k16 * k22 * k32 * k33 * 4.0 - k12 * k13 * k22 * k23 * k32 * k36 * 4.0 - k12 * k13 * k22 * k23 * k33 * k35 * 4.0 + k12 * k13 * k15 * k32 * k33 * k35 * 8.0 - k12 * k15 * k22 * k26 * k32 * k33 * 4.0 - k12 * k15 * k23 * k25 * k32 * k33 * 4.0 + k12 * k16 * k22 * k25 * k32 * k35 * 6.0 - k13 * k15 * k22 * k25 * k32 * k35 * 1.0E1 - k12 * k15 * k22 * k32 * k33 * k36 * 4.0 - k12 * k15 * k23 * k32 * k33 * k35 * 4.0 - k12 * k22 * k25 * k26 * k32 * k35 * 1.0E1 + k15 * k22 * k23 * k25 * k32 * k35 * 8.0 + k13 * k22 * k25 * k32 * k33 * k35 * 8.0 - k12 * k22 * k25 * k32 * k35 * k36 * 1.0E1 + k12 * k15 * k16 * k22 * k25 * t4 * 2.0 - k12 * k13 * k16 * k25 * k32 * t3 * 4.0 + k13 * k15 * k16 * k22 * k32 * t3 * 2.0 + k13 * k15 * k16 * k22 * k32 * t4 * 2.0 + k12 * k13 * k15 * k25 * k35 * t3 * 4.0 + k12 * k13 * k15 * k25 * k35 * t4 * 4.0 - k12 * k15 * k16 * k22 * k35 * t4 * 4.0 - k12 * k15 * k22 * k25 * k26 * t4 * 2.0 - k12 * k13 * k25 * k26 * k32 * t2 * 4.0 + k12 * k16 * k22 * k25 * k33 * t2 + k12 * k16 * k22 * k26 * k32 * t2 + k12 * k16 * k23 * k25 * k32 * t2 + k13 * k15 * k22 * k25 * k33 * t2 * 3.0 + k13 * k15 * k22 * k26 * k32 * t2 * 3.0 + k13 * k15 * k23 * k25 * k32 * t2 * 3.0 - k15 * k16 * k22 * k23 * k32 * t2 * 2.0 + k12 * k13 * k16 * k32 * k35 * t3 * 2.0 - k12 * k13 * k22 * k23 * k32 * t9 * 2.0 - k12 * k13 * k25 * k26 * k32 * t3 * 2.0 + k12 * k16 * k22 * k25 * k33 * t3 + k12 * k16 * k22 * k26 * k32 * t3 + k12 * k16 * k23 * k25 * k32 * t3 * 3.0 + k13 * k15 * k22 * k25 * k33 * t3 + k13 * k15 * k22 * k26 * k32 * t3 + k13 * k15 * k23 * k25 * k32 * t3 * 3.0 - k15 * k16 * k22 * k23 * k32 * t3 * 4.0 - k12 * k15 * k22 * k25 * k36 * t2 * 4.0 - k12 * k15 * k22 * k26 * k35 * t2 * 4.0 - k12 * k15 * k23 * k25 * k35 * t2 * 4.0 - k12 * k16 * k22 * k25 * k33 * t4 * 6.0 - k12 * k16 * k22 * k26 * k32 * t4 * 2.0 - k12 * k16 * k23 * k25 * k32 * t4 * 2.0 + k13 * k15 * k22 * k25 * k33 * t4 * 6.0 + k13 * k15 * k22 * k26 * k32 * t4 * 2.0 + k13 * k15 * k23 * k25 * k32 * t4 * 2.0 - k15 * k16 * k22 * k23 * k32 * t4 * 4.0 - k12 * k13 * k22 * k23 * k32 * t11 * 2.0 - k12 * k15 * k22 * k25 * k36 * t3 * 2.0 - k12 * k15 * k22 * k26 * k35 * t3 * 2.0 - k12 * k15 * k23 * k25 * k35 * t3 * 6.0 - k12 * k13 * k22 * k23 * k32 * t12 * 6.0 - k12 * k13 * k22 * k23 * k32 * t13 * 2.0 - k13 * k16 * k22 * k32 * k33 * t2 * 2.0 - k13 * k16 * k22 * k32 * k33 * t3 * 4.0 - k12 * k13 * k25 * k32 * k36 * t2 * 4.0 - k12 * k13 * k25 * k33 * k35 * t2 * 4.0 - k12 * k13 * k26 * k32 * k35 * t2 * 4.0 + k12 * k16 * k22 * k32 * k36 * t2 + k12 * k16 * k22 * k33 * k35 * t2 + k12 * k16 * k23 * k32 * k35 * t2 + k12 * k22 * k25 * k26 * k33 * t2 * 2.0 + k12 * k23 * k25 * k26 * k32 * t2 * 2.0 + k13 * k15 * k22 * k32 * k36 * t2 * 3.0 + k13 * k15 * k22 * k33 * k35 * t2 * 3.0 + k13 * k15 * k23 * k32 * k35 * t2 * 3.0 - k13 * k16 * k22 * k32 * k33 * t4 * 4.0 - k15 * k22 * k23 * k25 * k33 * t2 * 4.0 - k15 * k22 * k23 * k26 * k32 * t2 * 4.0 - k12 * k16 * k22 * k32 * k36 * t3 * 2.0 - k12 * k16 * k22 * k33 * k35 * t3 * 2.0 - k12 * k16 * k23 * k32 * k35 * t3 * 6.0 + k13 * k15 * k22 * k32 * k36 * t3 * 2.0 + k13 * k15 * k22 * k33 * k35 * t3 * 2.0 + k13 * k15 * k23 * k32 * k35 * t3 * 6.0 - k12 * k13 * k25 * k32 * k36 * t4 * 2.0 - k12 * k13 * k25 * k33 * k35 * t4 * 6.0 - k12 * k13 * k26 * k32 * k35 * t4 * 2.0 - k12 * k15 * k22 * k32 * k33 * t8 * 2.0 - k12 * k15 * k22 * k35 * k36 * t2 * 4.0 + k12 * k16 * k22 * k32 * k36 * t4 + k12 * k16 * k22 * k33 * k35 * t4 * 3.0 + k12 * k16 * k23 * k32 * k35 * t4 + k12 * k22 * k25 * k26 * k33 * t4 * 6.0 + k12 * k23 * k25 * k26 * k32 * t4 * 2.0 + k13 * k15 * k22 * k32 * k36 * t4 + k13 * k15 * k22 * k33 * k35 * t4 * 3.0 + k13 * k15 * k23 * k32 * k35 * t4 - k12 * k15 * k22 * k32 * k33 * t10 * 6.0 - k12 * k15 * k22 * k35 * k36 * t4 * 2.0 - k12 * k15 * k22 * k32 * k33 * t11 * 2.0 - k12 * k15 * k22 * k32 * k33 * t13 * 2.0 - k13 * k22 * k26 * k32 * k33 * t2 * 8.0 - k13 * k23 * k25 * k32 * k33 * t2 * 8.0; coeffs(4) += k16 * k22 * k23 * k32 * k33 * t2 * 8.0 - k13 * k22 * k26 * k32 * k33 * t3 * 2.0 - k13 * k23 * k25 * k32 * k33 * t3 * 6.0 + k16 * k22 * k23 * k32 * k33 * t3 * 8.0 + k12 * k22 * k25 * k33 * k36 * t2 * 4.0 + k12 * k22 * k26 * k32 * k36 * t2 * 4.0 + k12 * k22 * k26 * k33 * k35 * t2 * 4.0 + k12 * k23 * k25 * k32 * k36 * t2 * 4.0 + k12 * k23 * k25 * k33 * k35 * t2 * 4.0 + k12 * k23 * k26 * k32 * k35 * t2 * 4.0 - k13 * k22 * k26 * k32 * k33 * t4 * 4.0 - k13 * k23 * k25 * k32 * k33 * t4 * 4.0 - k15 * k22 * k23 * k32 * k36 * t2 * 8.0 - k15 * k22 * k23 * k33 * k35 * t2 * 8.0 + k16 * k22 * k23 * k32 * k33 * t4 * 8.0 - k12 * k13 * k32 * k35 * k36 * t3 * 2.0 + k12 * k22 * k25 * k33 * k36 * t3 + k12 * k22 * k26 * k32 * k36 * t3 + k12 * k22 * k26 * k33 * k35 * t3 + k12 * k23 * k25 * k32 * k36 * t3 * 3.0 + k12 * k23 * k25 * k33 * k35 * t3 * 3.0 + k12 * k23 * k26 * k32 * k35 * t3 * 3.0 - k15 * k22 * k23 * k32 * k36 * t3 * 4.0 - k15 * k22 * k23 * k33 * k35 * t3 * 4.0 + k12 * k22 * k25 * k33 * k36 * t4 * 3.0 + k12 * k22 * k26 * k32 * k36 * t4 + k12 * k22 * k26 * k33 * k35 * t4 * 3.0 + k12 * k23 * k25 * k32 * k36 * t4 + k12 * k23 * k25 * k33 * k35 * t4 * 3.0 + k12 * k23 * k26 * k32 * k35 * t4 - k15 * k22 * k23 * k32 * k36 * t4 * 2.0 - k15 * k22 * k23 * k33 * k35 * t4 * 6.0 - k12 * k22 * k25 * k32 * k35 * t8 - k12 * k22 * k25 * k32 * k35 * t9 + k12 * k22 * k25 * k32 * k35 * t10 * 3.0 + k12 * k22 * k25 * k32 * k35 * t12 * 3.0 - k13 * k22 * k32 * k33 * k36 * t2 * 4.0 - k13 * k23 * k32 * k33 * k35 * t2 * 4.0 + k22 * k23 * k26 * k32 * k33 * t2 * 4.0 + k12 * k22 * k33 * k35 * k36 * t2 * 2.0 + k12 * k23 * k32 * k35 * k36 * t2 * 2.0 + k12 * k22 * k33 * k35 * k36 * t3 * 2.0 + k12 * k23 * k32 * k35 * k36 * t3 * 6.0 + k22 * k23 * k32 * k33 * k36 * t2 * 4.0 + k12 * k13 * k15 * k16 * t3 * t4 + k12 * k13 * k15 * k26 * t3 * t4 - k12 * k15 * k16 * k23 * t3 * t4 * 3.0 - k12 * k13 * k16 * k33 * t3 * t4 * 3.0 - k13 * k16 * k22 * k25 * t2 * t4 + k12 * k13 * k15 * k36 * t3 * t4 - k12 * k15 * k22 * k25 * t4 * t8 - k12 * k15 * k22 * k25 * t2 * t13 * 2.0 - k12 * k13 * k26 * k33 * t3 * t4 * 3.0 + k12 * k16 * k23 * k33 * t3 * t4 * 9.0 - k13 * k22 * k25 * k26 * t2 * t4 * 2.0 + k16 * k22 * k23 * k25 * t2 * t4 * 2.0 - k12 * k15 * k23 * k36 * t3 * t4 * 3.0 - k12 * k13 * k25 * k32 * t4 * t10 + k12 * k16 * k22 * k32 * t4 * t10 * 3.0 - k12 * k13 * k25 * k32 * t3 * t12 * 3.0 + k12 * k16 * k22 * k32 * t3 * t12 * 3.0 - k12 * k15 * k22 * k35 * t4 * t10 * 3.0 - k12 * k15 * k22 * k35 * t3 * t12 - k13 * k22 * k25 * k36 * t2 * t4 * 2.0 - k13 * k22 * k26 * k35 * t2 * t4 * 2.0 - k13 * k23 * k25 * k35 * t2 * t4 * 2.0 + k16 * k22 * k23 * k35 * t2 * t4 * 2.0 - k15 * k16 * k32 * k35 * t2 * t3 - k12 * k13 * k32 * k35 * t3 * t9 + k12 * k22 * k25 * k33 * t4 * t8 * 3.0 + k12 * k22 * k26 * k32 * t4 * t8 + k12 * k23 * k25 * k32 * t4 * t8 - k12 * k13 * k32 * k35 * t2 * t11 * 2.0 + k12 * k22 * k25 * k33 * t2 * t13 + k12 * k22 * k26 * k32 * t2 * t13 + k12 * k23 * k25 * k32 * t2 * t13 - k15 * k22 * k23 * k32 * t2 * t13 * 2.0 + k16 * k25 * k32 * k33 * t2 * t3 * 2.0 + k22 * k23 * k25 * k36 * t2 * t4 * 2.0 + k22 * k23 * k26 * k35 * t2 * t4 * 2.0 - k13 * k22 * k32 * k33 * t2 * t11 * 2.0 - k15 * k25 * k32 * k36 * t2 * t3 * 2.0 - k15 * k25 * k33 * k35 * t2 * t3 * 2.0 - k15 * k26 * k32 * k35 * t2 * t3 * 2.0 + k12 * k22 * k32 * k36 * t3 * t9 + k12 * k22 * k33 * k35 * t3 * t9 + k12 * k23 * k32 * k35 * t3 * t9 * 3.0 + k12 * k22 * k32 * k36 * t2 * t11 + k12 * k22 * k33 * k35 * t2 * t11 + k12 * k23 * k32 * k35 * t2 * t11 + k16 * k32 * k33 * k35 * t2 * t3 * 2.0 - k15 * k32 * k35 * k36 * t2 * t3 * 2.0 + k25 * k32 * k33 * k36 * t2 * t3 * 2.0 + k26 * k32 * k33 * k35 * t2 * t3 * 2.0 + k12 * k13 * k15 * k22 * k23 * k32 * k33 * 8.0 + k12 * k13 * k15 * k22 * k23 * k25 * t4 * 2.0 - k12 * k13 * k16 * k22 * k23 * k32 * t4 * 2.0 + k12 * k13 * k15 * k22 * k23 * k35 * t4 * 2.0 - k12 * k13 * k22 * k23 * k25 * k33 * t4 * 6.0 - k12 * k13 * k22 * k23 * k26 * k32 * t4 * 2.0 + k12 * k13 * k15 * k25 * k32 * k33 * t3 * 2.0 - k12 * k15 * k16 * k22 * k32 * k33 * t3 * 2.0 + k12 * k13 * k15 * k32 * k33 * k35 * t3 * 2.0 + k12 * k16 * k22 * k25 * k32 * k35 * t2 + k13 * k15 * k22 * k25 * k32 * k35 * t2 * 3.0 - k12 * k15 * k22 * k32 * k33 * k36 * t3 * 2.0 - k12 * k15 * k23 * k32 * k33 * k35 * t3 * 6.0 + k12 * k22 * k25 * k26 * k32 * k35 * t2 * 2.0 - k15 * k22 * k23 * k25 * k32 * k35 * t2 * 4.0 - k13 * k22 * k25 * k32 * k33 * k35 * t2 * 4.0 + k12 * k22 * k25 * k32 * k35 * k36 * t2 * 2.0 + k22 * k23 * k25 * k32 * k33 * k35 * t2 * 4.0; coeffs(5) = k25 * t14 * 2.4E1 + k25 * t15 * 2.4E1 + k25 * t16 * 2.4E1 + k25 * t17 * 2.0 + k25 * t19 * 2.0 + k35 * t14 * 2.4E1 + k35 * t15 * 2.4E1 + k35 * t16 * 2.4E1 + k35 * t18 * 2.0 + k35 * t20 * 2.0 - k16 * k25 * k26 * 4.8E1 - k16 * k25 * k36 * 4.8E1 - k16 * k26 * k35 * 4.8E1 - k16 * k35 * k36 * 4.8E1 + k25 * k26 * k36 * 4.8E1 + k26 * k35 * k36 * 4.8E1 + k13 * k22 * t14 * 8.0 - k16 * k25 * t8 * 1.2E1 - k13 * k22 * t15 * 8.0 - k16 * k25 * t9 * 4.0 + k13 * k22 * t16 * 8.0 + k16 * k25 * t10 * 1.2E1 - k16 * k25 * t11 * 4.0 + k16 * k25 * t12 * 2.0E1 - k13 * k22 * t19 - k16 * k25 * t13 * 1.2E1 - k16 * k35 * t8 * 4.0 - k22 * k23 * t14 * 2.0E1 + k25 * k26 * t8 * 2.0E1 - k16 * k35 * t9 * 1.2E1 - k22 * k23 * t15 * 4.0 + k25 * k26 * t9 * 4.0 + k15 * k32 * t14 * 8.0 + k16 * k35 * t10 * 2.0E1 - k22 * k23 * t16 * 2.0E1 - k25 * k26 * t10 * 4.0 + k15 * k32 * t15 * 8.0 - k16 * k35 * t11 * 1.2E1 + k25 * k26 * t11 * 4.0 - k15 * k32 * t16 * 8.0 + k16 * k35 * t12 * 1.2E1 - k25 * k26 * t12 * 2.0E1 - k16 * k35 * t13 * 4.0 + k25 * k26 * t13 * 1.2E1 - k15 * k32 * t20 + k25 * k36 * t8 * 1.2E1 + k26 * k35 * t8 * 1.2E1 + k25 * k36 * t9 * 1.2E1 + k26 * k35 * t9 * 1.2E1 - k25 * k36 * t10 * 1.2E1 - k26 * k35 * t10 * 1.2E1 + k25 * k36 * t11 * 4.0 + k26 * k35 * t11 * 1.2E1 - k25 * k36 * t12 * 1.2E1 - k26 * k35 * t12 * 1.2E1 + k25 * k36 * t13 * 1.2E1 + k26 * k35 * t13 * 4.0 - k32 * k33 * t14 * 2.0E1 + k35 * k36 * t8 * 4.0 - k32 * k33 * t15 * 2.0E1 + k35 * k36 * t9 * 2.0E1 - k32 * k33 * t16 * 4.0 - k35 * k36 * t10 * 2.0E1 + k35 * k36 * t11 * 1.2E1 - k35 * k36 * t12 * 4.0 + k35 * k36 * t13 * 4.0 - k25 * t2 * t14 * 2.0 - k25 * t2 * t15 * 6.0 - k25 * t3 * t14 * 4.0 - k25 * t8 * t9 * 2.0 - k25 * t2 * t16 * 1.4E1 - k25 * t4 * t14 * 6.0 - k25 * t8 * t10 * 2.0 - k25 * t3 * t16 * 4.0 - k25 * t4 * t15 * 6.0 + k25 * t8 * t11 * 2.0 - k25 * t9 * t10 * 4.0 - k25 * t4 * t16 * 2.0 - k25 * t8 * t12 * 8.0 + k25 * t5 * t16 * 2.0 + k25 * t8 * t13 * 2.0 - k25 * t9 * t12 * 2.0 + k25 * t9 * t13 * 4.0 + k25 * t10 * t12 * 2.0 - k25 * t10 * t13 * 4.0 - k25 * t11 * t12 * 2.0 - k25 * t12 * t13 * 2.0 - k35 * t2 * t14 * 2.0 - k35 * t2 * t15 * 1.4E1 - k35 * t3 * t14 * 6.0 - k35 * t8 * t9 * 2.0 - k35 * t2 * t16 * 6.0 - k35 * t3 * t15 * 2.0 - k35 * t4 * t14 * 4.0 - k35 * t8 * t10 * 2.0 - k35 * t3 * t16 * 6.0 - k35 * t4 * t15 * 4.0 + k35 * t8 * t11 * 4.0 - k35 * t9 * t10 * 8.0 + k35 * t5 * t15 * 2.0 - k35 * t8 * t12 * 4.0 + k35 * t9 * t11 * 2.0 - k35 * t9 * t12 * 2.0 - k35 * t10 * t11 * 2.0 + k35 * t9 * t13 * 2.0 + k35 * t10 * t12 * 2.0 - k35 * t10 * t13 * 2.0 - k35 * t11 * t12 * 4.0 - k13 * k16 * k22 * k36 * 1.6E1 - k13 * k16 * k23 * k35 * 1.6E1 - k13 * k23 * k25 * k26 * 1.6E1 + k16 * k22 * k23 * k26 * 2.4E1 - k15 * k16 * k25 * k33 * 1.6E1 - k15 * k16 * k26 * k32 * 1.6E1 + k16 * k22 * k23 * k36 * 4.0E1 + k15 * k25 * k26 * k33 * 1.6E1 + k13 * k23 * k35 * k36 * 1.6E1 + k16 * k26 * k32 * k33 * 4.0E1 - k22 * k23 * k26 * k36 * 2.4E1 + k16 * k32 * k33 * k36 * 2.4E1 - k15 * k33 * k35 * k36 * 1.6E1 - k26 * k32 * k33 * k36 * 2.4E1 - k13 * k16 * k22 * t8 * 2.0 - k13 * k16 * k22 * t9 * 2.0 + k13 * k16 * k22 * t10 * 6.0 + k13 * k16 * k22 * t11 * 2.0 - k12 * k15 * k22 * t14 * 2.0 + k13 * k16 * k22 * t12 * 6.0 + k12 * k15 * k22 * t15 * 6.0 - k13 * k16 * k22 * t13 * 6.0 - k12 * k15 * k22 * t16 * 2.0 - k13 * k22 * k26 * t8 * 2.0 - k13 * k23 * k25 * t8 * 2.0 + k16 * k22 * k23 * t8 * 4.0 + k16 * k25 * k26 * t2 * 8.0 + k13 * k22 * k26 * t9 * 6.0 + k13 * k23 * k25 * t9 * 6.0 + k16 * k22 * k23 * t9 * 4.0 + k16 * k25 * k26 * t3 * 4.0 - k12 * k13 * k32 * t14 * 2.0 + k13 * k22 * k26 * t10 * 6.0 + k13 * k23 * k25 * t10 * 2.0 - k15 * k16 * k32 * t8 * 2.0 - k16 * k22 * k23 * t10 * 8.0 + k16 * k25 * k26 * t4 * 1.2E1 - k12 * k13 * k32 * t15 * 2.0 - k13 * k22 * k26 * t11 * 6.0 - k13 * k23 * k25 * t11 * 2.0 - k15 * k16 * k32 * t9 * 2.0 + k16 * k22 * k23 * t11 * 4.0 + k12 * k13 * k32 * t16 * 6.0 + k13 * k22 * k26 * t12 * 6.0 + k13 * k23 * k25 * t12 * 6.0 + k15 * k16 * k32 * t10 * 6.0 - k16 * k22 * k23 * t12 * 1.6E1 + k13 * k22 * k26 * t13 * 2.0 + k13 * k23 * k25 * t13 * 2.0 - k15 * k16 * k32 * t11 * 6.0 + k16 * k22 * k23 * t13 * 1.2E1 + k15 * k16 * k32 * t12 * 6.0 + k15 * k16 * k32 * t13 * 2.0 + k13 * k22 * k36 * t8 * 2.0 + k13 * k23 * k35 * t8 * 2.0 + k16 * k25 * k36 * t2 * 1.6E1 + k16 * k26 * k35 * t2 * 1.6E1 - k22 * k23 * k26 * t8 * 4.0 + k13 * k22 * k36 * t9 * 1.0E1 + k13 * k23 * k35 * t9 * 1.0E1 + k16 * k25 * k36 * t3 * 8.0 + k16 * k26 * k35 * t3 * 8.0 - k22 * k23 * k26 * t9 * 8.0 - k12 * k22 * k33 * t14 * 2.0 - k12 * k23 * k32 * t14 * 2.0 - k13 * k22 * k36 * t10 * 6.0 - k13 * k23 * k35 * t10 * 2.0 + k15 * k25 * k33 * t8 * 1.0E1 + k15 * k26 * k32 * t8 * 1.0E1 + k16 * k25 * k36 * t4 * 8.0 + k16 * k26 * k35 * t4 * 8.0 - k12 * k22 * k33 * t15 * 2.0 - k12 * k23 * k32 * t15 * 2.0 - k13 * k22 * k36 * t11 * 2.0 - k13 * k23 * k35 * t11 * 2.0 + k15 * k25 * k33 * t9 * 2.0 + k15 * k26 * k32 * t9 * 2.0 - k12 * k22 * k33 * t16 * 2.0 - k12 * k23 * k32 * t16 * 2.0 + k13 * k22 * k36 * t12 * 2.0 + k13 * k23 * k35 * t12 * 2.0 + k15 * k25 * k33 * t10 * 2.0 + k15 * k26 * k32 * t10 * 2.0 + k22 * k23 * k26 * t12 * 4.0 + k13 * k22 * k36 * t13 * 6.0 + k13 * k23 * k35 * t13 * 2.0 + k15 * k25 * k33 * t11 * 2.0 + k15 * k26 * k32 * t11 * 6.0 - k22 * k23 * k26 * t13 * 8.0 - k15 * k25 * k33 * t12 * 2.0 - k15 * k26 * k32 * t12 * 6.0 - k15 * k25 * k33 * t13 * 2.0 - k15 * k26 * k32 * t13 * 2.0 + k16 * k32 * k33 * t8 * 4.0 + k16 * k35 * k36 * t2 * 8.0 - k22 * k23 * k36 * t8 * 4.0 - k25 * k26 * k36 * t2 * 2.8E1 + k16 * k32 * k33 * t9 * 4.0 + k16 * k35 * k36 * t3 * 1.2E1 - k22 * k23 * k36 * t9 * 1.6E1 - k25 * k26 * k36 * t3 * 4.0 + k15 * k32 * k36 * t8 * 6.0 + k15 * k33 * k35 * t8 * 6.0 - k16 * k32 * k33 * t10 * 1.6E1 + k16 * k35 * k36 * t4 * 4.0 + k22 * k23 * k36 * t10 * 8.0 - k25 * k26 * k36 * t4 * 8.0 - k15 * k32 * k36 * t9 * 2.0 - k15 * k33 * k35 * t9 * 2.0 + k16 * k32 * k33 * t11 * 1.2E1 - k22 * k23 * k36 * t11 * 4.0 + k25 * k26 * k36 * t5 * 4.0 + k15 * k32 * k36 * t10 * 6.0 + k15 * k33 * k35 * t10 * 6.0 - k16 * k32 * k33 * t12 * 8.0 + k22 * k23 * k36 * t12 * 4.0 + k15 * k32 * k36 * t11 * 2.0 + k15 * k33 * k35 * t11 * 2.0 + k16 * k32 * k33 * t13 * 4.0 - k22 * k23 * k36 * t13 * 1.2E1 + k15 * k32 * k36 * t12 * 6.0 + k15 * k33 * k35 * t12 * 2.0 - k15 * k32 * k36 * t13 * 6.0 - k15 * k33 * k35 * t13 * 2.0 - k26 * k32 * k33 * t8 * 1.6E1 - k26 * k35 * k36 * t2 * 2.8E1 - k26 * k32 * k33 * t9 * 4.0 - k26 * k35 * k36 * t3 * 8.0 + k26 * k32 * k33 * t10 * 4.0 - k26 * k35 * k36 * t4 * 4.0 - k26 * k32 * k33 * t11 * 1.2E1 + k26 * k35 * k36 * t5 * 4.0 + k26 * k32 * k33 * t12 * 8.0 - k26 * k32 * k33 * t13 * 4.0 - k32 * k33 * k36 * t8 * 8.0 - k32 * k33 * k36 * t9 * 4.0 + k32 * k33 * k36 * t10 * 4.0 - k32 * k33 * k36 * t11 * 8.0 - k13 * k22 * t2 * t15 * 2.0 - k13 * k22 * t3 * t14 * 2.0 + k16 * k25 * t3 * t8 * 4.0 + k13 * k22 * t2 * t16 * 2.0 - k13 * k22 * t4 * t14 * 3.0 + k16 * k25 * t4 * t8 * 2.0 - k13 * k22 * t3 * t16 * 2.0 + k13 * k22 * t4 * t15 - k13 * k22 * t8 * t11 * 2.0 - k13 * k22 * t9 * t10 * 6.0 - k16 * k25 * t4 * t9 * 2.0 - k13 * k22 * t4 * t16 - k13 * k22 * t8 * t12 - k16 * k25 * t2 * t12 * 4.0 - k16 * k25 * t4 * t10 * 4.0 - k13 * k22 * t9 * t12 * 3.0 + k16 * k25 * t2 * t13 * 4.0 - k16 * k25 * t3 * t12 * 4.0 + k13 * k22 * t9 * t13 * 2.0 - k13 * k22 * t10 * t12 * 3.0 - k16 * k25 * t4 * t12 * 1.2E1 - k13 * k22 * t10 * t13 * 6.0 + k13 * k22 * t11 * t12 - k13 * k22 * t12 * t13 + k22 * k23 * t2 * t14 * 2.0 + k25 * k26 * t2 * t8 * 4.0 - k16 * k35 * t3 * t8 * 2.0 + k22 * k23 * t2 * t15 * 2.0 + k22 * k23 * t3 * t14 * 4.0 + k22 * k23 * t8 * t9 * 2.0 + k25 * k26 * t3 * t8 * 2.0 - k16 * k35 * t2 * t10 * 4.0 + k16 * k35 * t3 * t9 * 2.0 + k22 * k23 * t2 * t16 * 6.0 + k22 * k23 * t4 * t14 * 6.0 + k25 * k26 * t2 * t10 * 2.0 - k25 * k26 * t4 * t8 * 4.0 + k15 * k32 * t2 * t15 * 2.0 - k15 * k32 * t3 * t14 * 3.0 + k16 * k35 * t2 * t11 * 4.0 - k16 * k35 * t3 * t10 * 1.2E1 + k16 * k35 * t4 * t9 * 4.0 + k22 * k23 * t3 * t16 * 4.0 + k22 * k23 * t4 * t15 * 2.0 + k22 * k23 * t8 * t11 * 2.0 + k22 * k23 * t9 * t10 * 4.0 + k25 * k26 * t4 * t9 * 2.0 - k15 * k32 * t2 * t16 * 2.0 - k15 * k32 * t3 * t15 - k15 * k32 * t4 * t14 * 2.0 - k15 * k32 * t8 * t10 * 3.0 - k16 * k35 * t4 * t10 * 4.0 + k22 * k23 * t4 * t16 * 2.0 + k22 * k23 * t8 * t12 * 4.0 + k25 * k26 * t2 * t12 * 6.0 + k25 * k26 * t4 * t10 * 2.0 + k15 * k32 * t3 * t16 - k15 * k32 * t4 * t15 * 2.0 + k15 * k32 * t8 * t11 * 2.0 - k15 * k32 * t9 * t10 - k16 * k35 * t3 * t12 * 4.0 + k22 * k23 * t8 * t13 * 2.0 + k22 * k23 * t9 * t12 * 4.0 - k25 * k26 * t2 * t13 * 8.0 + k25 * k26 * t3 * t12 * 2.0 - k15 * k32 * t8 * t12 * 6.0 - k22 * k23 * t9 * t13 * 4.0 + k25 * k26 * t4 * t12 * 1.2E1 - k15 * k32 * t10 * t11 + k22 * k23 * t10 * t13 * 4.0 + k22 * k23 * t11 * t12 * 2.0 - k15 * k32 * t9 * t13 * 2.0 - k15 * k32 * t10 * t12 * 3.0 + k25 * k26 * t5 * t13 * 2.0 + k15 * k32 * t10 * t13 - k15 * k32 * t11 * t12 * 6.0 + k22 * k23 * t12 * t13 * 2.0 + k25 * k36 * t2 * t8 * 4.0 + k26 * k35 * t2 * t8 * 4.0 + k25 * k36 * t2 * t9 * 4.0 - k25 * k36 * t3 * t8 * 4.0 + k26 * k35 * t2 * t9 * 4.0 - k26 * k35 * t3 * t8 * 4.0 + k25 * k36 * t2 * t10 * 2.0 - k25 * k36 * t3 * t9 * 4.0 - k25 * k36 * t4 * t8 * 4.0 + k26 * k35 * t2 * t10 * 2.0 - k26 * k35 * t3 * t9 * 4.0 - k26 * k35 * t4 * t8 * 4.0 - k25 * k36 * t2 * t11 * 2.0 - k25 * k36 * t4 * t9 * 4.0 - k26 * k35 * t2 * t11 * 6.0 - k26 * k35 * t4 * t9 * 4.0; coeffs(5) += k25 * k36 * t2 * t12 * 2.0 + k26 * k35 * t2 * t12 * 2.0 - k25 * k36 * t2 * t13 * 6.0 - k26 * k35 * t2 * t13 * 2.0 + k32 * k33 * t2 * t14 * 2.0 + k32 * k33 * t2 * t15 * 6.0 + k32 * k33 * t3 * t14 * 6.0 + k32 * k33 * t8 * t9 * 2.0 + k35 * k36 * t2 * t9 * 4.0 + k35 * k36 * t3 * t8 * 2.0 + k32 * k33 * t2 * t16 * 2.0 + k32 * k33 * t3 * t15 * 2.0 + k32 * k33 * t4 * t14 * 4.0 + k32 * k33 * t8 * t10 * 4.0 + k35 * k36 * t2 * t10 * 6.0 - k35 * k36 * t3 * t9 * 4.0 + k32 * k33 * t3 * t16 * 2.0 + k32 * k33 * t4 * t15 * 4.0 - k32 * k33 * t8 * t11 * 4.0 + k32 * k33 * t9 * t10 * 4.0 - k35 * k36 * t2 * t11 * 8.0 + k35 * k36 * t3 * t10 * 1.2E1 + k35 * k36 * t4 * t9 * 2.0 + k32 * k33 * t8 * t12 * 4.0 + k32 * k33 * t9 * t11 * 2.0 + k35 * k36 * t2 * t12 * 2.0 + k35 * k36 * t4 * t10 * 2.0 + k32 * k33 * t10 * t11 * 2.0 + k35 * k36 * t3 * t12 * 2.0 + k32 * k33 * t9 * t13 * 2.0 + k35 * k36 * t5 * t11 * 2.0 + k32 * k33 * t10 * t13 * 2.0 + k32 * k33 * t11 * t12 * 4.0 + k25 * t2 * t4 * t14 + k25 * t2 * t4 * t15 * 3.0 + k25 * t4 * t8 * t9 + k25 * t2 * t4 * t16 + k25 * t2 * t8 * t12 * 2.0 + k25 * t4 * t8 * t10 * 2.0 + k25 * t3 * t8 * t12 * 2.0 + k25 * t2 * t9 * t13 * 2.0 + k25 * t2 * t10 * t12 + k25 * t4 * t8 * t12 * 6.0 + k25 * t2 * t11 * t12 + k25 * t2 * t12 * t13 + k35 * t2 * t3 * t14 + k35 * t2 * t3 * t15 + k35 * t3 * t8 * t9 + k35 * t2 * t3 * t16 * 3.0 + k35 * t2 * t8 * t11 * 2.0 + k35 * t2 * t9 * t10 * 2.0 + k35 * t3 * t9 * t10 * 6.0 + k35 * t2 * t10 * t11 + k35 * t4 * t9 * t10 * 2.0 + k35 * t2 * t10 * t12 + k35 * t3 * t9 * t12 * 2.0 + k35 * t2 * t10 * t13 + k12 * k13 * k15 * k16 * k25 * 8.0 + k12 * k13 * k15 * k16 * k35 * 8.0 - k12 * k13 * k15 * k25 * k26 * 1.6E1 - k12 * k15 * k16 * k22 * k26 * 4.0 - k12 * k15 * k16 * k23 * k25 * 4.0 + k12 * k13 * k16 * k25 * k33 * 4.0 + k12 * k13 * k16 * k26 * k32 * 4.0 - k13 * k15 * k16 * k22 * k33 * 4.0 - k13 * k15 * k16 * k23 * k32 * 4.0 - k12 * k13 * k15 * k25 * k36 * 3.2E1 - k12 * k13 * k15 * k26 * k35 * 3.2E1 + k12 * k15 * k16 * k22 * k36 * 4.0 + k12 * k15 * k16 * k23 * k35 * 4.0 + k12 * k15 * k23 * k25 * k26 * 1.2E1 - k12 * k13 * k16 * k32 * k36 * 4.0 - k12 * k13 * k16 * k33 * k35 * 4.0 - k12 * k13 * k25 * k26 * k33 * 4.0 + k12 * k16 * k22 * k26 * k33 * 4.0 + k12 * k16 * k23 * k25 * k33 * 4.0 + k12 * k16 * k23 * k26 * k32 * 4.0 - k13 * k15 * k22 * k26 * k33 * 1.2E1 - k13 * k15 * k23 * k25 * k33 * 1.2E1 - k13 * k15 * k23 * k26 * k32 * 1.2E1 + k15 * k16 * k22 * k23 * k33 * 1.2E1 - k12 * k13 * k15 * k35 * k36 * 1.6E1 + k12 * k15 * k22 * k26 * k36 * 2.0E1 + k12 * k15 * k23 * k25 * k36 * 2.0E1 + k12 * k15 * k23 * k26 * k35 * 2.0E1 - k13 * k16 * k22 * k25 * k35 * 4.0 + k13 * k16 * k23 * k32 * k33 * 1.2E1 + k12 * k13 * k25 * k33 * k36 * 2.0E1 + k12 * k13 * k26 * k32 * k36 * 2.0E1 + k12 * k13 * k26 * k33 * k35 * 2.0E1 + k12 * k16 * k22 * k33 * k36 * 4.0 + k12 * k16 * k23 * k32 * k36 * 4.0 + k12 * k16 * k23 * k33 * k35 * 4.0 - k12 * k23 * k25 * k26 * k33 * 4.0 - k13 * k15 * k22 * k33 * k36 * 1.2E1 - k13 * k15 * k23 * k32 * k36 * 1.2E1 - k13 * k15 * k23 * k33 * k35 * 1.2E1 + k15 * k22 * k23 * k26 * k33 * 4.0 - k12 * k15 * k23 * k35 * k36 * 4.0 - k13 * k22 * k25 * k26 * k35 * 4.0 + k16 * k22 * k23 * k25 * k35 * 1.6E1 - k15 * k16 * k25 * k32 * k35 * 4.0 + k13 * k23 * k26 * k32 * k33 * 1.2E1 + k12 * k13 * k33 * k35 * k36 * 1.2E1 - k12 * k22 * k26 * k33 * k36 * 2.0E1 - k12 * k23 * k25 * k33 * k36 * 2.0E1 - k12 * k23 * k26 * k32 * k36 * 2.0E1 - k12 * k23 * k26 * k33 * k35 * 2.0E1 + k15 * k22 * k23 * k33 * k36 * 1.2E1 + k13 * k22 * k25 * k35 * k36 * 4.0 - k22 * k23 * k25 * k26 * k35 * 8.0 + k15 * k25 * k26 * k32 * k35 * 4.0 + k13 * k23 * k32 * k33 * k36 * 4.0 - k12 * k23 * k33 * k35 * k36 * 4.0 + k16 * k25 * k32 * k33 * k35 * 1.6E1 - k22 * k23 * k25 * k35 * k36 * 1.6E1 - k15 * k25 * k32 * k35 * k36 * 4.0 - k25 * k26 * k32 * k33 * k35 * 1.6E1 - k25 * k32 * k33 * k35 * k36 * 8.0 + k12 * k13 * k15 * k25 * t10 * 4.0 + k12 * k13 * k15 * k25 * t12 * 4.0 - k12 * k15 * k16 * k22 * t12 * 2.0 - k12 * k13 * k15 * k25 * t13 * 8.0 + k12 * k15 * k16 * k22 * t13 * 4.0 - k13 * k16 * k22 * k26 * t2 * 2.0 - k13 * k16 * k23 * k25 * t2 * 2.0 - k13 * k16 * k22 * k26 * t3 * 2.0 - k13 * k16 * k23 * k25 * t3 * 6.0 + k13 * k16 * k22 * k26 * t4 * 2.0 + k13 * k16 * k23 * k25 * t4 * 2.0 - k12 * k13 * k16 * k32 * t10 * 2.0 - k12 * k15 * k22 * k26 * t8 * 2.0 - k12 * k15 * k23 * k25 * t8 * 2.0 + k12 * k13 * k16 * k32 * t11 * 4.0 + k12 * k13 * k15 * k35 * t10 * 4.0 - k12 * k15 * k22 * k26 * t10 * 6.0 - k12 * k15 * k23 * k25 * t10 * 2.0 - k12 * k13 * k15 * k35 * t11 * 8.0 + k12 * k13 * k15 * k35 * t12 * 4.0 - k12 * k15 * k22 * k26 * t12 * 2.0 - k12 * k15 * k23 * k25 * t12 * 2.0 + k12 * k15 * k22 * k26 * t13 * 4.0 + k12 * k15 * k23 * k25 * t13 * 4.0 - k13 * k16 * k22 * k36 * t2 * 2.0 - k13 * k16 * k23 * k35 * t2 * 2.0 - k13 * k23 * k25 * k26 * t2 * 4.0 + k13 * k16 * k22 * k36 * t3 * 4.0 + k13 * k16 * k23 * k35 * t3 * 1.2E1 - k12 * k13 * k25 * k33 * t8 * 4.0 - k12 * k13 * k26 * k32 * t8 * 4.0 + k12 * k16 * k22 * k33 * t8 * 3.0 + k12 * k16 * k23 * k32 * t8 * 3.0 + k13 * k15 * k22 * k33 * t8 + k13 * k15 * k23 * k32 * t8 + k13 * k16 * k22 * k36 * t4 * 4.0 + k13 * k16 * k23 * k35 * t4 * 4.0 + k13 * k23 * k25 * k26 * t4 * 2.0 - k15 * k16 * k25 * k33 * t2 * 2.0 - k15 * k16 * k26 * k32 * t2 * 2.0 - k16 * k22 * k23 * k26 * t4 * 8.0 - k12 * k13 * k25 * k33 * t9 * 2.0 - k12 * k13 * k26 * k32 * t9 * 2.0 + k12 * k16 * k22 * k33 * t9 * 3.0 + k12 * k16 * k23 * k32 * t9 * 3.0 + k13 * k15 * k22 * k33 * t9 + k13 * k15 * k23 * k32 * t9 + k15 * k16 * k25 * k33 * t3 * 4.0 + k15 * k16 * k26 * k32 * t3 * 4.0 - k12 * k13 * k25 * k33 * t10 * 2.0 - k12 * k13 * k26 * k32 * t10 * 2.0 - k12 * k15 * k22 * k36 * t8 * 2.0 - k12 * k15 * k23 * k35 * t8 * 2.0 - k12 * k16 * k22 * k33 * t10 * 3.0 - k12 * k16 * k23 * k32 * t10 + k13 * k15 * k22 * k33 * t10 * 9.0 + k13 * k15 * k23 * k32 * t10 * 3.0 + k15 * k16 * k25 * k33 * t4 * 1.2E1 + k15 * k16 * k26 * k32 * t4 * 4.0 - k12 * k13 * k25 * k33 * t11 * 2.0 - k12 * k13 * k26 * k32 * t11 * 6.0 - k12 * k15 * k22 * k36 * t9 * 4.0 - k12 * k15 * k23 * k35 * t9 * 4.0 - k12 * k16 * k22 * k33 * t11 - k12 * k16 * k23 * k32 * t11 - k13 * k15 * k22 * k33 * t11 - k13 * k15 * k23 * k32 * t11 - k12 * k13 * k25 * k33 * t12 * 2.0 - k12 * k13 * k26 * k32 * t12 * 6.0 - k12 * k15 * k22 * k36 * t10 * 6.0 - k12 * k15 * k23 * k35 * t10 * 2.0 - k12 * k16 * k22 * k33 * t12 - k12 * k16 * k23 * k32 * t12 * 3.0 + k13 * k15 * k22 * k33 * t12 * 3.0 + k13 * k15 * k23 * k32 * t12 * 9.0 + k12 * k13 * k25 * k33 * t13 * 6.0 + k12 * k13 * k26 * k32 * t13 * 6.0 + k12 * k15 * k22 * k36 * t11 * 6.0 + k12 * k15 * k23 * k35 * t11 * 6.0 - k12 * k16 * k22 * k33 * t13 - k12 * k16 * k23 * k32 * t13 - k13 * k15 * k22 * k33 * t13 - k13 * k15 * k23 * k32 * t13 - k12 * k15 * k22 * k36 * t12 * 2.0 - k12 * k15 * k23 * k35 * t12 * 2.0 - k12 * k15 * k22 * k36 * t13 * 6.0 - k12 * k15 * k23 * k35 * t13 * 2.0 - k16 * k22 * k23 * k36 * t2 * 8.0 + k13 * k22 * k26 * k36 * t3 * 2.0 + k13 * k23 * k25 * k36 * t3 * 6.0 + k13 * k23 * k26 * k35 * t3 * 6.0 - k16 * k22 * k23 * k36 * t3 * 8.0 + k12 * k22 * k26 * k33 * t8 * 5.0 + k12 * k23 * k25 * k33 * t8 * 5.0 + k12 * k23 * k26 * k32 * t8 * 5.0 + k13 * k22 * k26 * k36 * t4 * 4.0 + k13 * k23 * k25 * k36 * t4 * 4.0 + k13 * k23 * k26 * k35 * t4 * 4.0 - k15 * k16 * k32 * k36 * t2 * 2.0 - k15 * k16 * k33 * k35 * t2 * 2.0 - k15 * k22 * k23 * k33 * t8 * 6.0 + k15 * k25 * k26 * k33 * t2 * 4.0 - k16 * k22 * k23 * k36 * t4 * 8.0 - k12 * k13 * k32 * k36 * t9 * 2.0 - k12 * k13 * k33 * k35 * t9 * 2.0 - k12 * k22 * k26 * k33 * t9 - k12 * k23 * k25 * k33 * t9 - k12 * k23 * k26 * k32 * t9 + k15 * k16 * k32 * k36 * t3 * 2.0 + k15 * k16 * k33 * k35 * t3 * 2.0 - k15 * k22 * k23 * k33 * t9 * 2.0 - k15 * k25 * k26 * k33 * t3 * 2.0 - k12 * k13 * k32 * k36 * t10 * 2.0 - k12 * k13 * k33 * k35 * t10 * 2.0 + k12 * k22 * k26 * k33 * t10 * 3.0 + k12 * k23 * k25 * k33 * t10 + k12 * k23 * k26 * k32 * t10 + k13 * k22 * k25 * k35 * t8 * 2.0 - k15 * k16 * k32 * k36 * t4 * 2.0 - k15 * k16 * k33 * k35 * t4 * 6.0 - k15 * k22 * k23 * k33 * t10 * 4.0 - k15 * k25 * k26 * k33 * t4 * 1.2E1 + k12 * k13 * k32 * k36 * t11 * 4.0 + k12 * k13 * k33 * k35 * t11 * 4.0 + k12 * k22 * k26 * k33 * t11 * 3.0 + k12 * k23 * k25 * k33 * t11 + k12 * k23 * k26 * k32 * t11 * 3.0 + k13 * k22 * k25 * k35 * t9 * 6.0 - k15 * k22 * k23 * k33 * t11 * 2.0 - k12 * k13 * k32 * k36 * t12 * 6.0 - k12 * k13 * k33 * k35 * t12 * 2.0 - k12 * k22 * k25 * k32 * t14; coeffs(5) += k12 * k22 * k26 * k33 * t12 * 3.0 + k12 * k23 * k25 * k33 * t12 * 3.0 + k12 * k23 * k26 * k32 * t12 * 9.0 + k13 * k22 * k25 * k35 * t10 * 6.0 - k15 * k22 * k23 * k33 * t12 * 2.0 + k12 * k22 * k25 * k32 * t15 * 3.0 - k12 * k22 * k26 * k33 * t13 * 5.0 - k12 * k23 * k25 * k33 * t13 * 5.0 - k12 * k23 * k26 * k32 * t13 * 5.0 + k15 * k22 * k23 * k33 * t13 * 2.0 - k12 * k22 * k25 * k32 * t16 * 5.0 + k13 * k22 * k25 * k35 * t12 * 4.0 - k13 * k23 * k32 * k33 * t8 * 2.0 + k13 * k23 * k35 * k36 * t2 * 4.0 - k16 * k26 * k32 * k33 * t2 * 8.0 + k22 * k23 * k26 * k36 * t2 * 4.0 - k13 * k23 * k32 * k33 * t9 * 6.0 - k13 * k23 * k35 * k36 * t3 * 1.2E1 - k16 * k26 * k32 * k33 * t3 * 8.0 - k12 * k22 * k33 * k36 * t8 - k12 * k23 * k32 * k36 * t8 - k12 * k23 * k33 * k35 * t8 - k13 * k23 * k32 * k33 * t10 * 2.0 - k13 * k23 * k35 * k36 * t4 * 2.0 - k16 * k26 * k32 * k33 * t4 * 8.0 + k12 * k22 * k33 * k36 * t9 * 5.0 + k12 * k23 * k32 * k36 * t9 * 5.0 + k12 * k23 * k33 * k35 * t9 * 5.0 + k13 * k23 * k32 * k33 * t11 * 2.0 + k15 * k25 * k33 * k36 * t3 * 4.0 + k15 * k26 * k32 * k36 * t3 * 4.0 + k15 * k26 * k33 * k35 * t3 * 4.0 + k12 * k22 * k33 * k36 * t10 * 9.0 + k12 * k23 * k32 * k36 * t10 * 3.0 + k12 * k23 * k33 * k35 * t10 * 3.0 - k13 * k23 * k32 * k33 * t12 * 4.0 + k15 * k25 * k33 * k36 * t4 * 6.0 + k15 * k26 * k32 * k36 * t4 * 2.0 + k15 * k26 * k33 * k35 * t4 * 6.0 - k22 * k23 * k25 * k35 * t8 * 8.0 - k12 * k22 * k33 * k36 * t11 * 5.0 - k12 * k23 * k32 * k36 * t11 * 5.0 - k12 * k23 * k33 * k35 * t11 * 5.0 - k13 * k23 * k32 * k33 * t13 * 2.0 - k22 * k23 * k25 * k35 * t9 * 8.0 - k12 * k22 * k32 * k35 * t14 + k12 * k22 * k33 * k36 * t12 + k12 * k23 * k32 * k36 * t12 * 3.0 + k12 * k23 * k33 * k35 * t12 + k15 * k25 * k32 * k35 * t8 * 6.0 - k12 * k22 * k32 * k35 * t15 * 5.0 + k12 * k22 * k33 * k36 * t13 * 3.0 + k12 * k23 * k32 * k36 * t13 * 3.0 + k12 * k23 * k33 * k35 * t13 + k15 * k25 * k32 * k35 * t9 * 2.0 + k12 * k22 * k32 * k35 * t16 * 3.0 + k15 * k25 * k32 * k35 * t10 * 4.0 + k15 * k25 * k32 * k35 * t12 * 6.0 - k16 * k32 * k33 * k36 * t3 * 8.0 - k15 * k33 * k35 * k36 * t2 * 4.0 + k15 * k33 * k35 * k36 * t3 * 2.0 + k26 * k32 * k33 * k36 * t2 * 4.0 - k25 * k32 * k33 * k35 * t8 * 8.0 - k25 * k32 * k33 * k35 * t9 * 8.0 - k13 * k16 * k22 * t4 * t9 - k13 * k16 * k22 * t2 * t12 - k13 * k16 * k22 * t4 * t10 * 6.0 - k13 * k16 * k22 * t3 * t12 * 2.0 - k12 * k15 * k22 * t2 * t16 * 2.0 - k12 * k15 * k22 * t4 * t14 - k13 * k16 * k22 * t4 * t12 * 6.0 - k12 * k15 * k22 * t4 * t15 - k12 * k15 * k22 * t4 * t16 - k12 * k15 * k22 * t8 * t12 - k12 * k15 * k22 * t9 * t13 * 2.0 - k12 * k15 * k22 * t10 * t12 * 3.0 - k12 * k15 * k22 * t11 * t12 - k13 * k22 * k26 * t4 * t8 - k13 * k23 * k25 * t4 * t8 + k16 * k22 * k23 * t4 * t8 * 2.0 - k16 * k25 * k26 * t2 * t4 * 4.0 - k12 * k13 * k32 * t2 * t15 * 2.0 - k12 * k13 * k32 * t3 * t14 - k12 * k15 * k22 * t12 * t13 - k13 * k22 * k26 * t4 * t9 - k13 * k23 * k25 * t4 * t9 - k15 * k16 * k32 * t3 * t8 + k16 * k22 * k23 * t4 * t9 * 2.0 - k12 * k13 * k32 * t3 * t15 - k13 * k22 * k26 * t2 * t12 * 4.0 - k13 * k22 * k26 * t4 * t10 * 3.0 - k13 * k23 * k25 * t2 * t12 * 4.0 - k13 * k23 * k25 * t4 * t10 - k15 * k16 * k32 * t2 * t10 + k16 * k22 * k23 * t2 * t12 * 4.0 + k16 * k22 * k23 * t4 * t10 * 4.0 - k12 * k13 * k32 * t3 * t16 - k12 * k13 * k32 * t8 * t11 * 2.0 - k12 * k13 * k32 * t9 * t10 + k13 * k22 * k26 * t2 * t13 * 2.0 - k13 * k22 * k26 * t3 * t12 + k13 * k23 * k25 * t2 * t13 * 2.0 - k13 * k23 * k25 * t3 * t12 * 3.0 - k15 * k16 * k32 * t3 * t10 * 6.0 - k16 * k22 * k23 * t2 * t13 * 4.0 + k16 * k22 * k23 * t3 * t12 * 4.0 - k13 * k22 * k26 * t4 * t12 * 6.0 - k13 * k23 * k25 * t4 * t12 * 6.0 - k15 * k16 * k32 * t4 * t10 * 2.0 + k16 * k22 * k23 * t4 * t12 * 1.2E1 - k12 * k13 * k32 * t10 * t11 - k15 * k16 * k32 * t3 * t12 * 6.0 - k12 * k13 * k32 * t10 * t12 * 3.0 - k12 * k13 * k32 * t10 * t13 - k13 * k22 * k36 * t3 * t9 * 2.0 - k13 * k23 * k35 * t3 * t9 * 6.0 + k22 * k23 * k26 * t4 * t8 * 4.0 + k12 * k22 * k33 * t2 * t15 + k12 * k22 * k33 * t3 * t14 + k12 * k23 * k32 * t2 * t15 + k12 * k23 * k32 * t3 * t14 * 3.0 - k13 * k22 * k36 * t2 * t11 * 2.0 - k13 * k22 * k36 * t4 * t9 * 2.0 - k13 * k23 * k35 * t2 * t11 * 2.0 - k13 * k23 * k35 * t4 * t9 * 2.0 - k15 * k25 * k33 * t3 * t8 * 2.0 - k15 * k26 * k32 * t3 * t8 * 2.0 + k12 * k22 * k33 * t2 * t16 + k12 * k22 * k33 * t4 * t14 * 3.0 + k12 * k23 * k32 * t2 * t16 + k12 * k23 * k32 * t4 * t14 - k13 * k22 * k36 * t2 * t12 * 2.0 - k13 * k23 * k35 * t2 * t12 * 2.0 - k15 * k25 * k33 * t2 * t10 * 2.0 - k15 * k25 * k33 * t4 * t8 * 6.0 - k15 * k26 * k32 * t2 * t10 * 2.0 - k15 * k26 * k32 * t4 * t8 * 2.0 + k22 * k23 * k26 * t2 * t12 * 2.0 + k12 * k22 * k33 * t3 * t16 + k12 * k22 * k33 * t4 * t15 * 3.0 + k12 * k22 * k33 * t8 * t11 * 3.0 + k12 * k22 * k33 * t9 * t10 * 3.0 + k12 * k23 * k32 * t3 * t16 * 3.0 + k12 * k23 * k32 * t4 * t15 + k12 * k23 * k32 * t8 * t11 * 3.0 + k12 * k23 * k32 * t9 * t10 + k12 * k22 * k33 * t8 * t12 + k12 * k23 * k32 * t8 * t12 * 3.0 - k15 * k25 * k33 * t2 * t13 * 2.0 - k15 * k26 * k32 * t2 * t13 * 2.0 + k12 * k22 * k33 * t9 * t13 * 3.0 + k12 * k23 * k32 * t9 * t13 * 3.0 + k12 * k22 * k33 * t10 * t13 * 3.0 + k12 * k22 * k33 * t11 * t12 + k12 * k23 * k32 * t10 * t13 + k12 * k23 * k32 * t11 * t12 * 3.0 + k16 * k32 * k33 * t3 * t8 * 2.0 - k16 * k35 * k36 * t2 * t3 * 4.0 + k22 * k23 * k36 * t2 * t9 * 4.0 + k25 * k26 * k36 * t2 * t3 * 2.0 + k16 * k32 * k33 * t2 * t10 * 4.0 + k16 * k32 * k33 * t3 * t9 * 2.0 + k22 * k23 * k36 * t3 * t9 * 4.0 - k15 * k32 * k36 * t3 * t8 - k15 * k33 * k35 * t3 * t8 - k16 * k32 * k33 * t2 * t11 * 4.0 + k16 * k32 * k33 * t3 * t10 * 1.2E1 + k22 * k23 * k36 * t2 * t11 * 2.0 + k22 * k23 * k36 * t4 * t9 * 4.0 - k15 * k32 * k36 * t2 * t10 * 4.0 - k15 * k32 * k36 * t3 * t9 - k15 * k33 * k35 * t2 * t10 * 4.0 - k15 * k33 * k35 * t3 * t9 + k16 * k32 * k33 * t4 * t10 * 4.0 + k22 * k23 * k36 * t2 * t12 * 2.0 + k15 * k32 * k36 * t2 * t11 * 2.0 - k15 * k32 * k36 * t3 * t10 * 6.0 + k15 * k33 * k35 * t2 * t11 * 2.0 - k15 * k33 * k35 * t3 * t10 * 6.0 + k16 * k32 * k33 * t3 * t12 * 4.0 + k22 * k23 * k36 * t2 * t13 * 6.0 - k15 * k32 * k36 * t4 * t10 - k15 * k33 * k35 * t4 * t10 * 3.0 - k15 * k32 * k36 * t3 * t12 * 3.0 - k15 * k33 * k35 * t3 * t12 + k26 * k32 * k33 * t2 * t8 * 4.0 + k26 * k32 * k33 * t3 * t8 * 4.0 + k26 * k32 * k33 * t2 * t10 * 2.0 + k26 * k32 * k33 * t4 * t8 * 4.0 + k26 * k35 * k36 * t2 * t4 * 2.0 + k26 * k32 * k33 * t2 * t11 * 6.0 + k26 * k32 * k33 * t2 * t13 * 2.0 + k32 * k33 * k36 * t2 * t10 * 2.0 + k32 * k33 * k36 * t3 * t9 * 4.0 - k13 * k22 * t2 * t4 * t15 + k16 * k25 * t2 * t4 * t10 + k16 * k25 * t2 * t3 * t12 - k13 * k22 * t2 * t11 * t12 + k25 * k26 * t2 * t4 * t8 * 2.0 + k16 * k35 * t2 * t4 * t10 - k15 * k32 * t2 * t3 * t16 + k16 * k35 * t2 * t3 * t12 + k22 * k23 * t2 * t9 * t13 * 2.0 - k15 * k32 * t2 * t10 * t13 + k25 * k36 * t2 * t4 * t10 + k26 * k35 * t2 * t4 * t10 + k25 * k36 * t2 * t3 * t12 + k26 * k35 * t2 * t3 * t12 + k35 * k36 * t2 * t3 * t9 * 2.0 + k32 * k33 * t2 * t8 * t11 * 2.0 + k12 * k13 * k15 * k22 * k23 * k26 * 8.0 - k12 * k13 * k16 * k22 * k23 * k33 * 4.0 + k12 * k13 * k15 * k22 * k23 * k36 * 8.0 - k13 * k15 * k16 * k22 * k25 * k32 * 2.0 - k12 * k15 * k16 * k22 * k25 * k35 * 4.0 - k12 * k13 * k22 * k23 * k26 * k33 * 4.0 + k12 * k13 * k15 * k26 * k32 * k33 * 8.0 - k12 * k15 * k16 * k23 * k32 * k33 * 4.0 - k12 * k13 * k16 * k25 * k32 * k35 * 4.0 - k12 * k16 * k22 * k25 * k26 * k32 * 2.0 - k13 * k15 * k16 * k22 * k32 * k35 * 2.0 - k13 * k15 * k22 * k25 * k26 * k32 * 2.0 + k15 * k16 * k22 * k23 * k25 * k32 * 8.0 + k12 * k15 * k22 * k25 * k26 * k35 * 1.2E1 - k12 * k13 * k22 * k23 * k33 * k36 * 4.0 + k12 * k13 * k15 * k32 * k33 * k36 * 8.0 - k12 * k15 * k23 * k26 * k32 * k33 * 4.0 + k13 * k16 * k22 * k25 * k32 * k33 * 4.0 + k12 * k13 * k25 * k26 * k32 * k35 * 8.0 + k12 * k16 * k22 * k25 * k32 * k36 * 6.0 + k12 * k16 * k22 * k25 * k33 * k35 * 6.0 + k12 * k16 * k22 * k26 * k32 * k35 * 6.0 + k12 * k16 * k23 * k25 * k32 * k35 * 6.0 - k13 * k15 * k22 * k25 * k32 * k36 * 1.0E1 - k13 * k15 * k22 * k25 * k33 * k35 * 1.0E1 - k13 * k15 * k22 * k26 * k32 * k35 * 1.0E1 - k13 * k15 * k23 * k25 * k32 * k35 * 1.0E1 + k15 * k16 * k22 * k23 * k32 * k35 * 4.0 - k15 * k22 * k23 * k25 * k26 * k32 * 4.0 + k12 * k15 * k22 * k25 * k35 * k36 * 8.0 - k12 * k15 * k23 * k32 * k33 * k36 * 4.0 + k13 * k16 * k22 * k32 * k33 * k35 * 8.0 + k13 * k22 * k25 * k26 * k32 * k33 * 4.0 - k16 * k22 * k23 * k25 * k32 * k33 * 1.6E1 + k12 * k13 * k25 * k32 * k35 * k36 * 1.2E1 - k12 * k16 * k22 * k32 * k35 * k36 * 2.0 - k12 * k22 * k25 * k26 * k32 * k36 * 1.0E1 - k12 * k22 * k25 * k26 * k33 * k35 * 1.0E1 - k12 * k23 * k25 * k26 * k32 * k35 * 1.0E1 - k13 * k15 * k22 * k32 * k35 * k36 * 2.0 + k15 * k22 * k23 * k25 * k32 * k36 * 8.0 + k15 * k22 * k23 * k25 * k33 * k35 * 8.0 + k15 * k22 * k23 * k26 * k32 * k35 * 8.0 + k13 * k22 * k25 * k32 * k33 * k36 * 8.0 + k13 * k22 * k26 * k32 * k33 * k35 * 8.0 + k13 * k23 * k25 * k32 * k33 * k35 * 8.0 - k16 * k22 * k23 * k32 * k33 * k35 * 1.6E1 + k22 * k23 * k25 * k26 * k32 * k33 * 8.0 - k12 * k22 * k25 * k33 * k35 * k36 * 1.0E1 - k12 * k22 * k26 * k32 * k35 * k36 * 1.0E1 - k12 * k23 * k25 * k32 * k35 * k36 * 1.0E1 + k15 * k22 * k23 * k32 * k35 * k36 * 4.0 - k13 * k22 * k32 * k33 * k35 * k36 * 4.0 + k22 * k23 * k32 * k33 * k35 * k36 * 8.0 + k12 * k15 * k16 * k22 * k26 * t4 * 2.0 + k12 * k15 * k16 * k23 * k25 * t4 * 2.0 + k12 * k13 * k15 * k22 * k23 * t12 * 4.0 - k12 * k13 * k16 * k25 * k33 * t3 * 4.0 - k12 * k13 * k16 * k26 * k32 * t3 * 4.0 + k13 * k15 * k16 * k22 * k33 * t3 * 2.0 + k13 * k15 * k16 * k23 * k32 * t3 * 6.0 + k13 * k15 * k16 * k22 * k33 * t4 * 6.0 + k13 * k15 * k16 * k23 * k32 * t4 * 2.0 + k12 * k13 * k15 * k25 * k36 * t3 * 4.0 + k12 * k13 * k15 * k26 * k35 * t3 * 4.0 + k12 * k13 * k15 * k25 * k36 * t4 * 4.0 + k12 * k13 * k15 * k26 * k35 * t4 * 4.0 - k12 * k15 * k16 * k22 * k36 * t4 * 4.0 - k12 * k15 * k16 * k23 * k35 * t4 * 4.0 - k12 * k15 * k23 * k25 * k26 * t4 * 2.0 - k12 * k13 * k25 * k26 * k33 * t2 * 4.0 + k12 * k16 * k22 * k26 * k33 * t2 + k12 * k16 * k23 * k25 * k33 * t2 + k12 * k16 * k23 * k26 * k32 * t2 + k13 * k15 * k22 * k26 * k33 * t2 * 3.0 + k13 * k15 * k23 * k25 * k33 * t2 * 3.0 + k13 * k15 * k23 * k26 * k32 * t2 * 3.0 - k15 * k16 * k22 * k23 * k33 * t2 * 2.0 + k12 * k13 * k16 * k32 * k36 * t3 * 2.0 + k12 * k13 * k16 * k33 * k35 * t3 * 2.0 - k12 * k13 * k22 * k23 * k33 * t9 * 2.0 - k12 * k13 * k25 * k26 * k33 * t3 * 2.0 + k12 * k16 * k22 * k26 * k33 * t3 + k12 * k16 * k23 * k25 * k33 * t3 * 3.0 + k12 * k16 * k23 * k26 * k32 * t3 * 3.0 + k13 * k15 * k22 * k26 * k33 * t3 + k13 * k15 * k23 * k25 * k33 * t3 * 3.0 + k13 * k15 * k23 * k26 * k32 * t3 * 3.0 - k15 * k16 * k22 * k23 * k33 * t3 * 4.0 - k12 * k15 * k22 * k26 * k36 * t2 * 4.0 - k12 * k15 * k23 * k25 * k36 * t2 * 4.0 - k12 * k15 * k23 * k26 * k35 * t2 * 4.0 - k12 * k16 * k22 * k26 * k33 * t4 * 6.0 - k12 * k16 * k23 * k25 * k33 * t4 * 6.0 - k12 * k16 * k23 * k26 * k32 * t4 * 2.0 + k13 * k15 * k22 * k26 * k33 * t4 * 6.0 + k13 * k15 * k23 * k25 * k33 * t4 * 6.0 + k13 * k15 * k23 * k26 * k32 * t4 * 2.0 - k13 * k16 * k22 * k25 * k35 * t2 * 2.0 - k15 * k16 * k22 * k23 * k33 * t4 * 1.2E1 - k12 * k13 * k22 * k23 * k33 * t11 * 2.0 - k12 * k15 * k22 * k26 * k36 * t3 * 2.0 - k12 * k15 * k23 * k25 * k36 * t3 * 6.0 - k12 * k15 * k23 * k26 * k35 * t3 * 6.0 + k12 * k13 * k15 * k32 * k33 * t10 * 4.0 - k12 * k13 * k22 * k23 * k33 * t12 * 2.0 + k12 * k16 * k22 * k25 * k32 * t8 * 3.0 + k13 * k15 * k22 * k25 * k32 * t8 - k12 * k13 * k22 * k23 * k33 * t13 * 2.0 - k12 * k15 * k22 * k25 * k35 * t8 * 2.0 + k12 * k16 * k22 * k25 * k32 * t10 * 3.0 + k13 * k15 * k22 * k25 * k32 * t10 * 3.0 - k12 * k15 * k22 * k25 * k35 * t10 * 6.0 - k12 * k16 * k22 * k25 * k32 * t12 * 6.0 + k13 * k15 * k22 * k25 * k32 * t12 * 6.0 - k13 * k16 * k23 * k32 * k33 * t2 * 2.0 - k13 * k16 * k23 * k32 * k33 * t3 * 1.2E1 - k12 * k13 * k25 * k33 * k36 * t2 * 4.0 - k12 * k13 * k26 * k32 * k36 * t2 * 4.0 - k12 * k13 * k26 * k33 * k35 * t2 * 4.0 + k12 * k16 * k22 * k33 * k36 * t2 + k12 * k16 * k23 * k32 * k36 * t2 + k12 * k16 * k23 * k33 * k35 * t2 + k12 * k23 * k25 * k26 * k33 * t2 * 2.0 + k13 * k15 * k22 * k33 * k36 * t2 * 3.0 + k13 * k15 * k23 * k32 * k36 * t2 * 3.0 + k13 * k15 * k23 * k33 * k35 * t2 * 3.0 - k13 * k16 * k23 * k32 * k33 * t4 * 4.0 - k15 * k22 * k23 * k26 * k33 * t2 * 4.0 - k12 * k16 * k22 * k33 * k36 * t3 * 2.0 - k12 * k16 * k23 * k32 * k36 * t3 * 6.0 - k12 * k16 * k23 * k33 * k35 * t3 * 6.0 + k13 * k15 * k22 * k33 * k36 * t3 * 2.0 + k13 * k15 * k23 * k32 * k36 * t3 * 6.0 + k13 * k15 * k23 * k33 * k35 * t3 * 6.0 - k12 * k13 * k25 * k33 * k36 * t4 * 6.0 - k12 * k13 * k26 * k32 * k36 * t4 * 2.0 - k12 * k13 * k26 * k33 * k35 * t4 * 6.0 - k12 * k15 * k23 * k32 * k33 * t8 * 2.0 - k12 * k15 * k23 * k35 * k36 * t2 * 4.0 + k12 * k16 * k22 * k33 * k36 * t4 * 3.0 + k12 * k16 * k23 * k32 * k36 * t4 + k12 * k16 * k23 * k33 * k35 * t4 * 3.0 + k12 * k23 * k25 * k26 * k33 * t4 * 6.0 + k13 * k15 * k22 * k33 * k36 * t4 * 3.0; coeffs(5) += k13 * k15 * k23 * k32 * k36 * t4 + k13 * k15 * k23 * k33 * k35 * t4 * 3.0 - k13 * k22 * k25 * k26 * k35 * t2 * 4.0 - k12 * k15 * k23 * k32 * k33 * t10 * 2.0 - k12 * k15 * k23 * k35 * k36 * t4 * 2.0 + k12 * k22 * k25 * k26 * k32 * t8 * 6.0 - k15 * k16 * k25 * k32 * k35 * t2 * 2.0 - k15 * k22 * k23 * k25 * k32 * t8 * 4.0 - k12 * k13 * k25 * k32 * k35 * t9 * 2.0 - k12 * k15 * k23 * k32 * k33 * t11 * 2.0 + k12 * k16 * k22 * k32 * k35 * t9 * 3.0 + k13 * k15 * k22 * k32 * k35 * t9 - k12 * k16 * k22 * k32 * k35 * t10 * 6.0 + k13 * k15 * k22 * k32 * k35 * t10 * 6.0 - k12 * k15 * k23 * k32 * k33 * t13 * 2.0 - k12 * k13 * k25 * k32 * k35 * t12 * 6.0 + k12 * k16 * k22 * k32 * k35 * t12 * 3.0 + k12 * k22 * k25 * k26 * k32 * t12 * 6.0 + k13 * k15 * k22 * k32 * k35 * t12 * 3.0 - k13 * k23 * k26 * k32 * k33 * t2 * 8.0 - k13 * k23 * k26 * k32 * k33 * t3 * 6.0 + k12 * k22 * k26 * k33 * k36 * t2 * 4.0 + k12 * k23 * k25 * k33 * k36 * t2 * 4.0 + k12 * k23 * k26 * k32 * k36 * t2 * 4.0 + k12 * k23 * k26 * k33 * k35 * t2 * 4.0 - k13 * k23 * k26 * k32 * k33 * t4 * 4.0 - k15 * k22 * k23 * k33 * k36 * t2 * 8.0 - k12 * k13 * k33 * k35 * k36 * t3 * 2.0 + k12 * k22 * k26 * k33 * k36 * t3 + k12 * k23 * k25 * k33 * k36 * t3 * 3.0 + k12 * k23 * k26 * k32 * k36 * t3 * 3.0 + k12 * k23 * k26 * k33 * k35 * t3 * 3.0 - k15 * k22 * k23 * k33 * k36 * t3 * 4.0 + k12 * k22 * k26 * k33 * k36 * t4 * 3.0 + k12 * k23 * k25 * k33 * k36 * t4 * 3.0 + k12 * k23 * k26 * k32 * k36 * t4 + k12 * k23 * k26 * k33 * k35 * t4 * 3.0 - k13 * k22 * k25 * k32 * k33 * t8 * 2.0 + k13 * k22 * k25 * k35 * k36 * t2 * 4.0 - k15 * k22 * k23 * k33 * k36 * t4 * 6.0 + k22 * k23 * k25 * k26 * k35 * t2 * 4.0 - k13 * k22 * k25 * k32 * k33 * t9 * 2.0 - k12 * k22 * k25 * k32 * k36 * t8 - k12 * k22 * k25 * k33 * k35 * t8 - k12 * k22 * k26 * k32 * k35 * t8 - k12 * k23 * k25 * k32 * k35 * t8 - k13 * k22 * k25 * k32 * k33 * t10 * 6.0 - k15 * k22 * k23 * k32 * k35 * t8 * 2.0 + k15 * k25 * k26 * k32 * k35 * t2 * 4.0 - k12 * k22 * k25 * k32 * k36 * t9 - k12 * k22 * k25 * k33 * k35 * t9 - k12 * k22 * k26 * k32 * k35 * t9 - k12 * k23 * k25 * k32 * k35 * t9 - k15 * k22 * k23 * k32 * k35 * t9 * 2.0 + k12 * k22 * k25 * k32 * k36 * t10 * 3.0 + k12 * k22 * k25 * k33 * k35 * t10 * 3.0 + k12 * k22 * k26 * k32 * k35 * t10 * 3.0 + k12 * k23 * k25 * k32 * k35 * t10 - k13 * k22 * k25 * k32 * k33 * t12 * 4.0 - k15 * k22 * k23 * k32 * k35 * t10 * 4.0 + k12 * k22 * k25 * k32 * k36 * t12 * 3.0 + k12 * k22 * k25 * k33 * k35 * t12 + k12 * k22 * k26 * k32 * k35 * t12 * 3.0 + k12 * k23 * k25 * k32 * k35 * t12 * 3.0 - k13 * k23 * k32 * k33 * k36 * t2 * 4.0 - k15 * k22 * k23 * k32 * k35 * t12 * 6.0 + k12 * k23 * k33 * k35 * k36 * t2 * 2.0 + k12 * k23 * k33 * k35 * k36 * t3 * 6.0 + k22 * k23 * k25 * k32 * k33 * t8 * 8.0 - k13 * k22 * k32 * k33 * k35 * t9 * 4.0 - k15 * k25 * k32 * k35 * k36 * t2 * 4.0 + k12 * k22 * k32 * k35 * k36 * t9 * 6.0 + k12 * k22 * k32 * k35 * k36 * t10 * 6.0 + k22 * k23 * k32 * k33 * k35 * t9 * 8.0 + k25 * k32 * k33 * k35 * k36 * t2 * 4.0 + k12 * k13 * k15 * k25 * t4 * t10 - k12 * k15 * k16 * k22 * t4 * t10 * 3.0 + k12 * k13 * k15 * k25 * t3 * t12 - k12 * k15 * k16 * k22 * t3 * t12 - k13 * k16 * k22 * k26 * t2 * t4 - k13 * k16 * k23 * k25 * t2 * t4 - k12 * k13 * k16 * k32 * t4 * t10 - k12 * k15 * k22 * k26 * t4 * t8 - k12 * k15 * k23 * k25 * t4 * t8 - k12 * k13 * k16 * k32 * t3 * t12 * 3.0 + k12 * k13 * k15 * k35 * t4 * t10 + k12 * k13 * k15 * k35 * t3 * t12 - k12 * k15 * k22 * k26 * t2 * t13 * 2.0 - k12 * k15 * k23 * k25 * t2 * t13 * 2.0 - k13 * k23 * k25 * k26 * t2 * t4 * 2.0 + k16 * k22 * k23 * k26 * t2 * t4 * 2.0 - k12 * k13 * k25 * k33 * t4 * t10 * 3.0 - k12 * k13 * k26 * k32 * t4 * t10 + k12 * k16 * k22 * k33 * t4 * t10 * 9.0 + k12 * k16 * k23 * k32 * t4 * t10 - k12 * k13 * k25 * k33 * t3 * t12 - k12 * k13 * k26 * k32 * t3 * t12 * 3.0 + k12 * k16 * k22 * k33 * t3 * t12 + k12 * k16 * k23 * k32 * t3 * t12 * 9.0 - k12 * k15 * k22 * k36 * t4 * t10 * 3.0 - k12 * k15 * k23 * k35 * t4 * t10 - k12 * k15 * k22 * k36 * t3 * t12 - k12 * k15 * k23 * k35 * t3 * t12 * 3.0 - k13 * k22 * k26 * k36 * t2 * t4 * 2.0 - k13 * k23 * k25 * k36 * t2 * t4 * 2.0 - k13 * k23 * k26 * k35 * t2 * t4 * 2.0 + k16 * k22 * k23 * k36 * t2 * t4 * 2.0 - k15 * k16 * k32 * k36 * t2 * t3 - k15 * k16 * k33 * k35 * t2 * t3 - k12 * k13 * k32 * k36 * t3 * t9 - k12 * k13 * k33 * k35 * t3 * t9 + k12 * k22 * k26 * k33 * t4 * t8 * 3.0 + k12 * k23 * k25 * k33 * t4 * t8 * 3.0 + k12 * k23 * k26 * k32 * t4 * t8 - k12 * k13 * k32 * k36 * t2 * t11 * 2.0 - k12 * k13 * k33 * k35 * t2 * t11 * 2.0 + k12 * k22 * k26 * k33 * t2 * t13 + k12 * k23 * k25 * k33 * t2 * t13 + k12 * k23 * k26 * k32 * t2 * t13 - k15 * k22 * k23 * k33 * t2 * t13 * 2.0 + k12 * k22 * k25 * k32 * t2 * t16 - k13 * k22 * k25 * k35 * t2 * t12 * 2.0 + k12 * k22 * k25 * k32 * t8 * t12 * 3.0 + k16 * k26 * k32 * k33 * t2 * t3 * 2.0 + k22 * k23 * k26 * k36 * t2 * t4 * 2.0 - k13 * k23 * k32 * k33 * t2 * t11 * 2.0 - k15 * k25 * k33 * k36 * t2 * t3 * 2.0 - k15 * k26 * k32 * k36 * t2 * t3 * 2.0 - k15 * k26 * k33 * k35 * t2 * t3 * 2.0 + k12 * k22 * k33 * k36 * t3 * t9 + k12 * k23 * k32 * k36 * t3 * t9 * 3.0 + k12 * k23 * k33 * k35 * t3 * t9 * 3.0 + k12 * k22 * k33 * k36 * t2 * t11 + k12 * k23 * k32 * k36 * t2 * t11 + k12 * k23 * k33 * k35 * t2 * t11 + k12 * k22 * k32 * k35 * t2 * t15 - k15 * k25 * k32 * k35 * t2 * t10 * 2.0 + k22 * k23 * k25 * k35 * t2 * t12 * 2.0 + k12 * k22 * k32 * k35 * t9 * t10 * 3.0 + k16 * k32 * k33 * k36 * t2 * t3 * 2.0 - k15 * k33 * k35 * k36 * t2 * t3 * 2.0 + k26 * k32 * k33 * k36 * t2 * t3 * 2.0 + k25 * k32 * k33 * k35 * t2 * t10 * 2.0 - k12 * k13 * k16 * k22 * k23 * k25 * k32 * 8.0 + k12 * k13 * k15 * k22 * k23 * k25 * k35 * 8.0 + k12 * k13 * k16 * k22 * k23 * k32 * k35 * 4.0 - k12 * k13 * k22 * k23 * k25 * k26 * k32 * 4.0 + k12 * k15 * k16 * k22 * k25 * k32 * k33 * 4.0 + k12 * k13 * k15 * k25 * k32 * k33 * k35 * 8.0 - k12 * k15 * k16 * k22 * k32 * k33 * k35 * 8.0 - k12 * k15 * k22 * k25 * k26 * k32 * k33 * 4.0 - k12 * k13 * k22 * k23 * k32 * k35 * k36 * 4.0 - k12 * k15 * k22 * k32 * k33 * k35 * k36 * 4.0 + k12 * k13 * k15 * k16 * k22 * k23 * t4 * 2.0 + k12 * k13 * k15 * k22 * k23 * k26 * t4 * 2.0 - k12 * k13 * k16 * k22 * k23 * k33 * t4 * 6.0 + k12 * k13 * k15 * k16 * k32 * k33 * t3 * 2.0 + k12 * k13 * k15 * k22 * k23 * k36 * t4 * 2.0 - k12 * k13 * k22 * k23 * k26 * k33 * t4 * 6.0 + k12 * k13 * k15 * k26 * k32 * k33 * t3 * 2.0 - k12 * k15 * k16 * k23 * k32 * k33 * t3 * 6.0 - k12 * k13 * k22 * k23 * k25 * k32 * t12 * 6.0 - k13 * k16 * k22 * k25 * k32 * k33 * t2 * 2.0 + k12 * k13 * k15 * k32 * k33 * k36 * t3 * 2.0 - k12 * k13 * k25 * k26 * k32 * k35 * t2 * 4.0 + k12 * k16 * k22 * k25 * k32 * k36 * t2 + k12 * k16 * k22 * k25 * k33 * k35 * t2 + k12 * k16 * k22 * k26 * k32 * k35 * t2 + k12 * k16 * k23 * k25 * k32 * k35 * t2 + k13 * k15 * k22 * k25 * k32 * k36 * t2 * 3.0 + k13 * k15 * k22 * k25 * k33 * k35 * t2 * 3.0 + k13 * k15 * k22 * k26 * k32 * k35 * t2 * 3.0 + k13 * k15 * k23 * k25 * k32 * k35 * t2 * 3.0 - k15 * k16 * k22 * k23 * k32 * k35 * t2 * 2.0 - k12 * k13 * k22 * k23 * k32 * k35 * t9 * 2.0 - k12 * k15 * k22 * k25 * k32 * k33 * t8 * 2.0 - k12 * k15 * k22 * k25 * k35 * k36 * t2 * 4.0 - k13 * k22 * k25 * k26 * k32 * k33 * t2 * 4.0 + k16 * k22 * k23 * k25 * k32 * k33 * t2 * 4.0 - k12 * k15 * k23 * k32 * k33 * k36 * t3 * 6.0 + k12 * k22 * k25 * k26 * k32 * k36 * t2 * 2.0 + k12 * k22 * k25 * k26 * k33 * k35 * t2 * 2.0 + k12 * k23 * k25 * k26 * k32 * k35 * t2 * 2.0 - k15 * k22 * k23 * k25 * k32 * k36 * t2 * 4.0 - k15 * k22 * k23 * k25 * k33 * k35 * t2 * 4.0 - k15 * k22 * k23 * k26 * k32 * k35 * t2 * 4.0 - k12 * k15 * k22 * k32 * k33 * k35 * t10 * 6.0 - k13 * k22 * k25 * k32 * k33 * k36 * t2 * 4.0 - k13 * k22 * k26 * k32 * k33 * k35 * t2 * 4.0 - k13 * k23 * k25 * k32 * k33 * k35 * t2 * 4.0 + k16 * k22 * k23 * k32 * k33 * k35 * t2 * 4.0 + k12 * k22 * k25 * k33 * k35 * k36 * t2 * 2.0 + k12 * k22 * k26 * k32 * k35 * k36 * t2 * 2.0 + k12 * k23 * k25 * k32 * k35 * k36 * t2 * 2.0 - k15 * k22 * k23 * k32 * k35 * k36 * t2 * 4.0 + k22 * k23 * k25 * k32 * k33 * k36 * t2 * 4.0 + k22 * k23 * k26 * k32 * k33 * k35 * t2 * 4.0 + k12 * k13 * k15 * k22 * k23 * k25 * k32 * k33 * 4.0 + k12 * k13 * k15 * k22 * k23 * k32 * k33 * k35 * 4.0; coeffs(6) = k16 * t14 * -8.0 - k16 * t15 * 2.4E1 - k16 * t16 * 2.4E1 - k16 * t19 * 2.0 - k16 * t20 * 2.0 + k26 * t14 * 2.4E1 + k26 * t15 * 8.0 + k26 * t16 * 2.4E1 + k26 * t17 * 2.0 + k26 * t19 * 2.0 + k36 * t14 * 2.4E1 + k36 * t15 * 2.4E1 + k36 * t16 * 8.0 + k36 * t18 * 2.0 + k36 * t20 * 2.0 + t8 * t14 * 2.0 + t8 * t15 * 1.0E1 + t9 * t14 * 2.0 + t8 * t16 * 2.0 + t9 * t15 * 2.0 - t10 * t14 * 1.0E1 + t9 * t16 * 1.0E1 - t10 * t15 * 2.0 + t11 * t14 * 6.0 - t10 * t16 * 1.0E1 + t11 * t15 * 6.0 - t12 * t14 * 1.0E1 + t8 * t19 + t11 * t16 * 6.0 - t12 * t15 * 1.0E1 + t13 * t14 * 6.0 + t11 * t17 - t12 * t16 * 2.0 + t13 * t15 * 6.0 + t9 * t20 + t13 * t16 * 6.0 + t11 * t19 + t13 * t18 + t13 * t20 - k16 * k26 * k36 * 4.8E1 + k13 * k23 * t14 * 8.0 - k16 * k26 * t8 * 1.2E1 - k13 * k23 * t15 * 8.0 - k16 * k26 * t9 * 4.0 + k13 * k23 * t16 * 8.0 + k16 * k26 * t10 * 1.2E1 - k16 * k26 * t11 * 1.2E1 + k16 * k26 * t12 * 2.0E1 - k13 * k23 * t19 - k16 * k26 * t13 * 1.2E1 - k16 * k36 * t8 * 4.0 - k16 * k36 * t9 * 1.2E1 + k15 * k33 * t14 * 8.0 + k16 * k36 * t10 * 2.0E1 + k15 * k33 * t15 * 8.0 - k16 * k36 * t11 * 1.2E1 - k15 * k33 * t16 * 8.0 + k16 * k36 * t12 * 1.2E1 - k16 * k36 * t13 * 1.2E1 - k15 * k33 * t20 + k26 * k36 * t8 * 1.2E1 + k26 * k36 * t9 * 1.2E1 - k26 * k36 * t10 * 1.2E1 + k26 * k36 * t11 * 1.2E1 + k25 * k35 * t14 * 1.2E1 - k26 * k36 * t12 * 1.2E1 + k25 * k35 * t15 * 1.2E1 + k26 * k36 * t13 * 1.2E1 + k25 * k35 * t16 * 1.2E1 + k16 * t2 * t15 * 4.0 + k16 * t3 * t14 * 2.0 + k16 * t2 * t16 * 4.0 + k16 * t3 * t15 * 2.0 + k16 * t4 * t14 * 2.0 + k16 * t8 * t10 * 2.0 + k16 * t3 * t16 * 6.0 + k16 * t4 * t15 * 6.0 - k16 * t8 * t11 * 4.0 + k16 * t9 * t10 * 2.0 + k16 * t4 * t16 * 2.0 + k16 * t8 * t12 * 2.0 + k16 * t9 * t12 * 2.0 + k16 * t10 * t11 * 2.0 + k16 * t3 * t19 - k16 * t9 * t13 * 4.0 - k16 * t10 * t12 * 8.0 + k16 * t10 * t13 * 6.0 + k16 * t11 * t12 * 6.0 + k16 * t4 * t20 + k16 * t12 * t13 * 2.0 - k26 * t2 * t14 * 2.0 - k26 * t2 * t15 * 2.0 - k26 * t3 * t14 * 4.0 - k26 * t8 * t9 * 2.0 - k26 * t2 * t16 * 1.4E1 - k26 * t4 * t14 * 6.0 - k26 * t8 * t10 * 2.0 - k26 * t3 * t16 * 4.0 - k26 * t4 * t15 * 2.0 + k26 * t8 * t11 * 6.0 - k26 * t9 * t10 * 4.0 - k26 * t4 * t16 * 2.0 - k26 * t8 * t12 * 8.0 + k26 * t5 * t16 * 2.0 + k26 * t8 * t13 * 2.0 - k26 * t9 * t12 * 2.0 + k26 * t9 * t13 * 4.0 + k26 * t10 * t12 * 2.0 - k26 * t10 * t13 * 4.0 - k26 * t11 * t12 * 6.0 - k26 * t12 * t13 * 2.0 - k36 * t2 * t14 * 2.0 - k36 * t2 * t15 * 1.4E1 - k36 * t3 * t14 * 6.0 - k36 * t8 * t9 * 2.0 - k36 * t2 * t16 * 2.0 - k36 * t3 * t15 * 2.0 - k36 * t4 * t14 * 4.0 - k36 * t8 * t10 * 2.0 - k36 * t3 * t16 * 2.0 - k36 * t4 * t15 * 4.0 + k36 * t8 * t11 * 4.0 - k36 * t9 * t10 * 8.0 + k36 * t5 * t15 * 2.0 - k36 * t8 * t12 * 4.0 + k36 * t9 * t11 * 2.0 - k36 * t9 * t12 * 2.0 - k36 * t10 * t11 * 2.0 + k36 * t9 * t13 * 6.0 + k36 * t10 * t12 * 2.0 - k36 * t10 * t13 * 6.0 - k36 * t11 * t12 * 4.0 + t2 * t8 * t15 * 2.0 + t3 * t8 * t14 + t2 * t10 * t14 + t3 * t8 * t15 + t2 * t9 * t16 * 2.0 + t2 * t10 * t15 + t3 * t8 * t16 + t3 * t10 * t14 * 6.0 - t4 * t8 * t15 * 2.0 + t4 * t9 * t14 + t8 * t9 * t10 + t2 * t10 * t16 * 3.0 + t2 * t12 * t14 - t3 * t9 * t16 * 2.0 + t4 * t9 * t15 + t4 * t10 * t14 * 3.0 - t2 * t11 * t16 * 4.0 + t2 * t12 * t15 * 3.0 + t3 * t10 * t16 * 6.0 + t3 * t12 * t14 * 3.0 + t4 * t9 * t16 + t4 * t10 * t15 + t8 * t9 * t12 + t8 * t10 * t11 + t2 * t12 * t16 - t2 * t13 * t15 * 4.0 + t3 * t12 * t15 + t4 * t10 * t16 + t4 * t12 * t14 * 6.0 + t8 * t10 * t12 * 2.0 + t3 * t12 * t16 + t4 * t12 * t15 * 6.0 + t8 * t10 * t13 - t8 * t11 * t12 * 2.0 + t9 * t10 * t12 * 2.0 + t5 * t11 * t16 - t9 * t10 * t13 * 2.0 + t9 * t11 * t12 + t5 * t13 * t15 + t10 * t11 * t12 + t9 * t12 * t13 + t10 * t12 * t13 - k13 * k16 * k23 * k36 * 1.6E1 - k15 * k16 * k26 * k33 * 1.6E1 - k16 * k25 * k26 * k35 * 2.4E1 - k16 * k25 * k35 * k36 * 2.4E1 + k25 * k26 * k35 * k36 * 2.4E1 - k12 * k13 * k15 * t15 * 8.0 - k12 * k13 * k15 * t16 * 8.0 - k13 * k16 * k23 * t8 * 2.0 - k13 * k16 * k23 * t9 * 2.0 + k13 * k16 * k23 * t10 * 2.0 + k13 * k16 * k23 * t11 * 2.0 - k12 * k15 * k23 * t14 * 2.0 + k13 * k16 * k23 * t12 * 6.0 + k12 * k15 * k23 * t15 * 6.0 - k13 * k16 * k23 * t13 * 6.0 - k12 * k15 * k23 * t16 * 2.0 - k13 * k23 * k26 * t8 * 2.0 + k13 * k23 * k26 * t9 * 6.0 - k12 * k13 * k33 * t14 * 2.0 + k13 * k23 * k26 * t10 * 2.0 - k15 * k16 * k33 * t8 * 2.0 - k12 * k13 * k33 * t15 * 2.0 - k13 * k23 * k26 * t11 * 6.0 - k15 * k16 * k33 * t9 * 2.0 + k12 * k13 * k33 * t16 * 6.0 + k13 * k22 * k25 * t14 * 2.0 + k13 * k23 * k26 * t12 * 6.0 + k15 * k16 * k33 * t10 * 6.0 - k13 * k22 * k25 * t15 * 6.0 + k13 * k23 * k26 * t13 * 2.0 - k15 * k16 * k33 * t11 * 6.0 + k13 * k22 * k25 * t16 * 2.0 + k15 * k16 * k33 * t12 * 2.0 + k15 * k16 * k33 * t13 * 2.0 - k13 * k22 * k25 * t19 + k13 * k23 * k36 * t8 * 2.0 + k16 * k26 * k36 * t2 * 1.6E1 + k13 * k23 * k36 * t9 * 1.0E1 + k16 * k26 * k36 * t3 * 8.0 - k12 * k23 * k33 * t14 * 2.0 - k13 * k23 * k36 * t10 * 2.0 + k15 * k26 * k33 * t8 * 1.0E1 + k16 * k26 * k36 * t4 * 8.0 - k12 * k23 * k33 * t15 * 2.0 - k13 * k23 * k36 * t11 * 2.0 + k15 * k26 * k33 * t9 * 2.0 - k12 * k23 * k33 * t16 * 2.0 + k13 * k22 * k35 * t14 * 6.0 + k13 * k23 * k36 * t12 * 2.0 + k15 * k26 * k33 * t10 * 2.0 - k16 * k25 * k35 * t8 * 4.0 - k22 * k23 * k25 * t14 * 8.0 - k13 * k22 * k35 * t15 * 2.0 + k13 * k23 * k36 * t13 * 6.0 + k15 * k26 * k33 * t11 * 6.0 - k16 * k25 * k35 * t9 * 4.0 + k13 * k22 * k35 * t16 * 6.0 + k15 * k25 * k32 * t14 * 6.0 - k15 * k26 * k33 * t12 * 2.0 + k16 * k25 * k35 * t10 * 8.0 - k22 * k23 * k25 * t16 * 8.0 + k15 * k25 * k32 * t15 * 6.0 - k15 * k26 * k33 * t13 * 2.0 - k15 * k25 * k32 * t16 * 2.0 + k16 * k25 * k35 * t12 * 8.0 + k15 * k33 * k36 * t8 * 6.0 - k15 * k33 * k36 * t9 * 2.0 + k15 * k33 * k36 * t10 * 6.0 - k22 * k23 * k35 * t14 * 1.2E1 + k25 * k26 * k35 * t8 * 8.0 + k15 * k33 * k36 * t11 * 2.0 - k22 * k23 * k35 * t15 * 4.0 + k25 * k26 * k35 * t9 * 4.0 + k15 * k32 * k35 * t14 * 2.0 + k15 * k33 * k36 * t12 * 2.0 - k22 * k23 * k35 * t16 * 1.2E1 - k25 * k26 * k35 * t10 * 4.0 + k15 * k32 * k35 * t15 * 2.0 - k15 * k33 * k36 * t13 * 6.0 - k15 * k32 * k35 * t16 * 6.0 - k25 * k26 * k35 * t12 * 8.0 - k15 * k32 * k35 * t20 - k25 * k32 * k33 * t14 * 1.2E1 + k25 * k35 * k36 * t8 * 4.0 - k25 * k32 * k33 * t15 * 1.2E1 + k25 * k35 * k36 * t9 * 8.0 - k25 * k32 * k33 * t16 * 4.0 - k25 * k35 * k36 * t10 * 8.0 - k25 * k35 * k36 * t12 * 4.0 - k32 * k33 * k35 * t14 * 8.0 - k32 * k33 * k35 * t15 * 8.0 - k13 * k23 * t2 * t15 * 2.0 - k13 * k23 * t3 * t14 * 6.0 + k16 * k26 * t3 * t8 * 4.0 + k13 * k23 * t2 * t16 * 2.0 - k13 * k23 * t4 * t14 * 3.0 + k16 * k26 * t4 * t8 * 2.0 - k13 * k23 * t3 * t16 * 6.0 + k13 * k23 * t4 * t15 - k13 * k23 * t8 * t11 * 2.0 - k13 * k23 * t9 * t10 * 2.0 - k16 * k26 * t4 * t9 * 2.0 - k13 * k23 * t4 * t16 - k13 * k23 * t8 * t12 - k16 * k26 * t2 * t12 * 4.0 - k16 * k26 * t4 * t10 * 4.0 - k13 * k23 * t9 * t12 * 3.0 + k16 * k26 * t2 * t13 * 4.0 - k16 * k26 * t3 * t12 * 4.0 + k13 * k23 * t9 * t13 * 2.0 - k13 * k23 * t10 * t12 - k16 * k26 * t4 * t12 * 1.2E1 - k13 * k23 * t10 * t13 * 2.0 + k13 * k23 * t11 * t12 - k13 * k23 * t12 * t13 - k16 * k36 * t3 * t8 * 2.0 - k16 * k36 * t2 * t10 * 4.0 + k16 * k36 * t3 * t9 * 2.0 + k15 * k33 * t2 * t15 * 2.0 - k15 * k33 * t3 * t14 * 3.0 + k16 * k36 * t2 * t11 * 4.0 - k16 * k36 * t3 * t10 * 1.2E1 + k16 * k36 * t4 * t9 * 4.0 - k15 * k33 * t2 * t16 * 2.0 - k15 * k33 * t3 * t15 - k15 * k33 * t4 * t14 * 6.0 - k15 * k33 * t8 * t10 * 3.0 - k16 * k36 * t4 * t10 * 4.0 + k15 * k33 * t3 * t16 - k15 * k33 * t4 * t15 * 6.0 + k15 * k33 * t8 * t11 * 2.0 - k15 * k33 * t9 * t10 - k16 * k36 * t3 * t12 * 4.0 - k15 * k33 * t8 * t12 * 2.0 - k15 * k33 * t10 * t11 - k15 * k33 * t9 * t13 * 2.0 - k15 * k33 * t10 * t12 + k15 * k33 * t10 * t13 - k15 * k33 * t11 * t12 * 2.0 + k26 * k36 * t2 * t8 * 4.0 + k26 * k36 * t2 * t9 * 4.0 - k26 * k36 * t3 * t8 * 4.0 + k26 * k36 * t2 * t10 * 2.0 - k26 * k36 * t3 * t9 * 4.0 - k26 * k36 * t4 * t8 * 4.0 - k26 * k36 * t2 * t11 * 6.0 - k26 * k36 * t4 * t9 * 4.0 - k25 * k35 * t2 * t14 * 2.0 + k26 * k36 * t2 * t12 * 2.0 - k25 * k35 * t2 * t15 * 6.0 - k25 * k35 * t8 * t9 * 2.0 - k26 * k36 * t2 * t13 * 6.0 - k25 * k35 * t2 * t16 * 6.0 - k25 * k35 * t8 * t10 * 4.0 - k25 * k35 * t9 * t10 * 4.0 - k25 * k35 * t8 * t12 * 4.0 - k25 * k35 * t9 * t12 * 4.0 - k16 * t2 * t3 * t16 * 2.0 - k16 * t2 * t4 * t15 * 2.0 + k16 * t4 * t8 * t10 + k16 * t3 * t8 * t12 + k16 * t4 * t9 * t10 + k16 * t2 * t10 * t12 * 2.0 + k16 * t3 * t9 * t12 - k16 * t2 * t10 * t13 * 2.0 - k16 * t2 * t11 * t12 * 2.0 + k16 * t3 * t10 * t12 * 6.0 + k16 * t4 * t10 * t12 * 6.0 + k26 * t2 * t4 * t14 + k26 * t2 * t4 * t15 + k26 * t4 * t8 * t9 + k26 * t2 * t4 * t16 + k26 * t2 * t8 * t12 * 2.0 + k26 * t4 * t8 * t10 * 2.0 + k26 * t3 * t8 * t12 * 2.0 + k26 * t2 * t9 * t13 * 2.0 + k26 * t2 * t10 * t12 + k26 * t4 * t8 * t12 * 6.0 + k26 * t2 * t11 * t12 * 3.0 + k26 * t2 * t12 * t13 + k36 * t2 * t3 * t14 + k36 * t2 * t3 * t15 + k36 * t3 * t8 * t9 + k36 * t2 * t3 * t16 + k36 * t2 * t8 * t11 * 2.0 + k36 * t2 * t9 * t10 * 2.0 + k36 * t3 * t9 * t10 * 6.0 + k36 * t2 * t10 * t11 + k36 * t4 * t9 * t10 * 2.0 + k36 * t2 * t10 * t12 + k36 * t3 * t9 * t12 * 2.0 + k36 * t2 * t10 * t13 * 3.0 + t2 * t4 * t8 * t15 + t2 * t3 * t9 * t16 + t2 * t8 * t11 * t12 + t2 * t9 * t10 * t13 + k12 * k13 * k15 * k16 * k26 * 8.0; coeffs(6) += k12 * k13 * k15 * k16 * k36 * 8.0 - k12 * k15 * k16 * k23 * k26 * 4.0 + k12 * k13 * k16 * k26 * k33 * 4.0 - k13 * k15 * k16 * k23 * k33 * 4.0 - k12 * k13 * k15 * k26 * k36 * 3.2E1 + k12 * k15 * k16 * k23 * k36 * 4.0 + k13 * k16 * k22 * k25 * k26 * 4.0 - k12 * k13 * k16 * k33 * k36 * 4.0 + k12 * k16 * k23 * k26 * k33 * 4.0 - k13 * k15 * k23 * k26 * k33 * 1.2E1 + k12 * k15 * k23 * k26 * k36 * 2.0E1 - k13 * k16 * k22 * k25 * k36 * 4.0 - k13 * k16 * k22 * k26 * k35 * 4.0 - k13 * k16 * k23 * k25 * k35 * 4.0 + k16 * k22 * k23 * k25 * k26 * 8.0 - k15 * k16 * k25 * k26 * k32 * 1.2E1 + k12 * k13 * k26 * k33 * k36 * 2.0E1 + k12 * k16 * k23 * k33 * k36 * 4.0 - k13 * k15 * k23 * k33 * k36 * 1.2E1 - k13 * k16 * k22 * k35 * k36 * 1.2E1 - k13 * k22 * k25 * k26 * k36 * 4.0 - k13 * k23 * k25 * k26 * k35 * 4.0 + k16 * k22 * k23 * k25 * k36 * 1.6E1 + k16 * k22 * k23 * k26 * k35 * 1.6E1 - k15 * k16 * k25 * k32 * k36 * 4.0 - k15 * k16 * k25 * k33 * k35 * 4.0 - k15 * k16 * k26 * k32 * k35 * 4.0 - k12 * k23 * k26 * k33 * k36 * 2.0E1 + k13 * k22 * k26 * k35 * k36 * 4.0 + k13 * k23 * k25 * k35 * k36 * 4.0 + k16 * k22 * k23 * k35 * k36 * 2.4E1 + k16 * k25 * k26 * k32 * k33 * 2.4E1 - k22 * k23 * k25 * k26 * k36 * 8.0 + k15 * k16 * k32 * k35 * k36 * 4.0 + k15 * k25 * k26 * k32 * k36 * 4.0 + k15 * k25 * k26 * k33 * k35 * 4.0 + k16 * k25 * k32 * k33 * k36 * 1.6E1 + k16 * k26 * k32 * k33 * k35 * 1.6E1 - k22 * k23 * k26 * k35 * k36 * 1.6E1 - k15 * k25 * k33 * k35 * k36 * 4.0 - k15 * k26 * k32 * k35 * k36 * 4.0 + k16 * k32 * k33 * k35 * k36 * 8.0 - k25 * k26 * k32 * k33 * k36 * 1.6E1 - k26 * k32 * k33 * k35 * k36 * 8.0 + k12 * k13 * k15 * k26 * t10 * 4.0 + k12 * k13 * k15 * k26 * t12 * 4.0 - k12 * k15 * k16 * k23 * t12 * 2.0 - k12 * k13 * k15 * k26 * t13 * 8.0 + k12 * k15 * k16 * k23 * t13 * 4.0 - k13 * k16 * k23 * k26 * t2 * 2.0 - k13 * k16 * k23 * k26 * t3 * 6.0 + k13 * k16 * k23 * k26 * t4 * 2.0 - k12 * k13 * k16 * k33 * t10 * 2.0 - k12 * k15 * k23 * k26 * t8 * 2.0 - k13 * k16 * k22 * k25 * t8 * 2.0 + k12 * k13 * k16 * k33 * t11 * 4.0 + k12 * k13 * k15 * k36 * t10 * 4.0 - k12 * k15 * k23 * k26 * t10 * 2.0 - k13 * k16 * k22 * k25 * t10 * 6.0 - k12 * k13 * k15 * k36 * t11 * 8.0 + k12 * k13 * k15 * k36 * t12 * 4.0 - k12 * k15 * k23 * k26 * t12 * 2.0 + k13 * k16 * k22 * k25 * t12 * 2.0 + k12 * k15 * k23 * k26 * t13 * 4.0 + k12 * k15 * k22 * k25 * t16 * 4.0 - k13 * k16 * k23 * k36 * t2 * 2.0 + k13 * k16 * k23 * k36 * t3 * 1.2E1 - k12 * k13 * k26 * k33 * t8 * 4.0 + k12 * k16 * k23 * k33 * t8 * 3.0 + k13 * k15 * k23 * k33 * t8 + k13 * k16 * k23 * k36 * t4 * 4.0 - k15 * k16 * k26 * k33 * t2 * 2.0 - k12 * k13 * k26 * k33 * t9 * 2.0 + k12 * k16 * k23 * k33 * t9 * 3.0 + k13 * k15 * k23 * k33 * t9 + k15 * k16 * k26 * k33 * t3 * 4.0 - k12 * k13 * k26 * k33 * t10 * 2.0 - k12 * k15 * k23 * k36 * t8 * 2.0 - k12 * k16 * k23 * k33 * t10 + k13 * k15 * k23 * k33 * t10 * 3.0 - k13 * k22 * k25 * k26 * t8 * 4.0 + k15 * k16 * k26 * k33 * t4 * 1.2E1 + k16 * k22 * k23 * k25 * t8 * 8.0 - k12 * k13 * k26 * k33 * t11 * 6.0 - k12 * k15 * k23 * k36 * t9 * 4.0 - k12 * k16 * k23 * k33 * t11 - k13 * k15 * k23 * k33 * t11 - k13 * k16 * k22 * k35 * t9 * 2.0 - k12 * k13 * k25 * k32 * t14 * 2.0 - k12 * k13 * k26 * k33 * t12 * 2.0 - k12 * k15 * k23 * k36 * t10 * 2.0 + k12 * k16 * k22 * k32 * t14 - k12 * k16 * k23 * k33 * t12 + k13 * k15 * k22 * k32 * t14 * 3.0 + k13 * k15 * k23 * k33 * t12 * 3.0 + k13 * k16 * k22 * k35 * t10 * 1.2E1 - k15 * k16 * k25 * k32 * t8 * 2.0 - k12 * k13 * k25 * k32 * t15 * 6.0 + k12 * k13 * k26 * k33 * t13 * 6.0 + k12 * k15 * k23 * k36 * t11 * 6.0 - k12 * k16 * k22 * k32 * t15 - k12 * k16 * k23 * k33 * t13 - k13 * k15 * k22 * k32 * t15 - k13 * k15 * k23 * k33 * t13 + k12 * k13 * k25 * k32 * t16 * 6.0 - k12 * k15 * k22 * k35 * t14 * 2.0 - k12 * k15 * k23 * k36 * t12 * 2.0 - k12 * k16 * k22 * k32 * t16 - k13 * k15 * k22 * k32 * t16 + k13 * k16 * k22 * k35 * t12 * 4.0 + k13 * k22 * k25 * k26 * t12 * 2.0 + k15 * k16 * k25 * k32 * t10 * 4.0 - k16 * k22 * k23 * k25 * t12 * 8.0 + k12 * k15 * k22 * k35 * t15 * 6.0 - k12 * k15 * k23 * k36 * t13 * 6.0 - k12 * k15 * k22 * k35 * t16 * 6.0 + k15 * k16 * k25 * k32 * t12 * 1.2E1 + k13 * k23 * k26 * k36 * t3 * 6.0 + k12 * k23 * k26 * k33 * t8 * 5.0 + k13 * k23 * k26 * k36 * t4 * 4.0 - k15 * k16 * k33 * k36 * t2 * 2.0 - k12 * k13 * k33 * k36 * t9 * 2.0 - k12 * k23 * k26 * k33 * t9 + k15 * k16 * k33 * k36 * t3 * 2.0 - k12 * k13 * k33 * k36 * t10 * 2.0 + k12 * k23 * k26 * k33 * t10 + k13 * k22 * k25 * k36 * t8 * 2.0 + k13 * k22 * k26 * k35 * t8 * 2.0 + k13 * k23 * k25 * k35 * t8 * 2.0 - k15 * k16 * k33 * k36 * t4 * 6.0 - k16 * k22 * k23 * k35 * t8 * 4.0 + k16 * k25 * k26 * k35 * t2 * 8.0 + k22 * k23 * k25 * k26 * t8 * 4.0 + k12 * k13 * k33 * k36 * t11 * 4.0 + k12 * k23 * k26 * k33 * t11 * 3.0 + k13 * k22 * k25 * k36 * t9 * 6.0 + k13 * k22 * k26 * k35 * t9 * 6.0 + k13 * k23 * k25 * k35 * t9 * 6.0 + k16 * k22 * k23 * k35 * t9 * 4.0 - k12 * k13 * k33 * k36 * t12 * 2.0 - k12 * k22 * k25 * k33 * t14 - k12 * k22 * k26 * k32 * t14 - k12 * k23 * k25 * k32 * t14 + k12 * k23 * k26 * k33 * t12 * 3.0 + k13 * k22 * k25 * k36 * t10 * 6.0 + k13 * k22 * k26 * k35 * t10 * 6.0 + k13 * k23 * k25 * k35 * t10 * 2.0 - k15 * k22 * k23 * k32 * t14 * 6.0 + k15 * k25 * k26 * k32 * t8 * 4.0 - k16 * k22 * k23 * k35 * t10 * 8.0 + k12 * k13 * k32 * k35 * t15 * 4.0 + k12 * k22 * k25 * k33 * t15 * 3.0 + k12 * k22 * k26 * k32 * t15 + k12 * k23 * k25 * k32 * t15 * 3.0 - k12 * k23 * k26 * k33 * t13 * 5.0 - k15 * k16 * k32 * k35 * t9 * 2.0 - k15 * k22 * k23 * k32 * t15 * 2.0 - k12 * k22 * k25 * k33 * t16 * 5.0 - k12 * k22 * k26 * k32 * t16 * 5.0 - k12 * k23 * k25 * k32 * t16 * 5.0 + k13 * k22 * k25 * k36 * t12 * 4.0 + k13 * k22 * k26 * k35 * t12 * 4.0 + k13 * k23 * k25 * k35 * t12 * 4.0 + k15 * k16 * k32 * k35 * t10 * 2.0 + k15 * k22 * k23 * k32 * t16 * 2.0 - k15 * k25 * k26 * k32 * t10 * 2.0 - k16 * k22 * k23 * k35 * t12 * 8.0 + k22 * k23 * k25 * k26 * t12 * 4.0 - k15 * k16 * k32 * k35 * t12 * 6.0 - k15 * k25 * k26 * k32 * t12 * 1.2E1 - k12 * k23 * k33 * k36 * t8 + k12 * k23 * k33 * k36 * t9 * 5.0 + k15 * k26 * k33 * k36 * t3 * 4.0 + k12 * k23 * k33 * k36 * t10 * 3.0 - k13 * k22 * k32 * k33 * t14 * 6.0 + k15 * k26 * k33 * k36 * t4 * 6.0 + k16 * k25 * k32 * k33 * t8 * 4.0 + k16 * k25 * k35 * k36 * t2 * 8.0 - k22 * k23 * k25 * k36 * t8 * 8.0 - k22 * k23 * k26 * k35 * t8 * 8.0 - k12 * k23 * k33 * k36 * t11 * 5.0 + k13 * k22 * k32 * k33 * t15 * 2.0 + k13 * k22 * k35 * k36 * t9 * 4.0 - k16 * k25 * k32 * k33 * t9 * 4.0 - k22 * k23 * k25 * k36 * t9 * 8.0 - k22 * k23 * k26 * k35 * t9 * 8.0 - k12 * k22 * k32 * k36 * t14 - k12 * k22 * k33 * k35 * t14 - k12 * k23 * k32 * k35 * t14 + k12 * k23 * k33 * k36 * t12 - k13 * k22 * k32 * k33 * t16 * 2.0 - k13 * k22 * k35 * k36 * t10 * 1.2E1 + k15 * k25 * k32 * k36 * t8 * 6.0 + k15 * k25 * k33 * k35 * t8 * 6.0 + k15 * k26 * k32 * k35 * t8 * 6.0 - k16 * k25 * k32 * k33 * t10 * 8.0 - k12 * k22 * k32 * k36 * t15 * 5.0 - k12 * k22 * k33 * k35 * t15 * 5.0 - k12 * k23 * k32 * k35 * t15 * 5.0 + k12 * k23 * k33 * k36 * t13 * 3.0 + k15 * k25 * k32 * k36 * t9 * 2.0 + k15 * k25 * k33 * k35 * t9 * 2.0 + k15 * k26 * k32 * k35 * t9 * 2.0 + k12 * k22 * k32 * k36 * t16 + k12 * k22 * k33 * k35 * t16 * 3.0 + k12 * k23 * k32 * k35 * t16 * 3.0 - k13 * k22 * k35 * k36 * t12 * 2.0 + k15 * k25 * k32 * k36 * t10 * 4.0 + k15 * k25 * k33 * k35 * t10 * 4.0 + k15 * k26 * k32 * k35 * t10 * 4.0 - k16 * k25 * k32 * k33 * t12 * 8.0 + k15 * k25 * k32 * k36 * t12 * 6.0 + k15 * k25 * k33 * k35 * t12 * 2.0 + k15 * k26 * k32 * k35 * t12 * 6.0 + k22 * k23 * k32 * k33 * t14 * 1.2E1 + k22 * k23 * k35 * k36 * t8 * 4.0 - k25 * k26 * k32 * k33 * t8 * 8.0 - k25 * k26 * k35 * k36 * t2 * 1.6E1 + k16 * k32 * k33 * k35 * t9 * 8.0 + k22 * k23 * k32 * k33 * t15 * 4.0 - k22 * k23 * k35 * k36 * t9 * 8.0 + k25 * k26 * k32 * k33 * t9 * 4.0 - k16 * k32 * k33 * k35 * t10 * 8.0 + k22 * k23 * k32 * k33 * t16 * 4.0 + k22 * k23 * k35 * k36 * t10 * 8.0 + k25 * k26 * k32 * k33 * t10 * 4.0 - k15 * k32 * k35 * k36 * t9 * 4.0 + k25 * k26 * k35 * k36 * t5 * 4.0 + k15 * k32 * k35 * k36 * t10 * 2.0 + k22 * k23 * k35 * k36 * t12 * 4.0 + k25 * k26 * k32 * k33 * t12 * 8.0 - k25 * k32 * k33 * k36 * t8 * 8.0 - k26 * k32 * k33 * k35 * t8 * 8.0 - k25 * k32 * k33 * k36 * t9 * 8.0 - k26 * k32 * k33 * k35 * t9 * 8.0 + k32 * k33 * k35 * k36 * t9 * 4.0 + k32 * k33 * k35 * k36 * t10 * 4.0 + k12 * k13 * k15 * t10 * t12 * 2.0 - k13 * k16 * k23 * t4 * t9 - k13 * k16 * k23 * t2 * t12 - k13 * k16 * k23 * t4 * t10 * 2.0 - k13 * k16 * k23 * t3 * t12 * 6.0 - k12 * k15 * k23 * t2 * t16 * 2.0 - k12 * k15 * k23 * t4 * t14 - k13 * k16 * k23 * t4 * t12 * 6.0 - k12 * k15 * k23 * t4 * t15 - k12 * k15 * k23 * t4 * t16 - k12 * k15 * k23 * t8 * t12 - k12 * k15 * k23 * t9 * t13 * 2.0 - k12 * k15 * k23 * t10 * t12 - k12 * k15 * k23 * t11 * t12 - k13 * k23 * k26 * t4 * t8 - k12 * k13 * k33 * t2 * t15 * 2.0 - k12 * k13 * k33 * t3 * t14 - k12 * k15 * k23 * t12 * t13 - k13 * k23 * k26 * t4 * t9 - k15 * k16 * k33 * t3 * t8 - k12 * k13 * k33 * t3 * t15 - k13 * k23 * k26 * t2 * t12 * 4.0 - k13 * k23 * k26 * t4 * t10 - k15 * k16 * k33 * t2 * t10 - k12 * k13 * k33 * t3 * t16 - k12 * k13 * k33 * t8 * t11 * 2.0 - k12 * k13 * k33 * t9 * t10 + k13 * k23 * k26 * t2 * t13 * 2.0 - k13 * k23 * k26 * t3 * t12 * 3.0 - k15 * k16 * k33 * t3 * t10 * 6.0 + k13 * k22 * k25 * t2 * t16 * 2.0 - k13 * k23 * k26 * t4 * t12 * 6.0 - k15 * k16 * k33 * t4 * t10 * 6.0 - k12 * k13 * k33 * t10 * t11 - k15 * k16 * k33 * t3 * t12 * 2.0 - k12 * k13 * k33 * t10 * t12 - k13 * k22 * k25 * t8 * t12 - k12 * k13 * k33 * t10 * t13 - k13 * k22 * k25 * t9 * t12 - k13 * k22 * k25 * t10 * t12 * 3.0 - k13 * k23 * k36 * t3 * t9 * 6.0 + k12 * k23 * k33 * t2 * t15 + k12 * k23 * k33 * t3 * t14 * 3.0 - k13 * k23 * k36 * t2 * t11 * 2.0 - k13 * k23 * k36 * t4 * t9 * 2.0 - k15 * k26 * k33 * t3 * t8 * 2.0 + k12 * k23 * k33 * t2 * t16 + k12 * k23 * k33 * t4 * t14 * 3.0 - k13 * k23 * k36 * t2 * t12 * 2.0 - k15 * k26 * k33 * t2 * t10 * 2.0 - k15 * k26 * k33 * t4 * t8 * 6.0 + k12 * k23 * k33 * t3 * t16 * 3.0 + k12 * k23 * k33 * t4 * t15 * 3.0 + k12 * k23 * k33 * t8 * t11 * 3.0 + k12 * k23 * k33 * t9 * t10 - k13 * k22 * k35 * t2 * t15 * 2.0 + k12 * k23 * k33 * t8 * t12 + k12 * k22 * k32 * t8 * t15 * 3.0 - k13 * k22 * k35 * t9 * t10 * 6.0 - k15 * k26 * k33 * t2 * t13 * 2.0 + k12 * k22 * k32 * t10 * t14 * 3.0 + k12 * k23 * k33 * t9 * t13 * 3.0 - k15 * k25 * k32 * t2 * t16 * 2.0 - k15 * k25 * k32 * t8 * t10 * 2.0 + k22 * k23 * k25 * t8 * t12 * 4.0 + k12 * k22 * k32 * t9 * t16 * 3.0 + k12 * k23 * k33 * t10 * t13 + k12 * k23 * k33 * t11 * t12 - k13 * k22 * k35 * t9 * t12 * 2.0 + k12 * k22 * k32 * t10 * t16 * 3.0 + k12 * k22 * k32 * t12 * t14 * 3.0 - k15 * k25 * k32 * t8 * t12 * 6.0 + k12 * k22 * k32 * t12 * t15 * 3.0 - k15 * k33 * k36 * t3 * t8 - k15 * k33 * k36 * t2 * t10 * 4.0 - k15 * k33 * k36 * t3 * t9 + k22 * k23 * k35 * t2 * t14 * 2.0 + k25 * k26 * k35 * t2 * t8 * 4.0 + k15 * k33 * k36 * t2 * t11 * 2.0 - k15 * k33 * k36 * t3 * t10 * 6.0 + k22 * k23 * k35 * t2 * t15 * 2.0 + k22 * k23 * k35 * t8 * t9 * 2.0 - k15 * k33 * k36 * t4 * t10 * 3.0 + k22 * k23 * k35 * t2 * t16 * 6.0 + k25 * k26 * k35 * t2 * t10 * 2.0 + k15 * k32 * k35 * t2 * t15 * 2.0 - k15 * k33 * k36 * t3 * t12 + k22 * k23 * k35 * t9 * t10 * 4.0 - k15 * k32 * k35 * t8 * t10 - k15 * k32 * k35 * t9 * t10 + k22 * k23 * k35 * t9 * t12 * 4.0 - k15 * k32 * k35 * t10 * t12 * 3.0 + k25 * k32 * k33 * t2 * t14 * 2.0 + k25 * k32 * k33 * t2 * t15 * 6.0 + k25 * k32 * k33 * t8 * t9 * 2.0 + k25 * k35 * k36 * t2 * t9 * 4.0 + k25 * k32 * k33 * t2 * t16 * 2.0 + k25 * k32 * k33 * t8 * t10 * 4.0 + k25 * k32 * k33 * t8 * t12 * 4.0 + k25 * k35 * k36 * t2 * t12 * 2.0 + k32 * k33 * k35 * t9 * t10 * 4.0 - k13 * k23 * t2 * t4 * t15 + k16 * k26 * t2 * t4 * t10 + k16 * k26 * t2 * t3 * t12 - k13 * k23 * t2 * t11 * t12 + k16 * k36 * t2 * t4 * t10 - k15 * k33 * t2 * t3 * t16 + k16 * k36 * t2 * t3 * t12 - k15 * k33 * t2 * t10 * t13 + k26 * k36 * t2 * t4 * t10 + k26 * k36 * t2 * t3 * t12 + k25 * k35 * t2 * t10 * t12 + k12 * k13 * k15 * k16 * k25 * k35 * 8.0; coeffs(6) += k12 * k13 * k16 * k25 * k26 * k32 * 8.0 - k13 * k15 * k16 * k22 * k25 * k33 * 2.0 - k13 * k15 * k16 * k22 * k26 * k32 * 2.0 - k13 * k15 * k16 * k23 * k25 * k32 * 2.0 - k12 * k13 * k15 * k25 * k26 * k35 * 1.6E1 - k12 * k15 * k16 * k22 * k25 * k36 * 4.0 - k12 * k15 * k16 * k22 * k26 * k35 * 4.0 - k12 * k15 * k16 * k23 * k25 * k35 * 4.0 - k12 * k13 * k16 * k25 * k32 * k36 * 4.0 - k12 * k13 * k16 * k25 * k33 * k35 * 4.0 - k12 * k13 * k16 * k26 * k32 * k35 * 4.0 - k12 * k16 * k22 * k25 * k26 * k33 * 2.0 - k12 * k16 * k23 * k25 * k26 * k32 * 2.0 - k13 * k15 * k16 * k22 * k32 * k36 * 2.0 - k13 * k15 * k16 * k22 * k33 * k35 * 2.0 - k13 * k15 * k16 * k23 * k32 * k35 * 2.0 - k13 * k15 * k22 * k25 * k26 * k33 * 2.0 - k13 * k15 * k23 * k25 * k26 * k32 * 2.0 + k15 * k16 * k22 * k23 * k25 * k33 * 8.0 + k15 * k16 * k22 * k23 * k26 * k32 * 8.0 - k12 * k13 * k15 * k25 * k35 * k36 * 1.6E1 + k12 * k15 * k16 * k22 * k35 * k36 * 8.0 + k12 * k15 * k22 * k25 * k26 * k36 * 1.2E1 + k12 * k15 * k23 * k25 * k26 * k35 * 1.2E1 + k13 * k16 * k22 * k26 * k32 * k33 * 4.0 + k13 * k16 * k23 * k25 * k32 * k33 * 4.0 + k12 * k13 * k25 * k26 * k32 * k36 * 8.0 + k12 * k13 * k25 * k26 * k33 * k35 * 8.0 + k12 * k16 * k22 * k25 * k33 * k36 * 6.0 + k12 * k16 * k22 * k26 * k32 * k36 * 6.0 + k12 * k16 * k22 * k26 * k33 * k35 * 6.0 + k12 * k16 * k23 * k25 * k32 * k36 * 6.0 + k12 * k16 * k23 * k25 * k33 * k35 * 6.0 + k12 * k16 * k23 * k26 * k32 * k35 * 6.0 - k13 * k15 * k22 * k25 * k33 * k36 * 1.0E1 - k13 * k15 * k22 * k26 * k32 * k36 * 1.0E1 - k13 * k15 * k22 * k26 * k33 * k35 * 1.0E1 - k13 * k15 * k23 * k25 * k32 * k36 * 1.0E1 - k13 * k15 * k23 * k25 * k33 * k35 * 1.0E1 - k13 * k15 * k23 * k26 * k32 * k35 * 1.0E1 + k15 * k16 * k22 * k23 * k32 * k36 * 4.0 + k15 * k16 * k22 * k23 * k33 * k35 * 4.0 - k15 * k22 * k23 * k25 * k26 * k33 * 4.0 + k12 * k15 * k22 * k26 * k35 * k36 * 8.0 + k12 * k15 * k23 * k25 * k35 * k36 * 8.0 + k13 * k16 * k22 * k32 * k33 * k36 * 8.0 + k13 * k16 * k23 * k32 * k33 * k35 * 8.0 + k13 * k23 * k25 * k26 * k32 * k33 * 4.0 - k16 * k22 * k23 * k26 * k32 * k33 * 1.6E1 + k12 * k13 * k25 * k33 * k35 * k36 * 1.2E1 + k12 * k13 * k26 * k32 * k35 * k36 * 1.2E1 - k12 * k16 * k22 * k33 * k35 * k36 * 2.0 - k12 * k16 * k23 * k32 * k35 * k36 * 2.0 - k12 * k22 * k25 * k26 * k33 * k36 * 1.0E1 - k12 * k23 * k25 * k26 * k32 * k36 * 1.0E1 - k12 * k23 * k25 * k26 * k33 * k35 * 1.0E1 - k13 * k15 * k22 * k33 * k35 * k36 * 2.0 - k13 * k15 * k23 * k32 * k35 * k36 * 2.0 + k15 * k22 * k23 * k25 * k33 * k36 * 8.0 + k15 * k22 * k23 * k26 * k32 * k36 * 8.0 + k15 * k22 * k23 * k26 * k33 * k35 * 8.0 + k13 * k22 * k26 * k32 * k33 * k36 * 8.0 + k13 * k23 * k25 * k32 * k33 * k36 * 8.0 + k13 * k23 * k26 * k32 * k33 * k35 * 8.0 - k16 * k22 * k23 * k32 * k33 * k36 * 1.6E1 - k12 * k22 * k26 * k33 * k35 * k36 * 1.0E1 - k12 * k23 * k25 * k33 * k35 * k36 * 1.0E1 - k12 * k23 * k26 * k32 * k35 * k36 * 1.0E1 + k15 * k22 * k23 * k33 * k35 * k36 * 4.0 - k13 * k23 * k32 * k33 * k35 * k36 * 4.0 + k12 * k15 * k16 * k23 * k26 * t4 * 2.0 + k12 * k15 * k16 * k22 * k25 * t12 * 2.0 - k12 * k13 * k16 * k26 * k33 * t3 * 4.0 + k13 * k15 * k16 * k23 * k33 * t3 * 6.0 + k13 * k15 * k16 * k23 * k33 * t4 * 6.0 + k12 * k13 * k15 * k26 * k36 * t3 * 4.0 + k12 * k13 * k15 * k26 * k36 * t4 * 4.0 - k12 * k15 * k16 * k23 * k36 * t4 * 4.0 - k12 * k13 * k16 * k25 * k32 * t10 * 4.0 + k13 * k15 * k16 * k22 * k32 * t10 * 6.0 + k12 * k13 * k15 * k25 * k35 * t10 * 4.0 + k13 * k15 * k16 * k22 * k32 * t12 * 6.0 + k12 * k13 * k15 * k25 * k35 * t12 * 4.0 - k12 * k15 * k16 * k22 * k35 * t12 * 4.0 - k12 * k15 * k22 * k25 * k26 * t12 * 2.0 + k12 * k16 * k23 * k26 * k33 * t2 + k13 * k15 * k23 * k26 * k33 * t2 * 3.0 + k12 * k13 * k16 * k33 * k36 * t3 * 2.0 + k12 * k16 * k23 * k26 * k33 * t3 * 3.0 + k13 * k15 * k23 * k26 * k33 * t3 * 3.0 - k12 * k15 * k23 * k26 * k36 * t2 * 4.0 - k12 * k16 * k23 * k26 * k33 * t4 * 6.0 + k13 * k15 * k23 * k26 * k33 * t4 * 6.0 - k13 * k16 * k22 * k25 * k36 * t2 * 2.0 - k13 * k16 * k22 * k26 * k35 * t2 * 2.0 - k13 * k16 * k23 * k25 * k35 * t2 * 2.0 - k12 * k15 * k23 * k26 * k36 * t3 * 6.0 - k12 * k13 * k22 * k23 * k32 * t14 * 2.0 - k12 * k13 * k25 * k26 * k32 * t8 * 4.0 + k12 * k16 * k22 * k25 * k33 * t8 * 3.0 + k12 * k16 * k22 * k26 * k32 * t8 * 3.0 + k12 * k16 * k23 * k25 * k32 * t8 * 3.0 + k13 * k15 * k22 * k25 * k33 * t8 + k13 * k15 * k22 * k26 * k32 * t8 + k13 * k15 * k23 * k25 * k32 * t8 - k15 * k16 * k22 * k23 * k32 * t8 * 2.0 - k12 * k13 * k22 * k23 * k32 * t15 * 2.0 + k12 * k13 * k16 * k32 * k35 * t10 * 2.0 - k12 * k13 * k22 * k23 * k32 * t16 * 2.0 - k12 * k13 * k25 * k26 * k32 * t10 * 2.0 - k12 * k15 * k22 * k25 * k36 * t8 * 2.0 - k12 * k15 * k22 * k26 * k35 * t8 * 2.0 - k12 * k15 * k23 * k25 * k35 * t8 * 2.0 + k12 * k16 * k22 * k25 * k33 * t10 * 3.0 + k12 * k16 * k22 * k26 * k32 * t10 * 3.0 + k12 * k16 * k23 * k25 * k32 * t10 + k13 * k15 * k22 * k25 * k33 * t10 * 3.0 + k13 * k15 * k22 * k26 * k32 * t10 * 3.0 + k13 * k15 * k23 * k25 * k32 * t10 - k15 * k16 * k22 * k23 * k32 * t10 * 4.0 - k12 * k15 * k22 * k25 * k36 * t10 * 6.0 - k12 * k15 * k22 * k26 * k35 * t10 * 6.0 - k12 * k15 * k23 * k25 * k35 * t10 * 2.0 - k12 * k16 * k22 * k25 * k33 * t12 * 2.0 - k12 * k16 * k22 * k26 * k32 * t12 * 6.0 - k12 * k16 * k23 * k25 * k32 * t12 * 6.0 + k13 * k15 * k22 * k25 * k33 * t12 * 2.0 + k13 * k15 * k22 * k26 * k32 * t12 * 6.0 + k13 * k15 * k23 * k25 * k32 * t12 * 6.0 - k15 * k16 * k22 * k23 * k32 * t12 * 1.2E1 - k12 * k13 * k26 * k33 * k36 * t2 * 4.0 + k12 * k16 * k23 * k33 * k36 * t2 + k13 * k15 * k23 * k33 * k36 * t2 * 3.0 - k12 * k16 * k23 * k33 * k36 * t3 * 6.0 + k13 * k15 * k23 * k33 * k36 * t3 * 6.0 - k12 * k13 * k26 * k33 * k36 * t4 * 6.0 + k12 * k16 * k23 * k33 * k36 * t4 * 3.0 + k13 * k15 * k23 * k33 * k36 * t4 * 3.0 - k13 * k22 * k25 * k26 * k36 * t2 * 4.0 - k13 * k23 * k25 * k26 * k35 * t2 * 4.0 - k13 * k16 * k22 * k32 * k33 * t9 * 2.0 + k12 * k22 * k25 * k26 * k33 * t8 * 6.0 + k12 * k23 * k25 * k26 * k32 * t8 * 6.0 - k13 * k16 * k22 * k32 * k33 * t10 * 1.2E1 - k15 * k16 * k25 * k32 * k36 * t2 * 2.0 - k15 * k16 * k25 * k33 * k35 * t2 * 2.0 - k15 * k16 * k26 * k32 * k35 * t2 * 2.0 - k15 * k22 * k23 * k25 * k33 * t8 * 4.0 - k15 * k22 * k23 * k26 * k32 * t8 * 4.0 - k12 * k13 * k25 * k32 * k36 * t9 * 2.0 - k12 * k13 * k25 * k33 * k35 * t9 * 2.0 - k12 * k13 * k26 * k32 * k35 * t9 * 2.0; coeffs(6) += k12 * k16 * k22 * k32 * k36 * t9 * 3.0 + k12 * k16 * k22 * k33 * k35 * t9 * 3.0 + k12 * k16 * k23 * k32 * k35 * t9 * 3.0 + k13 * k15 * k22 * k32 * k36 * t9 + k13 * k15 * k22 * k33 * k35 * t9 + k13 * k15 * k23 * k32 * k35 * t9 - k12 * k15 * k22 * k32 * k33 * t14 * 2.0 - k12 * k16 * k22 * k32 * k36 * t10 * 6.0 - k12 * k16 * k22 * k33 * k35 * t10 * 6.0 - k12 * k16 * k23 * k32 * k35 * t10 * 2.0 + k13 * k15 * k22 * k32 * k36 * t10 * 6.0 + k13 * k15 * k22 * k33 * k35 * t10 * 6.0 + k13 * k15 * k23 * k32 * k35 * t10 * 2.0 - k13 * k16 * k22 * k32 * k33 * t12 * 4.0 - k12 * k15 * k22 * k32 * k33 * t15 * 2.0 - k12 * k15 * k22 * k35 * k36 * t9 * 4.0 - k12 * k13 * k25 * k32 * k36 * t12 * 6.0 - k12 * k13 * k25 * k33 * k35 * t12 * 2.0 - k12 * k13 * k26 * k32 * k35 * t12 * 6.0 - k12 * k15 * k22 * k32 * k33 * t16 * 2.0 + k12 * k16 * k22 * k32 * k36 * t12 * 3.0 + k12 * k16 * k22 * k33 * k35 * t12 + k12 * k16 * k23 * k32 * k35 * t12 * 3.0 + k12 * k22 * k25 * k26 * k33 * t12 * 2.0 + k12 * k23 * k25 * k26 * k32 * t12 * 6.0 + k13 * k15 * k22 * k32 * k36 * t12 * 3.0 + k13 * k15 * k22 * k33 * k35 * t12 + k13 * k15 * k23 * k32 * k35 * t12 * 3.0 - k12 * k15 * k22 * k35 * k36 * t12 * 2.0 + k12 * k23 * k26 * k33 * k36 * t2 * 4.0 + k12 * k23 * k26 * k33 * k36 * t3 * 3.0 + k12 * k23 * k26 * k33 * k36 * t4 * 3.0 - k13 * k22 * k26 * k32 * k33 * t8 * 2.0 + k13 * k22 * k26 * k35 * k36 * t2 * 4.0 - k13 * k23 * k25 * k32 * k33 * t8 * 2.0 + k13 * k23 * k25 * k35 * k36 * t2 * 4.0 + k16 * k22 * k23 * k32 * k33 * t8 * 4.0 - k16 * k22 * k23 * k35 * k36 * t2 * 8.0 - k16 * k25 * k26 * k32 * k33 * t2 * 8.0 + k22 * k23 * k25 * k26 * k36 * t2 * 4.0 - k13 * k22 * k26 * k32 * k33 * t9 * 2.0 - k13 * k23 * k25 * k32 * k33 * t9 * 2.0 + k16 * k22 * k23 * k32 * k33 * t9 * 4.0 - k12 * k22 * k25 * k33 * k36 * t8 - k12 * k22 * k26 * k32 * k36 * t8 - k12 * k22 * k26 * k33 * k35 * t8 - k12 * k23 * k25 * k32 * k36 * t8 - k12 * k23 * k25 * k33 * k35 * t8 - k12 * k23 * k26 * k32 * k35 * t8 - k13 * k22 * k26 * k32 * k33 * t10 * 6.0 - k13 * k23 * k25 * k32 * k33 * t10 * 2.0 - k15 * k22 * k23 * k32 * k36 * t8 * 2.0 - k15 * k22 * k23 * k33 * k35 * t8 * 2.0 + k15 * k25 * k26 * k32 * k36 * t2 * 4.0 + k15 * k25 * k26 * k33 * k35 * t2 * 4.0 + k16 * k22 * k23 * k32 * k33 * t10 * 8.0 - k12 * k22 * k25 * k33 * k36 * t9 - k12 * k22 * k26 * k32 * k36 * t9 - k12 * k22 * k26 * k33 * k35 * t9 - k12 * k23 * k25 * k32 * k36 * t9 - k12 * k23 * k25 * k33 * k35 * t9 - k12 * k23 * k26 * k32 * k35 * t9 - k15 * k22 * k23 * k32 * k36 * t9 * 2.0 - k15 * k22 * k23 * k33 * k35 * t9 * 2.0 - k12 * k13 * k32 * k35 * k36 * t10 * 2.0 + k12 * k22 * k25 * k33 * k36 * t10 * 3.0 + k12 * k22 * k26 * k32 * k36 * t10 * 3.0 + k12 * k22 * k26 * k33 * k35 * t10 * 3.0 + k12 * k23 * k25 * k32 * k36 * t10 + k12 * k23 * k25 * k33 * k35 * t10 + k12 * k23 * k26 * k32 * k35 * t10 - k13 * k22 * k26 * k32 * k33 * t12 * 4.0 - k13 * k23 * k25 * k32 * k33 * t12 * 4.0 - k15 * k22 * k23 * k32 * k36 * t10 * 4.0 - k15 * k22 * k23 * k33 * k35 * t10 * 4.0 + k16 * k22 * k23 * k32 * k33 * t12 * 8.0 + k12 * k22 * k25 * k33 * k36 * t12 + k12 * k22 * k26 * k32 * k36 * t12 * 3.0 + k12 * k22 * k26 * k33 * k35 * t12 + k12 * k23 * k25 * k32 * k36 * t12 * 3.0 + k12 * k23 * k25 * k33 * k35 * t12 + k12 * k23 * k26 * k32 * k35 * t12 * 3.0 - k15 * k22 * k23 * k32 * k36 * t12 * 6.0 - k15 * k22 * k23 * k33 * k35 * t12 * 2.0 + k22 * k23 * k26 * k32 * k33 * t8 * 8.0 - k13 * k22 * k32 * k33 * k36 * t9 * 4.0 - k13 * k23 * k32 * k33 * k35 * t9 * 4.0 - k15 * k25 * k33 * k35 * k36 * t2 * 4.0 - k15 * k26 * k32 * k35 * k36 * t2 * 4.0 + k12 * k22 * k33 * k35 * k36 * t9 * 6.0 + k12 * k23 * k32 * k35 * k36 * t9 * 6.0 + k12 * k22 * k33 * k35 * k36 * t10 * 6.0 + k12 * k23 * k32 * k35 * k36 * t10 * 2.0 + k22 * k23 * k32 * k33 * k36 * t9 * 8.0 + k26 * k32 * k33 * k35 * k36 * t2 * 4.0 + k12 * k13 * k15 * k16 * t4 * t10 + k12 * k13 * k15 * k16 * t3 * t12 + k12 * k13 * k15 * k26 * t4 * t10 - k12 * k15 * k16 * k23 * t4 * t10 + k12 * k13 * k15 * k26 * t3 * t12 - k12 * k15 * k16 * k23 * t3 * t12 * 3.0 - k13 * k16 * k23 * k26 * t2 * t4 - k12 * k13 * k16 * k33 * t4 * t10 * 3.0 - k12 * k15 * k23 * k26 * t4 * t8 - k12 * k13 * k16 * k33 * t3 * t12 + k12 * k13 * k15 * k36 * t4 * t10 - k13 * k16 * k22 * k25 * t2 * t12 + k12 * k13 * k15 * k36 * t3 * t12 - k12 * k15 * k23 * k26 * t2 * t13 * 2.0 - k12 * k15 * k22 * k25 * t2 * t16 * 2.0 - k12 * k15 * k22 * k25 * t8 * t12 - k12 * k13 * k26 * k33 * t4 * t10 * 3.0 + k12 * k16 * k23 * k33 * t4 * t10 * 3.0 - k12 * k13 * k26 * k33 * t3 * t12 + k12 * k16 * k23 * k33 * t3 * t12 * 3.0 - k12 * k15 * k23 * k36 * t4 * t10 - k13 * k22 * k25 * k26 * t2 * t12 * 2.0 + k16 * k22 * k23 * k25 * t2 * t12 * 2.0 - k12 * k15 * k23 * k36 * t3 * t12 * 3.0 - k12 * k13 * k25 * k32 * t10 * t12 * 3.0 + k12 * k16 * k22 * k32 * t10 * t12 * 9.0 - k13 * k23 * k26 * k36 * t2 * t4 * 2.0 - k15 * k16 * k33 * k36 * t2 * t3 - k12 * k13 * k33 * k36 * t3 * t9 - k12 * k15 * k22 * k35 * t10 * t12 * 3.0 + k12 * k23 * k26 * k33 * t4 * t8 * 3.0 - k12 * k13 * k33 * k36 * t2 * t11 * 2.0 - k12 * k13 * k32 * k35 * t2 * t15 * 2.0 + k12 * k23 * k26 * k33 * t2 * t13 + k12 * k22 * k25 * k33 * t2 * t16 + k12 * k22 * k26 * k32 * t2 * t16 + k12 * k23 * k25 * k32 * t2 * t16 - k13 * k22 * k25 * k36 * t2 * t12 * 2.0 - k13 * k22 * k26 * k35 * t2 * t12 * 2.0 - k13 * k23 * k25 * k35 * t2 * t12 * 2.0 - k15 * k16 * k32 * k35 * t2 * t10 - k15 * k22 * k23 * k32 * t2 * t16 * 2.0 + k16 * k22 * k23 * k35 * t2 * t12 * 2.0 - k12 * k13 * k32 * k35 * t9 * t10 + k12 * k22 * k25 * k33 * t8 * t12 + k12 * k22 * k26 * k32 * t8 * t12 * 3.0 + k12 * k23 * k25 * k32 * t8 * t12 * 3.0 - k15 * k26 * k33 * k36 * t2 * t3 * 2.0 + k12 * k23 * k33 * k36 * t3 * t9 * 3.0 + k12 * k23 * k33 * k36 * t2 * t11 - k13 * k22 * k32 * k33 * t2 * t15 * 2.0 + k16 * k25 * k32 * k33 * t2 * t10 * 2.0 + k12 * k22 * k32 * k36 * t2 * t15 + k12 * k22 * k33 * k35 * t2 * t15 + k12 * k23 * k32 * k35 * t2 * t15 - k15 * k25 * k32 * k36 * t2 * t10 * 2.0 - k15 * k25 * k33 * k35 * t2 * t10 * 2.0 - k15 * k26 * k32 * k35 * t2 * t10 * 2.0 + k22 * k23 * k25 * k36 * t2 * t12 * 2.0 + k22 * k23 * k26 * k35 * t2 * t12 * 2.0 + k12 * k22 * k32 * k36 * t9 * t10 * 3.0 + k12 * k22 * k33 * k35 * t9 * t10 * 3.0 + k12 * k23 * k32 * k35 * t9 * t10 + k25 * k26 * k32 * k33 * t2 * t8 * 4.0 + k22 * k23 * k35 * k36 * t2 * t9 * 4.0 + k16 * k32 * k33 * k35 * t2 * t10 * 2.0 - k15 * k32 * k35 * k36 * t2 * t10 * 2.0 + k25 * k32 * k33 * k36 * t2 * t10 * 2.0 + k26 * k32 * k33 * k35 * t2 * t10 * 2.0 - k12 * k13 * k16 * k22 * k23 * k25 * k33 * 8.0 - k12 * k13 * k16 * k22 * k23 * k26 * k32 * 8.0 + k12 * k13 * k15 * k22 * k23 * k25 * k36 * 8.0 + k12 * k13 * k15 * k22 * k23 * k26 * k35 * 8.0 + k12 * k13 * k16 * k22 * k23 * k32 * k36 * 4.0 + k12 * k13 * k16 * k22 * k23 * k33 * k35 * 4.0 - k12 * k13 * k22 * k23 * k25 * k26 * k33 * 4.0 + k12 * k15 * k16 * k22 * k26 * k32 * k33 * 4.0 + k12 * k15 * k16 * k23 * k25 * k32 * k33 * 4.0 + k12 * k13 * k15 * k25 * k32 * k33 * k36 * 8.0 + k12 * k13 * k15 * k26 * k32 * k33 * k35 * 8.0 - k12 * k15 * k16 * k22 * k32 * k33 * k36 * 8.0 - k12 * k15 * k16 * k23 * k32 * k33 * k35 * 8.0 - k12 * k15 * k23 * k25 * k26 * k32 * k33 * 4.0 - k12 * k13 * k22 * k23 * k33 * k35 * k36 * 4.0 - k12 * k15 * k23 * k32 * k33 * k35 * k36 * 4.0 + k12 * k13 * k15 * k22 * k23 * k25 * t12 * 2.0 - k12 * k13 * k16 * k22 * k23 * k32 * t12 * 6.0 + k12 * k13 * k15 * k22 * k23 * k35 * t12 * 2.0 + k12 * k13 * k15 * k25 * k32 * k33 * t10 * 2.0 - k12 * k13 * k22 * k23 * k25 * k33 * t12 * 2.0 - k12 * k13 * k22 * k23 * k26 * k32 * t12 * 6.0 - k12 * k15 * k16 * k22 * k32 * k33 * t10 * 6.0 - k13 * k16 * k22 * k26 * k32 * k33 * t2 * 2.0 - k13 * k16 * k23 * k25 * k32 * k33 * t2 * 2.0 - k12 * k13 * k25 * k26 * k32 * k36 * t2 * 4.0 - k12 * k13 * k25 * k26 * k33 * k35 * t2 * 4.0 + k12 * k16 * k22 * k25 * k33 * k36 * t2 + k12 * k16 * k22 * k26 * k32 * k36 * t2 + k12 * k16 * k22 * k26 * k33 * k35 * t2 + k12 * k16 * k23 * k25 * k32 * k36 * t2 + k12 * k16 * k23 * k25 * k33 * k35 * t2 + k12 * k16 * k23 * k26 * k32 * k35 * t2 + k13 * k15 * k22 * k25 * k33 * k36 * t2 * 3.0 + k13 * k15 * k22 * k26 * k32 * k36 * t2 * 3.0 + k13 * k15 * k22 * k26 * k33 * k35 * t2 * 3.0 + k13 * k15 * k23 * k25 * k32 * k36 * t2 * 3.0 + k13 * k15 * k23 * k25 * k33 * k35 * t2 * 3.0 + k13 * k15 * k23 * k26 * k32 * k35 * t2 * 3.0 - k15 * k16 * k22 * k23 * k32 * k36 * t2 * 2.0 - k15 * k16 * k22 * k23 * k33 * k35 * t2 * 2.0 - k12 * k13 * k22 * k23 * k32 * k36 * t9 * 2.0 - k12 * k13 * k22 * k23 * k33 * k35 * t9 * 2.0 - k12 * k15 * k22 * k26 * k32 * k33 * t8 * 2.0 - k12 * k15 * k22 * k26 * k35 * k36 * t2 * 4.0 - k12 * k15 * k23 * k25 * k32 * k33 * t8 * 2.0 - k12 * k15 * k23 * k25 * k35 * k36 * t2 * 4.0 + k12 * k13 * k15 * k32 * k33 * k35 * t10 * 2.0 - k13 * k23 * k25 * k26 * k32 * k33 * t2 * 4.0 + k16 * k22 * k23 * k26 * k32 * k33 * t2 * 4.0 + k12 * k22 * k25 * k26 * k33 * k36 * t2 * 2.0 + k12 * k23 * k25 * k26 * k32 * k36 * t2 * 2.0 + k12 * k23 * k25 * k26 * k33 * k35 * t2 * 2.0 - k15 * k22 * k23 * k25 * k33 * k36 * t2 * 4.0 - k15 * k22 * k23 * k26 * k32 * k36 * t2 * 4.0 - k15 * k22 * k23 * k26 * k33 * k35 * t2 * 4.0 - k12 * k15 * k22 * k32 * k33 * k36 * t10 * 6.0 - k12 * k15 * k23 * k32 * k33 * k35 * t10 * 2.0 - k13 * k22 * k26 * k32 * k33 * k36 * t2 * 4.0 - k13 * k23 * k25 * k32 * k33 * k36 * t2 * 4.0 - k13 * k23 * k26 * k32 * k33 * k35 * t2 * 4.0 + k16 * k22 * k23 * k32 * k33 * k36 * t2 * 4.0 + k12 * k22 * k26 * k33 * k35 * k36 * t2 * 2.0 + k12 * k23 * k25 * k33 * k35 * k36 * t2 * 2.0 + k12 * k23 * k26 * k32 * k35 * k36 * t2 * 2.0 - k15 * k22 * k23 * k33 * k35 * k36 * t2 * 4.0 + k22 * k23 * k26 * k32 * k33 * k36 * t2 * 4.0 + k12 * k13 * k15 * k16 * k22 * k23 * k32 * k33 * 4.0 + k12 * k13 * k15 * k22 * k23 * k26 * k32 * k33 * 4.0 + k12 * k13 * k15 * k22 * k23 * k32 * k33 * k36 * 4.0; coeffs(7) = k16 * k25 * t14 * -4.0 - k16 * k25 * t15 * 1.2E1 - k16 * k25 * t16 * 1.2E1 - k16 * k25 * t19 * 2.0 - k16 * k35 * t14 * 4.0 + k25 * k26 * t14 * 1.2E1 - k16 * k35 * t15 * 1.2E1 + k25 * k26 * t15 * 4.0 - k16 * k35 * t16 * 1.2E1 + k25 * k26 * t16 * 1.2E1 + k25 * k26 * t17 * 2.0 + k25 * k26 * t19 * 2.0 - k16 * k35 * t20 * 2.0 + k25 * k36 * t14 * 1.2E1 + k26 * k35 * t14 * 1.2E1 + k25 * k36 * t15 * 1.2E1 + k26 * k35 * t15 * 4.0 + k25 * k36 * t16 * 4.0 + k26 * k35 * t16 * 1.2E1 + k35 * k36 * t14 * 1.2E1 + k35 * k36 * t15 * 1.2E1 + k35 * k36 * t16 * 4.0 + k35 * k36 * t18 * 2.0 + k35 * k36 * t20 * 2.0 + k25 * t8 * t14 * 2.0 + k25 * t8 * t15 * 6.0 + k25 * t8 * t16 * 2.0 - k25 * t10 * t14 * 4.0 + k25 * t9 * t16 * 4.0 - k25 * t10 * t16 * 4.0 - k25 * t12 * t14 * 6.0 + k25 * t8 * t19 - k25 * t12 * t15 * 6.0 - k25 * t12 * t16 * 2.0 + k35 * t8 * t15 * 4.0 + k35 * t9 * t14 * 2.0 + k35 * t9 * t15 * 2.0 - k35 * t10 * t14 * 6.0 + k35 * t9 * t16 * 6.0 - k35 * t10 * t15 * 2.0 - k35 * t10 * t16 * 6.0 - k35 * t12 * t14 * 4.0 - k35 * t12 * t15 * 4.0 + k35 * t9 * t20 - k16 * k25 * k26 * k36 * 2.4E1 - k16 * k26 * k35 * k36 * 2.4E1 - k13 * k16 * k22 * t14 * 2.0 + k13 * k16 * k22 * t15 * 2.0 - k13 * k16 * k22 * t16 * 6.0 - k13 * k16 * k22 * t19 + k13 * k22 * k26 * t14 * 2.0 + k13 * k23 * k25 * t14 * 2.0 + k16 * k22 * k23 * t14 * 4.0 - k16 * k25 * k26 * t8 * 8.0 - k13 * k22 * k26 * t15 * 2.0 - k13 * k23 * k25 * t15 * 6.0 + k16 * k22 * k23 * t15 * 4.0 + k13 * k22 * k26 * t16 * 2.0 + k13 * k23 * k25 * t16 * 2.0 - k15 * k16 * k32 * t14 * 2.0 + k16 * k22 * k23 * t16 * 1.2E1 + k16 * k25 * k26 * t10 * 4.0 - k15 * k16 * k32 * t15 * 6.0 + k15 * k16 * k32 * t16 * 2.0 + k16 * k25 * k26 * t12 * 1.2E1 - k13 * k22 * k26 * t19 - k13 * k23 * k25 * t19 + k16 * k22 * k23 * t19 * 2.0 - k15 * k16 * k32 * t20 + k13 * k22 * k36 * t14 * 6.0 + k13 * k23 * k35 * t14 * 6.0 - k16 * k25 * k36 * t8 * 4.0 - k16 * k26 * k35 * t8 * 4.0 - k22 * k23 * k26 * t14 * 8.0 - k13 * k22 * k36 * t15 * 2.0 - k13 * k23 * k35 * t15 * 2.0 - k16 * k25 * k36 * t9 * 4.0 - k16 * k26 * k35 * t9 * 4.0 + k13 * k22 * k36 * t16 * 2.0 + k13 * k23 * k35 * t16 * 6.0 + k15 * k25 * k33 * t14 * 6.0 + k15 * k26 * k32 * t14 * 6.0 + k16 * k25 * k36 * t10 * 8.0 + k16 * k26 * k35 * t10 * 8.0 - k22 * k23 * k26 * t16 * 8.0 + k15 * k25 * k33 * t15 * 6.0 + k15 * k26 * k32 * t15 * 2.0 - k15 * k25 * k33 * t16 * 2.0 - k15 * k26 * k32 * t16 * 2.0 + k16 * k25 * k36 * t12 * 8.0 + k16 * k26 * k35 * t12 * 8.0 + k16 * k32 * k33 * t14 * 4.0 - k22 * k23 * k36 * t14 * 1.2E1 + k25 * k26 * k36 * t8 * 8.0 + k16 * k32 * k33 * t15 * 1.2E1 - k16 * k35 * k36 * t9 * 8.0 - k22 * k23 * k36 * t15 * 4.0 + k25 * k26 * k36 * t9 * 4.0 + k15 * k32 * k36 * t14 * 2.0 + k15 * k33 * k35 * t14 * 2.0 + k16 * k32 * k33 * t16 * 4.0 + k16 * k35 * k36 * t10 * 1.2E1 - k22 * k23 * k36 * t16 * 4.0 - k25 * k26 * k36 * t10 * 4.0 + k15 * k32 * k36 * t15 * 2.0 + k15 * k33 * k35 * t15 * 2.0 - k15 * k32 * k36 * t16 * 2.0 - k15 * k33 * k35 * t16 * 6.0 + k16 * k35 * k36 * t12 * 4.0 - k25 * k26 * k36 * t12 * 8.0 + k16 * k32 * k33 * t20 * 2.0 - k15 * k32 * k36 * t20 - k15 * k33 * k35 * t20 - k26 * k32 * k33 * t14 * 1.2E1 + k26 * k35 * k36 * t8 * 4.0 - k26 * k32 * k33 * t15 * 4.0 + k26 * k35 * k36 * t9 * 8.0 - k26 * k32 * k33 * t16 * 4.0 - k26 * k35 * k36 * t10 * 8.0 - k26 * k35 * k36 * t12 * 4.0 - k32 * k33 * k36 * t14 * 8.0 - k32 * k33 * k36 * t15 * 8.0 - k13 * k22 * t8 * t15 * 2.0 - k13 * k22 * t10 * t14 * 6.0 + k16 * k25 * t2 * t16 * 4.0 + k16 * k25 * t8 * t10 * 4.0 + k13 * k22 * t9 * t16 * 2.0 - k13 * k22 * t10 * t16 * 6.0 - k13 * k22 * t12 * t14 * 3.0 + k16 * k25 * t8 * t12 * 2.0 + k13 * k22 * t12 * t15 - k16 * k25 * t9 * t12 * 2.0 - k13 * k22 * t12 * t16 - k16 * k25 * t10 * t12 * 4.0 + k22 * k23 * t8 * t14 * 2.0 + k16 * k35 * t2 * t15 * 4.0 + k22 * k23 * t8 * t15 * 2.0 - k16 * k35 * t8 * t10 * 2.0 + k22 * k23 * t8 * t16 * 2.0 + k22 * k23 * t10 * t14 * 4.0 - k25 * k26 * t2 * t16 * 8.0 + k25 * k26 * t8 * t10 * 2.0 + k15 * k32 * t8 * t15 * 2.0 + k16 * k35 * t9 * t10 * 2.0 - k22 * k23 * t9 * t16 * 4.0 - k15 * k32 * t10 * t14 * 3.0 + k22 * k23 * t10 * t16 * 4.0 + k22 * k23 * t12 * t14 * 6.0 - k25 * k26 * t8 * t12 * 4.0 - k15 * k32 * t9 * t16 * 2.0 - k15 * k32 * t10 * t15 + k16 * k35 * t9 * t12 * 4.0 + k22 * k23 * t12 * t15 * 2.0 + k25 * k26 * t5 * t16 * 2.0 + k25 * k26 * t9 * t12 * 2.0; coeffs(7) += k15 * k32 * t10 * t16 - k15 * k32 * t12 * t14 * 6.0 - k16 * k35 * t10 * t12 * 4.0 + k22 * k23 * t12 * t16 * 2.0 + k25 * k26 * t10 * t12 * 2.0 - k15 * k32 * t12 * t15 * 6.0 - k25 * k36 * t2 * t14 * 2.0 - k26 * k35 * t2 * t14 * 2.0 - k25 * k36 * t2 * t15 * 6.0 - k25 * k36 * t8 * t9 * 2.0 - k26 * k35 * t2 * t15 * 2.0 - k26 * k35 * t8 * t9 * 2.0 - k25 * k36 * t2 * t16 * 2.0 - k25 * k36 * t8 * t10 * 4.0 - k26 * k35 * t2 * t16 * 6.0 - k26 * k35 * t8 * t10 * 4.0 - k25 * k36 * t9 * t10 * 4.0 - k26 * k35 * t9 * t10 * 4.0 - k25 * k36 * t8 * t12 * 4.0 - k26 * k35 * t8 * t12 * 4.0 - k25 * k36 * t9 * t12 * 4.0 - k26 * k35 * t9 * t12 * 4.0 - k32 * k33 * t8 * t15 * 4.0 + k32 * k33 * t9 * t14 * 2.0 - k35 * k36 * t2 * t15 * 8.0 + k32 * k33 * t9 * t15 * 2.0 + k32 * k33 * t10 * t14 * 6.0 + k35 * k36 * t8 * t10 * 2.0 + k32 * k33 * t9 * t16 * 2.0 + k32 * k33 * t10 * t15 * 2.0 - k35 * k36 * t9 * t10 * 4.0 + k32 * k33 * t10 * t16 * 2.0 + k32 * k33 * t12 * t14 * 4.0 + k35 * k36 * t5 * t15 * 2.0 + k32 * k33 * t12 * t15 * 4.0 + k35 * k36 * t9 * t12 * 2.0 + k35 * k36 * t10 * t12 * 2.0 + k25 * t2 * t9 * t16 * 2.0 + k25 * t2 * t12 * t14 + k25 * t2 * t12 * t15 * 3.0 + k25 * t8 * t9 * t12 + k25 * t2 * t12 * t16 + k25 * t8 * t10 * t12 * 2.0 + k35 * t2 * t8 * t15 * 2.0 + k35 * t2 * t10 * t14 + k35 * t2 * t10 * t15 + k35 * t8 * t9 * t10 + k35 * t2 * t10 * t16 * 3.0 + k35 * t9 * t10 * t12 * 2.0 + k13 * k16 * k23 * k25 * k26 * 4.0 - k13 * k16 * k22 * k26 * k36 * 4.0 - k13 * k16 * k23 * k25 * k36 * 4.0 - k13 * k16 * k23 * k26 * k35 * 4.0 - k15 * k16 * k25 * k26 * k33 * 1.2E1 - k13 * k16 * k23 * k35 * k36 * 1.2E1 - k13 * k23 * k25 * k26 * k36 * 4.0 + k16 * k22 * k23 * k26 * k36 * 1.6E1 - k15 * k16 * k25 * k33 * k36 * 4.0 - k15 * k16 * k26 * k32 * k36 * 4.0 - k15 * k16 * k26 * k33 * k35 * 4.0 + k13 * k23 * k26 * k35 * k36 * 4.0 + k15 * k16 * k33 * k35 * k36 * 4.0 + k15 * k25 * k26 * k33 * k36 * 4.0 + k16 * k26 * k32 * k33 * k36 * 1.6E1 - k15 * k26 * k33 * k35 * k36 * 4.0 - k12 * k13 * k15 * k25 * t16 * 8.0 + k12 * k15 * k16 * k22 * t16 * 4.0 - k13 * k16 * k22 * k26 * t8 * 2.0 - k13 * k16 * k23 * k25 * t8 * 2.0 - k13 * k16 * k22 * k26 * t10 * 6.0 - k13 * k16 * k23 * k25 * t10 * 2.0 + k12 * k13 * k16 * k32 * t15 * 4.0 + k13 * k16 * k22 * k26 * t12 * 2.0 + k13 * k16 * k23 * k25 * t12 * 2.0 - k12 * k13 * k15 * k35 * t15 * 8.0 + k12 * k15 * k22 * k26 * t16 * 4.0 + k12 * k15 * k23 * k25 * t16 * 4.0 - k13 * k23 * k25 * k26 * t8 * 4.0 + k16 * k22 * k23 * k26 * t8 * 8.0 - k13 * k16 * k22 * k36 * t9 * 2.0 - k13 * k16 * k23 * k35 * t9 * 2.0 - k12 * k13 * k25 * k33 * t14 * 2.0 - k12 * k13 * k26 * k32 * t14 * 2.0 + k12 * k16 * k22 * k33 * t14 + k12 * k16 * k23 * k32 * t14 + k13 * k15 * k22 * k33 * t14 * 3.0 + k13 * k15 * k23 * k32 * t14 * 3.0 + k13 * k16 * k22 * k36 * t10 * 1.2E1 + k13 * k16 * k23 * k35 * t10 * 4.0 - k15 * k16 * k25 * k33 * t8 * 2.0 - k15 * k16 * k26 * k32 * t8 * 2.0 - k12 * k13 * k25 * k33 * t15 * 6.0 - k12 * k13 * k26 * k32 * t15 * 2.0 - k12 * k16 * k22 * k33 * t15 - k12 * k16 * k23 * k32 * t15 - k13 * k15 * k22 * k33 * t15 - k13 * k15 * k23 * k32 * t15 + k12 * k13 * k25 * k33 * t16 * 6.0 + k12 * k13 * k26 * k32 * t16 * 6.0 - k12 * k15 * k22 * k36 * t14 * 2.0 - k12 * k15 * k23 * k35 * t14 * 2.0 - k12 * k16 * k22 * k33 * t16 - k12 * k16 * k23 * k32 * t16 - k13 * k15 * k22 * k33 * t16 - k13 * k15 * k23 * k32 * t16 + k13 * k16 * k22 * k36 * t12 * 4.0 + k13 * k16 * k23 * k35 * t12 * 4.0 + k13 * k23 * k25 * k26 * t12 * 2.0 + k15 * k16 * k25 * k33 * t10 * 4.0 + k15 * k16 * k26 * k32 * t10 * 4.0 - k16 * k22 * k23 * k26 * t12 * 8.0 + k12 * k15 * k22 * k36 * t15 * 6.0 + k12 * k15 * k23 * k35 * t15 * 6.0 - k12 * k15 * k22 * k36 * t16 * 2.0 - k12 * k15 * k23 * k35 * t16 * 6.0 + k15 * k16 * k25 * k33 * t12 * 4.0 + k15 * k16 * k26 * k32 * t12 * 1.2E1 + k13 * k22 * k26 * k36 * t8 * 2.0 + k13 * k23 * k25 * k36 * t8 * 2.0 + k13 * k23 * k26 * k35 * t8 * 2.0 - k16 * k22 * k23 * k36 * t8 * 4.0 + k16 * k25 * k26 * k36 * t2 * 8.0 + k13 * k22 * k26 * k36 * t9 * 6.0 + k13 * k23 * k25 * k36 * t9 * 6.0 + k13 * k23 * k26 * k35 * t9 * 6.0 + k16 * k22 * k23 * k36 * t9 * 4.0 - k12 * k22 * k26 * k33 * t14 - k12 * k23 * k25 * k33 * t14 - k12 * k23 * k26 * k32 * t14 + k13 * k22 * k26 * k36 * t10 * 6.0 + k13 * k23 * k25 * k36 * t10 * 2.0 + k13 * k23 * k26 * k35 * t10 * 2.0 - k15 * k22 * k23 * k33 * t14 * 6.0 + k15 * k25 * k26 * k33 * t8 * 4.0 - k16 * k22 * k23 * k36 * t10 * 8.0 + k12 * k13 * k32 * k36 * t15 * 4.0 + k12 * k13 * k33 * k35 * t15 * 4.0 + k12 * k22 * k26 * k33 * t15 + k12 * k23 * k25 * k33 * t15 * 3.0 + k12 * k23 * k26 * k32 * t15 - k15 * k16 * k32 * k36 * t9 * 2.0 - k15 * k16 * k33 * k35 * t9 * 2.0 - k15 * k22 * k23 * k33 * t15 * 2.0 - k12 * k22 * k26 * k33 * t16 * 5.0 - k12 * k23 * k25 * k33 * t16 * 5.0 - k12 * k23 * k26 * k32 * t16 * 5.0 + k13 * k22 * k26 * k36 * t12 * 4.0 + k13 * k23 * k25 * k36 * t12 * 4.0 + k13 * k23 * k26 * k35 * t12 * 4.0 + k15 * k16 * k32 * k36 * t10 * 2.0 + k15 * k16 * k33 * k35 * t10 * 2.0 + k15 * k22 * k23 * k33 * t16 * 2.0 - k15 * k25 * k26 * k33 * t10 * 2.0 - k16 * k22 * k23 * k36 * t12 * 8.0 - k15 * k16 * k32 * k36 * t12 * 6.0 - k15 * k16 * k33 * k35 * t12 * 2.0 - k15 * k25 * k26 * k33 * t12 * 4.0 - k13 * k23 * k32 * k33 * t14 * 6.0 + k16 * k26 * k32 * k33 * t8 * 4.0 + k16 * k26 * k35 * k36 * t2 * 8.0 - k22 * k23 * k26 * k36 * t8 * 8.0 + k13 * k23 * k32 * k33 * t15 * 2.0 + k13 * k23 * k35 * k36 * t9 * 4.0 - k16 * k26 * k32 * k33 * t9 * 4.0 - k22 * k23 * k26 * k36 * t9 * 8.0 - k12 * k22 * k33 * k36 * t14 - k12 * k23 * k32 * k36 * t14 - k12 * k23 * k33 * k35 * t14 - k13 * k23 * k32 * k33 * t16 * 2.0 - k13 * k23 * k35 * k36 * t10 * 4.0 + k15 * k25 * k33 * k36 * t8 * 6.0 + k15 * k26 * k32 * k36 * t8 * 6.0 + k15 * k26 * k33 * k35 * t8 * 6.0 - k16 * k26 * k32 * k33 * t10 * 8.0 - k12 * k22 * k33 * k36 * t15 * 5.0 - k12 * k23 * k32 * k36 * t15 * 5.0 - k12 * k23 * k33 * k35 * t15 * 5.0 + k15 * k25 * k33 * k36 * t9 * 2.0 + k15 * k26 * k32 * k36 * t9 * 2.0 + k15 * k26 * k33 * k35 * t9 * 2.0 + k12 * k22 * k33 * k36 * t16 + k12 * k23 * k32 * k36 * t16 + k12 * k23 * k33 * k35 * t16 * 3.0 - k13 * k23 * k35 * k36 * t12 * 2.0 + k15 * k25 * k33 * k36 * t10 * 4.0 + k15 * k26 * k32 * k36 * t10 * 4.0 + k15 * k26 * k33 * k35 * t10 * 4.0 - k16 * k26 * k32 * k33 * t12 * 8.0 + k15 * k25 * k33 * k36 * t12 * 2.0 + k15 * k26 * k32 * k36 * t12 * 6.0 + k15 * k26 * k33 * k35 * t12 * 2.0 + k16 * k32 * k33 * k36 * t9 * 8.0 - k16 * k32 * k33 * k36 * t10 * 8.0 - k15 * k33 * k35 * k36 * t9 * 4.0 + k15 * k33 * k35 * k36 * t10 * 2.0 - k26 * k32 * k33 * k36 * t8 * 8.0 - k26 * k32 * k33 * k36 * t9 * 8.0 - k13 * k16 * k22 * t9 * t12 - k13 * k16 * k22 * t10 * t12 * 6.0 - k12 * k15 * k22 * t9 * t16 * 2.0 - k12 * k15 * k22 * t12 * t14 - k12 * k15 * k22 * t12 * t15 - k12 * k15 * k22 * t12 * t16 + k13 * k22 * k26 * t2 * t16 * 2.0 + k13 * k23 * k25 * t2 * t16 * 2.0 - k16 * k22 * k23 * t2 * t16 * 4.0 - k12 * k13 * k32 * t8 * t15 * 2.0 - k12 * k13 * k32 * t10 * t14 - k13 * k22 * k26 * t8 * t12 - k13 * k23 * k25 * t8 * t12 - k15 * k16 * k32 * t8 * t10 + k16 * k22 * k23 * t8 * t12 * 2.0 - k16 * k25 * k26 * t2 * t12 * 4.0 - k12 * k13 * k32 * t10 * t15 - k13 * k22 * k26 * t9 * t12 - k13 * k23 * k25 * t9 * t12 + k16 * k22 * k23 * t9 * t12 * 2.0 - k12 * k13 * k32 * t10 * t16 - k13 * k22 * k26 * t10 * t12 * 3.0 - k13 * k23 * k25 * t10 * t12 + k16 * k22 * k23 * t10 * t12 * 4.0 - k15 * k16 * k32 * t10 * t12 * 6.0 - k13 * k22 * k36 * t2 * t15 * 2.0 - k13 * k23 * k35 * t2 * t15 * 2.0 + k12 * k22 * k33 * t8 * t15 * 3.0 + k12 * k23 * k32 * t8 * t15 * 3.0 - k13 * k22 * k36 * t9 * t10 * 6.0 - k13 * k23 * k35 * t9 * t10 * 2.0 + k12 * k22 * k33 * t10 * t14 * 3.0 + k12 * k23 * k32 * t10 * t14 - k15 * k25 * k33 * t2 * t16 * 2.0 - k15 * k25 * k33 * t8 * t10 * 2.0 - k15 * k26 * k32 * t2 * t16 * 2.0 - k15 * k26 * k32 * t8 * t10 * 2.0 + k22 * k23 * k26 * t8 * t12 * 4.0 + k12 * k22 * k33 * t9 * t16 * 3.0 + k12 * k23 * k32 * t9 * t16 * 3.0 - k13 * k22 * k36 * t9 * t12 * 2.0 - k13 * k23 * k35 * t9 * t12 * 2.0 + k12 * k22 * k33 * t10 * t16 * 3.0 + k12 * k22 * k33 * t12 * t14 + k12 * k23 * k32 * t10 * t16 + k12 * k23 * k32 * t12 * t14 * 3.0 - k15 * k25 * k33 * t8 * t12 * 2.0 - k15 * k26 * k32 * t8 * t12 * 6.0 + k12 * k22 * k33 * t12 * t15 + k12 * k23 * k32 * t12 * t15 * 3.0 + k22 * k23 * k36 * t2 * t14 * 2.0 + k25 * k26 * k36 * t2 * t8 * 4.0 - k16 * k32 * k33 * t2 * t15 * 4.0 + k22 * k23 * k36 * t2 * t15 * 2.0 + k22 * k23 * k36 * t8 * t9 * 2.0 + k16 * k32 * k33 * t8 * t10 * 2.0 - k16 * k35 * k36 * t2 * t10 * 4.0 + k22 * k23 * k36 * t2 * t16 * 2.0 + k25 * k26 * k36 * t2 * t10 * 2.0 + k15 * k32 * k36 * t2 * t15 * 2.0 + k15 * k33 * k35 * t2 * t15 * 2.0 + k16 * k32 * k33 * t9 * t10 * 2.0 + k22 * k23 * k36 * t9 * t10 * 4.0 - k15 * k32 * k36 * t8 * t10 - k15 * k33 * k35 * t8 * t10 - k15 * k32 * k36 * t9 * t10 - k15 * k33 * k35 * t9 * t10 + k22 * k23 * k36 * t9 * t12 * 4.0 + k16 * k32 * k33 * t10 * t12 * 4.0 - k15 * k32 * k36 * t10 * t12 * 3.0 - k15 * k33 * k35 * t10 * t12 + k26 * k32 * k33 * t2 * t14 * 2.0 + k26 * k32 * k33 * t2 * t15 * 2.0 + k26 * k32 * k33 * t8 * t9 * 2.0 + k26 * k35 * k36 * t2 * t9 * 4.0 + k26 * k32 * k33 * t2 * t16 * 2.0 + k26 * k32 * k33 * t8 * t10 * 4.0 + k26 * k32 * k33 * t8 * t12 * 4.0 + k26 * k35 * k36 * t2 * t12 * 2.0 + k32 * k33 * k36 * t9 * t10 * 4.0 - k13 * k22 * t2 * t12 * t15 + k16 * k25 * t2 * t10 * t12 + k22 * k23 * t2 * t9 * t16 * 2.0 + k25 * k26 * t2 * t8 * t12 * 2.0 - k15 * k32 * t2 * t10 * t16 + k16 * k35 * t2 * t10 * t12 + k25 * k36 * t2 * t10 * t12 + k26 * k35 * t2 * t10 * t12 + k32 * k33 * t2 * t8 * t15 * 2.0; coeffs(7) += k35 * k36 * t2 * t9 * t10 * 2.0 + k12 * k13 * k15 * k16 * k25 * k36 * 8.0 + k12 * k13 * k15 * k16 * k26 * k35 * 8.0 + k12 * k13 * k16 * k25 * k26 * k33 * 8.0 - k13 * k15 * k16 * k22 * k26 * k33 * 2.0 - k13 * k15 * k16 * k23 * k25 * k33 * 2.0 - k13 * k15 * k16 * k23 * k26 * k32 * 2.0 - k12 * k13 * k15 * k25 * k26 * k36 * 1.6E1 - k12 * k15 * k16 * k22 * k26 * k36 * 4.0 - k12 * k15 * k16 * k23 * k25 * k36 * 4.0 - k12 * k15 * k16 * k23 * k26 * k35 * 4.0 - k12 * k13 * k16 * k25 * k33 * k36 * 4.0 - k12 * k13 * k16 * k26 * k32 * k36 * 4.0 - k12 * k13 * k16 * k26 * k33 * k35 * 4.0 - k12 * k16 * k23 * k25 * k26 * k33 * 2.0 - k13 * k15 * k16 * k22 * k33 * k36 * 2.0 - k13 * k15 * k16 * k23 * k32 * k36 * 2.0 - k13 * k15 * k16 * k23 * k33 * k35 * 2.0 - k13 * k15 * k23 * k25 * k26 * k33 * 2.0 + k15 * k16 * k22 * k23 * k26 * k33 * 8.0 - k12 * k13 * k15 * k26 * k35 * k36 * 1.6E1 + k12 * k15 * k16 * k23 * k35 * k36 * 8.0 + k12 * k15 * k23 * k25 * k26 * k36 * 1.2E1 + k13 * k16 * k23 * k26 * k32 * k33 * 4.0 + k12 * k13 * k25 * k26 * k33 * k36 * 8.0 + k12 * k16 * k22 * k26 * k33 * k36 * 6.0 + k12 * k16 * k23 * k25 * k33 * k36 * 6.0 + k12 * k16 * k23 * k26 * k32 * k36 * 6.0 + k12 * k16 * k23 * k26 * k33 * k35 * 6.0 - k13 * k15 * k22 * k26 * k33 * k36 * 1.0E1 - k13 * k15 * k23 * k25 * k33 * k36 * 1.0E1 - k13 * k15 * k23 * k26 * k32 * k36 * 1.0E1 - k13 * k15 * k23 * k26 * k33 * k35 * 1.0E1 + k15 * k16 * k22 * k23 * k33 * k36 * 4.0 + k12 * k15 * k23 * k26 * k35 * k36 * 8.0 + k13 * k16 * k23 * k32 * k33 * k36 * 8.0 + k12 * k13 * k26 * k33 * k35 * k36 * 1.2E1 - k12 * k16 * k23 * k33 * k35 * k36 * 2.0 - k12 * k23 * k25 * k26 * k33 * k36 * 1.0E1 - k13 * k15 * k23 * k33 * k35 * k36 * 2.0 + k15 * k22 * k23 * k26 * k33 * k36 * 8.0 + k13 * k23 * k26 * k32 * k33 * k36 * 8.0 - k12 * k23 * k26 * k33 * k35 * k36 * 1.0E1 + k12 * k15 * k16 * k22 * k26 * t12 * 2.0 + k12 * k15 * k16 * k23 * k25 * t12 * 2.0 - k12 * k13 * k16 * k25 * k33 * t10 * 4.0 - k12 * k13 * k16 * k26 * k32 * t10 * 4.0 + k13 * k15 * k16 * k22 * k33 * t10 * 6.0 + k13 * k15 * k16 * k23 * k32 * t10 * 2.0 + k12 * k13 * k15 * k25 * k36 * t10 * 4.0 + k12 * k13 * k15 * k26 * k35 * t10 * 4.0 + k13 * k15 * k16 * k22 * k33 * t12 * 2.0 + k13 * k15 * k16 * k23 * k32 * t12 * 6.0 + k12 * k13 * k15 * k25 * k36 * t12 * 4.0 + k12 * k13 * k15 * k26 * k35 * t12 * 4.0 - k12 * k15 * k16 * k22 * k36 * t12 * 4.0 - k12 * k15 * k16 * k23 * k35 * t12 * 4.0 - k12 * k15 * k23 * k25 * k26 * t12 * 2.0 - k13 * k16 * k22 * k26 * k36 * t2 * 2.0 - k13 * k16 * k23 * k25 * k36 * t2 * 2.0 - k13 * k16 * k23 * k26 * k35 * t2 * 2.0 - k12 * k13 * k22 * k23 * k33 * t14 * 2.0 - k12 * k13 * k25 * k26 * k33 * t8 * 4.0 + k12 * k16 * k22 * k26 * k33 * t8 * 3.0 + k12 * k16 * k23 * k25 * k33 * t8 * 3.0 + k12 * k16 * k23 * k26 * k32 * t8 * 3.0 + k13 * k15 * k22 * k26 * k33 * t8 + k13 * k15 * k23 * k25 * k33 * t8 + k13 * k15 * k23 * k26 * k32 * t8 - k15 * k16 * k22 * k23 * k33 * t8 * 2.0 - k12 * k13 * k22 * k23 * k33 * t15 * 2.0 + k12 * k13 * k16 * k32 * k36 * t10 * 2.0 + k12 * k13 * k16 * k33 * k35 * t10 * 2.0 - k12 * k13 * k22 * k23 * k33 * t16 * 2.0 - k12 * k13 * k25 * k26 * k33 * t10 * 2.0 - k12 * k15 * k22 * k26 * k36 * t8 * 2.0 - k12 * k15 * k23 * k25 * k36 * t8 * 2.0 - k12 * k15 * k23 * k26 * k35 * t8 * 2.0 + k12 * k16 * k22 * k26 * k33 * t10 * 3.0 + k12 * k16 * k23 * k25 * k33 * t10 + k12 * k16 * k23 * k26 * k32 * t10 + k13 * k15 * k22 * k26 * k33 * t10 * 3.0 + k13 * k15 * k23 * k25 * k33 * t10 + k13 * k15 * k23 * k26 * k32 * t10 - k15 * k16 * k22 * k23 * k33 * t10 * 4.0 - k12 * k15 * k22 * k26 * k36 * t10 * 6.0 - k12 * k15 * k23 * k25 * k36 * t10 * 2.0 - k12 * k15 * k23 * k26 * k35 * t10 * 2.0 - k12 * k16 * k22 * k26 * k33 * t12 * 2.0 - k12 * k16 * k23 * k25 * k33 * t12 * 2.0 - k12 * k16 * k23 * k26 * k32 * t12 * 6.0 + k13 * k15 * k22 * k26 * k33 * t12 * 2.0 + k13 * k15 * k23 * k25 * k33 * t12 * 2.0 + k13 * k15 * k23 * k26 * k32 * t12 * 6.0 - k15 * k16 * k22 * k23 * k33 * t12 * 4.0 - k13 * k23 * k25 * k26 * k36 * t2 * 4.0 - k13 * k16 * k23 * k32 * k33 * t9 * 2.0 + k12 * k23 * k25 * k26 * k33 * t8 * 6.0 - k13 * k16 * k23 * k32 * k33 * t10 * 4.0 - k15 * k16 * k25 * k33 * k36 * t2 * 2.0 - k15 * k16 * k26 * k32 * k36 * t2 * 2.0 - k15 * k16 * k26 * k33 * k35 * t2 * 2.0 - k15 * k22 * k23 * k26 * k33 * t8 * 4.0 - k12 * k13 * k25 * k33 * k36 * t9 * 2.0 - k12 * k13 * k26 * k32 * k36 * t9 * 2.0 - k12 * k13 * k26 * k33 * k35 * t9 * 2.0 + k12 * k16 * k22 * k33 * k36 * t9 * 3.0 + k12 * k16 * k23 * k32 * k36 * t9 * 3.0; coeffs(7) += k12 * k16 * k23 * k33 * k35 * t9 * 3.0 + k13 * k15 * k22 * k33 * k36 * t9 + k13 * k15 * k23 * k32 * k36 * t9 + k13 * k15 * k23 * k33 * k35 * t9 - k12 * k15 * k23 * k32 * k33 * t14 * 2.0 - k12 * k16 * k22 * k33 * k36 * t10 * 6.0 - k12 * k16 * k23 * k32 * k36 * t10 * 2.0 - k12 * k16 * k23 * k33 * k35 * t10 * 2.0 + k13 * k15 * k22 * k33 * k36 * t10 * 6.0 + k13 * k15 * k23 * k32 * k36 * t10 * 2.0 + k13 * k15 * k23 * k33 * k35 * t10 * 2.0 - k13 * k16 * k23 * k32 * k33 * t12 * 4.0 - k12 * k15 * k23 * k32 * k33 * t15 * 2.0 - k12 * k15 * k23 * k35 * k36 * t9 * 4.0 - k12 * k13 * k25 * k33 * k36 * t12 * 2.0 - k12 * k13 * k26 * k32 * k36 * t12 * 6.0 - k12 * k13 * k26 * k33 * k35 * t12 * 2.0 - k12 * k15 * k23 * k32 * k33 * t16 * 2.0 + k12 * k16 * k22 * k33 * k36 * t12 + k12 * k16 * k23 * k32 * k36 * t12 * 3.0 + k12 * k16 * k23 * k33 * k35 * t12 + k12 * k23 * k25 * k26 * k33 * t12 * 2.0 + k13 * k15 * k22 * k33 * k36 * t12 + k13 * k15 * k23 * k32 * k36 * t12 * 3.0 + k13 * k15 * k23 * k33 * k35 * t12 - k12 * k15 * k23 * k35 * k36 * t12 * 2.0 - k13 * k23 * k26 * k32 * k33 * t8 * 2.0 + k13 * k23 * k26 * k35 * k36 * t2 * 4.0 - k13 * k23 * k26 * k32 * k33 * t9 * 2.0 - k12 * k22 * k26 * k33 * k36 * t8 - k12 * k23 * k25 * k33 * k36 * t8 - k12 * k23 * k26 * k32 * k36 * t8 - k12 * k23 * k26 * k33 * k35 * t8 - k13 * k23 * k26 * k32 * k33 * t10 * 2.0 - k15 * k22 * k23 * k33 * k36 * t8 * 2.0 + k15 * k25 * k26 * k33 * k36 * t2 * 4.0 - k12 * k22 * k26 * k33 * k36 * t9 - k12 * k23 * k25 * k33 * k36 * t9 - k12 * k23 * k26 * k32 * k36 * t9 - k12 * k23 * k26 * k33 * k35 * t9 - k15 * k22 * k23 * k33 * k36 * t9 * 2.0 - k12 * k13 * k33 * k35 * k36 * t10 * 2.0 + k12 * k22 * k26 * k33 * k36 * t10 * 3.0 + k12 * k23 * k25 * k33 * k36 * t10 + k12 * k23 * k26 * k32 * k36 * t10 + k12 * k23 * k26 * k33 * k35 * t10 - k13 * k23 * k26 * k32 * k33 * t12 * 4.0 - k15 * k22 * k23 * k33 * k36 * t10 * 4.0 + k12 * k22 * k26 * k33 * k36 * t12 + k12 * k23 * k25 * k33 * k36 * t12 + k12 * k23 * k26 * k32 * k36 * t12 * 3.0 + k12 * k23 * k26 * k33 * k35 * t12 - k15 * k22 * k23 * k33 * k36 * t12 * 2.0 - k13 * k23 * k32 * k33 * k36 * t9 * 4.0 - k15 * k26 * k33 * k35 * k36 * t2 * 4.0 + k12 * k23 * k33 * k35 * k36 * t9 * 6.0 + k12 * k23 * k33 * k35 * k36 * t10 * 2.0 + k12 * k13 * k15 * k25 * t10 * t12 - k12 * k15 * k16 * k22 * t10 * t12 * 3.0 - k13 * k16 * k22 * k26 * t2 * t12 - k13 * k16 * k23 * k25 * t2 * t12 - k12 * k15 * k22 * k26 * t2 * t16 * 2.0 - k12 * k15 * k23 * k25 * t2 * t16 * 2.0 - k12 * k13 * k16 * k32 * t10 * t12 * 3.0 - k12 * k15 * k22 * k26 * t8 * t12 - k12 * k15 * k23 * k25 * t8 * t12 + k12 * k13 * k15 * k35 * t10 * t12 - k13 * k23 * k25 * k26 * t2 * t12 * 2.0 + k16 * k22 * k23 * k26 * t2 * t12 * 2.0 - k12 * k13 * k25 * k33 * t10 * t12 - k12 * k13 * k26 * k32 * t10 * t12 * 3.0 + k12 * k16 * k22 * k33 * t10 * t12 * 3.0 + k12 * k16 * k23 * k32 * t10 * t12 * 3.0 - k12 * k15 * k22 * k36 * t10 * t12 * 3.0 - k12 * k15 * k23 * k35 * t10 * t12 - k12 * k13 * k32 * k36 * t2 * t15 * 2.0 - k12 * k13 * k33 * k35 * t2 * t15 * 2.0 + k12 * k22 * k26 * k33 * t2 * t16 + k12 * k23 * k25 * k33 * t2 * t16 + k12 * k23 * k26 * k32 * t2 * t16 - k13 * k22 * k26 * k36 * t2 * t12 * 2.0 - k13 * k23 * k25 * k36 * t2 * t12 * 2.0 - k13 * k23 * k26 * k35 * t2 * t12 * 2.0 - k15 * k16 * k32 * k36 * t2 * t10 - k15 * k16 * k33 * k35 * t2 * t10 - k15 * k22 * k23 * k33 * t2 * t16 * 2.0 + k16 * k22 * k23 * k36 * t2 * t12 * 2.0 - k12 * k13 * k32 * k36 * t9 * t10 - k12 * k13 * k33 * k35 * t9 * t10 + k12 * k22 * k26 * k33 * t8 * t12 + k12 * k23 * k25 * k33 * t8 * t12 + k12 * k23 * k26 * k32 * t8 * t12 * 3.0 - k13 * k23 * k32 * k33 * t2 * t15 * 2.0 + k16 * k26 * k32 * k33 * t2 * t10 * 2.0 + k12 * k22 * k33 * k36 * t2 * t15 + k12 * k23 * k32 * k36 * t2 * t15 + k12 * k23 * k33 * k35 * t2 * t15 - k15 * k25 * k33 * k36 * t2 * t10 * 2.0 - k15 * k26 * k32 * k36 * t2 * t10 * 2.0 - k15 * k26 * k33 * k35 * t2 * t10 * 2.0 + k22 * k23 * k26 * k36 * t2 * t12 * 2.0 + k12 * k22 * k33 * k36 * t9 * t10 * 3.0 + k12 * k23 * k32 * k36 * t9 * t10 + k12 * k23 * k33 * k35 * t9 * t10 + k16 * k32 * k33 * k36 * t2 * t10 * 2.0 - k15 * k33 * k35 * k36 * t2 * t10 * 2.0 + k26 * k32 * k33 * k36 * t2 * t10 * 2.0 - k12 * k13 * k16 * k22 * k23 * k26 * k33 * 8.0 + k12 * k13 * k15 * k22 * k23 * k26 * k36 * 8.0 + k12 * k13 * k16 * k22 * k23 * k33 * k36 * 4.0 + k12 * k15 * k16 * k23 * k26 * k32 * k33 * 4.0 + k12 * k13 * k15 * k26 * k32 * k33 * k36 * 8.0 - k12 * k15 * k16 * k23 * k32 * k33 * k36 * 8.0 + k12 * k13 * k15 * k16 * k22 * k23 * t12 * 2.0 + k12 * k13 * k15 * k22 * k23 * k26 * t12 * 2.0 + k12 * k13 * k15 * k16 * k32 * k33 * t10 * 2.0 - k12 * k13 * k16 * k22 * k23 * k33 * t12 * 2.0 + k12 * k13 * k15 * k22 * k23 * k36 * t12 * 2.0 + k12 * k13 * k15 * k26 * k32 * k33 * t10 * 2.0 - k12 * k13 * k22 * k23 * k26 * k33 * t12 * 2.0 - k12 * k15 * k16 * k23 * k32 * k33 * t10 * 2.0 - k13 * k16 * k23 * k26 * k32 * k33 * t2 * 2.0 - k12 * k13 * k25 * k26 * k33 * k36 * t2 * 4.0 + k12 * k16 * k22 * k26 * k33 * k36 * t2 + k12 * k16 * k23 * k25 * k33 * k36 * t2 + k12 * k16 * k23 * k26 * k32 * k36 * t2 + k12 * k16 * k23 * k26 * k33 * k35 * t2 + k13 * k15 * k22 * k26 * k33 * k36 * t2 * 3.0 + k13 * k15 * k23 * k25 * k33 * k36 * t2 * 3.0 + k13 * k15 * k23 * k26 * k32 * k36 * t2 * 3.0 + k13 * k15 * k23 * k26 * k33 * k35 * t2 * 3.0 - k15 * k16 * k22 * k23 * k33 * k36 * t2 * 2.0 - k12 * k13 * k22 * k23 * k33 * k36 * t9 * 2.0 - k12 * k15 * k23 * k26 * k32 * k33 * t8 * 2.0 - k12 * k15 * k23 * k26 * k35 * k36 * t2 * 4.0 + k12 * k13 * k15 * k32 * k33 * k36 * t10 * 2.0 + k12 * k23 * k25 * k26 * k33 * k36 * t2 * 2.0 - k15 * k22 * k23 * k26 * k33 * k36 * t2 * 4.0 - k12 * k15 * k23 * k32 * k33 * k36 * t10 * 2.0 - k13 * k23 * k26 * k32 * k33 * k36 * t2 * 4.0 + k12 * k23 * k26 * k33 * k35 * k36 * t2 * 2.0; coeffs(8) = t14 * t15 * 6.0 + t14 * t16 * 6.0 + t15 * t16 * 6.0 + t15 * t17 + t14 * t19 + t14 * t20 + t15 * t19 + t16 * t18 + t16 * t20 + t14 * t14 + t15 * t15 + t16 * t16 - k16 * k26 * t14 * 4.0 - k16 * k26 * t15 * 4.0 - k16 * k26 * t16 * 1.2E1 - k16 * k26 * t19 * 2.0 - k16 * k36 * t14 * 4.0 - k16 * k36 * t15 * 1.2E1 - k16 * k36 * t16 * 4.0 - k16 * k36 * t20 * 2.0 + k26 * k36 * t14 * 1.2E1 + k26 * k36 * t15 * 4.0 + k26 * k36 * t16 * 4.0 - k16 * t8 * t15 * 4.0 + k16 * t10 * t14 * 2.0 - k16 * t9 * t16 * 4.0 + k16 * t10 * t15 * 2.0 + k16 * t10 * t16 * 6.0 + k16 * t12 * t14 * 2.0 + k16 * t12 * t15 * 6.0 + k16 * t12 * t16 * 2.0 + k16 * t10 * t19 + k16 * t12 * t20 + k26 * t8 * t14 * 2.0 + k26 * t8 * t15 * 2.0 + k26 * t8 * t16 * 2.0 - k26 * t10 * t14 * 4.0 + k26 * t9 * t16 * 4.0 - k26 * t10 * t16 * 4.0 - k26 * t12 * t14 * 6.0 + k26 * t8 * t19 - k26 * t12 * t15 * 2.0 - k26 * t12 * t16 * 2.0 + k36 * t8 * t15 * 4.0 + k36 * t9 * t14 * 2.0 + k36 * t9 * t15 * 2.0 - k36 * t10 * t14 * 6.0 + k36 * t9 * t16 * 2.0 - k36 * t10 * t15 * 2.0 - k36 * t10 * t16 * 2.0 - k36 * t12 * t14 * 4.0 - k36 * t12 * t15 * 4.0 + k36 * t9 * t20 + t8 * t10 * t14 - t2 * t15 * t16 * 4.0 + t8 * t10 * t15 + t8 * t10 * t16 - t8 * t12 * t15 * 2.0 - t9 * t10 * t16 * 2.0 + t9 * t12 * t14 + t5 * t15 * t16 + t9 * t12 * t15 + t10 * t12 * t14 * 3.0 + t9 * t12 * t16 + t10 * t12 * t15 + t10 * t12 * t16 - k13 * k16 * k23 * t14 * 2.0 + k13 * k16 * k23 * t15 * 2.0 - k13 * k16 * k23 * t16 * 6.0 - k13 * k16 * k23 * t19 + k13 * k23 * k26 * t14 * 2.0 - k13 * k23 * k26 * t15 * 2.0 + k13 * k23 * k26 * t16 * 2.0 - k15 * k16 * k33 * t14 * 2.0 - k15 * k16 * k33 * t15 * 6.0 + k15 * k16 * k33 * t16 * 2.0 - k13 * k23 * k26 * t19 - k15 * k16 * k33 * t20 + k13 * k23 * k36 * t14 * 6.0 - k16 * k26 * k36 * t8 * 4.0 - k13 * k23 * k36 * t15 * 2.0 - k16 * k26 * k36 * t9 * 4.0 + k13 * k23 * k36 * t16 * 2.0 + k15 * k26 * k33 * t14 * 6.0 + k16 * k26 * k36 * t10 * 8.0 + k15 * k26 * k33 * t15 * 2.0 - k15 * k26 * k33 * t16 * 2.0 + k16 * k26 * k36 * t12 * 8.0 + k15 * k33 * k36 * t14 * 2.0 + k15 * k33 * k36 * t15 * 2.0 - k15 * k33 * k36 * t16 * 2.0 - k15 * k33 * k36 * t20 - k13 * k23 * t8 * t15 * 2.0 - k13 * k23 * t10 * t14 * 2.0 + k16 * k26 * t2 * t16 * 4.0 + k16 * k26 * t8 * t10 * 4.0 + k13 * k23 * t9 * t16 * 2.0 - k13 * k23 * t10 * t16 * 2.0 - k13 * k23 * t12 * t14 * 3.0 + k16 * k26 * t8 * t12 * 2.0 + k13 * k23 * t12 * t15 - k16 * k26 * t9 * t12 * 2.0 - k13 * k23 * t12 * t16 - k16 * k26 * t10 * t12 * 4.0 + k16 * k36 * t2 * t15 * 4.0 - k16 * k36 * t8 * t10 * 2.0 + k15 * k33 * t8 * t15 * 2.0 + k16 * k36 * t9 * t10 * 2.0 - k15 * k33 * t10 * t14 * 3.0 - k15 * k33 * t9 * t16 * 2.0 - k15 * k33 * t10 * t15 + k16 * k36 * t9 * t12 * 4.0 + k15 * k33 * t10 * t16 - k15 * k33 * t12 * t14 * 2.0 - k16 * k36 * t10 * t12 * 4.0 - k15 * k33 * t12 * t15 * 2.0 - k26 * k36 * t2 * t14 * 2.0 - k26 * k36 * t2 * t15 * 2.0 - k26 * k36 * t8 * t9 * 2.0 - k26 * k36 * t2 * t16 * 2.0 - k26 * k36 * t8 * t10 * 4.0 - k26 * k36 * t9 * t10 * 4.0 - k26 * k36 * t8 * t12 * 4.0 - k26 * k36 * t9 * t12 * 4.0 - k16 * t2 * t10 * t16 * 2.0 - k16 * t2 * t12 * t15 * 2.0 + k16 * t8 * t10 * t12 + k16 * t9 * t10 * t12 + k26 * t2 * t9 * t16 * 2.0 + k26 * t2 * t12 * t14 + k26 * t2 * t12 * t15 + k26 * t8 * t9 * t12 + k26 * t2 * t12 * t16 + k26 * t8 * t10 * t12 * 2.0 + k36 * t2 * t8 * t15 * 2.0 + k36 * t2 * t10 * t14 + k36 * t2 * t10 * t15 + k36 * t8 * t9 * t10 + k36 * t2 * t10 * t16 + k36 * t9 * t10 * t12 * 2.0 + t2 * t8 * t12 * t15 + t2 * t9 * t10 * t16 - k13 * k16 * k23 * k26 * k36 * 4.0 - k15 * k16 * k26 * k33 * k36 * 4.0 - k12 * k13 * k15 * k26 * t16 * 8.0 + k12 * k15 * k16 * k23 * t16 * 4.0 - k13 * k16 * k23 * k26 * t8 * 2.0 - k13 * k16 * k23 * k26 * t10 * 2.0 + k12 * k13 * k16 * k33 * t15 * 4.0 + k13 * k16 * k23 * k26 * t12 * 2.0 - k12 * k13 * k15 * k36 * t15 * 8.0 + k12 * k15 * k23 * k26 * t16 * 4.0 - k13 * k16 * k23 * k36 * t9 * 2.0 - k12 * k13 * k26 * k33 * t14 * 2.0 + k12 * k16 * k23 * k33 * t14 + k13 * k15 * k23 * k33 * t14 * 3.0 + k13 * k16 * k23 * k36 * t10 * 4.0 - k15 * k16 * k26 * k33 * t8 * 2.0 - k12 * k13 * k26 * k33 * t15 * 2.0 - k12 * k16 * k23 * k33 * t15 - k13 * k15 * k23 * k33 * t15 + k12 * k13 * k26 * k33 * t16 * 6.0 - k12 * k15 * k23 * k36 * t14 * 2.0 - k12 * k16 * k23 * k33 * t16 - k13 * k15 * k23 * k33 * t16 + k13 * k16 * k23 * k36 * t12 * 4.0 + k15 * k16 * k26 * k33 * t10 * 4.0 + k12 * k15 * k23 * k36 * t15 * 6.0 - k12 * k15 * k23 * k36 * t16 * 2.0 + k15 * k16 * k26 * k33 * t12 * 4.0 + k13 * k23 * k26 * k36 * t8 * 2.0 + k13 * k23 * k26 * k36 * t9 * 6.0 - k12 * k23 * k26 * k33 * t14 + k13 * k23 * k26 * k36 * t10 * 2.0 + k12 * k13 * k33 * k36 * t15 * 4.0 + k12 * k23 * k26 * k33 * t15 - k15 * k16 * k33 * k36 * t9 * 2.0 - k12 * k23 * k26 * k33 * t16 * 5.0 + k13 * k23 * k26 * k36 * t12 * 4.0 + k15 * k16 * k33 * k36 * t10 * 2.0 - k15 * k16 * k33 * k36 * t12 * 2.0 - k12 * k23 * k33 * k36 * t14 + k15 * k26 * k33 * k36 * t8 * 6.0 - k12 * k23 * k33 * k36 * t15 * 5.0 + k15 * k26 * k33 * k36 * t9 * 2.0 + k12 * k23 * k33 * k36 * t16 + k15 * k26 * k33 * k36 * t10 * 4.0 + k15 * k26 * k33 * k36 * t12 * 2.0 - k13 * k16 * k23 * t9 * t12 - k13 * k16 * k23 * t10 * t12 * 2.0 - k12 * k15 * k23 * t9 * t16 * 2.0 - k12 * k15 * k23 * t12 * t14 - k12 * k15 * k23 * t12 * t15 - k12 * k15 * k23 * t12 * t16 + k13 * k23 * k26 * t2 * t16 * 2.0 - k12 * k13 * k33 * t8 * t15 * 2.0 - k12 * k13 * k33 * t10 * t14 - k13 * k23 * k26 * t8 * t12 - k15 * k16 * k33 * t8 * t10 - k12 * k13 * k33 * t10 * t15 - k13 * k23 * k26 * t9 * t12 - k12 * k13 * k33 * t10 * t16 - k13 * k23 * k26 * t10 * t12 - k15 * k16 * k33 * t10 * t12 * 2.0 - k13 * k23 * k36 * t2 * t15 * 2.0 + k12 * k23 * k33 * t8 * t15 * 3.0 - k13 * k23 * k36 * t9 * t10 * 2.0 + k12 * k23 * k33 * t10 * t14 - k15 * k26 * k33 * t2 * t16 * 2.0 - k15 * k26 * k33 * t8 * t10 * 2.0 + k12 * k23 * k33 * t9 * t16 * 3.0 - k13 * k23 * k36 * t9 * t12 * 2.0; coeffs(8) += k12 * k23 * k33 * t10 * t16 + k12 * k23 * k33 * t12 * t14 - k15 * k26 * k33 * t8 * t12 * 2.0 + k12 * k23 * k33 * t12 * t15 + k15 * k33 * k36 * t2 * t15 * 2.0 - k15 * k33 * k36 * t8 * t10 - k15 * k33 * k36 * t9 * t10 - k15 * k33 * k36 * t10 * t12 - k13 * k23 * t2 * t12 * t15 + k16 * k26 * t2 * t10 * t12 - k15 * k33 * t2 * t10 * t16 + k16 * k36 * t2 * t10 * t12 + k26 * k36 * t2 * t10 * t12 + k12 * k13 * k15 * k16 * k26 * k36 * 8.0 - k13 * k15 * k16 * k23 * k26 * k33 * 2.0 - k12 * k15 * k16 * k23 * k26 * k36 * 4.0 - k12 * k13 * k16 * k26 * k33 * k36 * 4.0 - k13 * k15 * k16 * k23 * k33 * k36 * 2.0 + k12 * k16 * k23 * k26 * k33 * k36 * 6.0 - k13 * k15 * k23 * k26 * k33 * k36 * 1.0E1 + k12 * k15 * k16 * k23 * k26 * t12 * 2.0 - k12 * k13 * k16 * k26 * k33 * t10 * 4.0 + k13 * k15 * k16 * k23 * k33 * t10 * 2.0 + k12 * k13 * k15 * k26 * k36 * t10 * 4.0 + k13 * k15 * k16 * k23 * k33 * t12 * 2.0 + k12 * k13 * k15 * k26 * k36 * t12 * 4.0 - k12 * k15 * k16 * k23 * k36 * t12 * 4.0 - k13 * k16 * k23 * k26 * k36 * t2 * 2.0 + k12 * k16 * k23 * k26 * k33 * t8 * 3.0 + k13 * k15 * k23 * k26 * k33 * t8 + k12 * k13 * k16 * k33 * k36 * t10 * 2.0 - k12 * k15 * k23 * k26 * k36 * t8 * 2.0 + k12 * k16 * k23 * k26 * k33 * t10 + k13 * k15 * k23 * k26 * k33 * t10 - k12 * k15 * k23 * k26 * k36 * t10 * 2.0 - k12 * k16 * k23 * k26 * k33 * t12 * 2.0 + k13 * k15 * k23 * k26 * k33 * t12 * 2.0 - k15 * k16 * k26 * k33 * k36 * t2 * 2.0 - k12 * k13 * k26 * k33 * k36 * t9 * 2.0 + k12 * k16 * k23 * k33 * k36 * t9 * 3.0 + k13 * k15 * k23 * k33 * k36 * t9 - k12 * k16 * k23 * k33 * k36 * t10 * 2.0 + k13 * k15 * k23 * k33 * k36 * t10 * 2.0 - k12 * k13 * k26 * k33 * k36 * t12 * 2.0 + k12 * k16 * k23 * k33 * k36 * t12 + k13 * k15 * k23 * k33 * k36 * t12 - k12 * k23 * k26 * k33 * k36 * t8 - k12 * k23 * k26 * k33 * k36 * t9 + k12 * k23 * k26 * k33 * k36 * t10 + k12 * k23 * k26 * k33 * k36 * t12 + k12 * k13 * k15 * k16 * t10 * t12 + k12 * k13 * k15 * k26 * t10 * t12 - k12 * k15 * k16 * k23 * t10 * t12 - k13 * k16 * k23 * k26 * t2 * t12 - k12 * k15 * k23 * k26 * t2 * t16 * 2.0 - k12 * k13 * k16 * k33 * t10 * t12 - k12 * k15 * k23 * k26 * t8 * t12 + k12 * k13 * k15 * k36 * t10 * t12 - k12 * k13 * k26 * k33 * t10 * t12 + k12 * k16 * k23 * k33 * t10 * t12 - k12 * k15 * k23 * k36 * t10 * t12 - k12 * k13 * k33 * k36 * t2 * t15 * 2.0 + k12 * k23 * k26 * k33 * t2 * t16 - k13 * k23 * k26 * k36 * t2 * t12 * 2.0 - k15 * k16 * k33 * k36 * t2 * t10 - k12 * k13 * k33 * k36 * t9 * t10 + k12 * k23 * k26 * k33 * t8 * t12 + k12 * k23 * k33 * k36 * t2 * t15 - k15 * k26 * k33 * k36 * t2 * t10 * 2.0 + k12 * k23 * k33 * k36 * t9 * t10 + k12 * k16 * k23 * k26 * k33 * k36 * t2 + k13 * k15 * k23 * k26 * k33 * k36 * t2 * 3.0; return coeffs; } } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_absolute_pose_coeffs.h000066400000000000000000000035001454702036400261510ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include namespace colmap { Eigen::Matrix ComputeDepthsSylvesterCoeffs( const Eigen::Matrix& K); } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_absolute_pose_test.cc000066400000000000000000000111021454702036400260160ustar00rootroot00000000000000// 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. #include "colmap/estimators/generalized_absolute_pose.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/rigid3.h" #include "colmap/optim/ransac.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { namespace { TEST(GeneralizedAbsolutePose, Estimate) { std::vector points3D; points3D.emplace_back(1, 1, 1); points3D.emplace_back(0, 1, 1); points3D.emplace_back(3, 1.0, 4); points3D.emplace_back(3, 1.1, 4); points3D.emplace_back(3, 1.2, 4); points3D.emplace_back(3, 1.3, 4); points3D.emplace_back(3, 1.4, 4); points3D.emplace_back(2, 1, 7); auto points3D_faulty = points3D; for (size_t i = 0; i < points3D.size(); ++i) { points3D_faulty[i](0) = 20; } // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double qx = 0; qx < 1; qx += 0.2) { // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double tx = 0; tx < 1; tx += 0.1) { const int kRefCamIdx = 1; const int kNumCams = 3; const std::array cams_from_world = {{ Rigid3d(Eigen::Quaterniond(1, qx, 0, 0).normalized(), Eigen::Vector3d(tx, -0.1, 0)), Rigid3d(Eigen::Quaterniond(1, qx, 0, 0).normalized(), Eigen::Vector3d(tx, 0, 0)), Rigid3d(Eigen::Quaterniond(1, qx, 0, 0).normalized(), Eigen::Vector3d(tx, 0.1, 0)), }}; const Rigid3d& rig_from_world = cams_from_world[kRefCamIdx]; std::array cams_from_rig; for (size_t i = 0; i < kNumCams; ++i) { cams_from_rig[i] = cams_from_world[i] * Inverse(rig_from_world); } // Project points to camera coordinate system. std::vector points2D; for (size_t i = 0; i < points3D.size(); ++i) { points2D.emplace_back(); points2D.back().cam_from_rig = cams_from_rig[i % kNumCams]; points2D.back().ray_in_cam = (cams_from_world[i % kNumCams] * points3D[i]).normalized(); } RANSACOptions options; options.max_error = 1e-5; RANSAC ransac(options); const auto report = ransac.Estimate(points2D, points3D); EXPECT_TRUE(report.success); EXPECT_LT((rig_from_world.ToMatrix() - report.model.ToMatrix()).norm(), 1e-2) << report.model.ToMatrix() << "\n\n" << rig_from_world.ToMatrix(); // Test residuals of exact points. std::vector residuals; ransac.estimator.Residuals(points2D, points3D, report.model, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_LT(residuals[i], 1e-10); } // Test residuals of faulty points. ransac.estimator.Residuals( points2D, points3D_faulty, report.model, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_GT(residuals[i], 1e-10); } } } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_pose.cc000066400000000000000000000300171454702036400230670ustar00rootroot00000000000000// 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. #include "colmap/estimators/generalized_pose.h" #include "colmap/estimators/bundle_adjustment.h" #include "colmap/estimators/cost_functions.h" #include "colmap/estimators/generalized_absolute_pose.h" #include "colmap/estimators/pose.h" #include "colmap/geometry/rigid3.h" #include "colmap/math/matrix.h" #include "colmap/optim/ransac.h" #include "colmap/optim/support_measurement.h" #include "colmap/scene/camera.h" #include "colmap/sensor/models.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/misc.h" #include #include namespace colmap { namespace { double ComputeMaxErrorInCamera(const std::vector& camera_idxs, const std::vector& cameras, const double max_error_px) { CHECK_GT(max_error_px, 0.0); double max_error_cam = 0.; for (const auto& camera_idx : camera_idxs) { max_error_cam += cameras[camera_idx].CamFromImgThreshold(max_error_px); } return max_error_cam / camera_idxs.size(); } bool LowerVector3d(const Eigen::Vector3d& v1, const Eigen::Vector3d& v2) { if (v1.x() < v2.x()) { return true; } else if (v1.x() == v2.x()) { if (v1.y() < v2.y()) { return true; } else if (v1.y() == v2.y()) { return v1.z() < v2.z(); } else { return false; } } else { return false; } } std::vector ComputeUniquePointIds( const std::vector& points3D) { std::vector point3D_ids(points3D.size()); std::iota(point3D_ids.begin(), point3D_ids.end(), 0); std::sort(point3D_ids.begin(), point3D_ids.end(), [&](size_t i, size_t j) { return LowerVector3d(points3D[i], points3D[j]); }); std::vector::iterator unique_it = point3D_ids.begin(); std::vector::iterator current_it = point3D_ids.begin(); std::vector unique_point3D_ids(points3D.size()); while (current_it != point3D_ids.end()) { if (!points3D[*unique_it].isApprox(points3D[*current_it], 1e-5)) { unique_it = current_it; } unique_point3D_ids[*current_it] = unique_it - point3D_ids.begin(); current_it++; } return unique_point3D_ids; } } // namespace bool EstimateGeneralizedAbsolutePose( const RANSACOptions& options, const std::vector& points2D, const std::vector& points3D, const std::vector& camera_idxs, const std::vector& cams_from_rig, const std::vector& cameras, Rigid3d* rig_from_world, size_t* num_inliers, std::vector* inlier_mask) { CHECK_EQ(points2D.size(), points3D.size()); CHECK_EQ(points2D.size(), camera_idxs.size()); CHECK_EQ(cams_from_rig.size(), cameras.size()); CHECK_GE(*std::min_element(camera_idxs.begin(), camera_idxs.end()), 0); CHECK_LT(*std::max_element(camera_idxs.begin(), camera_idxs.end()), cameras.size()); options.Check(); if (points2D.size() == 0) { return false; } std::vector rig_points2D(points2D.size()); for (size_t i = 0; i < points2D.size(); i++) { const size_t camera_idx = camera_idxs[i]; rig_points2D[i].ray_in_cam = cameras[camera_idx].CamFromImg(points2D[i]).homogeneous().normalized(); rig_points2D[i].cam_from_rig = cams_from_rig[camera_idx]; } // Associate unique ids to each 3D point. // Needed for UniqueInlierSupportMeasurer to avoid counting the same // 3D point multiple times due to FoV overlap in rig. // TODO(sarlinpe): Allow passing unique_point3D_ids as argument. const std::vector unique_point3D_ids = ComputeUniquePointIds(points3D); // Average of the errors over the cameras, weighted by the number of // correspondences RANSACOptions options_copy(options); options_copy.max_error = ComputeMaxErrorInCamera(camera_idxs, cameras, options.max_error); RANSAC ransac(options_copy); ransac.support_measurer.SetUniqueSampleIds(unique_point3D_ids); ransac.estimator.residual_type = GP3PEstimator::ResidualType::ReprojectionError; const auto report = ransac.Estimate(rig_points2D, points3D); if (!report.success) { return false; } *rig_from_world = report.model; *num_inliers = report.support.num_unique_inliers; *inlier_mask = report.inlier_mask; return true; } bool RefineGeneralizedAbsolutePose(const AbsolutePoseRefinementOptions& options, const std::vector& inlier_mask, const std::vector& points2D, const std::vector& points3D, const std::vector& camera_idxs, const std::vector& cams_from_rig, Rigid3d* rig_from_world, std::vector* cameras, Eigen::Matrix6d* rig_from_world_cov) { CHECK_EQ(points2D.size(), inlier_mask.size()); CHECK_EQ(points2D.size(), points3D.size()); CHECK_EQ(points2D.size(), camera_idxs.size()); CHECK_EQ(cams_from_rig.size(), cameras->size()); CHECK_GE(*std::min_element(camera_idxs.begin(), camera_idxs.end()), 0); CHECK_LT(*std::max_element(camera_idxs.begin(), camera_idxs.end()), cameras->size()); options.Check(); const auto loss_function = std::make_unique(options.loss_function_scale); std::vector cameras_params_data; for (size_t i = 0; i < cameras->size(); i++) { cameras_params_data.push_back(cameras->at(i).params.data()); } std::vector camera_counts(cameras->size(), 0); double* rig_from_world_rotation = rig_from_world->rotation.coeffs().data(); double* rig_from_world_translation = rig_from_world->translation.data(); std::vector points3D_copy = points3D; std::vector cams_from_rig_copy = cams_from_rig; ceres::Problem::Options problem_options; problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP; ceres::Problem problem(problem_options); for (size_t i = 0; i < points2D.size(); ++i) { // Skip outlier observations if (!inlier_mask[i]) { continue; } const size_t camera_idx = camera_idxs[i]; camera_counts[camera_idx] += 1; ceres::CostFunction* cost_function = nullptr; switch (cameras->at(camera_idx).model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = \ RigReprojErrorCostFunction::Create(points2D[i]); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem.AddResidualBlock( cost_function, loss_function.get(), cams_from_rig_copy[camera_idx].rotation.coeffs().data(), cams_from_rig_copy[camera_idx].translation.data(), rig_from_world_rotation, rig_from_world_translation, points3D_copy[i].data(), cameras_params_data[camera_idx]); problem.SetParameterBlockConstant(points3D_copy[i].data()); } if (problem.NumResiduals() > 0) { SetQuaternionManifold(&problem, rig_from_world_rotation); // Camera parameterization. for (size_t i = 0; i < cameras->size(); i++) { if (camera_counts[i] == 0) continue; Camera& camera = cameras->at(i); // We don't optimize the rig parameters (it's likely under-constrained) problem.SetParameterBlockConstant( cams_from_rig_copy[i].rotation.coeffs().data()); problem.SetParameterBlockConstant( cams_from_rig_copy[i].translation.data()); if (!options.refine_focal_length && !options.refine_extra_params) { problem.SetParameterBlockConstant(camera.params.data()); } else { // Always set the principal point as fixed. std::vector camera_params_const; const span principal_point_idxs = camera.PrincipalPointIdxs(); camera_params_const.insert(camera_params_const.end(), principal_point_idxs.begin(), principal_point_idxs.end()); if (!options.refine_focal_length) { const span focal_length_idxs = camera.FocalLengthIdxs(); camera_params_const.insert(camera_params_const.end(), focal_length_idxs.begin(), focal_length_idxs.end()); } if (!options.refine_extra_params) { const span extra_params_idxs = camera.ExtraParamsIdxs(); camera_params_const.insert(camera_params_const.end(), extra_params_idxs.begin(), extra_params_idxs.end()); } if (camera_params_const.size() == camera.params.size()) { problem.SetParameterBlockConstant(camera.params.data()); } else { SetSubsetManifold(static_cast(camera.params.size()), camera_params_const, &problem, camera.params.data()); } } } } ceres::Solver::Options solver_options; solver_options.gradient_tolerance = options.gradient_tolerance; solver_options.max_num_iterations = options.max_num_iterations; solver_options.linear_solver_type = ceres::DENSE_QR; solver_options.logging_type = ceres::LoggingType::SILENT; // The overhead of creating threads is too large. solver_options.num_threads = 1; #if CERES_VERSION_MAJOR < 2 solver_options.num_linear_solver_threads = 1; #endif // CERES_VERSION_MAJOR ceres::Solver::Summary summary; ceres::Solve(solver_options, &problem, &summary); if (options.print_summary) { PrintHeading2("Pose refinement report"); PrintSolverSummary(summary); } if (problem.NumResiduals() > 0 && rig_from_world_cov != nullptr) { ceres::Covariance::Options options; ceres::Covariance covariance(options); std::vector parameter_blocks = {rig_from_world_rotation, rig_from_world_translation}; if (!covariance.Compute(parameter_blocks, &problem)) { return false; } covariance.GetCovarianceMatrixInTangentSpace(parameter_blocks, rig_from_world_cov->data()); } return summary.IsSolutionUsable(); } } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_pose.h000066400000000000000000000105231454702036400227310ustar00rootroot00000000000000// 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. #pragma once #include "colmap/estimators/pose.h" #include "colmap/geometry/rigid3.h" #include "colmap/optim/ransac.h" #include "colmap/scene/camera.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { // Estimate generalized absolute pose from 2D-3D correspondences. // // @param options RANSAC options. // @param points2D Corresponding 2D points. // @param points3D Corresponding 3D points. // @param camera_idxs Index of the rig camera for each correspondence. // @param cams_from_rig Relative pose from rig to each camera frame. // @param cameras Cameras for which to estimate pose. // @param rig_from_world Estimated rig from world pose. // @param num_inliers Number of inliers in RANSAC. // @param inlier_mask Inlier mask for 2D-3D correspondences. // // @return Whether pose is estimated successfully. bool EstimateGeneralizedAbsolutePose( const RANSACOptions& options, const std::vector& points2D, const std::vector& points3D, const std::vector& camera_idxs, const std::vector& cams_from_rig, const std::vector& cameras, Rigid3d* rig_from_world, size_t* num_inliers, std::vector* inlier_mask); // Refine generalized absolute pose (optionally focal lengths) // from 2D-3D correspondences. // // @param options Refinement options. // @param inlier_mask Inlier mask for 2D-3D correspondences. // @param points2D Corresponding 2D points. // @param points3D Corresponding 3D points. // @param camera_idxs Index of the rig camera for each correspondence. // @param cams_from_rig Relative pose from rig to each camera frame. // @param rig_from_world Estimated rig from world pose. // @param cameras Cameras for which to estimate pose. Modified // in-place to store the estimated focal lengths. // @param rig_from_world_cov Estimated 6x6 covariance matrix of // the rotation (as axis-angle, in tangent space) // and translation terms (optional). // // @return Whether the solution is usable. bool RefineGeneralizedAbsolutePose( const AbsolutePoseRefinementOptions& options, const std::vector& inlier_mask, const std::vector& points2D, const std::vector& points3D, const std::vector& camera_idxs, const std::vector& cams_from_rig, Rigid3d* rig_from_world, std::vector* cameras, Eigen::Matrix6d* rig_from_world_cov = nullptr); } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_pose_test.cc000066400000000000000000000162751454702036400241400ustar00rootroot00000000000000// 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. #include "colmap/estimators/generalized_pose.h" #include "colmap/estimators/generalized_absolute_pose.h" #include "colmap/geometry/rigid3.h" #include "colmap/math/random.h" #include "colmap/optim/ransac.h" #include "colmap/scene/camera.h" #include "colmap/scene/reconstruction.h" #include "colmap/scene/synthetic.h" #include #include namespace colmap { namespace { struct GeneralizedCameraProblem { Rigid3d gt_rig_from_world; std::vector points2D; std::vector points3D; std::vector point3D_ids; std::vector camera_idxs; std::vector cams_from_rig; std::vector cameras; }; GeneralizedCameraProblem BuildGeneralizedCameraProblem() { Reconstruction reconstruction; SyntheticDatasetOptions synthetic_dataset_options; synthetic_dataset_options.num_cameras = 3; synthetic_dataset_options.num_images = 3; synthetic_dataset_options.num_points3D = 50; synthetic_dataset_options.point2D_stddev = 0; SynthesizeDataset(synthetic_dataset_options, &reconstruction); GeneralizedCameraProblem problem; problem.gt_rig_from_world = Rigid3d(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); for (const image_t image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); for (const auto& point2D : image.Points2D()) { if (point2D.HasPoint3D()) { problem.points2D.push_back(point2D.xy); problem.points3D.push_back( reconstruction.Point3D(point2D.point3D_id).xyz); problem.point3D_ids.push_back(point2D.point3D_id); problem.camera_idxs.push_back(problem.cameras.size()); } } problem.cameras.push_back(reconstruction.Camera(image.CameraId())); problem.cams_from_rig.push_back(image.CamFromWorld() * Inverse(problem.gt_rig_from_world)); } return problem; } TEST(EstimateGeneralizedAbsolutePose, Nominal) { GeneralizedCameraProblem problem = BuildGeneralizedCameraProblem(); const size_t num_points = problem.points2D.size(); const double gt_inlier_ratio = 0.8; const double outlier_distance = 50; const size_t gt_num_inliers = std::max(static_cast(gt_inlier_ratio * num_points), static_cast(GP3PEstimator::kMinNumSamples)); std::vector shuffled_idxs(num_points); std::iota(shuffled_idxs.begin(), shuffled_idxs.end(), 0); std::shuffle(shuffled_idxs.begin(), shuffled_idxs.end(), *PRNG); std::unordered_set unique_inlier_ids; unique_inlier_ids.reserve(gt_num_inliers); for (size_t i = 0; i < gt_num_inliers; ++i) { unique_inlier_ids.insert(problem.point3D_ids[shuffled_idxs[i]]); } std::vector gt_inlier_mask(num_points, true); for (size_t i = gt_num_inliers; i < num_points; ++i) { problem.points2D[shuffled_idxs[i]] += Eigen::Vector2d::Random().normalized() * outlier_distance; gt_inlier_mask[shuffled_idxs[i]] = false; } RANSACOptions ransac_options; ransac_options.max_error = 2; ransac_options.min_inlier_ratio = gt_inlier_ratio / 2; ransac_options.confidence = 0.99999; Rigid3d rig_from_world; size_t num_inliers; std::vector inlier_mask; EXPECT_TRUE(EstimateGeneralizedAbsolutePose(ransac_options, problem.points2D, problem.points3D, problem.camera_idxs, problem.cams_from_rig, problem.cameras, &rig_from_world, &num_inliers, &inlier_mask)); EXPECT_EQ(num_inliers, unique_inlier_ids.size()); EXPECT_EQ(inlier_mask, gt_inlier_mask); EXPECT_LT(problem.gt_rig_from_world.rotation.angularDistance( rig_from_world.rotation), 1e-6); EXPECT_LT((problem.gt_rig_from_world.translation - rig_from_world.translation) .norm(), 1e-6); } TEST(RefineGeneralizedAbsolutePose, Nominal) { GeneralizedCameraProblem problem = BuildGeneralizedCameraProblem(); const std::vector gt_inlier_mask(problem.points2D.size(), true); const double rotation_noise_degree = 1; const double translation_noise = 0.1; const Rigid3d rig_from_gt_rig(Eigen::Quaterniond(Eigen::AngleAxisd( DegToRad(rotation_noise_degree), Eigen::Vector3d::Random().normalized())), Eigen::Vector3d::Random() * translation_noise); Rigid3d rig_from_world = rig_from_gt_rig * problem.gt_rig_from_world; AbsolutePoseRefinementOptions options; options.refine_focal_length = false; options.refine_extra_params = false; EXPECT_TRUE(RefineGeneralizedAbsolutePose(options, gt_inlier_mask, problem.points2D, problem.points3D, problem.camera_idxs, problem.cams_from_rig, &rig_from_world, &problem.cameras)); EXPECT_LT(problem.gt_rig_from_world.rotation.angularDistance( rig_from_world.rotation), 1e-6); EXPECT_LT((problem.gt_rig_from_world.translation - rig_from_world.translation) .norm(), 1e-6); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_relative_pose.cc000066400000000000000000000755171454702036400250000ustar00rootroot00000000000000// 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. #include "colmap/estimators/generalized_relative_pose.h" #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/triangulation.h" #include "colmap/math/random.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include namespace colmap { namespace { void ComposePlueckerData(const Rigid3d& rig_from_cam, const Eigen::Vector3d& ray_in_cam, Eigen::Vector3d* proj_center, Eigen::Vector6d* pluecker) { const Eigen::Vector3d ray_in_rig = (rig_from_cam.rotation * ray_in_cam).normalized(); *proj_center = rig_from_cam.translation; *pluecker << ray_in_rig, rig_from_cam.translation.cross(ray_in_rig); } Eigen::Matrix3d CayleyToRotationMatrix(const Eigen::Vector3d& cayley) { const double cayley0_sqr = cayley[0] * cayley[0]; const double cayley1_sqr = cayley[1] * cayley[1]; const double cayley2_sqr = cayley[2] * cayley[2]; const double cayley01 = cayley[0] * cayley[1]; const double cayley12 = cayley[1] * cayley[2]; const double cayley02 = cayley[0] * cayley[2]; const double scale = 1 + cayley0_sqr + cayley1_sqr + cayley2_sqr; const double inv_scale = 1.0 / scale; Eigen::Matrix3d R; R(0, 0) = inv_scale * (1 + cayley0_sqr - cayley1_sqr - cayley2_sqr); R(0, 1) = inv_scale * (2 * (cayley01 - cayley[2])); R(0, 2) = inv_scale * (2 * (cayley02 + cayley[1])); R(1, 0) = inv_scale * (2 * (cayley01 + cayley[2])); R(1, 1) = inv_scale * (1 - cayley0_sqr + cayley1_sqr - cayley2_sqr); R(1, 2) = inv_scale * (2 * (cayley12 - cayley[0])); R(2, 0) = inv_scale * (2 * (cayley02 - cayley[1])); R(2, 1) = inv_scale * (2 * (cayley12 + cayley[0])); R(2, 2) = inv_scale * (1 - cayley0_sqr - cayley1_sqr + cayley2_sqr); return R; } Eigen::Vector3d RotationMatrixToCaley(const Eigen::Matrix3d& R) { const Eigen::Matrix3d C1 = R - Eigen::Matrix3d::Identity(); const Eigen::Matrix3d C2 = R + Eigen::Matrix3d::Identity(); const Eigen::Matrix3d C = C1 * C2.inverse(); return Eigen::Vector3d(-C(1, 2), C(0, 2), -C(0, 1)); } Eigen::Vector3d ComputeRotationBetweenPoints( const std::vector& plueckers1, const std::vector& plueckers2) { CHECK_EQ(plueckers1.size(), plueckers2.size()); // Compute the center of all observed points. Eigen::Vector3d points_center1 = Eigen::Vector3d::Zero(); Eigen::Vector3d points_center2 = Eigen::Vector3d::Zero(); for (size_t i = 0; i < plueckers1.size(); i++) { points_center1 += plueckers1[i].head<3>(); points_center2 += plueckers2[i].head<3>(); } points_center1 = points_center1 / plueckers1.size(); points_center2 = points_center2 / plueckers1.size(); Eigen::Matrix3d Hcross = Eigen::Matrix3d::Zero(); for (size_t i = 0; i < plueckers1.size(); i++) { const Eigen::Vector3d f1 = plueckers1[i].head<3>() - points_center1; const Eigen::Vector3d f2 = plueckers2[i].head<3>() - points_center2; Hcross += f2 * f1.transpose(); } const Eigen::JacobiSVD svd( Hcross, Eigen::ComputeFullU | Eigen::ComputeFullV); const Eigen::Matrix3d& V = svd.matrixV(); const Eigen::Matrix3d& U = svd.matrixU(); Eigen::Matrix3d R = V * U.transpose(); if (R.determinant() < 0) { Eigen::Matrix3d V_prime; V_prime.col(0) = V.col(0); V_prime.col(1) = V.col(1); V_prime.col(2) = -V.col(2); R = V_prime * U.transpose(); } return RotationMatrixToCaley(R); } Eigen::Matrix4d ComposeG(const Eigen::Matrix3d& xxF, const Eigen::Matrix3d& yyF, const Eigen::Matrix3d& zzF, const Eigen::Matrix3d& xyF, const Eigen::Matrix3d& yzF, const Eigen::Matrix3d& zxF, const Eigen::Matrix& x1P, const Eigen::Matrix& y1P, const Eigen::Matrix& z1P, const Eigen::Matrix& x2P, const Eigen::Matrix& y2P, const Eigen::Matrix& z2P, const Eigen::Matrix& m11P, const Eigen::Matrix& m12P, const Eigen::Matrix& m22P, const Eigen::Vector3d& rotation) { const Eigen::Matrix3d R = CayleyToRotationMatrix(rotation); Eigen::Matrix R_rows; R_rows << R.row(0), R.row(1), R.row(2); Eigen::Matrix R_cols; R_cols << R.col(0), R.col(1), R.col(2); const Eigen::Vector3d xxFr1t = xxF * R.row(1).transpose(); const Eigen::Vector3d yyFr0t = yyF * R.row(0).transpose(); const Eigen::Vector3d zzFr0t = zzF * R.row(0).transpose(); const Eigen::Vector3d yzFr0t = yzF * R.row(0).transpose(); const Eigen::Vector3d xyFr1t = xyF * R.row(1).transpose(); const Eigen::Vector3d xyFr2t = xyF * R.row(2).transpose(); const Eigen::Vector3d zxFr1t = zxF * R.row(1).transpose(); const Eigen::Vector3d zxFr2t = zxF * R.row(2).transpose(); const Eigen::Vector3d x1PC = x1P * R_cols; const Eigen::Vector3d y1PC = y1P * R_cols; const Eigen::Vector3d z1PC = z1P * R_cols; const Eigen::Vector3d x2PR = x2P * R_rows.transpose(); const Eigen::Vector3d y2PR = y2P * R_rows.transpose(); const Eigen::Vector3d z2PR = z2P * R_rows.transpose(); Eigen::Matrix4d G; G(0, 0) = R.row(2) * yyF * R.row(2).transpose(); G(0, 0) += -2.0 * R.row(2) * yzF * R.row(1).transpose(); G(0, 0) += R.row(1) * zzF * R.row(1).transpose(); G(0, 1) = R.row(2) * yzFr0t; G(0, 1) += -1.0 * R.row(2) * xyFr2t; G(0, 1) += -1.0 * R.row(1) * zzFr0t; G(0, 1) += R.row(1) * zxFr2t; G(0, 2) = R.row(2) * xyFr1t; G(0, 2) += -1.0 * R.row(2) * yyFr0t; G(0, 2) += -1.0 * R.row(1) * zxFr1t; G(0, 2) += R.row(1) * yzFr0t; G(1, 1) = R.row(0) * zzFr0t; G(1, 1) += -2.0 * R.row(0) * zxFr2t; G(1, 1) += R.row(2) * xxF * R.row(2).transpose(); G(1, 2) = R.row(0) * zxFr1t; G(1, 2) += -1.0 * R.row(0) * yzFr0t; G(1, 2) += -1.0 * R.row(2) * xxFr1t; G(1, 2) += R.row(0) * xyFr2t; G(2, 2) = R.row(1) * xxFr1t; G(2, 2) += -2.0 * R.row(0) * xyFr1t; G(2, 2) += R.row(0) * yyFr0t; G(1, 0) = G(0, 1); G(2, 0) = G(0, 2); G(2, 1) = G(1, 2); G(0, 3) = R.row(2) * y1PC; G(0, 3) += R.row(2) * y2PR; G(0, 3) += -1.0 * R.row(1) * z1PC; G(0, 3) += -1.0 * R.row(1) * z2PR; G(1, 3) = R.row(0) * z1PC; G(1, 3) += R.row(0) * z2PR; G(1, 3) += -1.0 * R.row(2) * x1PC; G(1, 3) += -1.0 * R.row(2) * x2PR; G(2, 3) = R.row(1) * x1PC; G(2, 3) += R.row(1) * x2PR; G(2, 3) += -1.0 * R.row(0) * y1PC; G(2, 3) += -1.0 * R.row(0) * y2PR; G(3, 3) = -1.0 * R_cols.transpose() * m11P * R_cols; G(3, 3) += -1.0 * R_rows * m22P * R_rows.transpose(); G(3, 3) += -2.0 * R_rows * m12P * R_cols; G(3, 0) = G(0, 3); G(3, 1) = G(1, 3); G(3, 2) = G(2, 3); return G; } Eigen::Vector4d ComputeEigenValue(const Eigen::Matrix3d& xxF, const Eigen::Matrix3d& yyF, const Eigen::Matrix3d& zzF, const Eigen::Matrix3d& xyF, const Eigen::Matrix3d& yzF, const Eigen::Matrix3d& zxF, const Eigen::Matrix& x1P, const Eigen::Matrix& y1P, const Eigen::Matrix& z1P, const Eigen::Matrix& x2P, const Eigen::Matrix& y2P, const Eigen::Matrix& z2P, const Eigen::Matrix& m11P, const Eigen::Matrix& m12P, const Eigen::Matrix& m22P, const Eigen::Vector3d& rotation) { const Eigen::Matrix4d G = ComposeG(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, rotation); // Compute the roots in closed-form. // const double G00_2 = G(0,0) * G(0,0); const double G01_2 = G(0, 1) * G(0, 1); const double G02_2 = G(0, 2) * G(0, 2); const double G03_2 = G(0, 3) * G(0, 3); // const double G11_2 = G(1,1) * G(1,1); const double G12_2 = G(1, 2) * G(1, 2); const double G13_2 = G(1, 3) * G(1, 3); // const double G22_2 = G(2,2) * G(2,2); const double G23_2 = G(2, 3) * G(2, 3); // const double G33_2 = G(3,3) * G(3,3); const double B = -G(3, 3) - G(2, 2) - G(1, 1) - G(0, 0); const double C = -G23_2 + G(2, 2) * G(3, 3) - G13_2 - G12_2 + G(1, 1) * G(3, 3) + G(1, 1) * G(2, 2) - G03_2 - G02_2 - G01_2 + G(0, 0) * G(3, 3) + G(0, 0) * G(2, 2) + G(0, 0) * G(1, 1); const double D = G13_2 * G(2, 2) - 2.0 * G(1, 2) * G(1, 3) * G(2, 3) + G12_2 * G(3, 3) + G(1, 1) * G23_2 - G(1, 1) * G(2, 2) * G(3, 3) + G03_2 * G(2, 2) + G03_2 * G(1, 1) - 2.0 * G(0, 2) * G(0, 3) * G(2, 3) + G02_2 * G(3, 3) + G02_2 * G(1, 1) - 2.0 * G(0, 1) * G(0, 3) * G(1, 3) - 2.0 * G(0, 1) * G(0, 2) * G(1, 2) + G01_2 * G(3, 3) + G01_2 * G(2, 2) + G(0, 0) * G23_2 - G(0, 0) * G(2, 2) * G(3, 3) + G(0, 0) * G13_2 + G(0, 0) * G12_2 - G(0, 0) * G(1, 1) * G(3, 3) - G(0, 0) * G(1, 1) * G(2, 2); const double E = G03_2 * G12_2 - G03_2 * G(1, 1) * G(2, 2) - 2.0 * G(0, 2) * G(0, 3) * G(1, 2) * G(1, 3) + 2.0 * G(0, 2) * G(0, 3) * G(1, 1) * G(2, 3) + G02_2 * G13_2 - G02_2 * G(1, 1) * G(3, 3) + 2.0 * G(0, 1) * G(0, 3) * G(1, 3) * G(2, 2) - 2.0 * G(0, 1) * G(0, 3) * G(1, 2) * G(2, 3) - 2.0 * G(0, 1) * G(0, 2) * G(1, 3) * G(2, 3) + 2.0 * G(0, 1) * G(0, 2) * G(1, 2) * G(3, 3) + G01_2 * G23_2 - G01_2 * G(2, 2) * G(3, 3) - G(0, 0) * G13_2 * G(2, 2) + 2.0 * G(0, 0) * G(1, 2) * G(1, 3) * G(2, 3) - G(0, 0) * G12_2 * G(3, 3) - G(0, 0) * G(1, 1) * G23_2 + G(0, 0) * G(1, 1) * G(2, 2) * G(3, 3); const double B_pw2 = B * B; const double B_pw3 = B_pw2 * B; const double B_pw4 = B_pw3 * B; const double alpha = -0.375 * B_pw2 + C; const double beta = B_pw3 / 8.0 - B * C / 2.0 + D; const double gamma = -0.01171875 * B_pw4 + B_pw2 * C / 16.0 - B * D / 4.0 + E; const double alpha_pw2 = alpha * alpha; const double alpha_pw3 = alpha_pw2 * alpha; const double p = -alpha_pw2 / 12.0 - gamma; const double q = -alpha_pw3 / 108.0 + alpha * gamma / 3.0 - beta * beta / 8.0; const double helper1 = -p * p * p / 27.0; const double theta2 = std::pow(helper1, (1.0 / 3.0)); const double theta1 = std::sqrt(theta2) * std::cos((1.0 / 3.0) * std::acos((-q / 2.0) / std::sqrt(helper1))); const double y = -(5.0 / 6.0) * alpha - ((1.0 / 3.0) * p * theta1 - theta1 * theta2) / theta2; const double w = std::sqrt(alpha + 2.0 * y); Eigen::Vector4d roots; roots(0) = -B / 4.0 + 0.5 * w + 0.5 * std::sqrt(-3.0 * alpha - 2.0 * y - 2.0 * beta / w); roots(1) = -B / 4.0 + 0.5 * w - 0.5 * std::sqrt(-3.0 * alpha - 2.0 * y - 2.0 * beta / w); roots(2) = -B / 4.0 - 0.5 * w + 0.5 * std::sqrt(-3.0 * alpha - 2.0 * y + 2.0 * beta / w); roots(3) = -B / 4.0 - 0.5 * w - 0.5 * std::sqrt(-3.0 * alpha - 2.0 * y + 2.0 * beta / w); return roots; } double ComputeCost(const Eigen::Matrix3d& xxF, const Eigen::Matrix3d& yyF, const Eigen::Matrix3d& zzF, const Eigen::Matrix3d& xyF, const Eigen::Matrix3d& yzF, const Eigen::Matrix3d& zxF, const Eigen::Matrix& x1P, const Eigen::Matrix& y1P, const Eigen::Matrix& z1P, const Eigen::Matrix& x2P, const Eigen::Matrix& y2P, const Eigen::Matrix& z2P, const Eigen::Matrix& m11P, const Eigen::Matrix& m12P, const Eigen::Matrix& m22P, const Eigen::Vector3d& rotation, const int step) { CHECK_GE(step, 0); CHECK_LE(step, 1); const Eigen::Vector4d roots = ComputeEigenValue(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, rotation); if (step == 0) { return roots[2]; } else if (step == 1) { return roots[3]; } return 0; } Eigen::Vector3d ComputeJacobian(const Eigen::Matrix3d& xxF, const Eigen::Matrix3d& yyF, const Eigen::Matrix3d& zzF, const Eigen::Matrix3d& xyF, const Eigen::Matrix3d& yzF, const Eigen::Matrix3d& zxF, const Eigen::Matrix& x1P, const Eigen::Matrix& y1P, const Eigen::Matrix& z1P, const Eigen::Matrix& x2P, const Eigen::Matrix& y2P, const Eigen::Matrix& z2P, const Eigen::Matrix& m11P, const Eigen::Matrix& m12P, const Eigen::Matrix& m22P, const Eigen::Vector3d& rotation, const double current_cost, const int step) { Eigen::Vector3d jacobian; const double kEpsilon = 0.00000001; for (int j = 0; j < 3; j++) { Eigen::Vector3d cayley_j = rotation; cayley_j[j] += kEpsilon; const double cost_j = ComputeCost(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, cayley_j, step); jacobian(j) = cost_j - current_cost; } return jacobian; } } // namespace void GR6PEstimator::Estimate(const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_GE(points1.size(), 6); CHECK_EQ(points1.size(), points2.size()); CHECK(models != nullptr); models->clear(); std::vector proj_centers1(points1.size()); std::vector proj_centers2(points1.size()); std::vector plueckers1(points1.size()); std::vector plueckers2(points1.size()); for (size_t i = 0; i < points1.size(); ++i) { ComposePlueckerData(Inverse(points1[i].cam_from_rig), points1[i].ray_in_cam, &proj_centers1[i], &plueckers1[i]); ComposePlueckerData(Inverse(points2[i].cam_from_rig), points2[i].ray_in_cam, &proj_centers2[i], &plueckers2[i]); } Eigen::Matrix3d xxF = Eigen::Matrix3d::Zero(); Eigen::Matrix3d yyF = Eigen::Matrix3d::Zero(); Eigen::Matrix3d zzF = Eigen::Matrix3d::Zero(); Eigen::Matrix3d xyF = Eigen::Matrix3d::Zero(); Eigen::Matrix3d yzF = Eigen::Matrix3d::Zero(); Eigen::Matrix3d zxF = Eigen::Matrix3d::Zero(); Eigen::Matrix x1P = Eigen::Matrix::Zero(); Eigen::Matrix y1P = Eigen::Matrix::Zero(); Eigen::Matrix z1P = Eigen::Matrix::Zero(); Eigen::Matrix x2P = Eigen::Matrix::Zero(); Eigen::Matrix y2P = Eigen::Matrix::Zero(); Eigen::Matrix z2P = Eigen::Matrix::Zero(); Eigen::Matrix m11P = Eigen::Matrix::Zero(); Eigen::Matrix m12P = Eigen::Matrix::Zero(); Eigen::Matrix m22P = Eigen::Matrix::Zero(); for (size_t i = 0; i < points1.size(); ++i) { const Eigen::Vector3d f1 = plueckers1[i].head<3>(); const Eigen::Vector3d f2 = plueckers2[i].head<3>(); const Eigen::Vector3d t1 = proj_centers1[i]; const Eigen::Vector3d t2 = proj_centers2[i]; const Eigen::Matrix3d F = f2 * f2.transpose(); xxF += f1[0] * f1[0] * F; yyF += f1[1] * f1[1] * F; zzF += f1[2] * f1[2] * F; xyF += f1[0] * f1[1] * F; yzF += f1[1] * f1[2] * F; zxF += f1[2] * f1[0] * F; Eigen::Matrix ff1; ff1(0) = f1[0] * (f2[1] * t2[2] - f2[2] * t2[1]); ff1(1) = f1[1] * (f2[1] * t2[2] - f2[2] * t2[1]); ff1(2) = f1[2] * (f2[1] * t2[2] - f2[2] * t2[1]); ff1(3) = f1[0] * (f2[2] * t2[0] - f2[0] * t2[2]); ff1(4) = f1[1] * (f2[2] * t2[0] - f2[0] * t2[2]); ff1(5) = f1[2] * (f2[2] * t2[0] - f2[0] * t2[2]); ff1(6) = f1[0] * (f2[0] * t2[1] - f2[1] * t2[0]); ff1(7) = f1[1] * (f2[0] * t2[1] - f2[1] * t2[0]); ff1(8) = f1[2] * (f2[0] * t2[1] - f2[1] * t2[0]); x1P += f1[0] * f2 * ff1.transpose(); y1P += f1[1] * f2 * ff1.transpose(); z1P += f1[2] * f2 * ff1.transpose(); Eigen::Matrix ff2; ff2(0) = f2[0] * (f1[1] * t1[2] - f1[2] * t1[1]); ff2(1) = f2[1] * (f1[1] * t1[2] - f1[2] * t1[1]); ff2(2) = f2[2] * (f1[1] * t1[2] - f1[2] * t1[1]); ff2(3) = f2[0] * (f1[2] * t1[0] - f1[0] * t1[2]); ff2(4) = f2[1] * (f1[2] * t1[0] - f1[0] * t1[2]); ff2(5) = f2[2] * (f1[2] * t1[0] - f1[0] * t1[2]); ff2(6) = f2[0] * (f1[0] * t1[1] - f1[1] * t1[0]); ff2(7) = f2[1] * (f1[0] * t1[1] - f1[1] * t1[0]); ff2(8) = f2[2] * (f1[0] * t1[1] - f1[1] * t1[0]); x2P += f1[0] * f2 * ff2.transpose(); y2P += f1[1] * f2 * ff2.transpose(); z2P += f1[2] * f2 * ff2.transpose(); m11P -= ff1 * ff1.transpose(); m22P -= ff2 * ff2.transpose(); m12P -= ff2 * ff1.transpose(); } const Eigen::Vector3d initial_rotation = ComputeRotationBetweenPoints(plueckers1, plueckers2); const double kMinLambda = 0.00001; const double kMaxLambda = 0.08; const double kLambdaModifier = 2.0; const int kMaxNumIterations = 50; const bool kDisableIncrements = true; double perturbation_amplitude = 0.3; int num_random_trials = 0; Eigen::Vector3d rotation; while (num_random_trials < 5) { if (num_random_trials > 2) { perturbation_amplitude = 0.6; } if (num_random_trials == 0) { rotation = initial_rotation; } else { const Eigen::Vector3d perturbation( RandomUniformReal(-perturbation_amplitude, perturbation_amplitude), RandomUniformReal(-perturbation_amplitude, perturbation_amplitude), RandomUniformReal(-perturbation_amplitude, perturbation_amplitude)); rotation = initial_rotation + perturbation; } double lambda = 0.01; int num_iterations = 0; double smallest_eigen_value = ComputeCost(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, rotation, 1); for (int iter = 0; iter < kMaxNumIterations; ++iter) { const Eigen::Vector3d jacobian = ComputeJacobian(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, rotation, smallest_eigen_value, 1); const Eigen::Vector3d normalized_jacobian = jacobian.normalized(); Eigen::Vector3d sampling_point = rotation - lambda * normalized_jacobian; double sampling_eigen_value = ComputeCost(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, sampling_point, 1); if (num_iterations == 0 || !kDisableIncrements) { while (sampling_eigen_value < smallest_eigen_value) { smallest_eigen_value = sampling_eigen_value; if (lambda * kLambdaModifier > kMaxLambda) { break; } lambda *= kLambdaModifier; sampling_point = rotation - lambda * normalized_jacobian; sampling_eigen_value = ComputeCost(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, sampling_point, 1); } } while (sampling_eigen_value > smallest_eigen_value) { lambda /= kLambdaModifier; sampling_point = rotation - lambda * normalized_jacobian; sampling_eigen_value = ComputeCost(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, sampling_point, 1); } rotation = sampling_point; smallest_eigen_value = sampling_eigen_value; if (lambda < kMinLambda) { break; } } if (rotation.norm() < 0.01) { const double eigen_value2 = ComputeCost(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, rotation, 0); if (eigen_value2 > 0.001) { num_random_trials += 1; } else { break; } } else { break; } } const Eigen::Matrix3d R = CayleyToRotationMatrix(rotation).transpose(); const Eigen::Matrix4d G = ComposeG(xxF, yyF, zzF, xyF, yzF, zxF, x1P, y1P, z1P, x2P, y2P, z2P, m11P, m12P, m22P, rotation); const Eigen::EigenSolver eigen_solver_G(G, true); const Eigen::Matrix4cd V = eigen_solver_G.eigenvectors(); const Eigen::Matrix3x4d VV = V.real().colwise().hnormalized(); models->resize(4); for (int i = 0; i < 4; ++i) { (*models)[i].rotation = Eigen::Quaterniond(R); (*models)[i].translation = -R * VV.col(i); } } void GR6PEstimator::Residuals(const std::vector& points1, const std::vector& points2, const M_t& rig2_from_rig1, std::vector* residuals) { CHECK_EQ(points1.size(), points2.size()); residuals->resize(points1.size(), 0); for (size_t i = 0; i < points1.size(); ++i) { const Rigid3d cam2_from_cam1 = points2[i].cam_from_rig * rig2_from_rig1 * Inverse(points1[i].cam_from_rig); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_cam1); const Eigen::Vector3d Ex1 = E * points1[i].ray_in_cam.hnormalized().homogeneous(); const Eigen::Vector3d x2 = points2[i].ray_in_cam.hnormalized().homogeneous(); const Eigen::Vector3d Etx2 = E.transpose() * x2; const double x2tEx1 = x2.transpose() * Ex1; (*residuals)[i] = x2tEx1 * x2tEx1 / (Ex1(0) * Ex1(0) + Ex1(1) * Ex1(1) + Etx2(0) * Etx2(0) + Etx2(1) * Etx2(1)); } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_relative_pose.h000066400000000000000000000071351454702036400246310ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Solver for the Generalized Relative Pose problem using a minimal of 8 2D-2D // correspondences. This implementation is based on: // // "Efficient Computation of Relative Pose for Multi-Camera Systems", // Kneip and Li. CVPR 2014. // // Note that the solution to this problem is degenerate in the case of pure // translation and when all correspondences are observed from the same cameras. // // The implementation is a modified and improved version of Kneip's original // implementation in OpenGV licensed under the BSD license. class GR6PEstimator { public: // The generalized image observations of the left camera, which is composed of // the relative pose of a camera in the generalized camera and a ray in the // camera frame. struct X_t { Rigid3d cam_from_rig; Eigen::Vector3d ray_in_cam; }; // The normalized image feature points in the right camera. typedef X_t Y_t; // The estimated rig2_from_rig1 relative pose between the generalized cameras. typedef Rigid3d M_t; // The minimum number of samples needed to estimate a model. Note that in // theory the minimum required number of samples is 6 but Laurent Kneip showed // in his paper that using 8 samples is more stable. static const int kMinNumSamples = 8; // Estimate the most probable solution of the GR6P problem from a set of // six 2D-2D point correspondences. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Calculate the squared Sampson error between corresponding points. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& rig2_from_rig1, std::vector* residuals); }; } // namespace colmap colmap-3.9.1/src/colmap/estimators/generalized_relative_pose_test.cc000066400000000000000000000105721454702036400260250ustar00rootroot00000000000000// 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. #include "colmap/estimators/generalized_relative_pose.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/rigid3.h" #include "colmap/optim/loransac.h" #include #include namespace colmap { namespace { TEST(GeneralizedRelativePose, Estimate) { const size_t kNumPoints = 100; std::vector points3D; for (size_t i = 0; i < kNumPoints; ++i) { points3D.emplace_back(Eigen::Vector3d::Random()); } // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double qx = 0; qx < 0.4; qx += 0.1) { // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double tx = 0; tx < 0.5; tx += 0.1) { const int kRefCamIdx = 1; const int kNumCams = 3; const std::array cams_from_world = {{ Rigid3d(Eigen::Quaterniond(1, qx, 0, 0).normalized(), Eigen::Vector3d(tx, 0.1, 0)), Rigid3d(Eigen::Quaterniond(1, qx + 0.05, 0, 0).normalized(), Eigen::Vector3d(tx, 0.2, 0)), Rigid3d(Eigen::Quaterniond(1, qx + 0.1, 0, 0).normalized(), Eigen::Vector3d(tx, 0.3, 0)), }}; std::array cams_from_rig; for (size_t i = 0; i < kNumCams; ++i) { cams_from_rig[i] = cams_from_world[i] * Inverse(cams_from_world[kRefCamIdx]); } // Project points to cameras. std::vector points1; std::vector points2; for (size_t i = 0; i < points3D.size(); ++i) { const Eigen::Vector3d point3D_camera1 = cams_from_rig[i % kNumCams] * points3D[i]; const Eigen::Vector3d point3D_camera2 = cams_from_world[(i + 1) % kNumCams] * points3D[i]; if (point3D_camera1.z() < 0 || point3D_camera2.z() < 0) { continue; } points1.emplace_back(); points1.back().cam_from_rig = cams_from_rig[i % kNumCams]; points1.back().ray_in_cam = point3D_camera1.normalized(); points2.emplace_back(); points2.back().cam_from_rig = cams_from_rig[(i + 1) % kNumCams]; points2.back().ray_in_cam = point3D_camera2.normalized(); } RANSACOptions options; options.max_error = 1e-3; LORANSAC ransac(options); const auto report = ransac.Estimate(points1, points2); EXPECT_TRUE(report.success); EXPECT_LT( (cams_from_world[kRefCamIdx].ToMatrix() - report.model.ToMatrix()) .norm(), 1e-2); std::vector residuals; GR6PEstimator::Residuals(points1, points2, report.model, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_LE(residuals[i], options.max_error); } } } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/homography_matrix.cc000066400000000000000000000115271454702036400233160ustar00rootroot00000000000000// 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. #include "colmap/estimators/homography_matrix.h" #include "colmap/estimators/utils.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include #include #include namespace colmap { void HomographyMatrixEstimator::Estimate(const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_EQ(points1.size(), points2.size()); CHECK(models != nullptr); models->clear(); const size_t N = points1.size(); // Center and normalize image points for better numerical stability. std::vector normed_points1; std::vector normed_points2; Eigen::Matrix3d normed_from_orig1; Eigen::Matrix3d normed_from_orig2; CenterAndNormalizeImagePoints(points1, &normed_points1, &normed_from_orig1); CenterAndNormalizeImagePoints(points2, &normed_points2, &normed_from_orig2); // Setup constraint matrix. Eigen::Matrix A = Eigen::MatrixXd::Zero(2 * N, 9); for (size_t i = 0, j = N; i < points1.size(); ++i, ++j) { const double s_0 = normed_points1[i](0); const double s_1 = normed_points1[i](1); const double d_0 = normed_points2[i](0); const double d_1 = normed_points2[i](1); A(i, 0) = -s_0; A(i, 1) = -s_1; A(i, 2) = -1; A(i, 6) = s_0 * d_0; A(i, 7) = s_1 * d_0; A(i, 8) = d_0; A(j, 3) = -s_0; A(j, 4) = -s_1; A(j, 5) = -1; A(j, 6) = s_0 * d_1; A(j, 7) = s_1 * d_1; A(j, 8) = d_1; } // Solve for the nullspace of the constraint matrix. Eigen::JacobiSVD> svd( A, Eigen::ComputeFullV); const Eigen::VectorXd nullspace = svd.matrixV().col(8); Eigen::Map H_t(nullspace.data()); models->resize(1); (*models)[0] = normed_from_orig2.inverse() * H_t.transpose() * normed_from_orig1; } void HomographyMatrixEstimator::Residuals(const std::vector& points1, const std::vector& points2, const M_t& H, std::vector* residuals) { CHECK_EQ(points1.size(), points2.size()); residuals->resize(points1.size()); // Note that this code might not be as nice as Eigen expressions, // but it is significantly faster in various tests. const double H_00 = H(0, 0); const double H_01 = H(0, 1); const double H_02 = H(0, 2); const double H_10 = H(1, 0); const double H_11 = H(1, 1); const double H_12 = H(1, 2); const double H_20 = H(2, 0); const double H_21 = H(2, 1); const double H_22 = H(2, 2); for (size_t i = 0; i < points1.size(); ++i) { const double s_0 = points1[i](0); const double s_1 = points1[i](1); const double d_0 = points2[i](0); const double d_1 = points2[i](1); const double pd_0 = H_00 * s_0 + H_01 * s_1 + H_02; const double pd_1 = H_10 * s_0 + H_11 * s_1 + H_12; const double pd_2 = H_20 * s_0 + H_21 * s_1 + H_22; const double inv_pd_2 = 1.0 / pd_2; const double dd_0 = d_0 - pd_0 * inv_pd_2; const double dd_1 = d_1 - pd_1 * inv_pd_2; (*residuals)[i] = dd_0 * dd_0 + dd_1 * dd_1; } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/homography_matrix.h000066400000000000000000000064341454702036400231610ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Direct linear transformation algorithm to compute the homography between // point pairs. This algorithm computes the least squares estimate for // the homography from at least 4 correspondences. class HomographyMatrixEstimator { public: typedef Eigen::Vector2d X_t; typedef Eigen::Vector2d Y_t; typedef Eigen::Matrix3d M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 4; // Estimate the projective transformation (homography). // // The number of corresponding points must be at least 4. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // // @return 3x3 homogeneous transformation matrix. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Calculate the transformation error for each corresponding point pair. // // Residuals are defined as the squared transformation error when // transforming the source to the destination coordinates. // // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param H 3x3 projective matrix. // @param residuals Output vector of residuals. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& H, std::vector* residuals); }; } // namespace colmap colmap-3.9.1/src/colmap/estimators/homography_matrix_test.cc000066400000000000000000000052061454702036400243520ustar00rootroot00000000000000// 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. #include "colmap/estimators/homography_matrix.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { namespace { TEST(HomographyMatrix, Estimate) { for (int x = 0; x < 10; ++x) { Eigen::Matrix3d H0; H0 << x, 0.2, 0.3, 30, 0.2, 0.1, 0.3, 20, 1; std::vector src; src.emplace_back(x, 0); src.emplace_back(1, 0); src.emplace_back(2, 1); src.emplace_back(10, 30); std::vector dst; for (size_t i = 0; i < 4; ++i) { const Eigen::Vector3d dsth = H0 * src[i].homogeneous(); dst.push_back(dsth.hnormalized()); } HomographyMatrixEstimator est_tform; std::vector models; est_tform.Estimate(src, dst, &models); ASSERT_EQ(models.size(), 1); std::vector residuals; est_tform.Residuals(src, dst, models[0], &residuals); for (size_t i = 0; i < 4; ++i) { EXPECT_TRUE(residuals[i] < 1e-6); } } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/pose.cc000066400000000000000000000372501454702036400205240ustar00rootroot00000000000000// 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. #include "colmap/estimators/pose.h" #include "colmap/estimators/absolute_pose.h" #include "colmap/estimators/bundle_adjustment.h" #include "colmap/estimators/cost_functions.h" #include "colmap/estimators/essential_matrix.h" #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/math/matrix.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" #include "colmap/util/threading.h" namespace colmap { namespace { typedef LORANSAC AbsolutePoseRANSAC; void EstimateAbsolutePoseKernel(const Camera& camera, const double focal_length_factor, const std::vector& points2D, const std::vector& points3D, const RANSACOptions& options, AbsolutePoseRANSAC::Report* report) { // Scale the focal length by the given factor. Camera scaled_camera = camera; for (const size_t idx : camera.FocalLengthIdxs()) { scaled_camera.params[idx] *= focal_length_factor; } // Normalize image coordinates with current camera hypothesis. std::vector points2D_in_cam(points2D.size()); for (size_t i = 0; i < points2D.size(); ++i) { points2D_in_cam[i] = scaled_camera.CamFromImg(points2D[i]); } // Estimate pose for given focal length. auto custom_options = options; custom_options.max_error = scaled_camera.CamFromImgThreshold(options.max_error); AbsolutePoseRANSAC ransac(custom_options); *report = ransac.Estimate(points2D_in_cam, points3D); } } // namespace bool EstimateAbsolutePose(const AbsolutePoseEstimationOptions& options, const std::vector& points2D, const std::vector& points3D, Rigid3d* cam_from_world, Camera* camera, size_t* num_inliers, std::vector* inlier_mask) { options.Check(); std::vector focal_length_factors; if (options.estimate_focal_length) { // Generate focal length factors using a quadratic function, // such that more samples are drawn for small focal lengths focal_length_factors.reserve(options.num_focal_length_samples + 1); const double fstep = 1.0 / options.num_focal_length_samples; const double fscale = options.max_focal_length_ratio - options.min_focal_length_ratio; double focal = 0.; for (size_t i = 0; i <= options.num_focal_length_samples; ++i, focal += fstep) { focal_length_factors.push_back(options.min_focal_length_ratio + fscale * focal * focal); } } else { focal_length_factors.reserve(1); focal_length_factors.push_back(1); } std::vector> futures; futures.resize(focal_length_factors.size()); std::vector> reports; reports.resize(focal_length_factors.size()); ThreadPool thread_pool(std::min( options.num_threads, static_cast(focal_length_factors.size()))); for (size_t i = 0; i < focal_length_factors.size(); ++i) { futures[i] = thread_pool.AddTask(EstimateAbsolutePoseKernel, *camera, focal_length_factors[i], points2D, points3D, options.ransac_options, &reports[i]); } double focal_length_factor = 0; Eigen::Matrix3x4d cam_from_world_matrix; *num_inliers = 0; inlier_mask->clear(); // Find best model among all focal lengths. for (size_t i = 0; i < focal_length_factors.size(); ++i) { futures[i].get(); const auto report = reports[i]; if (report.success && report.support.num_inliers > *num_inliers) { *num_inliers = report.support.num_inliers; cam_from_world_matrix = report.model; *inlier_mask = report.inlier_mask; focal_length_factor = focal_length_factors[i]; } } if (*num_inliers == 0) { return false; } // Scale output camera with best estimated focal length. if (options.estimate_focal_length && *num_inliers > 0) { for (const size_t idx : camera->FocalLengthIdxs()) { camera->params[idx] *= focal_length_factor; } } *cam_from_world = Rigid3d(Eigen::Quaterniond(cam_from_world_matrix.leftCols<3>()), cam_from_world_matrix.col(3)); if (cam_from_world->rotation.coeffs().array().isNaN().any() || cam_from_world->translation.array().isNaN().any()) { return false; } return true; } size_t EstimateRelativePose(const RANSACOptions& ransac_options, const std::vector& points1, const std::vector& points2, Rigid3d* cam2_from_cam1) { RANSAC ransac(ransac_options); const auto report = ransac.Estimate(points1, points2); if (!report.success) { return 0; } std::vector inliers1(report.support.num_inliers); std::vector inliers2(report.support.num_inliers); size_t j = 0; for (size_t i = 0; i < points1.size(); ++i) { if (report.inlier_mask[i]) { inliers1[j] = points1[i]; inliers2[j] = points2[i]; j += 1; } } Eigen::Matrix3d cam2_from_cam1_rot_mat; std::vector points3D; PoseFromEssentialMatrix(report.model, inliers1, inliers2, &cam2_from_cam1_rot_mat, &cam2_from_cam1->translation, &points3D); cam2_from_cam1->rotation = Eigen::Quaterniond(cam2_from_cam1_rot_mat); if (cam2_from_cam1->rotation.coeffs().array().isNaN().any() || cam2_from_cam1->translation.array().isNaN().any()) { return 0; } return points3D.size(); } bool RefineAbsolutePose(const AbsolutePoseRefinementOptions& options, const std::vector& inlier_mask, const std::vector& points2D, const std::vector& points3D, Rigid3d* cam_from_world, Camera* camera, Eigen::Matrix6d* cam_from_world_cov) { CHECK_EQ(inlier_mask.size(), points2D.size()); CHECK_EQ(points2D.size(), points3D.size()); options.Check(); const auto loss_function = std::make_unique(options.loss_function_scale); double* camera_params = camera->params.data(); double* rig_from_world_rotation = cam_from_world->rotation.coeffs().data(); double* rig_from_world_translation = cam_from_world->translation.data(); ceres::Problem::Options problem_options; problem_options.loss_function_ownership = ceres::DO_NOT_TAKE_OWNERSHIP; ceres::Problem problem(problem_options); for (size_t i = 0; i < points2D.size(); ++i) { // Skip outlier observations if (!inlier_mask[i]) { continue; } ceres::CostFunction* cost_function = nullptr; switch (camera->model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ cost_function = \ ReprojErrorConstantPoint3DCostFunction::Create( \ points2D[i], points3D[i]); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } problem.AddResidualBlock(cost_function, loss_function.get(), rig_from_world_rotation, rig_from_world_translation, camera_params); } if (problem.NumResiduals() > 0) { SetQuaternionManifold(&problem, rig_from_world_rotation); // Camera parameterization. if (!options.refine_focal_length && !options.refine_extra_params) { problem.SetParameterBlockConstant(camera->params.data()); } else { // Always set the principal point as fixed. std::vector camera_params_const; const span principal_point_idxs = camera->PrincipalPointIdxs(); camera_params_const.insert(camera_params_const.end(), principal_point_idxs.begin(), principal_point_idxs.end()); if (!options.refine_focal_length) { const span focal_length_idxs = camera->FocalLengthIdxs(); camera_params_const.insert(camera_params_const.end(), focal_length_idxs.begin(), focal_length_idxs.end()); } if (!options.refine_extra_params) { const span extra_params_idxs = camera->ExtraParamsIdxs(); camera_params_const.insert(camera_params_const.end(), extra_params_idxs.begin(), extra_params_idxs.end()); } if (camera_params_const.size() == camera->params.size()) { problem.SetParameterBlockConstant(camera->params.data()); } else { SetSubsetManifold(static_cast(camera->params.size()), camera_params_const, &problem, camera->params.data()); } } } ceres::Solver::Options solver_options; solver_options.gradient_tolerance = options.gradient_tolerance; solver_options.max_num_iterations = options.max_num_iterations; solver_options.linear_solver_type = ceres::DENSE_QR; solver_options.logging_type = ceres::LoggingType::SILENT; // The overhead of creating threads is too large. solver_options.num_threads = 1; #if CERES_VERSION_MAJOR < 2 solver_options.num_linear_solver_threads = 1; #endif // CERES_VERSION_MAJOR ceres::Solver::Summary summary; ceres::Solve(solver_options, &problem, &summary); if (options.print_summary) { PrintHeading2("Pose refinement report"); PrintSolverSummary(summary); } if (problem.NumResiduals() > 0 && cam_from_world_cov != nullptr) { ceres::Covariance::Options options; ceres::Covariance covariance(options); std::vector parameter_blocks = {rig_from_world_rotation, rig_from_world_translation}; if (!covariance.Compute(parameter_blocks, &problem)) { return false; } // The rotation covariance is estimated in the tangent space of the // quaternion, which corresponds to the 3-DoF axis-angle local // parameterization. covariance.GetCovarianceMatrixInTangentSpace(parameter_blocks, cam_from_world_cov->data()); } return summary.IsSolutionUsable(); } bool RefineRelativePose(const ceres::Solver::Options& options, const std::vector& points1, const std::vector& points2, Rigid3d* cam2_from_cam1) { CHECK_EQ(points1.size(), points2.size()); // CostFunction assumes unit quaternions. cam2_from_cam1->rotation.normalize(); double* cam2_from_cam1_rotation = cam2_from_cam1->rotation.coeffs().data(); double* cam2_from_cam1_translation = cam2_from_cam1->translation.data(); const double kMaxL2Error = 1.0; ceres::LossFunction* loss_function = new ceres::CauchyLoss(kMaxL2Error); ceres::Problem problem; for (size_t i = 0; i < points1.size(); ++i) { ceres::CostFunction* cost_function = SampsonErrorCostFunction::Create(points1[i], points2[i]); problem.AddResidualBlock(cost_function, loss_function, cam2_from_cam1_rotation, cam2_from_cam1_translation); } SetQuaternionManifold(&problem, cam2_from_cam1_rotation); SetSphereManifold<3>(&problem, cam2_from_cam1_translation); ceres::Solver::Summary summary; ceres::Solve(options, &problem, &summary); return summary.IsSolutionUsable(); } bool RefineEssentialMatrix(const ceres::Solver::Options& options, const std::vector& points1, const std::vector& points2, const std::vector& inlier_mask, Eigen::Matrix3d* E) { CHECK_EQ(points1.size(), points2.size()); CHECK_EQ(points1.size(), inlier_mask.size()); // Extract inlier points for decomposing the essential matrix into // rotation and translation components. size_t num_inliers = 0; for (const auto inlier : inlier_mask) { if (inlier) { num_inliers += 1; } } std::vector inlier_points1(num_inliers); std::vector inlier_points2(num_inliers); size_t j = 0; for (size_t i = 0; i < inlier_mask.size(); ++i) { if (inlier_mask[i]) { inlier_points1[j] = points1[i]; inlier_points2[j] = points2[i]; j += 1; } } // Extract relative pose from essential matrix. Rigid3d cam2_from_cam1; Eigen::Matrix3d cam2_from_cam1_rot_mat; std::vector points3D; PoseFromEssentialMatrix(*E, inlier_points1, inlier_points2, &cam2_from_cam1_rot_mat, &cam2_from_cam1.translation, &points3D); cam2_from_cam1.rotation = Eigen::Quaterniond(cam2_from_cam1_rot_mat); if (points3D.size() == 0) { return false; } // Refine essential matrix, use all points so that refinement is able to // consider points as inliers that were originally outliers. const bool refinement_success = RefineRelativePose( options, inlier_points1, inlier_points2, &cam2_from_cam1); if (!refinement_success) { return false; } *E = EssentialMatrixFromPose(cam2_from_cam1); return true; } } // namespace colmap colmap-3.9.1/src/colmap/estimators/pose.h000066400000000000000000000214431454702036400203630ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/optim/loransac.h" #include "colmap/scene/camera.h" #include "colmap/sensor/models.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include "colmap/util/threading.h" #include "colmap/util/types.h" #include #include #include namespace colmap { struct AbsolutePoseEstimationOptions { // Whether to estimate the focal length. bool estimate_focal_length = false; // Number of discrete samples for focal length estimation. size_t num_focal_length_samples = 30; // Minimum focal length ratio for discrete focal length sampling // around focal length of given camera. double min_focal_length_ratio = 0.2; // Maximum focal length ratio for discrete focal length sampling // around focal length of given camera. double max_focal_length_ratio = 5; // Number of threads for parallel estimation of focal length. int num_threads = ThreadPool::kMaxNumThreads; // Options used for P3P RANSAC. RANSACOptions ransac_options; void Check() const { CHECK_GT(num_focal_length_samples, 0); CHECK_GT(min_focal_length_ratio, 0); CHECK_GT(max_focal_length_ratio, 0); CHECK_LT(min_focal_length_ratio, max_focal_length_ratio); ransac_options.Check(); } }; struct AbsolutePoseRefinementOptions { // Convergence criterion. double gradient_tolerance = 1.0; // Maximum number of solver iterations. int max_num_iterations = 100; // Scaling factor determines at which residual robustification takes place. double loss_function_scale = 1.0; // Whether to refine the focal length parameter group. bool refine_focal_length = true; // Whether to refine the extra parameter group. bool refine_extra_params = true; // Whether to print final summary. bool print_summary = true; void Check() const { CHECK_GE(gradient_tolerance, 0.0); CHECK_GE(max_num_iterations, 0); CHECK_GE(loss_function_scale, 0.0); } }; // Estimate absolute pose (optionally focal length) from 2D-3D correspondences. // // Focal length estimation is performed using discrete sampling around the // focal length of the given camera. The focal length that results in the // maximal number of inliers is assigned to the given camera. // // @param options Absolute pose estimation options. // @param points2D Corresponding 2D points. // @param points3D Corresponding 3D points. // @param cam_from_world Estimated absolute camera pose. // @param camera Camera for which to estimate pose. Modified // in-place to store the estimated focal length. // @param num_inliers Number of inliers in RANSAC. // @param inlier_mask Inlier mask for 2D-3D correspondences. // // @return Whether pose is estimated successfully. bool EstimateAbsolutePose(const AbsolutePoseEstimationOptions& options, const std::vector& points2D, const std::vector& points3D, Rigid3d* cam_from_world, Camera* camera, size_t* num_inliers, std::vector* inlier_mask); // Estimate relative from 2D-2D correspondences. // // Pose of first camera is assumed to be at the origin without rotation. Pose // of second camera is given as world-to-image transformation, // i.e. `x2 = [R | t] * X2`. // // @param ransac_options RANSAC options. // @param points1 Corresponding 2D points. // @param points2 Corresponding 2D points. // @param cam2_from_cam1 Estimated pose between cameras. // // @return Number of RANSAC inliers. size_t EstimateRelativePose(const RANSACOptions& ransac_options, const std::vector& points1, const std::vector& points2, Rigid3d* cam2_from_cam1); // Refine absolute pose (optionally focal length) from 2D-3D correspondences. // // @param options Refinement options. // @param inlier_mask Inlier mask for 2D-3D correspondences. // @param points2D Corresponding 2D points. // @param points3D Corresponding 3D points. // @param cam_from_world Refined absolute camera pose. // @param camera Camera for which to estimate pose. Modified // in-place to store the estimated focal length. // @param cam_from_world_cov Estimated 6x6 covariance matrix of // the rotation (as axis-angle, in tangent space) // and translation terms (optional). // // @return Whether the solution is usable. bool RefineAbsolutePose(const AbsolutePoseRefinementOptions& options, const std::vector& inlier_mask, const std::vector& points2D, const std::vector& points3D, Rigid3d* cam_from_world, Camera* camera, Eigen::Matrix6d* cam_from_world_cov = nullptr); // Refine relative pose of two cameras. // // Minimizes the Sampson error between corresponding normalized points using // a robust cost function, i.e. the corresponding points need not necessarily // be inliers given a sufficient initial guess for the relative pose. // // Assumes that first camera pose has projection matrix P = [I | 0], and // pose of second camera is given as transformation from world to camera system. // // Assumes that the given translation vector is normalized, and refines // the translation up to an unknown scale (i.e. refined translation vector // is a unit vector again). // // @param options Solver options. // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param cam_from_world Refined pose between cameras. // // @return Flag indicating if solution is usable. bool RefineRelativePose(const ceres::Solver::Options& options, const std::vector& points1, const std::vector& points2, Rigid3d* cam_from_world); // Refine essential matrix. // // Decomposes the essential matrix into rotation and translation components // and refines the relative pose using the function `RefineRelativePose`. // // @param E 3x3 essential matrix. // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param inlier_mask Inlier mask for corresponding points. // @param options Solver options. // // @return Flag indicating if solution is usable. bool RefineEssentialMatrix(const ceres::Solver::Options& options, const std::vector& points1, const std::vector& points2, const std::vector& inlier_mask, Eigen::Matrix3d* E); } // namespace colmap colmap-3.9.1/src/colmap/estimators/pose_test.cc000066400000000000000000000064171454702036400215640ustar00rootroot00000000000000// 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. #include "colmap/estimators/pose.h" #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/rigid3.h" #include namespace colmap { namespace { TEST(RefineEssentialMatrix, Nominal) { const Rigid3d cam1_from_world; const Rigid3d cam2_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d(1, 0, 0).normalized()); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_world * Inverse(cam1_from_world)); std::vector points3D(150); for (size_t i = 0; i < points3D.size() / 3; ++i) { points3D[3 * i + 0] = Eigen::Vector3d(i * 0.01, 0, 1); points3D[3 * i + 1] = Eigen::Vector3d(0, i * 0.01, 1); points3D[3 * i + 2] = Eigen::Vector3d(i * 0.01, i * 0.01, 1); } std::vector points1(points3D.size()); std::vector points2(points3D.size()); for (size_t i = 0; i < points3D.size(); ++i) { points1[i] = (cam1_from_world * points3D[i]).hnormalized(); points2[i] = (cam2_from_world * points3D[i]).hnormalized(); } const Rigid3d cam2_from_world_perturbed( Eigen::Quaterniond::Identity(), Eigen::Vector3d(1.02, 0.02, 0.01).normalized()); const Eigen::Matrix3d E_pertubated = EssentialMatrixFromPose(cam2_from_world * Inverse(cam1_from_world)); Eigen::Matrix3d E_refined = E_pertubated; ceres::Solver::Options options; RefineEssentialMatrix(options, points1, points2, std::vector(points1.size(), true), &E_refined); EXPECT_LE((E - E_refined).norm(), (E - E_pertubated).norm()); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/similarity_transform.h000066400000000000000000000135321454702036400236760ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/sim3.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include "colmap/util/types.h" #include #include #include namespace colmap { // N-D similarity transform estimator from corresponding point pairs in the // source and destination coordinate systems. // // This algorithm is based on the following paper: // // S. Umeyama. Least-Squares Estimation of Transformation Parameters // Between Two Point Patterns. IEEE Transactions on Pattern Analysis and // Machine Intelligence, Volume 13 Issue 4, Page 376-380, 1991. // http://www.stanford.edu/class/cs273/refs/umeyama.pdf // // and uses the Eigen implementation. template class SimilarityTransformEstimator { public: typedef Eigen::Matrix X_t; typedef Eigen::Matrix Y_t; typedef Eigen::Matrix M_t; // The minimum number of samples needed to estimate a model. Note that // this only returns the true minimal sample in the two-dimensional case. // For higher dimensions, the system will alway be over-determined. static const int kMinNumSamples = kDim; // Estimate the similarity transform. // // @param src Set of corresponding source points. // @param dst Set of corresponding destination points. // // @return 4x4 homogeneous transformation matrix. static void Estimate(const std::vector& src, const std::vector& dst, std::vector* models); // Calculate the transformation error for each corresponding point pair. // // Residuals are defined as the squared transformation error when // transforming the source to the destination coordinates. // // @param src Set of corresponding points in the source coordinate // system as a Nx3 matrix. // @param dst Set of corresponding points in the destination // coordinate system as a Nx3 matrix. // @param matrix 4x4 homogeneous transformation matrix. // @param residuals Output vector of residuals for each point pair. static void Residuals(const std::vector& src, const std::vector& dst, const M_t& matrix, std::vector* residuals); }; inline bool EstimateSim3d(const std::vector& src, const std::vector& tgt, Sim3d& tgt_from_src) { std::vector models; SimilarityTransformEstimator<3, true>().Estimate(src, tgt, &models); if (models.empty()) { return false; } CHECK_EQ(models.size(), 1); tgt_from_src = Sim3d::FromMatrix(models[0]); return true; } //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template void SimilarityTransformEstimator::Estimate( const std::vector& src, const std::vector& dst, std::vector* models) { CHECK_EQ(src.size(), dst.size()); CHECK(models != nullptr); models->clear(); Eigen::Matrix src_mat(kDim, src.size()); Eigen::Matrix dst_mat(kDim, dst.size()); for (size_t i = 0; i < src.size(); ++i) { src_mat.col(i) = src[i]; dst_mat.col(i) = dst[i]; } const M_t model = Eigen::umeyama(src_mat, dst_mat, kEstimateScale) .template topLeftCorner(); if (model.array().isNaN().any()) { return; } models->resize(1); (*models)[0] = model; } template void SimilarityTransformEstimator::Residuals( const std::vector& src, const std::vector& dst, const M_t& matrix, std::vector* residuals) { CHECK_EQ(src.size(), dst.size()); residuals->resize(src.size()); for (size_t i = 0; i < src.size(); ++i) { const Y_t dst_transformed = matrix * src[i].homogeneous(); (*residuals)[i] = (dst[i] - dst_transformed).squaredNorm(); } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/similarity_transform_test.cc000066400000000000000000000057751454702036400251050ustar00rootroot00000000000000// 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. #include "colmap/estimators/similarity_transform.h" #include "colmap/geometry/sim3.h" #include "colmap/math/random.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { void TestEstimateSim3dWithNumCoords(const size_t num_coords) { const Sim3d gt_tgt_from_src(RandomUniformReal(0.1, 10), Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); std::vector src; std::vector dst; for (size_t i = 0; i < num_coords; ++i) { src.emplace_back(i, i + 2, i * i); dst.push_back(gt_tgt_from_src * src.back()); } Sim3d tgt_from_src; EXPECT_TRUE(EstimateSim3d(src, dst, tgt_from_src)); EXPECT_NEAR(gt_tgt_from_src.scale, tgt_from_src.scale, 1e-6); EXPECT_LT(gt_tgt_from_src.rotation.angularDistance(tgt_from_src.rotation), 1e-6); EXPECT_LT((gt_tgt_from_src.translation - tgt_from_src.translation).norm(), 1e-6); } TEST(Sim3d, EstimateMinimal) { TestEstimateSim3dWithNumCoords(3); } TEST(Sim3d, EstimateOverDetermined) { TestEstimateSim3dWithNumCoords(100); } TEST(Sim3d, EstimateDegenerate) { std::vector invalid_src_dst(3, Eigen::Vector3d::Zero()); Sim3d tgt_from_src; EXPECT_FALSE(EstimateSim3d(invalid_src_dst, invalid_src_dst, tgt_from_src)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/translation_transform.h000066400000000000000000000104041454702036400240410ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include "colmap/util/types.h" #include #include #include namespace colmap { // Estimate a N-D translation transformation between point pairs. template class TranslationTransformEstimator { public: typedef Eigen::Matrix X_t; typedef Eigen::Matrix Y_t; typedef Eigen::Matrix M_t; // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 1; // Estimate the 2D translation transform. // // @param points1 Set of corresponding source 2D points. // @param points2 Set of corresponding destination 2D points. // // @return Translation vector. static void Estimate(const std::vector& points1, const std::vector& points2, std::vector* models); // Calculate the squared translation error. // // @param points1 Set of corresponding source 2D points. // @param points2 Set of corresponding destination 2D points. // @param translation Translation vector. // @param residuals Output vector of residuals for each point pair. static void Residuals(const std::vector& points1, const std::vector& points2, const M_t& translation, std::vector* residuals); }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template void TranslationTransformEstimator::Estimate( const std::vector& points1, const std::vector& points2, std::vector* models) { CHECK_EQ(points1.size(), points2.size()); CHECK(models != nullptr); models->clear(); X_t mean_src = X_t::Zero(); Y_t mean_dst = Y_t::Zero(); for (size_t i = 0; i < points1.size(); ++i) { mean_src += points1[i]; mean_dst += points2[i]; } mean_src /= points1.size(); mean_dst /= points2.size(); models->resize(1); (*models)[0] = mean_dst - mean_src; } template void TranslationTransformEstimator::Residuals( const std::vector& points1, const std::vector& points2, const M_t& translation, std::vector* residuals) { CHECK_EQ(points1.size(), points2.size()); residuals->resize(points1.size()); for (size_t i = 0; i < points1.size(); ++i) { const M_t diff = points2[i] - points1[i] - translation; (*residuals)[i] = diff.squaredNorm(); } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/translation_transform_test.cc000066400000000000000000000056371454702036400252520ustar00rootroot00000000000000// 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. #include "colmap/estimators/translation_transform.h" #include "colmap/math/random.h" #include "colmap/optim/ransac.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(TranslationTransform, Estimate) { SetPRNGSeed(0); std::vector src; for (size_t i = 0; i < 100; ++i) { src.emplace_back(RandomUniformReal(-1000.0, 1000.0), RandomUniformReal(-1000.0, 1000.0)); } Eigen::Vector2d translation(RandomUniformReal(-1000.0, 1000.0), RandomUniformReal(-1000.0, 1000.0)); std::vector dst; for (size_t i = 0; i < src.size(); ++i) { dst.push_back(src[i] + translation); } std::vector models; TranslationTransformEstimator<2>::Estimate(src, dst, &models); ASSERT_EQ(models.size(), 1); const Eigen::Vector2d& estimated_translation = models[0]; EXPECT_NEAR(translation(0), estimated_translation(0), 1e-6); EXPECT_NEAR(translation(1), estimated_translation(1), 1e-6); std::vector residuals; TranslationTransformEstimator<2>::Residuals( src, dst, estimated_translation, &residuals); for (size_t i = 0; i < residuals.size(); ++i) { EXPECT_TRUE(residuals[i] < 1e-6); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/estimators/triangulation.cc000066400000000000000000000146631454702036400224410ustar00rootroot00000000000000// 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. #include "colmap/estimators/triangulation.h" #include "colmap/estimators/essential_matrix.h" #include "colmap/geometry/triangulation.h" #include "colmap/math/math.h" #include "colmap/optim/combination_sampler.h" #include "colmap/optim/loransac.h" #include "colmap/scene/projection.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include namespace colmap { void TriangulationEstimator::SetMinTriAngle(const double min_tri_angle) { CHECK_GE(min_tri_angle, 0); min_tri_angle_ = min_tri_angle; } void TriangulationEstimator::SetResidualType(const ResidualType residual_type) { residual_type_ = residual_type; } void TriangulationEstimator::Estimate(const std::vector& point_data, const std::vector& pose_data, std::vector* models) const { CHECK_GE(point_data.size(), 2); CHECK_EQ(point_data.size(), pose_data.size()); CHECK(models != nullptr); models->clear(); if (point_data.size() == 2) { // Two-view triangulation. const M_t xyz = TriangulatePoint(pose_data[0].proj_matrix, pose_data[1].proj_matrix, point_data[0].point_normalized, point_data[1].point_normalized); if (HasPointPositiveDepth(pose_data[0].proj_matrix, xyz) && HasPointPositiveDepth(pose_data[1].proj_matrix, xyz) && CalculateTriangulationAngle(pose_data[0].proj_center, pose_data[1].proj_center, xyz) >= min_tri_angle_) { models->resize(1); (*models)[0] = xyz; return; } } else { // Multi-view triangulation. std::vector proj_matrices; proj_matrices.reserve(point_data.size()); std::vector points; points.reserve(point_data.size()); for (size_t i = 0; i < point_data.size(); ++i) { proj_matrices.push_back(pose_data[i].proj_matrix); points.push_back(point_data[i].point_normalized); } const M_t xyz = TriangulateMultiViewPoint(proj_matrices, points); // Check for cheirality constraint. for (const auto& pose : pose_data) { if (!HasPointPositiveDepth(pose.proj_matrix, xyz)) { return; } } // Check for sufficient triangulation angle. for (size_t i = 0; i < pose_data.size(); ++i) { for (size_t j = 0; j < i; ++j) { const double tri_angle = CalculateTriangulationAngle( pose_data[i].proj_center, pose_data[j].proj_center, xyz); if (tri_angle >= min_tri_angle_) { models->resize(1); (*models)[0] = xyz; return; } } } } } void TriangulationEstimator::Residuals(const std::vector& point_data, const std::vector& pose_data, const M_t& xyz, std::vector* residuals) const { CHECK_EQ(point_data.size(), pose_data.size()); residuals->resize(point_data.size()); for (size_t i = 0; i < point_data.size(); ++i) { if (residual_type_ == ResidualType::REPROJECTION_ERROR) { (*residuals)[i] = CalculateSquaredReprojectionError(point_data[i].point, xyz, pose_data[i].proj_matrix, *pose_data[i].camera); } else if (residual_type_ == ResidualType::ANGULAR_ERROR) { const double angular_error = CalculateNormalizedAngularError( point_data[i].point_normalized, xyz, pose_data[i].proj_matrix); (*residuals)[i] = angular_error * angular_error; } } } bool EstimateTriangulation( const EstimateTriangulationOptions& options, const std::vector& point_data, const std::vector& pose_data, std::vector* inlier_mask, Eigen::Vector3d* xyz) { CHECK_NOTNULL(inlier_mask); CHECK_NOTNULL(xyz); CHECK_GE(point_data.size(), 2); CHECK_EQ(point_data.size(), pose_data.size()); options.Check(); // Robustly estimate track using LORANSAC. LORANSAC ransac(options.ransac_options); ransac.estimator.SetMinTriAngle(options.min_tri_angle); ransac.estimator.SetResidualType(options.residual_type); ransac.local_estimator.SetMinTriAngle(options.min_tri_angle); ransac.local_estimator.SetResidualType(options.residual_type); const auto report = ransac.Estimate(point_data, pose_data); if (!report.success) { return false; } *inlier_mask = report.inlier_mask; *xyz = report.model; return report.success; } } // namespace colmap colmap-3.9.1/src/colmap/estimators/triangulation.h000066400000000000000000000126641454702036400223020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/math/math.h" #include "colmap/optim/ransac.h" #include "colmap/scene/camera.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Triangulation estimator to estimate 3D point from multiple observations. // The triangulation must satisfy the following constraints: // - Sufficient triangulation angle between observation pairs. // - All observations must satisfy cheirality constraint. // // An observation is composed of an image measurement and the corresponding // camera pose and calibration. class TriangulationEstimator { public: enum class ResidualType { ANGULAR_ERROR, REPROJECTION_ERROR, }; struct PointData { PointData() {} PointData(const Eigen::Vector2d& point_, const Eigen::Vector2d& point_N_) : point(point_), point_normalized(point_N_) {} // Image observation in pixels. Only needs to be set for REPROJECTION_ERROR. Eigen::Vector2d point; // Normalized image observation. Must always be set. Eigen::Vector2d point_normalized; }; struct PoseData { PoseData() : camera(nullptr) {} PoseData(const Eigen::Matrix3x4d& proj_matrix_, const Eigen::Vector3d& pose_, const Camera* camera_) : proj_matrix(proj_matrix_), proj_center(pose_), camera(camera_) {} // The projection matrix for the image of the observation. Eigen::Matrix3x4d proj_matrix; // The projection center for the image of the observation. Eigen::Vector3d proj_center; // The camera for the image of the observation. const Camera* camera; }; typedef PointData X_t; typedef PoseData Y_t; typedef Eigen::Vector3d M_t; // Specify settings for triangulation estimator. void SetMinTriAngle(double min_tri_angle); void SetResidualType(ResidualType residual_type); // The minimum number of samples needed to estimate a model. static const int kMinNumSamples = 2; // Estimate a 3D point from a two-view observation. // // @param point_data Image measurement. // @param point_data Camera poses. // // @return Triangulated point if successful, otherwise none. void Estimate(const std::vector& point_data, const std::vector& pose_data, std::vector* models) const; // Calculate residuals in terms of squared reprojection or angular error. // // @param point_data Image measurements. // @param point_data Camera poses. // @param xyz 3D point. // // @return Residual for each observation. void Residuals(const std::vector& point_data, const std::vector& pose_data, const M_t& xyz, std::vector* residuals) const; private: ResidualType residual_type_ = ResidualType::REPROJECTION_ERROR; double min_tri_angle_ = 0.0; }; struct EstimateTriangulationOptions { // Minimum triangulation angle in radians. double min_tri_angle = 0.0; // The employed residual type. TriangulationEstimator::ResidualType residual_type = TriangulationEstimator::ResidualType::ANGULAR_ERROR; // RANSAC options for TriangulationEstimator. RANSACOptions ransac_options; void Check() const { CHECK_GE(min_tri_angle, 0.0); ransac_options.Check(); } }; // Robustly estimate 3D point from observations in multiple views using RANSAC // and a subsequent non-linear refinement using all inliers. Returns true // if the estimated number of inliers has more than two views. bool EstimateTriangulation( const EstimateTriangulationOptions& options, const std::vector& point_data, const std::vector& pose_data, std::vector* inlier_mask, Eigen::Vector3d* xyz); } // namespace colmap colmap-3.9.1/src/colmap/estimators/two_view_geometry.cc000066400000000000000000000603541454702036400233350ustar00rootroot00000000000000// 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. #include "colmap/estimators/two_view_geometry.h" #include "colmap/estimators/essential_matrix.h" #include "colmap/estimators/fundamental_matrix.h" #include "colmap/estimators/homography_matrix.h" #include "colmap/estimators/translation_transform.h" #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/homography_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/triangulation.h" #include "colmap/math/random.h" #include "colmap/optim/loransac.h" #include "colmap/optim/ransac.h" #include "colmap/scene/camera.h" #include namespace colmap { namespace { FeatureMatches ExtractInlierMatches(const FeatureMatches& matches, const size_t num_inliers, const std::vector& inlier_mask) { FeatureMatches inlier_matches(num_inliers); size_t j = 0; for (size_t i = 0; i < matches.size(); ++i) { if (inlier_mask[i]) { inlier_matches[j] = matches[i]; j += 1; } } return inlier_matches; } FeatureMatches ExtractOutlierMatches(const FeatureMatches& matches, const FeatureMatches& inlier_matches) { CHECK_GE(matches.size(), inlier_matches.size()); std::unordered_set> inlier_matches_set; inlier_matches_set.reserve(inlier_matches.size()); for (const auto& match : inlier_matches) { inlier_matches_set.emplace(match.point2D_idx1, match.point2D_idx2); } FeatureMatches outlier_matches; outlier_matches.reserve(matches.size() - inlier_matches.size()); for (const auto& match : matches) { if (inlier_matches_set.count( std::make_pair(match.point2D_idx1, match.point2D_idx2)) == 0) { outlier_matches.push_back(match); } } return outlier_matches; } inline bool IsImagePointInBoundingBox(const Eigen::Vector2d& point, const double minx, const double maxx, const double miny, const double maxy) { return point.x() >= minx && point.x() <= maxx && point.y() >= miny && point.y() <= maxy; } TwoViewGeometry EstimateCalibratedHomography( const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const FeatureMatches& matches, const TwoViewGeometryOptions& options) { TwoViewGeometry geometry; const size_t min_num_inliers = static_cast(options.min_num_inliers); if (matches.size() < min_num_inliers) { geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; return geometry; } // Extract corresponding points. std::vector matched_points1(matches.size()); std::vector matched_points2(matches.size()); for (size_t i = 0; i < matches.size(); ++i) { matched_points1[i] = points1[matches[i].point2D_idx1]; matched_points2[i] = points2[matches[i].point2D_idx2]; } // Estimate planar or panoramic model. LORANSAC H_ransac( options.ransac_options); const auto H_report = H_ransac.Estimate(matched_points1, matched_points2); geometry.H = H_report.model; if (!H_report.success || H_report.support.num_inliers < min_num_inliers) { geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; return geometry; } else { geometry.config = TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC; } geometry.inlier_matches = ExtractInlierMatches( matches, H_report.support.num_inliers, H_report.inlier_mask); if (options.detect_watermark && DetectWatermark(camera1, matched_points1, camera2, matched_points2, H_report.support.num_inliers, H_report.inlier_mask, options)) { geometry.config = TwoViewGeometry::ConfigurationType::WATERMARK; } if (options.compute_relative_pose) { EstimateTwoViewGeometryPose(camera1, points1, camera2, points2, &geometry); } return geometry; } TwoViewGeometry EstimateUncalibratedTwoViewGeometry( const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const FeatureMatches& matches, const TwoViewGeometryOptions& options) { TwoViewGeometry geometry; const size_t min_num_inliers = static_cast(options.min_num_inliers); if (matches.size() < static_cast(min_num_inliers)) { geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; return geometry; } // Extract corresponding points. std::vector matched_points1(matches.size()); std::vector matched_points2(matches.size()); for (size_t i = 0; i < matches.size(); ++i) { matched_points1[i] = points1[matches[i].point2D_idx1]; matched_points2[i] = points2[matches[i].point2D_idx2]; } // Estimate epipolar model. LORANSAC F_ransac(options.ransac_options); const auto F_report = F_ransac.Estimate(matched_points1, matched_points2); geometry.F = F_report.model; // Estimate planar or panoramic model. LORANSAC H_ransac( options.ransac_options); const auto H_report = H_ransac.Estimate(matched_points1, matched_points2); geometry.H = H_report.model; if ((!F_report.success && !H_report.success) || (F_report.support.num_inliers < min_num_inliers && H_report.support.num_inliers < min_num_inliers)) { geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; return geometry; } // Determine inlier ratios of different models. const double H_F_inlier_ratio = static_cast(H_report.support.num_inliers) / F_report.support.num_inliers; const std::vector* best_inlier_mask = &F_report.inlier_mask; int num_inliers = F_report.support.num_inliers; if (H_F_inlier_ratio > options.max_H_inlier_ratio) { geometry.config = TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC; if (H_report.support.num_inliers >= F_report.support.num_inliers) { num_inliers = H_report.support.num_inliers; best_inlier_mask = &H_report.inlier_mask; } } else { geometry.config = TwoViewGeometry::ConfigurationType::UNCALIBRATED; } geometry.inlier_matches = ExtractInlierMatches(matches, num_inliers, *best_inlier_mask); if (options.detect_watermark && DetectWatermark(camera1, matched_points1, camera2, matched_points2, num_inliers, *best_inlier_mask, options)) { geometry.config = TwoViewGeometry::ConfigurationType::WATERMARK; } if (options.compute_relative_pose) { EstimateTwoViewGeometryPose(camera1, points1, camera2, points2, &geometry); } return geometry; } TwoViewGeometry EstimateMultipleTwoViewGeometries( const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const FeatureMatches& matches, const TwoViewGeometryOptions& options) { FeatureMatches remaining_matches = matches; TwoViewGeometry multi_geometry; std::vector geometries; TwoViewGeometryOptions options_copy = options; // Set to false to prevent recursive calls to this function. options_copy.multiple_models = false; while (true) { TwoViewGeometry geometry = EstimateTwoViewGeometry( camera1, points1, camera2, points2, remaining_matches, options_copy); if (geometry.config == TwoViewGeometry::ConfigurationType::DEGENERATE) { break; } if (options.multiple_ignore_watermark) { if (geometry.config != TwoViewGeometry::ConfigurationType::WATERMARK) { geometries.push_back(geometry); } } else { geometries.push_back(geometry); } remaining_matches = ExtractOutlierMatches(remaining_matches, geometry.inlier_matches); } if (geometries.empty()) { multi_geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; } else if (geometries.size() == 1) { multi_geometry = geometries[0]; } else { multi_geometry.config = TwoViewGeometry::ConfigurationType::MULTIPLE; for (const auto& geometry : geometries) { multi_geometry.inlier_matches.insert(multi_geometry.inlier_matches.end(), geometry.inlier_matches.begin(), geometry.inlier_matches.end()); } } return multi_geometry; } } // namespace bool TwoViewGeometryOptions::Check() const { CHECK_OPTION_GE(min_num_inliers, 0); CHECK_OPTION_GE(min_E_F_inlier_ratio, 0); CHECK_OPTION_LE(min_E_F_inlier_ratio, 1); CHECK_OPTION_GE(max_H_inlier_ratio, 0); CHECK_OPTION_LE(max_H_inlier_ratio, 1); CHECK_OPTION_GE(watermark_min_inlier_ratio, 0); CHECK_OPTION_LE(watermark_min_inlier_ratio, 1); CHECK_OPTION_GE(watermark_border_size, 0); CHECK_OPTION_LE(watermark_border_size, 1); CHECK_OPTION_GT(ransac_options.max_error, 0); CHECK_OPTION_GE(ransac_options.min_inlier_ratio, 0); CHECK_OPTION_LE(ransac_options.min_inlier_ratio, 1); CHECK_OPTION_GE(ransac_options.confidence, 0); CHECK_OPTION_LE(ransac_options.confidence, 1); CHECK_OPTION_LE(ransac_options.min_num_trials, ransac_options.max_num_trials); return true; } TwoViewGeometry EstimateTwoViewGeometry( const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const FeatureMatches& matches, const TwoViewGeometryOptions& options) { if (options.multiple_models) { return EstimateMultipleTwoViewGeometries( camera1, points1, camera2, points2, matches, options); } else if (options.force_H_use) { return EstimateCalibratedHomography( camera1, points1, camera2, points2, matches, options); } else if (camera1.has_prior_focal_length && camera2.has_prior_focal_length) { return EstimateCalibratedTwoViewGeometry( camera1, points1, camera2, points2, matches, options); } else { return EstimateUncalibratedTwoViewGeometry( camera1, points1, camera2, points2, matches, options); } } bool EstimateTwoViewGeometryPose(const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, TwoViewGeometry* geometry) { // We need a valid epopolar geometry to estimate the relative pose. if (geometry->config != TwoViewGeometry::ConfigurationType::CALIBRATED && geometry->config != TwoViewGeometry::ConfigurationType::UNCALIBRATED && geometry->config != TwoViewGeometry::ConfigurationType::PLANAR && geometry->config != TwoViewGeometry::ConfigurationType::PANORAMIC && geometry->config != TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC) { return false; } // Extract normalized inlier points. std::vector inlier_points1_normalized; inlier_points1_normalized.reserve(geometry->inlier_matches.size()); std::vector inlier_points2_normalized; inlier_points2_normalized.reserve(geometry->inlier_matches.size()); for (const auto& match : geometry->inlier_matches) { inlier_points1_normalized.push_back( camera1.CamFromImg(points1[match.point2D_idx1])); inlier_points2_normalized.push_back( camera2.CamFromImg(points2[match.point2D_idx2])); } Eigen::Matrix3d cam2_from_cam1_rot_mat; std::vector points3D; if (geometry->config == TwoViewGeometry::ConfigurationType::CALIBRATED || geometry->config == TwoViewGeometry::ConfigurationType::UNCALIBRATED) { // Try to recover relative pose for calibrated and uncalibrated // configurations. In the uncalibrated case, this most likely leads to a // ill-defined reconstruction, but sometimes it succeeds anyways after e.g. // subsequent bundle-adjustment etc. PoseFromEssentialMatrix(geometry->E, inlier_points1_normalized, inlier_points2_normalized, &cam2_from_cam1_rot_mat, &geometry->cam2_from_cam1.translation, &points3D); } else if (geometry->config == TwoViewGeometry::ConfigurationType::PLANAR || geometry->config == TwoViewGeometry::ConfigurationType::PANORAMIC || geometry->config == TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC) { Eigen::Vector3d normal; PoseFromHomographyMatrix(geometry->H, camera1.CalibrationMatrix(), camera2.CalibrationMatrix(), inlier_points1_normalized, inlier_points2_normalized, &cam2_from_cam1_rot_mat, &geometry->cam2_from_cam1.translation, &normal, &points3D); } else { return false; } geometry->cam2_from_cam1.rotation = Eigen::Quaterniond(cam2_from_cam1_rot_mat); if (points3D.empty()) { geometry->tri_angle = 0; } else { geometry->tri_angle = Median( CalculateTriangulationAngles(Eigen::Vector3d::Zero(), -cam2_from_cam1_rot_mat.transpose() * geometry->cam2_from_cam1.translation, points3D)); } if (geometry->config == TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC) { if (geometry->cam2_from_cam1.translation.norm() == 0) { geometry->config = TwoViewGeometry::ConfigurationType::PANORAMIC; geometry->tri_angle = 0; } else { geometry->config = TwoViewGeometry::ConfigurationType::PLANAR; } } return true; } TwoViewGeometry EstimateCalibratedTwoViewGeometry( const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const FeatureMatches& matches, const TwoViewGeometryOptions& options) { CHECK(options.Check()); TwoViewGeometry geometry; const size_t min_num_inliers = static_cast(options.min_num_inliers); if (matches.size() < min_num_inliers) { geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; return geometry; } // Extract corresponding points. std::vector matched_points1(matches.size()); std::vector matched_points2(matches.size()); std::vector matched_points1_normalized(matches.size()); std::vector matched_points2_normalized(matches.size()); for (size_t i = 0; i < matches.size(); ++i) { const point2D_t idx1 = matches[i].point2D_idx1; const point2D_t idx2 = matches[i].point2D_idx2; matched_points1[i] = points1[idx1]; matched_points2[i] = points2[idx2]; matched_points1_normalized[i] = camera1.CamFromImg(points1[idx1]); matched_points2_normalized[i] = camera2.CamFromImg(points2[idx2]); } // Estimate epipolar models. auto E_ransac_options = options.ransac_options; E_ransac_options.max_error = (camera1.CamFromImgThreshold(options.ransac_options.max_error) + camera2.CamFromImgThreshold(options.ransac_options.max_error)) / 2; LORANSAC E_ransac(E_ransac_options); const auto E_report = E_ransac.Estimate(matched_points1_normalized, matched_points2_normalized); geometry.E = E_report.model; LORANSAC F_ransac(options.ransac_options); const auto F_report = F_ransac.Estimate(matched_points1, matched_points2); geometry.F = F_report.model; // Estimate planar or panoramic model. LORANSAC H_ransac( options.ransac_options); const auto H_report = H_ransac.Estimate(matched_points1, matched_points2); geometry.H = H_report.model; if ((!E_report.success && !F_report.success && !H_report.success) || (E_report.support.num_inliers < min_num_inliers && F_report.support.num_inliers < min_num_inliers && H_report.support.num_inliers < min_num_inliers)) { geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; return geometry; } // Determine inlier ratios of different models. const double E_F_inlier_ratio = static_cast(E_report.support.num_inliers) / F_report.support.num_inliers; const double H_F_inlier_ratio = static_cast(H_report.support.num_inliers) / F_report.support.num_inliers; const double H_E_inlier_ratio = static_cast(H_report.support.num_inliers) / E_report.support.num_inliers; const std::vector* best_inlier_mask = nullptr; size_t num_inliers = 0; if (E_report.success && E_F_inlier_ratio > options.min_E_F_inlier_ratio && E_report.support.num_inliers >= min_num_inliers) { // Calibrated configuration. // Always use the model with maximum matches. if (E_report.support.num_inliers >= F_report.support.num_inliers) { num_inliers = E_report.support.num_inliers; best_inlier_mask = &E_report.inlier_mask; } else { num_inliers = F_report.support.num_inliers; best_inlier_mask = &F_report.inlier_mask; } if (H_E_inlier_ratio > options.max_H_inlier_ratio) { geometry.config = TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC; if (H_report.support.num_inliers > num_inliers) { num_inliers = H_report.support.num_inliers; best_inlier_mask = &H_report.inlier_mask; } } else { geometry.config = TwoViewGeometry::ConfigurationType::CALIBRATED; } } else if (F_report.success && F_report.support.num_inliers >= min_num_inliers) { // Uncalibrated configuration. num_inliers = F_report.support.num_inliers; best_inlier_mask = &F_report.inlier_mask; if (H_F_inlier_ratio > options.max_H_inlier_ratio) { geometry.config = TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC; if (H_report.support.num_inliers > num_inliers) { num_inliers = H_report.support.num_inliers; best_inlier_mask = &H_report.inlier_mask; } } else { geometry.config = TwoViewGeometry::ConfigurationType::UNCALIBRATED; } } else if (H_report.success && H_report.support.num_inliers >= min_num_inliers) { num_inliers = H_report.support.num_inliers; best_inlier_mask = &H_report.inlier_mask; geometry.config = TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC; } else { geometry.config = TwoViewGeometry::ConfigurationType::DEGENERATE; return geometry; } if (best_inlier_mask != nullptr) { geometry.inlier_matches = ExtractInlierMatches(matches, num_inliers, *best_inlier_mask); if (options.detect_watermark && DetectWatermark(camera1, matched_points1, camera2, matched_points2, num_inliers, *best_inlier_mask, options)) { geometry.config = TwoViewGeometry::ConfigurationType::WATERMARK; } if (options.compute_relative_pose) { EstimateTwoViewGeometryPose( camera1, points1, camera2, points2, &geometry); } } return geometry; } bool DetectWatermark(const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const size_t num_inliers, const std::vector& inlier_mask, const TwoViewGeometryOptions& options) { CHECK(options.Check()); // Check if inlier points in border region and extract inlier matches. const double diagonal1 = std::sqrt(camera1.width * camera1.width + camera1.height * camera1.height); const double diagonal2 = std::sqrt(camera2.width * camera2.width + camera2.height * camera2.height); const double minx1 = options.watermark_border_size * diagonal1; const double miny1 = minx1; const double maxx1 = camera1.width - minx1; const double maxy1 = camera1.height - miny1; const double minx2 = options.watermark_border_size * diagonal2; const double miny2 = minx2; const double maxx2 = camera2.width - minx2; const double maxy2 = camera2.height - miny2; std::vector inlier_points1(num_inliers); std::vector inlier_points2(num_inliers); size_t num_matches_in_border = 0; size_t j = 0; for (size_t i = 0; i < inlier_mask.size(); ++i) { if (inlier_mask[i]) { const auto& point1 = points1[i]; const auto& point2 = points2[i]; inlier_points1[j] = point1; inlier_points2[j] = point2; j += 1; if (!IsImagePointInBoundingBox(point1, minx1, maxx1, miny1, maxy1) && !IsImagePointInBoundingBox(point2, minx2, maxx2, miny2, maxy2)) { num_matches_in_border += 1; } } } const double matches_in_border_ratio = static_cast(num_matches_in_border) / num_inliers; if (matches_in_border_ratio < options.watermark_min_inlier_ratio) { return false; } // Check if matches follow a translational model. RANSACOptions ransac_options = options.ransac_options; ransac_options.min_inlier_ratio = options.watermark_min_inlier_ratio; LORANSAC, TranslationTransformEstimator<2>> ransac(ransac_options); const auto report = ransac.Estimate(inlier_points1, inlier_points2); const double inlier_ratio = static_cast(report.support.num_inliers) / num_inliers; return inlier_ratio >= options.watermark_min_inlier_ratio; } } // namespace colmap colmap-3.9.1/src/colmap/estimators/two_view_geometry.h000066400000000000000000000166401454702036400231760ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/types.h" #include "colmap/geometry/rigid3.h" #include "colmap/optim/ransac.h" #include "colmap/scene/camera.h" #include "colmap/scene/two_view_geometry.h" #include "colmap/util/logging.h" namespace colmap { // Estimation options. struct TwoViewGeometryOptions { // Minimum number of inliers for non-degenerate two-view geometry. int min_num_inliers = 15; // In case both cameras are calibrated, the calibration is verified by // estimating an essential and fundamental matrix and comparing their // fractions of number of inliers. If the essential matrix produces // a similar number of inliers (`min_E_F_inlier_ratio * F_num_inliers`), // the calibration is assumed to be correct. double min_E_F_inlier_ratio = 0.95; // In case an epipolar geometry can be verified, it is checked whether // the geometry describes a planar scene or panoramic view (pure rotation) // described by a homography. This is a degenerate case, since epipolar // geometry is only defined for a moving camera. If the inlier ratio of // a homography comes close to the inlier ratio of the epipolar geometry, // a planar or panoramic configuration is assumed. double max_H_inlier_ratio = 0.8; // In case of valid two-view geometry, it is checked whether the geometry // describes a pure translation in the border region of the image. If more // than a certain ratio of inlier points conform with a pure image // translation, a watermark is assumed. double watermark_min_inlier_ratio = 0.7; // Watermark matches have to be in the border region of the image. The // border region is defined as the area around the image borders and // is defined as a fraction of the image diagonal. double watermark_border_size = 0.1; // Whether to enable watermark detection. A watermark causes a pure // translation in the image space with inliers in the border region. bool detect_watermark = true; // Whether to ignore watermark models in multiple model estimation. bool multiple_ignore_watermark = true; // In case the user asks for it, only going to estimate a Homography // between both cameras. bool force_H_use = false; // Whether to compute the relative pose between the two views. bool compute_relative_pose = false; // Recursively estimate multiple configurations by removing the previous set // of inliers from the matches until not enough inliers are found. Inlier // matches are concatenated and the configuration type is `MULTIPLE` if // multiple models could be estimated. This is useful to estimate the two-view // geometry for images with large distortion or multiple rigidly moving // objects in the scene. // // Note that in case the model type is `MULTIPLE`, only the `inlier_matches` // field will be initialized. bool multiple_models = false; // TwoViewGeometryOptions used to robustly estimate the geometry. RANSACOptions ransac_options; TwoViewGeometryOptions() { ransac_options.max_error = 4.0; ransac_options.confidence = 0.999; ransac_options.min_num_trials = 100; ransac_options.max_num_trials = 10000; ransac_options.min_inlier_ratio = 0.25; } bool Check() const; }; // Estimate two-view geometry from calibrated or uncalibrated image pair, // depending on whether a prior focal length is given or not. // // @param camera1 Camera of first image. // @param points1 Feature points in first image. // @param camera2 Camera of second image. // @param points2 Feature points in second image. // @param matches Feature matches between first and second image. // @param options Two-view geometry estimation options. TwoViewGeometry EstimateTwoViewGeometry( const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const FeatureMatches& matches, const TwoViewGeometryOptions& options); // Estimate relative pose for two-view geometry. // // @param camera1 Camera of first image. // @param points1 Feature points in first image. // @param camera2 Camera of second image. // @param points2 Feature points in second image. // @param matches Feature matches between first and second image. // @param options Two-view geometry estimation options. bool EstimateTwoViewGeometryPose(const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, TwoViewGeometry* geometry); // Estimate two-view geometry from calibrated image pair. // // @param camera1 Camera of first image. // @param points1 Feature points in first image. // @param camera2 Camera of second image. // @param points2 Feature points in second image. // @param matches Feature matches between first and second image. // @param options Two-view geometry estimation options. TwoViewGeometry EstimateCalibratedTwoViewGeometry( const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, const FeatureMatches& matches, const TwoViewGeometryOptions& options); // Detect if inlier matches are caused by a watermark. // A watermark causes a pure translation in the border are of the image. bool DetectWatermark(const Camera& camera1, const std::vector& points1, const Camera& camera2, const std::vector& points2, size_t num_inliers, const std::vector& inlier_mask, const TwoViewGeometryOptions& options); } // namespace colmap colmap-3.9.1/src/colmap/estimators/utils.cc000066400000000000000000000111411454702036400207050ustar00rootroot00000000000000// 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. #include "colmap/estimators/utils.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include namespace colmap { void CenterAndNormalizeImagePoints(const std::vector& points, std::vector* normed_points, Eigen::Matrix3d* normed_from_orig) { const size_t num_points = points.size(); CHECK_GT(num_points, 0); // Calculate centroid. Eigen::Vector2d centroid(0, 0); for (const Eigen::Vector2d& point : points) { centroid += point; } centroid /= num_points; // Root mean square distance to centroid of all points. double rms_mean_dist = 0; for (const Eigen::Vector2d& point : points) { rms_mean_dist += (point - centroid).squaredNorm(); } rms_mean_dist = std::sqrt(rms_mean_dist / num_points); // Compose normalization matrix. const double norm_factor = std::sqrt(2.0) / rms_mean_dist; *normed_from_orig << norm_factor, 0, -norm_factor * centroid(0), 0, norm_factor, -norm_factor * centroid(1), 0, 0, 1; // Apply normalization matrix. normed_points->resize(num_points); for (size_t i = 0; i < num_points; ++i) { (*normed_points)[i] = (*normed_from_orig * points[i].homogeneous()).hnormalized(); } } void ComputeSquaredSampsonError(const std::vector& points1, const std::vector& points2, const Eigen::Matrix3d& E, std::vector* residuals) { const size_t num_points1 = points1.size(); CHECK_EQ(num_points1, points2.size()); residuals->resize(num_points1); for (size_t i = 0; i < num_points1; ++i) { const Eigen::Vector3d epipolar_line1 = E * points1[i].homogeneous(); const Eigen::Vector3d point2_homogeneous = points2[i].homogeneous(); const double num = point2_homogeneous.dot(epipolar_line1); const Eigen::Vector4d denom(point2_homogeneous.dot(E.col(0)), point2_homogeneous.dot(E.col(1)), epipolar_line1.x(), epipolar_line1.y()); (*residuals)[i] = num * num / denom.squaredNorm(); } } void ComputeSquaredReprojectionError( const std::vector& points2D, const std::vector& points3D, const Eigen::Matrix3x4d& cam_from_world, std::vector* residuals) { const size_t num_points2D = points2D.size(); CHECK_EQ(num_points2D, points3D.size()); residuals->resize(num_points2D); for (size_t i = 0; i < num_points2D; ++i) { const Eigen::Vector3d point3D_in_cam = cam_from_world * points3D[i].homogeneous(); // Check if 3D point is in front of camera. if (point3D_in_cam.z() > std::numeric_limits::epsilon()) { (*residuals)[i] = (point3D_in_cam.hnormalized() - points2D[i]).squaredNorm(); } else { (*residuals)[i] = std::numeric_limits::max(); } } } } // namespace colmap colmap-3.9.1/src/colmap/estimators/utils.h000066400000000000000000000076161454702036400205630ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Center and normalize image points. // // The points are transformed in a two-step procedure that is expressed // as a transformation matrix. The matrix of the resulting points is usually // better conditioned than the matrix of the original points. // // Center the image points, such that the new coordinate system has its // origin at the centroid of the image points. // // Normalize the image points, such that the mean distance from the points // to the coordinate system is sqrt(2). // // @param points Image coordinates. // @param normed_points Transformed image coordinates. // @param normed_from_orig 3x3 transformation matrix. void CenterAndNormalizeImagePoints(const std::vector& points, std::vector* normed_points, Eigen::Matrix3d* normed_from_orig); // Calculate the residuals of a set of corresponding points and a given // fundamental or essential matrix. // // Residuals are defined as the squared Sampson error. // // @param points1 First set of corresponding points as Nx2 matrix. // @param points2 Second set of corresponding points as Nx2 matrix. // @param E 3x3 fundamental or essential matrix. // @param residuals Output vector of residuals. void ComputeSquaredSampsonError(const std::vector& points1, const std::vector& points2, const Eigen::Matrix3d& E, std::vector* residuals); // Calculate the squared reprojection error given a set of 2D-3D point // correspondences and a projection matrix. Returns DBL_MAX if a 3D point is // behind the given camera. // // @param points2D Normalized 2D image points. // @param points3D 3D world points. // @param proj_matrix 3x4 projection matrix. // @param residuals Output vector of residuals. void ComputeSquaredReprojectionError( const std::vector& points2D, const std::vector& points3D, const Eigen::Matrix3x4d& cam_from_world, std::vector* residuals); } // namespace colmap colmap-3.9.1/src/colmap/estimators/utils_test.cc000066400000000000000000000074441454702036400217570ustar00rootroot00000000000000// 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. #include "colmap/estimators/utils.h" #include "colmap/geometry/essential_matrix.h" #include namespace colmap { namespace { TEST(CenterAndNormalizeImagePoints, Nominal) { std::vector points; for (size_t i = 0; i < 11; ++i) { points.emplace_back(i, i); } std::vector normed_points; Eigen::Matrix3d matrix; CenterAndNormalizeImagePoints(points, &normed_points, &matrix); EXPECT_EQ(matrix(0, 0), 0.31622776601683794); EXPECT_EQ(matrix(1, 1), 0.31622776601683794); EXPECT_EQ(matrix(0, 2), -1.5811388300841898); EXPECT_EQ(matrix(1, 2), -1.5811388300841898); Eigen::Vector2d mean_point(0, 0); for (const auto& point : normed_points) { mean_point += point; } EXPECT_LT(std::abs(mean_point[0]), 1e-6); EXPECT_LT(std::abs(mean_point[1]), 1e-6); } TEST(ComputeSquaredSampsonError, Nominal) { std::vector points1; points1.emplace_back(0, 0); points1.emplace_back(0, 0); points1.emplace_back(0, 0); std::vector points2; points2.emplace_back(2, 0); points2.emplace_back(2, 1); points2.emplace_back(2, 2); const Eigen::Matrix3d E = EssentialMatrixFromPose( Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(1, 0, 0))); std::vector residuals; ComputeSquaredSampsonError(points1, points2, E, &residuals); EXPECT_EQ(residuals.size(), 3); EXPECT_EQ(residuals[0], 0); EXPECT_EQ(residuals[1], 0.5); EXPECT_EQ(residuals[2], 2); } TEST(ComputeSquaredReprojectionError, Nominal) { std::vector points2D; points2D.emplace_back(0, 0); points2D.emplace_back(0, 0); points2D.emplace_back(0, 0); std::vector points3D; points3D.emplace_back(2, 0, 1); points3D.emplace_back(2, 1, 1); points3D.emplace_back(2, 2, 1); const Rigid3d cam_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d(1, 0, 0)); std::vector residuals; ComputeSquaredReprojectionError( points2D, points3D, cam_from_world.ToMatrix(), &residuals); EXPECT_EQ(residuals.size(), 3); EXPECT_EQ(residuals[0], 9); EXPECT_EQ(residuals[1], 10); EXPECT_EQ(residuals[2], 13); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/exe/000077500000000000000000000000001454702036400156275ustar00rootroot00000000000000colmap-3.9.1/src/colmap/exe/CMakeLists.txt000066400000000000000000000052231454702036400203710ustar00rootroot00000000000000# 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. set(FOLDER_NAME "exe") if(IS_MSVC) add_compile_options("/bigobj") endif() set(OPTIONAL_LIBS) if(CUDA_ENABLED) list(APPEND OPTIONAL_LIBS colmap_util_cuda colmap_mvs_cuda ) endif() if(GUI_ENABLED) list(APPEND OPTIONAL_LIBS colmap_ui ) endif() COLMAP_ADD_LIBRARY( NAME colmap_exe SRCS feature.h feature.cc sfm.h sfm.cc model.h model.cc PUBLIC_LINK_LIBS colmap_controllers colmap_estimators colmap_geometry colmap_optim colmap_scene colmap_util PRIVATE_LINK_LIBS Boost::boost ) COLMAP_ADD_EXECUTABLE( NAME colmap_main SRCS feature.cc sfm.cc colmap.cc database.cc feature.cc gui.cc image.cc model.cc mvs.cc sfm.cc vocab_tree.cc LINK_LIBS colmap_controllers colmap_retrieval colmap_scene colmap_sfm colmap_util ${OPTIONAL_LIBS} Boost::boost ) set_target_properties(colmap_main PROPERTIES OUTPUT_NAME colmap) colmap-3.9.1/src/colmap/exe/colmap.cc000066400000000000000000000174411454702036400174200ustar00rootroot00000000000000// 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. #include "colmap/exe/database.h" #include "colmap/exe/feature.h" #include "colmap/exe/gui.h" #include "colmap/exe/image.h" #include "colmap/exe/model.h" #include "colmap/exe/mvs.h" #include "colmap/exe/sfm.h" #include "colmap/exe/vocab_tree.h" #include "colmap/util/version.h" namespace { typedef std::function command_func_t; int ShowHelp( const std::vector>& commands) { std::cout << colmap::StringPrintf( "%s -- Structure-from-Motion and Multi-View Stereo\n(%s)", colmap::GetVersionInfo().c_str(), colmap::GetBuildInfo().c_str()) << std::endl << std::endl; std::cout << "Usage:" << std::endl; std::cout << " colmap [command] [options]" << std::endl << std::endl; std::cout << "Documentation:" << std::endl; std::cout << " https://colmap.github.io/" << std::endl << std::endl; std::cout << "Example usage:" << std::endl; std::cout << " colmap help [ -h, --help ]" << std::endl; std::cout << " colmap gui" << std::endl; std::cout << " colmap gui -h [ --help ]" << std::endl; std::cout << " colmap automatic_reconstructor -h [ --help ]" << std::endl; std::cout << " colmap automatic_reconstructor --image_path IMAGES " "--workspace_path WORKSPACE" << std::endl; std::cout << " colmap feature_extractor --image_path IMAGES --database_path " "DATABASE" << std::endl; std::cout << " colmap exhaustive_matcher --database_path DATABASE" << std::endl; std::cout << " colmap mapper --image_path IMAGES --database_path DATABASE " "--output_path MODEL" << std::endl; std::cout << " ..." << std::endl << std::endl; std::cout << "Available commands:" << std::endl; std::cout << " help" << std::endl; for (const auto& command : commands) { std::cout << " " << command.first << std::endl; } std::cout << std::endl; return EXIT_SUCCESS; } } // namespace int main(int argc, char** argv) { colmap::InitializeGlog(argv); #if defined(COLMAP_GUI_ENABLED) Q_INIT_RESOURCE(resources); #endif std::vector> commands; commands.emplace_back("gui", &colmap::RunGraphicalUserInterface); commands.emplace_back("automatic_reconstructor", &colmap::RunAutomaticReconstructor); commands.emplace_back("bundle_adjuster", &colmap::RunBundleAdjuster); commands.emplace_back("color_extractor", &colmap::RunColorExtractor); commands.emplace_back("database_cleaner", &colmap::RunDatabaseCleaner); commands.emplace_back("database_creator", &colmap::RunDatabaseCreator); commands.emplace_back("database_merger", &colmap::RunDatabaseMerger); commands.emplace_back("delaunay_mesher", &colmap::RunDelaunayMesher); commands.emplace_back("exhaustive_matcher", &colmap::RunExhaustiveMatcher); commands.emplace_back("feature_extractor", &colmap::RunFeatureExtractor); commands.emplace_back("feature_importer", &colmap::RunFeatureImporter); commands.emplace_back("hierarchical_mapper", &colmap::RunHierarchicalMapper); commands.emplace_back("image_deleter", &colmap::RunImageDeleter); commands.emplace_back("image_filterer", &colmap::RunImageFilterer); commands.emplace_back("image_rectifier", &colmap::RunImageRectifier); commands.emplace_back("image_registrator", &colmap::RunImageRegistrator); commands.emplace_back("image_undistorter", &colmap::RunImageUndistorter); commands.emplace_back("image_undistorter_standalone", &colmap::RunImageUndistorterStandalone); commands.emplace_back("mapper", &colmap::RunMapper); commands.emplace_back("matches_importer", &colmap::RunMatchesImporter); commands.emplace_back("model_aligner", &colmap::RunModelAligner); commands.emplace_back("model_analyzer", &colmap::RunModelAnalyzer); commands.emplace_back("model_comparer", &colmap::RunModelComparer); commands.emplace_back("model_converter", &colmap::RunModelConverter); commands.emplace_back("model_cropper", &colmap::RunModelCropper); commands.emplace_back("model_merger", &colmap::RunModelMerger); commands.emplace_back("model_orientation_aligner", &colmap::RunModelOrientationAligner); commands.emplace_back("model_splitter", &colmap::RunModelSplitter); commands.emplace_back("model_transformer", &colmap::RunModelTransformer); commands.emplace_back("patch_match_stereo", &colmap::RunPatchMatchStereo); commands.emplace_back("point_filtering", &colmap::RunPointFiltering); commands.emplace_back("point_triangulator", &colmap::RunPointTriangulator); commands.emplace_back("poisson_mesher", &colmap::RunPoissonMesher); commands.emplace_back("project_generator", &colmap::RunProjectGenerator); commands.emplace_back("rig_bundle_adjuster", &colmap::RunRigBundleAdjuster); commands.emplace_back("sequential_matcher", &colmap::RunSequentialMatcher); commands.emplace_back("spatial_matcher", &colmap::RunSpatialMatcher); commands.emplace_back("stereo_fusion", &colmap::RunStereoFuser); commands.emplace_back("transitive_matcher", &colmap::RunTransitiveMatcher); commands.emplace_back("vocab_tree_builder", &colmap::RunVocabTreeBuilder); commands.emplace_back("vocab_tree_matcher", &colmap::RunVocabTreeMatcher); commands.emplace_back("vocab_tree_retriever", &colmap::RunVocabTreeRetriever); if (argc == 1) { return ShowHelp(commands); } const std::string command = argv[1]; if (command == "help" || command == "-h" || command == "--help") { return ShowHelp(commands); } else { command_func_t matched_command_func = nullptr; for (const auto& command_func : commands) { if (command == command_func.first) { matched_command_func = command_func.second; break; } } if (matched_command_func == nullptr) { LOG(ERROR) << colmap::StringPrintf( "Command `%s` not recognized. To list the " "available commands, run `colmap help`.", command.c_str()); return EXIT_FAILURE; } else { int command_argc = argc - 1; char** command_argv = &argv[1]; command_argv[0] = argv[0]; return matched_command_func(command_argc, command_argv); } } return ShowHelp(commands); } colmap-3.9.1/src/colmap/exe/database.cc000066400000000000000000000076521454702036400177140ustar00rootroot00000000000000// 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. #include "colmap/exe/database.h" #include "colmap/controllers/option_manager.h" #include "colmap/scene/database.h" #include "colmap/util/misc.h" namespace colmap { int RunDatabaseCleaner(int argc, char** argv) { std::string type; OptionManager options; options.AddRequiredOption("type", &type, "{all, images, features, matches}"); options.AddDatabaseOptions(); options.Parse(argc, argv); StringToLower(&type); Database database(*options.database_path); PrintHeading1("Clearing database"); { DatabaseTransaction transaction(&database); if (type == "all") { PrintHeading2("Clearing all tables"); database.ClearAllTables(); } else if (type == "images") { PrintHeading2("Clearing Images and all dependent tables"); database.ClearImages(); database.ClearTwoViewGeometries(); database.ClearMatches(); } else if (type == "features") { PrintHeading2("Clearing image features and matches"); database.ClearDescriptors(); database.ClearKeypoints(); database.ClearTwoViewGeometries(); database.ClearMatches(); } else if (type == "matches") { PrintHeading2("Clearing image matches"); database.ClearTwoViewGeometries(); database.ClearMatches(); } else { LOG(ERROR) << "Invalid cleanup type; no changes in database"; return EXIT_FAILURE; } } return EXIT_SUCCESS; } int RunDatabaseCreator(int argc, char** argv) { OptionManager options; options.AddDatabaseOptions(); options.Parse(argc, argv); Database database(*options.database_path); return EXIT_SUCCESS; } int RunDatabaseMerger(int argc, char** argv) { std::string database_path1; std::string database_path2; std::string merged_database_path; OptionManager options; options.AddRequiredOption("database_path1", &database_path1); options.AddRequiredOption("database_path2", &database_path2); options.AddRequiredOption("merged_database_path", &merged_database_path); options.Parse(argc, argv); if (ExistsFile(merged_database_path)) { LOG(ERROR) << "Merged database file must not exist."; return EXIT_FAILURE; } Database database1(database_path1); Database database2(database_path2); Database merged_database(merged_database_path); Database::Merge(database1, database2, &merged_database); return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/database.h000066400000000000000000000034301454702036400175440ustar00rootroot00000000000000// 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. namespace colmap { int RunDatabaseCleaner(int argc, char** argv); int RunDatabaseCreator(int argc, char** argv); int RunDatabaseMerger(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/exe/feature.cc000066400000000000000000000326351454702036400176020ustar00rootroot00000000000000// 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. #include "colmap/exe/feature.h" #include "colmap/controllers/feature_extraction.h" #include "colmap/controllers/feature_matching.h" #include "colmap/controllers/image_reader.h" #include "colmap/controllers/option_manager.h" #include "colmap/exe/gui.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" #include "colmap/util/opengl_utils.h" namespace colmap { bool VerifyCameraParams(const std::string& camera_model, const std::string& params) { if (!ExistsCameraModelWithName(camera_model)) { LOG(ERROR) << "Camera model does not exist"; return false; } const std::vector camera_params = CSVToVector(params); const CameraModelId camera_model_id = CameraModelNameToId(camera_model); if (camera_params.size() > 0 && !CameraModelVerifyParams(camera_model_id, camera_params)) { LOG(ERROR) << "Invalid camera parameters"; return false; } return true; } bool VerifySiftGPUParams(const bool use_gpu) { #if !defined(COLMAP_GPU_ENABLED) if (use_gpu) { LOG(ERROR) << "Cannot use Sift GPU without CUDA or OpenGL support; " "set SiftExtraction.use_gpu or SiftMatching.use_gpu to false."; return false; } #endif return true; } void UpdateImageReaderOptionsFromCameraMode(ImageReaderOptions& options, CameraMode mode) { switch (mode) { case CameraMode::AUTO: options.single_camera = false; options.single_camera_per_folder = false; options.single_camera_per_image = false; break; case CameraMode::SINGLE: options.single_camera = true; options.single_camera_per_folder = false; options.single_camera_per_image = false; break; case CameraMode::PER_FOLDER: options.single_camera = false; options.single_camera_per_folder = true; options.single_camera_per_image = false; break; case CameraMode::PER_IMAGE: options.single_camera = false; options.single_camera_per_folder = false; options.single_camera_per_image = true; break; } } int RunFeatureExtractor(int argc, char** argv) { std::string image_list_path; int camera_mode = -1; std::string descriptor_normalization = "l1_root"; OptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); options.AddDefaultOption("camera_mode", &camera_mode); options.AddDefaultOption("image_list_path", &image_list_path); options.AddDefaultOption("descriptor_normalization", &descriptor_normalization, "{'l1_root', 'l2'}"); options.AddExtractionOptions(); options.Parse(argc, argv); ImageReaderOptions reader_options = *options.image_reader; reader_options.database_path = *options.database_path; reader_options.image_path = *options.image_path; if (camera_mode >= 0) { UpdateImageReaderOptionsFromCameraMode(reader_options, (CameraMode)camera_mode); } StringToLower(&descriptor_normalization); if (descriptor_normalization == "l1_root") { options.sift_extraction->normalization = SiftExtractionOptions::Normalization::L1_ROOT; } else if (descriptor_normalization == "l2") { options.sift_extraction->normalization = SiftExtractionOptions::Normalization::L2; } else { LOG(ERROR) << "Invalid `descriptor_normalization`"; return EXIT_FAILURE; } if (!image_list_path.empty()) { reader_options.image_list = ReadTextFileLines(image_list_path); if (reader_options.image_list.empty()) { return EXIT_SUCCESS; } } if (!ExistsCameraModelWithName(reader_options.camera_model)) { LOG(ERROR) << "Camera model does not exist"; } if (!VerifyCameraParams(reader_options.camera_model, reader_options.camera_params)) { return EXIT_FAILURE; } if (!VerifySiftGPUParams(options.sift_extraction->use_gpu)) { return EXIT_FAILURE; } std::unique_ptr app; if (options.sift_extraction->use_gpu && kUseOpenGL) { app.reset(new QApplication(argc, argv)); } auto feature_extractor = CreateFeatureExtractorController( reader_options, *options.sift_extraction); if (options.sift_extraction->use_gpu && kUseOpenGL) { RunThreadWithOpenGLContext(feature_extractor.get()); } else { feature_extractor->Start(); feature_extractor->Wait(); } return EXIT_SUCCESS; } int RunFeatureImporter(int argc, char** argv) { std::string import_path; std::string image_list_path; int camera_mode = -1; OptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); options.AddDefaultOption("camera_mode", &camera_mode); options.AddRequiredOption("import_path", &import_path); options.AddDefaultOption("image_list_path", &image_list_path); options.AddExtractionOptions(); options.Parse(argc, argv); ImageReaderOptions reader_options = *options.image_reader; reader_options.database_path = *options.database_path; reader_options.image_path = *options.image_path; if (camera_mode >= 0) { UpdateImageReaderOptionsFromCameraMode(reader_options, (CameraMode)camera_mode); } if (!image_list_path.empty()) { reader_options.image_list = ReadTextFileLines(image_list_path); if (reader_options.image_list.empty()) { return EXIT_SUCCESS; } } if (!VerifyCameraParams(reader_options.camera_model, reader_options.camera_params)) { return EXIT_FAILURE; } auto feature_importer = CreateFeatureImporterController(reader_options, import_path); feature_importer->Start(); feature_importer->Wait(); return EXIT_SUCCESS; } int RunExhaustiveMatcher(int argc, char** argv) { OptionManager options; options.AddDatabaseOptions(); options.AddExhaustiveMatchingOptions(); options.Parse(argc, argv); if (!VerifySiftGPUParams(options.sift_matching->use_gpu)) { return EXIT_FAILURE; } std::unique_ptr app; if (options.sift_matching->use_gpu && kUseOpenGL) { app.reset(new QApplication(argc, argv)); } auto matcher = CreateExhaustiveFeatureMatcher(*options.exhaustive_matching, *options.sift_matching, *options.two_view_geometry, *options.database_path); if (options.sift_matching->use_gpu && kUseOpenGL) { RunThreadWithOpenGLContext(matcher.get()); } else { matcher->Start(); matcher->Wait(); } return EXIT_SUCCESS; } int RunMatchesImporter(int argc, char** argv) { std::string match_list_path; std::string match_type = "pairs"; OptionManager options; options.AddDatabaseOptions(); options.AddRequiredOption("match_list_path", &match_list_path); options.AddDefaultOption( "match_type", &match_type, "{'pairs', 'raw', 'inliers'}"); options.AddMatchingOptions(); options.Parse(argc, argv); if (!VerifySiftGPUParams(options.sift_matching->use_gpu)) { return EXIT_FAILURE; } std::unique_ptr app; if (options.sift_matching->use_gpu && kUseOpenGL) { app.reset(new QApplication(argc, argv)); } std::unique_ptr matcher; if (match_type == "pairs") { ImagePairsMatchingOptions matcher_options; matcher_options.match_list_path = match_list_path; matcher = CreateImagePairsFeatureMatcher(matcher_options, *options.sift_matching, *options.two_view_geometry, *options.database_path); } else if (match_type == "raw" || match_type == "inliers") { FeaturePairsMatchingOptions matcher_options; matcher_options.match_list_path = match_list_path; matcher_options.verify_matches = match_type == "raw"; matcher = CreateFeaturePairsFeatureMatcher(matcher_options, *options.sift_matching, *options.two_view_geometry, *options.database_path); } else { LOG(ERROR) << "Invalid `match_type`"; return EXIT_FAILURE; } if (options.sift_matching->use_gpu && kUseOpenGL) { RunThreadWithOpenGLContext(matcher.get()); } else { matcher->Start(); matcher->Wait(); } return EXIT_SUCCESS; } int RunSequentialMatcher(int argc, char** argv) { OptionManager options; options.AddDatabaseOptions(); options.AddSequentialMatchingOptions(); options.Parse(argc, argv); if (!VerifySiftGPUParams(options.sift_matching->use_gpu)) { return EXIT_FAILURE; } std::unique_ptr app; if (options.sift_matching->use_gpu && kUseOpenGL) { app.reset(new QApplication(argc, argv)); } auto matcher = CreateSequentialFeatureMatcher(*options.sequential_matching, *options.sift_matching, *options.two_view_geometry, *options.database_path); if (options.sift_matching->use_gpu && kUseOpenGL) { RunThreadWithOpenGLContext(matcher.get()); } else { matcher->Start(); matcher->Wait(); } return EXIT_SUCCESS; } int RunSpatialMatcher(int argc, char** argv) { OptionManager options; options.AddDatabaseOptions(); options.AddSpatialMatchingOptions(); options.Parse(argc, argv); if (!VerifySiftGPUParams(options.sift_matching->use_gpu)) { return EXIT_FAILURE; } std::unique_ptr app; if (options.sift_matching->use_gpu && kUseOpenGL) { app.reset(new QApplication(argc, argv)); } auto matcher = CreateSpatialFeatureMatcher(*options.spatial_matching, *options.sift_matching, *options.two_view_geometry, *options.database_path); if (options.sift_matching->use_gpu && kUseOpenGL) { RunThreadWithOpenGLContext(matcher.get()); } else { matcher->Start(); matcher->Wait(); } return EXIT_SUCCESS; } int RunTransitiveMatcher(int argc, char** argv) { OptionManager options; options.AddDatabaseOptions(); options.AddTransitiveMatchingOptions(); options.Parse(argc, argv); if (!VerifySiftGPUParams(options.sift_matching->use_gpu)) { return EXIT_FAILURE; } std::unique_ptr app; if (options.sift_matching->use_gpu && kUseOpenGL) { app.reset(new QApplication(argc, argv)); } auto matcher = CreateTransitiveFeatureMatcher(*options.transitive_matching, *options.sift_matching, *options.two_view_geometry, *options.database_path); if (options.sift_matching->use_gpu && kUseOpenGL) { RunThreadWithOpenGLContext(matcher.get()); } else { matcher->Start(); matcher->Wait(); } return EXIT_SUCCESS; } int RunVocabTreeMatcher(int argc, char** argv) { OptionManager options; options.AddDatabaseOptions(); options.AddVocabTreeMatchingOptions(); options.Parse(argc, argv); if (!VerifySiftGPUParams(options.sift_matching->use_gpu)) { return EXIT_FAILURE; } std::unique_ptr app; if (options.sift_matching->use_gpu && kUseOpenGL) { app.reset(new QApplication(argc, argv)); } auto matcher = CreateVocabTreeFeatureMatcher(*options.vocab_tree_matching, *options.sift_matching, *options.two_view_geometry, *options.database_path); if (options.sift_matching->use_gpu && kUseOpenGL) { RunThreadWithOpenGLContext(matcher.get()); } else { matcher->Start(); matcher->Wait(); } return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/feature.h000066400000000000000000000101171454702036400174330ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/image_reader.h" namespace colmap { // This enum can be used as optional input for feature_extractor and // feature_importer to ensure that the camera flags of ImageReader are set in an // exclusive and unambigous way. The table below explains the corespondence of // each setting with the flags // // ----------------------------------------------------------------------------------- // | | ImageReaderOptions | | CameraMode | // single_camera | single_camera_per_folder | single_camera_per_image | // |------------|---------------|--------------------------|-------------------------| // | AUTO | false | false | false | | SINGLE | // true | false | false | | // PER_FOLDER | false | true | false | | PER_IMAGE // | false | false | true | // ----------------------------------------------------------------------------------- // // Note: When using AUTO mode a camera model will be uniquely identified by the // following 5 parameters from EXIF tags: // 1. Camera Make // 2. Camera Model // 3. Focal Length // 4. Image Width // 5. Image Height // // If any of the tags is missing then a camera model is considered invalid and a // new camera is created similar to the PER_IMAGE mode. // // If these considered fields are not sufficient to uniquely identify a camera // then using the AUTO mode will lead to incorrect setup for the cameras, e.g. // the same camera is used with same focal length but different principal point // between captures. In these cases it is recommended to either use the // PER_FOLDER or PER_IMAGE settings. enum class CameraMode { AUTO = 0, SINGLE = 1, PER_FOLDER = 2, PER_IMAGE = 3 }; void UpdateImageReaderOptionsFromCameraMode(ImageReaderOptions& options, CameraMode mode); bool VerifySiftGPUParams(bool use_gpu); bool VerifyCameraParams(const std::string& camera_model, const std::string& params); int RunFeatureExtractor(int argc, char** argv); int RunFeatureImporter(int argc, char** argv); int RunExhaustiveMatcher(int argc, char** argv); int RunMatchesImporter(int argc, char** argv); int RunSequentialMatcher(int argc, char** argv); int RunSpatialMatcher(int argc, char** argv); int RunTransitiveMatcher(int argc, char** argv); int RunVocabTreeMatcher(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/exe/gui.cc000066400000000000000000000066011454702036400167250ustar00rootroot00000000000000// 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. #include "colmap/exe/gui.h" #include "colmap/controllers/option_manager.h" #include "colmap/util/opengl_utils.h" namespace colmap { int RunGraphicalUserInterface(int argc, char** argv) { #if !defined(COLMAP_GUI_ENABLED) LOG(ERROR) << "Cannot start colmap GUI; colmap was built without GUI " "support or QT dependency is missing."; return EXIT_FAILURE; #else colmap::OptionManager options; std::string import_path; if (argc > 1) { options.AddDefaultOption("import_path", &import_path); options.AddAllOptions(); options.Parse(argc, argv); } #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif QApplication app(argc, argv); colmap::MainWindow main_window(options); main_window.show(); if (!import_path.empty()) { main_window.ImportReconstruction(import_path); } return app.exec(); #endif } int RunProjectGenerator(int argc, char** argv) { std::string output_path; std::string quality = "high"; OptionManager options; options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("quality", &quality, "{low, medium, high, extreme}"); options.Parse(argc, argv); OptionManager output_options; output_options.AddAllOptions(); StringToLower(&quality); if (quality == "low") { output_options.ModifyForLowQuality(); } else if (quality == "medium") { output_options.ModifyForMediumQuality(); } else if (quality == "high") { output_options.ModifyForHighQuality(); } else if (quality == "extreme") { output_options.ModifyForExtremeQuality(); } else { LOG(FATAL) << "Invalid quality provided"; } output_options.Write(output_path); return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/gui.h000066400000000000000000000041701454702036400165660ustar00rootroot00000000000000// 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. #include #if defined(COLMAP_GUI_ENABLED) #include "colmap/ui/main_window.h" #include #else // Dummy QApplication class when GUI is disabled class QApplication { public: QApplication(int argc, char** argv) {} }; #endif namespace colmap { #if defined(COLMAP_CUDA_ENABLED) || !defined(COLMAP_GUI_ENABLED) const bool kUseOpenGL = false; #else const bool kUseOpenGL = true; #endif int RunGraphicalUserInterface(int argc, char** argv); int RunProjectGenerator(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/exe/image.cc000066400000000000000000000417311454702036400172260ustar00rootroot00000000000000// 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. #include "colmap/exe/image.h" #include "colmap/controllers/incremental_mapper.h" #include "colmap/controllers/option_manager.h" #include "colmap/image/undistortion.h" #include "colmap/scene/reconstruction.h" #include "colmap/sfm/incremental_mapper.h" #include "colmap/util/misc.h" namespace colmap { namespace { // Read stereo image pair names from a text file. The text file is expected to // have one image pair per line, e.g.: // // image_name1.jpg image_name2.jpg // image_name3.jpg image_name4.jpg // image_name5.jpg image_name6.jpg // ... // std::vector> ReadStereoImagePairs( const std::string& path, const Reconstruction& reconstruction) { const std::vector stereo_pair_lines = ReadTextFileLines(path); std::vector> stereo_pairs; stereo_pairs.reserve(stereo_pair_lines.size()); for (const auto& line : stereo_pair_lines) { const std::vector names = StringSplit(line, " "); CHECK_EQ(names.size(), 2); const Image* image1 = reconstruction.FindImageWithName(names[0]); const Image* image2 = reconstruction.FindImageWithName(names[1]); CHECK_NOTNULL(image1); CHECK_NOTNULL(image2); stereo_pairs.emplace_back(image1->ImageId(), image2->ImageId()); } return stereo_pairs; } } // namespace int RunImageDeleter(int argc, char** argv) { std::string input_path; std::string output_path; std::string image_ids_path; std::string image_names_path; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption( "image_ids_path", &image_ids_path, "Path to text file containing one image_id to delete per line"); options.AddDefaultOption( "image_names_path", &image_names_path, "Path to text file containing one image name to delete per line"); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(input_path); if (!image_ids_path.empty()) { const auto image_ids = ReadTextFileLines(image_ids_path); for (const auto& image_id_str : image_ids) { if (image_id_str.empty()) { continue; } const image_t image_id = std::stoi(image_id_str); if (reconstruction.ExistsImage(image_id)) { const auto& image = reconstruction.Image(image_id); LOG(INFO) << StringPrintf( "Deleting image_id=%d, image_name=%s from reconstruction", image.ImageId(), image.Name().c_str()); reconstruction.DeRegisterImage(image_id); } else { LOG(WARNING) << StringPrintf( "Skipping image_id=%s, because it does not " "exist in the reconstruction", image_id_str.c_str()); } } } if (!image_names_path.empty()) { const auto image_names = ReadTextFileLines(image_names_path); for (const auto& image_name : image_names) { if (image_name.empty()) { continue; } const Image* image = reconstruction.FindImageWithName(image_name); if (image != nullptr) { LOG(INFO) << StringPrintf( "Deleting image_id=%d, image_name=%s from reconstruction", image->ImageId(), image->Name().c_str()); reconstruction.DeRegisterImage(image->ImageId()); } else { LOG(WARNING) << StringPrintf( "Skipping image_name=%s, because it does not " "exist in the reconstruction", image_name.c_str()); } } } reconstruction.Write(output_path); return EXIT_SUCCESS; } int RunImageFilterer(int argc, char** argv) { std::string input_path; std::string output_path; double min_focal_length_ratio = 0.1; double max_focal_length_ratio = 10.0; double max_extra_param = 100.0; size_t min_num_observations = 10; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("min_focal_length_ratio", &min_focal_length_ratio); options.AddDefaultOption("max_focal_length_ratio", &max_focal_length_ratio); options.AddDefaultOption("max_extra_param", &max_extra_param); options.AddDefaultOption("min_num_observations", &min_num_observations); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(input_path); const size_t num_reg_images = reconstruction.NumRegImages(); reconstruction.FilterImages( min_focal_length_ratio, max_focal_length_ratio, max_extra_param); std::vector filtered_image_ids; for (const auto& image : reconstruction.Images()) { if (image.second.IsRegistered() && image.second.NumPoints3D() < min_num_observations) { filtered_image_ids.push_back(image.first); } } for (const auto image_id : filtered_image_ids) { reconstruction.DeRegisterImage(image_id); } const size_t num_filtered_images = num_reg_images - reconstruction.NumRegImages(); LOG(INFO) << StringPrintf("Filtered %d images from a total of %d images", num_filtered_images, num_reg_images); reconstruction.Write(output_path); return EXIT_SUCCESS; } int RunImageRectifier(int argc, char** argv) { std::string input_path; std::string output_path; std::string stereo_pairs_list; UndistortCameraOptions undistort_camera_options; OptionManager options; options.AddImageOptions(); options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddRequiredOption("stereo_pairs_list", &stereo_pairs_list); options.AddDefaultOption("blank_pixels", &undistort_camera_options.blank_pixels); options.AddDefaultOption("min_scale", &undistort_camera_options.min_scale); options.AddDefaultOption("max_scale", &undistort_camera_options.max_scale); options.AddDefaultOption("max_image_size", &undistort_camera_options.max_image_size); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(input_path); const auto stereo_pairs = ReadStereoImagePairs(stereo_pairs_list, reconstruction); StereoImageRectifier rectifier(undistort_camera_options, reconstruction, *options.image_path, output_path, stereo_pairs); rectifier.Start(); rectifier.Wait(); return EXIT_SUCCESS; } int RunImageRegistrator(int argc, char** argv) { std::string input_path; std::string output_path; OptionManager options; options.AddDatabaseOptions(); options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddMapperOptions(); options.Parse(argc, argv); if (!ExistsDir(input_path)) { LOG(ERROR) << "`input_path` is not a directory"; return EXIT_FAILURE; } if (!ExistsDir(output_path)) { LOG(ERROR) << "`output_path` is not a directory"; return EXIT_FAILURE; } PrintHeading1("Loading database"); std::shared_ptr database_cache; { Timer timer; timer.Start(); const size_t min_num_matches = static_cast(options.mapper->min_num_matches); database_cache = DatabaseCache::Create(Database(*options.database_path), min_num_matches, options.mapper->ignore_watermarks, options.mapper->image_names); timer.PrintMinutes(); } auto reconstruction = std::make_shared(); reconstruction->Read(input_path); IncrementalMapper mapper(database_cache); mapper.BeginReconstruction(reconstruction); const auto mapper_options = options.mapper->Mapper(); for (const auto& image : reconstruction->Images()) { if (image.second.IsRegistered()) { continue; } PrintHeading1("Registering image #" + std::to_string(image.first) + " (" + std::to_string(reconstruction->NumRegImages() + 1) + ")"); LOG(INFO) << "\n=> Image sees " << image.second.NumVisiblePoints3D() << " / " << image.second.NumObservations() << " points"; mapper.RegisterNextImage(mapper_options, image.first); } mapper.EndReconstruction(/*discard=*/false); reconstruction->Write(output_path); return EXIT_SUCCESS; } int RunImageUndistorter(int argc, char** argv) { std::string input_path; std::string output_path; std::string output_type = "COLMAP"; std::string image_list_path; std::string copy_policy = "copy"; int num_patch_match_src_images = 20; CopyType copy_type; UndistortCameraOptions undistort_camera_options; OptionManager options; options.AddImageOptions(); options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption( "output_type", &output_type, "{COLMAP, PMVS, CMP-MVS}"); options.AddDefaultOption("image_list_path", &image_list_path); options.AddDefaultOption( "copy_policy", ©_policy, "{copy, soft-link, hard-link}"); options.AddDefaultOption("num_patch_match_src_images", &num_patch_match_src_images); options.AddDefaultOption("blank_pixels", &undistort_camera_options.blank_pixels); options.AddDefaultOption("min_scale", &undistort_camera_options.min_scale); options.AddDefaultOption("max_scale", &undistort_camera_options.max_scale); options.AddDefaultOption("max_image_size", &undistort_camera_options.max_image_size); options.AddDefaultOption("roi_min_x", &undistort_camera_options.roi_min_x); options.AddDefaultOption("roi_min_y", &undistort_camera_options.roi_min_y); options.AddDefaultOption("roi_max_x", &undistort_camera_options.roi_max_x); options.AddDefaultOption("roi_max_y", &undistort_camera_options.roi_max_y); options.Parse(argc, argv); CreateDirIfNotExists(output_path); PrintHeading1("Reading reconstruction"); Reconstruction reconstruction; reconstruction.Read(input_path); LOG(INFO) << StringPrintf("=> Reconstruction with %d images and %d points", reconstruction.NumImages(), reconstruction.NumPoints3D()); std::vector image_ids; if (!image_list_path.empty()) { const auto& image_names = ReadTextFileLines(image_list_path); for (const auto& image_name : image_names) { const Image* image = reconstruction.FindImageWithName(image_name); if (image != nullptr) { image_ids.push_back(image->ImageId()); } else { LOG(WARNING) << "Cannot find image " << image_name; } } } StringToLower(©_policy); if (copy_policy == "copy") { copy_type = CopyType::COPY; } else if (copy_policy == "soft-link") { copy_type = CopyType::SOFT_LINK; } else if (copy_policy == "hard-link") { copy_type = CopyType::HARD_LINK; } else { LOG(ERROR) << "Invalid `copy_policy` - supported values are " "{'copy', 'soft-link', 'hard-link'}."; return EXIT_FAILURE; } std::unique_ptr undistorter; if (output_type == "COLMAP") { undistorter = std::make_unique(undistort_camera_options, reconstruction, *options.image_path, output_path, num_patch_match_src_images, copy_type, image_ids); } else if (output_type == "PMVS") { undistorter = std::make_unique(undistort_camera_options, reconstruction, *options.image_path, output_path); } else if (output_type == "CMP-MVS") { undistorter = std::make_unique(undistort_camera_options, reconstruction, *options.image_path, output_path); } else { LOG(ERROR) << "Invalid `output_type` - supported values are " "{'COLMAP', 'PMVS', 'CMP-MVS'}."; return EXIT_FAILURE; } undistorter->Start(); undistorter->Wait(); return EXIT_SUCCESS; } int RunImageUndistorterStandalone(int argc, char** argv) { std::string input_file; std::string output_path; UndistortCameraOptions undistort_camera_options; OptionManager options; options.AddImageOptions(); options.AddRequiredOption("input_file", &input_file); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("blank_pixels", &undistort_camera_options.blank_pixels); options.AddDefaultOption("min_scale", &undistort_camera_options.min_scale); options.AddDefaultOption("max_scale", &undistort_camera_options.max_scale); options.AddDefaultOption("max_image_size", &undistort_camera_options.max_image_size); options.AddDefaultOption("roi_min_x", &undistort_camera_options.roi_min_x); options.AddDefaultOption("roi_min_y", &undistort_camera_options.roi_min_y); options.AddDefaultOption("roi_max_x", &undistort_camera_options.roi_max_x); options.AddDefaultOption("roi_max_y", &undistort_camera_options.roi_max_y); options.Parse(argc, argv); CreateDirIfNotExists(output_path); // Loads a text file containing the image names and camera information. // The format of the text file is // image_name CAMERA_MODEL camera_params std::vector> image_names_and_cameras; { std::ifstream file(input_file); CHECK(file.is_open()) << input_file; std::string line; std::vector lines; while (std::getline(file, line)) { StringTrim(&line); if (line.empty()) { continue; } std::string item; std::stringstream line_stream(line); // Loads the image name. std::string image_name; std::getline(line_stream, image_name, ' '); // Loads the camera and its parameters struct Camera camera; std::getline(line_stream, item, ' '); camera.model_id = CameraModelNameToId(item); if (camera.model_id == CameraModelId::kInvalid) { LOG(ERROR) << "Camera model " << item << " does not exist"; return EXIT_FAILURE; } std::getline(line_stream, item, ' '); camera.width = std::stoll(item); std::getline(line_stream, item, ' '); camera.height = std::stoll(item); camera.params.reserve(CameraModelNumParams(camera.model_id)); while (!line_stream.eof()) { std::getline(line_stream, item, ' '); camera.params.push_back(std::stold(item)); } CHECK(camera.VerifyParams()); image_names_and_cameras.emplace_back(image_name, camera); } } std::unique_ptr undistorter; undistorter.reset(new PureImageUndistorter(undistort_camera_options, *options.image_path, output_path, image_names_and_cameras)); undistorter->Start(); undistorter->Wait(); return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/image.h000066400000000000000000000036551454702036400170730ustar00rootroot00000000000000// 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. namespace colmap { int RunImageDeleter(int argc, char** argv); int RunImageFilterer(int argc, char** argv); int RunImageRectifier(int argc, char** argv); int RunImageRegistrator(int argc, char** argv); int RunImageUndistorter(int argc, char** argv); int RunImageUndistorterStandalone(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/exe/model.cc000066400000000000000000001143721454702036400172460ustar00rootroot00000000000000// 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. #include "colmap/exe/model.h" #include "colmap/controllers/option_manager.h" #include "colmap/estimators/alignment.h" #include "colmap/estimators/coordinate_frame.h" #include "colmap/geometry/gps.h" #include "colmap/geometry/pose.h" #include "colmap/optim/ransac.h" #include "colmap/util/misc.h" #include "colmap/util/threading.h" namespace colmap { namespace { std::vector> ComputeEqualPartsBounds(const Reconstruction& reconstruction, const Eigen::Vector3i& split) { std::vector> bounds; const auto bbox = reconstruction.ComputeBoundingBox(); const Eigen::Vector3d extent = bbox.second - bbox.first; const Eigen::Vector3d offset( extent(0) / split(0), extent(1) / split(1), extent(2) / split(2)); for (int k = 0; k < split(2); ++k) { for (int j = 0; j < split(1); ++j) { for (int i = 0; i < split(0); ++i) { Eigen::Vector3d min_bound(bbox.first(0) + i * offset(0), bbox.first(1) + j * offset(1), bbox.first(2) + k * offset(2)); bounds.emplace_back(min_bound, min_bound + offset); } } } return bounds; } Eigen::Vector3d TransformLatLonAltToModelCoords(const Sim3d& tform, const double lat, const double lon, const double alt) { // Since this is intended for use in ENU aligned models we want to define the // altitude along the ENU frame z axis and not the Earth's radius. Thus, we // set the altitude to 0 when converting from LLA to ECEF and then we use the // altitude at the end, after scaling, to set it as the z coordinate in the // ENU frame. Eigen::Vector3d xyz = tform * GPSTransform(GPSTransform::WGS84) .EllToXYZ({Eigen::Vector3d(lat, lon, 0.0)})[0]; xyz(2) = tform.scale * alt; return xyz; } void WriteBoundingBox(const std::string& reconstruction_path, const std::pair& bounds, const std::string& suffix = "") { const Eigen::Vector3d extent = bounds.second - bounds.first; // write axis-aligned bounding box { const std::string path = JoinPaths(reconstruction_path, "bbox_aligned" + suffix + ".txt"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; // Ensure that we don't loose any precision by storing in text. file.precision(17); file << bounds.first.transpose() << "\n"; file << bounds.second.transpose() << "\n"; } // write oriented bounding box { const std::string path = JoinPaths(reconstruction_path, "bbox_oriented" + suffix + ".txt"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; // Ensure that we don't loose any precision by storing in text. file.precision(17); const Eigen::Vector3d center = (bounds.first + bounds.second) * 0.5; file << center.transpose() << "\n\n"; file << "1 0 0\n0 1 0\n0 0 1\n\n"; file << extent.transpose() << "\n"; } } std::vector ConvertCameraLocations( const bool ref_is_gps, const std::string& alignment_type, const std::vector& ref_locations) { if (ref_is_gps) { const GPSTransform gps_transform(GPSTransform::WGS84); if (alignment_type != "enu") { LOG(INFO) << "\nConverting Alignment Coordinates from GPS (lat/lon/alt) " "to ECEF.\n"; return gps_transform.EllToXYZ(ref_locations); } else { LOG(INFO) << "\nConverting Alignment Coordinates from GPS (lat/lon/alt) " "to ENU.\n"; return gps_transform.EllToENU( ref_locations, ref_locations[0](0), ref_locations[0](1)); } } else { LOG(INFO) << "\nCartesian Alignment Coordinates extracted (MUST NOT BE " "GPS coords!).\n"; return ref_locations; } } void ReadFileCameraLocations(const std::string& ref_images_path, const bool ref_is_gps, const std::string& alignment_type, std::vector* ref_image_names, std::vector* ref_locations) { for (const auto& line : ReadTextFileLines(ref_images_path)) { std::stringstream line_parser(line); std::string image_name; Eigen::Vector3d camera_position; line_parser >> image_name >> camera_position[0] >> camera_position[1] >> camera_position[2]; ref_image_names->push_back(image_name); ref_locations->push_back(camera_position); } *ref_locations = ConvertCameraLocations(ref_is_gps, alignment_type, *ref_locations); } void ReadDatabaseCameraLocations(const std::string& database_path, const bool ref_is_gps, const std::string& alignment_type, std::vector* ref_image_names, std::vector* ref_locations) { Database database(database_path); for (const auto& image : database.ReadAllImages()) { if (image.CamFromWorldPrior().translation.array().isFinite().all()) { ref_image_names->push_back(image.Name()); ref_locations->push_back(image.CamFromWorldPrior().translation); } } *ref_locations = ConvertCameraLocations(ref_is_gps, alignment_type, *ref_locations); } void WriteComparisonErrorsCSV(const std::string& path, const std::vector& errors) { std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file.precision(17); file << "# Model comparison pose errors: one entry per common image\n"; file << "# , \n"; for (size_t i = 0; i < errors.size(); ++i) { file << errors[i].rotation_error_deg << ", " << errors[i].proj_center_error << "\n"; } } void PrintErrorStats(std::ostream& out, std::vector& vals) { const size_t len = vals.size(); if (len == 0) { out << "Cannot extract error statistics from empty input\n"; return; } std::sort(vals.begin(), vals.end()); out << "Min: " << vals.front() << "\n"; out << "Max: " << vals.back() << "\n"; out << "Mean: " << Mean(vals) << "\n"; out << "Median: " << Median(vals) << "\n"; out << "P90: " << vals[size_t(0.9 * len)] << "\n"; out << "P99: " << vals[size_t(0.99 * len)] << "\n"; } void PrintComparisonSummary(std::ostream& out, const std::vector& errors) { std::vector rotation_errors_deg; rotation_errors_deg.reserve(errors.size()); std::vector proj_center_errors; proj_center_errors.reserve(errors.size()); for (const auto& error : errors) { rotation_errors_deg.push_back(error.rotation_error_deg); proj_center_errors.push_back(error.proj_center_error); } out << "\nRotation errors (degrees)\n"; PrintErrorStats(out, rotation_errors_deg); out << "\nProjection center errors\n"; PrintErrorStats(out, proj_center_errors); } } // namespace // Align given reconstruction with user provided cameras positions // (can be used for geo-registration for instance). // The cameras positions to be used for aligning the reconstruction // model must be provided either by a txt file (with each line being: img_name x // y z) or through a colmap database file containing a prior position for the // registered images. // // Required Options: // - input_path: path to initial reconstruction model // - output_path: path to store the aligned reconstruction model // // Additional Options: // - database_path: path to database file with prior positions for // reconstruction images // - ref_images_path: path to txt file with prior positions for reconstruction // images (WARNING: provide only one of the above) // - ref_is_gps: if true the prior positions are converted from GPS // (lat/lon/alt) to ECEF or ENU // - merge_image_and_ref_origins: if true the reconstuction will be shifted so // that the first prior position is used for its camera position // - transform_path: path to store the Sim3 transformation used for the // alignment // - alignment_type: // > plane: align with reconstruction principal plane // > ecef: align with ecef coords. (requires gps coords. or user provided // ecef coords.) // > enu: align with enu coords. (requires gps coords. or user provided enu // coords.) // > enu-plane: align to ecef and then to enu plane (requires gps // coords. or user provided ecef coords.) // > enu-plane-unscaled: same as above but do not apply the computed // scale when aligning the reconstruction // > custom: align to provided coords. // - min_common_images: minimum number of images with prior positions to perform // the estimate an alignment // - estimate_scale: if true apply the computed scale when aligning the // reconstruction // - robust_alignment: if true use a ransac-based estimation for robust // alignment // - robust_alignment_max_error: ransac error to use if robust alignment is // enabled int RunModelAligner(int argc, char** argv) { std::string input_path; std::string output_path; std::string database_path; std::string ref_images_path; bool ref_is_gps = true; bool merge_origins = false; std::string transform_path; std::string alignment_type = "custom"; int min_common_images = 3; RANSACOptions ransac_options; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("database_path", &database_path); options.AddDefaultOption("ref_images_path", &ref_images_path); options.AddDefaultOption("ref_is_gps", &ref_is_gps); options.AddDefaultOption("merge_image_and_ref_origins", &merge_origins); options.AddDefaultOption("transform_path", &transform_path); options.AddDefaultOption( "alignment_type", &alignment_type, "{plane, ecef, enu, enu-plane, enu-plane-unscaled, custom}"); options.AddDefaultOption("min_common_images", &min_common_images); options.AddDefaultOption("alignment_max_error", &ransac_options.max_error); options.Parse(argc, argv); StringToLower(&alignment_type); const std::unordered_set alignment_options{ "plane", "ecef", "enu", "enu-plane", "enu-plane-unscaled", "custom"}; if (alignment_options.count(alignment_type) == 0) { LOG(ERROR) << "Invalid `alignment_type` - supported values are " "{'plane', 'ecef', 'enu', 'enu-plane', 'enu-plane-unscaled', " "'custom'}"; return EXIT_FAILURE; } if (ransac_options.max_error <= 0) { LOG(ERROR) << "You must provide a maximum alignment error > 0"; return EXIT_FAILURE; } if (alignment_type != "plane" && database_path.empty() && ref_images_path.empty()) { LOG(ERROR) << "Location alignment requires either database or " "location file path."; return EXIT_FAILURE; } std::vector ref_image_names; std::vector ref_locations; if (!ref_images_path.empty() && database_path.empty()) { ReadFileCameraLocations(ref_images_path, ref_is_gps, alignment_type, &ref_image_names, &ref_locations); } else if (!database_path.empty() && ref_images_path.empty()) { ReadDatabaseCameraLocations(database_path, ref_is_gps, alignment_type, &ref_image_names, &ref_locations); } else if (alignment_type != "plane") { LOG(ERROR) << "Use location file or database, not both"; return EXIT_FAILURE; } if (alignment_type != "plane" && static_cast(ref_locations.size()) < min_common_images) { LOG(ERROR) << "Cannot align with insufficient reference locations."; return EXIT_FAILURE; } Reconstruction reconstruction; reconstruction.Read(input_path); Sim3d tform; bool alignment_success = true; if (alignment_type == "plane") { PrintHeading2("Aligning reconstruction to principal plane"); AlignToPrincipalPlane(&reconstruction, &tform); } else { PrintHeading2("Aligning reconstruction to " + alignment_type); LOG(INFO) << StringPrintf("=> Using %d reference images", ref_image_names.size()); const bool alignment_success = AlignReconstructionToLocations(reconstruction, ref_image_names, ref_locations, min_common_images, ransac_options, &tform); std::vector errors; errors.reserve(ref_image_names.size()); for (size_t i = 0; i < ref_image_names.size(); ++i) { const Image* image = reconstruction.FindImageWithName(ref_image_names[i]); if (image != nullptr) { errors.push_back((image->ProjectionCenter() - ref_locations[i]).norm()); } } LOG(INFO) << StringPrintf("=> Alignment error: %f (mean), %f (median)", Mean(errors), Median(errors)); if (alignment_success && StringStartsWith(alignment_type, "enu-plane")) { PrintHeading2("Aligning ECEF aligned reconstruction to ENU plane"); AlignToENUPlane( &reconstruction, &tform, alignment_type == "enu-plane-unscaled"); } } if (merge_origins) { for (size_t i = 0; i < ref_image_names.size(); i++) { const Image* first_image = reconstruction.FindImageWithName(ref_image_names[i]); if (first_image != nullptr) { const Eigen::Vector3d& first_img_position = ref_locations[i]; const Eigen::Vector3d trans_align = first_img_position - first_image->ProjectionCenter(); const Sim3d origin_align( 1.0, Eigen::Quaterniond::Identity(), trans_align); LOG(INFO) << "\n Aligning reconstruction's origin with ref origin: " << first_img_position.transpose() << "\n"; reconstruction.Transform(origin_align); // Update the Sim3 transformation in case it is stored next. tform = Sim3d(tform.scale, tform.rotation, tform.translation + trans_align); break; } } } if (alignment_success) { LOG(INFO) << "=> Alignment succeeded"; reconstruction.Write(output_path); if (!transform_path.empty()) { tform.ToFile(transform_path); } return EXIT_SUCCESS; } else { LOG(INFO) << "=> Alignment failed"; return EXIT_FAILURE; } } int RunModelAnalyzer(int argc, char** argv) { std::string path; bool verbose = false; OptionManager options; options.AddRequiredOption("path", &path); options.AddDefaultOption("verbose", &verbose); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(path); LOG(INFO) << StringPrintf("Cameras: %d", reconstruction.NumCameras()); LOG(INFO) << StringPrintf("Images: %d", reconstruction.NumImages()); LOG(INFO) << StringPrintf("Registered images: %d", reconstruction.NumRegImages()); LOG(INFO) << StringPrintf("Points: %d", reconstruction.NumPoints3D()); LOG(INFO) << StringPrintf("Observations: %d", reconstruction.ComputeNumObservations()); LOG(INFO) << StringPrintf("Mean track length: %f", reconstruction.ComputeMeanTrackLength()); LOG(INFO) << StringPrintf( "Mean observations per image: %f", reconstruction.ComputeMeanObservationsPerRegImage()); LOG(INFO) << StringPrintf("Mean reprojection error: %fpx", reconstruction.ComputeMeanReprojectionError()); // verbose information if (verbose) { PrintHeading2("Cameras"); for (const auto& camera : reconstruction.Cameras()) { LOG(INFO) << StringPrintf(" - Camera Id: %d, Model Name: %s, Params: %s", camera.first, camera.second.ModelName().c_str(), camera.second.ParamsToString().c_str()); } PrintHeading2("Images"); for (const auto& image_id : reconstruction.RegImageIds()) { LOG(INFO) << StringPrintf(" - Registered Image Id: %d, Name: %s", image_id, reconstruction.Image(image_id).Name().c_str()); } } return EXIT_SUCCESS; } int RunModelComparer(int argc, char** argv) { std::string input_path1; std::string input_path2; std::string output_path; std::string alignment_error = "reprojection"; double min_inlier_observations = 0.3; double max_reproj_error = 8.0; double max_proj_center_error = 0.1; OptionManager options; options.AddRequiredOption("input_path1", &input_path1); options.AddRequiredOption("input_path2", &input_path2); options.AddDefaultOption("output_path", &output_path); options.AddDefaultOption( "alignment_error", &alignment_error, "{reprojection, proj_center}"); options.AddDefaultOption("min_inlier_observations", &min_inlier_observations); options.AddDefaultOption("max_reproj_error", &max_reproj_error); options.AddDefaultOption("max_proj_center_error", &max_proj_center_error); options.Parse(argc, argv); if (!output_path.empty() && !ExistsDir(output_path)) { LOG(ERROR) << "Provided output path is not a valid directory"; return EXIT_FAILURE; } Reconstruction reconstruction1; reconstruction1.Read(input_path1); Reconstruction reconstruction2; reconstruction2.Read(input_path2); std::vector errors; Sim3d rec2_from_rec1; bool success = CompareModels(reconstruction1, reconstruction2, alignment_error, min_inlier_observations, max_reproj_error, max_proj_center_error, errors, rec2_from_rec1); if (!success) { return EXIT_FAILURE; } if (!output_path.empty()) { const std::string errors_path = JoinPaths(output_path, "errors.csv"); WriteComparisonErrorsCSV(errors_path, errors); const std::string summary_path = JoinPaths(output_path, "errors_summary.txt"); std::ofstream file(summary_path, std::ios::trunc); CHECK(file.is_open()) << summary_path; PrintComparisonSummary(file, errors); } return EXIT_SUCCESS; } bool CompareModels(const Reconstruction& reconstruction1, const Reconstruction& reconstruction2, const std::string& alignment_error, const double min_inlier_observations, const double max_reproj_error, const double max_proj_center_error, std::vector& errors, Sim3d& rec2_from_rec1) { PrintHeading1("Reconstruction 1"); LOG(INFO) << StringPrintf("Images: %d", reconstruction1.NumRegImages()); LOG(INFO) << StringPrintf("Points: %d", reconstruction1.NumPoints3D()); PrintHeading1("Reconstruction 2"); LOG(INFO) << StringPrintf("Images: %d", reconstruction2.NumRegImages()); LOG(INFO) << StringPrintf("Points: %d", reconstruction2.NumPoints3D()); PrintHeading1("Comparing reconstructed image poses"); const std::vector> common_image_ids = reconstruction1.FindCommonRegImageIds(reconstruction2); LOG(INFO) << StringPrintf("Common images: %d", common_image_ids.size()); bool success = false; if (alignment_error == "reprojection") { success = AlignReconstructionsViaReprojections( reconstruction1, reconstruction2, /*min_inlier_observations=*/min_inlier_observations, /*max_reproj_error=*/max_reproj_error, &rec2_from_rec1); } else if (alignment_error == "proj_center") { success = AlignReconstructionsViaProjCenters( reconstruction1, reconstruction2, /*max_proj_center_error=*/max_proj_center_error, &rec2_from_rec1); } else { LOG(ERROR) << "Invalid alignment_error specified."; return false; } if (!success) { LOG(INFO) << "=> Reconstruction alignment failed"; return false; } LOG(INFO) << "Computed alignment transform:" << std::endl << rec2_from_rec1.ToMatrix(); errors = ComputeImageAlignmentError( reconstruction1, reconstruction2, rec2_from_rec1); PrintHeading2("Image alignment error summary"); PrintComparisonSummary(std::cout, errors); return true; } int RunModelConverter(int argc, char** argv) { std::string input_path; std::string output_path; std::string output_type; bool skip_distortion = false; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddRequiredOption("output_type", &output_type, "{BIN, TXT, NVM, Bundler, VRML, PLY, R3D, CAM}"); options.AddDefaultOption("skip_distortion", &skip_distortion); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(input_path); StringToLower(&output_type); if (output_type == "bin") { reconstruction.WriteBinary(output_path); } else if (output_type == "txt") { reconstruction.WriteText(output_path); } else if (output_type == "nvm") { reconstruction.ExportNVM(output_path, skip_distortion); } else if (output_type == "bundler") { reconstruction.ExportBundler(output_path + ".bundle.out", output_path + ".list.txt", skip_distortion); } else if (output_type == "r3d") { reconstruction.ExportRecon3D(output_path, skip_distortion); } else if (output_type == "cam") { reconstruction.ExportCam(output_path, skip_distortion); } else if (output_type == "ply") { reconstruction.ExportPLY(output_path); } else if (output_type == "vrml") { const auto base_path = output_path.substr(0, output_path.find_last_of('.')); reconstruction.ExportVRML(base_path + ".images.wrl", base_path + ".points3D.wrl", 1, Eigen::Vector3d(1, 0, 0)); } else { LOG(ERROR) << "Invalid `output_type`"; return EXIT_FAILURE; } return EXIT_SUCCESS; } int RunModelCropper(int argc, char** argv) { Timer timer; timer.Start(); std::string input_path; std::string output_path; std::string boundary; std::string gps_transform_path; bool is_gps = false; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddRequiredOption("boundary", &boundary); options.AddDefaultOption("gps_transform_path", &gps_transform_path); options.Parse(argc, argv); if (!ExistsDir(input_path)) { LOG(ERROR) << "`input_path` is not a directory"; return EXIT_FAILURE; } if (!ExistsDir(output_path)) { LOG(ERROR) << "`output_path` is not a directory"; return EXIT_FAILURE; } std::vector boundary_elements = CSVToVector(boundary); if (boundary_elements.size() != 2 && boundary_elements.size() != 6) { LOG(ERROR) << "Invalid `boundary` - supported values are " "'x1,y1,z1,x2,y2,z2' or 'p1,p2'."; return EXIT_FAILURE; } Reconstruction reconstruction; reconstruction.Read(input_path); PrintHeading2("Calculating boundary coordinates"); std::pair bounding_box; if (boundary_elements.size() == 6) { Sim3d tform; if (!gps_transform_path.empty()) { PrintHeading2("Reading model to ECEF transform"); is_gps = true; tform = Inverse(Sim3d::FromFile(gps_transform_path)); } bounding_box.first = is_gps ? TransformLatLonAltToModelCoords(tform, boundary_elements[0], boundary_elements[1], boundary_elements[2]) : Eigen::Vector3d(boundary_elements[0], boundary_elements[1], boundary_elements[2]); bounding_box.second = is_gps ? TransformLatLonAltToModelCoords(tform, boundary_elements[3], boundary_elements[4], boundary_elements[5]) : Eigen::Vector3d(boundary_elements[3], boundary_elements[4], boundary_elements[5]); } else { bounding_box = reconstruction.ComputeBoundingBox(boundary_elements[0], boundary_elements[1]); } PrintHeading2("Cropping reconstruction"); reconstruction.Crop(bounding_box).Write(output_path); WriteBoundingBox(output_path, bounding_box); LOG(INFO) << "=> Cropping succeeded"; timer.PrintMinutes(); return EXIT_SUCCESS; } int RunModelMerger(int argc, char** argv) { std::string input_path1; std::string input_path2; std::string output_path; double max_reproj_error = 64.0; OptionManager options; options.AddRequiredOption("input_path1", &input_path1); options.AddRequiredOption("input_path2", &input_path2); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("max_reproj_error", &max_reproj_error); options.Parse(argc, argv); Reconstruction reconstruction1; reconstruction1.Read(input_path1); PrintHeading2("Reconstruction 1"); LOG(INFO) << StringPrintf("Images: %d", reconstruction1.NumRegImages()); LOG(INFO) << StringPrintf("Points: %d", reconstruction1.NumPoints3D()); Reconstruction reconstruction2; reconstruction2.Read(input_path2); PrintHeading2("Reconstruction 2"); LOG(INFO) << StringPrintf("Images: %d", reconstruction2.NumRegImages()); LOG(INFO) << StringPrintf("Points: %d", reconstruction2.NumPoints3D()); PrintHeading2("Merging reconstructions"); if (MergeReconstructions( max_reproj_error, reconstruction1, &reconstruction2)) { LOG(INFO) << "=> Merge succeeded"; PrintHeading2("Merged reconstruction"); LOG(INFO) << StringPrintf("Images: %d", reconstruction2.NumRegImages()); LOG(INFO) << StringPrintf("Points: %d", reconstruction2.NumPoints3D()); } else { LOG(INFO) << "=> Merge failed"; } reconstruction2.Write(output_path); return EXIT_SUCCESS; } int RunModelOrientationAligner(int argc, char** argv) { std::string input_path; std::string output_path; std::string method = "MANHATTAN-WORLD"; ManhattanWorldFrameEstimationOptions frame_estimation_options; OptionManager options; options.AddImageOptions(); options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption( "method", &method, "{MANHATTAN-WORLD, IMAGE-ORIENTATION}"); options.AddDefaultOption("max_image_size", &frame_estimation_options.max_image_size); options.Parse(argc, argv); StringToLower(&method); if (method != "manhattan-world" && method != "image-orientation") { LOG(ERROR) << "Invalid `method` - supported values are " "'MANHATTAN-WORLD' or 'IMAGE-ORIENTATION'."; return EXIT_FAILURE; } Reconstruction reconstruction; reconstruction.Read(input_path); PrintHeading1("Aligning Reconstruction"); Sim3d new_from_old_world; if (method == "manhattan-world") { const Eigen::Matrix3d frame = EstimateManhattanWorldFrame( frame_estimation_options, reconstruction, *options.image_path); if (frame.col(0).lpNorm<1>() == 0) { LOG(INFO) << "Only aligning vertical axis"; new_from_old_world.rotation = Eigen::Quaterniond::FromTwoVectors( frame.col(1), Eigen::Vector3d(0, 1, 0)); } else if (frame.col(1).lpNorm<1>() == 0) { new_from_old_world.rotation = Eigen::Quaterniond::FromTwoVectors( frame.col(0), Eigen::Vector3d(1, 0, 0)); LOG(INFO) << "Only aligning horizontal axis"; } else { new_from_old_world.rotation = Eigen::Quaterniond(frame.transpose()); LOG(INFO) << "Aligning horizontal and vertical axes"; } } else if (method == "image-orientation") { const Eigen::Vector3d gravity_axis = EstimateGravityVectorFromImageOrientation(reconstruction); new_from_old_world.rotation = Eigen::Quaterniond::FromTwoVectors( gravity_axis, Eigen::Vector3d(0, 1, 0)); } else { LOG(FATAL) << "Alignment method not supported"; } LOG(INFO) << "Using the rotation matrix:"; LOG(INFO) << new_from_old_world.rotation.toRotationMatrix(); reconstruction.Transform(new_from_old_world); LOG(INFO) << "Writing aligned reconstruction..."; reconstruction.Write(output_path); return EXIT_SUCCESS; } int RunModelSplitter(int argc, char** argv) { Timer timer; timer.Start(); std::string input_path; std::string output_path; std::string split_type; std::string split_params; std::string gps_transform_path; int num_threads = -1; int min_reg_images = 10; int min_num_points = 100; double overlap_ratio = 0.0; double min_area_ratio = 0.0; bool is_gps = false; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddRequiredOption( "split_type", &split_type, "{tiles, extent, parts}"); options.AddRequiredOption("split_params", &split_params); options.AddDefaultOption("gps_transform_path", &gps_transform_path); options.AddDefaultOption("num_threads", &num_threads); options.AddDefaultOption("min_reg_images", &min_reg_images); options.AddDefaultOption("min_num_points", &min_num_points); options.AddDefaultOption("overlap_ratio", &overlap_ratio); options.AddDefaultOption("min_area_ratio", &min_area_ratio); options.Parse(argc, argv); if (!ExistsDir(input_path)) { LOG(ERROR) << "`input_path` is not a directory"; return EXIT_FAILURE; } if (!ExistsDir(output_path)) { LOG(ERROR) << "`output_path` is not a directory"; return EXIT_FAILURE; } if (overlap_ratio < 0) { LOG(WARNING) << "Invalid `overlap_ratio`; resetting to 0"; overlap_ratio = 0.0; } PrintHeading1("Splitting sparse model"); LOG(INFO) << StringPrintf("=> Using \"%s\" split type", split_type.c_str()); Reconstruction reconstruction; reconstruction.Read(input_path); Sim3d tform; if (!gps_transform_path.empty()) { PrintHeading2("Reading model to ECEF transform"); is_gps = true; tform = Inverse(Sim3d::FromFile(gps_transform_path)); } // Create the necessary number of reconstructions based on the split method // and get the bounding boxes for each sub-reconstruction PrintHeading2("Computing bound_coords"); std::vector tile_keys; std::vector> exact_bounds; StringToLower(&split_type); if (split_type == "tiles") { std::ifstream file(split_params); CHECK(file.is_open()) << split_params; double x1, y1, z1, x2, y2, z2; std::string tile_key; std::vector> bounds; tile_keys.clear(); file >> tile_key >> x1 >> y1 >> z1 >> x2 >> y2 >> z2; while (!file.fail()) { tile_keys.push_back(tile_key); if (is_gps) { exact_bounds.emplace_back( TransformLatLonAltToModelCoords(tform, x1, y1, z1), TransformLatLonAltToModelCoords(tform, x2, y2, z2)); } else { exact_bounds.emplace_back(Eigen::Vector3d(x1, y1, z1), Eigen::Vector3d(x2, y2, z2)); } file >> tile_key >> x1 >> y1 >> z1 >> x2 >> y2 >> z2; } } else if (split_type == "extent") { std::vector parts = CSVToVector(split_params); Eigen::Vector3d extent(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); for (size_t i = 0; i < parts.size(); ++i) { extent(i) = parts[i] * tform.scale; } const auto bbox = reconstruction.ComputeBoundingBox(); const Eigen::Vector3d full_extent = bbox.second - bbox.first; const Eigen::Vector3i split( static_cast(full_extent(0) / extent(0)) + 1, static_cast(full_extent(1) / extent(1)) + 1, static_cast(full_extent(2) / extent(2)) + 1); exact_bounds = ComputeEqualPartsBounds(reconstruction, split); } else if (split_type == "parts") { auto parts = CSVToVector(split_params); Eigen::Vector3i split(1, 1, 1); for (size_t i = 0; i < parts.size(); ++i) { split(i) = parts[i]; if (split(i) < 1) { LOG(ERROR) << "Cannot split in less than 1 parts for dim " << i; return EXIT_FAILURE; } } exact_bounds = ComputeEqualPartsBounds(reconstruction, split); } else { LOG(ERROR) << "Invalid split type: " << split_type; return EXIT_FAILURE; } std::vector> bounds; for (const auto& bbox : exact_bounds) { const Eigen::Vector3d padding = (overlap_ratio * (bbox.second - bbox.first)); bounds.emplace_back(bbox.first - padding, bbox.second + padding); } PrintHeading2("Applying split and writing reconstructions"); const size_t num_parts = bounds.size(); LOG(INFO) << StringPrintf("=> Splitting to %d parts", num_parts); const bool use_tile_keys = split_type == "tiles"; auto SplitReconstruction = [&](const int idx) { Reconstruction tile_recon = reconstruction.Crop(bounds[idx]); // calculate area covered by model as proportion of box area auto bbox_extent = bounds[idx].second - bounds[idx].first; auto model_bbox = tile_recon.ComputeBoundingBox(); auto model_extent = model_bbox.second - model_bbox.first; double area_ratio = (model_extent(0) * model_extent(1)) / (bbox_extent(0) * bbox_extent(1)); int tile_num_points = tile_recon.NumPoints3D(); std::string name = use_tile_keys ? tile_keys[idx] : std::to_string(idx); const bool include_tile = area_ratio >= min_area_ratio && // tile_num_points >= min_num_points && // tile_recon.NumRegImages() >= static_cast(min_reg_images); if (include_tile) { LOG(INFO) << StringPrintf( "Writing reconstruction %s with %d images, %d points, " "and %.2f%% area coverage", name.c_str(), tile_recon.NumRegImages(), tile_num_points, 100.0 * area_ratio); const std::string reconstruction_path = JoinPaths(output_path, name); CreateDirIfNotExists(reconstruction_path); tile_recon.Write(reconstruction_path); WriteBoundingBox(reconstruction_path, bounds[idx]); WriteBoundingBox(reconstruction_path, exact_bounds[idx], "_exact"); } else { LOG(INFO) << StringPrintf( "Skipping reconstruction %s with %d images, %d points, " "and %.2f%% area coverage", name.c_str(), tile_recon.NumRegImages(), tile_num_points, 100.0 * area_ratio); } }; ThreadPool thread_pool(GetEffectiveNumThreads(num_threads)); for (size_t idx = 0; idx < num_parts; ++idx) { thread_pool.AddTask(SplitReconstruction, idx); } thread_pool.Wait(); timer.PrintMinutes(); return EXIT_SUCCESS; } int RunModelTransformer(int argc, char** argv) { std::string input_path; std::string output_path; std::string transform_path; bool is_inverse = false; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddRequiredOption("transform_path", &transform_path); options.AddDefaultOption("is_inverse", &is_inverse); options.Parse(argc, argv); LOG(INFO) << "Reading points input: " << input_path; Reconstruction recon; bool is_dense = false; if (HasFileExtension(input_path, ".ply")) { is_dense = true; recon.ImportPLY(input_path); } else if (ExistsDir(input_path)) { recon.Read(input_path); } else { LOG(ERROR) << "Invalid model input; not a PLY file or sparse reconstruction " "directory."; return EXIT_FAILURE; } LOG(INFO) << "Reading transform input: " << transform_path; Sim3d tform = Sim3d::FromFile(transform_path); if (is_inverse) { tform = Inverse(tform); } LOG(INFO) << "Applying transform to recon with " << recon.NumPoints3D() << " points"; recon.Transform(tform); LOG(INFO) << "Writing output: " << output_path; if (is_dense) { recon.ExportPLY(output_path); } else { recon.Write(output_path); } return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/model.h000066400000000000000000000051221454702036400171000ustar00rootroot00000000000000// 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. #pragma once #include "colmap/estimators/alignment.h" #include "colmap/geometry/sim3.h" #include "colmap/scene/reconstruction.h" namespace colmap { bool CompareModels(const Reconstruction& reconstruction1, const Reconstruction& reconstruction2, const std::string& alignment_error, double min_inlier_observations, double max_reproj_error, double max_proj_center_error, std::vector& errors, Sim3d& rec2_from_rec1); int RunModelAligner(int argc, char** argv); int RunModelAnalyzer(int argc, char** argv); int RunModelComparer(int argc, char** argv); int RunModelConverter(int argc, char** argv); int RunModelCropper(int argc, char** argv); int RunModelMerger(int argc, char** argv); int RunModelOrientationAligner(int argc, char** argv); int RunModelSplitter(int argc, char** argv); int RunModelTransformer(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/exe/mvs.cc000066400000000000000000000175351454702036400167560ustar00rootroot00000000000000// 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. #include "colmap/exe/mvs.h" #include "colmap/controllers/option_manager.h" #include "colmap/mvs/fusion.h" #include "colmap/mvs/meshing.h" #include "colmap/mvs/patch_match.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/misc.h" namespace colmap { int RunDelaunayMesher(int argc, char** argv) { #if !defined(COLMAP_CGAL_ENABLED) LOG(ERROR) << "Delaunay meshing requires CGAL, which is not " "available on your system."; return EXIT_FAILURE; #else // COLMAP_CGAL_ENABLED std::string input_path; std::string input_type = "dense"; std::string output_path; OptionManager options; options.AddRequiredOption( "input_path", &input_path, "Path to either the dense workspace folder or the sparse reconstruction"); options.AddDefaultOption("input_type", &input_type, "{dense, sparse}"); options.AddRequiredOption("output_path", &output_path); options.AddDelaunayMeshingOptions(); options.Parse(argc, argv); StringToLower(&input_type); if (input_type == "sparse") { mvs::SparseDelaunayMeshing( *options.delaunay_meshing, input_path, output_path); } else if (input_type == "dense") { mvs::DenseDelaunayMeshing( *options.delaunay_meshing, input_path, output_path); } else { LOG(ERROR) << "Invalid input type - " "supported values are 'sparse' and 'dense'."; return EXIT_FAILURE; } return EXIT_SUCCESS; #endif // COLMAP_CGAL_ENABLED } int RunPatchMatchStereo(int argc, char** argv) { #if !defined(COLMAP_CUDA_ENABLED) LOG(ERROR) << "Dense stereo reconstruction requires CUDA, which is not " "available on your system."; return EXIT_FAILURE; #else // COLMAP_CUDA_ENABLED std::string workspace_path; std::string workspace_format = "COLMAP"; std::string pmvs_option_name = "option-all"; std::string config_path; OptionManager options; options.AddRequiredOption( "workspace_path", &workspace_path, "Path to the folder containing the undistorted images"); options.AddDefaultOption( "workspace_format", &workspace_format, "{COLMAP, PMVS}"); options.AddDefaultOption("pmvs_option_name", &pmvs_option_name); options.AddDefaultOption("config_path", &config_path); options.AddPatchMatchStereoOptions(); options.Parse(argc, argv); StringToLower(&workspace_format); if (workspace_format != "colmap" && workspace_format != "pmvs") { LOG(ERROR) << "Invalid `workspace_format` - supported values are " "'COLMAP' or 'PMVS'."; return EXIT_FAILURE; } mvs::PatchMatchController controller(*options.patch_match_stereo, workspace_path, workspace_format, pmvs_option_name, config_path); controller.Start(); controller.Wait(); return EXIT_SUCCESS; #endif // COLMAP_CUDA_ENABLED } int RunPoissonMesher(int argc, char** argv) { std::string input_path; std::string output_path; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddPoissonMeshingOptions(); options.Parse(argc, argv); CHECK(mvs::PoissonMeshing(*options.poisson_meshing, input_path, output_path)); return EXIT_SUCCESS; } int RunStereoFuser(int argc, char** argv) { std::string workspace_path; std::string input_type = "geometric"; std::string workspace_format = "COLMAP"; std::string pmvs_option_name = "option-all"; std::string output_type = "PLY"; std::string output_path; std::string bbox_path; OptionManager options; options.AddRequiredOption("workspace_path", &workspace_path); options.AddDefaultOption( "workspace_format", &workspace_format, "{COLMAP, PMVS}"); options.AddDefaultOption("pmvs_option_name", &pmvs_option_name); options.AddDefaultOption( "input_type", &input_type, "{photometric, geometric}"); options.AddDefaultOption("output_type", &output_type, "{BIN, TXT, PLY}"); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("bbox_path", &bbox_path); options.AddStereoFusionOptions(); options.Parse(argc, argv); StringToLower(&workspace_format); if (workspace_format != "colmap" && workspace_format != "pmvs") { LOG(ERROR) << "Invalid `workspace_format` - supported values are " "'COLMAP' or 'PMVS'."; return EXIT_FAILURE; } StringToLower(&input_type); if (input_type != "photometric" && input_type != "geometric") { LOG(ERROR) << "Invalid input type - supported values are " "'photometric' and 'geometric'."; return EXIT_FAILURE; } if (!bbox_path.empty()) { std::ifstream file(bbox_path); if (file.is_open()) { auto& min_bound = options.stereo_fusion->bounding_box.first; auto& max_bound = options.stereo_fusion->bounding_box.second; file >> min_bound(0) >> min_bound(1) >> min_bound(2); file >> max_bound(0) >> max_bound(1) >> max_bound(2); } else { LOG(WARNING) << "Invalid bounds path: \"" << bbox_path << "\" - continuing without bounds check"; } } mvs::StereoFusion fuser(*options.stereo_fusion, workspace_path, workspace_format, pmvs_option_name, input_type); fuser.Start(); fuser.Wait(); Reconstruction reconstruction; // read data from sparse reconstruction if (workspace_format == "colmap") { reconstruction.Read(JoinPaths(workspace_path, "sparse")); } // overwrite sparse point cloud with dense point cloud from fuser reconstruction.ImportPLY(fuser.GetFusedPoints()); LOG(INFO) << "Writing output: " << output_path; // write output StringToLower(&output_type); if (output_type == "bin") { reconstruction.WriteBinary(output_path); } else if (output_type == "txt") { reconstruction.WriteText(output_path); } else if (output_type == "ply") { WriteBinaryPlyPoints(output_path, fuser.GetFusedPoints()); mvs::WritePointsVisibility(output_path + ".vis", fuser.GetFusedPointsVisibility()); } else { LOG(ERROR) << "Invalid `output_type`"; return EXIT_FAILURE; } return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/mvs.h000066400000000000000000000035021454702036400166050ustar00rootroot00000000000000// 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. namespace colmap { int RunDelaunayMesher(int argc, char** argv); int RunPatchMatchStereo(int argc, char** argv); int RunPoissonMesher(int argc, char** argv); int RunStereoFuser(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/exe/sfm.cc000066400000000000000000000657541454702036400167440ustar00rootroot00000000000000// 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. #include "colmap/exe/sfm.h" #include "colmap/controllers/automatic_reconstruction.h" #include "colmap/controllers/bundle_adjustment.h" #include "colmap/controllers/hierarchical_mapper.h" #include "colmap/controllers/option_manager.h" #include "colmap/estimators/similarity_transform.h" #include "colmap/exe/gui.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/misc.h" #include "colmap/util/opengl_utils.h" #include #include namespace colmap { int RunAutomaticReconstructor(int argc, char** argv) { AutomaticReconstructionController::Options reconstruction_options; std::string data_type = "individual"; std::string quality = "high"; std::string mesher = "poisson"; OptionManager options; options.AddRequiredOption("workspace_path", &reconstruction_options.workspace_path); options.AddRequiredOption("image_path", &reconstruction_options.image_path); options.AddDefaultOption("mask_path", &reconstruction_options.mask_path); options.AddDefaultOption("vocab_tree_path", &reconstruction_options.vocab_tree_path); options.AddDefaultOption( "data_type", &data_type, "{individual, video, internet}"); options.AddDefaultOption("quality", &quality, "{low, medium, high, extreme}"); options.AddDefaultOption("camera_model", &reconstruction_options.camera_model); options.AddDefaultOption("single_camera", &reconstruction_options.single_camera); options.AddDefaultOption("camera_params", &reconstruction_options.camera_params); options.AddDefaultOption("sparse", &reconstruction_options.sparse); options.AddDefaultOption("dense", &reconstruction_options.dense); options.AddDefaultOption("mesher", &mesher, "{poisson, delaunay}"); options.AddDefaultOption("num_threads", &reconstruction_options.num_threads); options.AddDefaultOption("use_gpu", &reconstruction_options.use_gpu); options.AddDefaultOption("gpu_index", &reconstruction_options.gpu_index); options.Parse(argc, argv); StringToLower(&data_type); if (data_type == "individual") { reconstruction_options.data_type = AutomaticReconstructionController::DataType::INDIVIDUAL; } else if (data_type == "video") { reconstruction_options.data_type = AutomaticReconstructionController::DataType::VIDEO; } else if (data_type == "internet") { reconstruction_options.data_type = AutomaticReconstructionController::DataType::INTERNET; } else { LOG(FATAL) << "Invalid data type provided"; } StringToLower(&quality); if (quality == "low") { reconstruction_options.quality = AutomaticReconstructionController::Quality::LOW; } else if (quality == "medium") { reconstruction_options.quality = AutomaticReconstructionController::Quality::MEDIUM; } else if (quality == "high") { reconstruction_options.quality = AutomaticReconstructionController::Quality::HIGH; } else if (quality == "extreme") { reconstruction_options.quality = AutomaticReconstructionController::Quality::EXTREME; } else { LOG(FATAL) << "Invalid quality provided"; } StringToLower(&mesher); if (mesher == "poisson") { reconstruction_options.mesher = AutomaticReconstructionController::Mesher::POISSON; } else if (mesher == "delaunay") { reconstruction_options.mesher = AutomaticReconstructionController::Mesher::DELAUNAY; } else { LOG(FATAL) << "Invalid mesher provided"; } auto reconstruction_manager = std::make_shared(); if (reconstruction_options.use_gpu && kUseOpenGL) { QApplication app(argc, argv); AutomaticReconstructionController controller(reconstruction_options, reconstruction_manager); RunThreadWithOpenGLContext(&controller); } else { AutomaticReconstructionController controller(reconstruction_options, reconstruction_manager); controller.Start(); controller.Wait(); } return EXIT_SUCCESS; } int RunBundleAdjuster(int argc, char** argv) { std::string input_path; std::string output_path; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddBundleAdjustmentOptions(); options.Parse(argc, argv); if (!ExistsDir(input_path)) { LOG(ERROR) << "`input_path` is not a directory"; return EXIT_FAILURE; } if (!ExistsDir(output_path)) { LOG(ERROR) << "`output_path` is not a directory"; return EXIT_FAILURE; } auto reconstruction = std::make_shared(); reconstruction->Read(input_path); BundleAdjustmentController ba_controller(options, reconstruction); ba_controller.Start(); ba_controller.Wait(); reconstruction->Write(output_path); return EXIT_SUCCESS; } int RunColorExtractor(int argc, char** argv) { std::string input_path; std::string output_path; OptionManager options; options.AddImageOptions(); options.AddDefaultOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(input_path); reconstruction.ExtractColorsForAllImages(*options.image_path); reconstruction.Write(output_path); return EXIT_SUCCESS; } int RunMapper(int argc, char** argv) { std::string input_path; std::string output_path; std::string image_list_path; OptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); options.AddDefaultOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("image_list_path", &image_list_path); options.AddMapperOptions(); options.Parse(argc, argv); if (!ExistsDir(output_path)) { LOG(ERROR) << "`output_path` is not a directory."; return EXIT_FAILURE; } if (!image_list_path.empty()) { const auto image_names = ReadTextFileLines(image_list_path); options.mapper->image_names = std::unordered_set(image_names.begin(), image_names.end()); } auto reconstruction_manager = std::make_shared(); if (input_path != "") { if (!ExistsDir(input_path)) { LOG(ERROR) << "`input_path` is not a directory."; return EXIT_FAILURE; } reconstruction_manager->Read(input_path); } // If fix_existing_images is enabled, we store the initial positions of // existing images in order to transform them back to the original coordinate // frame, as the reconstruction is normalized multiple times for numerical // stability. std::vector orig_fixed_image_positions; std::vector fixed_image_ids; if (options.mapper->fix_existing_images) { const auto& reconstruction = reconstruction_manager->Get(0); fixed_image_ids = reconstruction->RegImageIds(); orig_fixed_image_positions.reserve(fixed_image_ids.size()); for (const image_t image_id : fixed_image_ids) { orig_fixed_image_positions.push_back( reconstruction->Image(image_id).ProjectionCenter()); } } IncrementalMapperController mapper(options.mapper, *options.image_path, *options.database_path, reconstruction_manager); // In case a new reconstruction is started, write results of individual sub- // models to as their reconstruction finishes instead of writing all results // after all reconstructions finished. size_t prev_num_reconstructions = 0; if (input_path == "") { mapper.AddCallback( IncrementalMapperController::LAST_IMAGE_REG_CALLBACK, [&]() { // If the number of reconstructions has not changed, the last model // was discarded for some reason. if (reconstruction_manager->Size() > prev_num_reconstructions) { const std::string reconstruction_path = JoinPaths( output_path, std::to_string(prev_num_reconstructions)); CreateDirIfNotExists(reconstruction_path); reconstruction_manager->Get(prev_num_reconstructions) ->Write(reconstruction_path); options.Write(JoinPaths(reconstruction_path, "project.ini")); prev_num_reconstructions = reconstruction_manager->Size(); } }); } mapper.Start(); mapper.Wait(); if (reconstruction_manager->Size() == 0) { LOG(ERROR) << "failed to create sparse model"; return EXIT_FAILURE; } // In case the reconstruction is continued from an existing reconstruction, do // not create sub-folders but directly write the results. if (input_path != "" && reconstruction_manager->Size() > 0) { const auto& reconstruction = reconstruction_manager->Get(0); // Map the coordinate back to the original coordinate frame. if (options.mapper->fix_existing_images) { std::vector new_fixed_image_positions; new_fixed_image_positions.reserve(fixed_image_ids.size()); for (const image_t image_id : fixed_image_ids) { new_fixed_image_positions.push_back( reconstruction->Image(image_id).ProjectionCenter()); } Sim3d orig_from_new; EstimateSim3d( new_fixed_image_positions, orig_fixed_image_positions, orig_from_new); reconstruction->Transform(orig_from_new); } reconstruction->Write(output_path); } return EXIT_SUCCESS; } int RunHierarchicalMapper(int argc, char** argv) { HierarchicalMapperController::Options mapper_options; std::string output_path; OptionManager options; options.AddRequiredOption("database_path", &mapper_options.database_path); options.AddRequiredOption("image_path", &mapper_options.image_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("num_workers", &mapper_options.num_workers); options.AddDefaultOption("image_overlap", &mapper_options.clustering_options.image_overlap); options.AddDefaultOption( "leaf_max_num_images", &mapper_options.clustering_options.leaf_max_num_images); options.AddMapperOptions(); options.Parse(argc, argv); if (!ExistsDir(output_path)) { LOG(ERROR) << "`output_path` is not a directory."; return EXIT_FAILURE; } mapper_options.incremental_options = *options.mapper; auto reconstruction_manager = std::make_shared(); HierarchicalMapperController hierarchical_mapper(mapper_options, reconstruction_manager); hierarchical_mapper.Start(); hierarchical_mapper.Wait(); if (reconstruction_manager->Size() == 0) { LOG(ERROR) << "failed to create sparse model"; return EXIT_FAILURE; } reconstruction_manager->Write(output_path); options.Write(JoinPaths(output_path, "project.ini")); return EXIT_SUCCESS; } int RunPointFiltering(int argc, char** argv) { std::string input_path; std::string output_path; size_t min_track_len = 2; double max_reproj_error = 4.0; double min_tri_angle = 1.5; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption("min_track_len", &min_track_len); options.AddDefaultOption("max_reproj_error", &max_reproj_error); options.AddDefaultOption("min_tri_angle", &min_tri_angle); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(input_path); size_t num_filtered = reconstruction.FilterAllPoints3D(max_reproj_error, min_tri_angle); for (const auto point3D_id : reconstruction.Point3DIds()) { const auto& point3D = reconstruction.Point3D(point3D_id); if (point3D.track.Length() < min_track_len) { num_filtered += point3D.track.Length(); reconstruction.DeletePoint3D(point3D_id); } } LOG(INFO) << "Filtered observations: " << num_filtered; reconstruction.Write(output_path); return EXIT_SUCCESS; } int RunPointTriangulator(int argc, char** argv) { std::string input_path; std::string output_path; bool clear_points = true; bool refine_intrinsics = false; OptionManager options; options.AddDatabaseOptions(); options.AddImageOptions(); options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddDefaultOption( "clear_points", &clear_points, "Whether to clear all existing points and observations and recompute " "the image_ids based on matching filenames between the model and the " "database"); options.AddDefaultOption("refine_intrinsics", &refine_intrinsics, "Whether to refine the intrinsics of the cameras " "(fixing the principal point)"); options.AddMapperOptions(); options.Parse(argc, argv); if (!ExistsDir(input_path)) { LOG(ERROR) << "`input_path` is not a directory"; return EXIT_FAILURE; } if (!ExistsDir(output_path)) { LOG(ERROR) << "`output_path` is not a directory"; return EXIT_FAILURE; } PrintHeading1("Loading model"); auto reconstruction = std::make_shared(); reconstruction->Read(input_path); return RunPointTriangulatorImpl(reconstruction, *options.database_path, *options.image_path, output_path, *options.mapper, clear_points, refine_intrinsics); } int RunPointTriangulatorImpl( const std::shared_ptr& reconstruction, const std::string& database_path, const std::string& image_path, const std::string& output_path, const IncrementalMapperOptions& mapper_options, const bool clear_points, const bool refine_intrinsics) { PrintHeading1("Loading database"); std::shared_ptr database_cache; { Timer timer; timer.Start(); const Database database(database_path); const size_t min_num_matches = static_cast(mapper_options.min_num_matches); database_cache = DatabaseCache::Create(database, min_num_matches, mapper_options.ignore_watermarks, mapper_options.image_names); if (clear_points) { reconstruction->DeleteAllPoints2DAndPoints3D(); reconstruction->TranscribeImageIdsToDatabase(database); } timer.PrintMinutes(); } CHECK_GE(reconstruction->NumRegImages(), 2) << "Need at least two images for triangulation"; IncrementalMapper mapper(database_cache); mapper.BeginReconstruction(reconstruction); ////////////////////////////////////////////////////////////////////////////// // Triangulation ////////////////////////////////////////////////////////////////////////////// const auto tri_options = mapper_options.Triangulation(); const std::vector& reg_image_ids = reconstruction->RegImageIds(); for (size_t i = 0; i < reg_image_ids.size(); ++i) { const image_t image_id = reg_image_ids[i]; const auto& image = reconstruction->Image(image_id); PrintHeading1(StringPrintf("Triangulating image #%d (%d)", image_id, i)); const size_t num_existing_points3D = image.NumPoints3D(); LOG(INFO) << "=> Image sees " << num_existing_points3D << " / " << image.NumObservations() << " points"; mapper.TriangulateImage(tri_options, image_id); LOG(INFO) << "=> Triangulated " << (image.NumPoints3D() - num_existing_points3D) << " points"; } ////////////////////////////////////////////////////////////////////////////// // Retriangulation ////////////////////////////////////////////////////////////////////////////// PrintHeading1("Retriangulation"); CompleteAndMergeTracks(mapper_options, &mapper); ////////////////////////////////////////////////////////////////////////////// // Bundle adjustment ////////////////////////////////////////////////////////////////////////////// auto ba_options = mapper_options.GlobalBundleAdjustment(); ba_options.refine_focal_length = refine_intrinsics; ba_options.refine_principal_point = false; ba_options.refine_extra_params = refine_intrinsics; ba_options.refine_extrinsics = false; // Configure bundle adjustment. BundleAdjustmentConfig ba_config; for (const image_t image_id : reg_image_ids) { ba_config.AddImage(image_id); } for (int i = 0; i < mapper_options.ba_global_max_refinements; ++i) { // Avoid degeneracies in bundle adjustment. reconstruction->FilterObservationsWithNegativeDepth(); const size_t num_observations = reconstruction->ComputeNumObservations(); PrintHeading1("Bundle adjustment"); BundleAdjuster bundle_adjuster(ba_options, ba_config); CHECK(bundle_adjuster.Solve(reconstruction.get())); size_t num_changed_observations = 0; num_changed_observations += CompleteAndMergeTracks(mapper_options, &mapper); num_changed_observations += FilterPoints(mapper_options, &mapper); const double changed = static_cast(num_changed_observations) / num_observations; LOG(INFO) << StringPrintf("=> Changed observations: %.6f", changed); if (changed < mapper_options.ba_global_max_refinement_change) { break; } } PrintHeading1("Extracting colors"); reconstruction->ExtractColorsForAllImages(image_path); mapper.EndReconstruction(/*discard=*/false); reconstruction->Write(output_path); return EXIT_SUCCESS; } namespace { // Read the configuration of the camera rigs from a JSON file. The input images // of a camera rig must be named consistently to assign them to the appropriate // camera rig and the respective snapshots. // // An example configuration of a single camera rig: // [ // { // "ref_camera_id": 1, // "cameras": // [ // { // "camera_id": 1, // "image_prefix": "left1_image" // "cam_from_rig_rotation": [1, 0, 0, 0], // "cam_from_rig_translation": [0, 0, 0] // }, // { // "camera_id": 2, // "image_prefix": "left2_image" // "cam_from_rig_rotation": [1, 0, 0, 0], // "cam_from_rig_translation": [0, 0, 1] // }, // { // "camera_id": 3, // "image_prefix": "right1_image" // "cam_from_rig_rotation": [1, 0, 0, 0], // "cam_from_rig_translation": [0, 0, 2] // }, // { // "camera_id": 4, // "image_prefix": "right2_image" // "cam_from_rig_rotation": [1, 0, 0, 0], // "cam_from_rig_translation": [0, 0, 3] // } // ] // } // ] // // The "camera_id" and "image_prefix" fields are required, whereas the // "cam_from_rig_rotation" and "cam_from_rig_translation" fields optionally // specify the relative extrinsics of the camera rig in the form of a // translation vector and a rotation quaternion (w, x, y, z). If the relative // extrinsics are not provided then they are automatically inferred from the // reconstruction. // // This file specifies the configuration for a single camera rig and that you // could potentially define multiple camera rigs. The rig is composed of 4 // cameras: all images of the first camera must have "left1_image" as a name // prefix, e.g., "left1_image_frame000.png" or "left1_image/frame000.png". // Images with the same suffix ("_frame000.png" and "/frame000.png") are // assigned to the same snapshot, i.e., they are assumed to be captured at the // same time. Only snapshots with the reference image registered will be added // to the bundle adjustment problem. The remaining images will be added with // independent poses to the bundle adjustment problem. The above configuration // could have the following input image file structure: // // /path/to/images/... // left1_image/... // frame000.png // frame001.png // frame002.png // ... // left2_image/... // frame000.png // frame001.png // frame002.png // ... // right1_image/... // frame000.png // frame001.png // frame002.png // ... // right2_image/... // frame000.png // frame001.png // frame002.png // ... // std::vector ReadCameraRigConfig(const std::string& rig_config_path, const Reconstruction& reconstruction, bool estimate_rig_relative_poses) { boost::property_tree::ptree pt; boost::property_tree::read_json(rig_config_path.c_str(), pt); std::vector camera_rigs; for (const auto& rig_config : pt) { CameraRig camera_rig; std::vector image_prefixes; for (const auto& camera : rig_config.second.get_child("cameras")) { const int camera_id = camera.second.get("camera_id"); image_prefixes.push_back(camera.second.get("image_prefix")); Rigid3d cam_from_rig; auto cam_from_rig_rotation_node = camera.second.get_child_optional("cam_from_rig_rotation"); if (cam_from_rig_rotation_node) { int index = 0; Eigen::Vector4d cam_from_rig_wxyz; for (const auto& node : cam_from_rig_rotation_node.get()) { cam_from_rig_wxyz[index++] = node.second.get_value(); } cam_from_rig.rotation = Eigen::Quaterniond(cam_from_rig_wxyz(0), cam_from_rig_wxyz(1), cam_from_rig_wxyz(2), cam_from_rig_wxyz(3)); } else { estimate_rig_relative_poses = true; } auto cam_from_rig_translation_node = camera.second.get_child_optional("cam_from_rig_translation"); if (cam_from_rig_translation_node) { int index = 0; for (const auto& node : cam_from_rig_translation_node.get()) { cam_from_rig.translation(index++) = node.second.get_value(); } } else { estimate_rig_relative_poses = true; } camera_rig.AddCamera(camera_id, cam_from_rig); } camera_rig.SetRefCameraId(rig_config.second.get("ref_camera_id")); std::unordered_map> snapshots; for (const auto image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); for (const auto& image_prefix : image_prefixes) { if (StringContains(image.Name(), image_prefix)) { const std::string image_suffix = StringGetAfter(image.Name(), image_prefix); snapshots[image_suffix].push_back(image_id); } } } for (const auto& snapshot : snapshots) { bool has_ref_camera = false; for (const auto image_id : snapshot.second) { const auto& image = reconstruction.Image(image_id); if (image.CameraId() == camera_rig.RefCameraId()) { has_ref_camera = true; break; } } if (has_ref_camera) { camera_rig.AddSnapshot(snapshot.second); } } camera_rig.Check(reconstruction); if (estimate_rig_relative_poses) { PrintHeading2("Estimating relative rig poses"); if (!camera_rig.ComputeCamsFromRigs(reconstruction)) { LOG(WARNING) << "Failed to estimate rig poses from reconstruction; " "cannot use rig BA"; return std::vector(); } } camera_rigs.push_back(camera_rig); } return camera_rigs; } } // namespace int RunRigBundleAdjuster(int argc, char** argv) { std::string input_path; std::string output_path; std::string rig_config_path; bool estimate_rig_relative_poses = true; RigBundleAdjuster::Options rig_ba_options; OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.AddRequiredOption("rig_config_path", &rig_config_path); options.AddDefaultOption("estimate_rig_relative_poses", &estimate_rig_relative_poses); options.AddDefaultOption("RigBundleAdjustment.refine_relative_poses", &rig_ba_options.refine_relative_poses); options.AddBundleAdjustmentOptions(); options.Parse(argc, argv); Reconstruction reconstruction; reconstruction.Read(input_path); PrintHeading1("Camera rig configuration"); auto camera_rigs = ReadCameraRigConfig( rig_config_path, reconstruction, estimate_rig_relative_poses); BundleAdjustmentConfig config; for (size_t i = 0; i < camera_rigs.size(); ++i) { const auto& camera_rig = camera_rigs[i]; PrintHeading2(StringPrintf("Camera Rig %d", i + 1)); LOG(INFO) << StringPrintf("Cameras: %d", camera_rig.NumCameras()); LOG(INFO) << StringPrintf("Snapshots: %d", camera_rig.NumSnapshots()); // Add all registered images to the bundle adjustment configuration. for (const auto image_id : reconstruction.RegImageIds()) { config.AddImage(image_id); } } PrintHeading1("Rig bundle adjustment"); BundleAdjustmentOptions ba_options = *options.bundle_adjustment; RigBundleAdjuster bundle_adjuster(ba_options, rig_ba_options, config); CHECK(bundle_adjuster.Solve(&reconstruction, &camera_rigs)); reconstruction.Write(output_path); return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/sfm.h000066400000000000000000000046401454702036400165710ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/incremental_mapper.h" #include "colmap/scene/reconstruction.h" namespace colmap { int RunPointTriangulatorImpl( const std::shared_ptr& reconstruction, const std::string& database_path, const std::string& image_path, const std::string& output_path, const IncrementalMapperOptions& mapper_options, bool clear_points, bool refine_intrinsics); int RunAutomaticReconstructor(int argc, char** argv); int RunBundleAdjuster(int argc, char** argv); int RunColorExtractor(int argc, char** argv); int RunMapper(int argc, char** argv); int RunHierarchicalMapper(int argc, char** argv); int RunPointFiltering(int argc, char** argv); int RunPointTriangulator(int argc, char** argv); int RunRigBundleAdjuster(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/exe/vocab_tree.cc000066400000000000000000000244141454702036400202540ustar00rootroot00000000000000// 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. #include "colmap/exe/vocab_tree.h" #include "colmap/controllers/feature_matching.h" #include "colmap/controllers/option_manager.h" #include "colmap/feature/sift.h" #include "colmap/feature/utils.h" #include "colmap/optim/random_sampler.h" #include "colmap/retrieval/visual_index.h" #include "colmap/scene/database.h" #include "colmap/util/misc.h" #include "colmap/util/opengl_utils.h" #include namespace colmap { namespace { // Loads descriptors for training from the database. Loads all descriptors from // the database if max_num_images < 0, otherwise the descriptors of a random // subset of images are selected. FeatureDescriptors LoadRandomDatabaseDescriptors( const std::string& database_path, const int max_num_images) { Database database(database_path); DatabaseTransaction database_transaction(&database); const std::vector images = database.ReadAllImages(); FeatureDescriptors descriptors; std::vector image_idxs; size_t num_descriptors = 0; if (max_num_images < 0) { // All images in the database. image_idxs.resize(images.size()); std::iota(image_idxs.begin(), image_idxs.end(), 0); num_descriptors = database.NumDescriptors(); } else { // Random subset of images in the database. CHECK_LE(max_num_images, images.size()); RandomSampler random_sampler(max_num_images); random_sampler.Initialize(images.size()); random_sampler.Sample(&image_idxs); for (const size_t image_idx : image_idxs) { const auto& image = images.at(image_idx); num_descriptors += database.NumDescriptorsForImage(image.ImageId()); } } descriptors.resize(num_descriptors, 128); size_t descriptor_row = 0; for (const size_t image_idx : image_idxs) { const auto& image = images.at(image_idx); const FeatureDescriptors image_descriptors = database.ReadDescriptors(image.ImageId()); descriptors.block(descriptor_row, 0, image_descriptors.rows(), 128) = image_descriptors; descriptor_row += image_descriptors.rows(); } CHECK_EQ(descriptor_row, num_descriptors); return descriptors; } std::vector ReadVocabTreeRetrievalImageList(const std::string& path, Database* database) { std::vector images; if (path.empty()) { images.reserve(database->NumImages()); for (const auto& image : database->ReadAllImages()) { images.push_back(image); } } else { DatabaseTransaction database_transaction(database); const auto image_names = ReadTextFileLines(path); images.reserve(image_names.size()); for (const auto& image_name : image_names) { const auto image = database->ReadImageWithName(image_name); CHECK_NE(image.ImageId(), kInvalidImageId); images.push_back(image); } } return images; } } // namespace int RunVocabTreeBuilder(int argc, char** argv) { std::string vocab_tree_path; retrieval::VisualIndex<>::BuildOptions build_options; int max_num_images = -1; OptionManager options; options.AddDatabaseOptions(); options.AddRequiredOption("vocab_tree_path", &vocab_tree_path); options.AddDefaultOption("num_visual_words", &build_options.num_visual_words); options.AddDefaultOption("num_checks", &build_options.num_checks); options.AddDefaultOption("branching", &build_options.branching); options.AddDefaultOption("num_iterations", &build_options.num_iterations); options.AddDefaultOption("max_num_images", &max_num_images); options.Parse(argc, argv); LOG(INFO) << "Loading descriptors..."; const auto descriptors = LoadRandomDatabaseDescriptors(*options.database_path, max_num_images); LOG(INFO) << "=> Loaded a total of " << descriptors.rows() << " descriptors"; CHECK_GT(descriptors.size(), 0); retrieval::VisualIndex<> visual_index; LOG(INFO) << "Building index for visual words..."; // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) visual_index.Build(build_options, descriptors); LOG(INFO) << "=> Quantized descriptor space using " << visual_index.NumVisualWords() << " visual words"; LOG(INFO) << "Saving index to file..."; visual_index.Write(vocab_tree_path); return EXIT_SUCCESS; } int RunVocabTreeRetriever(int argc, char** argv) { std::string vocab_tree_path; std::string database_image_list_path; std::string query_image_list_path; std::string output_index_path; retrieval::VisualIndex<>::QueryOptions query_options; int max_num_features = -1; OptionManager options; options.AddDatabaseOptions(); options.AddRequiredOption("vocab_tree_path", &vocab_tree_path); options.AddDefaultOption("database_image_list_path", &database_image_list_path); options.AddDefaultOption("query_image_list_path", &query_image_list_path); options.AddDefaultOption("output_index_path", &output_index_path); options.AddDefaultOption("num_images", &query_options.max_num_images); options.AddDefaultOption("num_neighbors", &query_options.num_neighbors); options.AddDefaultOption("num_checks", &query_options.num_checks); options.AddDefaultOption("num_images_after_verification", &query_options.num_images_after_verification); options.AddDefaultOption("max_num_features", &max_num_features); options.Parse(argc, argv); retrieval::VisualIndex<> visual_index; visual_index.Read(vocab_tree_path); Database database(*options.database_path); const auto database_images = ReadVocabTreeRetrievalImageList(database_image_list_path, &database); const auto query_images = (!query_image_list_path.empty() || output_index_path.empty()) ? ReadVocabTreeRetrievalImageList(query_image_list_path, &database) : std::vector(); ////////////////////////////////////////////////////////////////////////////// // Perform image indexing ////////////////////////////////////////////////////////////////////////////// for (size_t i = 0; i < database_images.size(); ++i) { Timer timer; timer.Start(); LOG(INFO) << StringPrintf( "Indexing image [%d/%d]", i + 1, database_images.size()) << std::flush; if (visual_index.ImageIndexed(database_images[i].ImageId())) { continue; } auto keypoints = database.ReadKeypoints(database_images[i].ImageId()); auto descriptors = database.ReadDescriptors(database_images[i].ImageId()); if (max_num_features > 0 && descriptors.rows() > max_num_features) { ExtractTopScaleFeatures(&keypoints, &descriptors, max_num_features); } visual_index.Add(retrieval::VisualIndex<>::IndexOptions(), database_images[i].ImageId(), keypoints, descriptors); LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); } // Compute the TF-IDF weights, etc. visual_index.Prepare(); // Optionally save the indexing data for the database images (as well as the // original vocabulary tree data) to speed up future indexing. if (!output_index_path.empty()) { visual_index.Write(output_index_path); } if (query_images.empty()) { return EXIT_SUCCESS; } ////////////////////////////////////////////////////////////////////////////// // Perform image queries ////////////////////////////////////////////////////////////////////////////// std::unordered_map image_id_to_image; image_id_to_image.reserve(database_images.size()); for (const auto& image : database_images) { image_id_to_image.emplace(image.ImageId(), &image); } for (size_t i = 0; i < query_images.size(); ++i) { Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Querying for image %s [%d/%d]", query_images[i].Name().c_str(), i + 1, query_images.size()) << std::flush; auto keypoints = database.ReadKeypoints(query_images[i].ImageId()); auto descriptors = database.ReadDescriptors(query_images[i].ImageId()); if (max_num_features > 0 && descriptors.rows() > max_num_features) { ExtractTopScaleFeatures(&keypoints, &descriptors, max_num_features); } std::vector image_scores; visual_index.Query(query_options, keypoints, descriptors, &image_scores); LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); for (const auto& image_score : image_scores) { const auto& image = *image_id_to_image.at(image_score.image_id); LOG(INFO) << StringPrintf(" image_id=%d, image_name=%s, score=%f", image_score.image_id, image.Name().c_str(), image_score.score); } } return EXIT_SUCCESS; } } // namespace colmap colmap-3.9.1/src/colmap/exe/vocab_tree.h000066400000000000000000000033561454702036400201200ustar00rootroot00000000000000// 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. namespace colmap { int RunVocabTreeBuilder(int argc, char** argv); int RunVocabTreeRetriever(int argc, char** argv); } // namespace colmap colmap-3.9.1/src/colmap/feature/000077500000000000000000000000001454702036400165015ustar00rootroot00000000000000colmap-3.9.1/src/colmap/feature/CMakeLists.txt000066400000000000000000000050001454702036400212340ustar00rootroot00000000000000# 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. set(FOLDER_NAME "feature") COLMAP_ADD_LIBRARY( NAME colmap_feature SRCS extractor.h matcher.h sift.h sift.cc types.h types.cc utils.h utils.cc PUBLIC_LINK_LIBS Eigen3::Eigen PRIVATE_LINK_LIBS colmap_util colmap_math colmap_sensor colmap_vlfeat flann lz4 ) if(GPU_ENABLED) target_link_libraries(colmap_feature PRIVATE colmap_sift_gpu) if(NOT GUI_ENABLED) target_link_libraries(colmap_feature PRIVATE GLEW::GLEW) endif() endif() COLMAP_ADD_TEST( NAME feature_utils_test SRCS utils_test.cc LINK_LIBS colmap_feature ) COLMAP_ADD_TEST( NAME sift_test SRCS sift_test.cc LINK_LIBS colmap_feature ) if(TESTS_ENABLED AND GUI_ENABLED) target_link_libraries(colmap_feature_sift_test Qt5::Widgets) endif() COLMAP_ADD_TEST( NAME types_test SRCS types_test.cc LINK_LIBS colmap_feature ) colmap-3.9.1/src/colmap/feature/extractor.h000066400000000000000000000037141454702036400206720ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/types.h" #include "colmap/sensor/bitmap.h" namespace colmap { class FeatureExtractor { public: virtual ~FeatureExtractor() = default; virtual bool Extract(const Bitmap& bitmap, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors) = 0; }; } // namespace colmap colmap-3.9.1/src/colmap/feature/matcher.h000066400000000000000000000054711454702036400203040ustar00rootroot00000000000000// 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. #pragma once #include "colmap/estimators/two_view_geometry.h" #include "colmap/feature/types.h" #include namespace colmap { class FeatureMatcher { public: virtual ~FeatureMatcher() = default; // If the same matcher is used for matching multiple pairs of feature sets, // then the caller may pass a nullptr to one of the keypoint/descriptor // arguments to inform the implementation that the keypoints/descriptors are // identical to the previous call. This allows the implementation to skip e.g. // uploading data to GPU memory or pre-computing search data structures for // one of the descriptors. virtual void Match( const std::shared_ptr& descriptors1, const std::shared_ptr& descriptors2, FeatureMatches* matches) = 0; virtual void MatchGuided( const TwoViewGeometryOptions& options, const std::shared_ptr& keypoints1, const std::shared_ptr& keypoints2, const std::shared_ptr& descriptors1, const std::shared_ptr& descriptors2, TwoViewGeometry* two_view_geometry) = 0; }; } // namespace colmap colmap-3.9.1/src/colmap/feature/sift.cc000066400000000000000000001515121454702036400177620ustar00rootroot00000000000000// 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. #include "colmap/feature/sift.h" #include "colmap/feature/utils.h" #include "colmap/math/math.h" #include "colmap/util/cuda.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include "colmap/util/opengl_utils.h" #if defined(COLMAP_GPU_ENABLED) #include "thirdparty/SiftGPU/SiftGPU.h" #if !defined(COLMAP_GUI_ENABLED) // GLEW symbols are already defined by Qt. #include #endif // COLMAP_GUI_ENABLED #endif // COLMAP_GPU_ENABLED #include "colmap/util/eigen_alignment.h" #include "thirdparty/VLFeat/covdet.h" #include "thirdparty/VLFeat/sift.h" #include #include #include #include #include namespace colmap { bool SiftExtractionOptions::Check() const { if (use_gpu) { CHECK_OPTION_GT(CSVToVector(gpu_index).size(), 0); } CHECK_OPTION_GT(max_image_size, 0); CHECK_OPTION_GT(max_num_features, 0); CHECK_OPTION_GT(octave_resolution, 0); CHECK_OPTION_GT(peak_threshold, 0.0); CHECK_OPTION_GT(edge_threshold, 0.0); CHECK_OPTION_GT(max_num_orientations, 0); if (domain_size_pooling) { CHECK_OPTION_GT(dsp_min_scale, 0); CHECK_OPTION_GE(dsp_max_scale, dsp_min_scale); CHECK_OPTION_GT(dsp_num_scales, 0); } return true; } bool SiftMatchingOptions::Check() const { if (use_gpu) { CHECK_OPTION_GT(CSVToVector(gpu_index).size(), 0); } CHECK_OPTION_GT(max_ratio, 0.0); CHECK_OPTION_GT(max_distance, 0.0); CHECK_OPTION_GT(max_num_matches, 0); return true; } namespace { void WarnDarknessAdaptivityNotAvailable() { LOG(WARNING) << "Darkness adaptivity only available for GLSL SiftGPU."; } // VLFeat uses a different convention to store its descriptors. This transforms // the VLFeat format into the original SIFT format that is also used by SiftGPU. FeatureDescriptors TransformVLFeatToUBCFeatureDescriptors( const FeatureDescriptors& vlfeat_descriptors) { FeatureDescriptors ubc_descriptors(vlfeat_descriptors.rows(), vlfeat_descriptors.cols()); const std::array q{{0, 7, 6, 5, 4, 3, 2, 1}}; for (FeatureDescriptors::Index n = 0; n < vlfeat_descriptors.rows(); ++n) { for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { for (int k = 0; k < 8; ++k) { ubc_descriptors(n, 8 * (j + 4 * i) + q[k]) = vlfeat_descriptors(n, 8 * (j + 4 * i) + k); } } } } return ubc_descriptors; } class SiftCPUFeatureExtractor : public FeatureExtractor { public: using VlSiftType = std::unique_ptr; explicit SiftCPUFeatureExtractor(const SiftExtractionOptions& options) : options_(options), sift_(nullptr, &vl_sift_delete) { CHECK(options_.Check()); CHECK(!options_.estimate_affine_shape); CHECK(!options_.domain_size_pooling); if (options_.darkness_adaptivity) { WarnDarknessAdaptivityNotAvailable(); } } static std::unique_ptr Create( const SiftExtractionOptions& options) { return std::make_unique(options); } bool Extract(const Bitmap& bitmap, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors) { CHECK(bitmap.IsGrey()); CHECK_NOTNULL(keypoints); if (sift_ == nullptr || sift_->width != bitmap.Width() || sift_->height != bitmap.Height()) { sift_ = VlSiftType(vl_sift_new(bitmap.Width(), bitmap.Height(), options_.num_octaves, options_.octave_resolution, options_.first_octave), &vl_sift_delete); if (!sift_) { return false; } } vl_sift_set_peak_thresh(sift_.get(), options_.peak_threshold); vl_sift_set_edge_thresh(sift_.get(), options_.edge_threshold); // Iterate through octaves. std::vector level_num_features; std::vector level_keypoints; std::vector level_descriptors; bool first_octave = true; while (true) { if (first_octave) { const std::vector data_uint8 = bitmap.ConvertToRowMajorArray(); std::vector data_float(data_uint8.size()); for (size_t i = 0; i < data_uint8.size(); ++i) { data_float[i] = static_cast(data_uint8[i]) / 255.0f; } if (vl_sift_process_first_octave(sift_.get(), data_float.data())) { break; } first_octave = false; } else { if (vl_sift_process_next_octave(sift_.get())) { break; } } // Detect keypoints. vl_sift_detect(sift_.get()); // Extract detected keypoints. const VlSiftKeypoint* vl_keypoints = vl_sift_get_keypoints(sift_.get()); const int num_keypoints = vl_sift_get_nkeypoints(sift_.get()); if (num_keypoints == 0) { continue; } // Extract features with different orientations per DOG level. size_t level_idx = 0; int prev_level = -1; FeatureDescriptorsFloat desc(1, 128); for (int i = 0; i < num_keypoints; ++i) { if (vl_keypoints[i].is != prev_level) { if (i > 0) { // Resize containers of previous DOG level. level_keypoints.back().resize(level_idx); if (descriptors != nullptr) { level_descriptors.back().conservativeResize(level_idx, 128); } } // Add containers for new DOG level. level_idx = 0; level_num_features.push_back(0); level_keypoints.emplace_back(options_.max_num_orientations * num_keypoints); if (descriptors != nullptr) { level_descriptors.emplace_back( options_.max_num_orientations * num_keypoints, 128); } } level_num_features.back() += 1; prev_level = vl_keypoints[i].is; // Extract feature orientations. double angles[4]; int num_orientations; if (options_.upright) { num_orientations = 1; angles[0] = 0.0; } else { num_orientations = vl_sift_calc_keypoint_orientations( sift_.get(), angles, &vl_keypoints[i]); } // Note that this is different from SiftGPU, which selects the top // global maxima as orientations while this selects the first two // local maxima. It is not clear which procedure is better. const int num_used_orientations = std::min(num_orientations, options_.max_num_orientations); for (int o = 0; o < num_used_orientations; ++o) { level_keypoints.back()[level_idx] = FeatureKeypoint(vl_keypoints[i].x + 0.5f, vl_keypoints[i].y + 0.5f, vl_keypoints[i].sigma, angles[o]); if (descriptors != nullptr) { vl_sift_calc_keypoint_descriptor( sift_.get(), desc.data(), &vl_keypoints[i], angles[o]); if (options_.normalization == SiftExtractionOptions::Normalization::L2) { L2NormalizeFeatureDescriptors(&desc); } else if (options_.normalization == SiftExtractionOptions::Normalization::L1_ROOT) { L1RootNormalizeFeatureDescriptors(&desc); } else { LOG(FATAL) << "Normalization type not supported"; } level_descriptors.back().row(level_idx) = FeatureDescriptorsToUnsignedByte(desc); } level_idx += 1; } } // Resize containers for last DOG level in octave. level_keypoints.back().resize(level_idx); if (descriptors != nullptr) { level_descriptors.back().conservativeResize(level_idx, 128); } } // Determine how many DOG levels to keep to satisfy max_num_features option. int first_level_to_keep = 0; int num_features = 0; int num_features_with_orientations = 0; for (int i = level_keypoints.size() - 1; i >= 0; --i) { num_features += level_num_features[i]; num_features_with_orientations += level_keypoints[i].size(); if (num_features > options_.max_num_features) { first_level_to_keep = i; break; } } // Extract the features to be kept. { size_t k = 0; keypoints->resize(num_features_with_orientations); for (size_t i = first_level_to_keep; i < level_keypoints.size(); ++i) { for (size_t j = 0; j < level_keypoints[i].size(); ++j) { (*keypoints)[k] = level_keypoints[i][j]; k += 1; } } } // Compute the descriptors for the detected keypoints. if (descriptors != nullptr) { size_t k = 0; descriptors->resize(num_features_with_orientations, 128); for (size_t i = first_level_to_keep; i < level_keypoints.size(); ++i) { for (size_t j = 0; j < level_keypoints[i].size(); ++j) { descriptors->row(k) = level_descriptors[i].row(j); k += 1; } } *descriptors = TransformVLFeatToUBCFeatureDescriptors(*descriptors); } return true; } private: const SiftExtractionOptions options_; VlSiftType sift_; }; class CovariantSiftCPUFeatureExtractor : public FeatureExtractor { public: explicit CovariantSiftCPUFeatureExtractor( const SiftExtractionOptions& options) : options_(options) { CHECK(options_.Check()); if (options_.darkness_adaptivity) { WarnDarknessAdaptivityNotAvailable(); } } static std::unique_ptr Create( const SiftExtractionOptions& options) { return std::make_unique(options); } bool Extract(const Bitmap& bitmap, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors) { CHECK(bitmap.IsGrey()); CHECK_NOTNULL(keypoints); // Setup covariant SIFT detector. std::unique_ptr covdet( vl_covdet_new(VL_COVDET_METHOD_DOG), &vl_covdet_delete); if (!covdet) { return false; } const int kMaxOctaveResolution = 1000; CHECK_LE(options_.octave_resolution, kMaxOctaveResolution); vl_covdet_set_first_octave(covdet.get(), options_.first_octave); vl_covdet_set_octave_resolution(covdet.get(), options_.octave_resolution); vl_covdet_set_peak_threshold(covdet.get(), options_.peak_threshold); vl_covdet_set_edge_threshold(covdet.get(), options_.edge_threshold); { const std::vector data_uint8 = bitmap.ConvertToRowMajorArray(); std::vector data_float(data_uint8.size()); for (size_t i = 0; i < data_uint8.size(); ++i) { data_float[i] = static_cast(data_uint8[i]) / 255.0f; } vl_covdet_put_image( covdet.get(), data_float.data(), bitmap.Width(), bitmap.Height()); } vl_covdet_detect(covdet.get(), options_.max_num_features); if (!options_.upright) { if (options_.estimate_affine_shape) { vl_covdet_extract_affine_shape(covdet.get()); } else { vl_covdet_extract_orientations(covdet.get()); } } const int num_features = vl_covdet_get_num_features(covdet.get()); VlCovDetFeature* features = vl_covdet_get_features(covdet.get()); // Sort features according to detected octave and scale. std::sort( features, features + num_features, [](const VlCovDetFeature& feature1, const VlCovDetFeature& feature2) { if (feature1.o == feature2.o) { return feature1.s > feature2.s; } else { return feature1.o > feature2.o; } }); const size_t max_num_features = static_cast(options_.max_num_features); // Copy detected keypoints and clamp when maximum number of features // reached. int prev_octave_scale_idx = std::numeric_limits::max(); for (int i = 0; i < num_features; ++i) { FeatureKeypoint keypoint; keypoint.x = features[i].frame.x + 0.5; keypoint.y = features[i].frame.y + 0.5; keypoint.a11 = features[i].frame.a11; keypoint.a12 = features[i].frame.a12; keypoint.a21 = features[i].frame.a21; keypoint.a22 = features[i].frame.a22; keypoints->push_back(keypoint); const int octave_scale_idx = features[i].o * kMaxOctaveResolution + features[i].s; CHECK_LE(octave_scale_idx, prev_octave_scale_idx); if (octave_scale_idx != prev_octave_scale_idx && keypoints->size() >= max_num_features) { break; } prev_octave_scale_idx = octave_scale_idx; } // Compute the descriptors for the detected keypoints. if (descriptors != nullptr) { descriptors->resize(keypoints->size(), 128); const size_t kPatchResolution = 15; const size_t kPatchSide = 2 * kPatchResolution + 1; const double kPatchRelativeExtent = 7.5; const double kPatchRelativeSmoothing = 1; const double kPatchStep = kPatchRelativeExtent / kPatchResolution; const double kSigma = kPatchRelativeExtent / (3.0 * (4 + 1) / 2) / kPatchStep; std::vector patch(kPatchSide * kPatchSide); std::vector patchXY(2 * kPatchSide * kPatchSide); float dsp_min_scale = 1; float dsp_scale_step = 0; int dsp_num_scales = 1; if (options_.domain_size_pooling) { dsp_min_scale = options_.dsp_min_scale; dsp_scale_step = (options_.dsp_max_scale - options_.dsp_min_scale) / options_.dsp_num_scales; dsp_num_scales = options_.dsp_num_scales; } FeatureDescriptorsFloat descriptor(1, 128); FeatureDescriptorsFloat scaled_descriptors(dsp_num_scales, 128); std::unique_ptr sift( vl_sift_new(16, 16, 1, 3, 0), &vl_sift_delete); if (!sift) { return false; } vl_sift_set_magnif(sift.get(), 3.0); for (size_t i = 0; i < keypoints->size(); ++i) { for (int s = 0; s < dsp_num_scales; ++s) { const double dsp_scale = dsp_min_scale + s * dsp_scale_step; VlFrameOrientedEllipse scaled_frame = features[i].frame; scaled_frame.a11 *= dsp_scale; scaled_frame.a12 *= dsp_scale; scaled_frame.a21 *= dsp_scale; scaled_frame.a22 *= dsp_scale; vl_covdet_extract_patch_for_frame(covdet.get(), patch.data(), kPatchResolution, kPatchRelativeExtent, kPatchRelativeSmoothing, scaled_frame); vl_imgradient_polar_f(patchXY.data(), patchXY.data() + 1, 2, 2 * kPatchSide, patch.data(), kPatchSide, kPatchSide, kPatchSide); vl_sift_calc_raw_descriptor(sift.get(), patchXY.data(), scaled_descriptors.row(s).data(), kPatchSide, kPatchSide, kPatchResolution, kPatchResolution, kSigma, 0); } if (options_.domain_size_pooling) { descriptor = scaled_descriptors.colwise().mean(); } else { descriptor = scaled_descriptors; } CHECK_EQ(descriptor.cols(), 128); if (options_.normalization == SiftExtractionOptions::Normalization::L2) { L2NormalizeFeatureDescriptors(&descriptor); } else if (options_.normalization == SiftExtractionOptions::Normalization::L1_ROOT) { L1RootNormalizeFeatureDescriptors(&descriptor); } else { LOG(FATAL) << "Normalization type not supported"; } descriptors->row(i) = FeatureDescriptorsToUnsignedByte(descriptor); } *descriptors = TransformVLFeatToUBCFeatureDescriptors(*descriptors); } return true; } private: const SiftExtractionOptions options_; }; #if defined(COLMAP_GPU_ENABLED) // Mutexes that ensure that only one thread extracts/matches on the same GPU // at the same time, since SiftGPU internally uses static variables. static std::map> sift_gpu_mutexes_; class SiftGPUFeatureExtractor : public FeatureExtractor { public: explicit SiftGPUFeatureExtractor(const SiftExtractionOptions& options) : options_(options) { CHECK(options_.Check()); CHECK(!options_.estimate_affine_shape); CHECK(!options_.domain_size_pooling); } static std::unique_ptr Create( const SiftExtractionOptions& options) { // SiftGPU uses many global static state variables and the initialization // must be thread-safe in order to work correctly. This is enforced here. static std::mutex mutex; std::lock_guard lock(mutex); std::vector gpu_indices = CSVToVector(options.gpu_index); CHECK_EQ(gpu_indices.size(), 1) << "SiftGPU can only run on one GPU"; std::vector sift_gpu_args; sift_gpu_args.push_back("./sift_gpu"); #if defined(COLMAP_CUDA_ENABLED) // Use CUDA version by default if darkness adaptivity is disabled. if (!options.darkness_adaptivity && gpu_indices[0] < 0) { gpu_indices[0] = 0; } if (gpu_indices[0] >= 0) { sift_gpu_args.push_back("-cuda"); sift_gpu_args.push_back(std::to_string(gpu_indices[0])); } #endif // COLMAP_CUDA_ENABLED // Darkness adaptivity (hidden feature). Significantly improves // distribution of features. Only available in GLSL version. if (options.darkness_adaptivity) { if (gpu_indices[0] >= 0) { WarnDarknessAdaptivityNotAvailable(); } sift_gpu_args.push_back("-da"); } // No verbose logging. sift_gpu_args.push_back("-v"); sift_gpu_args.push_back("0"); // Set maximum image dimension. // Note the max dimension of SiftGPU is the maximum dimension of the // first octave in the pyramid (which is the 'first_octave'). const int compensation_factor = 1 << -std::min(0, options.first_octave); sift_gpu_args.push_back("-maxd"); sift_gpu_args.push_back( std::to_string(options.max_image_size * compensation_factor)); // Keep the highest level features. sift_gpu_args.push_back("-tc2"); sift_gpu_args.push_back(std::to_string(options.max_num_features)); // First octave level. sift_gpu_args.push_back("-fo"); sift_gpu_args.push_back(std::to_string(options.first_octave)); // Number of octave levels. sift_gpu_args.push_back("-d"); sift_gpu_args.push_back(std::to_string(options.octave_resolution)); // Peak threshold. sift_gpu_args.push_back("-t"); sift_gpu_args.push_back(std::to_string(options.peak_threshold)); // Edge threshold. sift_gpu_args.push_back("-e"); sift_gpu_args.push_back(std::to_string(options.edge_threshold)); if (options.upright) { // Fix the orientation to 0 for upright features. sift_gpu_args.push_back("-ofix"); // Maximum number of orientations. sift_gpu_args.push_back("-mo"); sift_gpu_args.push_back("1"); } else { // Maximum number of orientations. sift_gpu_args.push_back("-mo"); sift_gpu_args.push_back(std::to_string(options.max_num_orientations)); } std::vector sift_gpu_args_cstr; sift_gpu_args_cstr.reserve(sift_gpu_args.size()); for (const auto& arg : sift_gpu_args) { sift_gpu_args_cstr.push_back(arg.c_str()); } auto extractor = std::make_unique(options); // Note that the SiftGPU object is not movable (for whatever reason). // If we instead create the object here and move it to the constructor, the // program segfaults inside SiftGPU. extractor->sift_gpu_.ParseParam(sift_gpu_args_cstr.size(), sift_gpu_args_cstr.data()); extractor->sift_gpu_.gpu_index = gpu_indices[0]; if (sift_gpu_mutexes_.count(gpu_indices[0]) == 0) { sift_gpu_mutexes_.emplace(gpu_indices[0], std::make_unique()); } if (extractor->sift_gpu_.VerifyContextGL() != SiftGPU::SIFTGPU_FULL_SUPPORTED) { return nullptr; } return extractor; } bool Extract(const Bitmap& bitmap, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors) override { CHECK(bitmap.IsGrey()); CHECK_NOTNULL(keypoints); CHECK_NOTNULL(descriptors); // Note the max dimension of SiftGPU is the maximum dimension of the // first octave in the pyramid (which is the 'first_octave'). const int compensation_factor = 1 << -std::min(0, options_.first_octave); CHECK_EQ(options_.max_image_size * compensation_factor, sift_gpu_.GetMaxDimension()); std::lock_guard lock(*sift_gpu_mutexes_[sift_gpu_.gpu_index]); // Note, that this produces slightly different results than using SiftGPU // directly for RGB->GRAY conversion, since it uses different weights. const std::vector bitmap_raw_bits = bitmap.ConvertToRawBits(); const int code = sift_gpu_.RunSIFT(bitmap.ScanWidth(), bitmap.Height(), bitmap_raw_bits.data(), GL_LUMINANCE, GL_UNSIGNED_BYTE); const int kSuccessCode = 1; if (code != kSuccessCode) { return false; } const size_t num_features = static_cast(sift_gpu_.GetFeatureNum()); keypoints_buffer_.resize(num_features); FeatureDescriptorsFloat descriptors_float(num_features, 128); // Download the extracted keypoints and descriptors. sift_gpu_.GetFeatureVector(keypoints_buffer_.data(), descriptors_float.data()); keypoints->resize(num_features); for (size_t i = 0; i < num_features; ++i) { (*keypoints)[i] = FeatureKeypoint(keypoints_buffer_[i].x, keypoints_buffer_[i].y, keypoints_buffer_[i].s, keypoints_buffer_[i].o); } // Save and normalize the descriptors. if (options_.normalization == SiftExtractionOptions::Normalization::L2) { L2NormalizeFeatureDescriptors(&descriptors_float); } else if (options_.normalization == SiftExtractionOptions::Normalization::L1_ROOT) { L1RootNormalizeFeatureDescriptors(&descriptors_float); } else { LOG(FATAL) << "Normalization type not supported"; } *descriptors = FeatureDescriptorsToUnsignedByte(descriptors_float); return true; } private: const SiftExtractionOptions options_; SiftGPU sift_gpu_; std::vector keypoints_buffer_; }; #endif // COLMAP_GPU_ENABLED } // namespace std::unique_ptr CreateSiftFeatureExtractor( const SiftExtractionOptions& options) { if (options.estimate_affine_shape || options.domain_size_pooling || options.force_covariant_extractor) { return CovariantSiftCPUFeatureExtractor::Create(options); } else if (options.use_gpu) { #if defined(COLMAP_GPU_ENABLED) return SiftGPUFeatureExtractor::Create(options); #else return nullptr; #endif // COLMAP_GPU_ENABLED } else { return SiftCPUFeatureExtractor::Create(options); } } namespace { size_t FindBestMatchesOneWayBruteForce(const Eigen::MatrixXi& dists, const float max_ratio, const float max_distance, std::vector* matches) { // SIFT descriptor vectors are normalized to length 512. const float kDistNorm = 1.0f / (512.0f * 512.0f); size_t num_matches = 0; matches->resize(dists.rows(), -1); for (Eigen::Index i1 = 0; i1 < dists.rows(); ++i1) { int best_i2 = -1; int best_dist = 0; int second_best_dist = 0; for (Eigen::Index i2 = 0; i2 < dists.cols(); ++i2) { const int dist = dists(i1, i2); if (dist > best_dist) { best_i2 = i2; second_best_dist = best_dist; best_dist = dist; } else if (dist > second_best_dist) { second_best_dist = dist; } } // Check if any match found. if (best_i2 == -1) { continue; } const float best_dist_normed = std::acos(std::min(kDistNorm * best_dist, 1.0f)); // Check if match distance passes threshold. if (best_dist_normed > max_distance) { continue; } const float second_best_dist_normed = std::acos(std::min(kDistNorm * second_best_dist, 1.0f)); // Check if match passes ratio test. Keep this comparison >= in order to // ensure that the case of best == second_best is detected. if (best_dist_normed >= max_ratio * second_best_dist_normed) { continue; } num_matches += 1; (*matches)[i1] = best_i2; } return num_matches; } void FindBestMatchesBruteForce(const Eigen::MatrixXi& dists, const float max_ratio, const float max_distance, const bool cross_check, FeatureMatches* matches) { matches->clear(); std::vector matches12; const size_t num_matches12 = FindBestMatchesOneWayBruteForce( dists, max_ratio, max_distance, &matches12); if (cross_check) { std::vector matches21; const size_t num_matches21 = FindBestMatchesOneWayBruteForce( dists.transpose(), max_ratio, max_distance, &matches21); matches->reserve(std::min(num_matches12, num_matches21)); for (size_t i1 = 0; i1 < matches12.size(); ++i1) { if (matches12[i1] != -1 && matches21[matches12[i1]] != -1 && matches21[matches12[i1]] == static_cast(i1)) { FeatureMatch match; match.point2D_idx1 = i1; match.point2D_idx2 = matches12[i1]; matches->push_back(match); } } } else { matches->reserve(num_matches12); for (size_t i1 = 0; i1 < matches12.size(); ++i1) { if (matches12[i1] != -1) { FeatureMatch match; match.point2D_idx1 = i1; match.point2D_idx2 = matches12[i1]; matches->push_back(match); } } } } Eigen::MatrixXi ComputeSiftDistanceMatrix( const FeatureKeypoints* keypoints1, const FeatureKeypoints* keypoints2, const FeatureDescriptors& descriptors1, const FeatureDescriptors& descriptors2, const std::function& guided_filter) { if (guided_filter != nullptr) { CHECK_NOTNULL(keypoints1); CHECK_NOTNULL(keypoints2); CHECK_EQ(keypoints1->size(), descriptors1.rows()); CHECK_EQ(keypoints2->size(), descriptors2.rows()); } const Eigen::Matrix descriptors1_int = descriptors1.cast(); const Eigen::Matrix descriptors2_int = descriptors2.cast(); Eigen::Matrix dists( descriptors1.rows(), descriptors2.rows()); for (FeatureDescriptors::Index i1 = 0; i1 < descriptors1.rows(); ++i1) { for (FeatureDescriptors::Index i2 = 0; i2 < descriptors2.rows(); ++i2) { if (guided_filter != nullptr && guided_filter((*keypoints1)[i1].x, (*keypoints1)[i1].y, (*keypoints2)[i2].x, (*keypoints2)[i2].y)) { dists(i1, i2) = 0; } else { dists(i1, i2) = descriptors1_int.row(i1).dot(descriptors2_int.row(i2)); } } } return dists; } void FindNearestNeighborsFlann( const FeatureDescriptors& query, const FeatureDescriptors& index, const flann::Index>& flann_index, Eigen::Matrix* indices, Eigen::Matrix* distances) { if (query.rows() == 0 || index.rows() == 0) { return; } constexpr size_t kNumNearestNeighbors = 2; constexpr size_t kNumLeafsToVisit = 128; const size_t num_nearest_neighbors = std::min(kNumNearestNeighbors, static_cast(index.rows())); indices->resize(query.rows(), num_nearest_neighbors); distances->resize(query.rows(), num_nearest_neighbors); const flann::Matrix query_matrix( const_cast(query.data()), query.rows(), 128); flann::Matrix indices_matrix( indices->data(), query.rows(), num_nearest_neighbors); std::vector distances_vector(query.rows() * num_nearest_neighbors); flann::Matrix distances_matrix( distances_vector.data(), query.rows(), num_nearest_neighbors); flann_index.knnSearch(query_matrix, indices_matrix, distances_matrix, num_nearest_neighbors, flann::SearchParams(kNumLeafsToVisit)); for (Eigen::Index query_idx = 0; query_idx < indices->rows(); ++query_idx) { for (Eigen::Index k = 0; k < indices->cols(); ++k) { const Eigen::Index index_idx = indices->coeff(query_idx, k); (*distances)(query_idx, k) = query.row(query_idx).cast().dot( index.row(index_idx).cast()); } } } size_t FindBestMatchesOneWayFLANN( const Eigen::Matrix& indices, const Eigen::Matrix& distances, const float max_ratio, const float max_distance, std::vector* matches) { // SIFT descriptor vectors are normalized to length 512. const float kDistNorm = 1.0f / (512.0f * 512.0f); size_t num_matches = 0; matches->resize(indices.rows(), -1); for (int d1_idx = 0; d1_idx < indices.rows(); ++d1_idx) { int best_i2 = -1; int best_dist = 0; int second_best_dist = 0; for (int n_idx = 0; n_idx < indices.cols(); ++n_idx) { const int d2_idx = indices(d1_idx, n_idx); const int dist = distances(d1_idx, n_idx); if (dist > best_dist) { best_i2 = d2_idx; second_best_dist = best_dist; best_dist = dist; } else if (dist > second_best_dist) { second_best_dist = dist; } } // Check if any match found. if (best_i2 == -1) { continue; } const float best_dist_normed = std::acos(std::min(kDistNorm * best_dist, 1.0f)); // Check if match distance passes threshold. if (best_dist_normed > max_distance) { continue; } const float second_best_dist_normed = std::acos(std::min(kDistNorm * second_best_dist, 1.0f)); // Check if match passes ratio test. Keep this comparison >= in order to // ensure that the case of best == second_best is detected. if (best_dist_normed >= max_ratio * second_best_dist_normed) { continue; } num_matches += 1; (*matches)[d1_idx] = best_i2; } return num_matches; } void FindBestMatchesFlann( const Eigen::Matrix& indices_1to2, const Eigen::Matrix& distances_1to2, const Eigen::Matrix& indices_2to1, const Eigen::Matrix& distances_2to1, const float max_ratio, const float max_distance, const bool cross_check, FeatureMatches* matches) { matches->clear(); std::vector matches12; const size_t num_matches12 = FindBestMatchesOneWayFLANN( indices_1to2, distances_1to2, max_ratio, max_distance, &matches12); if (cross_check && indices_2to1.rows()) { std::vector matches21; const size_t num_matches21 = FindBestMatchesOneWayFLANN( indices_2to1, distances_2to1, max_ratio, max_distance, &matches21); matches->reserve(std::min(num_matches12, num_matches21)); for (size_t i1 = 0; i1 < matches12.size(); ++i1) { if (matches12[i1] != -1 && matches21[matches12[i1]] != -1 && matches21[matches12[i1]] == static_cast(i1)) { FeatureMatch match; match.point2D_idx1 = i1; match.point2D_idx2 = matches12[i1]; matches->push_back(match); } } } else { matches->reserve(num_matches12); for (size_t i1 = 0; i1 < matches12.size(); ++i1) { if (matches12[i1] != -1) { FeatureMatch match; match.point2D_idx1 = i1; match.point2D_idx2 = matches12[i1]; matches->push_back(match); } } } } class SiftCPUFeatureMatcher : public FeatureMatcher { public: explicit SiftCPUFeatureMatcher(const SiftMatchingOptions& options) : options_(options) { CHECK(options_.Check()); } static std::unique_ptr Create( const SiftMatchingOptions& options) { return std::make_unique(options); } void Match(const std::shared_ptr& descriptors1, const std::shared_ptr& descriptors2, FeatureMatches* matches) override { CHECK_NOTNULL(matches); matches->clear(); if (descriptors1 != nullptr) { CHECK_EQ(descriptors1->cols(), 128); descriptors1_ = descriptors1; flann_index1_ = BuildFlannIndex(*descriptors1_); } if (descriptors2 != nullptr) { CHECK_EQ(descriptors2->cols(), 128); descriptors2_ = descriptors2; flann_index2_ = BuildFlannIndex(*descriptors2_); } CHECK_NOTNULL(descriptors1_); CHECK_NOTNULL(descriptors2_); if (descriptors1_->rows() == 0 || descriptors2_->rows() == 0) { return; } if (options_.brute_force_cpu_matcher) { const Eigen::MatrixXi distances = ComputeSiftDistanceMatrix( nullptr, nullptr, *descriptors1_, *descriptors2_, nullptr); FindBestMatchesBruteForce(distances, options_.max_ratio, options_.max_distance, options_.cross_check, matches); return; } Eigen::Matrix indices_1to2; Eigen::Matrix distances_1to2; Eigen::Matrix indices_2to1; Eigen::Matrix distances_2to1; FindNearestNeighborsFlann(*descriptors1_, *descriptors2_, *flann_index2_, &indices_1to2, &distances_1to2); if (options_.cross_check) { FindNearestNeighborsFlann(*descriptors2_, *descriptors1_, *flann_index1_, &indices_2to1, &distances_2to1); } FindBestMatchesFlann(indices_1to2, distances_1to2, indices_2to1, distances_2to1, options_.max_ratio, options_.max_distance, options_.cross_check, matches); } void MatchGuided( const TwoViewGeometryOptions& options, const std::shared_ptr& keypoints1, const std::shared_ptr& keypoints2, const std::shared_ptr& descriptors1, const std::shared_ptr& descriptors2, TwoViewGeometry* two_view_geometry) override { CHECK_NOTNULL(two_view_geometry); two_view_geometry->inlier_matches.clear(); if (descriptors1 != nullptr) { CHECK_NOTNULL(keypoints1); CHECK_EQ(descriptors1->rows(), keypoints1->size()); CHECK_EQ(descriptors1->cols(), 128); keypoints1_ = keypoints1; descriptors1_ = descriptors1; flann_index1_ = BuildFlannIndex(*descriptors1_); } if (descriptors2 != nullptr) { CHECK_NOTNULL(keypoints2); CHECK_EQ(descriptors2->rows(), keypoints2->size()); CHECK_EQ(descriptors2->cols(), 128); keypoints2_ = keypoints2; descriptors2_ = descriptors2; flann_index2_ = BuildFlannIndex(*descriptors2_); } const float max_residual = options.ransac_options.max_error * options.ransac_options.max_error; const Eigen::Matrix3f F = two_view_geometry->F.cast(); const Eigen::Matrix3f H = two_view_geometry->H.cast(); std::function guided_filter; if (two_view_geometry->config == TwoViewGeometry::CALIBRATED || two_view_geometry->config == TwoViewGeometry::UNCALIBRATED) { guided_filter = [&](const float x1, const float y1, const float x2, const float y2) { const Eigen::Vector3f p1(x1, y1, 1.0f); const Eigen::Vector3f p2(x2, y2, 1.0f); const Eigen::Vector3f Fx1 = F * p1; const Eigen::Vector3f Ftx2 = F.transpose() * p2; const float x2tFx1 = p2.transpose() * Fx1; return x2tFx1 * x2tFx1 / (Fx1(0) * Fx1(0) + Fx1(1) * Fx1(1) + Ftx2(0) * Ftx2(0) + Ftx2(1) * Ftx2(1)) > max_residual; }; } else if (two_view_geometry->config == TwoViewGeometry::PLANAR || two_view_geometry->config == TwoViewGeometry::PANORAMIC || two_view_geometry->config == TwoViewGeometry::PLANAR_OR_PANORAMIC) { guided_filter = [&](const float x1, const float y1, const float x2, const float y2) { const Eigen::Vector3f p1(x1, y1, 1.0f); const Eigen::Vector2f p2(x2, y2); return ((H * p1).hnormalized() - p2).squaredNorm() > max_residual; }; } else { return; } CHECK(guided_filter); const Eigen::MatrixXi dists = ComputeSiftDistanceMatrix(keypoints1_.get(), keypoints2_.get(), *descriptors1_, *descriptors2_, guided_filter); Eigen::Matrix indices_1to2(dists.rows(), dists.cols()); Eigen::Matrix indices_2to1(dists.cols(), dists.rows()); Eigen::Matrix distances_1to2 = dists; Eigen::Matrix distances_2to1 = dists.transpose(); for (int i = 0; i < indices_1to2.rows(); ++i) { indices_1to2.row(i) = Eigen::VectorXi::LinSpaced( indices_1to2.cols(), 0, indices_1to2.cols() - 1); } for (int i = 0; i < indices_2to1.rows(); ++i) { indices_2to1.row(i) = Eigen::VectorXi::LinSpaced( indices_2to1.cols(), 0, indices_2to1.cols() - 1); } FindBestMatchesFlann(indices_1to2, distances_1to2, indices_2to1, distances_2to1, options_.max_ratio, options_.max_distance, options_.cross_check, &two_view_geometry->inlier_matches); } private: using FlannIndexType = flann::Index>; static std::unique_ptr BuildFlannIndex( const FeatureDescriptors& descriptors) { CHECK_EQ(descriptors.cols(), 128); if (descriptors.rows() == 0) { // Flann is not happy when the input has no descriptors. return nullptr; } const flann::Matrix descriptors_matrix( const_cast(descriptors.data()), descriptors.rows(), 128); constexpr size_t kNumTreesInForest = 4; auto index = std::make_unique( descriptors_matrix, flann::KDTreeIndexParams(kNumTreesInForest)); index->buildIndex(); return index; } const SiftMatchingOptions options_; std::shared_ptr keypoints1_; std::shared_ptr keypoints2_; std::shared_ptr descriptors1_; std::shared_ptr descriptors2_; std::unique_ptr flann_index1_; std::unique_ptr flann_index2_; }; #if defined(COLMAP_GPU_ENABLED) // Mutexes that ensure that only one thread extracts/matches on the same GPU // at the same time, since SiftGPU internally uses static variables. static std::map> sift_match_gpu_mutexes_; class SiftGPUFeatureMatcher : public FeatureMatcher { public: explicit SiftGPUFeatureMatcher(const SiftMatchingOptions& options) : options_(options) { CHECK(options_.Check()); } static std::unique_ptr Create( const SiftMatchingOptions& options) { // SiftGPU uses many global static state variables and the initialization // must be thread-safe in order to work correctly. This is enforced here. static std::mutex mutex; std::lock_guard lock(mutex); const std::vector gpu_indices = CSVToVector(options.gpu_index); CHECK_EQ(gpu_indices.size(), 1) << "SiftGPU can only run on one GPU"; SiftGPU sift_gpu; sift_gpu.SetVerbose(0); auto matcher = std::make_unique(options); // Note that the SiftMatchGPU object is not movable (for whatever reason). // If we instead create the object here and move it to the constructor, the // program segfaults inside SiftMatchGPU. matcher->sift_match_gpu_ = SiftMatchGPU(options.max_num_matches); #if defined(COLMAP_CUDA_ENABLED) if (gpu_indices[0] >= 0) { matcher->sift_match_gpu_.SetLanguage( SiftMatchGPU::SIFTMATCH_CUDA_DEVICE0 + gpu_indices[0]); } else { matcher->sift_match_gpu_.SetLanguage(SiftMatchGPU::SIFTMATCH_CUDA); } #else // COLMAP_CUDA_ENABLED matcher->sift_match_gpu_.SetLanguage(SiftMatchGPU::SIFTMATCH_GLSL); #endif // COLMAP_CUDA_ENABLED if (matcher->sift_match_gpu_.VerifyContextGL() == 0) { return nullptr; } if (!matcher->sift_match_gpu_.Allocate(options.max_num_matches, options.cross_check)) { LOG(ERROR) << StringPrintf( "Not enough GPU memory to match %d features. " "Reduce the maximum number of matches.", options.max_num_matches); return nullptr; } #if !defined(COLMAP_CUDA_ENABLED) if (matcher->sift_match_gpu_.GetMaxSift() < options.max_num_matches) { LOG(WARNING) << StringPrintf( "OpenGL version of SiftGPU only supports a " "maximum of %d matches - consider changing to CUDA-based " "feature matching to avoid this limitation.", matcher->sift_match_gpu_.GetMaxSift()); } #endif // COLMAP_CUDA_ENABLED matcher->sift_match_gpu_.gpu_index = gpu_indices[0]; if (sift_match_gpu_mutexes_.count(gpu_indices[0]) == 0) { sift_match_gpu_mutexes_.emplace(gpu_indices[0], std::make_unique()); } return matcher; } void Match(const std::shared_ptr& descriptors1, const std::shared_ptr& descriptors2, FeatureMatches* matches) override { CHECK_NOTNULL(matches); matches->clear(); std::lock_guard lock( *sift_match_gpu_mutexes_[sift_match_gpu_.gpu_index]); if (descriptors1 != nullptr) { CHECK_EQ(descriptors1->cols(), 128); WarnIfMaxNumMatchesReachedGPU(*descriptors1); sift_match_gpu_.SetDescriptors( 0, descriptors1->rows(), descriptors1->data()); } if (descriptors2 != nullptr) { CHECK_EQ(descriptors2->cols(), 128); WarnIfMaxNumMatchesReachedGPU(*descriptors2); sift_match_gpu_.SetDescriptors( 1, descriptors2->rows(), descriptors2->data()); } matches->resize(static_cast(options_.max_num_matches)); const int num_matches = sift_match_gpu_.GetSiftMatch( options_.max_num_matches, reinterpret_cast(matches->data()), static_cast(options_.max_distance), static_cast(options_.max_ratio), options_.cross_check); if (num_matches < 0) { LOG(ERROR) << "Feature matching failed. This is probably caused by " "insufficient GPU memory. Consider reducing the maximum " "number of features and/or matches."; matches->clear(); } else { CHECK_LE(num_matches, matches->size()); matches->resize(num_matches); } } void MatchGuided( const TwoViewGeometryOptions& options, const std::shared_ptr& keypoints1, const std::shared_ptr& keypoints2, const std::shared_ptr& descriptors1, const std::shared_ptr& descriptors2, TwoViewGeometry* two_view_geometry) override { static_assert(offsetof(FeatureKeypoint, x) == 0 * sizeof(float), "Invalid keypoint format"); static_assert(offsetof(FeatureKeypoint, y) == 1 * sizeof(float), "Invalid keypoint format"); static_assert(sizeof(FeatureKeypoint) == 6 * sizeof(float), "Invalid keypoint format"); CHECK_NOTNULL(two_view_geometry); two_view_geometry->inlier_matches.clear(); std::lock_guard lock( *sift_match_gpu_mutexes_[sift_match_gpu_.gpu_index]); constexpr size_t kFeatureShapeNumElems = 4; if (descriptors1 != nullptr) { CHECK_NOTNULL(keypoints1); CHECK_EQ(descriptors1->rows(), keypoints1->size()); CHECK_EQ(descriptors1->cols(), 128); WarnIfMaxNumMatchesReachedGPU(*descriptors1); const size_t kIndex = 0; sift_match_gpu_.SetDescriptors( kIndex, descriptors1->rows(), descriptors1->data()); sift_match_gpu_.SetFeautreLocation( kIndex, reinterpret_cast(keypoints1->data()), kFeatureShapeNumElems); } if (descriptors2 != nullptr) { CHECK_NOTNULL(keypoints2); CHECK_EQ(descriptors2->rows(), keypoints2->size()); CHECK_EQ(descriptors2->cols(), 128); WarnIfMaxNumMatchesReachedGPU(*descriptors2); const size_t kIndex = 1; sift_match_gpu_.SetDescriptors( kIndex, descriptors2->rows(), descriptors2->data()); sift_match_gpu_.SetFeautreLocation( kIndex, reinterpret_cast(keypoints2->data()), kFeatureShapeNumElems); } Eigen::Matrix F; Eigen::Matrix H; float* F_ptr = nullptr; float* H_ptr = nullptr; if (two_view_geometry->config == TwoViewGeometry::CALIBRATED || two_view_geometry->config == TwoViewGeometry::UNCALIBRATED) { F = two_view_geometry->F.cast(); F_ptr = F.data(); } else if (two_view_geometry->config == TwoViewGeometry::PLANAR || two_view_geometry->config == TwoViewGeometry::PANORAMIC || two_view_geometry->config == TwoViewGeometry::PLANAR_OR_PANORAMIC) { H = two_view_geometry->H.cast(); H_ptr = H.data(); } else { return; } CHECK(F_ptr != nullptr || H_ptr != nullptr); two_view_geometry->inlier_matches.resize( static_cast(options_.max_num_matches)); const float max_residual = static_cast( options.ransac_options.max_error * options.ransac_options.max_error); const int num_matches = sift_match_gpu_.GetGuidedSiftMatch( options_.max_num_matches, reinterpret_cast( two_view_geometry->inlier_matches.data()), H_ptr, F_ptr, static_cast(options_.max_distance), static_cast(options_.max_ratio), max_residual, max_residual, options_.cross_check); if (num_matches < 0) { LOG(ERROR) << "Feature matching failed. This is probably caused by " "insufficient GPU memory. Consider reducing the maximum " "number of features."; two_view_geometry->inlier_matches.clear(); } else { CHECK_LE(num_matches, two_view_geometry->inlier_matches.size()); two_view_geometry->inlier_matches.resize(num_matches); } } private: void WarnIfMaxNumMatchesReachedGPU(const FeatureDescriptors& descriptors) { if (sift_match_gpu_.GetMaxSift() < descriptors.rows()) { LOG(WARNING) << StringPrintf( "Clamping features from %d to %d - consider " "increasing the maximum number of matches.", descriptors.rows(), sift_match_gpu_.GetMaxSift()); } } const SiftMatchingOptions options_; SiftMatchGPU sift_match_gpu_; }; #endif // COLMAP_GPU_ENABLED } // namespace std::unique_ptr CreateSiftFeatureMatcher( const SiftMatchingOptions& options) { if (options.use_gpu) { #if defined(COLMAP_GPU_ENABLED) return SiftGPUFeatureMatcher::Create(options); #else return nullptr; #endif // COLMAP_GPU_ENABLED } else { return SiftCPUFeatureMatcher::Create(options); } } void LoadSiftFeaturesFromTextFile(const std::string& path, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors) { CHECK_NOTNULL(keypoints); CHECK_NOTNULL(descriptors); std::ifstream file(path.c_str()); CHECK(file.is_open()) << path; std::string line; std::string item; std::getline(file, line); std::stringstream header_line_stream(line); std::getline(header_line_stream >> std::ws, item, ' '); const point2D_t num_features = std::stoul(item); std::getline(header_line_stream >> std::ws, item, ' '); const size_t dim = std::stoul(item); CHECK_EQ(dim, 128) << "SIFT features must have 128 dimensions"; keypoints->resize(num_features); descriptors->resize(num_features, dim); for (size_t i = 0; i < num_features; ++i) { std::getline(file, line); std::stringstream feature_line_stream(line); std::getline(feature_line_stream >> std::ws, item, ' '); const float x = std::stold(item); std::getline(feature_line_stream >> std::ws, item, ' '); const float y = std::stold(item); std::getline(feature_line_stream >> std::ws, item, ' '); const float scale = std::stold(item); std::getline(feature_line_stream >> std::ws, item, ' '); const float orientation = std::stold(item); (*keypoints)[i] = FeatureKeypoint(x, y, scale, orientation); // Descriptor for (size_t j = 0; j < dim; ++j) { std::getline(feature_line_stream >> std::ws, item, ' '); const float value = std::stod(item); CHECK_GE(value, 0); CHECK_LE(value, 255); (*descriptors)(i, j) = TruncateCast(value); } } } } // namespace colmap colmap-3.9.1/src/colmap/feature/sift.h000066400000000000000000000155531454702036400176300ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/extractor.h" #include "colmap/feature/matcher.h" namespace colmap { struct SiftExtractionOptions { // Number of threads for feature extraction. int num_threads = -1; // Whether to use the GPU for feature extraction. bool use_gpu = true; // Index of the GPU used for feature extraction. For multi-GPU extraction, // you should separate multiple GPU indices by comma, e.g., "0,1,2,3". std::string gpu_index = "-1"; // Maximum image size, otherwise image will be down-scaled. int max_image_size = 3200; // Maximum number of features to detect, keeping larger-scale features. int max_num_features = 8192; // First octave in the pyramid, i.e. -1 upsamples the image by one level. int first_octave = -1; // Number of octaves. int num_octaves = 4; // Number of levels per octave. int octave_resolution = 3; // Peak threshold for detection. double peak_threshold = 0.02 / octave_resolution; // Edge threshold for detection. double edge_threshold = 10.0; // Estimate affine shape of SIFT features in the form of oriented ellipses as // opposed to original SIFT which estimates oriented disks. bool estimate_affine_shape = false; // Maximum number of orientations per keypoint if not estimate_affine_shape. int max_num_orientations = 2; // Fix the orientation to 0 for upright features. bool upright = false; // Whether to adapt the feature detection depending on the image darkness. // Note that this feature is only available in the OpenGL SiftGPU version. bool darkness_adaptivity = false; // Domain-size pooling parameters. Domain-size pooling computes an average // SIFT descriptor across multiple scales around the detected scale. This was // proposed in "Domain-Size Pooling in Local Descriptors and Network // Architectures", J. Dong and S. Soatto, CVPR 2015. This has been shown to // outperform other SIFT variants and learned descriptors in "Comparative // Evaluation of Hand-Crafted and Learned Local Features", Schönberger, // Hardmeier, Sattler, Pollefeys, CVPR 2016. bool domain_size_pooling = false; double dsp_min_scale = 1.0 / 6.0; double dsp_max_scale = 3.0; int dsp_num_scales = 10; // Whether to force usage of the covariant VLFeat implementation. // Otherwise, the covariant implementation is only used when // estimate_affine_shape or domain_size_pooling are enabled, since the normal // Sift implementation is faster. bool force_covariant_extractor = false; enum class Normalization { // L1-normalizes each descriptor followed by element-wise square rooting. // This normalization is usually better than standard L2-normalization. // See "Three things everyone should know to improve object retrieval", // Relja Arandjelovic and Andrew Zisserman, CVPR 2012. L1_ROOT, // Each vector is L2-normalized. L2, }; Normalization normalization = Normalization::L1_ROOT; bool Check() const; }; // Create a Sift feature extractor based on the provided options. The same // feature extractor instance can be used to extract features for multiple // images in the same thread. Note that, for GPU based extraction, a OpenGL // context must be made current in the thread of the caller. If the gpu_index is // not -1, the CUDA version of SiftGPU is used, which produces slightly // different results than the OpenGL implementation. std::unique_ptr CreateSiftFeatureExtractor( const SiftExtractionOptions& options); struct SiftMatchingOptions { // Number of threads for feature matching and geometric verification. int num_threads = -1; // Whether to use the GPU for feature matching. bool use_gpu = true; // Index of the GPU used for feature matching. For multi-GPU matching, // you should separate multiple GPU indices by comma, e.g., "0,1,2,3". std::string gpu_index = "-1"; // Maximum distance ratio between first and second best match. double max_ratio = 0.8; // Maximum distance to best match. double max_distance = 0.7; // Whether to enable cross checking in matching. bool cross_check = true; // Maximum number of matches. int max_num_matches = 32768; // Whether to perform guided matching, if geometric verification succeeds. bool guided_matching = false; // Whether to use brute-force instead of FLANN based CPU matching. bool brute_force_cpu_matcher = false; bool Check() const; }; std::unique_ptr CreateSiftFeatureMatcher( const SiftMatchingOptions& options); // Load keypoints and descriptors from text file in the following format: // // LINE_0: NUM_FEATURES DIM // LINE_1: X Y SCALE ORIENTATION D_1 D_2 D_3 ... D_DIM // LINE_I: ... // LINE_NUM_FEATURES: X Y SCALE ORIENTATION D_1 D_2 D_3 ... D_DIM // // where the first line specifies the number of features and the descriptor // dimensionality followed by one line per feature: X, Y, SCALE, ORIENTATION are // of type float and D_J represent the descriptor in the range [0, 255]. // // For example: // // 2 4 // 0.32 0.12 1.23 1.0 1 2 3 4 // 0.32 0.12 1.23 1.0 1 2 3 4 // void LoadSiftFeaturesFromTextFile(const std::string& path, FeatureKeypoints* keypoints, FeatureDescriptors* descriptors); } // namespace colmap colmap-3.9.1/src/colmap/feature/sift_test.cc000066400000000000000000000762451454702036400210320ustar00rootroot00000000000000// 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. #include #if defined(COLMAP_GUI_ENABLED) #include #else #include "colmap/exe/gui.h" #endif #include "colmap/feature/sift.h" #include "colmap/feature/utils.h" #include "colmap/math/math.h" #include "colmap/math/random.h" #include "colmap/util/opengl_utils.h" #include "thirdparty/SiftGPU/SiftGPU.h" namespace colmap { namespace { void CreateImageWithSquare(const int size, Bitmap* bitmap) { bitmap->Allocate(size, size, false); bitmap->Fill(BitmapColor(0, 0, 0)); for (int r = size / 2 - size / 8; r < size / 2 + size / 8; ++r) { for (int c = size / 2 - size / 8; c < size / 2 + size / 8; ++c) { bitmap->SetPixel(r, c, BitmapColor(255)); } } } TEST(ExtractSiftFeaturesCPU, Nominal) { Bitmap bitmap; CreateImageWithSquare(256, &bitmap); SiftExtractionOptions options; options.use_gpu = false; options.estimate_affine_shape = false; options.domain_size_pooling = false; options.force_covariant_extractor = false; auto extractor = CreateSiftFeatureExtractor(options); FeatureKeypoints keypoints; FeatureDescriptors descriptors; EXPECT_TRUE(extractor->Extract(bitmap, &keypoints, &descriptors)); EXPECT_EQ(keypoints.size(), 22); for (size_t i = 0; i < keypoints.size(); ++i) { EXPECT_GE(keypoints[i].x, 0); EXPECT_GE(keypoints[i].y, 0); EXPECT_LE(keypoints[i].x, bitmap.Width()); EXPECT_LE(keypoints[i].y, bitmap.Height()); EXPECT_GT(keypoints[i].ComputeScale(), 0); EXPECT_GT(keypoints[i].ComputeOrientation(), -M_PI); EXPECT_LT(keypoints[i].ComputeOrientation(), M_PI); } EXPECT_EQ(descriptors.rows(), 22); for (FeatureDescriptors::Index i = 0; i < descriptors.rows(); ++i) { EXPECT_LT(std::abs(descriptors.row(i).cast().norm() - 512), 1); } } TEST(ExtractCovariantSiftFeaturesCPU, Nominal) { Bitmap bitmap; CreateImageWithSquare(256, &bitmap); SiftExtractionOptions options; options.use_gpu = false; options.estimate_affine_shape = false; options.domain_size_pooling = false; options.force_covariant_extractor = true; auto extractor = CreateSiftFeatureExtractor(options); FeatureKeypoints keypoints; FeatureDescriptors descriptors; EXPECT_TRUE(extractor->Extract(bitmap, &keypoints, &descriptors)); EXPECT_EQ(keypoints.size(), 22); for (size_t i = 0; i < keypoints.size(); ++i) { EXPECT_GE(keypoints[i].x, 0); EXPECT_GE(keypoints[i].y, 0); EXPECT_LE(keypoints[i].x, bitmap.Width()); EXPECT_LE(keypoints[i].y, bitmap.Height()); EXPECT_GT(keypoints[i].ComputeScale(), 0); EXPECT_GT(keypoints[i].ComputeOrientation(), -M_PI); EXPECT_LT(keypoints[i].ComputeOrientation(), M_PI); } EXPECT_EQ(descriptors.rows(), 22); for (FeatureDescriptors::Index i = 0; i < descriptors.rows(); ++i) { EXPECT_LT(std::abs(descriptors.row(i).cast().norm() - 512), 1); } } TEST(ExtractCovariantAffineSiftFeaturesCPU, Nominal) { Bitmap bitmap; CreateImageWithSquare(256, &bitmap); SiftExtractionOptions options; options.use_gpu = false; options.estimate_affine_shape = true; options.domain_size_pooling = false; options.force_covariant_extractor = false; auto extractor = CreateSiftFeatureExtractor(options); FeatureKeypoints keypoints; FeatureDescriptors descriptors; EXPECT_TRUE(extractor->Extract(bitmap, &keypoints, &descriptors)); EXPECT_EQ(keypoints.size(), 10); for (size_t i = 0; i < keypoints.size(); ++i) { EXPECT_GE(keypoints[i].x, 0); EXPECT_GE(keypoints[i].y, 0); EXPECT_LE(keypoints[i].x, bitmap.Width()); EXPECT_LE(keypoints[i].y, bitmap.Height()); EXPECT_GT(keypoints[i].ComputeScale(), 0); EXPECT_GT(keypoints[i].ComputeOrientation(), -M_PI); EXPECT_LT(keypoints[i].ComputeOrientation(), M_PI); } EXPECT_EQ(descriptors.rows(), 10); for (FeatureDescriptors::Index i = 0; i < descriptors.rows(); ++i) { EXPECT_LT(std::abs(descriptors.row(i).cast().norm() - 512), 1); } } TEST(ExtractCovariantDSPSiftFeaturesCPU, Nominal) { Bitmap bitmap; CreateImageWithSquare(256, &bitmap); SiftExtractionOptions options; options.use_gpu = false; options.estimate_affine_shape = false; options.domain_size_pooling = true; options.force_covariant_extractor = false; auto extractor = CreateSiftFeatureExtractor(options); FeatureKeypoints keypoints; FeatureDescriptors descriptors; EXPECT_TRUE(extractor->Extract(bitmap, &keypoints, &descriptors)); EXPECT_EQ(keypoints.size(), 22); for (size_t i = 0; i < keypoints.size(); ++i) { EXPECT_GE(keypoints[i].x, 0); EXPECT_GE(keypoints[i].y, 0); EXPECT_LE(keypoints[i].x, bitmap.Width()); EXPECT_LE(keypoints[i].y, bitmap.Height()); EXPECT_GT(keypoints[i].ComputeScale(), 0); EXPECT_GT(keypoints[i].ComputeOrientation(), -M_PI); EXPECT_LT(keypoints[i].ComputeOrientation(), M_PI); } EXPECT_EQ(descriptors.rows(), 22); for (FeatureDescriptors::Index i = 0; i < descriptors.rows(); ++i) { EXPECT_LT(std::abs(descriptors.row(i).cast().norm() - 512), 1); } } TEST(ExtractCovariantAffineDSPSiftFeaturesCPU, Nominal) { Bitmap bitmap; CreateImageWithSquare(256, &bitmap); SiftExtractionOptions options; options.use_gpu = false; options.estimate_affine_shape = true; options.domain_size_pooling = true; options.force_covariant_extractor = false; auto extractor = CreateSiftFeatureExtractor(options); FeatureKeypoints keypoints; FeatureDescriptors descriptors; EXPECT_TRUE(extractor->Extract(bitmap, &keypoints, &descriptors)); EXPECT_EQ(keypoints.size(), 10); for (size_t i = 0; i < keypoints.size(); ++i) { EXPECT_GE(keypoints[i].x, 0); EXPECT_GE(keypoints[i].y, 0); EXPECT_LE(keypoints[i].x, bitmap.Width()); EXPECT_LE(keypoints[i].y, bitmap.Height()); EXPECT_GT(keypoints[i].ComputeScale(), 0); EXPECT_GT(keypoints[i].ComputeOrientation(), -M_PI); EXPECT_LT(keypoints[i].ComputeOrientation(), M_PI); } EXPECT_EQ(descriptors.rows(), 10); for (FeatureDescriptors::Index i = 0; i < descriptors.rows(); ++i) { EXPECT_LT(std::abs(descriptors.row(i).cast().norm() - 512), 1); } } TEST(ExtractSiftFeaturesGPU, Nominal) { char app_name[] = "Test"; int argc = 1; char* argv[] = {app_name}; QApplication app(argc, argv); class TestThread : public Thread { private: void Run() { opengl_context_.MakeCurrent(); Bitmap bitmap; CreateImageWithSquare(256, &bitmap); SiftExtractionOptions options; options.use_gpu = true; options.estimate_affine_shape = false; options.domain_size_pooling = false; options.force_covariant_extractor = false; auto extractor = CreateSiftFeatureExtractor(options); FeatureKeypoints keypoints; FeatureDescriptors descriptors; EXPECT_TRUE(extractor->Extract(bitmap, &keypoints, &descriptors)); EXPECT_GE(keypoints.size(), 12); for (size_t i = 0; i < keypoints.size(); ++i) { EXPECT_GE(keypoints[i].x, 0); EXPECT_GE(keypoints[i].y, 0); EXPECT_LE(keypoints[i].x, bitmap.Width()); EXPECT_LE(keypoints[i].y, bitmap.Height()); EXPECT_GT(keypoints[i].ComputeScale(), 0); EXPECT_GT(keypoints[i].ComputeOrientation(), -M_PI); EXPECT_LT(keypoints[i].ComputeOrientation(), M_PI); } EXPECT_GE(descriptors.rows(), 12); for (FeatureDescriptors::Index i = 0; i < descriptors.rows(); ++i) { EXPECT_LT(std::abs(descriptors.row(i).cast().norm() - 512), 1); } } OpenGLContextManager opengl_context_; }; TestThread thread; RunThreadWithOpenGLContext(&thread); } FeatureDescriptors CreateRandomFeatureDescriptors(const size_t num_features) { SetPRNGSeed(0); FeatureDescriptorsFloat descriptors(num_features, 128); for (size_t i = 0; i < num_features; ++i) { for (size_t j = 0; j < 128; ++j) { descriptors(i, j) = std::pow(RandomUniformReal(0.0f, 1.0f), 2); } } L2NormalizeFeatureDescriptors(&descriptors); return FeatureDescriptorsToUnsignedByte(descriptors); } void CheckEqualMatches(const FeatureMatches& matches1, const FeatureMatches& matches2) { ASSERT_EQ(matches1.size(), matches2.size()); for (size_t i = 0; i < matches1.size(); ++i) { EXPECT_EQ(matches1[i].point2D_idx1, matches2[i].point2D_idx1); EXPECT_EQ(matches1[i].point2D_idx2, matches2[i].point2D_idx2); } } TEST(CreateSiftGPUMatcherOpenGL, Nominal) { char app_name[] = "Test"; int argc = 1; char* argv[] = {app_name}; QApplication app(argc, argv); class TestThread : public Thread { private: void Run() { opengl_context_.MakeCurrent(); SiftMatchingOptions options; options.use_gpu = true; options.max_num_matches = 1000; EXPECT_NE(CreateSiftFeatureMatcher(options), nullptr); } OpenGLContextManager opengl_context_; }; TestThread thread; RunThreadWithOpenGLContext(&thread); } TEST(CreateSiftGPUMatcherCUDA, Nominal) { #if defined(COLMAP_CUDA_ENABLED) SiftMatchingOptions options; options.use_gpu = true; options.gpu_index = "0"; options.max_num_matches = 1000; EXPECT_NE(CreateSiftFeatureMatcher(options), nullptr); #endif } TEST(SiftCPUFeatureMatcher, Nominal) { const auto empty_descriptors = std::make_shared(0, 128); const auto descriptors1 = std::make_shared(CreateRandomFeatureDescriptors(2)); const auto descriptors2 = std::make_shared(descriptors1->colwise().reverse()); FeatureMatches matches; SiftMatchingOptions options; options.use_gpu = false; auto matcher = CreateSiftFeatureMatcher(options); matcher->Match(descriptors1, descriptors2, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(descriptors1, nullptr, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(nullptr, descriptors2, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(nullptr, nullptr, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(empty_descriptors, descriptors2, &matches); EXPECT_EQ(matches.size(), 0); matcher->Match(descriptors1, empty_descriptors, &matches); EXPECT_EQ(matches.size(), 0); matcher->Match(empty_descriptors, empty_descriptors, &matches); EXPECT_EQ(matches.size(), 0); } TEST(SiftCPUFeatureMatcherFlannVsBruteForce, Nominal) { SiftMatchingOptions match_options; match_options.max_num_matches = 1000; auto TestFlannVsBruteForce = [](const SiftMatchingOptions& options, const FeatureDescriptors& descriptors1, const FeatureDescriptors& descriptors2) { const auto descriptors1_ptr = std::make_shared(descriptors1); const auto descriptors2_ptr = std::make_shared(descriptors2); FeatureMatches matches_bf; FeatureMatches matches_flann; SiftMatchingOptions custom_options = options; custom_options.use_gpu = false; custom_options.brute_force_cpu_matcher = true; auto bf_matcher = CreateSiftFeatureMatcher(custom_options); custom_options.brute_force_cpu_matcher = false; auto flann_matcher = CreateSiftFeatureMatcher(custom_options); bf_matcher->Match(descriptors1_ptr, descriptors2_ptr, &matches_bf); flann_matcher->Match(descriptors1_ptr, descriptors2_ptr, &matches_flann); CheckEqualMatches(matches_bf, matches_flann); const size_t num_matches = matches_bf.size(); const auto empty_descriptors = std::make_shared(0, 128); bf_matcher->Match(empty_descriptors, descriptors2_ptr, &matches_bf); flann_matcher->Match(empty_descriptors, descriptors2_ptr, &matches_flann); CheckEqualMatches(matches_bf, matches_flann); bf_matcher->Match(descriptors1_ptr, empty_descriptors, &matches_bf); flann_matcher->Match(descriptors1_ptr, empty_descriptors, &matches_flann); CheckEqualMatches(matches_bf, matches_flann); bf_matcher->Match(empty_descriptors, empty_descriptors, &matches_bf); flann_matcher->Match(empty_descriptors, empty_descriptors, &matches_flann); CheckEqualMatches(matches_bf, matches_flann); return num_matches; }; { const FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); const FeatureDescriptors descriptors2 = CreateRandomFeatureDescriptors(50); SiftMatchingOptions match_options; TestFlannVsBruteForce(match_options, descriptors1, descriptors2); } { const FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); const FeatureDescriptors descriptors2 = descriptors1.colwise().reverse(); SiftMatchingOptions match_options; const size_t num_matches = TestFlannVsBruteForce(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches, 50); } // Check the ratio test. { FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); FeatureDescriptors descriptors2 = descriptors1; SiftMatchingOptions match_options; const size_t num_matches1 = TestFlannVsBruteForce(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches1, 50); descriptors2.row(49) = descriptors2.row(0); descriptors2(0, 0) += 50.0f; descriptors2.row(0) = FeatureDescriptorsToUnsignedByte( descriptors2.row(0).cast().normalized()); descriptors2(49, 0) += 100.0f; descriptors2.row(49) = FeatureDescriptorsToUnsignedByte( descriptors2.row(49).cast().normalized()); match_options.max_ratio = 0.4; const size_t num_matches2 = TestFlannVsBruteForce( match_options, descriptors1.topRows(49), descriptors2); EXPECT_EQ(num_matches2, 48); match_options.max_ratio = 0.5; const size_t num_matches3 = TestFlannVsBruteForce(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches3, 49); } // Check the cross check. { FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); FeatureDescriptors descriptors2 = descriptors1; descriptors1.row(0) = descriptors1.row(1); SiftMatchingOptions match_options; match_options.cross_check = false; const size_t num_matches1 = TestFlannVsBruteForce(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches1, 50); match_options.cross_check = true; const size_t num_matches2 = TestFlannVsBruteForce(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches2, 48); } } TEST(MatchGuidedSiftFeaturesCPU, Nominal) { auto empty_keypoints = std::make_shared(0); auto keypoints1 = std::make_shared(2); (*keypoints1)[0].x = 1; (*keypoints1)[1].x = 2; auto keypoints2 = std::make_shared(2); (*keypoints2)[0].x = 2; (*keypoints2)[1].x = 1; const auto empty_descriptors = std::make_shared(0, 128); const auto descriptors1 = std::make_shared(CreateRandomFeatureDescriptors(2)); const auto descriptors2 = std::make_shared(descriptors1->colwise().reverse()); TwoViewGeometry two_view_geometry; two_view_geometry.config = TwoViewGeometry::PLANAR_OR_PANORAMIC; two_view_geometry.H = Eigen::Matrix3d::Identity(); SiftMatchingOptions options; options.use_gpu = false; auto matcher = CreateSiftFeatureMatcher(options); matcher->MatchGuided(TwoViewGeometryOptions(), keypoints1, keypoints2, descriptors1, descriptors2, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 2); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 0); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx2, 0); (*keypoints1)[0].x = 100; matcher->MatchGuided(TwoViewGeometryOptions(), keypoints1, keypoints2, descriptors1, descriptors2, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 1); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 0); matcher->MatchGuided(TwoViewGeometryOptions(), empty_keypoints, keypoints2, empty_descriptors, descriptors2, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 0); matcher->MatchGuided(TwoViewGeometryOptions(), keypoints1, empty_keypoints, descriptors1, empty_descriptors, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 0); matcher->MatchGuided(TwoViewGeometryOptions(), empty_keypoints, empty_keypoints, empty_descriptors, empty_descriptors, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 0); } TEST(MatchSiftFeaturesGPU, Nominal) { char app_name[] = "Test"; int argc = 1; char* argv[] = {app_name}; QApplication app(argc, argv); class TestThread : public Thread { private: void Run() { opengl_context_.MakeCurrent(); SiftMatchingOptions options; options.use_gpu = true; options.max_num_matches = 1000; auto matcher = CHECK_NOTNULL(CreateSiftFeatureMatcher(options)); const auto empty_descriptors = std::make_shared(0, 128); const auto descriptors1 = std::make_shared( CreateRandomFeatureDescriptors(2)); const auto descriptors2 = std::make_shared( descriptors1->colwise().reverse()); FeatureMatches matches; matcher->Match(descriptors1, descriptors2, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(nullptr, nullptr, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(descriptors1, nullptr, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(nullptr, descriptors2, &matches); EXPECT_EQ(matches.size(), 2); EXPECT_EQ(matches[0].point2D_idx1, 0); EXPECT_EQ(matches[0].point2D_idx2, 1); EXPECT_EQ(matches[1].point2D_idx1, 1); EXPECT_EQ(matches[1].point2D_idx2, 0); matcher->Match(empty_descriptors, descriptors2, &matches); EXPECT_EQ(matches.size(), 0); matcher->Match(descriptors1, empty_descriptors, &matches); EXPECT_EQ(matches.size(), 0); matcher->Match(empty_descriptors, empty_descriptors, &matches); EXPECT_EQ(matches.size(), 0); } OpenGLContextManager opengl_context_; }; TestThread thread; RunThreadWithOpenGLContext(&thread); } TEST(MatchSiftFeaturesCPUvsGPU, Nominal) { char app_name[] = "Test"; int argc = 1; char* argv[] = {app_name}; QApplication app(argc, argv); class TestThread : public Thread { private: void Run() { opengl_context_.MakeCurrent(); auto TestCPUvsGPU = [](const SiftMatchingOptions& options, const FeatureDescriptors& descriptors1, const FeatureDescriptors& descriptors2) { SiftMatchingOptions custom_options = options; custom_options.use_gpu = true; custom_options.max_num_matches = 1000; auto gpu_matcher = CHECK_NOTNULL(CreateSiftFeatureMatcher(custom_options)); custom_options.use_gpu = false; auto cpu_matcher = CreateSiftFeatureMatcher(custom_options); const auto descriptors1_ptr = std::make_shared(descriptors1); const auto descriptors2_ptr = std::make_shared(descriptors2); FeatureMatches matches_cpu; FeatureMatches matches_gpu; cpu_matcher->Match(descriptors1_ptr, descriptors2_ptr, &matches_cpu); gpu_matcher->Match(descriptors1_ptr, descriptors2_ptr, &matches_gpu); CheckEqualMatches(matches_cpu, matches_gpu); const size_t num_matches = matches_cpu.size(); const auto empty_descriptors = std::make_shared(0, 128); cpu_matcher->Match(empty_descriptors, descriptors2_ptr, &matches_cpu); gpu_matcher->Match(empty_descriptors, descriptors2_ptr, &matches_gpu); CheckEqualMatches(matches_cpu, matches_gpu); cpu_matcher->Match(descriptors1_ptr, empty_descriptors, &matches_cpu); gpu_matcher->Match(descriptors1_ptr, empty_descriptors, &matches_gpu); CheckEqualMatches(matches_cpu, matches_gpu); cpu_matcher->Match(empty_descriptors, empty_descriptors, &matches_cpu); gpu_matcher->Match(empty_descriptors, empty_descriptors, &matches_gpu); CheckEqualMatches(matches_cpu, matches_gpu); return num_matches; }; { const FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); const FeatureDescriptors descriptors2 = CreateRandomFeatureDescriptors(50); SiftMatchingOptions match_options; TestCPUvsGPU(match_options, descriptors1, descriptors2); } { const FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); const FeatureDescriptors descriptors2 = descriptors1.colwise().reverse(); SiftMatchingOptions match_options; const size_t num_matches = TestCPUvsGPU(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches, 50); } // Check the ratio test. { FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); FeatureDescriptors descriptors2 = descriptors1; SiftMatchingOptions match_options; const size_t num_matches1 = TestCPUvsGPU(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches1, 50); descriptors2.row(49) = descriptors2.row(0); descriptors2(0, 0) += 50.0f; descriptors2.row(0) = FeatureDescriptorsToUnsignedByte( descriptors2.row(0).cast().normalized()); descriptors2(49, 0) += 100.0f; descriptors2.row(49) = FeatureDescriptorsToUnsignedByte( descriptors2.row(49).cast().normalized()); match_options.max_ratio = 0.4; const size_t num_matches2 = TestCPUvsGPU(match_options, descriptors1.topRows(49), descriptors2); EXPECT_EQ(num_matches2, 48); match_options.max_ratio = 0.5; const size_t num_matches3 = TestCPUvsGPU(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches3, 49); } // Check the cross check. { FeatureDescriptors descriptors1 = CreateRandomFeatureDescriptors(50); FeatureDescriptors descriptors2 = descriptors1; descriptors1.row(0) = descriptors1.row(1); SiftMatchingOptions match_options; match_options.cross_check = false; const size_t num_matches1 = TestCPUvsGPU(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches1, 50); match_options.cross_check = true; const size_t num_matches2 = TestCPUvsGPU(match_options, descriptors1, descriptors2); EXPECT_EQ(num_matches2, 48); } } OpenGLContextManager opengl_context_; }; TestThread thread; RunThreadWithOpenGLContext(&thread); } TEST(MatchGuidedSiftFeaturesGPU, Nominal) { char app_name[] = "Test"; int argc = 1; char* argv[] = {app_name}; QApplication app(argc, argv); class TestThread : public Thread { private: void Run() { opengl_context_.MakeCurrent(); SiftMatchingOptions options; options.use_gpu = true; options.max_num_matches = 1000; auto matcher = CHECK_NOTNULL(CreateSiftFeatureMatcher(options)); auto empty_keypoints = std::make_shared(0); auto keypoints1 = std::make_shared(2); (*keypoints1)[0].x = 1; (*keypoints1)[1].x = 2; auto keypoints2 = std::make_shared(2); (*keypoints2)[0].x = 2; (*keypoints2)[1].x = 1; const auto empty_descriptors = std::make_shared(0, 128); const auto descriptors1 = std::make_shared( CreateRandomFeatureDescriptors(2)); const auto descriptors2 = std::make_shared( descriptors1->colwise().reverse()); TwoViewGeometry two_view_geometry; two_view_geometry.config = TwoViewGeometry::PLANAR_OR_PANORAMIC; two_view_geometry.H = Eigen::Matrix3d::Identity(); matcher->MatchGuided(TwoViewGeometryOptions(), keypoints1, keypoints2, descriptors1, descriptors2, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 2); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 0); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx2, 0); matcher->MatchGuided(TwoViewGeometryOptions(), nullptr, nullptr, nullptr, nullptr, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 2); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 0); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx2, 0); matcher->MatchGuided(TwoViewGeometryOptions(), keypoints1, nullptr, descriptors1, nullptr, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 2); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 0); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx2, 0); matcher->MatchGuided(TwoViewGeometryOptions(), nullptr, keypoints2, nullptr, descriptors2, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 2); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 0); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx2, 0); (*keypoints1)[0].x = 100; matcher->MatchGuided(TwoViewGeometryOptions(), keypoints1, keypoints2, descriptors1, descriptors2, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 1); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 0); matcher->MatchGuided(TwoViewGeometryOptions(), empty_keypoints, keypoints2, empty_descriptors, descriptors2, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 0); matcher->MatchGuided(TwoViewGeometryOptions(), keypoints1, empty_keypoints, descriptors1, empty_descriptors, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 0); matcher->MatchGuided(TwoViewGeometryOptions(), empty_keypoints, empty_keypoints, empty_descriptors, empty_descriptors, &two_view_geometry); EXPECT_EQ(two_view_geometry.inlier_matches.size(), 0); } OpenGLContextManager opengl_context_; }; TestThread thread; RunThreadWithOpenGLContext(&thread); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/feature/types.cc000066400000000000000000000104371454702036400201610ustar00rootroot00000000000000// 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. #include "colmap/feature/types.h" #include "colmap/util/logging.h" namespace colmap { FeatureKeypoint::FeatureKeypoint() : FeatureKeypoint(0, 0) {} FeatureKeypoint::FeatureKeypoint(const float x, const float y) : FeatureKeypoint(x, y, 1, 0, 0, 1) {} FeatureKeypoint::FeatureKeypoint(const float x_, const float y_, const float scale, const float orientation) : x(x_), y(y_) { CHECK_GE(scale, 0.0); const float scale_cos_orientation = scale * std::cos(orientation); const float scale_sin_orientation = scale * std::sin(orientation); a11 = scale_cos_orientation; a12 = -scale_sin_orientation; a21 = scale_sin_orientation; a22 = scale_cos_orientation; } FeatureKeypoint::FeatureKeypoint(const float x_, const float y_, const float a11_, const float a12_, const float a21_, const float a22_) : x(x_), y(y_), a11(a11_), a12(a12_), a21(a21_), a22(a22_) {} FeatureKeypoint FeatureKeypoint::FromShapeParameters(const float x, const float y, const float scale_x, const float scale_y, const float orientation, const float shear) { CHECK_GE(scale_x, 0.0); CHECK_GE(scale_y, 0.0); return FeatureKeypoint(x, y, scale_x * std::cos(orientation), -scale_y * std::sin(orientation + shear), scale_x * std::sin(orientation), scale_y * std::cos(orientation + shear)); } void FeatureKeypoint::Rescale(const float scale) { Rescale(scale, scale); } void FeatureKeypoint::Rescale(const float scale_x, const float scale_y) { CHECK_GT(scale_x, 0); CHECK_GT(scale_y, 0); x *= scale_x; y *= scale_y; a11 *= scale_x; a12 *= scale_y; a21 *= scale_x; a22 *= scale_y; } float FeatureKeypoint::ComputeScale() const { return (ComputeScaleX() + ComputeScaleY()) / 2.0f; } float FeatureKeypoint::ComputeScaleX() const { return std::sqrt(a11 * a11 + a21 * a21); } float FeatureKeypoint::ComputeScaleY() const { return std::sqrt(a12 * a12 + a22 * a22); } float FeatureKeypoint::ComputeOrientation() const { return std::atan2(a21, a11); } float FeatureKeypoint::ComputeShear() const { return std::atan2(-a12, a22) - ComputeOrientation(); } } // namespace colmap colmap-3.9.1/src/colmap/feature/types.h000066400000000000000000000074001454702036400200170ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { struct FeatureKeypoint { FeatureKeypoint(); FeatureKeypoint(float x, float y); FeatureKeypoint(float x, float y, float scale, float orientation); FeatureKeypoint(float x, float y, float a11, float a12, float a21, float a22); static FeatureKeypoint FromShapeParameters(float x, float y, float scale_x, float scale_y, float orientation, float shear); // Rescale the feature location and shape size by the given scale factor. void Rescale(float scale); void Rescale(float scale_x, float scale_y); // Compute shape parameters from affine shape. float ComputeScale() const; float ComputeScaleX() const; float ComputeScaleY() const; float ComputeOrientation() const; float ComputeShear() const; // Location of the feature, with the origin at the upper left image corner, // i.e. the upper left pixel has the coordinate (0.5, 0.5). float x; float y; // Affine shape of the feature. float a11; float a12; float a21; float a22; }; typedef Eigen::Matrix FeatureDescriptor; typedef std::vector FeatureKeypoints; typedef Eigen::Matrix FeatureDescriptors; typedef Eigen::Matrix FeatureDescriptorsFloat; struct FeatureMatch { FeatureMatch() : point2D_idx1(kInvalidPoint2DIdx), point2D_idx2(kInvalidPoint2DIdx) {} FeatureMatch(const point2D_t point2D_idx1, const point2D_t point2D_idx2) : point2D_idx1(point2D_idx1), point2D_idx2(point2D_idx2) {} // Feature index in first image. point2D_t point2D_idx1 = kInvalidPoint2DIdx; // Feature index in second image. point2D_t point2D_idx2 = kInvalidPoint2DIdx; }; typedef std::vector FeatureMatches; } // namespace colmap colmap-3.9.1/src/colmap/feature/types_test.cc000066400000000000000000000231041454702036400212130ustar00rootroot00000000000000// 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. #include "colmap/feature/types.h" #include "colmap/math/math.h" #include namespace colmap { namespace { TEST(FeatureKeypoints, Nominal) { FeatureKeypoint keypoint; EXPECT_EQ(keypoint.x, 0.0f); EXPECT_EQ(keypoint.y, 0.0f); EXPECT_EQ(keypoint.a11, 1.0f); EXPECT_EQ(keypoint.a12, 0.0f); EXPECT_EQ(keypoint.a21, 0.0f); EXPECT_EQ(keypoint.a22, 1.0f); FeatureKeypoints keypoints(1); EXPECT_EQ(keypoints.size(), 1); EXPECT_EQ(keypoints[0].x, 0.0f); EXPECT_EQ(keypoints[0].y, 0.0f); EXPECT_EQ(keypoints[0].a11, 1.0f); EXPECT_EQ(keypoints[0].a12, 0.0f); EXPECT_EQ(keypoints[0].a21, 0.0f); EXPECT_EQ(keypoints[0].a22, 1.0f); keypoint = FeatureKeypoint(1, 2); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_EQ(keypoint.a11, 1.0f); EXPECT_EQ(keypoint.a12, 0.0f); EXPECT_EQ(keypoint.a21, 0.0f); EXPECT_EQ(keypoint.a22, 1.0f); EXPECT_NEAR(keypoint.ComputeScale(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), 0.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), 0.0f, 1e-6); keypoint = FeatureKeypoint(1, 2, 0, 0); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_EQ(keypoint.a11, 0.0f); EXPECT_EQ(keypoint.a12, 0.0f); EXPECT_EQ(keypoint.a21, 0.0f); EXPECT_EQ(keypoint.a22, 0.0f); keypoint = FeatureKeypoint(1, 2, 1, 0); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_EQ(keypoint.a11, 1.0f); EXPECT_EQ(keypoint.a12, 0.0f); EXPECT_EQ(keypoint.a21, 0.0f); EXPECT_EQ(keypoint.a22, 1.0f); EXPECT_NEAR(keypoint.ComputeScale(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), 0.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), 0.0f, 1e-6); keypoint = FeatureKeypoint(1, 2, 1, M_PI / 2); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a12, -1.0f, 1e-6); EXPECT_NEAR(keypoint.a21, 1.0f, 1e-6); EXPECT_NEAR(keypoint.a22, 0.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 1.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), M_PI / 2, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), 0.0f, 1e-6); keypoint = FeatureKeypoint(1, 2, 2, M_PI / 2); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a12, -2.0f, 1e-6); EXPECT_NEAR(keypoint.a21, 2.0f, 1e-6); EXPECT_NEAR(keypoint.a22, 0.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), M_PI / 2, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), 0.0f, 1e-6); keypoint = FeatureKeypoint(1, 2, 2, M_PI); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, -2.0f, 1e-6); EXPECT_NEAR(keypoint.a12, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a21, 0.0, 1e-6); EXPECT_NEAR(keypoint.a22, -2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 2.0f, 1e-6); EXPECT_TRUE(std::abs(keypoint.ComputeOrientation() - M_PI) < 1e-6 || std::abs(keypoint.ComputeOrientation() + M_PI) < 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), 0.0f, 1e-6); keypoint = FeatureKeypoint::FromShapeParameters(1, 2, 2, 2, M_PI, 0); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, -2.0f, 1e-6); EXPECT_NEAR(keypoint.a12, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a21, 0.0, 1e-6); EXPECT_NEAR(keypoint.a22, -2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 2.0f, 1e-6); EXPECT_TRUE(std::abs(keypoint.ComputeOrientation() - M_PI) < 1e-6 || std::abs(keypoint.ComputeOrientation() + M_PI) < 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), 0.0f, 1e-6); keypoint = FeatureKeypoint::FromShapeParameters(1, 2, 2, 3, M_PI, 0); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, -2.0f, 1e-6); EXPECT_NEAR(keypoint.a12, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a21, 0.0, 1e-6); EXPECT_NEAR(keypoint.a22, -3.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 2.5f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 3.0f, 1e-6); EXPECT_TRUE(std::abs(keypoint.ComputeOrientation() - M_PI) < 1e-6 || std::abs(keypoint.ComputeOrientation() + M_PI) < 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), 0.0f, 1e-6); keypoint = FeatureKeypoint::FromShapeParameters(1, 2, 2, 3, -M_PI / 2, M_PI / 4); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a12, 2.12132025f, 1e-6); EXPECT_NEAR(keypoint.a21, -2.0f, 1e-6); EXPECT_NEAR(keypoint.a22, 2.12132025f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 2.5f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 3.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), -M_PI / 2, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), M_PI / 4, 1e-6); keypoint = FeatureKeypoint::FromShapeParameters(1, 2, 2, 3, M_PI / 2, M_PI / 4); EXPECT_EQ(keypoint.x, 1.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a12, -2.12132025f, 1e-6); EXPECT_NEAR(keypoint.a21, 2.0f, 1e-6); EXPECT_NEAR(keypoint.a22, -2.12132025f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 2.5f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 3.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), M_PI / 2, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), M_PI / 4, 1e-6); keypoint.Rescale(2, 2); EXPECT_EQ(keypoint.x, 2.0f); EXPECT_EQ(keypoint.y, 4.0f); EXPECT_NEAR(keypoint.a11, 2 * 0.0f, 1e-6); EXPECT_NEAR(keypoint.a12, 2 * -2.12132025f, 1e-6); EXPECT_NEAR(keypoint.a21, 2 * 2.0f, 1e-6); EXPECT_NEAR(keypoint.a22, 2 * -2.12132025f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 2 * 2.5f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX(), 2 * 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 2 * 3.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), M_PI / 2, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), M_PI / 4, 1e-6); keypoint.Rescale(1, 0.5); EXPECT_EQ(keypoint.x, 2.0f); EXPECT_EQ(keypoint.y, 2.0f); EXPECT_NEAR(keypoint.a11, 0.0f, 1e-6); EXPECT_NEAR(keypoint.a12, -2.12132025f, 1e-6); EXPECT_NEAR(keypoint.a21, 4.0f, 1e-6); EXPECT_NEAR(keypoint.a22, -2.12132025f, 1e-6); EXPECT_NEAR(keypoint.ComputeScale(), 3.5f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleX() - 2, 2.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeScaleY(), 3.0f, 1e-6); EXPECT_NEAR(keypoint.ComputeOrientation(), M_PI / 2, 1e-6); EXPECT_NEAR(keypoint.ComputeShear(), M_PI / 4, 1e-6); } TEST(FeatureDescriptors, Nominal) { FeatureDescriptors descriptors = FeatureDescriptors::Random(2, 3); EXPECT_EQ(descriptors.rows(), 2); EXPECT_EQ(descriptors.cols(), 3); EXPECT_EQ(descriptors(0, 0), descriptors.data()[0]); EXPECT_EQ(descriptors(0, 1), descriptors.data()[1]); EXPECT_EQ(descriptors(0, 2), descriptors.data()[2]); EXPECT_EQ(descriptors(1, 0), descriptors.data()[3]); EXPECT_EQ(descriptors(1, 1), descriptors.data()[4]); EXPECT_EQ(descriptors(1, 2), descriptors.data()[5]); } TEST(FeatureMatches, Nominal) { FeatureMatch match; EXPECT_EQ(match.point2D_idx1, kInvalidPoint2DIdx); EXPECT_EQ(match.point2D_idx2, kInvalidPoint2DIdx); FeatureMatches matches(1); EXPECT_EQ(matches.size(), 1); EXPECT_EQ(matches[0].point2D_idx1, kInvalidPoint2DIdx); EXPECT_EQ(matches[0].point2D_idx2, kInvalidPoint2DIdx); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/feature/utils.cc000066400000000000000000000103631454702036400201530ustar00rootroot00000000000000// 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. #include "colmap/feature/utils.h" #include "colmap/math/math.h" namespace colmap { std::vector FeatureKeypointsToPointsVector( const FeatureKeypoints& keypoints) { std::vector points(keypoints.size()); for (size_t i = 0; i < keypoints.size(); ++i) { points[i] = Eigen::Vector2d(keypoints[i].x, keypoints[i].y); } return points; } void L2NormalizeFeatureDescriptors(FeatureDescriptorsFloat* descriptors) { descriptors->rowwise().normalize(); } void L1RootNormalizeFeatureDescriptors(FeatureDescriptorsFloat* descriptors) { for (Eigen::MatrixXf::Index r = 0; r < descriptors->rows(); ++r) { descriptors->row(r) *= 1 / descriptors->row(r).lpNorm<1>(); descriptors->row(r) = descriptors->row(r).array().sqrt(); } } FeatureDescriptors FeatureDescriptorsToUnsignedByte( const Eigen::Ref& descriptors) { FeatureDescriptors descriptors_unsigned_byte(descriptors.rows(), descriptors.cols()); for (Eigen::MatrixXf::Index r = 0; r < descriptors.rows(); ++r) { for (Eigen::MatrixXf::Index c = 0; c < descriptors.cols(); ++c) { const float scaled_value = std::round(512.0f * descriptors(r, c)); descriptors_unsigned_byte(r, c) = TruncateCast(scaled_value); } } return descriptors_unsigned_byte; } void ExtractTopScaleFeatures(FeatureKeypoints* keypoints, FeatureDescriptors* descriptors, const size_t num_features) { CHECK_EQ(keypoints->size(), descriptors->rows()); CHECK_GT(num_features, 0); if (static_cast(descriptors->rows()) <= num_features) { return; } std::vector> scales; scales.reserve(keypoints->size()); for (size_t i = 0; i < keypoints->size(); ++i) { scales.emplace_back(i, (*keypoints)[i].ComputeScale()); } std::partial_sort(scales.begin(), scales.begin() + num_features, scales.end(), [](const std::pair& scale1, const std::pair& scale2) { return scale1.second > scale2.second; }); FeatureKeypoints top_scale_keypoints(num_features); FeatureDescriptors top_scale_descriptors(num_features, descriptors->cols()); for (size_t i = 0; i < num_features; ++i) { top_scale_keypoints[i] = (*keypoints)[scales[i].first]; top_scale_descriptors.row(i) = descriptors->row(scales[i].first); } *keypoints = std::move(top_scale_keypoints); *descriptors = std::move(top_scale_descriptors); } } // namespace colmap colmap-3.9.1/src/colmap/feature/utils.h000066400000000000000000000056151454702036400200210ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/types.h" namespace colmap { // Convert feature keypoints to vector of points. std::vector FeatureKeypointsToPointsVector( const FeatureKeypoints& keypoints); // L2-normalize feature descriptor, where each row represents one feature. void L2NormalizeFeatureDescriptors(FeatureDescriptorsFloat* descriptors); // L1-Root-normalize feature descriptors, where each row represents one feature. // See "Three things everyone should know to improve object retrieval", // Relja Arandjelovic and Andrew Zisserman, CVPR 2012. void L1RootNormalizeFeatureDescriptors(FeatureDescriptorsFloat* descriptors); // Convert normalized floating point feature descriptor to unsigned byte // representation by linear scaling from range [0, 0.5] to [0, 255]. Truncation // to a maximum value of 0.5 is used to avoid precision loss and follows the // common practice of representing SIFT vectors. FeatureDescriptors FeatureDescriptorsToUnsignedByte( const Eigen::Ref& descriptors); // Extract the descriptors corresponding to the largest-scale features. void ExtractTopScaleFeatures(FeatureKeypoints* keypoints, FeatureDescriptors* descriptors, size_t num_features); } // namespace colmap colmap-3.9.1/src/colmap/feature/utils_test.cc000066400000000000000000000110311454702036400212030ustar00rootroot00000000000000// 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. #include "colmap/feature/utils.h" #include namespace colmap { namespace { TEST(FeatureKeypointsToPointsVector, Nominal) { FeatureKeypoints keypoints(2); keypoints[1].x = 0.1; keypoints[1].y = 0.2; const std::vector points = FeatureKeypointsToPointsVector(keypoints); EXPECT_EQ(points[0], Eigen::Vector2d(0, 0)); EXPECT_EQ(points[1].cast(), Eigen::Vector2f(0.1, 0.2)); } TEST(L2NormalizeFeatureDescriptors, Nominal) { FeatureDescriptorsFloat descriptors = Eigen::MatrixXf::Random(100, 128); descriptors.array() += 1.0f; L2NormalizeFeatureDescriptors(&descriptors); for (Eigen::MatrixXf::Index r = 0; r < descriptors.rows(); ++r) { EXPECT_NEAR(descriptors.row(r).norm(), 1, 1e-6); } } TEST(L1RootNormalizeFeatureDescriptors, Nominal) { FeatureDescriptorsFloat descriptors = Eigen::MatrixXf::Random(100, 128); descriptors.array() += 1.0f; L1RootNormalizeFeatureDescriptors(&descriptors); for (Eigen::MatrixXf::Index r = 0; r < descriptors.rows(); ++r) { EXPECT_NEAR(descriptors.row(r).norm(), 1, 1e-6); } } TEST(FeatureDescriptorsToUnsignedByte, Nominal) { Eigen::MatrixXf descriptors = Eigen::MatrixXf::Random(100, 128); descriptors.array() += 1.0f; const FeatureDescriptors descriptors_uint8 = FeatureDescriptorsToUnsignedByte(descriptors); for (Eigen::MatrixXf::Index r = 0; r < descriptors.rows(); ++r) { for (Eigen::MatrixXf::Index c = 0; c < descriptors.cols(); ++c) { EXPECT_EQ(static_cast( std::min(255.0f, std::round(512.0f * descriptors(r, c)))), descriptors_uint8(r, c)); } } } TEST(ExtractTopScaleFeatures, Nominal) { FeatureKeypoints keypoints(5); keypoints[0].Rescale(3); keypoints[1].Rescale(4); keypoints[2].Rescale(1); keypoints[3].Rescale(5); keypoints[4].Rescale(2); const FeatureDescriptors descriptors = FeatureDescriptors::Random(5, 128); auto top_keypoints2 = keypoints; auto top_descriptors2 = descriptors; ExtractTopScaleFeatures(&top_keypoints2, &top_descriptors2, 2); EXPECT_EQ(top_keypoints2.size(), 2); EXPECT_EQ(top_keypoints2[0].ComputeScale(), keypoints[3].ComputeScale()); EXPECT_EQ(top_keypoints2[1].ComputeScale(), keypoints[1].ComputeScale()); EXPECT_EQ(top_descriptors2.rows(), 2); EXPECT_EQ(top_descriptors2.row(0), descriptors.row(3)); EXPECT_EQ(top_descriptors2.row(1), descriptors.row(1)); auto top_keypoints5 = keypoints; auto top_descriptors5 = descriptors; ExtractTopScaleFeatures(&top_keypoints5, &top_descriptors5, 5); EXPECT_EQ(top_keypoints5.size(), 5); EXPECT_EQ(top_descriptors5.rows(), 5); EXPECT_EQ(top_descriptors5, descriptors); auto top_keypoints6 = keypoints; auto top_descriptors6 = descriptors; ExtractTopScaleFeatures(&top_keypoints6, &top_descriptors6, 6); EXPECT_EQ(top_keypoints5.size(), 5); EXPECT_EQ(top_descriptors6.rows(), 5); EXPECT_EQ(top_descriptors6, descriptors); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/geometry/000077500000000000000000000000001454702036400167015ustar00rootroot00000000000000colmap-3.9.1/src/colmap/geometry/CMakeLists.txt000066400000000000000000000052231454702036400214430ustar00rootroot00000000000000# 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. set(FOLDER_NAME "geometry") COLMAP_ADD_LIBRARY( NAME colmap_geometry SRCS essential_matrix.h essential_matrix.cc gps.h gps.cc homography_matrix.h homography_matrix.cc pose.h pose.cc rigid3.h sim3.h sim3.cc triangulation.h triangulation.cc PUBLIC_LINK_LIBS colmap_util colmap_math Eigen3::Eigen ) COLMAP_ADD_TEST( NAME essential_matrix_utils_test SRCS essential_matrix_test.cc LINK_LIBS colmap_geometry ) COLMAP_ADD_TEST( NAME gps_test SRCS gps_test.cc LINK_LIBS colmap_geometry ) COLMAP_ADD_TEST( NAME homography_matrix_utils_test SRCS homography_matrix_test.cc LINK_LIBS colmap_geometry ) COLMAP_ADD_TEST( NAME pose_test SRCS pose_test.cc LINK_LIBS colmap_geometry ) COLMAP_ADD_TEST( NAME rigid3_test SRCS rigid3_test.cc LINK_LIBS colmap_geometry ) COLMAP_ADD_TEST( NAME sim3_test SRCS sim3_test.cc LINK_LIBS colmap_geometry ) COLMAP_ADD_TEST( NAME triangulation_test SRCS triangulation_test.cc LINK_LIBS colmap_geometry ) colmap-3.9.1/src/colmap/geometry/essential_matrix.cc000066400000000000000000000124171454702036400225700ustar00rootroot00000000000000// 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. #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/pose.h" #include #include namespace colmap { void DecomposeEssentialMatrix(const Eigen::Matrix3d& E, Eigen::Matrix3d* R1, Eigen::Matrix3d* R2, Eigen::Vector3d* t) { Eigen::JacobiSVD svd( E, Eigen::ComputeFullU | Eigen::ComputeFullV); Eigen::Matrix3d U = svd.matrixU(); Eigen::Matrix3d V = svd.matrixV().transpose(); if (U.determinant() < 0) { U *= -1; } if (V.determinant() < 0) { V *= -1; } Eigen::Matrix3d W; W << 0, 1, 0, -1, 0, 0, 0, 0, 1; *R1 = U * W * V; *R2 = U * W.transpose() * V; *t = U.col(2).normalized(); } void PoseFromEssentialMatrix(const Eigen::Matrix3d& E, const std::vector& points1, const std::vector& points2, Eigen::Matrix3d* R, Eigen::Vector3d* t, std::vector* points3D) { CHECK_EQ(points1.size(), points2.size()); Eigen::Matrix3d R1; Eigen::Matrix3d R2; DecomposeEssentialMatrix(E, &R1, &R2, t); // Generate all possible projection matrix combinations. const std::array R_cmbs{{R1, R2, R1, R2}}; const std::array t_cmbs{{*t, *t, -*t, -*t}}; points3D->clear(); for (size_t i = 0; i < R_cmbs.size(); ++i) { std::vector points3D_cmb; CheckCheirality(R_cmbs[i], t_cmbs[i], points1, points2, &points3D_cmb); if (points3D_cmb.size() >= points3D->size()) { *R = R_cmbs[i]; *t = t_cmbs[i]; *points3D = points3D_cmb; } } } Eigen::Matrix3d EssentialMatrixFromPose(const Rigid3d& cam2_from_cam1) { return CrossProductMatrix(cam2_from_cam1.translation.normalized()) * cam2_from_cam1.rotation.toRotationMatrix(); } void FindOptimalImageObservations(const Eigen::Matrix3d& E, const Eigen::Vector2d& point1, const Eigen::Vector2d& point2, Eigen::Vector2d* optimal_point1, Eigen::Vector2d* optimal_point2) { const Eigen::Vector3d& point1h = point1.homogeneous(); const Eigen::Vector3d& point2h = point2.homogeneous(); Eigen::Matrix S; S << 1, 0, 0, 0, 1, 0; // Epipolar lines. Eigen::Vector2d n1 = S * E * point2h; Eigen::Vector2d n2 = S * E.transpose() * point1h; const Eigen::Matrix2d E_tilde = E.block<2, 2>(0, 0); const double a = n1.transpose() * E_tilde * n2; const double b = (n1.squaredNorm() + n2.squaredNorm()) / 2.0; const double c = point1h.transpose() * E * point2h; const double d = std::sqrt(b * b - a * c); double lambda = c / (b + d); n1 -= E_tilde * lambda * n1; n2 -= E_tilde.transpose() * lambda * n2; lambda *= (2.0 * d) / (n1.squaredNorm() + n2.squaredNorm()); *optimal_point1 = (point1h - S.transpose() * lambda * n1).hnormalized(); *optimal_point2 = (point2h - S.transpose() * lambda * n2).hnormalized(); } Eigen::Vector3d EpipoleFromEssentialMatrix(const Eigen::Matrix3d& E, const bool left_image) { Eigen::Vector3d e; if (left_image) { Eigen::JacobiSVD svd(E, Eigen::ComputeFullV); e = svd.matrixV().block<3, 1>(0, 2); } else { Eigen::JacobiSVD svd(E.transpose(), Eigen::ComputeFullV); e = svd.matrixV().block<3, 1>(0, 2); } return e; } Eigen::Matrix3d InvertEssentialMatrix(const Eigen::Matrix3d& E) { return E.transpose(); } } // namespace colmap colmap-3.9.1/src/colmap/geometry/essential_matrix.h000066400000000000000000000132511454702036400224270ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Decompose an essential matrix into the possible rotations and translations. // // The first pose is assumed to be P = [I | 0] and the set of four other // possible second poses are defined as: {[R1 | t], [R2 | t], // [R1 | -t], [R2 | -t]} // // @param E 3x3 essential matrix. // @param R1 First possible 3x3 rotation matrix. // @param R2 Second possible 3x3 rotation matrix. // @param t 3x1 possible translation vector (also -t possible). void DecomposeEssentialMatrix(const Eigen::Matrix3d& E, Eigen::Matrix3d* R1, Eigen::Matrix3d* R2, Eigen::Vector3d* t); // Recover the most probable pose from the given essential matrix. // // The pose of the first image is assumed to be P = [I | 0]. // // @param E 3x3 essential matrix. // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param inlier_mask Only points with `true` in the inlier mask are // considered in the cheirality test. Size of the // inlier mask must match the number of points N. // @param R Most probable 3x3 rotation matrix. // @param t Most probable 3x1 translation vector. // @param points3D Triangulated 3D points infront of camera. void PoseFromEssentialMatrix(const Eigen::Matrix3d& E, const std::vector& points1, const std::vector& points2, Eigen::Matrix3d* R, Eigen::Vector3d* t, std::vector* points3D); // Compose essential matrix from relative camera poses. // // Assumes that first camera pose has projection matrix P = [I | 0], and // pose of second camera is given as transformation from world to camera system. // // @param R 3x3 rotation matrix. // @param t 3x1 translation vector. // // @return 3x3 essential matrix. Eigen::Matrix3d EssentialMatrixFromPose(const Rigid3d& cam2_from_cam1); // Find optimal image points, such that: // // optimal_point1^t * E * optimal_point2 = 0 // // as described in: // // Lindstrom, P., "Triangulation made easy", // Computer Vision and Pattern Recognition (CVPR), // 2010 IEEE Conference on , vol., no., pp.1554,1561, 13-18 June 2010 // // @param E Essential or fundamental matrix. // @param point1 Corresponding 2D point in first image. // @param point2 Corresponding 2D point in second image. // @param optimal_point1 Estimated optimal image point in the first image. // @param optimal_point2 Estimated optimal image point in the second image. void FindOptimalImageObservations(const Eigen::Matrix3d& E, const Eigen::Vector2d& point1, const Eigen::Vector2d& point2, Eigen::Vector2d* optimal_point1, Eigen::Vector2d* optimal_point2); // Compute the location of the epipole in homogeneous coordinates. // // @param E 3x3 essential matrix. // @param left_image If true, epipole in left image is computed, // else in right image. // // @return Epipole in homogeneous coordinates. Eigen::Vector3d EpipoleFromEssentialMatrix(const Eigen::Matrix3d& E, bool left_image); // Invert the essential matrix, i.e. if the essential matrix E describes the // transformation from camera A to B, the inverted essential matrix E' describes // the transformation from camera B to A. // // @param E 3x3 essential matrix. // // @return Inverted essential matrix. Eigen::Matrix3d InvertEssentialMatrix(const Eigen::Matrix3d& matrix); } // namespace colmap colmap-3.9.1/src/colmap/geometry/essential_matrix_test.cc000066400000000000000000000142651454702036400236320ustar00rootroot00000000000000// 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. #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(DecomposeEssentialMatrix, Nominal) { const Rigid3d cam2_from_cam1(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d(0.5, 1, 1).normalized()); const Eigen::Matrix3d cam2_from_cam1_rot_mat = cam2_from_cam1.rotation.toRotationMatrix(); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_cam1); Eigen::Matrix3d R1; Eigen::Matrix3d R2; Eigen::Vector3d t; DecomposeEssentialMatrix(E, &R1, &R2, &t); EXPECT_TRUE((R1 - cam2_from_cam1_rot_mat).norm() < 1e-10 || (R2 - cam2_from_cam1_rot_mat).norm() < 1e-10); EXPECT_TRUE((t - cam2_from_cam1.translation).norm() < 1e-10 || (t + cam2_from_cam1.translation).norm() < 1e-10); } TEST(EssentialMatrixFromPose, Nominal) { EXPECT_EQ(EssentialMatrixFromPose(Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 0, 1))), (Eigen::MatrixXd(3, 3) << 0, -1, 0, 1, 0, 0, 0, 0, 0).finished()); EXPECT_EQ(EssentialMatrixFromPose(Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 0, 2))), (Eigen::MatrixXd(3, 3) << 0, -1, 0, 1, 0, 0, 0, 0, 0).finished()); } TEST(PoseFromEssentialMatrix, Nominal) { const Rigid3d cam1_from_world; const Rigid3d cam2_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d(1, 0, 0).normalized()); const Rigid3d cam2_from_cam1 = cam2_from_world * Inverse(cam1_from_world); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_cam1); std::vector points3D(4); points3D[0] = Eigen::Vector3d(0, 0, 1); points3D[1] = Eigen::Vector3d(0, 0.1, 1); points3D[2] = Eigen::Vector3d(0.1, 0, 1); points3D[3] = Eigen::Vector3d(0.1, 0.1, 1); std::vector points1(4); std::vector points2(4); for (size_t i = 0; i < points3D.size(); ++i) { points1[i] = (cam1_from_world * points3D[i]).hnormalized(); points2[i] = (cam2_from_world * points3D[i]).hnormalized(); } points3D.clear(); Eigen::Matrix3d R; Eigen::Vector3d t; PoseFromEssentialMatrix(E, points1, points2, &R, &t, &points3D); EXPECT_EQ(points3D.size(), 4); EXPECT_TRUE(R.isApprox(cam2_from_cam1.rotation.toRotationMatrix())); EXPECT_TRUE(t.isApprox(cam2_from_cam1.translation)); } TEST(FindOptimalImageObservations, Nominal) { const Rigid3d cam1_from_world; const Rigid3d cam2_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d(1, 0, 0).normalized()); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_world * Inverse(cam1_from_world)); std::vector points3D(4); points3D[0] = Eigen::Vector3d(0, 0, 1); points3D[1] = Eigen::Vector3d(0, 0.1, 1); points3D[2] = Eigen::Vector3d(0.1, 0, 1); points3D[3] = Eigen::Vector3d(0.1, 0.1, 1); // Test if perfect projection is equivalent to optimal image observations. for (size_t i = 0; i < points3D.size(); ++i) { const Eigen::Vector2d point1 = (cam1_from_world * points3D[i]).hnormalized(); const Eigen::Vector2d point2 = (cam2_from_world * points3D[i]).hnormalized(); Eigen::Vector2d optimal_point1; Eigen::Vector2d optimal_point2; FindOptimalImageObservations( E, point1, point2, &optimal_point1, &optimal_point2); EXPECT_TRUE(point1.isApprox(optimal_point1)); EXPECT_TRUE(point2.isApprox(optimal_point2)); } } TEST(EpipoleFromEssentialMatrix, Nominal) { const Rigid3d cam2_from_cam1(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 0, -1).normalized()); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_cam1); const Eigen::Vector3d left_epipole = EpipoleFromEssentialMatrix(E, true); const Eigen::Vector3d right_epipole = EpipoleFromEssentialMatrix(E, false); EXPECT_TRUE(left_epipole.isApprox(Eigen::Vector3d(0, 0, 1))); EXPECT_TRUE(right_epipole.isApprox(Eigen::Vector3d(0, 0, 1))); } TEST(InvertEssentialMatrix, Nominal) { for (size_t i = 1; i < 10; ++i) { const Rigid3d cam2_from_cam1( Eigen::Quaterniond(EulerAnglesToRotationMatrix(0, 0.1, 0)), Eigen::Vector3d(0, 0, i).normalized()); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_cam1); const Eigen::Matrix3d inv_inv_E = InvertEssentialMatrix(InvertEssentialMatrix(E)); EXPECT_TRUE(E.isApprox(inv_inv_E)); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/geometry/gps.cc000066400000000000000000000145401454702036400200050ustar00rootroot00000000000000// 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. #include "colmap/geometry/gps.h" #include "colmap/math/math.h" namespace colmap { GPSTransform::GPSTransform(const int ellipsoid) { switch (ellipsoid) { case GRS80: a_ = 6378137.0; f_ = 1.0 / 298.257222100882711243162837; // More accurate GRS80 ellipsoid b_ = (1.0 - f_) * a_; break; case WGS84: a_ = 6378137.0; f_ = 1.0 / 298.257223563; // The WGS84 ellipsoid b_ = (1.0 - f_) * a_; break; default: a_ = std::numeric_limits::quiet_NaN(); b_ = std::numeric_limits::quiet_NaN(); f_ = std::numeric_limits::quiet_NaN(); throw std::invalid_argument("Ellipsoid not defined"); } e2_ = f_ * (2.0 - f_); } std::vector GPSTransform::EllToXYZ( const std::vector& ell) const { std::vector xyz(ell.size()); for (size_t i = 0; i < ell.size(); ++i) { const double lat = DegToRad(ell[i](0)); const double lon = DegToRad(ell[i](1)); const double alt = ell[i](2); const double sin_lat = std::sin(lat); const double sin_lon = std::sin(lon); const double cos_lat = std::cos(lat); const double cos_lon = std::cos(lon); // Normalized radius const double N = a_ / std::sqrt(1 - e2_ * sin_lat * sin_lat); xyz[i](0) = (N + alt) * cos_lat * cos_lon; xyz[i](1) = (N + alt) * cos_lat * sin_lon; xyz[i](2) = (N * (1 - e2_) + alt) * sin_lat; } return xyz; } std::vector GPSTransform::XYZToEll( const std::vector& xyz) const { std::vector ell(xyz.size()); for (size_t i = 0; i < ell.size(); ++i) { const double x = xyz[i](0); const double y = xyz[i](1); const double z = xyz[i](2); const double radius_xy = std::sqrt(x * x + y * y); const double kEps = 1e-12; // Latitude double lat = std::atan2(z, radius_xy); double alt = 0.0; for (size_t j = 0; j < 100; ++j) { const double sin_lat0 = std::sin(lat); const double N = a_ / std::sqrt(1 - e2_ * sin_lat0 * sin_lat0); const double prev_alt = alt; alt = radius_xy / std::cos(lat) - N; const double prev_lat = lat; lat = std::atan((z / radius_xy) * 1 / (1 - e2_ * N / (N + alt))); if (std::abs(prev_lat - lat) < kEps && std::abs(prev_alt - alt) < kEps) { break; } } ell[i](0) = RadToDeg(lat); // Longitude ell[i](1) = RadToDeg(std::atan2(y, x)); // Alt ell[i](2) = alt; } return ell; } std::vector GPSTransform::EllToENU( const std::vector& ell, const double lat0, const double lon0) const { // Convert GPS (lat / lon / alt) to ECEF std::vector xyz = EllToXYZ(ell); return XYZToENU(xyz, lat0, lon0); } std::vector GPSTransform::XYZToENU( const std::vector& xyz, const double lat0, const double lon0) const { std::vector enu(xyz.size()); // https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_ECEF_to_ENU // ECEF to ENU Rot : const double cos_lat0 = std::cos(DegToRad(lat0)); const double sin_lat0 = std::sin(DegToRad(lat0)); const double cos_lon0 = std::cos(DegToRad(lon0)); const double sin_lon0 = std::sin(DegToRad(lon0)); Eigen::Matrix3d R; R << -sin_lon0, cos_lon0, 0., -sin_lat0 * cos_lon0, -sin_lat0 * sin_lon0, cos_lat0, cos_lat0 * cos_lon0, cos_lat0 * sin_lon0, sin_lat0; // Convert ECEF to ENU coords. (w.r.t. ECEF ref == xyz[0]) for (size_t i = 0; i < xyz.size(); ++i) { enu[i] = R * (xyz[i] - xyz[0]); } return enu; } std::vector GPSTransform::ENUToEll( const std::vector& enu, const double lat0, const double lon0, const double alt0) const { return XYZToEll(ENUToXYZ(enu, lat0, lon0, alt0)); } std::vector GPSTransform::ENUToXYZ( const std::vector& enu, const double lat0, const double lon0, const double alt0) const { std::vector xyz(enu.size()); // ECEF ref (origin) const Eigen::Vector3d xyz_ref = EllToXYZ({Eigen::Vector3d(lat0, lon0, alt0)})[0]; // ENU to ECEF Rot : const double cos_lat0 = std::cos(DegToRad(lat0)); const double sin_lat0 = std::sin(DegToRad(lat0)); const double cos_lon0 = std::cos(DegToRad(lon0)); const double sin_lon0 = std::sin(DegToRad(lon0)); Eigen::Matrix3d R; R << -sin_lon0, cos_lon0, 0., -sin_lat0 * cos_lon0, -sin_lat0 * sin_lon0, cos_lat0, cos_lat0 * cos_lon0, cos_lat0 * sin_lon0, sin_lat0; // R is ECEF to ENU so Transpose to get inverse R.transposeInPlace(); // Convert ENU to ECEF coords. for (size_t i = 0; i < enu.size(); ++i) { xyz[i] = (R * enu[i]) + xyz_ref; } return xyz; } } // namespace colmap colmap-3.9.1/src/colmap/geometry/gps.h000066400000000000000000000064461454702036400176550ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Transform ellipsoidal GPS coordinates to Cartesian GPS coordinate // representation and vice versa. class GPSTransform { public: enum ELLIPSOID { GRS80, WGS84 }; explicit GPSTransform(int ellipsoid = GRS80); std::vector EllToXYZ( const std::vector& ell) const; std::vector XYZToEll( const std::vector& xyz) const; // Convert GPS (lat / lon / alt) to ENU coords. with lat0 and lon0 // defining the origin of the ENU frame std::vector EllToENU(const std::vector& ell, double lat0, double lon0) const; std::vector XYZToENU(const std::vector& xyz, double lat0, double lon0) const; std::vector ENUToEll(const std::vector& enu, double lat0, double lon0, double alt0) const; std::vector ENUToXYZ(const std::vector& enu, double lat0, double lon0, double alt0) const; private: // Semimajor axis. double a_; // Semiminor axis. double b_; // Flattening. double f_; // Numerical eccentricity. double e2_; }; } // namespace colmap colmap-3.9.1/src/colmap/geometry/gps_test.cc000066400000000000000000000234301454702036400210420ustar00rootroot00000000000000// 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. #include "colmap/geometry/gps.h" #include namespace colmap { namespace { TEST(GPS, EllToXYZGRS80) { std::vector ell; ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); std::vector ref_xyz; ref_xyz.emplace_back( 4.1772397090808507e6, 0.85515377993121441e6, 4.7282674046563692e6); ref_xyz.emplace_back( 4.1772186604902023e6, 0.8551759313518483e6, 4.7282818502697079e6); GPSTransform gps_tform(GPSTransform::GRS80); const auto xyz = gps_tform.EllToXYZ(ell); for (size_t i = 0; i < ell.size(); ++i) { EXPECT_TRUE(xyz[i].isApprox(ref_xyz[i], 1e-8)); } } TEST(GPS, EllToXYZWGS84) { std::vector ell; ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); std::vector ref_xyz; ref_xyz.emplace_back( 4.177239709042750e6, 0.855153779923415e6, 4.728267404769168e6); ref_xyz.emplace_back( 4.177218660452103e6, 0.855175931344048e6, 4.728281850382507e6); GPSTransform gps_tform(GPSTransform::WGS84); const auto xyz = gps_tform.EllToXYZ(ell); for (size_t i = 0; i < ell.size(); ++i) { EXPECT_TRUE(xyz[i].isApprox(ref_xyz[i], 1e-8)); } } TEST(GPS, XYZToEll_GRS80) { std::vector xyz; xyz.emplace_back( 4.1772397090808507e6, 0.85515377993121441e6, 4.7282674046563692e6); xyz.emplace_back( 4.1772186604902023e6, 0.8551759313518483e6, 4.7282818502697079e6); std::vector ref_ell; ref_ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ref_ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); GPSTransform gps_tform(GPSTransform::GRS80); const auto ell = gps_tform.XYZToEll(xyz); for (size_t i = 0; i < xyz.size(); ++i) { EXPECT_TRUE(ell[i].isApprox(ref_ell[i], 1e-5)); } } TEST(GPS, XYZToEll_WGS84) { std::vector xyz; xyz.emplace_back( 4.177239709042750e6, 0.855153779923415e6, 4.728267404769168e6); xyz.emplace_back( 4.177218660452103e6, 0.855175931344048e6, 4.728281850382507e6); std::vector ref_ell; ref_ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ref_ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); GPSTransform gps_tform(GPSTransform::WGS84); const auto ell = gps_tform.XYZToEll(xyz); for (size_t i = 0; i < xyz.size(); ++i) { EXPECT_TRUE(ell[i].isApprox(ref_ell[i], 1e-5)); } } TEST(GPS, XYZToEllToXYZ_GRS80) { std::vector xyz; xyz.emplace_back( 4.177239709080851e6, 0.855153779931214e6, 4.728267404656370e6); xyz.emplace_back( 4.177218660490202e6, 0.855175931351848e6, 4.728281850269709e6); GPSTransform gps_tform(GPSTransform::GRS80); const auto ell = gps_tform.XYZToEll(xyz); const auto xyz2 = gps_tform.EllToXYZ(ell); for (size_t i = 0; i < xyz.size(); ++i) { EXPECT_TRUE(xyz[i].isApprox(xyz2[i], 1e-5)); } } TEST(GPS, XYZToEllToXYZ_WGS84) { std::vector xyz; xyz.emplace_back( 4.177239709080851e6, 0.855153779931214e6, 4.728267404656370e6); xyz.emplace_back( 4.177218660490202e6, 0.855175931351848e6, 4.728281850269709e6); GPSTransform gps_tform(GPSTransform::WGS84); const auto ell = gps_tform.XYZToEll(xyz); const auto xyz2 = gps_tform.EllToXYZ(ell); for (size_t i = 0; i < xyz.size(); ++i) { EXPECT_TRUE(xyz[i].isApprox(xyz2[i], 1e-5)); } } TEST(GPS, EllToENUWGS84) { std::vector ell; ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); std::vector ref_xyz; ref_xyz.emplace_back( 4.177239709042750e6, 0.855153779923415e6, 4.728267404769168e6); ref_xyz.emplace_back( 4.177218660452103e6, 0.855175931344048e6, 4.728281850382507e6); GPSTransform gps_tform(GPSTransform::WGS84); // Get lat0, lon0 origin from ref const auto ori_ell = gps_tform.XYZToEll({ref_xyz[0]})[0]; // Get ENU ref from ECEF ref const auto ref_enu = gps_tform.XYZToENU(ref_xyz, ori_ell(0), ori_ell(1)); // Get ENU from Ell const auto enu = gps_tform.EllToENU(ell, ori_ell(0), ori_ell(1)); for (size_t i = 0; i < ell.size(); ++i) { EXPECT_TRUE(enu[i].isApprox(ref_enu[i], 1e-8)); } } TEST(GPS, XYZToENU) { std::vector ell; ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); std::vector ref_xyz; ref_xyz.emplace_back( 4.177239709042750e6, 0.855153779923415e6, 4.728267404769168e6); ref_xyz.emplace_back( 4.177218660452103e6, 0.855175931344048e6, 4.728281850382507e6); GPSTransform gps_tform(GPSTransform::WGS84); const auto xyz = gps_tform.EllToXYZ(ell); // Get lat0, lon0 origin from ref const auto ori_ell = gps_tform.XYZToEll({ref_xyz[0]})[0]; // Get ENU from ECEF ref const auto ref_enu = gps_tform.XYZToENU(ref_xyz, ori_ell(0), ori_ell(1)); // Get ENU from ECEF const auto enu = gps_tform.XYZToENU(xyz, ori_ell(0), ori_ell(1)); for (size_t i = 0; i < ell.size(); ++i) { EXPECT_TRUE(enu[i].isApprox(ref_enu[i], 1e-8)); } } TEST(GPS, ENUToEllWGS84) { std::vector ref_ell; ref_ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ref_ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); std::vector xyz; xyz.emplace_back( 4.177239709042750e6, 0.855153779923415e6, 4.728267404769168e6); xyz.emplace_back( 4.177218660452103e6, 0.855175931344048e6, 4.728281850382507e6); GPSTransform gps_tform(GPSTransform::WGS84); // Get lat0, lon0 origin from ref const auto ori_ell = gps_tform.XYZToEll(xyz); const double lat0 = ori_ell[0](0); const double lon0 = ori_ell[0](1); const double alt0 = ori_ell[0](2); // Get ENU from ECEF const auto enu = gps_tform.XYZToENU(xyz, lat0, lon0); const auto xyz_enu = gps_tform.ENUToXYZ(enu, lat0, lon0, alt0); // Get Ell from ENU const auto ell = gps_tform.ENUToEll(enu, lat0, lon0, alt0); for (size_t i = 0; i < ell.size(); ++i) { EXPECT_TRUE(ell[i].isApprox(ref_ell[i], 1e-5)); } } TEST(GPS, ENUToXYZ) { std::vector ell; ell.emplace_back(48 + 8. / 60 + 51.70361 / 3600, 11 + 34. / 60 + 10.51777 / 3600, 561.1851); ell.emplace_back(48 + 8. / 60 + 52.40575 / 3600, 11 + 34. / 60 + 11.77179 / 3600, 561.1509); std::vector ref_xyz; ref_xyz.emplace_back( 4.177239709042750e6, 0.855153779923415e6, 4.728267404769168e6); ref_xyz.emplace_back( 4.177218660452103e6, 0.855175931344048e6, 4.728281850382507e6); GPSTransform gps_tform(GPSTransform::WGS84); // Get lat0, lon0 origin from Ell const double lat0 = ell[0](0); const double lon0 = ell[0](1); const double alt0 = ell[0](2); // Get ENU from Ell const auto enu = gps_tform.EllToENU(ell, lat0, lon0); // Get XYZ from ENU const auto xyz = gps_tform.ENUToXYZ(enu, lat0, lon0, alt0); for (size_t i = 0; i < ell.size(); ++i) { EXPECT_TRUE(xyz[i].isApprox(ref_xyz[i], 1e-8)); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/geometry/homography_matrix.cc000066400000000000000000000203071454702036400227530ustar00rootroot00000000000000// 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. #include "colmap/geometry/homography_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/math/math.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include #include namespace colmap { namespace { double ComputeOppositeOfMinor(const Eigen::Matrix3d& matrix, const size_t row, const size_t col) { const size_t col1 = col == 0 ? 1 : 0; const size_t col2 = col == 2 ? 1 : 2; const size_t row1 = row == 0 ? 1 : 0; const size_t row2 = row == 2 ? 1 : 2; return (matrix(row1, col2) * matrix(row2, col1) - matrix(row1, col1) * matrix(row2, col2)); } Eigen::Matrix3d ComputeHomographyRotation(const Eigen::Matrix3d& H_normalized, const Eigen::Vector3d& tstar, const Eigen::Vector3d& n, const double v) { return H_normalized * (Eigen::Matrix3d::Identity() - (2.0 / v) * tstar * n.transpose()); } } // namespace void DecomposeHomographyMatrix(const Eigen::Matrix3d& H, const Eigen::Matrix3d& K1, const Eigen::Matrix3d& K2, std::vector* R, std::vector* t, std::vector* n) { // Remove calibration from homography. Eigen::Matrix3d H_normalized = K2.inverse() * H * K1; // Remove scale from normalized homography. Eigen::JacobiSVD hmatrix_norm_svd(H_normalized); H_normalized.array() /= hmatrix_norm_svd.singularValues()[1]; // Ensure that we always return rotations, and never reflections. // // It's enough to take det(H_normalized) > 0. // // To see this: // - In the paper: R := H_normalized * (Id + x y^t)^{-1} (page 32). // - Can check that this implies that R is orthogonal: RR^t = Id. // - To return a rotation, we also need det(R) > 0. // - By Sylvester's idenitity: det(Id + x y^t) = (1 + x^t y), which // is positive by choice of x and y (page 24). // - So det(R) and det(H_normalized) have the same sign. if (H_normalized.determinant() < 0) { H_normalized.array() *= -1.0; } const Eigen::Matrix3d S = H_normalized.transpose() * H_normalized - Eigen::Matrix3d::Identity(); // Check if H is rotation matrix. const double kMinInfinityNorm = 1e-3; if (S.lpNorm() < kMinInfinityNorm) { *R = {H_normalized}; *t = {Eigen::Vector3d::Zero()}; *n = {Eigen::Vector3d::Zero()}; return; } const double M00 = ComputeOppositeOfMinor(S, 0, 0); const double M11 = ComputeOppositeOfMinor(S, 1, 1); const double M22 = ComputeOppositeOfMinor(S, 2, 2); const double rtM00 = std::sqrt(M00); const double rtM11 = std::sqrt(M11); const double rtM22 = std::sqrt(M22); const double M01 = ComputeOppositeOfMinor(S, 0, 1); const double M12 = ComputeOppositeOfMinor(S, 1, 2); const double M02 = ComputeOppositeOfMinor(S, 0, 2); const int e12 = SignOfNumber(M12); const int e02 = SignOfNumber(M02); const int e01 = SignOfNumber(M01); const double nS00 = std::abs(S(0, 0)); const double nS11 = std::abs(S(1, 1)); const double nS22 = std::abs(S(2, 2)); const std::array nS{{nS00, nS11, nS22}}; const size_t idx = std::distance(nS.begin(), std::max_element(nS.begin(), nS.end())); Eigen::Vector3d np1; Eigen::Vector3d np2; if (idx == 0) { np1[0] = S(0, 0); np2[0] = S(0, 0); np1[1] = S(0, 1) + rtM22; np2[1] = S(0, 1) - rtM22; np1[2] = S(0, 2) + e12 * rtM11; np2[2] = S(0, 2) - e12 * rtM11; } else if (idx == 1) { np1[0] = S(0, 1) + rtM22; np2[0] = S(0, 1) - rtM22; np1[1] = S(1, 1); np2[1] = S(1, 1); np1[2] = S(1, 2) - e02 * rtM00; np2[2] = S(1, 2) + e02 * rtM00; } else if (idx == 2) { np1[0] = S(0, 2) + e01 * rtM11; np2[0] = S(0, 2) - e01 * rtM11; np1[1] = S(1, 2) + rtM00; np2[1] = S(1, 2) - rtM00; np1[2] = S(2, 2); np2[2] = S(2, 2); } const double traceS = S.trace(); const double v = 2.0 * std::sqrt(1.0 + traceS - M00 - M11 - M22); const double ESii = SignOfNumber(S(idx, idx)); const double r_2 = 2 + traceS + v; const double nt_2 = 2 + traceS - v; const double r = std::sqrt(r_2); const double n_t = std::sqrt(nt_2); const Eigen::Vector3d n1 = np1.normalized(); const Eigen::Vector3d n2 = np2.normalized(); const double half_nt = 0.5 * n_t; const double esii_t_r = ESii * r; const Eigen::Vector3d t1_star = half_nt * (esii_t_r * n2 - n_t * n1); const Eigen::Vector3d t2_star = half_nt * (esii_t_r * n1 - n_t * n2); const Eigen::Matrix3d R1 = ComputeHomographyRotation(H_normalized, t1_star, n1, v); const Eigen::Vector3d t1 = R1 * t1_star; const Eigen::Matrix3d R2 = ComputeHomographyRotation(H_normalized, t2_star, n2, v); const Eigen::Vector3d t2 = R2 * t2_star; *R = {R1, R1, R2, R2}; *t = {t1, -t1, t2, -t2}; *n = {-n1, n1, -n2, n2}; } void PoseFromHomographyMatrix(const Eigen::Matrix3d& H, const Eigen::Matrix3d& K1, const Eigen::Matrix3d& K2, const std::vector& points1, const std::vector& points2, Eigen::Matrix3d* R, Eigen::Vector3d* t, Eigen::Vector3d* n, std::vector* points3D) { CHECK_EQ(points1.size(), points2.size()); std::vector R_cmbs; std::vector t_cmbs; std::vector n_cmbs; DecomposeHomographyMatrix(H, K1, K2, &R_cmbs, &t_cmbs, &n_cmbs); points3D->clear(); for (size_t i = 0; i < R_cmbs.size(); ++i) { std::vector points3D_cmb; CheckCheirality(R_cmbs[i], t_cmbs[i], points1, points2, &points3D_cmb); if (points3D_cmb.size() >= points3D->size()) { *R = R_cmbs[i]; *t = t_cmbs[i]; *n = n_cmbs[i]; *points3D = points3D_cmb; } } } Eigen::Matrix3d HomographyMatrixFromPose(const Eigen::Matrix3d& K1, const Eigen::Matrix3d& K2, const Eigen::Matrix3d& R, const Eigen::Vector3d& t, const Eigen::Vector3d& n, const double d) { CHECK_GT(d, 0); return K2 * (R - t * n.normalized().transpose() / d) * K1.inverse(); } } // namespace colmap colmap-3.9.1/src/colmap/geometry/homography_matrix.h000066400000000000000000000121071454702036400226140ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Decompose an homography matrix into the possible rotations, translations, // and plane normal vectors, according to: // // Malis, Ezio, and Manuel Vargas. "Deeper understanding of the homography // decomposition for vision-based control." (2007): 90. // // The first pose is assumed to be P = [I | 0]. Note that the homography is // plane-induced if `R.size() == t.size() == n.size() == 4`. If `R.size() == // t.size() == n.size() == 1` the homography is pure-rotational. // // @param H 3x3 homography matrix. // @param K 3x3 calibration matrix. // @param R Possible 3x3 rotation matrices. // @param t Possible translation vectors. // @param n Possible normal vectors. void DecomposeHomographyMatrix(const Eigen::Matrix3d& H, const Eigen::Matrix3d& K1, const Eigen::Matrix3d& K2, std::vector* R, std::vector* t, std::vector* n); // Recover the most probable pose from the given homography matrix. // // The pose of the first image is assumed to be P = [I | 0]. // // @param H 3x3 homography matrix. // @param K1 3x3 calibration matrix of first camera. // @param K2 3x3 calibration matrix of second camera. // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param inlier_mask Only points with `true` in the inlier mask are // considered in the cheirality test. Size of the // inlier mask must match the number of points N. // @param R Most probable 3x3 rotation matrix. // @param t Most probable 3x1 translation vector. // @param n Most probable 3x1 normal vector. // @param points3D Triangulated 3D points infront of camera // (only if homography is not pure-rotational). void PoseFromHomographyMatrix(const Eigen::Matrix3d& H, const Eigen::Matrix3d& K1, const Eigen::Matrix3d& K2, const std::vector& points1, const std::vector& points2, Eigen::Matrix3d* R, Eigen::Vector3d* t, Eigen::Vector3d* n, std::vector* points3D); // Compose homography matrix from relative pose. // // @param K1 3x3 calibration matrix of first camera. // @param K2 3x3 calibration matrix of second camera. // @param R Most probable 3x3 rotation matrix. // @param t Most probable 3x1 translation vector. // @param n Most probable 3x1 normal vector. // @param d Orthogonal distance from plane. // // @return 3x3 homography matrix. Eigen::Matrix3d HomographyMatrixFromPose(const Eigen::Matrix3d& K1, const Eigen::Matrix3d& K2, const Eigen::Matrix3d& R, const Eigen::Vector3d& t, const Eigen::Vector3d& n, double d); } // namespace colmap colmap-3.9.1/src/colmap/geometry/homography_matrix_test.cc000066400000000000000000000140711454702036400240130ustar00rootroot00000000000000// 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. #include "colmap/geometry/homography_matrix.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { namespace { // Note that the test case values are obtained from OpenCV. TEST(DecomposeHomographyMatrix, Nominal) { Eigen::Matrix3d H; H << 2.649157564634028, 4.583875997496426, 70.694447785121326, -1.072756858861583, 3.533262150437228, 1513.656999614321649, 0.001303887589576, 0.003042206876298, 1; H *= 3; Eigen::Matrix3d K; K << 640, 0, 320, 0, 640, 240, 0, 0, 1; std::vector R; std::vector t; std::vector n; DecomposeHomographyMatrix(H, K, K, &R, &t, &n); EXPECT_EQ(R.size(), 4); EXPECT_EQ(t.size(), 4); EXPECT_EQ(n.size(), 4); Eigen::Matrix3d R_ref; R_ref << 0.43307983549125, 0.545749113549648, -0.717356090899523, -0.85630229674426, 0.497582023798831, -0.138414255706431, 0.281404038139784, 0.67421809131173, 0.682818960388909; const Eigen::Vector3d t_ref( 1.826751712278038, 1.264718492450820, 0.195080809998819); const Eigen::Vector3d n_ref( -0.244875830334816, -0.480857890778889, -0.841909446789566); bool ref_solution_exists = false; for (size_t i = 0; i < 4; ++i) { const double kEps = 1e-6; if ((R[i] - R_ref).norm() < kEps && (t[i] - t_ref).norm() < kEps && (n[i] - n_ref).norm() < kEps) { ref_solution_exists = true; } } EXPECT_TRUE(ref_solution_exists); } TEST(DecomposeHomographyMatrix, Random) { const int numIters = 100; const double epsilon = 1e-6; const Eigen::Matrix3d id3 = Eigen::Matrix3d::Identity(); for (int i = 0; i < numIters; ++i) { const Eigen::Matrix3d H = Eigen::Matrix3d::Random(); if (std::abs(H.determinant()) < epsilon) { continue; } std::vector R; std::vector t; std::vector n; DecomposeHomographyMatrix(H, id3, id3, &R, &t, &n); EXPECT_EQ(R.size(), 4); EXPECT_EQ(t.size(), 4); EXPECT_EQ(n.size(), 4); // Test that each candidate rotation is a rotation for (const Eigen::Matrix3d& candidate_R : R) { const Eigen::Matrix3d orthog_error = candidate_R.transpose() * candidate_R - id3; // Check that candidate_R is an orthognal matrix EXPECT_LT(orthog_error.lpNorm(), epsilon); // Check determinant is 1 EXPECT_NEAR(candidate_R.determinant(), 1.0, epsilon); } } } TEST(PoseFromHomographyMatrix, Nominal) { const Eigen::Matrix3d K1 = Eigen::Matrix3d::Identity(); const Eigen::Matrix3d K2 = Eigen::Matrix3d::Identity(); const Eigen::Matrix3d R_ref = Eigen::Matrix3d::Identity(); const Eigen::Vector3d t_ref(1, 0, 0); const Eigen::Vector3d n_ref(-1, 0, 0); const double d_ref = 1; const Eigen::Matrix3d H = HomographyMatrixFromPose(K1, K2, R_ref, t_ref, n_ref, d_ref); std::vector points1; points1.emplace_back(0.1, 0.4); points1.emplace_back(0.2, 0.3); points1.emplace_back(0.3, 0.2); points1.emplace_back(0.4, 0.1); std::vector points2; for (const auto& point1 : points1) { const Eigen::Vector3d point2 = H * point1.homogeneous(); points2.push_back(point2.hnormalized()); } Eigen::Matrix3d R; Eigen::Vector3d t; Eigen::Vector3d n; std::vector points3D; PoseFromHomographyMatrix(H, K1, K2, points1, points2, &R, &t, &n, &points3D); EXPECT_EQ(R, R_ref); EXPECT_EQ(t, t_ref); EXPECT_EQ(n, n_ref); EXPECT_EQ(points3D.size(), points1.size()); } TEST(HomographyMatrixFromPose, PureRotation) { const Eigen::Matrix3d K1 = Eigen::Matrix3d::Identity(); const Eigen::Matrix3d K2 = Eigen::Matrix3d::Identity(); const Eigen::Matrix3d R = Eigen::Matrix3d::Identity(); const Eigen::Vector3d t(0, 0, 0); const Eigen::Vector3d n(-1, 0, 0); const double d = 1; const Eigen::Matrix3d H = HomographyMatrixFromPose(K1, K2, R, t, n, d); EXPECT_EQ(H, Eigen::Matrix3d::Identity()); } TEST(HomographyMatrixFromPose, PlanarScene) { const Eigen::Matrix3d K1 = Eigen::Matrix3d::Identity(); const Eigen::Matrix3d K2 = Eigen::Matrix3d::Identity(); const Eigen::Matrix3d R = Eigen::Matrix3d::Identity(); const Eigen::Vector3d t(1, 0, 0); const Eigen::Vector3d n(-1, 0, 0); const double d = 1; const Eigen::Matrix3d H = HomographyMatrixFromPose(K1, K2, R, t, n, d); Eigen::Matrix3d H_ref; H_ref << 2, 0, 0, 0, 1, 0, 0, 0, 1; EXPECT_EQ(H, H_ref); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/geometry/pose.cc000066400000000000000000000157551454702036400201730ustar00rootroot00000000000000// 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. #include "colmap/geometry/pose.h" #include "colmap/geometry/triangulation.h" #include "colmap/math/matrix.h" #include "colmap/util/eigen_alignment.h" #include namespace colmap { Eigen::Matrix3d ComputeClosestRotationMatrix(const Eigen::Matrix3d& matrix) { const Eigen::JacobiSVD svd( matrix, Eigen::ComputeFullU | Eigen::ComputeFullV); Eigen::Matrix3d R = svd.matrixU() * (svd.matrixV().transpose()); if (R.determinant() < 0.0) { R *= -1.0; } return R; } bool DecomposeProjectionMatrix(const Eigen::Matrix3x4d& P, Eigen::Matrix3d* K, Eigen::Matrix3d* R, Eigen::Vector3d* T) { Eigen::Matrix3d RR; Eigen::Matrix3d QQ; DecomposeMatrixRQ(P.leftCols<3>().eval(), &RR, &QQ); *R = ComputeClosestRotationMatrix(QQ); const double det_K = RR.determinant(); if (det_K == 0) { return false; } else if (det_K > 0) { *K = RR; } else { *K = -RR; } for (int i = 0; i < 3; ++i) { if ((*K)(i, i) < 0.0) { K->col(i) = -K->col(i); R->row(i) = -R->row(i); } } *T = K->triangularView().solve(P.col(3)); if (det_K < 0) { *T = -(*T); } return true; } Eigen::Matrix3d CrossProductMatrix(const Eigen::Vector3d& vector) { Eigen::Matrix3d matrix; matrix << 0, -vector(2), vector(1), vector(2), 0, -vector(0), -vector(1), vector(0), 0; return matrix; } void RotationMatrixToEulerAngles(const Eigen::Matrix3d& R, double* rx, double* ry, double* rz) { *rx = std::atan2(R(2, 1), R(2, 2)); *ry = std::asin(-R(2, 0)); *rz = std::atan2(R(1, 0), R(0, 0)); *rx = std::isnan(*rx) ? 0 : *rx; *ry = std::isnan(*ry) ? 0 : *ry; *rz = std::isnan(*rz) ? 0 : *rz; } Eigen::Matrix3d EulerAnglesToRotationMatrix(const double rx, const double ry, const double rz) { const Eigen::Matrix3d Rx = Eigen::AngleAxisd(rx, Eigen::Vector3d::UnitX()).toRotationMatrix(); const Eigen::Matrix3d Ry = Eigen::AngleAxisd(ry, Eigen::Vector3d::UnitY()).toRotationMatrix(); const Eigen::Matrix3d Rz = Eigen::AngleAxisd(rz, Eigen::Vector3d::UnitZ()).toRotationMatrix(); return Rz * Ry * Rx; } Eigen::Quaterniond AverageQuaternions( const std::vector& quats, const std::vector& weights) { CHECK_EQ(quats.size(), weights.size()); CHECK_GT(quats.size(), 0); if (quats.size() == 1) { return quats[0]; } Eigen::Matrix4d A = Eigen::Matrix4d::Zero(); double weight_sum = 0; for (size_t i = 0; i < quats.size(); ++i) { CHECK_GT(weights[i], 0); const Eigen::Vector4d qvec = quats[i].normalized().coeffs(); A += weights[i] * qvec * qvec.transpose(); weight_sum += weights[i]; } A.array() /= weight_sum; const Eigen::Matrix4d eigenvectors = Eigen::SelfAdjointEigenSolver(A).eigenvectors(); const Eigen::Vector4d average_qvec = eigenvectors.col(3); return Eigen::Quaterniond( average_qvec(3), average_qvec(0), average_qvec(1), average_qvec(2)); } Rigid3d InterpolateCameraPoses(const Rigid3d& cam_from_world1, const Rigid3d& cam_from_world2, double t) { const Eigen::Vector3d translation12 = cam_from_world2.translation - cam_from_world1.translation; return Rigid3d(cam_from_world1.rotation.slerp(t, cam_from_world2.rotation), cam_from_world1.translation + translation12 * t); } namespace { double CalculateDepth(const Eigen::Matrix3x4d& cam_from_world, const Eigen::Vector3d& point3D) { const double proj_z = cam_from_world.row(2).dot(point3D.homogeneous()); return proj_z * cam_from_world.col(2).norm(); } } // namespace bool CheckCheirality(const Eigen::Matrix3d& R, const Eigen::Vector3d& t, const std::vector& points1, const std::vector& points2, std::vector* points3D) { CHECK_EQ(points1.size(), points2.size()); const Eigen::Matrix3x4d proj_matrix1 = Eigen::Matrix3x4d::Identity(); Eigen::Matrix3x4d proj_matrix2; proj_matrix2.leftCols<3>() = R; proj_matrix2.col(3) = t; const double kMinDepth = std::numeric_limits::epsilon(); const double max_depth = 1000.0f * (R.transpose() * t).norm(); points3D->clear(); for (size_t i = 0; i < points1.size(); ++i) { const Eigen::Vector3d point3D = TriangulatePoint(proj_matrix1, proj_matrix2, points1[i], points2[i]); const double depth1 = CalculateDepth(proj_matrix1, point3D); if (depth1 > kMinDepth && depth1 < max_depth) { const double depth2 = CalculateDepth(proj_matrix2, point3D); if (depth2 > kMinDepth && depth2 < max_depth) { points3D->push_back(point3D); } } } return !points3D->empty(); } Rigid3d TransformCameraWorld(const Sim3d& new_from_old_world, const Rigid3d& cam_from_world) { const Sim3d cam_from_new_world = Sim3d(1, cam_from_world.rotation, cam_from_world.translation) * Inverse(new_from_old_world); return Rigid3d(cam_from_new_world.rotation, cam_from_new_world.translation * new_from_old_world.scale); } } // namespace colmap colmap-3.9.1/src/colmap/geometry/pose.h000066400000000000000000000116531454702036400200260ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/geometry/sim3.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Compute the closes rotation matrix with the closest Frobenius norm by setting // the singular values of the given matrix to 1. Eigen::Matrix3d ComputeClosestRotationMatrix(const Eigen::Matrix3d& matrix); // Decompose projection matrix into intrinsic camera matrix, rotation matrix and // translation vector. Returns false if decomposition fails. bool DecomposeProjectionMatrix(const Eigen::Matrix3x4d& proj_matrix, Eigen::Matrix3d* K, Eigen::Matrix3d* R, Eigen::Vector3d* T); // Compose the skew symmetric cross product matrix from a vector. Eigen::Matrix3d CrossProductMatrix(const Eigen::Vector3d& vector); // Convert 3D rotation matrix to Euler angles. // // The convention `R = Rx * Ry * Rz` is used, // using a right-handed coordinate system. // // @param R 3x3 rotation matrix. // @param rx, ry, rz Euler angles in radians. void RotationMatrixToEulerAngles(const Eigen::Matrix3d& R, double* rx, double* ry, double* rz); // Convert Euler angles to 3D rotation matrix. // // The convention `R = Rz * Ry * Rx` is used, // using a right-handed coordinate system. // // @param rx, ry, rz Euler angles in radians. // // @return 3x3 rotation matrix. Eigen::Matrix3d EulerAnglesToRotationMatrix(double rx, double ry, double rz); // Compute the weighted average of multiple Quaternions according to: // // Markley, F. Landis, et al. "Averaging quaternions." // Journal of Guidance, Control, and Dynamics 30.4 (2007): 1193-1197. // // @param quats The Quaternions to be averaged. // @param weights Non-negative weights. // // @return The average Quaternion. Eigen::Quaterniond AverageQuaternions( const std::vector& quats, const std::vector& weights); // Linearly interpolate camera pose. Rigid3d InterpolateCameraPoses(const Rigid3d& cam_from_world1, const Rigid3d& cam_from_world2, double t); // Perform cheirality constraint test, i.e., determine which of the triangulated // correspondences lie in front of of both cameras. The first camera has the // projection matrix P1 = [I | 0] and the second camera has the projection // matrix P2 = [R | t]. // // @param R 3x3 rotation matrix of second projection matrix. // @param t 3x1 translation vector of second projection matrix. // @param points1 First set of corresponding points. // @param points2 Second set of corresponding points. // @param points3D Points that lie in front of both cameras. bool CheckCheirality(const Eigen::Matrix3d& R, const Eigen::Vector3d& t, const std::vector& points1, const std::vector& points2, std::vector* points3D); Rigid3d TransformCameraWorld(const Sim3d& new_from_old_world, const Rigid3d& cam_from_world); } // namespace colmap colmap-3.9.1/src/colmap/geometry/pose_test.cc000066400000000000000000000167701454702036400212300ustar00rootroot00000000000000// 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. #include "colmap/geometry/pose.h" #include "colmap/math/math.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(ComputeClosestRotationMatrix, Nominal) { const Eigen::Matrix3d A = Eigen::Matrix3d::Identity(); EXPECT_LT((ComputeClosestRotationMatrix(A) - A).norm(), 1e-6); EXPECT_LT((ComputeClosestRotationMatrix(2 * A) - A).norm(), 1e-6); } TEST(DecomposeProjectionMatrix, Nominal) { for (int i = 1; i < 100; ++i) { Eigen::Matrix3d ref_K = i * Eigen::Matrix3d::Identity(); ref_K(0, 2) = i; ref_K(1, 2) = 2 * i; const Rigid3d cam_from_world(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Eigen::Matrix3x4d P = ref_K * cam_from_world.ToMatrix(); Eigen::Matrix3d K; Eigen::Matrix3d R; Eigen::Vector3d T; DecomposeProjectionMatrix(P, &K, &R, &T); EXPECT_TRUE(ref_K.isApprox(K, 1e-6)); EXPECT_TRUE(cam_from_world.rotation.toRotationMatrix().isApprox(R, 1e-6)); EXPECT_TRUE(cam_from_world.translation.isApprox(T, 1e-6)); } } TEST(CrossProductMatrix, Nominal) { EXPECT_EQ(CrossProductMatrix(Eigen::Vector3d(0, 0, 0)), Eigen::Matrix3d::Zero()); Eigen::Matrix3d ref_matrix; ref_matrix << 0, -3, 2, 3, 0, -1, -2, 1, 0; EXPECT_EQ(CrossProductMatrix(Eigen::Vector3d(1, 2, 3)), ref_matrix); } TEST(EulerAngles, X) { const double rx = 0.3; const double ry = 0; const double rz = 0; double rxx, ryy, rzz; RotationMatrixToEulerAngles( EulerAnglesToRotationMatrix(rx, ry, rz), &rxx, &ryy, &rzz); EXPECT_NEAR(rx, rxx, 1e-6); EXPECT_NEAR(ry, ryy, 1e-6); EXPECT_NEAR(rz, rzz, 1e-6); } TEST(EulerAngles, Y) { const double rx = 0; const double ry = 0.3; const double rz = 0; double rxx, ryy, rzz; RotationMatrixToEulerAngles( EulerAnglesToRotationMatrix(rx, ry, rz), &rxx, &ryy, &rzz); EXPECT_NEAR(rx, rxx, 1e-6); EXPECT_NEAR(ry, ryy, 1e-6); EXPECT_NEAR(rz, rzz, 1e-6); } TEST(EulerAngles, Z) { const double rx = 0; const double ry = 0; const double rz = 0.3; double rxx, ryy, rzz; RotationMatrixToEulerAngles( EulerAnglesToRotationMatrix(rx, ry, rz), &rxx, &ryy, &rzz); EXPECT_NEAR(rx, rxx, 1e-6); EXPECT_NEAR(ry, ryy, 1e-6); EXPECT_NEAR(rz, rzz, 1e-6); } TEST(EulerAngles, XYZ) { const double rx = 0.1; const double ry = 0.2; const double rz = 0.3; double rxx, ryy, rzz; RotationMatrixToEulerAngles( EulerAnglesToRotationMatrix(rx, ry, rz), &rxx, &ryy, &rzz); EXPECT_NEAR(rx, rxx, 1e-6); EXPECT_NEAR(ry, ryy, 1e-6); EXPECT_NEAR(rz, rzz, 1e-6); } TEST(AverageQuaternions, Nominal) { std::vector quats; std::vector weights; quats = {{Eigen::Quaterniond::Identity()}}; weights = {1.0}; EXPECT_EQ(AverageQuaternions(quats, weights).coeffs(), Eigen::Quaterniond::Identity().coeffs()); quats = {Eigen::Quaterniond::Identity()}; weights = {2.0}; EXPECT_EQ(AverageQuaternions(quats, weights).coeffs(), Eigen::Quaterniond::Identity().coeffs()); quats = {Eigen::Quaterniond::Identity(), Eigen::Quaterniond::Identity()}; weights = {1.0, 1.0}; EXPECT_EQ(AverageQuaternions(quats, weights).coeffs(), Eigen::Quaterniond::Identity().coeffs()); quats = {Eigen::Quaterniond::Identity(), Eigen::Quaterniond::Identity()}; weights = {1.0, 2.0}; EXPECT_EQ(AverageQuaternions(quats, weights).coeffs(), Eigen::Quaterniond::Identity().coeffs()); quats = {Eigen::Quaterniond::Identity(), Eigen::Quaterniond(2, 0, 0, 0)}; weights = {1.0, 2.0}; EXPECT_EQ(AverageQuaternions(quats, weights).coeffs(), Eigen::Quaterniond::Identity().coeffs()); quats = {Eigen::Quaterniond::Identity(), Eigen::Quaterniond(1, 1, 0, 0)}; weights = {1.0, 1.0}; EXPECT_TRUE(AverageQuaternions(quats, weights) .isApprox(Eigen::Quaterniond(0.92388, 0.382683, 0, 0), 1e-6)); quats = {Eigen::Quaterniond::Identity(), Eigen::Quaterniond(1, 1, 0, 0)}; weights = {1.0, 2.0}; EXPECT_TRUE( AverageQuaternions(quats, weights) .isApprox(Eigen::Quaterniond(0.850651, 0.525731, 0, 0), 1e-6)); } TEST(InterpolateCameraPoses, Nominal) { const Rigid3d cam_from_world1(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Rigid3d cam_from_world2(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); const Rigid3d interp_cam_from_world1 = InterpolateCameraPoses(cam_from_world1, cam_from_world2, 0); EXPECT_TRUE( interp_cam_from_world1.translation.isApprox(cam_from_world1.translation)); const Rigid3d interp_cam_from_world2 = InterpolateCameraPoses(cam_from_world1, cam_from_world2, 1); EXPECT_TRUE( interp_cam_from_world2.translation.isApprox(cam_from_world2.translation)); const Rigid3d interp_cam_from_world3 = InterpolateCameraPoses(cam_from_world1, cam_from_world2, 0.5); EXPECT_TRUE(interp_cam_from_world3.translation.isApprox( (cam_from_world1.translation + cam_from_world2.translation) / 2)); } TEST(CheckCheirality, Nominal) { const Eigen::Matrix3d R = Eigen::Matrix3d::Identity(); const Eigen::Vector3d t(1, 0, 0); std::vector points1; std::vector points2; std::vector points3D; points1.emplace_back(0, 0); points2.emplace_back(0.1, 0); EXPECT_TRUE(CheckCheirality(R, t, points1, points2, &points3D)); EXPECT_EQ(points3D.size(), 1); points1.emplace_back(0, 0); points2.emplace_back(-0.1, 0); EXPECT_TRUE(CheckCheirality(R, t, points1, points2, &points3D)); EXPECT_EQ(points3D.size(), 1); points2[1][0] = 0.2; EXPECT_TRUE(CheckCheirality(R, t, points1, points2, &points3D)); EXPECT_EQ(points3D.size(), 2); points2[0][0] = -0.2; points2[1][0] = -0.2; EXPECT_FALSE(CheckCheirality(R, t, points1, points2, &points3D)); EXPECT_EQ(points3D.size(), 0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/geometry/rigid3.h000066400000000000000000000074711454702036400202440ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include namespace colmap { // 3D rigid transform with 6 degrees of freedom. // Transforms point x from a to b as: x_in_b = R * x_in_a + t. struct Rigid3d { public: Eigen::Quaterniond rotation = Eigen::Quaterniond::Identity(); Eigen::Vector3d translation = Eigen::Vector3d::Zero(); Rigid3d() = default; Rigid3d(const Eigen::Quaterniond& rotation, const Eigen::Vector3d& translation) : rotation(rotation), translation(translation) {} inline Eigen::Matrix3x4d ToMatrix() const { Eigen::Matrix3x4d matrix; matrix.leftCols<3>() = rotation.toRotationMatrix(); matrix.col(3) = translation; return matrix; } }; // Return inverse transform. inline Rigid3d Inverse(const Rigid3d& b_from_a) { Rigid3d a_from_b; a_from_b.rotation = b_from_a.rotation.inverse(); a_from_b.translation = a_from_b.rotation * -b_from_a.translation; return a_from_b; } // Apply transform to point such that one can write expressions like: // x_in_b = b_from_a * x_in_a // // Be careful when including multiple transformations in the same expression, as // the multiply operator in C++ is evaluated left-to-right. // For example, the following expression: // x_in_c = d_from_c * c_from_b * b_from_a * x_in_a // will be executed in the following order: // x_in_c = ((d_from_c * c_from_b) * b_from_a) * x_in_a // This will first concatenate all transforms and then apply it to the point. // While you may want to instead write and execute it as: // x_in_c = d_from_c * (c_from_b * (b_from_a * x_in_a)) // which will apply the transformations as a chain on the point. inline Eigen::Vector3d operator*(const Rigid3d& t, const Eigen::Vector3d& x) { return t.rotation * x + t.translation; } // Concatenate transforms such one can write expressions like: // d_from_a = d_from_c * c_from_b * b_from_a inline Rigid3d operator*(const Rigid3d& c_from_b, const Rigid3d& b_from_a) { Rigid3d c_from_a; c_from_a.rotation = (c_from_b.rotation * b_from_a.rotation).normalized(); c_from_a.translation = c_from_b.translation + (c_from_b.rotation * b_from_a.translation); return c_from_a; } } // namespace colmap colmap-3.9.1/src/colmap/geometry/rigid3_test.cc000066400000000000000000000107641454702036400214400ustar00rootroot00000000000000// 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. #include "colmap/geometry/rigid3.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { Rigid3d TestRigid3d() { return Rigid3d(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); } TEST(Rigid3d, Default) { const Rigid3d tform; EXPECT_EQ(tform.rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(tform.translation, Eigen::Vector3d::Zero()); } TEST(Rigid3d, Inverse) { const Rigid3d b_from_a = TestRigid3d(); const Rigid3d a_from_b = Inverse(b_from_a); for (int i = 0; i < 100; ++i) { const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); const Eigen::Vector3d x_in_b = b_from_a * x_in_a; EXPECT_LT((a_from_b * x_in_b - x_in_a).norm(), 1e-6); } } TEST(Rigid3d, Matrix) { const Rigid3d b_from_a = TestRigid3d(); const Eigen::Matrix3x4d b_from_a_mat = b_from_a.ToMatrix(); for (int i = 0; i < 100; ++i) { const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); EXPECT_LT((b_from_a * x_in_a - b_from_a_mat * x_in_a.homogeneous()).norm(), 1e-6); } } TEST(Rigid3d, ApplyNoRotation) { const Rigid3d b_from_a(Eigen::Quaterniond::Identity(), Eigen::Vector3d(1, 2, 3)); EXPECT_LT( (b_from_a * Eigen::Vector3d(1, 2, 3) - Eigen::Vector3d(2, 4, 6)).norm(), 1e-6); } TEST(Rigid3d, ApplyNoTranslation) { const Rigid3d b_from_a(Eigen::Quaterniond(Eigen::AngleAxisd( EIGEN_PI / 2, Eigen::Vector3d::UnitX())), Eigen::Vector3d::Zero()); EXPECT_LT( (b_from_a * Eigen::Vector3d(1, 2, 3) - Eigen::Vector3d(1, -3, 2)).norm(), 1e-6); } TEST(Rigid3d, ApplyRotationTranslation) { const Rigid3d b_from_a(Eigen::Quaterniond(Eigen::AngleAxisd( EIGEN_PI / 2, Eigen::Vector3d::UnitX())), Eigen::Vector3d(1, 2, 3)); EXPECT_LT( (b_from_a * Eigen::Vector3d(1, 2, 3) - Eigen::Vector3d(2, -1, 5)).norm(), 1e-6); } TEST(Rigid3d, ApplyChain) { const Rigid3d b_from_a = TestRigid3d(); const Rigid3d c_from_b = TestRigid3d(); const Rigid3d d_from_c = TestRigid3d(); const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); const Eigen::Vector3d x_in_b = b_from_a * x_in_a; const Eigen::Vector3d x_in_c = c_from_b * x_in_b; const Eigen::Vector3d x_in_d = d_from_c * x_in_c; EXPECT_EQ((d_from_c * (c_from_b * (b_from_a * x_in_a))), x_in_d); } TEST(Rigid3d, Compose) { const Rigid3d b_from_a = TestRigid3d(); const Rigid3d c_from_b = TestRigid3d(); const Rigid3d d_from_c = TestRigid3d(); const Rigid3d d_from_a = d_from_c * c_from_b * b_from_a; const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); const Eigen::Vector3d x_in_b = b_from_a * x_in_a; const Eigen::Vector3d x_in_c = c_from_b * x_in_b; const Eigen::Vector3d x_in_d = d_from_c * x_in_c; EXPECT_LT((d_from_a * x_in_a - x_in_d).norm(), 1e-6); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/geometry/sim3.cc000066400000000000000000000047431454702036400200730ustar00rootroot00000000000000// 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. #include "colmap/geometry/sim3.h" #include "colmap/util/logging.h" #include namespace colmap { void Sim3d::ToFile(const std::string& path) const { std::ofstream file(path, std::ios::trunc); CHECK(file.good()) << path; // Ensure that we don't loose any precision by storing in text. file.precision(17); file << scale << " " << rotation.w() << " " << rotation.x() << " " << rotation.y() << " " << rotation.z() << " " << translation.x() << " " << translation.y() << " " << translation.z() << "\n"; } Sim3d Sim3d::FromFile(const std::string& path) { std::ifstream file(path); CHECK(file.good()) << path; Sim3d t; file >> t.scale; file >> t.rotation.w(); file >> t.rotation.x(); file >> t.rotation.y(); file >> t.rotation.z(); file >> t.translation(0); file >> t.translation(1); file >> t.translation(2); return t; } } // namespace colmap colmap-3.9.1/src/colmap/geometry/sim3.h000066400000000000000000000107251454702036400177320ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include namespace colmap { // 3D similarity transform with 7 degrees of freedom. // Transforms point x from a to b as: x_in_b = scale * R * x_in_a + t. struct Sim3d { double scale = 1; Eigen::Quaterniond rotation = Eigen::Quaterniond::Identity(); Eigen::Vector3d translation = Eigen::Vector3d::Zero(); Sim3d() = default; Sim3d(double scale, const Eigen::Quaterniond& rotation, const Eigen::Vector3d& translation) : scale(scale), rotation(rotation), translation(translation) {} inline Eigen::Matrix3x4d ToMatrix() const { Eigen::Matrix3x4d matrix; matrix.leftCols<3>() = scale * rotation.toRotationMatrix(); matrix.col(3) = translation; return matrix; } static inline Sim3d FromMatrix(const Eigen::Matrix3x4d& matrix) { Sim3d t; t.scale = matrix.col(0).norm(); t.rotation = Eigen::Quaterniond(matrix.leftCols<3>() / t.scale).normalized(); t.translation = matrix.rightCols<1>(); return t; } // Read from or write to text file without loss of precision. void ToFile(const std::string& path) const; static Sim3d FromFile(const std::string& path); }; // Return inverse transform. inline Sim3d Inverse(const Sim3d& b_from_a) { Sim3d a_from_b; a_from_b.scale = 1 / b_from_a.scale; a_from_b.rotation = b_from_a.rotation.inverse(); a_from_b.translation = (a_from_b.rotation * b_from_a.translation) / -b_from_a.scale; return a_from_b; } // Apply transform to point such that one can write expressions like: // x_in_b = b_from_a * x_in_a // // Be careful when including multiple transformations in the same expression, as // the multiply operator in C++ is evaluated left-to-right. // For example, the following expression: // x_in_c = d_from_c * c_from_b * b_from_a * x_in_a // will be executed in the following order: // x_in_c = ((d_from_c * c_from_b) * b_from_a) * x_in_a // This will first concatenate all transforms and then apply it to the point. // While you may want to instead write and execute it as: // x_in_c = d_from_c * (c_from_b * (b_from_a * x_in_a)) // which will apply the transformations as a chain on the point. inline Eigen::Vector3d operator*(const Sim3d& t, const Eigen::Vector3d& x) { return t.scale * (t.rotation * x) + t.translation; } // Concatenate transforms such one can write expressions like: // d_from_a = d_from_c * c_from_b * b_from_a inline Sim3d operator*(const Sim3d& c_from_b, const Sim3d& b_from_a) { Sim3d c_from_a; c_from_a.scale = c_from_b.scale * b_from_a.scale; c_from_a.rotation = (c_from_b.rotation * b_from_a.rotation).normalized(); c_from_a.translation = c_from_b.translation + (c_from_b.scale * (c_from_b.rotation * b_from_a.translation)); return c_from_a; } } // namespace colmap colmap-3.9.1/src/colmap/geometry/sim3_test.cc000066400000000000000000000130371454702036400211260ustar00rootroot00000000000000// 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. #include "colmap/geometry/sim3.h" #include "colmap/math/random.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/testing.h" #include #include #include namespace colmap { namespace { Sim3d TestSim3d() { return Sim3d(RandomUniformReal(0.1, 10), Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); } TEST(Sim3d, Default) { const Sim3d tform; EXPECT_EQ(tform.scale, 1); EXPECT_EQ(tform.rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(tform.translation, Eigen::Vector3d::Zero()); } TEST(Sim3d, Inverse) { const Sim3d b_from_a = TestSim3d(); const Sim3d a_from_b = Inverse(b_from_a); for (int i = 0; i < 100; ++i) { const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); const Eigen::Vector3d x_in_b = b_from_a * x_in_a; EXPECT_LT((a_from_b * x_in_b - x_in_a).norm(), 1e-6); } } TEST(Sim3d, ToMatrix) { const Sim3d b_from_a = TestSim3d(); const Eigen::Matrix3x4d b_from_a_mat = b_from_a.ToMatrix(); for (int i = 0; i < 100; ++i) { const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); EXPECT_LT((b_from_a * x_in_a - b_from_a_mat * x_in_a.homogeneous()).norm(), 1e-6); } } TEST(Sim3d, FromMatrix) { const Sim3d b1_from_a = TestSim3d(); const Sim3d b2_from_a = Sim3d::FromMatrix(b1_from_a.ToMatrix()); for (int i = 0; i < 100; ++i) { const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); EXPECT_LT((b1_from_a * x_in_a - b2_from_a * x_in_a).norm(), 1e-6); } } TEST(Sim3d, ApplyScaleOnly) { const Sim3d b_from_a( 2, Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); EXPECT_LT( (b_from_a * Eigen::Vector3d(1, 2, 3) - Eigen::Vector3d(2, 4, 6)).norm(), 1e-6); } TEST(Sim3d, ApplyTranslationOnly) { const Sim3d b_from_a( 1, Eigen::Quaterniond::Identity(), Eigen::Vector3d(1, 2, 3)); EXPECT_LT( (b_from_a * Eigen::Vector3d(1, 2, 3) - Eigen::Vector3d(2, 4, 6)).norm(), 1e-6); } TEST(Sim3d, ApplyRotationOnly) { const Sim3d b_from_a(1, Eigen::Quaterniond(Eigen::AngleAxisd( EIGEN_PI / 2, Eigen::Vector3d::UnitX())), Eigen::Vector3d::Zero()); EXPECT_LT( (b_from_a * Eigen::Vector3d(1, 2, 3) - Eigen::Vector3d(1, -3, 2)).norm(), 1e-6); } TEST(Sim3d, ApplyScaleRotationTranslation) { const Sim3d b_from_a(2, Eigen::Quaterniond(Eigen::AngleAxisd( EIGEN_PI / 2, Eigen::Vector3d::UnitX())), Eigen::Vector3d(1, 2, 3)); EXPECT_LT( (b_from_a * Eigen::Vector3d(1, 2, 3) - Eigen::Vector3d(3, -4, 7)).norm(), 1e-6); } TEST(Rigid3d, ApplyChain) { const Sim3d b_from_a = TestSim3d(); const Sim3d c_from_b = TestSim3d(); const Sim3d d_from_c = TestSim3d(); const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); const Eigen::Vector3d x_in_b = b_from_a * x_in_a; const Eigen::Vector3d x_in_c = c_from_b * x_in_b; const Eigen::Vector3d x_in_d = d_from_c * x_in_c; EXPECT_EQ((d_from_c * (c_from_b * (b_from_a * x_in_a))), x_in_d); } TEST(Sim3d, Compose) { const Sim3d b_from_a = TestSim3d(); const Sim3d c_from_b = TestSim3d(); const Sim3d d_from_c = TestSim3d(); const Sim3d d_from_a = d_from_c * c_from_b * b_from_a; const Eigen::Vector3d x_in_a = Eigen::Vector3d::Random(); const Eigen::Vector3d x_in_b = b_from_a * x_in_a; const Eigen::Vector3d x_in_c = c_from_b * x_in_b; const Eigen::Vector3d x_in_d = d_from_c * x_in_c; EXPECT_LT((d_from_a * x_in_a - x_in_d).norm(), 1e-6); } TEST(Sim3d, ToFromFile) { const std::string path = CreateTestDir() + "/file.txt"; const Sim3d written = TestSim3d(); written.ToFile(path); const Sim3d read = Sim3d::FromFile(path); EXPECT_EQ(written.scale, read.scale); EXPECT_EQ(written.rotation.coeffs(), read.rotation.coeffs()); EXPECT_EQ(written.translation, read.translation); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/geometry/triangulation.cc000066400000000000000000000165571454702036400221060ustar00rootroot00000000000000// 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. #include "colmap/geometry/triangulation.h" #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/util/eigen_alignment.h" #include namespace colmap { Eigen::Vector3d TriangulatePoint(const Eigen::Matrix3x4d& cam1_from_world, const Eigen::Matrix3x4d& cam2_from_world, const Eigen::Vector2d& point1, const Eigen::Vector2d& point2) { Eigen::Matrix4d A; A.row(0) = point1(0) * cam1_from_world.row(2) - cam1_from_world.row(0); A.row(1) = point1(1) * cam1_from_world.row(2) - cam1_from_world.row(1); A.row(2) = point2(0) * cam2_from_world.row(2) - cam2_from_world.row(0); A.row(3) = point2(1) * cam2_from_world.row(2) - cam2_from_world.row(1); Eigen::JacobiSVD svd(A, Eigen::ComputeFullV); return svd.matrixV().col(3).hnormalized(); } std::vector TriangulatePoints( const Eigen::Matrix3x4d& cam1_from_world, const Eigen::Matrix3x4d& cam2_from_world, const std::vector& points1, const std::vector& points2) { CHECK_EQ(points1.size(), points2.size()); std::vector points3D(points1.size()); for (size_t i = 0; i < points3D.size(); ++i) { points3D[i] = TriangulatePoint( cam1_from_world, cam2_from_world, points1[i], points2[i]); } return points3D; } Eigen::Vector3d TriangulateMultiViewPoint( const std::vector& cams_from_world, const std::vector& points) { CHECK_EQ(cams_from_world.size(), points.size()); Eigen::Matrix4d A = Eigen::Matrix4d::Zero(); for (size_t i = 0; i < points.size(); i++) { const Eigen::Vector3d point = points[i].homogeneous().normalized(); const Eigen::Matrix3x4d term = cams_from_world[i] - point * point.transpose() * cams_from_world[i]; A += term.transpose() * term; } Eigen::SelfAdjointEigenSolver eigen_solver(A); return eigen_solver.eigenvectors().col(0).hnormalized(); } Eigen::Vector3d TriangulateOptimalPoint( const Eigen::Matrix3x4d& cam1_from_world_mat, const Eigen::Matrix3x4d& cam2_from_world_mat, const Eigen::Vector2d& point1, const Eigen::Vector2d& point2) { const Rigid3d cam1_from_world( Eigen::Quaterniond(cam1_from_world_mat.leftCols<3>()), cam1_from_world_mat.col(3)); const Rigid3d cam2_from_world( Eigen::Quaterniond(cam2_from_world_mat.leftCols<3>()), cam2_from_world_mat.col(3)); const Rigid3d cam2_from_cam1 = cam2_from_world * Inverse(cam1_from_world); const Eigen::Matrix3d E = EssentialMatrixFromPose(cam2_from_cam1); Eigen::Vector2d optimal_point1; Eigen::Vector2d optimal_point2; FindOptimalImageObservations( E, point1, point2, &optimal_point1, &optimal_point2); return TriangulatePoint( cam1_from_world_mat, cam2_from_world_mat, optimal_point1, optimal_point2); } std::vector TriangulateOptimalPoints( const Eigen::Matrix3x4d& cam1_from_world, const Eigen::Matrix3x4d& cam2_from_world, const std::vector& points1, const std::vector& points2) { std::vector points3D(points1.size()); for (size_t i = 0; i < points3D.size(); ++i) { points3D[i] = TriangulateOptimalPoint( cam1_from_world, cam2_from_world, points1[i], points2[i]); } return points3D; } double CalculateTriangulationAngle(const Eigen::Vector3d& proj_center1, const Eigen::Vector3d& proj_center2, const Eigen::Vector3d& point3D) { const double baseline_length_squared = (proj_center1 - proj_center2).squaredNorm(); const double ray_length_squared1 = (point3D - proj_center1).squaredNorm(); const double ray_length_squared2 = (point3D - proj_center2).squaredNorm(); // Using "law of cosines" to compute the enclosing angle between rays. const double denominator = 2.0 * std::sqrt(ray_length_squared1 * ray_length_squared2); if (denominator == 0.0) { return 0.0; } const double nominator = ray_length_squared1 + ray_length_squared2 - baseline_length_squared; const double angle = std::abs(std::acos(nominator / denominator)); // Triangulation is unstable for acute angles (far away points) and // obtuse angles (close points), so always compute the minimum angle // between the two intersecting rays. return std::min(angle, M_PI - angle); } std::vector CalculateTriangulationAngles( const Eigen::Vector3d& proj_center1, const Eigen::Vector3d& proj_center2, const std::vector& points3D) { // Baseline length between camera centers. const double baseline_length_squared = (proj_center1 - proj_center2).squaredNorm(); std::vector angles(points3D.size()); for (size_t i = 0; i < points3D.size(); ++i) { // Ray lengths from cameras to point. const double ray_length_squared1 = (points3D[i] - proj_center1).squaredNorm(); const double ray_length_squared2 = (points3D[i] - proj_center2).squaredNorm(); // Using "law of cosines" to compute the enclosing angle between rays. const double denominator = 2.0 * std::sqrt(ray_length_squared1 * ray_length_squared2); if (denominator == 0.0) { angles[i] = 0.0; continue; } const double nominator = ray_length_squared1 + ray_length_squared2 - baseline_length_squared; const double angle = std::abs(std::acos(nominator / denominator)); // Triangulation is unstable for acute angles (far away points) and // obtuse angles (close points), so always compute the minimum angle // between the two intersecting rays. angles[i] = std::min(angle, M_PI - angle); } return angles; } } // namespace colmap colmap-3.9.1/src/colmap/geometry/triangulation.h000066400000000000000000000122001454702036400217250ustar00rootroot00000000000000// 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. #pragma once #include "colmap/math/math.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include namespace colmap { // Triangulate 3D point from corresponding image point observations. // // Implementation of the direct linear transform triangulation method in // R. Hartley and A. Zisserman, Multiple View Geometry in Computer Vision, // Cambridge Univ. Press, 2003. // // @param cam_from_world1 Projection matrix of the first image as 3x4 matrix. // @param cam_from_world2 Projection matrix of the second image as 3x4 matrix. // @param point1 Corresponding 2D point in first image. // @param point2 Corresponding 2D point in second image. // // @return Triangulated 3D point. Eigen::Vector3d TriangulatePoint(const Eigen::Matrix3x4d& cam_from_world1, const Eigen::Matrix3x4d& cam_from_world2, const Eigen::Vector2d& point1, const Eigen::Vector2d& point2); // Triangulate multiple 3D points from multiple image correspondences. std::vector TriangulatePoints( const Eigen::Matrix3x4d& cam_from_world1, const Eigen::Matrix3x4d& cam_from_world2, const std::vector& points1, const std::vector& points2); // Triangulate point from multiple views minimizing the L2 error. // // @param cams_from_world Projection matrices of multi-view observations. // @param points Image observations of multi-view observations. // // @return Estimated 3D point. Eigen::Vector3d TriangulateMultiViewPoint( const std::vector& cams_from_world, const std::vector& points); // Triangulate optimal 3D point from corresponding image point observations by // finding the optimal image observations. // // Note that camera poses should be very good in order for this method to yield // good results. Otherwise just use `TriangulatePoint`. // // Implementation of the method described in // P. Lindstrom, "Triangulation Made Easy," IEEE Computer Vision and Pattern // Recognition 2010, pp. 1554-1561, June 2010. // // @param cam_from_world1 Projection matrix of the first image as 3x4 matrix. // @param cam_from_world2 Projection matrix of the second image as 3x4 matrix. // @param point1 Corresponding 2D point in first image. // @param point2 Corresponding 2D point in second image. // // @return Triangulated optimal 3D point. Eigen::Vector3d TriangulateOptimalPoint( const Eigen::Matrix3x4d& cam_from_world1, const Eigen::Matrix3x4d& cam_from_world2, const Eigen::Vector2d& point1, const Eigen::Vector2d& point2); // Triangulate multiple optimal 3D points from multiple image correspondences. std::vector TriangulateOptimalPoints( const Eigen::Matrix3x4d& cam_from_world1, const Eigen::Matrix3x4d& cam_from_world2, const std::vector& points1, const std::vector& points2); // Calculate angle in radians between the two rays of a triangulated point. double CalculateTriangulationAngle(const Eigen::Vector3d& proj_center1, const Eigen::Vector3d& proj_center2, const Eigen::Vector3d& point3D); std::vector CalculateTriangulationAngles( const Eigen::Vector3d& proj_center1, const Eigen::Vector3d& proj_center2, const std::vector& points3D); } // namespace colmap colmap-3.9.1/src/colmap/geometry/triangulation_test.cc000066400000000000000000000070001454702036400231240ustar00rootroot00000000000000// 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. #include "colmap/geometry/triangulation.h" #include "colmap/geometry/rigid3.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(TriangulatePoint, Nominal) { const std::vector points3D = { Eigen::Vector3d(0, 0.1, 0.1), Eigen::Vector3d(0, 1, 3), Eigen::Vector3d(0, 1, 2), Eigen::Vector3d(0.01, 0.2, 3), Eigen::Vector3d(-1, 0.1, 1), Eigen::Vector3d(0.1, 0.1, 0.2), }; const Rigid3d cam_from_world1; for (int z = 0; z < 5; ++z) { const double qz = z / 5.0; for (int tx = 0; tx < 10; tx += 2) { const Rigid3d cam_from_world2(Eigen::Quaterniond(0.2, 0.3, 0.4, qz), Eigen::Vector3d(tx, 2, 3)); for (size_t i = 0; i < points3D.size(); ++i) { const Eigen::Vector3d& point3D = points3D[i]; const Eigen::Vector3d point2D1 = cam_from_world1 * point3D; const Eigen::Vector3d point2D2 = cam_from_world2 * point3D; const Eigen::Vector3d tri_point3D = TriangulatePoint(cam_from_world1.ToMatrix(), cam_from_world2.ToMatrix(), point2D1.hnormalized(), point2D2.hnormalized()); EXPECT_TRUE((point3D - tri_point3D).norm() < 1e-10); } } } } TEST(CalculateTriangulationAngle, Nominal) { const Eigen::Vector3d tvec1(0, 0, 0); const Eigen::Vector3d tvec2(0, 1, 0); EXPECT_NEAR( CalculateTriangulationAngle(tvec1, tvec2, Eigen::Vector3d(0, 0, 100)), 0.009999666687, 1e-8); EXPECT_NEAR( CalculateTriangulationAngle(tvec1, tvec2, Eigen::Vector3d(0, 0, 50)), 0.019997333973, 1e-8); EXPECT_NEAR(CalculateTriangulationAngles( tvec1, tvec2, {Eigen::Vector3d(0, 0, 50)})[0], 0.019997333973, 1e-8); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/image/000077500000000000000000000000001454702036400161305ustar00rootroot00000000000000colmap-3.9.1/src/colmap/image/CMakeLists.txt000066400000000000000000000042221454702036400206700ustar00rootroot00000000000000# 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. set(FOLDER_NAME "image") COLMAP_ADD_LIBRARY( NAME colmap_image SRCS line.h line.cc undistortion.h undistortion.cc warp.h warp.cc PUBLIC_LINK_LIBS Eigen3::Eigen PRIVATE_LINK_LIBS colmap_util colmap_sensor colmap_scene colmap_lsd ) COLMAP_ADD_TEST( NAME line_test SRCS line_test.cc LINK_LIBS colmap_image ) COLMAP_ADD_TEST( NAME undistortion_test SRCS undistortion_test.cc LINK_LIBS colmap_image ) COLMAP_ADD_TEST( NAME warp_test SRCS warp_test.cc LINK_LIBS colmap_image ) colmap-3.9.1/src/colmap/image/line.cc000066400000000000000000000075471454702036400174030ustar00rootroot00000000000000// 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. #include "colmap/image/line.h" #include "colmap/util/logging.h" extern "C" { #include "thirdparty/LSD/lsd.h" } namespace colmap { namespace { struct RawDeleter { void operator()(double* p) { free(p); } }; } // namespace std::vector DetectLineSegments(const Bitmap& bitmap, const double min_length) { const double min_length_squared = min_length * min_length; std::vector bitmap_data; if (bitmap.IsGrey()) { bitmap_data = bitmap.ConvertToRowMajorArray(); } else { const Bitmap bitmap_gray = bitmap.CloneAsGrey(); bitmap_data = bitmap_gray.ConvertToRowMajorArray(); } std::vector bitmap_data_double(bitmap_data.begin(), bitmap_data.end()); int num_segments; std::unique_ptr segments_data( lsd(&num_segments, bitmap_data_double.data(), bitmap.Width(), bitmap.Height())); std::vector segments; segments.reserve(num_segments); for (int i = 0; i < num_segments; ++i) { const Eigen::Vector2d start(segments_data.get()[i * 7], segments_data.get()[i * 7 + 1]); const Eigen::Vector2d end(segments_data.get()[i * 7 + 2], segments_data.get()[i * 7 + 3]); if ((start - end).squaredNorm() >= min_length_squared) { segments.emplace_back(); segments.back().start = start; segments.back().end = end; } } return segments; } std::vector ClassifyLineSegmentOrientations( const std::vector& segments, const double tolerance) { CHECK_LE(tolerance, 0.5); std::vector orientations; orientations.reserve(segments.size()); for (const auto& segment : segments) { const Eigen::Vector2d direction = (segment.end - segment.start).normalized(); if (std::abs(direction.x()) + tolerance > 1) { orientations.push_back(LineSegmentOrientation::HORIZONTAL); } else if (std::abs(direction.y()) + tolerance > 1) { orientations.push_back(LineSegmentOrientation::VERTICAL); } else { orientations.push_back(LineSegmentOrientation::UNDEFINED); } } return orientations; } } // namespace colmap colmap-3.9.1/src/colmap/image/line.h000066400000000000000000000044351454702036400172360ustar00rootroot00000000000000// 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. #pragma once #include "colmap/sensor/bitmap.h" #include "colmap/util/eigen_alignment.h" #include namespace colmap { struct LineSegment { Eigen::Vector2d start; Eigen::Vector2d end; }; enum class LineSegmentOrientation { HORIZONTAL = 1, VERTICAL = -1, UNDEFINED = 0, }; // Detect line segments in the given bitmap image. std::vector DetectLineSegments(const Bitmap& bitmap, double min_length = 3); // Classify line segments into horizontal/vertical. std::vector ClassifyLineSegmentOrientations( const std::vector& segments, double tolerance = 0.25); } // namespace colmap colmap-3.9.1/src/colmap/image/line_test.cc000066400000000000000000000064451454702036400204360ustar00rootroot00000000000000// 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. #include "colmap/image/line.h" #include namespace colmap { namespace { TEST(DetectLineSegments, Nominal) { Bitmap bitmap; bitmap.Allocate(100, 100, false); for (size_t i = 0; i < 100; ++i) { bitmap.SetPixel(i, i, BitmapColor(255)); } const auto line_segments = DetectLineSegments(bitmap, 0); EXPECT_EQ(line_segments.size(), 2); const Eigen::Vector2d ref_start(0, 0); const Eigen::Vector2d ref_end(100, 100); EXPECT_LT((line_segments[0].start - ref_start).norm(), 5); EXPECT_LT((line_segments[0].end - ref_end).norm(), 5); EXPECT_LT((line_segments[1].start - ref_end).norm(), 5); EXPECT_LT((line_segments[1].end - ref_start).norm(), 5); EXPECT_EQ(DetectLineSegments(bitmap, 150).size(), 0); } TEST(ClassifyLineSegmentOrientations, Nominal) { Bitmap bitmap; bitmap.Allocate(100, 100, false); for (size_t i = 60; i < 100; ++i) { bitmap.SetPixel(i, 50, BitmapColor(255)); bitmap.SetPixel(50, i, BitmapColor(255)); bitmap.SetPixel(i, i, BitmapColor(255)); } const auto line_segments = DetectLineSegments(bitmap, 0); EXPECT_EQ(line_segments.size(), 6); const auto orientations = ClassifyLineSegmentOrientations(line_segments); EXPECT_EQ(orientations.size(), 6); EXPECT_TRUE(orientations[0] == LineSegmentOrientation::VERTICAL); EXPECT_TRUE(orientations[1] == LineSegmentOrientation::VERTICAL); EXPECT_TRUE(orientations[2] == LineSegmentOrientation::HORIZONTAL); EXPECT_TRUE(orientations[3] == LineSegmentOrientation::HORIZONTAL); EXPECT_TRUE(orientations[4] == LineSegmentOrientation::UNDEFINED); EXPECT_TRUE(orientations[5] == LineSegmentOrientation::UNDEFINED); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/image/undistortion.cc000066400000000000000000001237261454702036400212130ustar00rootroot00000000000000// 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. #include "colmap/image/undistortion.h" #include "colmap/geometry/pose.h" #include "colmap/image/warp.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" #include namespace colmap { namespace { template void WriteMatrix(const Eigen::MatrixBase& matrix, std::ofstream* file) { typedef typename Eigen::MatrixBase::Index index_t; for (index_t r = 0; r < matrix.rows(); ++r) { for (index_t c = 0; c < matrix.cols() - 1; ++c) { *file << matrix(r, c) << " "; } *file << matrix(r, matrix.cols() - 1) << std::endl; } } // Write projection matrix P = K * [R t] to file and prepend given header. void WriteProjectionMatrix(const std::string& path, const Camera& camera, const Image& image, const std::string& header) { CHECK(camera.model_id == PinholeCameraModel::model_id); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; Eigen::Matrix3d calib_matrix = Eigen::Matrix3d::Identity(); calib_matrix(0, 0) = camera.FocalLengthX(); calib_matrix(1, 1) = camera.FocalLengthY(); calib_matrix(0, 2) = camera.PrincipalPointX(); calib_matrix(1, 2) = camera.PrincipalPointY(); const Eigen::Matrix3x4d img_from_world = calib_matrix * image.CamFromWorld().ToMatrix(); if (!header.empty()) { file << header << std::endl; } WriteMatrix(img_from_world, &file); } void WriteCOLMAPCommands(const bool geometric, const std::string& workspace_path, const std::string& workspace_format, const std::string& pmvs_option_name, const std::string& output_prefix, const std::string& indent, std::ofstream* file) { if (geometric) { *file << indent << "$COLMAP_EXE_PATH/colmap patch_match_stereo \\" << std::endl; *file << indent << " --workspace_path " << workspace_path << " \\" << std::endl; *file << indent << " --workspace_format " << workspace_format << " \\" << std::endl; if (workspace_format == "PMVS") { *file << indent << " --pmvs_option_name " << pmvs_option_name << " \\" << std::endl; } *file << indent << " --PatchMatchStereo.max_image_size 2000 \\" << std::endl; *file << indent << " --PatchMatchStereo.geom_consistency true" << std::endl; } else { *file << indent << "$COLMAP_EXE_PATH/colmap patch_match_stereo \\" << std::endl; *file << indent << " --workspace_path " << workspace_path << " \\" << std::endl; *file << indent << " --workspace_format " << workspace_format << " \\" << std::endl; if (workspace_format == "PMVS") { *file << indent << " --pmvs_option_name " << pmvs_option_name << " \\" << std::endl; } *file << indent << " --PatchMatchStereo.max_image_size 2000 \\" << std::endl; *file << indent << " --PatchMatchStereo.geom_consistency false" << std::endl; } *file << indent << "$COLMAP_EXE_PATH/colmap stereo_fusion \\" << std::endl; *file << indent << " --workspace_path " << workspace_path << " \\" << std::endl; *file << indent << " --workspace_format " << workspace_format << " \\" << std::endl; if (workspace_format == "PMVS") { *file << indent << " --pmvs_option_name " << pmvs_option_name << " \\" << std::endl; } if (geometric) { *file << indent << " --input_type geometric \\" << std::endl; } else { *file << indent << " --input_type photometric \\" << std::endl; } *file << indent << " --output_path " << JoinPaths(workspace_path, output_prefix + "fused.ply") << std::endl; *file << indent << "$COLMAP_EXE_PATH/colmap poisson_mesher \\" << std::endl; *file << indent << " --input_path " << JoinPaths(workspace_path, output_prefix + "fused.ply") << " \\" << std::endl; *file << indent << " --output_path " << JoinPaths(workspace_path, output_prefix + "meshed-poisson.ply") << std::endl; *file << indent << "$COLMAP_EXE_PATH/colmap delaunay_mesher \\" << std::endl; *file << indent << " --input_path " << JoinPaths(workspace_path, output_prefix) << " \\" << std::endl; *file << indent << " --input_type dense \\" << std::endl; *file << indent << " --output_path " << JoinPaths(workspace_path, output_prefix + "meshed-delaunay.ply") << std::endl; } } // namespace COLMAPUndistorter::COLMAPUndistorter(const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path, const int num_patch_match_src_images, const CopyType copy_type, const std::vector& image_ids) : options_(options), image_path_(image_path), output_path_(output_path), copy_type_(copy_type), num_patch_match_src_images_(num_patch_match_src_images), reconstruction_(reconstruction), image_ids_(image_ids) {} void COLMAPUndistorter::Run() { PrintHeading1("Image undistortion"); CreateDirIfNotExists(JoinPaths(output_path_, "images")); CreateDirIfNotExists(JoinPaths(output_path_, "sparse")); CreateDirIfNotExists(JoinPaths(output_path_, "stereo")); CreateDirIfNotExists(JoinPaths(output_path_, "stereo/depth_maps")); CreateDirIfNotExists(JoinPaths(output_path_, "stereo/normal_maps")); CreateDirIfNotExists(JoinPaths(output_path_, "stereo/consistency_graphs")); reconstruction_.CreateImageDirs(JoinPaths(output_path_, "images")); reconstruction_.CreateImageDirs(JoinPaths(output_path_, "stereo/depth_maps")); reconstruction_.CreateImageDirs( JoinPaths(output_path_, "stereo/normal_maps")); reconstruction_.CreateImageDirs( JoinPaths(output_path_, "stereo/consistency_graphs")); ThreadPool thread_pool; std::vector> futures; futures.reserve(reconstruction_.NumRegImages()); if (image_ids_.empty()) { for (size_t i = 0; i < reconstruction_.NumRegImages(); ++i) { const image_t image_id = reconstruction_.RegImageIds().at(i); futures.push_back( thread_pool.AddTask(&COLMAPUndistorter::Undistort, this, image_id)); } } else { for (const image_t image_id : image_ids_) { futures.push_back( thread_pool.AddTask(&COLMAPUndistorter::Undistort, this, image_id)); } } // Only use the image names for the successfully undistorted images // when writing the MVS config files image_names_.clear(); for (size_t i = 0; i < futures.size(); ++i) { if (IsStopped()) { break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); if (futures[i].get()) { if (image_ids_.empty()) { const image_t image_id = reconstruction_.RegImageIds().at(i); image_names_.push_back(reconstruction_.Image(image_id).Name()); } else { image_names_.push_back(reconstruction_.Image(image_ids_[i]).Name()); } } } LOG(INFO) << "Writing reconstruction..."; Reconstruction undistorted_reconstruction = reconstruction_; UndistortReconstruction(options_, &undistorted_reconstruction); undistorted_reconstruction.Write(JoinPaths(output_path_, "sparse")); LOG(INFO) << "Writing configuration..."; WritePatchMatchConfig(); WriteFusionConfig(); LOG(INFO) << "Writing scripts..."; WriteScript(false); WriteScript(true); GetTimer().PrintMinutes(); } bool COLMAPUndistorter::Undistort(const image_t image_id) const { const Image& image = reconstruction_.Image(image_id); Bitmap distorted_bitmap; Bitmap undistorted_bitmap; const Camera& camera = reconstruction_.Camera(image.CameraId()); Camera undistorted_camera; const std::string input_image_path = JoinPaths(image_path_, image.Name()); const std::string output_image_path = JoinPaths(output_path_, "images", image.Name()); // Check if the image is already undistorted and copy from source if no // scaling is needed if (camera.IsUndistorted() && options_.max_image_size < 0 && ExistsFile(input_image_path)) { LOG(INFO) << "Undistorted image found; copying to location: " << output_image_path; FileCopy(input_image_path, output_image_path, copy_type_); return true; } if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path " << input_image_path; return false; } UndistortImage(options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); return undistorted_bitmap.Write(output_image_path); } void COLMAPUndistorter::WritePatchMatchConfig() const { const auto path = JoinPaths(output_path_, "stereo/patch-match.cfg"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; for (const auto& image_name : image_names_) { file << image_name << std::endl; file << "__auto__, " << num_patch_match_src_images_ << std::endl; } } void COLMAPUndistorter::WriteFusionConfig() const { const auto path = JoinPaths(output_path_, "stereo/fusion.cfg"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; for (const auto& image_name : image_names_) { file << image_name << std::endl; } } void COLMAPUndistorter::WriteScript(const bool geometric) const { const std::string path = JoinPaths( output_path_, geometric ? "run-colmap-geometric.sh" : "run-colmap-photometric.sh"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file << "# You must set $COLMAP_EXE_PATH to " << std::endl << "# the directory containing the COLMAP executables." << std::endl; WriteCOLMAPCommands(geometric, ".", "COLMAP", "option-all", "", "", &file); } PMVSUndistorter::PMVSUndistorter(const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path) : options_(options), image_path_(image_path), output_path_(output_path), reconstruction_(reconstruction) {} void PMVSUndistorter::Run() { PrintHeading1("Image undistortion (CMVS/PMVS)"); CreateDirIfNotExists(JoinPaths(output_path_, "pmvs")); CreateDirIfNotExists(JoinPaths(output_path_, "pmvs/txt")); CreateDirIfNotExists(JoinPaths(output_path_, "pmvs/visualize")); CreateDirIfNotExists(JoinPaths(output_path_, "pmvs/models")); ThreadPool thread_pool; std::vector> futures; futures.reserve(reconstruction_.NumRegImages()); for (size_t i = 0; i < reconstruction_.NumRegImages(); ++i) { futures.push_back( thread_pool.AddTask(&PMVSUndistorter::Undistort, this, i)); } for (size_t i = 0; i < futures.size(); ++i) { if (IsStopped()) { thread_pool.Stop(); LOG(WARNING) << "Stopped the undistortion process. Image point " "locations and camera parameters for not yet processed " "images in the Bundler output file is probably wrong."; break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); futures[i].get(); } LOG(INFO) << "Writing bundle file..."; Reconstruction undistorted_reconstruction = reconstruction_; UndistortReconstruction(options_, &undistorted_reconstruction); const std::string bundle_path = JoinPaths(output_path_, "pmvs/bundle.rd.out"); undistorted_reconstruction.ExportBundler(bundle_path, bundle_path + ".list.txt"); LOG(INFO) << "Writing visibility file..."; WriteVisibilityData(); LOG(INFO) << "Writing option file..."; WriteOptionFile(); LOG(INFO) << "Writing scripts..."; WritePMVSScript(); WriteCMVSPMVSScript(); WriteCOLMAPScript(false); WriteCOLMAPScript(true); WriteCMVSCOLMAPScript(false); WriteCMVSCOLMAPScript(true); GetTimer().PrintMinutes(); } bool PMVSUndistorter::Undistort(const size_t reg_image_idx) const { const std::string output_image_path = JoinPaths( output_path_, StringPrintf("pmvs/visualize/%08d.jpg", reg_image_idx)); const std::string proj_matrix_path = JoinPaths(output_path_, StringPrintf("pmvs/txt/%08d.txt", reg_image_idx)); const image_t image_id = reconstruction_.RegImageIds().at(reg_image_idx); const Image& image = reconstruction_.Image(image_id); const Camera& camera = reconstruction_.Camera(image.CameraId()); Bitmap distorted_bitmap; const std::string input_image_path = JoinPaths(image_path_, image.Name()); if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path " << input_image_path; return false; } Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); WriteProjectionMatrix(proj_matrix_path, undistorted_camera, image, "CONTOUR"); return undistorted_bitmap.Write(output_image_path); } void PMVSUndistorter::WriteVisibilityData() const { const auto path = JoinPaths(output_path_, "pmvs/vis.dat"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file << "VISDATA" << std::endl; file << reconstruction_.NumRegImages() << std::endl; const std::vector& reg_image_ids = reconstruction_.RegImageIds(); for (size_t i = 0; i < reg_image_ids.size(); ++i) { const image_t image_id = reg_image_ids[i]; const Image& image = reconstruction_.Image(image_id); std::unordered_set visible_image_ids; for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { const Point2D& point2D = image.Point2D(point2D_idx); if (point2D.HasPoint3D()) { const Point3D& point3D = reconstruction_.Point3D(point2D.point3D_id); for (const TrackElement& track_el : point3D.track.Elements()) { if (track_el.image_id != image_id) { visible_image_ids.insert(track_el.image_id); } } } } std::vector sorted_visible_image_ids(visible_image_ids.begin(), visible_image_ids.end()); std::sort(sorted_visible_image_ids.begin(), sorted_visible_image_ids.end()); file << i << " " << visible_image_ids.size(); for (const image_t visible_image_id : sorted_visible_image_ids) { file << " " << visible_image_id; } file << std::endl; } } void PMVSUndistorter::WritePMVSScript() const { const auto path = JoinPaths(output_path_, "run-pmvs.sh"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file << "# You must set $PMVS_EXE_PATH to " << std::endl << "# the directory containing the CMVS-PMVS executables." << std::endl; file << "$PMVS_EXE_PATH/pmvs2 pmvs/ option-all" << std::endl; } void PMVSUndistorter::WriteCMVSPMVSScript() const { const auto path = JoinPaths(output_path_, "run-cmvs-pmvs.sh"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file << "# You must set $PMVS_EXE_PATH to " << std::endl << "# the directory containing the CMVS-PMVS executables." << std::endl; file << "$PMVS_EXE_PATH/cmvs pmvs/" << std::endl; file << "$PMVS_EXE_PATH/genOption pmvs/" << std::endl; file << "find pmvs/ -iname \"option-*\" | sort | while read file_name" << std::endl; file << "do" << std::endl; file << " option_name=$(basename \"$file_name\")" << std::endl; file << " if [ \"$option_name\" = \"option-all\" ]; then" << std::endl; file << " continue" << std::endl; file << " fi" << std::endl; file << " $PMVS_EXE_PATH/pmvs2 pmvs/ $option_name" << std::endl; file << "done" << std::endl; } void PMVSUndistorter::WriteCOLMAPScript(const bool geometric) const { const std::string path = JoinPaths( output_path_, geometric ? "run-colmap-geometric.sh" : "run-colmap-photometric.sh"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file << "# You must set $COLMAP_EXE_PATH to " << std::endl << "# the directory containing the COLMAP executables." << std::endl; WriteCOLMAPCommands( geometric, "pmvs", "PMVS", "option-all", "option-all-", "", &file); } void PMVSUndistorter::WriteCMVSCOLMAPScript(const bool geometric) const { const std::string path = JoinPaths(output_path_, geometric ? "run-cmvs-colmap-geometric.sh" : "run-cmvs-colmap-photometric.sh"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file << "# You must set $PMVS_EXE_PATH to " << std::endl << "# the directory containing the CMVS-PMVS executables" << std::endl; file << "# and you must set $COLMAP_EXE_PATH to " << std::endl << "# the directory containing the COLMAP executables." << std::endl; file << "$PMVS_EXE_PATH/cmvs pmvs/" << std::endl; file << "$PMVS_EXE_PATH/genOption pmvs/" << std::endl; file << "find pmvs/ -iname \"option-*\" | sort | while read file_name" << std::endl; file << "do" << std::endl; file << " workspace_path=$(dirname \"$file_name\")" << std::endl; file << " option_name=$(basename \"$file_name\")" << std::endl; file << " if [ \"$option_name\" = \"option-all\" ]; then" << std::endl; file << " continue" << std::endl; file << " fi" << std::endl; file << " rm -rf \"$workspace_path/stereo\"" << std::endl; WriteCOLMAPCommands(geometric, "pmvs", "PMVS", "$option_name", "$option_name-", " ", &file); file << "done" << std::endl; } void PMVSUndistorter::WriteOptionFile() const { const auto path = JoinPaths(output_path_, "pmvs/option-all"); std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; file << "# Generated by COLMAP - all images, no clustering." << std::endl; file << "level 1" << std::endl; file << "csize 2" << std::endl; file << "threshold 0.7" << std::endl; file << "wsize 7" << std::endl; file << "minImageNum 3" << std::endl; file << "CPU " << std::thread::hardware_concurrency() << std::endl; file << "setEdge 0" << std::endl; file << "useBound 0" << std::endl; file << "useVisData 1" << std::endl; file << "sequence -1" << std::endl; file << "maxAngle 10" << std::endl; file << "quad 2.0" << std::endl; file << "timages " << reconstruction_.NumRegImages(); for (size_t i = 0; i < reconstruction_.NumRegImages(); ++i) { file << " " << i; } file << std::endl; file << "oimages 0" << std::endl; } CMPMVSUndistorter::CMPMVSUndistorter(const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path) : options_(options), image_path_(image_path), output_path_(output_path), reconstruction_(reconstruction) {} void CMPMVSUndistorter::Run() { PrintHeading1("Image undistortion (CMP-MVS)"); ThreadPool thread_pool; std::vector> futures; futures.reserve(reconstruction_.NumRegImages()); for (size_t i = 0; i < reconstruction_.NumRegImages(); ++i) { futures.push_back( thread_pool.AddTask(&CMPMVSUndistorter::Undistort, this, i)); } for (size_t i = 0; i < futures.size(); ++i) { if (IsStopped()) { break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); futures[i].get(); } GetTimer().PrintMinutes(); } bool CMPMVSUndistorter::Undistort(const size_t reg_image_idx) const { const std::string output_image_path = JoinPaths(output_path_, StringPrintf("%05d.jpg", reg_image_idx + 1)); const std::string proj_matrix_path = JoinPaths(output_path_, StringPrintf("%05d_P.txt", reg_image_idx + 1)); const image_t image_id = reconstruction_.RegImageIds().at(reg_image_idx); const Image& image = reconstruction_.Image(image_id); const Camera& camera = reconstruction_.Camera(image.CameraId()); Bitmap distorted_bitmap; const std::string input_image_path = JoinPaths(image_path_, image.Name()); if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path " << input_image_path; return false; } Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); WriteProjectionMatrix(proj_matrix_path, undistorted_camera, image, "CONTOUR"); return undistorted_bitmap.Write(output_image_path); } PureImageUndistorter::PureImageUndistorter( const UndistortCameraOptions& options, const std::string& image_path, const std::string& output_path, const std::vector>& image_names_and_cameras) : options_(options), image_path_(image_path), output_path_(output_path), image_names_and_cameras_(image_names_and_cameras) {} void PureImageUndistorter::Run() { PrintHeading1("Image undistortion"); CreateDirIfNotExists(output_path_); ThreadPool thread_pool; std::vector> futures; size_t num_images = image_names_and_cameras_.size(); futures.reserve(num_images); for (size_t i = 0; i < num_images; ++i) { futures.push_back( thread_pool.AddTask(&PureImageUndistorter::Undistort, this, i)); } for (size_t i = 0; i < futures.size(); ++i) { if (IsStopped()) { break; } LOG(INFO) << StringPrintf( "Undistorting image [%d/%d]", i + 1, futures.size()); futures[i].get(); } GetTimer().PrintMinutes(); } bool PureImageUndistorter::Undistort(const size_t image_idx) const { const std::string& image_name = image_names_and_cameras_[image_idx].first; const Camera& camera = image_names_and_cameras_[image_idx].second; const std::string output_image_path = JoinPaths(output_path_, image_name); Bitmap distorted_bitmap; const std::string input_image_path = JoinPaths(image_path_, image_name); if (!distorted_bitmap.Read(input_image_path)) { LOG(ERROR) << "Cannot read image at path " << input_image_path; return false; } Bitmap undistorted_bitmap; Camera undistorted_camera; UndistortImage(options_, distorted_bitmap, camera, &undistorted_bitmap, &undistorted_camera); return undistorted_bitmap.Write(output_image_path); } StereoImageRectifier::StereoImageRectifier( const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path, const std::vector>& stereo_pairs) : options_(options), image_path_(image_path), output_path_(output_path), stereo_pairs_(stereo_pairs), reconstruction_(reconstruction) {} void StereoImageRectifier::Run() { PrintHeading1("Stereo rectification"); ThreadPool thread_pool; std::vector> futures; futures.reserve(stereo_pairs_.size()); for (const auto& stereo_pair : stereo_pairs_) { futures.push_back(thread_pool.AddTask(&StereoImageRectifier::Rectify, this, stereo_pair.first, stereo_pair.second)); } for (size_t i = 0; i < futures.size(); ++i) { if (IsStopped()) { break; } LOG(INFO) << StringPrintf( "Rectifying image pair [%d/%d]", i + 1, futures.size()); futures[i].get(); } GetTimer().PrintMinutes(); } void StereoImageRectifier::Rectify(const image_t image_id1, const image_t image_id2) const { const Image& image1 = reconstruction_.Image(image_id1); const Image& image2 = reconstruction_.Image(image_id2); const Camera& camera1 = reconstruction_.Camera(image1.CameraId()); const Camera& camera2 = reconstruction_.Camera(image2.CameraId()); const std::string image_name1 = StringReplace(image1.Name(), "/", "-"); const std::string image_name2 = StringReplace(image2.Name(), "/", "-"); const std::string stereo_pair_name = StringPrintf("%s-%s", image_name1.c_str(), image_name2.c_str()); CreateDirIfNotExists(JoinPaths(output_path_, stereo_pair_name)); const std::string output_image1_path = JoinPaths(output_path_, stereo_pair_name, image_name1); const std::string output_image2_path = JoinPaths(output_path_, stereo_pair_name, image_name2); Bitmap distorted_bitmap1; const std::string input_image1_path = JoinPaths(image_path_, image1.Name()); if (!distorted_bitmap1.Read(input_image1_path)) { LOG(ERROR) << "Cannot read image at path " << input_image1_path; return; } Bitmap distorted_bitmap2; const std::string input_image2_path = JoinPaths(image_path_, image2.Name()); if (!distorted_bitmap2.Read(input_image2_path)) { LOG(ERROR) << "Cannot read image at path " << input_image2_path; return; } const Rigid3d cam2_from_cam1 = image2.CamFromWorld() * Inverse(image1.CamFromWorld()); Bitmap undistorted_bitmap1; Bitmap undistorted_bitmap2; Camera undistorted_camera; Eigen::Matrix4d Q; RectifyAndUndistortStereoImages(options_, distorted_bitmap1, distorted_bitmap2, camera1, camera2, cam2_from_cam1, &undistorted_bitmap1, &undistorted_bitmap2, &undistorted_camera, &Q); undistorted_bitmap1.Write(output_image1_path); undistorted_bitmap2.Write(output_image2_path); const auto Q_path = JoinPaths(output_path_, stereo_pair_name, "Q.txt"); std::ofstream Q_file(Q_path, std::ios::trunc); CHECK(Q_file.is_open()) << Q_path; WriteMatrix(Q, &Q_file); } Camera UndistortCamera(const UndistortCameraOptions& options, const Camera& camera) { CHECK_GE(options.blank_pixels, 0); CHECK_LE(options.blank_pixels, 1); CHECK_GT(options.min_scale, 0.0); CHECK_LE(options.min_scale, options.max_scale); CHECK_NE(options.max_image_size, 0); CHECK_GE(options.roi_min_x, 0.0); CHECK_GE(options.roi_min_y, 0.0); CHECK_LE(options.roi_max_x, 1.0); CHECK_LE(options.roi_max_y, 1.0); CHECK_LT(options.roi_min_x, options.roi_max_x); CHECK_LT(options.roi_min_y, options.roi_max_y); Camera undistorted_camera; undistorted_camera.model_id = PinholeCameraModel::model_id; undistorted_camera.width = camera.width; undistorted_camera.height = camera.height; undistorted_camera.params.resize(PinholeCameraModel::num_params, 0); // Copy focal length parameters. const span focal_length_idxs = camera.FocalLengthIdxs(); CHECK_LE(focal_length_idxs.size(), 2) << "Not more than two focal length parameters supported."; undistorted_camera.SetFocalLengthX(camera.FocalLengthX()); undistorted_camera.SetFocalLengthY(camera.FocalLengthY()); // Copy principal point parameters. undistorted_camera.SetPrincipalPointX(camera.PrincipalPointX()); undistorted_camera.SetPrincipalPointY(camera.PrincipalPointY()); // Modify undistorted camera parameters based on ROI if enabled size_t roi_min_x = 0; size_t roi_min_y = 0; size_t roi_max_x = camera.width; size_t roi_max_y = camera.height; const bool roi_enabled = options.roi_min_x > 0.0 || options.roi_min_y > 0.0 || options.roi_max_x < 1.0 || options.roi_max_y < 1.0; if (roi_enabled) { roi_min_x = static_cast( std::round(options.roi_min_x * static_cast(camera.width))); roi_min_y = static_cast( std::round(options.roi_min_y * static_cast(camera.height))); roi_max_x = static_cast( std::round(options.roi_max_x * static_cast(camera.width))); roi_max_y = static_cast( std::round(options.roi_max_y * static_cast(camera.height))); // Make sure that the roi is valid. roi_min_x = std::min(roi_min_x, camera.width - 1); roi_min_y = std::min(roi_min_y, camera.height - 1); roi_max_x = std::max(roi_max_x, roi_min_x + 1); roi_max_y = std::max(roi_max_y, roi_min_y + 1); undistorted_camera.width = roi_max_x - roi_min_x; undistorted_camera.height = roi_max_y - roi_min_y; undistorted_camera.SetPrincipalPointX(camera.PrincipalPointX() - static_cast(roi_min_x)); undistorted_camera.SetPrincipalPointY(camera.PrincipalPointY() - static_cast(roi_min_y)); } // Scale the image such the the boundary of the undistorted image. if (roi_enabled || (camera.model_id != SimplePinholeCameraModel::model_id && camera.model_id != PinholeCameraModel::model_id)) { // Determine min/max coordinates along top / bottom image border. double left_min_x = std::numeric_limits::max(); double left_max_x = std::numeric_limits::lowest(); double right_min_x = std::numeric_limits::max(); double right_max_x = std::numeric_limits::lowest(); for (size_t y = roi_min_y; y < roi_max_y; ++y) { // Left border. const Eigen::Vector2d point1_in_cam = camera.CamFromImg(Eigen::Vector2d(0.5, y + 0.5)); const Eigen::Vector2d undistorted_point1 = undistorted_camera.ImgFromCam(point1_in_cam); left_min_x = std::min(left_min_x, undistorted_point1(0)); left_max_x = std::max(left_max_x, undistorted_point1(0)); // Right border. const Eigen::Vector2d point2_in_cam = camera.CamFromImg(Eigen::Vector2d(camera.width - 0.5, y + 0.5)); const Eigen::Vector2d undistorted_point2 = undistorted_camera.ImgFromCam(point2_in_cam); right_min_x = std::min(right_min_x, undistorted_point2(0)); right_max_x = std::max(right_max_x, undistorted_point2(0)); } // Determine min, max coordinates along left / right image border. double top_min_y = std::numeric_limits::max(); double top_max_y = std::numeric_limits::lowest(); double bottom_min_y = std::numeric_limits::max(); double bottom_max_y = std::numeric_limits::lowest(); for (size_t x = roi_min_x; x < roi_max_x; ++x) { // Top border. const Eigen::Vector2d point1_in_cam = camera.CamFromImg(Eigen::Vector2d(x + 0.5, 0.5)); const Eigen::Vector2d undistorted_point1 = undistorted_camera.ImgFromCam(point1_in_cam); top_min_y = std::min(top_min_y, undistorted_point1(1)); top_max_y = std::max(top_max_y, undistorted_point1(1)); // Bottom border. const Eigen::Vector2d point2_in_cam = camera.CamFromImg(Eigen::Vector2d(x + 0.5, camera.height - 0.5)); const Eigen::Vector2d undistorted_point2 = undistorted_camera.ImgFromCam(point2_in_cam); bottom_min_y = std::min(bottom_min_y, undistorted_point2(1)); bottom_max_y = std::max(bottom_max_y, undistorted_point2(1)); } const double cx = undistorted_camera.PrincipalPointX(); const double cy = undistorted_camera.PrincipalPointY(); // Scale such that undistorted image contains all pixels of distorted image. const double min_scale_x = std::min(cx / (cx - left_min_x), (undistorted_camera.width - 0.5 - cx) / (right_max_x - cx)); const double min_scale_y = std::min(cy / (cy - top_min_y), (undistorted_camera.height - 0.5 - cy) / (bottom_max_y - cy)); // Scale such that there are no blank pixels in undistorted image. const double max_scale_x = std::max(cx / (cx - left_max_x), (undistorted_camera.width - 0.5 - cx) / (right_min_x - cx)); const double max_scale_y = std::max(cy / (cy - top_max_y), (undistorted_camera.height - 0.5 - cy) / (bottom_min_y - cy)); // Interpolate scale according to blank_pixels. double scale_x = 1.0 / (min_scale_x * options.blank_pixels + max_scale_x * (1.0 - options.blank_pixels)); double scale_y = 1.0 / (min_scale_y * options.blank_pixels + max_scale_y * (1.0 - options.blank_pixels)); // Clip the scaling factors. scale_x = Clamp(scale_x, options.min_scale, options.max_scale); scale_y = Clamp(scale_y, options.min_scale, options.max_scale); // Scale undistorted camera dimensions. const size_t orig_undistorted_camera_width = undistorted_camera.width; const size_t orig_undistorted_camera_height = undistorted_camera.height; undistorted_camera.width = static_cast(std::max(1.0, scale_x * undistorted_camera.width)); undistorted_camera.height = static_cast(std::max(1.0, scale_y * undistorted_camera.height)); // Scale the principal point according to the new dimensions of the camera. undistorted_camera.SetPrincipalPointX( undistorted_camera.PrincipalPointX() * static_cast(undistorted_camera.width) / static_cast(orig_undistorted_camera_width)); undistorted_camera.SetPrincipalPointY( undistorted_camera.PrincipalPointY() * static_cast(undistorted_camera.height) / static_cast(orig_undistorted_camera_height)); } if (options.max_image_size > 0) { const double max_image_scale_x = options.max_image_size / static_cast(undistorted_camera.width); const double max_image_scale_y = options.max_image_size / static_cast(undistorted_camera.height); const double max_image_scale = std::min(max_image_scale_x, max_image_scale_y); if (max_image_scale < 1.0) { undistorted_camera.Rescale(max_image_scale); } } return undistorted_camera; } void UndistortImage(const UndistortCameraOptions& options, const Bitmap& distorted_bitmap, const Camera& distorted_camera, Bitmap* undistorted_bitmap, Camera* undistorted_camera) { CHECK_EQ(distorted_camera.width, distorted_bitmap.Width()); CHECK_EQ(distorted_camera.height, distorted_bitmap.Height()); *undistorted_camera = UndistortCamera(options, distorted_camera); undistorted_bitmap->Allocate(static_cast(undistorted_camera->width), static_cast(undistorted_camera->height), distorted_bitmap.IsRGB()); distorted_bitmap.CloneMetadata(undistorted_bitmap); WarpImageBetweenCameras(distorted_camera, *undistorted_camera, distorted_bitmap, undistorted_bitmap); } void UndistortReconstruction(const UndistortCameraOptions& options, Reconstruction* reconstruction) { const auto distorted_cameras = reconstruction->Cameras(); for (const auto& camera : distorted_cameras) { if (camera.second.IsUndistorted()) { continue; } reconstruction->Camera(camera.first) = UndistortCamera(options, camera.second); } for (const auto& distorted_image : reconstruction->Images()) { auto& image = reconstruction->Image(distorted_image.first); const auto& distorted_camera = distorted_cameras.at(image.CameraId()); const auto& undistorted_camera = reconstruction->Camera(image.CameraId()); for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { auto& point2D = image.Point2D(point2D_idx); point2D.xy = undistorted_camera.ImgFromCam( distorted_camera.CamFromImg(point2D.xy)); } } } void RectifyStereoCameras(const Camera& camera1, const Camera& camera2, const Rigid3d& cam2_from_cam1, Eigen::Matrix3d* H1, Eigen::Matrix3d* H2, Eigen::Matrix4d* Q) { CHECK(camera1.model_id == SimplePinholeCameraModel::model_id || camera1.model_id == PinholeCameraModel::model_id); CHECK(camera2.model_id == SimplePinholeCameraModel::model_id || camera2.model_id == PinholeCameraModel::model_id); // Compute the average rotation between the first and the second camera. Eigen::AngleAxisd half_cam2_from_cam1(cam2_from_cam1.rotation); half_cam2_from_cam1.angle() *= -0.5; Eigen::Matrix3d R2 = half_cam2_from_cam1.toRotationMatrix(); Eigen::Matrix3d R1 = R2.transpose(); // Determine the translation, such that it coincides with the X-axis. Eigen::Vector3d t = R2 * cam2_from_cam1.translation; Eigen::Vector3d x_unit_vector(1, 0, 0); if (t.transpose() * x_unit_vector < 0) { x_unit_vector *= -1; } const Eigen::Vector3d rotation_axis = t.cross(x_unit_vector); Eigen::Matrix3d R_x; if (rotation_axis.norm() < std::numeric_limits::epsilon()) { R_x = Eigen::Matrix3d::Identity(); } else { const double angle = std::acos(std::abs(t.transpose() * x_unit_vector) / (t.norm() * x_unit_vector.norm())); R_x = Eigen::AngleAxisd(angle, rotation_axis.normalized()); } // Apply the X-axis correction. R1 = R_x * R1; R2 = R_x * R2; t = R_x * t; // Determine the intrinsic calibration matrix. Eigen::Matrix3d K = Eigen::Matrix3d::Identity(); K(0, 0) = std::min(camera1.MeanFocalLength(), camera2.MeanFocalLength()); K(1, 1) = K(0, 0); K(0, 2) = camera1.PrincipalPointX(); K(1, 2) = (camera1.PrincipalPointY() + camera2.PrincipalPointY()) / 2; // Compose the homographies. *H1 = K * R1 * camera1.CalibrationMatrix().inverse(); *H2 = K * R2 * camera2.CalibrationMatrix().inverse(); // Determine the inverse projection matrix that transforms disparity values // to 3D world coordinates: [x, y, disparity, 1] * Q = [X, Y, Z, 1] * w. *Q = Eigen::Matrix4d::Identity(); (*Q)(3, 0) = -K(1, 2); (*Q)(3, 1) = -K(0, 2); (*Q)(3, 2) = K(0, 0); (*Q)(2, 3) = -1 / t(0); (*Q)(3, 3) = 0; } void RectifyAndUndistortStereoImages(const UndistortCameraOptions& options, const Bitmap& distorted_image1, const Bitmap& distorted_image2, const Camera& distorted_camera1, const Camera& distorted_camera2, const Rigid3d& cam2_from_cam1, Bitmap* undistorted_image1, Bitmap* undistorted_image2, Camera* undistorted_camera, Eigen::Matrix4d* Q) { CHECK_EQ(distorted_camera1.width, distorted_image1.Width()); CHECK_EQ(distorted_camera1.height, distorted_image1.Height()); CHECK_EQ(distorted_camera2.width, distorted_image2.Width()); CHECK_EQ(distorted_camera2.height, distorted_image2.Height()); *undistorted_camera = UndistortCamera(options, distorted_camera1); undistorted_image1->Allocate(static_cast(undistorted_camera->width), static_cast(undistorted_camera->height), distorted_image1.IsRGB()); distorted_image1.CloneMetadata(undistorted_image1); undistorted_image2->Allocate(static_cast(undistorted_camera->width), static_cast(undistorted_camera->height), distorted_image2.IsRGB()); distorted_image2.CloneMetadata(undistorted_image2); Eigen::Matrix3d H1; Eigen::Matrix3d H2; RectifyStereoCameras( *undistorted_camera, *undistorted_camera, cam2_from_cam1, &H1, &H2, Q); WarpImageWithHomographyBetweenCameras(H1.inverse(), distorted_camera1, *undistorted_camera, distorted_image1, undistorted_image1); WarpImageWithHomographyBetweenCameras(H2.inverse(), distorted_camera2, *undistorted_camera, distorted_image2, undistorted_image2); } } // namespace colmap colmap-3.9.1/src/colmap/image/undistortion.h000066400000000000000000000224501454702036400210450ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/scene/reconstruction.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/misc.h" #include "colmap/util/threading.h" namespace colmap { struct UndistortCameraOptions { // The amount of blank pixels in the undistorted image in the range [0, 1]. double blank_pixels = 0.0; // Minimum and maximum scale change of camera used to satisfy the blank // pixel constraint. double min_scale = 0.2; double max_scale = 2.0; // Maximum image size in terms of width or height of the undistorted camera. int max_image_size = -1; // The 4 factors in the range [0, 1] that define the ROI (region of interest) // in original image. The bounding box pixel coordinates are calculated as // (roi_min_x * Width, roi_min_y * Height) and // (roi_max_x * Width, roi_max_y * Height). double roi_min_x = 0.0; double roi_min_y = 0.0; double roi_max_x = 1.0; double roi_max_y = 1.0; }; // Undistort images and export undistorted cameras, as required by the // mvs::PatchMatchController class. class COLMAPUndistorter : public Thread { public: COLMAPUndistorter( const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path, int num_related_images = 20, CopyType copy_type = CopyType::COPY, const std::vector& image_ids = std::vector()); private: void Run(); bool Undistort(image_t image_id) const; void WritePatchMatchConfig() const; void WriteFusionConfig() const; void WriteScript(bool geometric) const; UndistortCameraOptions options_; const std::string image_path_; const std::string output_path_; const CopyType copy_type_; const int num_patch_match_src_images_; const Reconstruction& reconstruction_; const std::vector image_ids_; std::vector image_names_; }; // Undistort images and prepare data for CMVS/PMVS. class PMVSUndistorter : public Thread { public: PMVSUndistorter(const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path); private: void Run(); bool Undistort(size_t reg_image_idx) const; void WriteVisibilityData() const; void WriteOptionFile() const; void WritePMVSScript() const; void WriteCMVSPMVSScript() const; void WriteCOLMAPScript(bool geometric) const; void WriteCMVSCOLMAPScript(bool geometric) const; UndistortCameraOptions options_; std::string image_path_; std::string output_path_; const Reconstruction& reconstruction_; }; // Undistort images and prepare data for CMP-MVS. class CMPMVSUndistorter : public Thread { public: CMPMVSUndistorter(const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path); private: void Run(); bool Undistort(size_t reg_image_idx) const; UndistortCameraOptions options_; std::string image_path_; std::string output_path_; const Reconstruction& reconstruction_; }; // Undistort images and export undistorted cameras without the need for a // reconstruction. Instead, the image names and camera model information are // read from a text file. class PureImageUndistorter : public Thread { public: PureImageUndistorter(const UndistortCameraOptions& options, const std::string& image_path, const std::string& output_path, const std::vector>& image_names_and_cameras); private: void Run(); bool Undistort(size_t reg_image_idx) const; UndistortCameraOptions options_; std::string image_path_; std::string output_path_; const std::vector>& image_names_and_cameras_; }; // Rectify stereo image pairs. class StereoImageRectifier : public Thread { public: StereoImageRectifier( const UndistortCameraOptions& options, const Reconstruction& reconstruction, const std::string& image_path, const std::string& output_path, const std::vector>& stereo_pairs); private: void Run(); void Rectify(image_t image_id1, image_t image_id2) const; UndistortCameraOptions options_; std::string image_path_; std::string output_path_; const std::vector>& stereo_pairs_; const Reconstruction& reconstruction_; }; // Undistort camera by resizing the image and shifting the principal point. // // The scaling factor is computed such that no blank pixels are in the // undistorted image (blank_pixels=0) or all pixels in distorted image are // contained in output image (blank_pixels=1). // // The focal length of the image is preserved and the dimensions of the // undistorted pinhole camera are adjusted such that either all pixels in // the undistorted image have a corresponding pixel in the distorted image // (i.e. no blank pixels at the borders, for `blank_pixels=0`), or all pixels // in the distorted image project have a corresponding pixel in the undistorted // image (i.e. blank pixels at the borders, for `blank_pixels=1`). Intermediate // states can be achieved by setting `blank_pixels` between 0 and 1. // // The relative location of the principal point of the distorted camera is // preserved. The scaling of the image dimensions is subject to the `min_scale`, // `max_scale`, and `max_image_size` constraints. Camera UndistortCamera(const UndistortCameraOptions& options, const Camera& camera); // Undistort image such that the viewing geometry of the undistorted image // follows a pinhole camera model. See `UndistortCamera` for more details // on the undistortion conventions. void UndistortImage(const UndistortCameraOptions& options, const Bitmap& distorted_image, const Camera& distorted_camera, Bitmap* undistorted_image, Camera* undistorted_camera); // Undistort all cameras in the reconstruction and accordingly all // observations in their corresponding images. void UndistortReconstruction(const UndistortCameraOptions& options, Reconstruction* reconstruction); // Compute stereo rectification homographies that transform two images, // such that corresponding pixels in one image lie on the same scanline in the // other image. The matrix Q transforms disparity values to world coordinates // as [x, y, disparity, 1] * Q = [X, Y, Z, 1] * w. Note that this function // assumes that the two cameras are already undistorted. void RectifyStereoCameras(const Camera& camera1, const Camera& camera2, const Rigid3d& cam2_from_cam1, Eigen::Matrix3d* H1, Eigen::Matrix3d* H2, Eigen::Matrix4d* Q); // Rectify and undistort the stereo image pair using the given geometry. void RectifyAndUndistortStereoImages(const UndistortCameraOptions& options, const Bitmap& distorted_image1, const Bitmap& distorted_image2, const Camera& distorted_camera1, const Camera& distorted_camera2, const Rigid3d& cam2_from_cam1, Bitmap* undistorted_image1, Bitmap* undistorted_image2, Camera* undistorted_camera, Eigen::Matrix4d* Q); } // namespace colmap colmap-3.9.1/src/colmap/image/undistortion_test.cc000066400000000000000000000222411454702036400222400ustar00rootroot00000000000000// 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. #include "colmap/image/undistortion.h" #include "colmap/geometry/pose.h" #include namespace colmap { namespace { TEST(UndistortCamera, Nominal) { UndistortCameraOptions options; Camera distorted_camera; Camera undistorted_camera; distorted_camera = Camera::CreateFromModelName(1, "SIMPLE_PINHOLE", 1, 1, 1); undistorted_camera = UndistortCamera(options, distorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 1); EXPECT_EQ(undistorted_camera.FocalLengthY(), 1); EXPECT_EQ(undistorted_camera.width, 1); EXPECT_EQ(undistorted_camera.height, 1); distorted_camera = Camera::CreateFromModelName(1, "SIMPLE_RADIAL", 1, 1, 1); undistorted_camera = UndistortCamera(options, distorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 1); EXPECT_EQ(undistorted_camera.FocalLengthY(), 1); EXPECT_EQ(undistorted_camera.width, 1); EXPECT_EQ(undistorted_camera.height, 1); distorted_camera = Camera::CreateFromModelName(1, "SIMPLE_RADIAL", 100, 100, 100); distorted_camera.params[3] = 0.5; undistorted_camera = UndistortCamera(options, distorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 100); EXPECT_EQ(undistorted_camera.FocalLengthY(), 100); EXPECT_EQ(undistorted_camera.PrincipalPointX(), 84.0 / 2.0); EXPECT_EQ(undistorted_camera.PrincipalPointY(), 84.0 / 2.0); EXPECT_EQ(undistorted_camera.width, 84); EXPECT_EQ(undistorted_camera.height, 84); options.blank_pixels = 1; undistorted_camera = UndistortCamera(options, distorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 100); EXPECT_EQ(undistorted_camera.FocalLengthY(), 100); EXPECT_EQ(undistorted_camera.width, 90); EXPECT_EQ(undistorted_camera.height, 90); options.max_scale = 0.75; undistorted_camera = UndistortCamera(options, distorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 100); EXPECT_EQ(undistorted_camera.FocalLengthY(), 100); EXPECT_EQ(undistorted_camera.width, 75); EXPECT_EQ(undistorted_camera.height, 75); options.max_scale = 1.0; options.roi_min_x = 0.1; options.roi_min_y = 0.2; options.roi_max_x = 0.9; options.roi_max_y = 0.8; undistorted_camera = UndistortCamera(options, distorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 100); EXPECT_EQ(undistorted_camera.FocalLengthY(), 100); EXPECT_EQ(undistorted_camera.width, 80); EXPECT_EQ(undistorted_camera.height, 60); EXPECT_EQ(undistorted_camera.PrincipalPointX(), 40); EXPECT_EQ(undistorted_camera.PrincipalPointY(), 30); } TEST(UndistortCamera, BlankPixels) { UndistortCameraOptions options; options.blank_pixels = 1; Camera distorted_camera; distorted_camera = Camera::CreateFromModelName(1, "SIMPLE_RADIAL", 100, 100, 100); distorted_camera.params[3] = 0.5; Bitmap distorted_image; distorted_image.Allocate(100, 100, false); distorted_image.Fill(BitmapColor(255)); Bitmap undistorted_image; Camera undistorted_camera; UndistortImage(options, distorted_image, distorted_camera, &undistorted_image, &undistorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 100); EXPECT_EQ(undistorted_camera.FocalLengthY(), 100); EXPECT_EQ(undistorted_camera.PrincipalPointX(), 90.0 / 2.0); EXPECT_EQ(undistorted_camera.PrincipalPointY(), 90.0 / 2.0); EXPECT_EQ(undistorted_camera.width, 90); EXPECT_EQ(undistorted_camera.height, 90); // Make sure that there is no blank pixel. size_t num_blank_pixels = 0; for (int y = 0; y < undistorted_image.Height(); ++y) { for (int x = 0; x < undistorted_image.Width(); ++x) { BitmapColor color; EXPECT_TRUE(undistorted_image.GetPixel(x, y, &color)); if (color == BitmapColor(0)) { num_blank_pixels += 1; } } } EXPECT_GT(num_blank_pixels, 0); } TEST(UndistortCamera, NoBlankPixels) { UndistortCameraOptions options; options.blank_pixels = 0; Camera distorted_camera; distorted_camera = Camera::CreateFromModelName(1, "SIMPLE_RADIAL", 100, 100, 100); distorted_camera.params[3] = 0.5; Bitmap distorted_image; distorted_image.Allocate(100, 100, false); distorted_image.Fill(BitmapColor(255)); Bitmap undistorted_image; Camera undistorted_camera; UndistortImage(options, distorted_image, distorted_camera, &undistorted_image, &undistorted_camera); EXPECT_EQ(undistorted_camera.ModelName(), "PINHOLE"); EXPECT_EQ(undistorted_camera.FocalLengthX(), 100); EXPECT_EQ(undistorted_camera.FocalLengthY(), 100); EXPECT_EQ(undistorted_camera.PrincipalPointX(), 84.0 / 2.0); EXPECT_EQ(undistorted_camera.PrincipalPointY(), 84.0 / 2.0); EXPECT_EQ(undistorted_camera.width, 84); EXPECT_EQ(undistorted_camera.height, 84); // Make sure that there is no blank pixel. for (int y = 0; y < undistorted_image.Height(); ++y) { for (int x = 0; x < undistorted_image.Width(); ++x) { BitmapColor color; EXPECT_TRUE(undistorted_image.GetPixel(x, y, &color)); EXPECT_NE(color.r, 0); EXPECT_EQ(color.g, 0); EXPECT_EQ(color.b, 0); } } } TEST(UndistortReconstruction, Nominal) { const size_t kNumImages = 10; const size_t kNumPoints2D = 10; Reconstruction reconstruction; Camera camera = Camera::CreateFromModelName(1, "OPENCV", 1, 1, 1); camera.params[4] = 1.0; reconstruction.AddCamera(camera); for (image_t image_id = 1; image_id <= kNumImages; ++image_id) { Image image; image.SetImageId(image_id); image.SetCameraId(1); image.SetName("image" + std::to_string(image_id)); image.SetPoints2D( std::vector(kNumPoints2D, Eigen::Vector2d::Ones())); reconstruction.AddImage(image); reconstruction.RegisterImage(image_id); } UndistortCameraOptions options; UndistortReconstruction(options, &reconstruction); for (const auto& camera : reconstruction.Cameras()) { EXPECT_EQ(camera.second.ModelName(), "PINHOLE"); } for (const auto& image : reconstruction.Images()) { for (const auto& point2D : image.second.Points2D()) { EXPECT_NE(point2D.xy, Eigen::Vector2d::Ones()); } } } TEST(RectifyStereoCameras, Nominal) { Camera camera1; camera1 = Camera::CreateFromModelName(1, "PINHOLE", 1, 1, 1); Camera camera2; camera2 = Camera::CreateFromModelName(1, "PINHOLE", 1, 1, 1); const Rigid3d cam2_from_cam1( Eigen::Quaterniond(EulerAnglesToRotationMatrix(0.1, 0.2, 0.3)), Eigen::Vector3d(0.1, 0.2, 0.3)); Camera rectified_camera1; Camera rectified_camera2; Eigen::Matrix3d H1; Eigen::Matrix3d H2; Eigen::Matrix4d Q; RectifyStereoCameras(camera1, camera2, cam2_from_cam1, &H1, &H2, &Q); Eigen::Matrix3d H1_ref; H1_ref << -0.202759, -0.815848, -0.897034, 0.416329, 0.733069, -0.199657, 0.910839, -0.175408, 0.942638; EXPECT_TRUE(H1.isApprox(H1_ref.transpose(), 1e-5)); Eigen::Matrix3d H2_ref; H2_ref << -0.082173, -1.01288, -0.698868, 0.301854, 0.472844, -0.465336, 0.963533, 0.292411, 1.12528; EXPECT_TRUE(H2.isApprox(H2_ref.transpose(), 1e-5)); Eigen::Matrix4d Q_ref; Q_ref << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -2.67261, -0.5, -0.5, 1, 0; EXPECT_TRUE(Q.isApprox(Q_ref, 1e-5)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/image/warp.cc000066400000000000000000000240051454702036400174110ustar00rootroot00000000000000// 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. #include "colmap/image/warp.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include "thirdparty/VLFeat/imopv.h" #include namespace colmap { namespace { float GetPixelConstantBorder(const float* data, const int rows, const int cols, const int row, const int col) { if (row >= 0 && col >= 0 && row < rows && col < cols) { return data[row * cols + col]; } else { return 0; } } } // namespace void WarpImageBetweenCameras(const Camera& source_camera, const Camera& target_camera, const Bitmap& source_image, Bitmap* target_image) { CHECK_EQ(source_camera.width, source_image.Width()); CHECK_EQ(source_camera.height, source_image.Height()); CHECK_NOTNULL(target_image); target_image->Allocate(static_cast(source_camera.width), static_cast(source_camera.height), source_image.IsRGB()); // To avoid aliasing, perform the warping in the source resolution and // then rescale the image at the end. Camera scaled_target_camera = target_camera; if (target_camera.width != source_camera.width || target_camera.height != source_camera.height) { scaled_target_camera.Rescale(source_camera.width, source_camera.height); } Eigen::Vector2d image_point; for (int y = 0; y < target_image->Height(); ++y) { image_point.y() = y + 0.5; for (int x = 0; x < target_image->Width(); ++x) { image_point.x() = x + 0.5; // Camera models assume that the upper left pixel center is (0.5, 0.5). const Eigen::Vector2d cam_point = scaled_target_camera.CamFromImg(image_point); const Eigen::Vector2d source_point = source_camera.ImgFromCam(cam_point); BitmapColor color; if (source_image.InterpolateBilinear( source_point.x() - 0.5, source_point.y() - 0.5, &color)) { target_image->SetPixel(x, y, color.Cast()); } else { target_image->SetPixel(x, y, BitmapColor(0)); } } } if (target_camera.width != source_camera.width || target_camera.height != source_camera.height) { target_image->Rescale(target_camera.width, target_camera.height); } } void WarpImageWithHomography(const Eigen::Matrix3d& H, const Bitmap& source_image, Bitmap* target_image) { CHECK_NOTNULL(target_image); CHECK_GT(target_image->Width(), 0); CHECK_GT(target_image->Height(), 0); CHECK_EQ(source_image.IsRGB(), target_image->IsRGB()); Eigen::Vector3d target_pixel(0, 0, 1); for (int y = 0; y < target_image->Height(); ++y) { target_pixel.y() = y + 0.5; for (int x = 0; x < target_image->Width(); ++x) { target_pixel.x() = x + 0.5; const Eigen::Vector2d source_pixel = (H * target_pixel).hnormalized(); BitmapColor color; if (source_image.InterpolateBilinear( source_pixel.x() - 0.5, source_pixel.y() - 0.5, &color)) { target_image->SetPixel(x, y, color.Cast()); } else { target_image->SetPixel(x, y, BitmapColor(0)); } } } } void WarpImageWithHomographyBetweenCameras(const Eigen::Matrix3d& H, const Camera& source_camera, const Camera& target_camera, const Bitmap& source_image, Bitmap* target_image) { CHECK_EQ(source_camera.width, source_image.Width()); CHECK_EQ(source_camera.height, source_image.Height()); CHECK_NOTNULL(target_image); target_image->Allocate(static_cast(source_camera.width), static_cast(source_camera.height), source_image.IsRGB()); // To avoid aliasing, perform the warping in the source resolution and // then rescale the image at the end. Camera scaled_target_camera = target_camera; if (target_camera.width != source_camera.width || target_camera.height != source_camera.height) { scaled_target_camera.Rescale(source_camera.width, source_camera.height); } Eigen::Vector3d image_point(0, 0, 1); for (int y = 0; y < target_image->Height(); ++y) { image_point.y() = y + 0.5; for (int x = 0; x < target_image->Width(); ++x) { image_point.x() = x + 0.5; // Camera models assume that the upper left pixel center is (0.5, 0.5). const Eigen::Vector3d warped_point = H * image_point; const Eigen::Vector2d cam_point = target_camera.CamFromImg(warped_point.hnormalized()); const Eigen::Vector2d source_point = source_camera.ImgFromCam(cam_point); BitmapColor color; if (source_image.InterpolateBilinear( source_point.x() - 0.5, source_point.y() - 0.5, &color)) { target_image->SetPixel(x, y, color.Cast()); } else { target_image->SetPixel(x, y, BitmapColor(0)); } } } if (target_camera.width != source_camera.width || target_camera.height != source_camera.height) { target_image->Rescale(target_camera.width, target_camera.height); } } void ResampleImageBilinear(const float* data, const int rows, const int cols, const int new_rows, const int new_cols, float* resampled) { CHECK_NOTNULL(data); CHECK_NOTNULL(resampled); CHECK_GT(rows, 0); CHECK_GT(cols, 0); CHECK_GT(new_rows, 0); CHECK_GT(new_cols, 0); const float scale_r = static_cast(rows) / static_cast(new_rows); const float scale_c = static_cast(cols) / static_cast(new_cols); for (int r = 0; r < new_rows; ++r) { const float r_i = (r + 0.5f) * scale_r - 0.5f; const int r_i_min = std::floor(r_i); const int r_i_max = r_i_min + 1; const float d_r_min = r_i - r_i_min; const float d_r_max = r_i_max - r_i; for (int c = 0; c < new_cols; ++c) { const float c_i = (c + 0.5f) * scale_c - 0.5f; const int c_i_min = std::floor(c_i); const int c_i_max = c_i_min + 1; const float d_c_min = c_i - c_i_min; const float d_c_max = c_i_max - c_i; // Interpolation in column direction. const float value1 = d_c_max * GetPixelConstantBorder(data, rows, cols, r_i_min, c_i_min) + d_c_min * GetPixelConstantBorder(data, rows, cols, r_i_min, c_i_max); const float value2 = d_c_max * GetPixelConstantBorder(data, rows, cols, r_i_max, c_i_min) + d_c_min * GetPixelConstantBorder(data, rows, cols, r_i_max, c_i_max); // Interpolation in row direction. resampled[r * new_cols + c] = d_r_max * value1 + d_r_min * value2; } } } void SmoothImage(const float* data, const int rows, const int cols, const float sigma_r, const float sigma_c, float* smoothed) { CHECK_NOTNULL(data); CHECK_NOTNULL(smoothed); CHECK_GT(rows, 0); CHECK_GT(cols, 0); CHECK_GT(sigma_r, 0); CHECK_GT(sigma_c, 0); vl_imsmooth_f(smoothed, cols, data, cols, rows, cols, sigma_c, sigma_r); } void DownsampleImage(const float* data, const int rows, const int cols, const int new_rows, const int new_cols, float* downsampled) { CHECK_NOTNULL(data); CHECK_NOTNULL(downsampled); CHECK_LE(new_rows, rows); CHECK_LE(new_cols, cols); CHECK_GT(rows, 0); CHECK_GT(cols, 0); CHECK_GT(new_rows, 0); CHECK_GT(new_cols, 0); const float scale_c = static_cast(cols) / static_cast(new_cols); const float scale_r = static_cast(rows) / static_cast(new_rows); const float kSigmaScale = 0.5f; const float sigma_c = std::max(std::numeric_limits::epsilon(), kSigmaScale * (scale_c - 1)); const float sigma_r = std::max(std::numeric_limits::epsilon(), kSigmaScale * (scale_r - 1)); std::vector smoothed(rows * cols); SmoothImage(data, rows, cols, sigma_r, sigma_c, smoothed.data()); ResampleImageBilinear( smoothed.data(), rows, cols, new_rows, new_cols, downsampled); } } // namespace colmap colmap-3.9.1/src/colmap/image/warp.h000066400000000000000000000077211454702036400172610ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/camera.h" #include "colmap/sensor/bitmap.h" namespace colmap { // Warp source image to target image by projecting the pixels of the target // image up to infinity and projecting it down into the source image // (i.e. an inverse mapping). The function allocates the target image. void WarpImageBetweenCameras(const Camera& source_camera, const Camera& target_camera, const Bitmap& source_image, Bitmap* target_image); // Warp an image with the given homography, where H defines the pixel mapping // from the target to source image. Note that the pixel centers are assumed to // have coordinates (0.5, 0.5). void WarpImageWithHomography(const Eigen::Matrix3d& H, const Bitmap& source_image, Bitmap* target_image); // First, warp source image to target image by projecting the pixels of the // target image up to infinity and projecting it down into the source image // (i.e. an inverse mapping). Second, warp the coordinates from the first // warping with the given homography. The function allocates the target image. void WarpImageWithHomographyBetweenCameras(const Eigen::Matrix3d& H, const Camera& source_camera, const Camera& target_camera, const Bitmap& source_image, Bitmap* target_image); // Resample row-major image using bilinear interpolation. void ResampleImageBilinear(const float* data, int rows, int cols, int new_rows, int new_cols, float* resampled); // Smooth row-major image using a Gaussian filter kernel. void SmoothImage(const float* data, int rows, int cols, float sigma_r, float sigma_c, float* smoothed); // Downsample row-major image by first smoothing and then resampling. void DownsampleImage(const float* data, int rows, int cols, int new_rows, int new_cols, float* downsampled); } // namespace colmap colmap-3.9.1/src/colmap/image/warp_test.cc000066400000000000000000000234641454702036400204600ustar00rootroot00000000000000// 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. #include "colmap/image/warp.h" #include "colmap/math/random.h" #include namespace colmap { namespace { namespace { void GenerateRandomBitmap(const int width, const int height, const bool as_rgb, Bitmap* bitmap) { bitmap->Allocate(width, height, as_rgb); for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { BitmapColor color; color.r = RandomUniformInteger(0, 255); color.g = RandomUniformInteger(0, 255); color.b = RandomUniformInteger(0, 255); bitmap->SetPixel(x, y, color); } } } // Check that the two bitmaps are equal, ignoring a 1px boundary. void CheckBitmapsEqual(const Bitmap& bitmap1, const Bitmap& bitmap2) { ASSERT_EQ(bitmap1.IsGrey(), bitmap2.IsGrey()); ASSERT_EQ(bitmap1.IsRGB(), bitmap2.IsRGB()); ASSERT_EQ(bitmap1.Width(), bitmap2.Width()); ASSERT_EQ(bitmap1.Height(), bitmap2.Height()); for (int x = 1; x < bitmap1.Width() - 1; ++x) { for (int y = 1; y < bitmap1.Height() - 1; ++y) { BitmapColor color1; BitmapColor color2; EXPECT_TRUE(bitmap1.GetPixel(x, y, &color1)); EXPECT_TRUE(bitmap2.GetPixel(x, y, &color2)); EXPECT_EQ(color1, color2); } } } // Check that the two bitmaps are equal, ignoring a 1px boundary. void CheckBitmapsTransposed(const Bitmap& bitmap1, const Bitmap& bitmap2) { ASSERT_EQ(bitmap1.IsGrey(), bitmap2.IsGrey()); ASSERT_EQ(bitmap1.IsRGB(), bitmap2.IsRGB()); ASSERT_EQ(bitmap1.Width(), bitmap2.Width()); ASSERT_EQ(bitmap1.Height(), bitmap2.Height()); for (int x = 1; x < bitmap1.Width() - 1; ++x) { for (int y = 1; y < bitmap1.Height() - 1; ++y) { BitmapColor color1; BitmapColor color2; EXPECT_TRUE(bitmap1.GetPixel(x, y, &color1)); EXPECT_TRUE(bitmap2.GetPixel(y, x, &color2)); EXPECT_EQ(color1, color2); } } } } // namespace TEST(Warp, IdenticalCameras) { const Camera camera = Camera::CreateFromModelName(1, "PINHOLE", 1, 100, 100); Bitmap source_image_gray; GenerateRandomBitmap(100, 100, false, &source_image_gray); Bitmap target_image_gray; WarpImageBetweenCameras( camera, camera, source_image_gray, &target_image_gray); CheckBitmapsEqual(source_image_gray, target_image_gray); Bitmap source_image_rgb; GenerateRandomBitmap(100, 100, true, &source_image_rgb); Bitmap target_image_rgb; WarpImageBetweenCameras(camera, camera, source_image_rgb, &target_image_rgb); CheckBitmapsEqual(source_image_rgb, target_image_rgb); } TEST(Warp, ShiftedCameras) { const Camera source_camera = Camera::CreateFromModelName(1, "PINHOLE", 1, 100, 100); Camera target_camera = source_camera; target_camera.SetPrincipalPointX(0.0); Bitmap source_image_gray; GenerateRandomBitmap(100, 100, true, &source_image_gray); Bitmap target_image_gray; WarpImageBetweenCameras( source_camera, target_camera, source_image_gray, &target_image_gray); for (int x = 0; x < target_image_gray.Width(); ++x) { for (int y = 0; y < target_image_gray.Height(); ++y) { BitmapColor color; EXPECT_TRUE(target_image_gray.GetPixel(x, y, &color)); if (x >= 50) { EXPECT_EQ(color, BitmapColor(0)); } else { BitmapColor source_color; if (source_image_gray.GetPixel(x + 50, y, &source_color) && color != BitmapColor(0)) { EXPECT_EQ(color, source_color); } } } } } TEST(Warp, WarpImageWithHomographyIdentity) { Bitmap source_image_gray; GenerateRandomBitmap(100, 100, false, &source_image_gray); Bitmap target_image_gray; target_image_gray.Allocate(100, 100, false); WarpImageWithHomography( Eigen::Matrix3d::Identity(), source_image_gray, &target_image_gray); CheckBitmapsEqual(source_image_gray, target_image_gray); Bitmap source_image_rgb; GenerateRandomBitmap(100, 100, true, &source_image_rgb); Bitmap target_image_rgb; target_image_rgb.Allocate(100, 100, true); WarpImageWithHomography( Eigen::Matrix3d::Identity(), source_image_rgb, &target_image_rgb); CheckBitmapsEqual(source_image_rgb, target_image_rgb); } TEST(Warp, WarpImageWithHomographyTransposed) { Eigen::Matrix3d H; H << 0, 1, 0, 1, 0, 0, 0, 0, 1; Bitmap source_image_gray; GenerateRandomBitmap(100, 100, false, &source_image_gray); Bitmap target_image_gray; target_image_gray.Allocate(100, 100, false); WarpImageWithHomography(H, source_image_gray, &target_image_gray); CheckBitmapsTransposed(source_image_gray, target_image_gray); Bitmap source_image_rgb; GenerateRandomBitmap(100, 100, true, &source_image_rgb); Bitmap target_image_rgb; target_image_rgb.Allocate(100, 100, true); WarpImageWithHomography(H, source_image_rgb, &target_image_rgb); CheckBitmapsTransposed(source_image_rgb, target_image_rgb); } TEST(Warp, WarpImageWithHomographyBetweenCamerasIdentity) { const Camera camera = Camera::CreateFromModelName(1, "PINHOLE", 1, 100, 100); Bitmap source_image_gray; GenerateRandomBitmap(100, 100, false, &source_image_gray); Bitmap target_image_gray; target_image_gray.Allocate(100, 100, false); WarpImageWithHomographyBetweenCameras(Eigen::Matrix3d::Identity(), camera, camera, source_image_gray, &target_image_gray); CheckBitmapsEqual(source_image_gray, target_image_gray); Bitmap source_image_rgb; GenerateRandomBitmap(100, 100, true, &source_image_rgb); Bitmap target_image_rgb; target_image_rgb.Allocate(100, 100, true); WarpImageWithHomographyBetweenCameras(Eigen::Matrix3d::Identity(), camera, camera, source_image_rgb, &target_image_rgb); CheckBitmapsEqual(source_image_rgb, target_image_rgb); } TEST(Warp, WarpImageWithHomographyBetweenCamerasTransposed) { const Camera camera = Camera::CreateFromModelName(1, "PINHOLE", 1, 100, 100); Eigen::Matrix3d H; H << 0, 1, 0, 1, 0, 0, 0, 0, 1; Bitmap source_image_gray; GenerateRandomBitmap(100, 100, false, &source_image_gray); Bitmap target_image_gray; target_image_gray.Allocate(100, 100, false); WarpImageWithHomographyBetweenCameras( H, camera, camera, source_image_gray, &target_image_gray); CheckBitmapsTransposed(source_image_gray, target_image_gray); Bitmap source_image_rgb; GenerateRandomBitmap(100, 100, true, &source_image_rgb); Bitmap target_image_rgb; target_image_rgb.Allocate(100, 100, true); WarpImageWithHomographyBetweenCameras( H, camera, camera, source_image_rgb, &target_image_rgb); CheckBitmapsTransposed(source_image_rgb, target_image_rgb); } TEST(Warp, ResampleImageBilinear) { std::vector image(16); for (size_t i = 0; i < image.size(); ++i) { image[i] = i; } std::vector resampled(4); ResampleImageBilinear(image.data(), 4, 4, 2, 2, resampled.data()); EXPECT_EQ(resampled[0], 2.5); EXPECT_EQ(resampled[1], 4.5); EXPECT_EQ(resampled[2], 10.5); EXPECT_EQ(resampled[3], 12.5); } TEST(Warp, SmoothImage) { std::vector image(16); for (size_t i = 0; i < image.size(); ++i) { image[i] = i; } std::vector smoothed(16); SmoothImage(image.data(), 4, 4, 1, 1, smoothed.data()); EXPECT_NEAR(smoothed[0], 1.81673253, 1e-3); EXPECT_NEAR(smoothed[1], 2.51182437, 1e-3); EXPECT_NEAR(smoothed[2], 3.39494729, 1e-3); EXPECT_NEAR(smoothed[3], 4.09003973, 1e-3); EXPECT_NEAR(smoothed[4], 4.59710073, 1e-3); EXPECT_NEAR(smoothed[5], 5.29219341, 1e-3); EXPECT_NEAR(smoothed[6], 6.17531633, 1e-3); EXPECT_NEAR(smoothed[7], 6.87040806, 1e-3); } TEST(Warp, DownsampleImage) { std::vector image(16); for (size_t i = 0; i < image.size(); ++i) { image[i] = i; } std::vector downsampled(4); DownsampleImage(image.data(), 4, 4, 2, 2, downsampled.data()); EXPECT_NEAR(downsampled[0], 2.76810598, 1e-3); EXPECT_NEAR(downsampled[1], 4.66086388, 1e-3); EXPECT_NEAR(downsampled[2], 10.3391361, 1e-3); EXPECT_NEAR(downsampled[3], 12.2318935, 1e-3); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/math/000077500000000000000000000000001454702036400157775ustar00rootroot00000000000000colmap-3.9.1/src/colmap/math/CMakeLists.txt000066400000000000000000000045631454702036400205470ustar00rootroot00000000000000# 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. set(FOLDER_NAME "math") COLMAP_ADD_LIBRARY( NAME colmap_math SRCS graph_cut.h graph_cut.cc math.h math.cc matrix.h polynomial.h polynomial.cc random.h random.cc PUBLIC_LINK_LIBS colmap_util Boost::graph Eigen3::Eigen PRIVATE_LINK_LIBS Boost::boost metis ) COLMAP_ADD_TEST( NAME graph_cut_test SRCS graph_cut_test.cc LINK_LIBS colmap_math ) COLMAP_ADD_TEST( NAME math_test SRCS math_test.cc LINK_LIBS colmap_math ) COLMAP_ADD_TEST( NAME matrix_test SRCS matrix_test.cc LINK_LIBS colmap_math ) COLMAP_ADD_TEST( NAME polynomial_test SRCS polynomial_test.cc LINK_LIBS colmap_math ) COLMAP_ADD_TEST( NAME random_test SRCS random_test.cc LINK_LIBS colmap_math ) colmap-3.9.1/src/colmap/math/graph_cut.cc000066400000000000000000000166461454702036400202770ustar00rootroot00000000000000// 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. #include "colmap/math/graph_cut.h" #include #include #include extern "C" { #include "metis.h" } #include "colmap/util/logging.h" namespace colmap { namespace { // Wrapper class for weighted, undirected Metis graph. class MetisGraph { public: MetisGraph(const std::vector>& edges, const std::vector& weights) { std::unordered_map>> adjacency_list; for (size_t i = 0; i < edges.size(); ++i) { const auto& edge = edges[i]; const auto weight = weights[i]; const int vertex_idx1 = GetVertexIdx(edge.first); const int vertex_idx2 = GetVertexIdx(edge.second); adjacency_list[vertex_idx1].emplace_back(vertex_idx2, weight); adjacency_list[vertex_idx2].emplace_back(vertex_idx1, weight); } xadj_.reserve(vertex_id_to_idx_.size() + 1); adjncy_.reserve(2 * edges.size()); adjwgt_.reserve(2 * edges.size()); idx_t edge_idx = 0; for (size_t i = 0; i < vertex_id_to_idx_.size(); ++i) { xadj_.push_back(edge_idx); if (adjacency_list.count(i) == 0) { continue; } for (const auto& edge : adjacency_list[i]) { edge_idx += 1; adjncy_.push_back(edge.first); adjwgt_.push_back(edge.second); } } xadj_.push_back(edge_idx); CHECK_EQ(edge_idx, 2 * edges.size()); CHECK_EQ(xadj_.size(), vertex_id_to_idx_.size() + 1); CHECK_EQ(adjncy_.size(), 2 * edges.size()); CHECK_EQ(adjwgt_.size(), 2 * edges.size()); nvtxs = vertex_id_to_idx_.size(); xadj = xadj_.data(); adjncy = adjncy_.data(); vwgt = nullptr; adjwgt = adjwgt_.data(); } int GetVertexIdx(const int id) { const auto it = vertex_id_to_idx_.find(id); if (it == vertex_id_to_idx_.end()) { const int idx = vertex_id_to_idx_.size(); vertex_id_to_idx_.emplace(id, idx); vertex_idx_to_id_.emplace(idx, id); return idx; } else { return it->second; } } int GetVertexId(const int idx) { return vertex_idx_to_id_.at(idx); } idx_t nvtxs = 0; idx_t* xadj = nullptr; idx_t* vwgt = nullptr; idx_t* adjncy = nullptr; idx_t* adjwgt = nullptr; private: std::unordered_map vertex_id_to_idx_; std::unordered_map vertex_idx_to_id_; std::vector xadj_; std::vector adjncy_; std::vector adjwgt_; }; } // namespace void ComputeMinGraphCutStoerWagner( const std::vector>& edges, const std::vector& weights, int* cut_weight, std::vector* cut_labels) { CHECK_EQ(edges.size(), weights.size()); CHECK_GE(edges.size(), 2); typedef boost::property edge_weight_t; typedef boost::adjacency_list undirected_graph_t; int max_vertex_index = 0; for (const auto& edge : edges) { CHECK_GE(edge.first, 0); CHECK_GE(edge.second, 0); max_vertex_index = std::max(max_vertex_index, edge.first); max_vertex_index = std::max(max_vertex_index, edge.second); } const undirected_graph_t graph(edges.begin(), edges.end(), weights.begin(), max_vertex_index + 1, edges.size()); const auto edge_weight = boost::get(boost::edge_weight, graph); const auto parities = boost::make_one_bit_color_map( boost::num_vertices(graph), boost::get(boost::vertex_index, graph)); const auto parity_map = boost::parity_map(parities); // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDelete) *cut_weight = boost::stoer_wagner_min_cut(graph, edge_weight, parity_map); cut_labels->resize(boost::num_vertices(graph)); for (size_t i = 0; i < boost::num_vertices(graph); ++i) { (*cut_labels)[i] = boost::get(parities, i); } } std::unordered_map ComputeNormalizedMinGraphCut( const std::vector>& edges, const std::vector& weights, const int num_parts) { CHECK(!edges.empty()); CHECK_EQ(edges.size(), weights.size()); CHECK_GT(num_parts, 0); MetisGraph graph(edges, weights); idx_t ncon = 1; idx_t edgecut = -1; idx_t nparts = num_parts; idx_t metisOptions[METIS_NOPTIONS]; METIS_SetDefaultOptions(metisOptions); std::vector cut_labels(graph.nvtxs, -1); const int metisResult = METIS_PartGraphKway(&graph.nvtxs, /*ncon=*/&ncon, graph.xadj, graph.adjncy, /*vwgt=*/nullptr, /*vsize=*/nullptr, graph.adjwgt, &nparts, /*tpwgts=*/nullptr, /*ubvec=*/nullptr, metisOptions, &edgecut, cut_labels.data()); if (metisResult == METIS_ERROR_INPUT) { LOG(FATAL) << "INTERNAL: Metis input error"; } else if (metisResult == METIS_ERROR_MEMORY) { LOG(FATAL) << "INTERNAL: Metis memory error"; } else if (metisResult == METIS_ERROR) { LOG(FATAL) << "INTERNAL: Metis 'some other type of error'"; } std::unordered_map labels; for (size_t idx = 0; idx < cut_labels.size(); ++idx) { labels.emplace(graph.GetVertexId(idx), cut_labels[idx]); } return labels; } } // namespace colmap colmap-3.9.1/src/colmap/math/graph_cut.h000066400000000000000000000173451454702036400201360ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include #include #include #include #include #include namespace colmap { // Compute the min-cut of a undirected graph using the Stoer Wagner algorithm. void ComputeMinGraphCutStoerWagner( const std::vector>& edges, const std::vector& weights, int* cut_weight, std::vector* cut_labels); // Compute the normalized min-cut of an undirected graph using Metis. // Partitions the graph into clusters and returns the cluster labels per vertex. std::unordered_map ComputeNormalizedMinGraphCut( const std::vector>& edges, const std::vector& weights, int num_parts); // Compute the minimum graph cut of a directed S-T graph using the // Boykov-Kolmogorov max-flow min-cut algorithm, as descibed in: // "An Experimental Comparison of Min-Cut/Max-Flow Algorithms for Energy // Minimization in Vision". Yuri Boykov and Vladimir Kolmogorov. PAMI, 2004. template class MinSTGraphCut { public: typedef boost:: adjacency_list_traits graph_traits_t; typedef graph_traits_t::edge_descriptor edge_descriptor_t; typedef graph_traits_t::vertices_size_type vertices_size_t; struct Edge { value_t capacity; value_t residual; edge_descriptor_t reverse; }; typedef boost:: adjacency_list graph_t; explicit MinSTGraphCut(size_t num_nodes); // Count the number of nodes and edges in the graph. size_t NumNodes() const; size_t NumEdges() const; // Add node to the graph. void AddNode(node_t node_idx, value_t source_capacity, value_t sink_capacity); // Add edge to the graph. void AddEdge(node_t node_idx1, node_t node_idx2, value_t capacity, value_t reverse_capacity); // Compute the min-cut using the max-flow algorithm. Returns the flow. value_t Compute(); // Check whether node is connected to source or sink after computing the cut. bool IsConnectedToSource(node_t node_idx) const; bool IsConnectedToSink(node_t node_idx) const; private: const node_t S_node_; const node_t T_node_; graph_t graph_; std::vector colors_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template MinSTGraphCut::MinSTGraphCut(const size_t num_nodes) : S_node_(num_nodes), T_node_(num_nodes + 1), graph_(num_nodes + 2) {} template size_t MinSTGraphCut::NumNodes() const { return boost::num_vertices(graph_) - 2; } template size_t MinSTGraphCut::NumEdges() const { return boost::num_edges(graph_); } template void MinSTGraphCut::AddNode(const node_t node_idx, const value_t source_capacity, const value_t sink_capacity) { CHECK_GE(node_idx, 0); CHECK_LE(node_idx, boost::num_vertices(graph_)); CHECK_GE(source_capacity, 0); CHECK_GE(sink_capacity, 0); if (source_capacity > 0) { const edge_descriptor_t edge = boost::add_edge(S_node_, node_idx, graph_).first; const edge_descriptor_t edge_reverse = boost::add_edge(node_idx, S_node_, graph_).first; graph_[edge].capacity = source_capacity; graph_[edge].reverse = edge_reverse; graph_[edge_reverse].reverse = edge; } if (sink_capacity > 0) { const edge_descriptor_t edge = boost::add_edge(node_idx, T_node_, graph_).first; const edge_descriptor_t edge_reverse = boost::add_edge(T_node_, node_idx, graph_).first; graph_[edge].capacity = sink_capacity; graph_[edge].reverse = edge_reverse; graph_[edge_reverse].reverse = edge; } } template void MinSTGraphCut::AddEdge(const node_t node_idx1, const node_t node_idx2, const value_t capacity, const value_t reverse_capacity) { CHECK_GE(node_idx1, 0); CHECK_LE(node_idx1, boost::num_vertices(graph_)); CHECK_GE(node_idx2, 0); CHECK_LE(node_idx2, boost::num_vertices(graph_)); CHECK_GE(capacity, 0); CHECK_GE(reverse_capacity, 0); const edge_descriptor_t edge = boost::add_edge(node_idx1, node_idx2, graph_).first; const edge_descriptor_t edge_reverse = boost::add_edge(node_idx2, node_idx1, graph_).first; graph_[edge].capacity = capacity; graph_[edge_reverse].capacity = reverse_capacity; graph_[edge].reverse = edge_reverse; graph_[edge_reverse].reverse = edge; } template value_t MinSTGraphCut::Compute() { const vertices_size_t num_vertices = boost::num_vertices(graph_); colors_.resize(num_vertices); std::vector predecessors(num_vertices); std::vector distances(num_vertices); return boost::boykov_kolmogorov_max_flow( graph_, boost::get(&Edge::capacity, graph_), boost::get(&Edge::residual, graph_), boost::get(&Edge::reverse, graph_), predecessors.data(), colors_.data(), distances.data(), boost::get(boost::vertex_index, graph_), S_node_, T_node_); } template bool MinSTGraphCut::IsConnectedToSource( const node_t node_idx) const { return colors_.at(node_idx) != boost::white_color; } template bool MinSTGraphCut::IsConnectedToSink( const node_t node_idx) const { return colors_.at(node_idx) == boost::white_color; } } // namespace colmap colmap-3.9.1/src/colmap/math/graph_cut_test.cc000066400000000000000000000273701454702036400213320ustar00rootroot00000000000000// 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. #include "colmap/math/graph_cut.h" #include namespace colmap { namespace { TEST(GraphCut, ComputeMinGraphCutStoerWagner) { const std::vector> edges = {{3, 4}, {3, 6}, {3, 5}, {0, 4}, {0, 1}, {0, 6}, {0, 7}, {0, 5}, {0, 2}, {4, 1}, {1, 6}, {1, 5}, {6, 7}, {7, 5}, {5, 2}, {3, 4}}; const std::vector weights = { 0, 3, 1, 3, 1, 2, 6, 1, 8, 1, 1, 80, 2, 1, 1, 4}; int cut_weight; std::vector cut_labels; ComputeMinGraphCutStoerWagner(edges, weights, &cut_weight, &cut_labels); EXPECT_EQ(cut_weight, 7); EXPECT_EQ(cut_labels.size(), 8); for (const auto& label : cut_labels) { EXPECT_GE(label, 0); EXPECT_LT(label, 2); } } TEST(GraphCut, ComputeMinGraphCutStoerWagnerDuplicateEdge) { const std::vector> edges = {{3, 4}, {3, 6}, {3, 5}, {0, 4}, {0, 1}, {0, 6}, {0, 7}, {0, 5}, {0, 2}, {4, 1}, {1, 6}, {1, 5}, {6, 7}, {7, 5}, {5, 2}, {3, 4}, {3, 4}}; const std::vector weights = { 0, 3, 1, 3, 1, 2, 6, 1, 8, 1, 1, 80, 2, 1, 1, 4, 4}; int cut_weight; std::vector cut_labels; ComputeMinGraphCutStoerWagner(edges, weights, &cut_weight, &cut_labels); EXPECT_EQ(cut_weight, 7); EXPECT_EQ(cut_labels.size(), 8); for (const auto& label : cut_labels) { EXPECT_GE(label, 0); EXPECT_LT(label, 2); } } TEST(GraphCut, ComputeMinGraphCutStoerWagnerMissingVertex) { const std::vector> edges = {{3, 4}, {3, 6}, {3, 5}, {0, 1}, {0, 6}, {0, 7}, {0, 5}, {0, 2}, {4, 1}, {1, 6}, {1, 5}, {6, 7}, {7, 5}, {5, 2}}; const std::vector weights = {0, 3, 1, 3, 1, 2, 6, 1, 8, 1, 1, 80, 2, 1}; int cut_weight; std::vector cut_labels; ComputeMinGraphCutStoerWagner(edges, weights, &cut_weight, &cut_labels); EXPECT_EQ(cut_weight, 2); EXPECT_EQ(cut_labels.size(), 8); for (const auto& label : cut_labels) { EXPECT_GE(label, 0); EXPECT_LT(label, 2); } } TEST(GraphCut, ComputeMinGraphCutStoerWagnerDisconnected) { const std::vector> edges = {{0, 1}, {1, 2}, {3, 4}}; const std::vector weights = {1, 3, 1}; int cut_weight; std::vector cut_labels; ComputeMinGraphCutStoerWagner(edges, weights, &cut_weight, &cut_labels); EXPECT_EQ(cut_weight, 0); EXPECT_EQ(cut_labels.size(), 5); for (const auto& label : cut_labels) { EXPECT_GE(label, 0); EXPECT_LT(label, 2); } } TEST(GraphCut, ComputeNormalizedMinGraphCut) { const std::vector> edges = {{3, 4}, {3, 6}, {3, 5}, {0, 4}, {0, 1}, {0, 6}, {0, 7}, {0, 5}, {0, 2}, {4, 1}, {1, 6}, {1, 5}, {6, 7}, {7, 5}, {5, 2}, {3, 4}}; const std::vector weights = { 0, 3, 1, 3, 1, 2, 6, 1, 8, 1, 1, 80, 2, 1, 1, 4}; const auto cut_labels = ComputeNormalizedMinGraphCut(edges, weights, 2); EXPECT_EQ(cut_labels.size(), 8); size_t num_labels[2] = {0}; for (const auto& label : cut_labels) { EXPECT_GE(label.second, 0); EXPECT_LT(label.second, 2); num_labels[label.second] += 1; } EXPECT_GT(num_labels[0], 0); EXPECT_GT(num_labels[1], 0); } TEST(GraphCut, ComputeNormalizedMinGraphCutDuplicateEdge) { const std::vector> edges = {{3, 4}, {3, 6}, {3, 5}, {0, 4}, {0, 1}, {0, 6}, {0, 7}, {0, 5}, {0, 2}, {4, 1}, {1, 6}, {1, 5}, {6, 7}, {7, 5}, {5, 2}, {3, 4}, {3, 4}}; const std::vector weights = { 0, 3, 1, 3, 1, 2, 6, 1, 8, 1, 1, 80, 2, 1, 1, 4, 4}; const auto cut_labels = ComputeNormalizedMinGraphCut(edges, weights, 2); EXPECT_EQ(cut_labels.size(), 8); size_t num_labels[2] = {0}; for (const auto& label : cut_labels) { EXPECT_GE(label.second, 0); EXPECT_LT(label.second, 2); num_labels[label.second] += 1; } EXPECT_GT(num_labels[0], 0); EXPECT_GT(num_labels[1], 0); } TEST(GraphCut, ComputeNormalizedMinGraphCutMissingVertex) { const std::vector> edges = {{3, 4}, {3, 6}, {3, 5}, {0, 1}, {0, 6}, {0, 7}, {0, 5}, {0, 2}, {4, 1}, {1, 6}, {1, 5}, {6, 7}, {7, 5}, {5, 2}}; const std::vector weights = {0, 3, 1, 3, 1, 2, 6, 1, 8, 1, 1, 80, 2, 1}; const auto cut_labels = ComputeNormalizedMinGraphCut(edges, weights, 2); EXPECT_EQ(cut_labels.size(), 8); size_t num_labels[2] = {0}; for (const auto& label : cut_labels) { EXPECT_GE(label.second, 0); EXPECT_LT(label.second, 2); num_labels[label.second] += 1; } EXPECT_GT(num_labels[0], 0); EXPECT_GT(num_labels[1], 0); } TEST(GraphCut, ComputeNormalizedMinGraphCutDisconnected) { const std::vector> edges = {{0, 1}, {1, 2}, {3, 4}}; const std::vector weights = {1, 3, 1}; const auto cut_labels = ComputeNormalizedMinGraphCut(edges, weights, 2); EXPECT_EQ(cut_labels.size(), 5); EXPECT_EQ(cut_labels.at(0), cut_labels.at(1)); EXPECT_EQ(cut_labels.at(1), cut_labels.at(2)); EXPECT_NE(cut_labels.at(2), cut_labels.at(3)); EXPECT_EQ(cut_labels.at(3), cut_labels.at(4)); } TEST(GraphCut, MinSTGraphCut1) { MinSTGraphCut graph(2); EXPECT_EQ(graph.NumNodes(), 2); EXPECT_EQ(graph.NumEdges(), 0); graph.AddNode(0, 5, 1); graph.AddNode(1, 2, 6); graph.AddEdge(0, 1, 3, 4); EXPECT_EQ(graph.NumEdges(), 10); EXPECT_EQ(graph.Compute(), 6); EXPECT_TRUE(graph.IsConnectedToSource(0)); EXPECT_TRUE(graph.IsConnectedToSink(1)); } TEST(GraphCut, MinSTGraphCut2) { MinSTGraphCut graph(2); graph.AddNode(0, 1, 5); graph.AddNode(1, 2, 6); graph.AddEdge(0, 1, 3, 4); EXPECT_EQ(graph.NumEdges(), 10); EXPECT_EQ(graph.Compute(), 3); EXPECT_TRUE(graph.IsConnectedToSink(0)); EXPECT_TRUE(graph.IsConnectedToSink(1)); } TEST(GraphCut, MinSTGraphCut3) { MinSTGraphCut graph(3); graph.AddNode(0, 6, 4); graph.AddNode(2, 3, 6); graph.AddEdge(0, 1, 2, 4); graph.AddEdge(1, 2, 3, 5); EXPECT_EQ(graph.NumEdges(), 12); EXPECT_EQ(graph.Compute(), 9); EXPECT_TRUE(graph.IsConnectedToSource(0)); EXPECT_TRUE(graph.IsConnectedToSink(1)); EXPECT_TRUE(graph.IsConnectedToSink(2)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/math/math.cc000066400000000000000000000034551454702036400172460ustar00rootroot00000000000000// 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. #include "colmap/math/math.h" namespace colmap { size_t NChooseK(const size_t n, const size_t k) { if (k == 0) { return 1; } return (n * NChooseK(n - 1, k - 1)) / k; } } // namespace colmap colmap-3.9.1/src/colmap/math/math.h000066400000000000000000000202751454702036400171070ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include #include #include #include #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846264338327950288 #endif namespace colmap { // Return 1 if number is positive, -1 if negative, and 0 if the number is 0. template int SignOfNumber(T val); // Clamp the given value to a low and maximum value. template inline T Clamp(const T& value, const T& low, const T& high); // Convert angle in degree to radians. inline float DegToRad(float deg); inline double DegToRad(double deg); // Convert angle in radians to degree. inline float RadToDeg(float rad); inline double RadToDeg(double rad); // Determine median value in vector. Returns NaN for empty vectors. template double Median(const std::vector& elems); // Determine mean value in a vector. template double Mean(const std::vector& elems); // Determine sample variance in a vector. template double Variance(const std::vector& elems); // Determine sample standard deviation in a vector. template double StdDev(const std::vector& elems); // Generate N-choose-K combinations. // // Note that elements in range [first, last) must be in sorted order, // according to `std::less`. template bool NextCombination(Iterator first, Iterator middle, Iterator last); // Sigmoid function. template T Sigmoid(T x, T alpha = 1); // Scale values according to sigmoid transform. // // x \in [0, 1] -> x \in [-x0, x0] -> sigmoid(x, alpha) -> x \in [0, 1] // // @param x Value to be scaled in the range [0, 1]. // @param x0 Spread that determines the range x is scaled to. // @param alpha Exponential sigmoid factor. // // @return The scaled value in the range [0, 1]. template T ScaleSigmoid(T x, T alpha = 1, T x0 = 10); // Binomial coefficient or all combinations, defined as n! / ((n - k)! k!). size_t NChooseK(size_t n, size_t k); // Cast value from one type to another and truncate instead of overflow, if the // input value is out of range of the output data type. template T2 TruncateCast(T1 value); // Compute the n-th percentile in the given sequence. template T Percentile(const std::vector& elems, double p); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// namespace internal { template bool NextCombination(Iterator first1, Iterator last1, Iterator first2, Iterator last2) { if ((first1 == last1) || (first2 == last2)) { return false; } Iterator m1 = last1; Iterator m2 = last2; --m2; while (--m1 != first1 && *m1 >= *m2) { } bool result = (m1 == first1) && *first1 >= *m2; if (!result) { while (first2 != m2 && *m1 >= *first2) { ++first2; } first1 = m1; std::iter_swap(first1, first2); ++first1; ++first2; } if ((first1 != last1) && (first2 != last2)) { m1 = last1; m2 = first2; while ((m1 != first1) && (m2 != last2)) { std::iter_swap(--m1, m2); ++m2; } std::reverse(first1, m1); std::reverse(first1, last1); std::reverse(m2, last2); std::reverse(first2, last2); } return !result; } } // namespace internal template int SignOfNumber(const T val) { return (T(0) < val) - (val < T(0)); } template T Clamp(const T& value, const T& low, const T& high) { return std::max(low, std::min(value, high)); } float DegToRad(const float deg) { return deg * 0.0174532925199432954743716805978692718781530857086181640625f; } double DegToRad(const double deg) { return deg * 0.0174532925199432954743716805978692718781530857086181640625; } // Convert angle in radians to degree. float RadToDeg(const float rad) { return rad * 57.29577951308232286464772187173366546630859375f; } double RadToDeg(const double rad) { return rad * 57.29577951308232286464772187173366546630859375; } template double Median(const std::vector& elems) { CHECK(!elems.empty()); const size_t mid_idx = elems.size() / 2; std::vector ordered_elems = elems; std::nth_element(ordered_elems.begin(), ordered_elems.begin() + mid_idx, ordered_elems.end()); if (elems.size() % 2 == 0) { const T mid_element1 = ordered_elems[mid_idx]; const T mid_element2 = *std::max_element(ordered_elems.begin(), ordered_elems.begin() + mid_idx); return 0.5 * mid_element1 + 0.5 * mid_element2; } else { return ordered_elems[mid_idx]; } } template T Percentile(const std::vector& elems, const double p) { CHECK(!elems.empty()); CHECK_GE(p, 0); CHECK_LE(p, 100); const int idx = static_cast(std::round(p / 100 * (elems.size() - 1))); const size_t percentile_idx = std::max(0, std::min(static_cast(elems.size() - 1), idx)); std::vector ordered_elems = elems; std::nth_element(ordered_elems.begin(), ordered_elems.begin() + percentile_idx, ordered_elems.end()); return ordered_elems.at(percentile_idx); } template double Mean(const std::vector& elems) { CHECK(!elems.empty()); double sum = 0; for (const auto el : elems) { sum += static_cast(el); } return sum / elems.size(); } template double Variance(const std::vector& elems) { const double mean = Mean(elems); double var = 0; for (const auto el : elems) { const double diff = el - mean; var += diff * diff; } return var / (elems.size() - 1); } template double StdDev(const std::vector& elems) { return std::sqrt(Variance(elems)); } template bool NextCombination(Iterator first, Iterator middle, Iterator last) { return internal::NextCombination(first, middle, middle, last); } template T Sigmoid(const T x, const T alpha) { return T(1) / (T(1) + std::exp(-x * alpha)); } template T ScaleSigmoid(T x, const T alpha, const T x0) { const T t0 = Sigmoid(-x0, alpha); const T t1 = Sigmoid(x0, alpha); x = (Sigmoid(2 * x0 * x - x0, alpha) - t0) / (t1 - t0); return x; } template T2 TruncateCast(const T1 value) { return static_cast(std::min( static_cast(std::numeric_limits::max()), std::max(static_cast(std::numeric_limits::min()), value))); } } // namespace colmap colmap-3.9.1/src/colmap/math/math_test.cc000066400000000000000000000155751454702036400203130ustar00rootroot00000000000000// 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. #include "colmap/math/math.h" #include namespace colmap { namespace { TEST(SignOfNumber, Nominal) { EXPECT_EQ(SignOfNumber(0), 0); EXPECT_EQ(SignOfNumber(-0.1), -1); EXPECT_EQ(SignOfNumber(0.1), 1); EXPECT_EQ(SignOfNumber(std::numeric_limits::quiet_NaN()), 0); EXPECT_EQ(SignOfNumber(std::numeric_limits::infinity()), 1); EXPECT_EQ(SignOfNumber(-std::numeric_limits::infinity()), -1); } TEST(Clamp, Nominal) { EXPECT_EQ(Clamp(0, -1, 1), 0); EXPECT_EQ(Clamp(0, 0, 1), 0); EXPECT_EQ(Clamp(0, -1, 0), 0); EXPECT_EQ(Clamp(0, -1, 1), 0); EXPECT_EQ(Clamp(0, 1, 2), 1); EXPECT_EQ(Clamp(0, -2, -1), -1); EXPECT_EQ(Clamp(0, 0, 0), 0); } TEST(DegToRad, Nominal) { EXPECT_EQ(DegToRad(0.0f), 0.0f); EXPECT_EQ(DegToRad(0.0), 0.0); EXPECT_LT(std::abs(DegToRad(180.0f) - M_PI), 1e-6f); EXPECT_LT(std::abs(DegToRad(180.0) - M_PI), 1e-6); } TEST(RadToDeg, Nominal) { EXPECT_EQ(RadToDeg(0.0f), 0.0f); EXPECT_EQ(RadToDeg(0.0), 0.0); EXPECT_LT(std::abs(RadToDeg(M_PI) - 180.0f), 1e-6f); EXPECT_LT(std::abs(RadToDeg(M_PI) - 180.0), 1e-6); } TEST(Median, Nominal) { EXPECT_EQ(Median({1, 2, 3, 4}), 2.5); EXPECT_EQ(Median({1, 2, 3, 100}), 2.5); EXPECT_EQ(Median({1, 2, 3, 4, 100}), 3); EXPECT_EQ(Median({-100, 1, 2, 3, 4}), 2); EXPECT_EQ(Median({-1, -2, -3, -4}), -2.5); EXPECT_EQ(Median({-1, -2, 3, 4}), 1); // Test integer overflow scenario. EXPECT_EQ(Median({100, 115, 119, 127}), 117); } TEST(Percentile, Nominal) { EXPECT_EQ(Percentile({0}, 0), 0); EXPECT_EQ(Percentile({0}, 50), 0); EXPECT_EQ(Percentile({0}, 100), 0); EXPECT_EQ(Percentile({0, 1}, 0), 0); EXPECT_EQ(Percentile({0, 1}, 50), 1); EXPECT_EQ(Percentile({0, 1}, 100), 1); EXPECT_EQ(Percentile({0, 1, 2}, 0), 0); EXPECT_EQ(Percentile({0, 1, 2}, 50), 1); EXPECT_EQ(Percentile({0, 1, 2}, 100), 2); EXPECT_EQ(Percentile({0, 1, 1, 2}, 0), 0); EXPECT_EQ(Percentile({0, 1, 1, 2}, 33), 1); EXPECT_EQ(Percentile({0, 1, 1, 2}, 50), 1); EXPECT_EQ(Percentile({0, 1, 1, 2}, 66), 1); EXPECT_EQ(Percentile({0, 1, 1, 2}, 100), 2); } TEST(Mean, Nominal) { EXPECT_EQ(Mean({1, 2, 3, 4}), 2.5); EXPECT_EQ(Mean({1, 2, 3, 100}), 26.5); EXPECT_EQ(Mean({1, 2, 3, 4, 100}), 22); EXPECT_EQ(Mean({-100, 1, 2, 3, 4}), -18); EXPECT_EQ(Mean({-1, -2, -3, -4}), -2.5); EXPECT_EQ(Mean({-1, -2, 3, 4}), 1); } TEST(Variance, Nominal) { EXPECT_LE(std::abs(Variance({1, 2, 3, 4}) - 1.66666666), 1e-6); EXPECT_LE(std::abs(Variance({1, 2, 3, 100}) - 2401.66666666), 1e-6); EXPECT_LE(std::abs(Variance({1, 2, 3, 4, 100}) - 1902.5), 1e-6); EXPECT_LE(std::abs(Variance({-100, 1, 2, 3, 4}) - 2102.5), 1e-6); EXPECT_LE(std::abs(Variance({-1, -2, -3, -4}) - 1.66666666), 1e-6); EXPECT_LE(std::abs(Variance({-1, -2, 3, 4}) - 8.66666666), 1e-6); } TEST(StdDev, Nominal) { EXPECT_LE(std::abs(std::sqrt(Variance({1, 2, 3, 4})) - StdDev({1, 2, 3, 4})), 1e-6); EXPECT_LE(std::abs(std::sqrt(Variance({1, 2, 3, 100})) - StdDev({1, 2, 3, 100})), 1e-6); } TEST(NextCombination, Nominal) { std::vector list{0}; EXPECT_FALSE(NextCombination(list.begin(), list.begin() + 1, list.end())); list = {0, 1}; EXPECT_FALSE(NextCombination(list.begin(), list.begin() + 2, list.end())); EXPECT_EQ(list[0], 0); EXPECT_TRUE(NextCombination(list.begin(), list.begin() + 1, list.end())); EXPECT_EQ(list[0], 1); EXPECT_FALSE(NextCombination(list.begin(), list.begin() + 1, list.end())); EXPECT_EQ(list[0], 0); list = {0, 1, 2}; EXPECT_EQ(list[0], 0); EXPECT_EQ(list[1], 1); EXPECT_EQ(list[2], 2); EXPECT_TRUE(NextCombination(list.begin(), list.begin() + 2, list.end())); EXPECT_EQ(list[0], 0); EXPECT_EQ(list[1], 2); EXPECT_EQ(list[2], 1); EXPECT_TRUE(NextCombination(list.begin(), list.begin() + 2, list.end())); EXPECT_EQ(list[0], 1); EXPECT_EQ(list[1], 2); EXPECT_EQ(list[2], 0); EXPECT_FALSE(NextCombination(list.begin(), list.begin() + 2, list.end())); EXPECT_EQ(list[0], 0); EXPECT_EQ(list[1], 1); EXPECT_EQ(list[2], 2); } TEST(Sigmoid, Nominal) { EXPECT_EQ(Sigmoid(0.0), 0.5); EXPECT_NEAR(Sigmoid(100.0), 1.0, 1e-10); EXPECT_NEAR(Sigmoid(-100.0), 0, 1e-10); } TEST(ScaleSigmoid, Nominal) { EXPECT_NEAR(ScaleSigmoid(0.5), 0.5, 1e-10); EXPECT_NEAR(ScaleSigmoid(1.0), 1.0, 1e-10); EXPECT_NEAR(ScaleSigmoid(-1.0), 0, 1e-4); } TEST(NChooseK, Nominal) { EXPECT_EQ(NChooseK(1, 0), 1); EXPECT_EQ(NChooseK(2, 0), 1); EXPECT_EQ(NChooseK(3, 0), 1); EXPECT_EQ(NChooseK(1, 1), 1); EXPECT_EQ(NChooseK(2, 1), 2); EXPECT_EQ(NChooseK(3, 1), 3); EXPECT_EQ(NChooseK(2, 2), 1); EXPECT_EQ(NChooseK(2, 3), 0); EXPECT_EQ(NChooseK(3, 2), 3); EXPECT_EQ(NChooseK(4, 2), 6); EXPECT_EQ(NChooseK(5, 2), 10); EXPECT_EQ(NChooseK(500, 3), 20708500); } TEST(TruncateCast, Nominal) { EXPECT_EQ((TruncateCast(-129)), -128); EXPECT_EQ((TruncateCast(128)), 127); EXPECT_EQ((TruncateCast(-1)), 0); EXPECT_EQ((TruncateCast(256)), 255); EXPECT_EQ((TruncateCast(-1)), 0); EXPECT_EQ((TruncateCast(65536)), 65535); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/math/matrix.h000066400000000000000000000057741454702036400174710ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { // Perform RQ decomposition on matrix. The RQ decomposition transforms a matrix // A into the product of an upper triangular matrix R (also known as // right-triangular) and an orthogonal matrix Q. template void DecomposeMatrixRQ(const MatrixType& A, MatrixType* R, MatrixType* Q); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template void DecomposeMatrixRQ(const MatrixType& A, MatrixType* R, MatrixType* Q) { const MatrixType A_flipud_transpose = A.transpose().rowwise().reverse().eval(); const Eigen::HouseholderQR QR(A_flipud_transpose); const MatrixType& Q0 = QR.householderQ(); const MatrixType& R0 = QR.matrixQR(); *R = R0.transpose().colwise().reverse().eval(); *R = R->rowwise().reverse().eval(); for (int i = 0; i < R->rows(); ++i) { for (int j = 0; j < R->cols() && (R->cols() - j) > (R->rows() - i); ++j) { (*R)(i, j) = 0; } } *Q = Q0.transpose().colwise().reverse().eval(); // Make the decomposition unique by requiring that det(Q) > 0. if (Q->determinant() < 0) { Q->row(1) *= -1.0; R->col(1) *= -1.0; } } } // namespace colmap colmap-3.9.1/src/colmap/math/matrix_test.cc000066400000000000000000000041211454702036400206470ustar00rootroot00000000000000// 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. #include "colmap/math/matrix.h" #include namespace colmap { namespace { TEST(DecomposeMatrixRQ, Nominal) { for (int i = 0; i < 10; ++i) { const Eigen::Matrix4d A = Eigen::Matrix4d::Random(); Eigen::Matrix4d R, Q; DecomposeMatrixRQ(A, &R, &Q); EXPECT_TRUE(R.bottomRows(4).isUpperTriangular()); EXPECT_TRUE(Q.isUnitary()); EXPECT_NEAR(Q.determinant(), 1.0, 1e-6); EXPECT_TRUE(A.isApprox(R * Q, 1e-6)); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/math/polynomial.cc000066400000000000000000000176551454702036400205070ustar00rootroot00000000000000// 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. #include "colmap/math/polynomial.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include namespace colmap { namespace { // Remove leading zero coefficients. Eigen::VectorXd RemoveLeadingZeros(const Eigen::VectorXd& coeffs) { Eigen::VectorXd::Index num_zeros = 0; for (; num_zeros < coeffs.size(); ++num_zeros) { if (coeffs(num_zeros) != 0) { break; } } return coeffs.tail(coeffs.size() - num_zeros); } // Remove trailing zero coefficients. Eigen::VectorXd RemoveTrailingZeros(const Eigen::VectorXd& coeffs) { Eigen::VectorXd::Index num_zeros = 0; for (; num_zeros < coeffs.size(); ++num_zeros) { if (coeffs(coeffs.size() - 1 - num_zeros) != 0) { break; } } return coeffs.head(coeffs.size() - num_zeros); } } // namespace bool FindLinearPolynomialRoots(const Eigen::VectorXd& coeffs, Eigen::VectorXd* real, Eigen::VectorXd* imag) { CHECK_EQ(coeffs.size(), 2); if (coeffs(0) == 0) { return false; } if (real != nullptr) { real->resize(1); (*real)(0) = -coeffs(1) / coeffs(0); } if (imag != nullptr) { imag->resize(1); (*imag)(0) = 0; } return true; } bool FindQuadraticPolynomialRoots(const Eigen::VectorXd& coeffs, Eigen::VectorXd* real, Eigen::VectorXd* imag) { CHECK_EQ(coeffs.size(), 3); const double a = coeffs(0); if (a == 0) { return FindLinearPolynomialRoots(coeffs.tail(2), real, imag); } const double b = coeffs(1); const double c = coeffs(2); if (b == 0 && c == 0) { if (real != nullptr) { real->resize(1); (*real)(0) = 0; } if (imag != nullptr) { imag->resize(1); (*imag)(0) = 0; } return true; } const double d = b * b - 4 * a * c; if (d >= 0) { const double sqrt_d = std::sqrt(d); if (real != nullptr) { real->resize(2); if (b >= 0) { (*real)(0) = (-b - sqrt_d) / (2 * a); (*real)(1) = (2 * c) / (-b - sqrt_d); } else { (*real)(0) = (2 * c) / (-b + sqrt_d); (*real)(1) = (-b + sqrt_d) / (2 * a); } } if (imag != nullptr) { imag->resize(2); imag->setZero(); } } else { if (real != nullptr) { real->resize(2); real->setConstant(-b / (2 * a)); } if (imag != nullptr) { imag->resize(2); (*imag)(0) = std::sqrt(-d) / (2 * a); (*imag)(1) = -(*imag)(0); } } return true; } bool FindPolynomialRootsDurandKerner(const Eigen::VectorXd& coeffs_all, Eigen::VectorXd* real, Eigen::VectorXd* imag) { CHECK_GE(coeffs_all.size(), 2); const Eigen::VectorXd coeffs = RemoveLeadingZeros(coeffs_all); const int degree = coeffs.size() - 1; if (degree <= 0) { return false; } else if (degree == 1) { return FindLinearPolynomialRoots(coeffs, real, imag); } else if (degree == 2) { return FindQuadraticPolynomialRoots(coeffs, real, imag); } // Initialize roots. Eigen::VectorXcd roots(degree); roots(degree - 1) = std::complex(1, 0); for (int i = degree - 2; i >= 0; --i) { roots(i) = roots(i + 1) * std::complex(1, 1); } // Iterative solver. const int kMaxNumIterations = 100; const double kMaxRootChange = 1e-10; for (int iter = 0; iter < kMaxNumIterations; ++iter) { double max_root_change = 0.0; for (int i = 0; i < degree; ++i) { const std::complex root_i = roots(i); std::complex numerator = coeffs[0]; std::complex denominator = coeffs[0]; for (int j = 0; j < degree; ++j) { numerator = numerator * root_i + coeffs[j + 1]; if (i != j) { denominator = denominator * (root_i - roots(j)); } } const std::complex root_i_change = numerator / denominator; roots(i) = root_i - root_i_change; max_root_change = std::max(max_root_change, std::abs(root_i_change.real())); max_root_change = std::max(max_root_change, std::abs(root_i_change.imag())); } // Break, if roots do not change anymore. if (max_root_change < kMaxRootChange) { break; } } if (real != nullptr) { real->resize(degree); *real = roots.real(); } if (imag != nullptr) { imag->resize(degree); *imag = roots.imag(); } return true; } bool FindPolynomialRootsCompanionMatrix(const Eigen::VectorXd& coeffs_all, Eigen::VectorXd* real, Eigen::VectorXd* imag) { CHECK_GE(coeffs_all.size(), 2); Eigen::VectorXd coeffs = RemoveLeadingZeros(coeffs_all); const int degree = coeffs.size() - 1; if (degree <= 0) { return false; } else if (degree == 1) { return FindLinearPolynomialRoots(coeffs, real, imag); } else if (degree == 2) { return FindQuadraticPolynomialRoots(coeffs, real, imag); } // Remove the coefficients where zero is a solution. coeffs = RemoveTrailingZeros(coeffs); // Check if only zero is a solution. if (coeffs.size() == 1) { if (real != nullptr) { real->resize(1); (*real)(0) = 0; } if (imag != nullptr) { imag->resize(1); (*imag)(0) = 0; } return true; } // Fill the companion matrix. Eigen::MatrixXd C(coeffs.size() - 1, coeffs.size() - 1); C.setZero(); for (Eigen::MatrixXd::Index i = 1; i < C.rows(); ++i) { C(i, i - 1) = 1; } C.row(0) = -coeffs.tail(coeffs.size() - 1) / coeffs(0); // Solve for the roots of the polynomial. Eigen::EigenSolver solver(C, false); if (solver.info() != Eigen::Success) { return false; } // If there are trailing zeros, we must add zero as a solution. const int effective_degree = coeffs.size() - 1 < degree ? coeffs.size() : coeffs.size() - 1; if (real != nullptr) { real->resize(effective_degree); real->head(coeffs.size() - 1) = solver.eigenvalues().real(); if (effective_degree > coeffs.size() - 1) { (*real)(real->size() - 1) = 0; } } if (imag != nullptr) { imag->resize(effective_degree); imag->head(coeffs.size() - 1) = solver.eigenvalues().imag(); if (effective_degree > coeffs.size() - 1) { (*imag)(imag->size() - 1) = 0; } } return true; } } // namespace colmap colmap-3.9.1/src/colmap/math/polynomial.h000066400000000000000000000106361454702036400203410ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include namespace colmap { // All polynomials are assumed to be the form: // // sum_{i=0}^N polynomial(i) x^{N-i}. // // and are given by a vector of coefficients of size N + 1. // // The implementation is based on COLMAP's old polynomial functionality and is // inspired by Ceres-Solver's/Theia's implementation to support complex // polynomials. The companion matrix implementation is based on NumPy. // Evaluate the polynomial for the given coefficients at x using the Horner // scheme. This function is templated such that the polynomial may be evaluated // at real and/or imaginary points. template T EvaluatePolynomial(const Eigen::VectorXd& coeffs, const T& x); // Find the root of polynomials of the form: a * x + b = 0. // The real and/or imaginary variable may be NULL if the output is not needed. bool FindLinearPolynomialRoots(const Eigen::VectorXd& coeffs, Eigen::VectorXd* real, Eigen::VectorXd* imag); // Find the roots of polynomials of the form: a * x^2 + b * x + c = 0. // The real and/or imaginary variable may be NULL if the output is not needed. bool FindQuadraticPolynomialRoots(const Eigen::VectorXd& coeffs, Eigen::VectorXd* real, Eigen::VectorXd* imag); // Find the roots of a polynomial using the Durand-Kerner method, based on: // // https://en.wikipedia.org/wiki/Durand%E2%80%93Kerner_method // // The Durand-Kerner is comparatively fast but often unstable/inaccurate. // The real and/or imaginary variable may be NULL if the output is not needed. bool FindPolynomialRootsDurandKerner(const Eigen::VectorXd& coeffs, Eigen::VectorXd* real, Eigen::VectorXd* imag); // Find the roots of a polynomial using the companion matrix method, based on: // // R. A. Horn & C. R. Johnson, Matrix Analysis. Cambridge, // UK: Cambridge University Press, 1999, pp. 146-7. // // Compared to Durand-Kerner, this method is slower but more stable/accurate. // The real and/or imaginary variable may be NULL if the output is not needed. bool FindPolynomialRootsCompanionMatrix(const Eigen::VectorXd& coeffs, Eigen::VectorXd* real, Eigen::VectorXd* imag); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template T EvaluatePolynomial(const Eigen::VectorXd& coeffs, const T& x) { T value = 0.0; for (Eigen::VectorXd::Index i = 0; i < coeffs.size(); ++i) { value = value * x + coeffs(i); } return value; } } // namespace colmap colmap-3.9.1/src/colmap/math/polynomial_test.cc000066400000000000000000000204031454702036400215270ustar00rootroot00000000000000// 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. #include "colmap/math/polynomial.h" #include namespace colmap { namespace { #define CHECK_EQUAL_RESULT(find_func1, coeffs1, find_func2, coeffs2) \ { \ Eigen::VectorXd real1; \ Eigen::VectorXd imag1; \ const bool success1 = find_func1(coeffs1, &real1, &imag1); \ Eigen::VectorXd real2; \ Eigen::VectorXd imag2; \ const bool success2 = find_func2(coeffs2, &real2, &imag2); \ EXPECT_EQ(success1, success2); \ if (success1) { \ EXPECT_EQ(real1, real2); \ EXPECT_EQ(imag1, imag2); \ } \ } TEST(EvaluatePolynomial, Nominal) { EXPECT_EQ(EvaluatePolynomial( (Eigen::VectorXd(5) << 1, -3, 3, -5, 10).finished(), 1), 1 - 3 + 3 - 5 + 10); EXPECT_NEAR( EvaluatePolynomial((Eigen::VectorXd(4) << 1, -3, 3, -5).finished(), 2.0), 1 * 2 * 2 * 2 - 3 * 2 * 2 + 3 * 2 - 5, 1e-6); } TEST(FindLinearPolynomialRoots, Nominal) { Eigen::VectorXd real; Eigen::VectorXd imag; EXPECT_TRUE(FindLinearPolynomialRoots(Eigen::Vector2d(3, -2), &real, &imag)); EXPECT_EQ(real(0), 2.0 / 3.0); EXPECT_EQ(imag(0), 0); EXPECT_NEAR(EvaluatePolynomial(Eigen::Vector2d(3, -2), std::complex(real(0), imag(0))) .real(), 0.0, 1e-6); EXPECT_NEAR(EvaluatePolynomial(Eigen::Vector2d(3, -2), std::complex(real(0), imag(0))) .imag(), 0.0, 1e-6); EXPECT_FALSE(FindLinearPolynomialRoots(Eigen::Vector2d(0, 1), &real, &imag)); } TEST(FindQuadraticPolynomialRootsReal, Nominal) { Eigen::VectorXd real; Eigen::VectorXd imag; Eigen::Vector3d coeffs(3, -2, -4); EXPECT_TRUE(FindQuadraticPolynomialRoots(coeffs, &real, &imag)); EXPECT_TRUE(real.isApprox(Eigen::Vector2d(-0.868517092, 1.535183758), 1e-6)); EXPECT_EQ(imag, Eigen::Vector2d(0, 0)); EXPECT_NEAR( EvaluatePolynomial(coeffs, std::complex(real(0), imag(0))).real(), 0.0, 1e-6); EXPECT_NEAR( EvaluatePolynomial(coeffs, std::complex(real(1), imag(1))).imag(), 0.0, 1e-6); } TEST(FindQuadraticPolynomialRootsComplex, Nominal) { Eigen::VectorXd real; Eigen::VectorXd imag; const Eigen::Vector3d coeffs( 0.276025076998578, 0.679702676853675, 0.655098003973841); EXPECT_TRUE(FindQuadraticPolynomialRoots(coeffs, &real, &imag)); EXPECT_TRUE(real.isApprox( Eigen::Vector2d(-1.231233560813707, -1.231233560813707), 1e-6)); EXPECT_TRUE(imag.isApprox( Eigen::Vector2d(0.925954520440279, -0.925954520440279), 1e-6)); EXPECT_NEAR( EvaluatePolynomial(coeffs, std::complex(real(0), imag(0))).real(), 0.0, 1e-6); EXPECT_NEAR( EvaluatePolynomial(coeffs, std::complex(real(1), imag(1))).imag(), 0.0, 1e-6); } TEST(FindPolynomialRootsDurandKerner, Nominal) { Eigen::VectorXd real; Eigen::VectorXd imag; Eigen::VectorXd coeffs(5); coeffs << 10, -5, 3, -3, 1; EXPECT_TRUE(FindPolynomialRootsDurandKerner(coeffs, &real, &imag)); // Reference values generated with OpenCV/Matlab. Eigen::VectorXd ref_real(4); ref_real << -0.201826, -0.201826, 0.451826, 0.451826; EXPECT_TRUE(real.isApprox(ref_real, 1e-6)); Eigen::VectorXd ref_imag(4); ref_imag << -0.627696, 0.627696, 0.160867, -0.160867; EXPECT_TRUE(imag.isApprox(ref_imag, 1e-6)); } TEST(FindPolynomialRootsDurandKernerLinearQuadratic, Nominal) { CHECK_EQUAL_RESULT(FindPolynomialRootsDurandKerner, Eigen::Vector2d(1, 2), FindLinearPolynomialRoots, Eigen::Vector2d(1, 2)); CHECK_EQUAL_RESULT(FindPolynomialRootsDurandKerner, (Eigen::VectorXd(4) << 0, 0, 1, 2).finished(), FindLinearPolynomialRoots, Eigen::Vector2d(1, 2)); CHECK_EQUAL_RESULT(FindPolynomialRootsDurandKerner, Eigen::Vector3d(1, 2, 3), FindQuadraticPolynomialRoots, Eigen::Vector3d(1, 2, 3)); CHECK_EQUAL_RESULT(FindPolynomialRootsDurandKerner, (Eigen::VectorXd(5) << 0, 0, 1, 2, 3).finished(), FindQuadraticPolynomialRoots, Eigen::Vector3d(1, 2, 3)); } TEST(FindPolynomialRootsCompanionMatrix, Nominal) { Eigen::VectorXd real; Eigen::VectorXd imag; Eigen::VectorXd coeffs(5); coeffs << 10, -5, 3, -3, 1; EXPECT_TRUE(FindPolynomialRootsCompanionMatrix(coeffs, &real, &imag)); // Reference values generated with OpenCV/Matlab. Eigen::VectorXd ref_real(4); ref_real << -0.201826, -0.201826, 0.451826, 0.451826; EXPECT_TRUE(real.isApprox(ref_real, 1e-6)); Eigen::VectorXd ref_imag(4); ref_imag << 0.627696, -0.627696, 0.160867, -0.160867; EXPECT_TRUE(imag.isApprox(ref_imag, 1e-6)); } TEST(FindPolynomialRootsCompanionMatrixLinearQuadratic, Nominal) { CHECK_EQUAL_RESULT(FindPolynomialRootsCompanionMatrix, Eigen::Vector2d(1, 2), FindLinearPolynomialRoots, Eigen::Vector2d(1, 2)); CHECK_EQUAL_RESULT(FindPolynomialRootsCompanionMatrix, (Eigen::VectorXd(4) << 0, 0, 1, 2).finished(), FindLinearPolynomialRoots, Eigen::Vector2d(1, 2)); CHECK_EQUAL_RESULT(FindPolynomialRootsCompanionMatrix, Eigen::Vector3d(1, 2, 3), FindQuadraticPolynomialRoots, Eigen::Vector3d(1, 2, 3)); CHECK_EQUAL_RESULT(FindPolynomialRootsCompanionMatrix, (Eigen::VectorXd(5) << 0, 0, 1, 2, 3).finished(), FindQuadraticPolynomialRoots, Eigen::Vector3d(1, 2, 3)); } TEST(FindPolynomialRootsCompanionMatrixZeroSolution, Nominal) { Eigen::VectorXd real; Eigen::VectorXd imag; Eigen::VectorXd coeffs(5); coeffs << 10, -5, 3, -3, 0; EXPECT_TRUE(FindPolynomialRootsCompanionMatrix(coeffs, &real, &imag)); // Reference values generated with Matlab. Eigen::VectorXd ref_real(4); ref_real << 0.692438, -0.0962191, -0.0962191, 0; EXPECT_TRUE(real.isApprox(ref_real, 1e-6)); Eigen::VectorXd ref_imag(4); ref_imag << 0, 0.651148, -0.651148, 0; EXPECT_TRUE(imag.isApprox(ref_imag, 1e-6)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/math/random.cc000066400000000000000000000037241454702036400175740ustar00rootroot00000000000000// 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. #include "colmap/math/random.h" #include namespace colmap { thread_local std::unique_ptr PRNG; int kDefaultPRNGSeed = 0; void SetPRNGSeed(unsigned seed) { PRNG = std::make_unique(seed); // srand is not thread-safe. static std::mutex mutex; std::unique_lock lock(mutex); srand(seed); } } // namespace colmap colmap-3.9.1/src/colmap/math/random.h000066400000000000000000000104221454702036400174270ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include #include #include #include namespace colmap { extern thread_local std::unique_ptr PRNG; extern int kDefaultPRNGSeed; // Initialize the PRNG with the given seed. // // @param seed The seed for the PRNG. If the seed is -1, the current time // is used as the seed. void SetPRNGSeed(unsigned seed = kDefaultPRNGSeed); // Generate uniformly distributed random integer number. // // This implementation is unbiased and thread-safe in contrast to `rand()`. template T RandomUniformInteger(T min, T max); // Generate uniformly distributed random real number. // // This implementation is unbiased and thread-safe in contrast to `rand()`. template T RandomUniformReal(T min, T max); // Generate Gaussian distributed random real number. // // This implementation is unbiased and thread-safe in contrast to `rand()`. template T RandomGaussian(T mean, T stddev); // Fisher-Yates shuffling. // // Note that the vector may not contain more values than UINT32_MAX. This // restriction comes from the fact that the 32-bit version of the // Mersenne Twister PRNG is significantly faster. // // @param elems Vector of elements to shuffle. // @param num_to_shuffle Optional parameter, specifying the number of first // N elements in the vector to shuffle. template void Shuffle(uint32_t num_to_shuffle, std::vector* elems); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template T RandomUniformInteger(const T min, const T max) { if (PRNG == nullptr) { SetPRNGSeed(); } std::uniform_int_distribution distribution(min, max); return distribution(*PRNG); } template T RandomUniformReal(const T min, const T max) { if (PRNG == nullptr) { SetPRNGSeed(); } std::uniform_real_distribution distribution(min, max); return distribution(*PRNG); } template T RandomGaussian(const T mean, const T stddev) { if (PRNG == nullptr) { SetPRNGSeed(); } std::normal_distribution distribution(mean, stddev); return distribution(*PRNG); } template void Shuffle(const uint32_t num_to_shuffle, std::vector* elems) { CHECK_LE(num_to_shuffle, elems->size()); const uint32_t last_idx = static_cast(elems->size() - 1); for (uint32_t i = 0; i < num_to_shuffle; ++i) { const auto j = RandomUniformInteger(i, last_idx); std::swap((*elems)[i], (*elems)[j]); } } } // namespace colmap colmap-3.9.1/src/colmap/math/random_test.cc000066400000000000000000000104071454702036400206270ustar00rootroot00000000000000// 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. #include "colmap/math/random.h" #include "colmap/math/math.h" #include #include #include namespace colmap { namespace { TEST(PRNGSeed, Nominal) { EXPECT_TRUE(PRNG == nullptr); SetPRNGSeed(); EXPECT_TRUE(PRNG != nullptr); SetPRNGSeed(0); EXPECT_TRUE(PRNG != nullptr); std::thread thread([]() { // Each thread defines their own PRNG instance. EXPECT_TRUE(PRNG == nullptr); SetPRNGSeed(); EXPECT_TRUE(PRNG != nullptr); SetPRNGSeed(0); EXPECT_TRUE(PRNG != nullptr); }); thread.join(); } TEST(Repeatability, Nominal) { SetPRNGSeed(0); std::vector numbers1; for (size_t i = 0; i < 100; ++i) { numbers1.push_back(RandomUniformInteger(0, 10000)); } SetPRNGSeed(1); std::vector numbers2; for (size_t i = 0; i < 100; ++i) { numbers2.push_back(RandomUniformInteger(0, 10000)); } SetPRNGSeed(0); std::vector numbers3; for (size_t i = 0; i < 100; ++i) { numbers3.push_back(RandomUniformInteger(0, 10000)); } EXPECT_EQ(numbers1, numbers3); bool all_equal = true; for (size_t i = 0; i < numbers1.size(); ++i) { if (numbers1[i] != numbers2[i]) { all_equal = false; } } EXPECT_FALSE(all_equal); } TEST(RandomUniformInteger, Nominal) { SetPRNGSeed(); for (size_t i = 0; i < 1000; ++i) { EXPECT_GE(RandomUniformInteger(-100, 100), -100); EXPECT_LE(RandomUniformInteger(-100, 100), 100); } } TEST(RandomUniformReal, Nominal) { SetPRNGSeed(); for (size_t i = 0; i < 1000; ++i) { EXPECT_GE(RandomUniformReal(-100.0, 100.0), -100.0); EXPECT_LE(RandomUniformReal(-100.0, 100.0), 100.0); } } TEST(RandomGaussian, Nominal) { SetPRNGSeed(0); const double kMean = 1.0; const double kSigma = 1.0; const size_t kNumValues = 100000; std::vector values; for (size_t i = 0; i < kNumValues; ++i) { values.push_back(RandomGaussian(kMean, kSigma)); } EXPECT_LE(std::abs(Mean(values) - kMean), 1e-2); EXPECT_LE(std::abs(StdDev(values) - kSigma), 1e-2); } TEST(ShuffleNone, Nominal) { SetPRNGSeed(); std::vector numbers(0); Shuffle(0, &numbers); numbers = {1, 2, 3, 4, 5}; std::vector shuffled_numbers = numbers; Shuffle(0, &shuffled_numbers); EXPECT_EQ(numbers, shuffled_numbers); } TEST(ShuffleAll, Nominal) { SetPRNGSeed(0); std::vector numbers(1000); std::iota(numbers.begin(), numbers.end(), 0); std::vector shuffled_numbers = numbers; Shuffle(1000, &shuffled_numbers); size_t num_shuffled = 0; for (size_t i = 0; i < numbers.size(); ++i) { if (numbers[i] != shuffled_numbers[i]) { num_shuffled += 1; } } EXPECT_GT(num_shuffled, 0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/mvs/000077500000000000000000000000001454702036400156535ustar00rootroot00000000000000colmap-3.9.1/src/colmap/mvs/CMakeLists.txt000066400000000000000000000060431454702036400204160ustar00rootroot00000000000000# 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. set(FOLDER_NAME "mvs") COLMAP_ADD_LIBRARY( NAME colmap_mvs SRCS consistency_graph.h consistency_graph.cc depth_map.h depth_map.cc fusion.h fusion.cc image.h image.cc meshing.h meshing.cc model.h model.cc normal_map.h normal_map.cc workspace.h workspace.cc PUBLIC_LINK_LIBS colmap_util colmap_scene PRIVATE_LINK_LIBS colmap_sensor colmap_image colmap_poisson_recon Eigen3::Eigen ) if(CGAL_ENABLED) target_link_libraries(colmap_mvs PRIVATE CGAL) endif() COLMAP_ADD_TEST( NAME consistency_graph_test SRCS consistency_graph_test.cc LINK_LIBS colmap_mvs ) COLMAP_ADD_TEST( NAME depth_map_test SRCS depth_map_test.cc LINK_LIBS colmap_mvs ) COLMAP_ADD_TEST( NAME mat_test SRCS mat_test.cc LINK_LIBS colmap_mvs ) COLMAP_ADD_TEST( NAME normal_map_test SRCS normal_map_test.cc LINK_LIBS colmap_mvs ) if(CUDA_ENABLED) COLMAP_ADD_LIBRARY( NAME colmap_mvs_cuda SRCS gpu_mat_prng.h gpu_mat_prng.cu gpu_mat_ref_image.h gpu_mat_ref_image.cu patch_match.h patch_match.cc patch_match_cuda.h patch_match_cuda.cu PUBLIC_LINK_LIBS colmap_mvs colmap_util_cuda CUDA::cudart CUDA::curand ) COLMAP_ADD_TEST( NAME gpu_mat_test SRCS gpu_mat_test.cu LINK_LIBS colmap_mvs_cuda ) endif() colmap-3.9.1/src/colmap/mvs/consistency_graph.cc000066400000000000000000000105511454702036400217060ustar00rootroot00000000000000// 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. #include "colmap/mvs/consistency_graph.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include #include namespace colmap { namespace mvs { const int ConsistencyGraph::kNoConsistentImageIds = -1; ConsistencyGraph::ConsistencyGraph() {} ConsistencyGraph::ConsistencyGraph(const size_t width, const size_t height, const std::vector& data) : data_(data) { InitializeMap(width, height); } size_t ConsistencyGraph::GetNumBytes() const { return (data_.size() + map_.size()) * sizeof(int); } void ConsistencyGraph::GetImageIdxs(const int row, const int col, int* num_images, const int** image_idxs) const { const int index = map_(row, col); if (index == kNoConsistentImageIds) { *num_images = 0; *image_idxs = nullptr; } else { *num_images = data_.at(index); *image_idxs = &data_.at(index + 1); } } void ConsistencyGraph::Read(const std::string& path) { std::fstream text_file(path, std::ios::in | std::ios::binary); CHECK(text_file.is_open()) << path; size_t width = 0; size_t height = 0; size_t depth = 0; char unused_char; text_file >> width >> unused_char >> height >> unused_char >> depth >> unused_char; const std::streampos pos = text_file.tellg(); text_file.close(); CHECK_GT(width, 0); CHECK_GT(height, 0); CHECK_GT(depth, 0); std::fstream binary_file(path, std::ios::in | std::ios::binary); CHECK(binary_file.is_open()) << path; binary_file.seekg(0, std::ios::end); const size_t num_bytes = binary_file.tellg() - pos; data_.resize(num_bytes / sizeof(int)); binary_file.seekg(pos); ReadBinaryLittleEndian(&binary_file, &data_); binary_file.close(); InitializeMap(width, height); } void ConsistencyGraph::Write(const std::string& path) const { std::fstream text_file(path, std::ios::out); CHECK(text_file.is_open()) << path; text_file << map_.cols() << "&" << map_.rows() << "&" << 1 << "&"; text_file.close(); std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); CHECK(binary_file.is_open()) << path; WriteBinaryLittleEndian(&binary_file, data_); binary_file.close(); } void ConsistencyGraph::InitializeMap(const size_t width, const size_t height) { map_.resize(height, width); map_.setConstant(kNoConsistentImageIds); for (size_t i = 0; i < data_.size();) { const int num_images = data_.at(i + 2); if (num_images > 0) { const int col = data_.at(i); const int row = data_.at(i + 1); map_(row, col) = i + 2; } i += 3 + num_images; } } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/consistency_graph.h000066400000000000000000000054551454702036400215570ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include namespace colmap { namespace mvs { // List of geometrically consistent images, in the following format: // // r_1, c_1, N_1, i_11, i_12, ..., i_1N_1, // r_2, c_2, N_2, i_21, i_22, ..., i_2N_2, ... // // where r, c are the row and column image coordinates of the pixel, // N is the number of consistent images, followed by the N image indices. // Note that only pixels are listed which are not filtered and that the // consistency graph is only filled if filtering is enabled. class ConsistencyGraph { public: ConsistencyGraph(); ConsistencyGraph(size_t width, size_t height, const std::vector& data); size_t GetNumBytes() const; void GetImageIdxs(int row, int col, int* num_images, const int** image_idxs) const; void Read(const std::string& path); void Write(const std::string& path) const; private: void InitializeMap(size_t width, size_t height); const static int kNoConsistentImageIds; std::vector data_; Eigen::MatrixXi map_; }; } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/consistency_graph_test.cc000066400000000000000000000074071454702036400227530ustar00rootroot00000000000000// 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. #include "colmap/mvs/consistency_graph.h" #include namespace colmap { namespace mvs { namespace { TEST(ConsistencyGraph, Empty) { const std::vector data; ConsistencyGraph consistency_graph(2, 2, data); for (size_t i = 0; i < 2; ++i) { for (size_t j = 0; j < 2; ++j) { int num_images; const int* image_idxs; consistency_graph.GetImageIdxs(0, 0, &num_images, &image_idxs); EXPECT_EQ(num_images, 0); EXPECT_TRUE(image_idxs == nullptr); } } EXPECT_EQ(consistency_graph.GetNumBytes(), 16); } TEST(ConsistencyGraph, Partial) { const std::vector data = {0, 0, 3, 5, 7, 33}; ConsistencyGraph consistency_graph(2, 1, data); int num_images; const int* image_idxs; consistency_graph.GetImageIdxs(0, 0, &num_images, &image_idxs); EXPECT_EQ(num_images, 3); EXPECT_EQ(image_idxs[0], 5); EXPECT_EQ(image_idxs[1], 7); EXPECT_EQ(image_idxs[2], 33); consistency_graph.GetImageIdxs(0, 1, &num_images, &image_idxs); EXPECT_EQ(num_images, 0); EXPECT_TRUE(image_idxs == nullptr); EXPECT_EQ(consistency_graph.GetNumBytes(), 32); } TEST(ConsistencyGraph, Zero) { const std::vector data = {0, 0, 0}; ConsistencyGraph consistency_graph(2, 1, data); int num_images; const int* image_idxs; consistency_graph.GetImageIdxs(0, 0, &num_images, &image_idxs); EXPECT_EQ(num_images, 0); EXPECT_TRUE(image_idxs == nullptr); consistency_graph.GetImageIdxs(0, 1, &num_images, &image_idxs); EXPECT_EQ(num_images, 0); EXPECT_TRUE(image_idxs == nullptr); EXPECT_EQ(consistency_graph.GetNumBytes(), 20); } TEST(ConsistencyGraph, Full) { const std::vector data = {0, 0, 3, 5, 7, 33, 0, 1, 1, 100}; ConsistencyGraph consistency_graph(1, 2, data); int num_images; const int* image_idxs; consistency_graph.GetImageIdxs(0, 0, &num_images, &image_idxs); EXPECT_EQ(num_images, 3); EXPECT_EQ(image_idxs[0], 5); EXPECT_EQ(image_idxs[1], 7); EXPECT_EQ(image_idxs[2], 33); consistency_graph.GetImageIdxs(1, 0, &num_images, &image_idxs); EXPECT_EQ(num_images, 1); EXPECT_EQ(image_idxs[0], 100); EXPECT_EQ(consistency_graph.GetNumBytes(), 48); } } // namespace } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/cuda_flip.h000066400000000000000000000105331454702036400177540ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { namespace mvs { // Flip the input matrix horizontally. template void CudaFlipHorizontal(const T* input, T* output, const int width, const int height, const int pitch_input, const int pitch_output); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// #ifdef __CUDACC__ // TILE_DIM_FLIP must divide by BLOCK_ROWS. Do not change these values. #define TILE_DIM_FLIP 32 #define BLOCK_ROWS_FLIP 8 namespace internal { template __global__ void CudaFlipHorizontalKernel(T* output_data, const T* input_data, const int width, const int height, const int input_pitch, const int output_pitch) { int x_index = blockIdx.x * TILE_DIM_FLIP + threadIdx.x; const int y_index = blockIdx.y * TILE_DIM_FLIP + threadIdx.y; __shared__ T tile[TILE_DIM_FLIP][TILE_DIM_FLIP + 1]; const int tile_x = min(threadIdx.x, width - 1 - blockIdx.x * TILE_DIM_FLIP); const int tile_y = min(threadIdx.y, height - 1 - blockIdx.y * TILE_DIM_FLIP); for (int i = 0; i < TILE_DIM_FLIP; i += BLOCK_ROWS_FLIP) { const int x = min(x_index, width - 1); const int y = min(y_index, height - i - 1); tile[tile_y + i][tile_x] = *((T*)((char*)input_data + y * input_pitch + i * input_pitch) + x); } __syncthreads(); x_index = width - 1 - (blockIdx.x * TILE_DIM_FLIP + threadIdx.x); if (x_index < width) { for (int i = 0; i < TILE_DIM_FLIP; i += BLOCK_ROWS_FLIP) { if (y_index + i < height) { *((T*)((char*)output_data + y_index * output_pitch + i * output_pitch) + x_index) = tile[threadIdx.y + i][threadIdx.x]; } } } } } // namespace internal template void CudaFlipHorizontal(const T* input, T* output, const int width, const int height, const int pitch_input, const int pitch_output) { dim3 block_dim(TILE_DIM_FLIP, BLOCK_ROWS_FLIP, 1); dim3 grid_dim; grid_dim.x = (width - 1) / TILE_DIM_FLIP + 1; grid_dim.y = (height - 1) / TILE_DIM_FLIP + 1; internal::CudaFlipHorizontalKernel<<>>( output, input, width, height, pitch_input, pitch_output); } #undef TILE_DIM_FLIP #undef BLOCK_ROWS_FLIP #endif // __CUDACC__ } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/cuda_rotate.h000066400000000000000000000067751454702036400203350ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { namespace mvs { // Rotate the input matrix by 90 degrees in counter-clockwise direction. template void CudaRotate(const T* input, T* output, const int width, const int height, const int pitch_input, const int pitch_output); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// #ifdef __CUDACC__ #define TILE_DIM_ROTATE 32 namespace internal { template __global__ void CudaRotateKernel(T* output_data, const T* input_data, const int width, const int height, const int input_pitch, const int output_pitch) { int input_x = blockDim.x * blockIdx.x + threadIdx.x; int input_y = blockDim.y * blockIdx.y + threadIdx.y; if (input_x >= width || input_y >= height) { return; } int output_x = input_y; int output_y = width - 1 - input_x; *((T*)((char*)output_data + output_y * output_pitch) + output_x) = *((T*)((char*)input_data + input_y * input_pitch) + input_x); } } // namespace internal template void CudaRotate(const T* input, T* output, const int width, const int height, const int pitch_input, const int pitch_output) { dim3 block_dim(TILE_DIM_ROTATE, 1, 1); dim3 grid_dim; grid_dim.x = (width - 1) / TILE_DIM_ROTATE + 1; grid_dim.y = height; internal::CudaRotateKernel<<>>( output, input, width, height, pitch_input, pitch_output); } #undef TILE_DIM_ROTATE #endif // __CUDACC__ } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/cuda_texture.h000066400000000000000000000141621454702036400205240ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/gpu_mat.h" #include "colmap/util/cudacc.h" #include "colmap/util/logging.h" #include #include namespace colmap { namespace mvs { template class CudaArrayLayeredTexture { public: static std::unique_ptr> FromGpuMat( const cudaTextureDesc& texture_desc, const GpuMat& mat); static std::unique_ptr> FromHostArray( const cudaTextureDesc& texture_desc, const size_t width, const size_t height, const size_t depth, const T* data); cudaTextureObject_t GetObj() const; size_t GetWidth() const; size_t GetHeight() const; size_t GetDepth() const; CudaArrayLayeredTexture(const cudaTextureDesc& texture_desc, const size_t width, const size_t height, const size_t depth); ~CudaArrayLayeredTexture(); private: // Define class as non-copyable and non-movable. CudaArrayLayeredTexture(CudaArrayLayeredTexture const&) = delete; void operator=(CudaArrayLayeredTexture const& obj) = delete; CudaArrayLayeredTexture(CudaArrayLayeredTexture&&) = delete; const size_t width_; const size_t height_; const size_t depth_; cudaArray_t array_; const cudaTextureDesc texture_desc_; cudaResourceDesc resource_desc_; cudaTextureObject_t texture_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template std::unique_ptr> CudaArrayLayeredTexture::FromGpuMat(const cudaTextureDesc& texture_desc, const GpuMat& mat) { auto array = std::make_unique>( texture_desc, mat.GetWidth(), mat.GetHeight(), mat.GetDepth()); cudaMemcpy3DParms params; memset(¶ms, 0, sizeof(params)); params.extent = make_cudaExtent(mat.GetWidth(), mat.GetHeight(), mat.GetDepth()); params.kind = cudaMemcpyDeviceToDevice; params.srcPtr = make_cudaPitchedPtr( (void*)mat.GetPtr(), mat.GetPitch(), mat.GetWidth(), mat.GetHeight()); params.dstArray = array->array_; CUDA_SAFE_CALL(cudaMemcpy3D(¶ms)); return array; } template std::unique_ptr> CudaArrayLayeredTexture::FromHostArray(const cudaTextureDesc& texture_desc, const size_t width, const size_t height, const size_t depth, const T* data) { auto array = std::make_unique>( texture_desc, width, height, depth); cudaMemcpy3DParms params; memset(¶ms, 0, sizeof(params)); params.extent = make_cudaExtent(width, height, depth); params.kind = cudaMemcpyHostToDevice; params.srcPtr = make_cudaPitchedPtr((void*)data, width * sizeof(T), width, height); params.dstArray = array->array_; CUDA_SAFE_CALL(cudaMemcpy3D(¶ms)); return array; } template CudaArrayLayeredTexture::CudaArrayLayeredTexture( const cudaTextureDesc& texture_desc, const size_t width, const size_t height, const size_t depth) : texture_desc_(texture_desc), width_(width), height_(height), depth_(depth) { CHECK_GT(width_, 0); CHECK_GT(height_, 0); CHECK_GT(depth_, 0); cudaExtent extent = make_cudaExtent(width_, height_, depth_); cudaChannelFormatDesc fmt = cudaCreateChannelDesc(); CUDA_SAFE_CALL(cudaMalloc3DArray(&array_, &fmt, extent, cudaArrayLayered)); memset(&resource_desc_, 0, sizeof(resource_desc_)); resource_desc_.resType = cudaResourceTypeArray; resource_desc_.res.array.array = array_; CUDA_SAFE_CALL(cudaCreateTextureObject( &texture_, &resource_desc_, &texture_desc_, nullptr)); } template CudaArrayLayeredTexture::~CudaArrayLayeredTexture() { CUDA_SAFE_CALL(cudaFreeArray(array_)); CUDA_SAFE_CALL(cudaDestroyTextureObject(texture_)); } template cudaTextureObject_t CudaArrayLayeredTexture::GetObj() const { return texture_; } template size_t CudaArrayLayeredTexture::GetWidth() const { return width_; } template size_t CudaArrayLayeredTexture::GetHeight() const { return height_; } template size_t CudaArrayLayeredTexture::GetDepth() const { return depth_; } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/cuda_transpose.h000066400000000000000000000106151454702036400210410ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { namespace mvs { // Transpose the input matrix. template void CudaTranspose(const T* input, T* output, const int width, const int height, const int pitch_input, const int pitch_output); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// #ifdef __CUDACC__ // TILE_DIM_TRANSPOSE must divide by BLOCK_ROWS. Do not change these values. #define TILE_DIM_TRANSPOSE 32 #define BLOCK_ROWS_TRANSPOSE 8 namespace internal { template __global__ void CudaTransposeKernel(T* output_data, const T* input_data, const int width, const int height, const int input_pitch, const int output_pitch) { int x_index = blockIdx.x * TILE_DIM_TRANSPOSE + threadIdx.x; int y_index = blockIdx.y * TILE_DIM_TRANSPOSE + threadIdx.y; __shared__ T tile[TILE_DIM_TRANSPOSE][TILE_DIM_TRANSPOSE + 1]; const int tile_x = min(threadIdx.x, width - 1 - blockIdx.x * TILE_DIM_TRANSPOSE); const int tile_y = min(threadIdx.y, height - 1 - blockIdx.y * TILE_DIM_TRANSPOSE); for (int i = 0; i < TILE_DIM_TRANSPOSE; i += BLOCK_ROWS_TRANSPOSE) { const int x = min(x_index, width - 1); const int y = min(y_index, height - i - 1); tile[tile_y + i][tile_x] = *((T*)((char*)input_data + y * input_pitch + i * input_pitch) + x); } __syncthreads(); x_index = blockIdx.y * TILE_DIM_TRANSPOSE + threadIdx.x; if (x_index < height) { y_index = blockIdx.x * TILE_DIM_TRANSPOSE + threadIdx.y; for (int i = 0; i < TILE_DIM_TRANSPOSE; i += BLOCK_ROWS_TRANSPOSE) { if (y_index + i < width) { *((T*)((char*)output_data + y_index * output_pitch + i * output_pitch) + x_index) = tile[threadIdx.x][threadIdx.y + i]; } } } } } // namespace internal template void CudaTranspose(const T* input, T* output, const int width, const int height, const int pitch_input, const int pitch_output) { dim3 block_dim(TILE_DIM_TRANSPOSE, BLOCK_ROWS_TRANSPOSE, 1); dim3 grid_dim; grid_dim.x = (width - 1) / TILE_DIM_TRANSPOSE + 1; grid_dim.y = (height - 1) / TILE_DIM_TRANSPOSE + 1; internal::CudaTransposeKernel<<>>( output, input, width, height, pitch_input, pitch_output); } #undef TILE_DIM_TRANSPOSE #undef BLOCK_ROWS_TRANSPOSE #endif // __CUDACC__ } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/depth_map.cc000066400000000000000000000110571454702036400201270ustar00rootroot00000000000000// 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. #include "colmap/mvs/depth_map.h" #include "colmap/image/warp.h" #include "colmap/math/math.h" namespace colmap { namespace mvs { DepthMap::DepthMap() : DepthMap(0, 0, -1.0f, -1.0f) {} DepthMap::DepthMap(const size_t width, const size_t height, const float depth_min, const float depth_max) : Mat(width, height, 1), depth_min_(depth_min), depth_max_(depth_max) {} DepthMap::DepthMap(const Mat& mat, const float depth_min, const float depth_max) : Mat(mat.GetWidth(), mat.GetHeight(), mat.GetDepth()), depth_min_(depth_min), depth_max_(depth_max) { CHECK_EQ(mat.GetDepth(), 1); data_ = mat.GetData(); } void DepthMap::Rescale(const float factor) { if (width_ * height_ == 0) { return; } const size_t new_width = std::round(width_ * factor); const size_t new_height = std::round(height_ * factor); std::vector new_data(new_width * new_height); DownsampleImage( data_.data(), height_, width_, new_height, new_width, new_data.data()); data_ = new_data; width_ = new_width; height_ = new_height; data_.shrink_to_fit(); } void DepthMap::Downsize(const size_t max_width, const size_t max_height) { if (height_ <= max_height && width_ <= max_width) { return; } const float factor_x = static_cast(max_width) / width_; const float factor_y = static_cast(max_height) / height_; Rescale(std::min(factor_x, factor_y)); } Bitmap DepthMap::ToBitmap(const float min_percentile, const float max_percentile) const { CHECK_GT(width_, 0); CHECK_GT(height_, 0); Bitmap bitmap; bitmap.Allocate(width_, height_, true); std::vector valid_depths; valid_depths.reserve(data_.size()); for (const float depth : data_) { if (depth > 0) { valid_depths.push_back(depth); } } if (valid_depths.empty()) { bitmap.Fill(BitmapColor(0)); return bitmap; } const float robust_depth_min = Percentile(valid_depths, min_percentile); const float robust_depth_max = Percentile(valid_depths, max_percentile); const float robust_depth_range = robust_depth_max - robust_depth_min; for (size_t y = 0; y < height_; ++y) { for (size_t x = 0; x < width_; ++x) { const float depth = Get(y, x); if (depth > 0) { const float robust_depth = std::max(robust_depth_min, std::min(robust_depth_max, depth)); const float gray = (robust_depth - robust_depth_min) / robust_depth_range; const BitmapColor color(255 * JetColormap::Red(gray), 255 * JetColormap::Green(gray), 255 * JetColormap::Blue(gray)); bitmap.SetPixel(x, y, color.Cast()); } else { bitmap.SetPixel(x, y, BitmapColor(0)); } } } return bitmap; } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/depth_map.h000066400000000000000000000053421454702036400177710ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/mat.h" #include "colmap/sensor/bitmap.h" #include #include namespace colmap { namespace mvs { class DepthMap : public Mat { public: DepthMap(); DepthMap(size_t width, size_t height, float depth_min, float depth_max); DepthMap(const Mat& mat, float depth_min, float depth_max); inline float GetDepthMin() const; inline float GetDepthMax() const; inline float Get(size_t row, size_t col) const; void Rescale(float factor); void Downsize(size_t max_width, size_t max_height); Bitmap ToBitmap(float min_percentile, float max_percentile) const; private: float depth_min_ = -1.0f; float depth_max_ = -1.0f; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// float DepthMap::GetDepthMin() const { return depth_min_; } float DepthMap::GetDepthMax() const { return depth_max_; } float DepthMap::Get(const size_t row, const size_t col) const { return data_.at(row * width_ + col); } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/depth_map_test.cc000066400000000000000000000071061454702036400211660ustar00rootroot00000000000000// 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. #include "colmap/mvs/depth_map.h" #include namespace colmap { namespace mvs { namespace { TEST(DepthMap, Empty) { DepthMap depth_map; EXPECT_EQ(depth_map.GetWidth(), 0); EXPECT_EQ(depth_map.GetHeight(), 0); EXPECT_EQ(depth_map.GetDepth(), 1); EXPECT_EQ(depth_map.GetDepthMin(), -1); EXPECT_EQ(depth_map.GetDepthMax(), -1); } TEST(DepthMap, NonEmpty) { DepthMap depth_map(1, 2, 0, 1); EXPECT_EQ(depth_map.GetWidth(), 1); EXPECT_EQ(depth_map.GetHeight(), 2); EXPECT_EQ(depth_map.GetDepth(), 1); EXPECT_EQ(depth_map.GetDepthMin(), 0); EXPECT_EQ(depth_map.GetDepthMax(), 1); } TEST(DepthMap, Rescale) { DepthMap depth_map(6, 7, 0, 1); depth_map.Rescale(0.5); EXPECT_EQ(depth_map.GetWidth(), 3); EXPECT_EQ(depth_map.GetHeight(), 4); EXPECT_EQ(depth_map.GetDepth(), 1); EXPECT_EQ(depth_map.GetDepthMin(), 0); EXPECT_EQ(depth_map.GetDepthMax(), 1); } TEST(DepthMap, Downsize) { DepthMap depth_map(6, 7, 0, 1); depth_map.Downsize(2, 4); EXPECT_EQ(depth_map.GetWidth(), 2); EXPECT_EQ(depth_map.GetHeight(), 2); EXPECT_EQ(depth_map.GetDepth(), 1); EXPECT_EQ(depth_map.GetDepthMin(), 0); EXPECT_EQ(depth_map.GetDepthMax(), 1); } TEST(DepthMap, ToBitmap) { DepthMap depth_map(2, 2, 0.1, 0.9); depth_map.Fill(0.9); depth_map.Set(0, 0, 0, 0.1); depth_map.Set(0, 1, 0, 0.5); const Bitmap bitmap = depth_map.ToBitmap(0, 100); EXPECT_EQ(bitmap.Width(), depth_map.GetWidth()); EXPECT_EQ(bitmap.Height(), depth_map.GetHeight()); EXPECT_TRUE(bitmap.IsRGB()); BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(0, 0, &color)); EXPECT_EQ(color, BitmapColor(0, 0, 128)); EXPECT_TRUE(bitmap.GetPixel(0, 1, &color)); EXPECT_EQ(color, BitmapColor(128, 0, 0)); EXPECT_TRUE(bitmap.GetPixel(1, 0, &color)); EXPECT_EQ(color, BitmapColor(128, 255, 127)); EXPECT_TRUE(bitmap.GetPixel(1, 1, &color)); EXPECT_EQ(color, BitmapColor(128, 0, 0)); } } // namespace } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/fusion.cc000066400000000000000000000511441454702036400174720ustar00rootroot00000000000000// 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. #include "colmap/mvs/fusion.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/misc.h" #include namespace colmap { namespace mvs { namespace internal { template float Median(std::vector* elems) { CHECK(!elems->empty()); const size_t mid_idx = elems->size() / 2; std::nth_element(elems->begin(), elems->begin() + mid_idx, elems->end()); if (elems->size() % 2 == 0) { const float mid_element1 = static_cast((*elems)[mid_idx]); const float mid_element2 = static_cast( *std::max_element(elems->begin(), elems->begin() + mid_idx)); return (mid_element1 + mid_element2) / 2.0f; } else { return static_cast((*elems)[mid_idx]); } } // Use the sparse model to find most connected image that has not yet been // fused. This is used as a heuristic to ensure that the workspace cache reuses // already cached images as efficient as possible. int FindNextImage(const std::vector>& overlapping_images, const std::vector& used_images, const std::vector& fused_images, const int prev_image_idx) { CHECK_EQ(used_images.size(), fused_images.size()); for (const auto image_idx : overlapping_images.at(prev_image_idx)) { if (used_images.at(image_idx) && !fused_images.at(image_idx)) { return image_idx; } } // If none of the overlapping images are not yet fused, simply return the // first image that has not yet been fused. for (size_t image_idx = 0; image_idx < fused_images.size(); ++image_idx) { if (used_images[image_idx] && !fused_images[image_idx]) { return image_idx; } } return -1; } } // namespace internal void StereoFusionOptions::Print() const { #define PrintOption(option) LOG(INFO) << #option ": " << (option) << std::endl PrintHeading2("StereoFusion::Options"); PrintOption(mask_path); PrintOption(max_image_size); PrintOption(min_num_pixels); PrintOption(max_num_pixels); PrintOption(max_traversal_depth); PrintOption(max_reproj_error); PrintOption(max_depth_error); PrintOption(max_normal_error); PrintOption(check_num_images); PrintOption(use_cache); PrintOption(cache_size); const auto& bbox_min = bounding_box.first.transpose().eval(); const auto& bbox_max = bounding_box.second.transpose().eval(); PrintOption(bbox_min); PrintOption(bbox_max); #undef PrintOption } bool StereoFusionOptions::Check() const { CHECK_OPTION_GE(min_num_pixels, 0); CHECK_OPTION_LE(min_num_pixels, max_num_pixels); CHECK_OPTION_GT(max_traversal_depth, 0); CHECK_OPTION_GE(max_reproj_error, 0); CHECK_OPTION_GE(max_depth_error, 0); CHECK_OPTION_GE(max_normal_error, 0); CHECK_OPTION_GT(check_num_images, 0); CHECK_OPTION_GT(cache_size, 0); return true; } StereoFusion::StereoFusion(const StereoFusionOptions& options, const std::string& workspace_path, const std::string& workspace_format, const std::string& pmvs_option_name, const std::string& input_type) : options_(options), workspace_path_(workspace_path), workspace_format_(workspace_format), pmvs_option_name_(pmvs_option_name), input_type_(input_type), max_squared_reproj_error_(options_.max_reproj_error * options_.max_reproj_error), min_cos_normal_error_(std::cos(DegToRad(options_.max_normal_error))) { CHECK(options_.Check()); } const std::vector& StereoFusion::GetFusedPoints() const { return fused_points_; } const std::vector>& StereoFusion::GetFusedPointsVisibility() const { return fused_points_visibility_; } void StereoFusion::Run() { fused_points_.clear(); fused_points_visibility_.clear(); options_.Print(); LOG(INFO) << "Reading workspace..."; Workspace::Options workspace_options; auto workspace_format_lower_case = workspace_format_; StringToLower(&workspace_format_lower_case); if (workspace_format_lower_case == "pmvs") { workspace_options.stereo_folder = StringPrintf("stereo-%s", pmvs_option_name_.c_str()); } workspace_options.num_threads = options_.num_threads; workspace_options.max_image_size = options_.max_image_size; workspace_options.image_as_rgb = true; workspace_options.cache_size = options_.cache_size; workspace_options.workspace_path = workspace_path_; workspace_options.workspace_format = workspace_format_; workspace_options.input_type = input_type_; const auto image_names = ReadTextFileLines(JoinPaths( workspace_path_, workspace_options.stereo_folder, "fusion.cfg")); int num_threads = 1; if (options_.use_cache) { workspace_ = std::make_unique(workspace_options); } else { workspace_ = std::make_unique(workspace_options); workspace_->Load(image_names); num_threads = GetEffectiveNumThreads(options_.num_threads); } if (IsStopped()) { GetTimer().PrintMinutes(); return; } LOG(INFO) << "Reading configuration..."; const auto& model = workspace_->GetModel(); const double kMinTriangulationAngle = 0; if (model.GetMaxOverlappingImagesFromPMVS().empty()) { overlapping_images_ = model.GetMaxOverlappingImages( options_.check_num_images, kMinTriangulationAngle); } else { overlapping_images_ = model.GetMaxOverlappingImagesFromPMVS(); } task_fused_points_.resize(num_threads); task_fused_points_visibility_.resize(num_threads); used_images_.resize(model.images.size(), false); fused_images_.resize(model.images.size(), false); fused_pixel_masks_.resize(model.images.size()); depth_map_sizes_.resize(model.images.size()); bitmap_scales_.resize(model.images.size()); P_.resize(model.images.size()); inv_P_.resize(model.images.size()); inv_R_.resize(model.images.size()); for (const auto& image_name : image_names) { const int image_idx = model.GetImageIdx(image_name); if (!workspace_->HasBitmap(image_idx) || !workspace_->HasDepthMap(image_idx) || !workspace_->HasNormalMap(image_idx)) { LOG(WARNING) << StringPrintf( "Ignoring image %s, because input does not exist.", image_name.c_str()); continue; } const auto& image = model.images.at(image_idx); const auto& depth_map = workspace_->GetDepthMap(image_idx); used_images_.at(image_idx) = true; InitFusedPixelMask(image_idx, depth_map.GetWidth(), depth_map.GetHeight()); depth_map_sizes_.at(image_idx) = std::make_pair(depth_map.GetWidth(), depth_map.GetHeight()); bitmap_scales_.at(image_idx) = std::make_pair( static_cast(depth_map.GetWidth()) / image.GetWidth(), static_cast(depth_map.GetHeight()) / image.GetHeight()); Eigen::Matrix K = Eigen::Map>( image.GetK()); K(0, 0) *= bitmap_scales_.at(image_idx).first; K(0, 2) *= bitmap_scales_.at(image_idx).first; K(1, 1) *= bitmap_scales_.at(image_idx).second; K(1, 2) *= bitmap_scales_.at(image_idx).second; ComposeProjectionMatrix( K.data(), image.GetR(), image.GetT(), P_.at(image_idx).data()); ComposeInverseProjectionMatrix( K.data(), image.GetR(), image.GetT(), inv_P_.at(image_idx).data()); inv_R_.at(image_idx) = Eigen::Map>( image.GetR()) .transpose(); } LOG(INFO) << StringPrintf("Starting fusion with %d threads", num_threads); ThreadPool thread_pool(num_threads); // Using a row stride of 10 to avoid starting parallel processing in rows that // are too close to each other which may lead to duplicated work, since nearby // pixels are likely to get fused into the same point. const int kRowStride = 10; auto ProcessImageRows = [&, this](const int row_start, const int height, const int width, const int image_idx, const Mat& fused_pixel_mask) { const int row_end = std::min(height, row_start + kRowStride); for (int row = row_start; row < row_end; ++row) { for (int col = 0; col < width; ++col) { if (fused_pixel_mask.Get(row, col) > 0) { continue; } const int thread_id = thread_pool.GetThreadIndex(); Fuse(thread_id, image_idx, row, col); } } }; size_t num_fused_images = 0; size_t total_fused_points = 0; for (int image_idx = 0; image_idx >= 0; image_idx = internal::FindNextImage( overlapping_images_, used_images_, fused_images_, image_idx)) { if (IsStopped()) { break; } Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Fusing image [%d/%d] with index %d", num_fused_images + 1, model.images.size(), image_idx) << std::flush; const int width = depth_map_sizes_.at(image_idx).first; const int height = depth_map_sizes_.at(image_idx).second; const auto& fused_pixel_mask = fused_pixel_masks_.at(image_idx); for (int row_start = 0; row_start < height; row_start += kRowStride) { thread_pool.AddTask(ProcessImageRows, row_start, height, width, image_idx, fused_pixel_mask); } thread_pool.Wait(); num_fused_images += 1; fused_images_.at(image_idx) = true; total_fused_points = 0; for (const auto& task_fused_points : task_fused_points_) { total_fused_points += task_fused_points.size(); } LOG(INFO) << StringPrintf( " in %.3fs (%d points)", timer.ElapsedSeconds(), total_fused_points); } fused_points_.reserve(total_fused_points); fused_points_visibility_.reserve(total_fused_points); for (size_t thread_id = 0; thread_id < task_fused_points_.size(); ++thread_id) { fused_points_.insert(fused_points_.end(), task_fused_points_[thread_id].begin(), task_fused_points_[thread_id].end()); task_fused_points_[thread_id].clear(); fused_points_visibility_.insert( fused_points_visibility_.end(), task_fused_points_visibility_[thread_id].begin(), task_fused_points_visibility_[thread_id].end()); task_fused_points_visibility_[thread_id].clear(); } if (fused_points_.empty()) { LOG(WARNING) << "Could not fuse any points. This is likely caused by " "incorrect settings - filtering must be enabled for the last " "call to patch match stereo."; } LOG(INFO) << "Number of fused points: " << fused_points_.size(); GetTimer().PrintMinutes(); } void StereoFusion::InitFusedPixelMask(int image_idx, size_t width, size_t height) { Bitmap mask; Mat& fused_pixel_mask = fused_pixel_masks_.at(image_idx); const std::string mask_path = JoinPaths(options_.mask_path, workspace_->GetModel().GetImageName(image_idx) + ".png"); fused_pixel_mask = Mat(width, height, 1); if (!options_.mask_path.empty() && ExistsFile(mask_path) && mask.Read(mask_path, false)) { BitmapColor color; mask.Rescale(static_cast(width), static_cast(height), Bitmap::RescaleFilter::kBox); for (size_t row = 0; row < height; ++row) { for (size_t col = 0; col < width; ++col) { mask.GetPixel(col, row, &color); fused_pixel_mask.Set(row, col, color.r == 0 ? 1 : 0); } } } else { fused_pixel_mask.Fill(0); } } void StereoFusion::Fuse(const int thread_id, const int image_idx, const int row, const int col) { // Next points to fuse. std::vector fusion_queue; fusion_queue.emplace_back(image_idx, row, col, 0); Eigen::Vector4f fused_ref_point = Eigen::Vector4f::Zero(); Eigen::Vector3f fused_ref_normal = Eigen::Vector3f::Zero(); // Points of different pixels of the currently point to be fused. std::vector fused_point_x; std::vector fused_point_y; std::vector fused_point_z; std::vector fused_point_nx; std::vector fused_point_ny; std::vector fused_point_nz; std::vector fused_point_r; std::vector fused_point_g; std::vector fused_point_b; std::unordered_set fused_point_visibility; while (!fusion_queue.empty()) { const auto data = fusion_queue.back(); const int image_idx = data.image_idx; const int row = data.row; const int col = data.col; const int traversal_depth = data.traversal_depth; fusion_queue.pop_back(); // Check if pixel already fused. auto& fused_pixel_mask = fused_pixel_masks_.at(image_idx); if (fused_pixel_mask.Get(row, col) > 0) { continue; } const auto& depth_map = workspace_->GetDepthMap(image_idx); const float depth = depth_map.Get(row, col); // Pixels with negative depth are filtered. if (depth <= 0.0f) { continue; } // If the traversal depth is greater than zero, the initial reference // pixel has already been added and we need to check for consistency. if (traversal_depth > 0) { // Project reference point into current view. const Eigen::Vector3f proj = P_.at(image_idx) * fused_ref_point; // Depth error of reference depth with current depth. const float depth_error = std::abs((proj(2) - depth) / depth); if (depth_error > options_.max_depth_error) { continue; } // Reprojection error reference point in the current view. const float col_diff = proj(0) / proj(2) - col; const float row_diff = proj(1) / proj(2) - row; const float squared_reproj_error = col_diff * col_diff + row_diff * row_diff; if (squared_reproj_error > max_squared_reproj_error_) { continue; } } // Determine normal direction in global reference frame. const auto& normal_map = workspace_->GetNormalMap(image_idx); const Eigen::Vector3f normal = inv_R_.at(image_idx) * Eigen::Vector3f(normal_map.Get(row, col, 0), normal_map.Get(row, col, 1), normal_map.Get(row, col, 2)); // Check for consistent normal direction with reference normal. if (traversal_depth > 0) { const float cos_normal_error = fused_ref_normal.dot(normal); if (cos_normal_error < min_cos_normal_error_) { continue; } } // Determine 3D location of current depth value. const Eigen::Vector3f xyz = inv_P_.at(image_idx) * Eigen::Vector4f(col * depth, row * depth, depth, 1.0f); // Read the color of the pixel. BitmapColor color; const auto& bitmap_scale = bitmap_scales_.at(image_idx); workspace_->GetBitmap(image_idx).InterpolateNearestNeighbor( col / bitmap_scale.first, row / bitmap_scale.second, &color); // Set the current pixel as visited. fused_pixel_mask.Set(row, col, 1); // Pixels out of bounds are filtered if (xyz(0) < options_.bounding_box.first(0) || xyz(1) < options_.bounding_box.first(1) || xyz(2) < options_.bounding_box.first(2) || xyz(0) > options_.bounding_box.second(0) || xyz(1) > options_.bounding_box.second(1) || xyz(2) > options_.bounding_box.second(2)) { continue; } // Accumulate statistics for fused point. fused_point_x.push_back(xyz(0)); fused_point_y.push_back(xyz(1)); fused_point_z.push_back(xyz(2)); fused_point_nx.push_back(normal(0)); fused_point_ny.push_back(normal(1)); fused_point_nz.push_back(normal(2)); fused_point_r.push_back(color.r); fused_point_g.push_back(color.g); fused_point_b.push_back(color.b); fused_point_visibility.insert(image_idx); // Remember the first pixel as the reference. if (traversal_depth == 0) { fused_ref_point = Eigen::Vector4f(xyz(0), xyz(1), xyz(2), 1.0f); fused_ref_normal = normal; } if (fused_point_x.size() >= static_cast(options_.max_num_pixels)) { break; } if (traversal_depth >= options_.max_traversal_depth - 1) { continue; } for (const auto next_image_idx : overlapping_images_.at(image_idx)) { if (!used_images_.at(next_image_idx) || fused_images_.at(next_image_idx)) { continue; } const Eigen::Vector3f next_proj = P_.at(next_image_idx) * xyz.homogeneous(); const int next_col = static_cast(std::round(next_proj(0) / next_proj(2))); const int next_row = static_cast(std::round(next_proj(1) / next_proj(2))); const auto& depth_map_size = depth_map_sizes_.at(next_image_idx); if (next_col < 0 || next_row < 0 || next_col >= depth_map_size.first || next_row >= depth_map_size.second) { continue; } fusion_queue.emplace_back( next_image_idx, next_row, next_col, traversal_depth + 1); } } const size_t num_pixels = fused_point_x.size(); if (num_pixels >= static_cast(options_.min_num_pixels)) { PlyPoint fused_point; Eigen::Vector3f fused_normal; fused_normal.x() = internal::Median(&fused_point_nx); fused_normal.y() = internal::Median(&fused_point_ny); fused_normal.z() = internal::Median(&fused_point_nz); const float fused_normal_norm = fused_normal.norm(); if (fused_normal_norm < std::numeric_limits::epsilon()) { return; } fused_point.x = internal::Median(&fused_point_x); fused_point.y = internal::Median(&fused_point_y); fused_point.z = internal::Median(&fused_point_z); fused_point.nx = fused_normal.x() / fused_normal_norm; fused_point.ny = fused_normal.y() / fused_normal_norm; fused_point.nz = fused_normal.z() / fused_normal_norm; fused_point.r = TruncateCast( std::round(internal::Median(&fused_point_r))); fused_point.g = TruncateCast( std::round(internal::Median(&fused_point_g))); fused_point.b = TruncateCast( std::round(internal::Median(&fused_point_b))); task_fused_points_[thread_id].push_back(fused_point); task_fused_points_visibility_[thread_id].emplace_back( fused_point_visibility.begin(), fused_point_visibility.end()); } } void WritePointsVisibility( const std::string& path, const std::vector>& points_visibility) { std::fstream file(path, std::ios::out | std::ios::binary); CHECK(file.is_open()) << path; WriteBinaryLittleEndian(&file, points_visibility.size()); for (const auto& visibility : points_visibility) { WriteBinaryLittleEndian(&file, visibility.size()); for (const auto& image_idx : visibility) { WriteBinaryLittleEndian(&file, image_idx); } } } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/fusion.h000066400000000000000000000153201454702036400173300ustar00rootroot00000000000000// 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. #pragma once #include "colmap/math/math.h" #include "colmap/mvs/depth_map.h" #include "colmap/mvs/image.h" #include "colmap/mvs/mat.h" #include "colmap/mvs/model.h" #include "colmap/mvs/normal_map.h" #include "colmap/mvs/workspace.h" #include "colmap/util/cache.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/ply.h" #include "colmap/util/threading.h" #include #include #include #include namespace colmap { namespace mvs { struct StereoFusionOptions { // Path for PNG masks. Same format expected as ImageReaderOptions. std::string mask_path = ""; // The number of threads to use during fusion. int num_threads = -1; // Maximum image size in either dimension. int max_image_size = -1; // Minimum number of fused pixels to produce a point. int min_num_pixels = 5; // Maximum number of pixels to fuse into a single point. int max_num_pixels = 10000; // Maximum depth in consistency graph traversal. int max_traversal_depth = 100; // Maximum relative difference between measured and projected pixel. double max_reproj_error = 2.0f; // Maximum relative difference between measured and projected depth. double max_depth_error = 0.01f; // Maximum angular difference in degrees of normals of pixels to be fused. double max_normal_error = 10.0f; // Number of overlapping images to transitively check for fusing points. int check_num_images = 50; // Flag indicating whether to use LRU cache or pre-load all data bool use_cache = false; // Cache size in gigabytes for fusion. The fusion keeps the bitmaps, depth // maps, normal maps, and consistency graphs of this number of images in // memory. A higher value leads to less disk access and faster fusion, while // a lower value leads to reduced memory usage. Note that a single image can // consume a lot of memory, if the consistency graph is dense. double cache_size = 32.0; std::pair bounding_box = std::make_pair(Eigen::Vector3f(-FLT_MAX, -FLT_MAX, -FLT_MAX), Eigen::Vector3f(FLT_MAX, FLT_MAX, FLT_MAX)); // Check the options for validity. bool Check() const; // Print the options to stdout. void Print() const; }; class StereoFusion : public Thread { public: StereoFusion(const StereoFusionOptions& options, const std::string& workspace_path, const std::string& workspace_format, const std::string& pmvs_option_name, const std::string& input_type); const std::vector& GetFusedPoints() const; const std::vector>& GetFusedPointsVisibility() const; private: void Run(); void InitFusedPixelMask(int image_idx, size_t width, size_t height); void Fuse(int thread_id, int image_idx, int row, int col); const StereoFusionOptions options_; const std::string workspace_path_; const std::string workspace_format_; const std::string pmvs_option_name_; const std::string input_type_; const float max_squared_reproj_error_; const float min_cos_normal_error_; std::unique_ptr workspace_; std::vector used_images_; std::vector fused_images_; std::vector> overlapping_images_; // Contains image masks of pre-masked and already fused pixels. // Initialized from image masks if provided in StereoFusionOptions. std::vector> fused_pixel_masks_; std::vector> depth_map_sizes_; std::vector> bitmap_scales_; std::vector> P_; std::vector> inv_P_; std::vector> inv_R_; struct FusionData { int image_idx = kInvalidImageId; int row = 0; int col = 0; int traversal_depth = -1; FusionData(int image_idx, int row, int col, int traversal_depth) : image_idx(image_idx), row(row), col(col), traversal_depth(traversal_depth) {} bool operator()(const FusionData& data1, const FusionData& data2) { return data1.image_idx > data2.image_idx; } }; // Already fused points. std::vector fused_points_; std::vector> fused_points_visibility_; std::vector> task_fused_points_; std::vector>> task_fused_points_visibility_; }; // Write the visiblity information into a binary file of the following format: // // // // ... // // ... // ... // // Note that an image_idx in the case of the mvs::StereoFuser does not // correspond to the image_id of a Reconstruction, but the index of the image in // the mvs::Model, which is the location of the image in the images.bin/.txt. void WritePointsVisibility( const std::string& path, const std::vector>& points_visibility); } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/gpu_mat.h000066400000000000000000000345561454702036400174750ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/cuda_flip.h" #include "colmap/mvs/cuda_rotate.h" #include "colmap/mvs/cuda_transpose.h" #include "colmap/mvs/mat.h" #include "colmap/util/cuda.h" #include "colmap/util/cudacc.h" #include "colmap/util/endian.h" #include #include #include #include #include #include namespace colmap { namespace mvs { template class GpuMat { public: GpuMat(const size_t width, const size_t height, const size_t depth = 1); ~GpuMat(); __host__ __device__ const T* GetPtr() const; __host__ __device__ T* GetPtr(); __host__ __device__ size_t GetPitch() const; __host__ __device__ size_t GetWidth() const; __host__ __device__ size_t GetHeight() const; __host__ __device__ size_t GetDepth() const; __device__ T Get(const size_t row, const size_t col, const size_t slice = 0) const; __device__ void GetSlice(const size_t row, const size_t col, T* values) const; __device__ T& GetRef(const size_t row, const size_t col); __device__ T& GetRef(const size_t row, const size_t col, const size_t slice); __device__ void Set(const size_t row, const size_t col, const T value); __device__ void Set(const size_t row, const size_t col, const size_t slice, const T value); __device__ void SetSlice(const size_t row, const size_t col, const T* values); void FillWithScalar(const T value); void FillWithVector(const T* values); void FillWithRandomNumbers(const T min_value, const T max_value, GpuMat random_state); void CopyToDevice(const T* data, const size_t pitch); void CopyToHost(T* data, const size_t pitch) const; Mat CopyToMat() const; // Transpose array by swapping x and y coordinates. void Transpose(GpuMat* output); // Flip array along vertical axis. void FlipHorizontal(GpuMat* output); // Rotate array in counter-clockwise direction. void Rotate(GpuMat* output); void Read(const std::string& path); void Write(const std::string& path); void Write(const std::string& path, const size_t slice); protected: void ComputeCudaConfig(); const static size_t kBlockDimX = 32; const static size_t kBlockDimY = 16; std::shared_ptr array_; T* array_ptr_; size_t pitch_; size_t width_; size_t height_; size_t depth_; dim3 blockSize_; dim3 gridSize_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// #ifdef __CUDACC__ namespace internal { template __global__ void FillWithScalarKernel(GpuMat output, const T value) { const size_t row = blockIdx.y * blockDim.y + threadIdx.y; const size_t col = blockIdx.x * blockDim.x + threadIdx.x; if (row < output.GetHeight() && col < output.GetWidth()) { for (size_t slice = 0; slice < output.GetDepth(); ++slice) { output.Set(row, col, slice, value); } } } template __global__ void FillWithVectorKernel(const T* values, GpuMat output) { const size_t row = blockIdx.y * blockDim.y + threadIdx.y; const size_t col = blockIdx.x * blockDim.x + threadIdx.x; if (row < output.GetHeight() && col < output.GetWidth()) { for (size_t slice = 0; slice < output.GetDepth(); ++slice) { output.Set(row, col, slice, values[slice]); } } } template __global__ void FillWithRandomNumbersKernel(GpuMat output, GpuMat random_state, const T min_value, const T max_value) { const size_t row = blockIdx.y * blockDim.y + threadIdx.y; const size_t col = blockIdx.x * blockDim.x + threadIdx.x; if (row < output.GetHeight() && col < output.GetWidth()) { curandState local_state = random_state.Get(row, col); for (size_t slice = 0; slice < output.GetDepth(); ++slice) { const T random_value = curand_uniform(&local_state) * (max_value - min_value) + min_value; output.Set(row, col, slice, random_value); } random_state.Set(row, col, local_state); } } } // namespace internal template GpuMat::GpuMat(const size_t width, const size_t height, const size_t depth) : array_(nullptr), array_ptr_(nullptr), width_(width), height_(height), depth_(depth) { CUDA_SAFE_CALL(cudaMallocPitch( (void**)&array_ptr_, &pitch_, width_ * sizeof(T), height_ * depth_)); array_ = std::shared_ptr(array_ptr_, cudaFree); ComputeCudaConfig(); } template GpuMat::~GpuMat() { array_.reset(); array_ptr_ = nullptr; pitch_ = 0; width_ = 0; height_ = 0; depth_ = 0; } template __host__ __device__ const T* GpuMat::GetPtr() const { return array_ptr_; } template __host__ __device__ T* GpuMat::GetPtr() { return array_ptr_; } template __host__ __device__ size_t GpuMat::GetPitch() const { return pitch_; } template __host__ __device__ size_t GpuMat::GetWidth() const { return width_; } template __host__ __device__ size_t GpuMat::GetHeight() const { return height_; } template __host__ __device__ size_t GpuMat::GetDepth() const { return depth_; } template __device__ T GpuMat::Get(const size_t row, const size_t col, const size_t slice) const { return *((T*)((char*)array_ptr_ + pitch_ * (slice * height_ + row)) + col); } template __device__ void GpuMat::GetSlice(const size_t row, const size_t col, T* values) const { for (size_t slice = 0; slice < depth_; ++slice) { values[slice] = Get(row, col, slice); } } template __device__ T& GpuMat::GetRef(const size_t row, const size_t col) { return GetRef(row, col, 0); } template __device__ T& GpuMat::GetRef(const size_t row, const size_t col, const size_t slice) { return *((T*)((char*)array_ptr_ + pitch_ * (slice * height_ + row)) + col); } template __device__ void GpuMat::Set(const size_t row, const size_t col, const T value) { Set(row, col, 0, value); } template __device__ void GpuMat::Set(const size_t row, const size_t col, const size_t slice, const T value) { *((T*)((char*)array_ptr_ + pitch_ * (slice * height_ + row)) + col) = value; } template __device__ void GpuMat::SetSlice(const size_t row, const size_t col, const T* values) { for (size_t slice = 0; slice < depth_; ++slice) { Set(row, col, slice, values[slice]); } } template void GpuMat::FillWithScalar(const T value) { internal::FillWithScalarKernel<<>>(*this, value); CUDA_SYNC_AND_CHECK(); } template void GpuMat::FillWithVector(const T* values) { T* values_device; CUDA_SAFE_CALL(cudaMalloc((void**)&values_device, depth_ * sizeof(T))); CUDA_SAFE_CALL(cudaMemcpy( values_device, values, depth_ * sizeof(T), cudaMemcpyHostToDevice)); internal::FillWithVectorKernel <<>>(values_device, *this); CUDA_SYNC_AND_CHECK(); CUDA_SAFE_CALL(cudaFree(values_device)); } template void GpuMat::FillWithRandomNumbers(const T min_value, const T max_value, const GpuMat random_state) { internal::FillWithRandomNumbersKernel <<>>(*this, random_state, min_value, max_value); CUDA_SYNC_AND_CHECK(); } template void GpuMat::CopyToDevice(const T* data, const size_t pitch) { CUDA_SAFE_CALL(cudaMemcpy2D((void*)array_ptr_, (size_t)pitch_, (void*)data, pitch, width_ * sizeof(T), height_ * depth_, cudaMemcpyHostToDevice)); } template void GpuMat::CopyToHost(T* data, const size_t pitch) const { CUDA_SAFE_CALL(cudaMemcpy2D((void*)data, pitch, (void*)array_ptr_, (size_t)pitch_, width_ * sizeof(T), height_ * depth_, cudaMemcpyDeviceToHost)); } template Mat GpuMat::CopyToMat() const { Mat mat(width_, height_, depth_); CopyToHost(mat.GetPtr(), mat.GetWidth() * sizeof(T)); return mat; } template void GpuMat::Transpose(GpuMat* output) { for (size_t slice = 0; slice < depth_; ++slice) { CudaTranspose(array_ptr_ + slice * pitch_ / sizeof(T) * GetHeight(), output->GetPtr() + slice * output->pitch_ / sizeof(T) * output->GetHeight(), width_, height_, pitch_, output->pitch_); } CUDA_SYNC_AND_CHECK(); } template void GpuMat::FlipHorizontal(GpuMat* output) { for (size_t slice = 0; slice < depth_; ++slice) { CudaFlipHorizontal(array_ptr_ + slice * pitch_ / sizeof(T) * GetHeight(), output->GetPtr() + slice * output->pitch_ / sizeof(T) * output->GetHeight(), width_, height_, pitch_, output->pitch_); } CUDA_SYNC_AND_CHECK(); } template void GpuMat::Rotate(GpuMat* output) { for (size_t slice = 0; slice < depth_; ++slice) { CudaRotate((T*)((char*)array_ptr_ + slice * pitch_ * GetHeight()), (T*)((char*)output->GetPtr() + slice * output->pitch_ * output->GetHeight()), width_, height_, pitch_, output->pitch_); } CUDA_SYNC_AND_CHECK(); // This is equivalent to the following code: // GpuMat flipped_array(width_, height_, GetDepth()); // FlipHorizontal(&flipped_array); // flipped_array.Transpose(output); } template void GpuMat::Read(const std::string& path) { std::fstream text_file(path, std::ios::in | std::ios::binary); CHECK(text_file.is_open()) << path; size_t width; size_t height; size_t depth; char unused_char; text_file >> width >> unused_char >> height >> unused_char >> depth >> unused_char; std::streampos pos = text_file.tellg(); text_file.close(); std::fstream binary_file(path, std::ios::in | std::ios::binary); binary_file.seekg(pos); std::vector source(width_ * height_ * depth_); ReadBinaryLittleEndian(&binary_file, &source); binary_file.close(); CopyToDevice(source.data(), width_ * sizeof(T)); } template void GpuMat::Write(const std::string& path) { std::vector dest(width_ * height_ * depth_); CopyToHost(dest.data(), width_ * sizeof(T)); std::fstream text_file(path, std::ios::out); text_file << width_ << "&" << height_ << "&" << depth_ << "&"; text_file.close(); std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); WriteBinaryLittleEndian(&binary_file, dest); binary_file.close(); } template void GpuMat::Write(const std::string& path, const size_t slice) { std::vector dest(width_ * height_); CUDA_SAFE_CALL( cudaMemcpy2D((void*)dest.data(), width_ * sizeof(T), (void*)(array_ptr_ + slice * height_ * pitch_ / sizeof(T)), pitch_, width_ * sizeof(T), height_, cudaMemcpyDeviceToHost)); std::fstream text_file(path, std::ios::out); text_file << width_ << "&" << height_ << "&" << 1 << "&"; text_file.close(); std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); WriteBinaryLittleEndian(&binary_file, dest); binary_file.close(); } template void GpuMat::ComputeCudaConfig() { blockSize_.x = kBlockDimX; blockSize_.y = kBlockDimY; blockSize_.z = 1; gridSize_.x = (width_ - 1) / kBlockDimX + 1; gridSize_.y = (height_ - 1) / kBlockDimY + 1; gridSize_.z = 1; } #endif // __CUDACC__ } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/gpu_mat_prng.cu000066400000000000000000000047141454702036400206740ustar00rootroot00000000000000// 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. #include "colmap/mvs/gpu_mat_prng.h" namespace colmap { namespace mvs { namespace { __global__ void InitRandomStateKernel(GpuMat output) { const size_t row = blockIdx.y * blockDim.y + threadIdx.y; const size_t col = blockIdx.x * blockDim.x + threadIdx.x; const size_t uniqueBlockIndex = blockIdx.y * gridDim.x + blockIdx.x; const size_t id = uniqueBlockIndex * blockDim.y * blockDim.x + threadIdx.y * blockDim.x + threadIdx.x; // Each thread gets same seed, a different sequence number, no offset. if (col < output.GetWidth() && row < output.GetHeight()) { curand_init(id, 0, 0, &output.GetRef(row, col)); } } } // namespace GpuMatPRNG::GpuMatPRNG(const int width, const int height) : GpuMat(width, height) { InitRandomStateKernel<<>>(*this); } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/gpu_mat_prng.h000066400000000000000000000035611454702036400205130ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/gpu_mat.h" namespace colmap { namespace mvs { class GpuMatPRNG : public GpuMat { public: GpuMatPRNG(const int width, const int height); private: void InitRandomState(); }; } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/gpu_mat_ref_image.cu000066400000000000000000000123031454702036400216350ustar00rootroot00000000000000// 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. #include "colmap/mvs/gpu_mat_ref_image.h" #include "colmap/util/cudacc.h" #include namespace colmap { namespace mvs { namespace { __global__ void FilterKernel(const cudaTextureObject_t image_texture, GpuMat image, GpuMat sum_image, GpuMat squared_sum_image, const int window_radius, const int window_step, const float sigma_spatial, const float sigma_color) { const size_t row = blockDim.y * blockIdx.y + threadIdx.y; const size_t col = blockDim.x * blockIdx.x + threadIdx.x; if (row >= image.GetHeight() || col >= image.GetWidth()) { return; } BilateralWeightComputer bilateral_weight_computer(sigma_spatial, sigma_color); const float center_color = tex2D(image_texture, col, row); float color_sum = 0.0f; float color_squared_sum = 0.0f; float bilateral_weight_sum = 0.0f; for (int window_row = -window_radius; window_row <= window_radius; window_row += window_step) { for (int window_col = -window_radius; window_col <= window_radius; window_col += window_step) { const float color = tex2D(image_texture, col + window_col, row + window_row); const float bilateral_weight = bilateral_weight_computer.Compute( window_row, window_col, center_color, color); color_sum += bilateral_weight * color; color_squared_sum += bilateral_weight * color * color; bilateral_weight_sum += bilateral_weight; } } color_sum /= bilateral_weight_sum; color_squared_sum /= bilateral_weight_sum; image.Set(row, col, static_cast(255.0f * center_color)); sum_image.Set(row, col, color_sum); squared_sum_image.Set(row, col, color_squared_sum); } } // namespace GpuMatRefImage::GpuMatRefImage(const size_t width, const size_t height) : height_(height), width_(width) { image.reset(new GpuMat(width, height)); sum_image.reset(new GpuMat(width, height)); squared_sum_image.reset(new GpuMat(width, height)); } void GpuMatRefImage::Filter(const uint8_t* image_data, const size_t window_radius, const size_t window_step, const float sigma_spatial, const float sigma_color) { cudaTextureDesc texture_desc; memset(&texture_desc, 0, sizeof(texture_desc)); texture_desc.addressMode[0] = cudaAddressModeBorder; texture_desc.addressMode[1] = cudaAddressModeBorder; texture_desc.addressMode[2] = cudaAddressModeBorder; texture_desc.filterMode = cudaFilterModePoint; texture_desc.readMode = cudaReadModeNormalizedFloat; texture_desc.normalizedCoords = false; auto image_texture = CudaArrayLayeredTexture::FromHostArray( texture_desc, width_, height_, 1, image_data); const dim3 block_size(kBlockDimX, kBlockDimY); const dim3 grid_size((width_ - 1) / block_size.x + 1, (height_ - 1) / block_size.y + 1); FilterKernel<<>>(image_texture->GetObj(), *image, *sum_image, *squared_sum_image, window_radius, window_step, sigma_spatial, sigma_color); CUDA_SYNC_AND_CHECK(); } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/gpu_mat_ref_image.h000066400000000000000000000067611454702036400214700ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/cuda_texture.h" #include "colmap/mvs/gpu_mat.h" #include namespace colmap { namespace mvs { class GpuMatRefImage { public: GpuMatRefImage(const size_t width, const size_t height); // Filter image using sum convolution kernel to compute local sum of // intensities. The filtered images can then be used for repeated, efficient // NCC computation. void Filter(const uint8_t* image_data, const size_t window_radius, const size_t window_step, const float sigma_spatial, const float sigma_color); // Image intensities. std::unique_ptr> image; // Local sum of image intensities. std::unique_ptr> sum_image; // Local sum of squared image intensities. std::unique_ptr> squared_sum_image; private: const static size_t kBlockDimX = 16; const static size_t kBlockDimY = 12; const size_t width_; const size_t height_; }; struct BilateralWeightComputer { __device__ BilateralWeightComputer(const float sigma_spatial, const float sigma_color) : spatial_normalization_(1.0f / (2.0f * sigma_spatial * sigma_spatial)), color_normalization_(1.0f / (2.0f * sigma_color * sigma_color)) {} __device__ inline float Compute(const float row_diff, const float col_diff, const float color1, const float color2) const { const float spatial_dist_squared = row_diff * row_diff + col_diff * col_diff; const float color_dist = color1 - color2; return exp(-spatial_dist_squared * spatial_normalization_ - color_dist * color_dist * color_normalization_); } private: const float spatial_normalization_; const float color_normalization_; }; } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/gpu_mat_test.cu000066400000000000000000000163721454702036400207100ustar00rootroot00000000000000// 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. #ifdef __CUDACC__ #define BOOST_PP_VARIADICS 0 #endif // __CUDACC__ #include "colmap/math/math.h" #include "colmap/mvs/gpu_mat.h" #include "colmap/mvs/gpu_mat_prng.h" #include namespace colmap { namespace mvs { TEST(GpuMat, FillWithVector) { GpuMat array(100, 100, 2); const std::vector vector = {1.0f, 2.0f}; array.FillWithVector(vector.data()); std::vector array_host(100 * 100 * 2, 0.0f); array.CopyToHost(array_host.data(), 100 * sizeof(float)); for (size_t r = 0; r < 100; ++r) { for (size_t c = 0; c < 100; ++c) { EXPECT_EQ(array_host[0 * 100 * 100 + r * 100 + c], 1.0f); EXPECT_EQ(array_host[1 * 100 * 100 + r * 100 + c], 2.0f); } } } template void TestTransposeImage(const size_t width, const size_t height, const size_t depth) { GpuMat array(width, height, depth); GpuMatPRNG prng_array(width, height); array.FillWithRandomNumbers(T(0.0), T(100.0), prng_array); GpuMat array_transposed(height, width, depth); array.Transpose(&array_transposed); std::vector array_host(width * height * depth, T(0.0)); array.CopyToHost(array_host.data(), width * sizeof(T)); std::vector array_transposed_host(width * height * depth, 0); array_transposed.CopyToHost(array_transposed_host.data(), height * sizeof(T)); for (size_t r = 0; r < height; ++r) { for (size_t c = 0; c < width; ++c) { for (size_t d = 0; d < depth; ++d) { EXPECT_EQ(array_host[d * width * height + r * width + c], array_transposed_host[d * width * height + c * height + r]); } } } } TEST(GpuMat, Transpose) { for (size_t w = 1; w <= 5; ++w) { for (size_t h = 1; h <= 5; ++h) { for (size_t d = 1; d <= 3; ++d) { const size_t width = 20 * w; const size_t height = 20 * h; TestTransposeImage(width, height, d); TestTransposeImage(width, height, d); TestTransposeImage(width, height, d); TestTransposeImage(width, height, d); TestTransposeImage(width, height, d); TestTransposeImage(width, height, d); } } } } template void TestFlipHorizontalImage(const size_t width, const size_t height, const size_t depth) { GpuMat array(width, height, depth); GpuMatPRNG prng_array(width, height); array.FillWithRandomNumbers(T(0.0), T(100.0), prng_array); GpuMat array_flipped(width, height, depth); array.FlipHorizontal(&array_flipped); std::vector array_host(width * height * depth, T(0.0)); array.CopyToHost(array_host.data(), width * sizeof(T)); std::vector array_flipped_host(width * height * depth, 0); array_flipped.CopyToHost(array_flipped_host.data(), width * sizeof(T)); for (size_t r = 0; r < height; ++r) { for (size_t c = 0; c < width; ++c) { for (size_t d = 0; d < depth; ++d) { EXPECT_EQ( array_host[d * width * height + r * width + c], array_flipped_host[d * width * height + r * width + width - 1 - c]); } } } } TEST(GpuMat, FlipHorizontal) { for (size_t w = 1; w <= 5; ++w) { for (size_t h = 1; h <= 5; ++h) { for (size_t d = 1; d <= 3; ++d) { const size_t width = 20 * w; const size_t height = 20 * h; TestFlipHorizontalImage(width, height, d); TestFlipHorizontalImage(width, height, d); TestFlipHorizontalImage(width, height, d); TestFlipHorizontalImage(width, height, d); TestFlipHorizontalImage(width, height, d); TestFlipHorizontalImage(width, height, d); } } } } template void TestRotateImage(const size_t width, const size_t height, const size_t depth) { GpuMat array(width, height, depth); GpuMatPRNG prng_array(width, height); array.FillWithRandomNumbers(T(0.0), T(100.0), prng_array); GpuMat array_rotated(height, width, depth); array.Rotate(&array_rotated); std::vector array_host(width * height * depth, T(0.0)); array.CopyToHost(array_host.data(), width * sizeof(T)); std::vector array_rotated_host(width * height * depth, 0); array_rotated.CopyToHost(array_rotated_host.data(), height * sizeof(T)); const double arrayCenterH = width / 2.0 - 0.5; const double arrayCenterV = height / 2.0 - 0.5; const double angle = -M_PI / 2; for (size_t r = 0; r < height; ++r) { for (size_t c = 0; c < width; ++c) { for (size_t d = 0; d < depth; ++d) { const size_t rotc = std::round(std::cos(angle) * (c - arrayCenterH) - std::sin(angle) * (r - arrayCenterV) + arrayCenterV); const size_t rotr = std::round(std::sin(angle) * (c - arrayCenterH) + std::cos(angle) * (r - arrayCenterV) + arrayCenterH); EXPECT_EQ( array_host[d * width * height + r * width + c], array_rotated_host[d * width * height + rotr * height + rotc]); } } } } TEST(GpuMat, Rotate) { for (size_t w = 1; w <= 5; ++w) { for (size_t h = 1; h <= 5; ++h) { for (size_t d = 1; d <= 3; ++d) { const size_t width = 20 * w; const size_t height = 20 * h; TestRotateImage(width, height, d); TestRotateImage(width, height, d); TestRotateImage(width, height, d); TestRotateImage(width, height, d); TestRotateImage(width, height, d); TestRotateImage(width, height, d); } } } } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/image.cc000066400000000000000000000132621454702036400172500ustar00rootroot00000000000000// 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. #include "colmap/mvs/image.h" #include "colmap/scene/projection.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include namespace colmap { namespace mvs { Image::Image() {} Image::Image(const std::string& path, const size_t width, const size_t height, const float* K, const float* R, const float* T) : path_(path), width_(width), height_(height) { memcpy(K_, K, 9 * sizeof(float)); memcpy(R_, R, 9 * sizeof(float)); memcpy(T_, T, 3 * sizeof(float)); ComposeProjectionMatrix(K_, R_, T_, P_); ComposeInverseProjectionMatrix(K_, R_, T_, inv_P_); } void Image::SetBitmap(const Bitmap& bitmap) { bitmap_ = bitmap; CHECK_EQ(width_, bitmap_.Width()); CHECK_EQ(height_, bitmap_.Height()); } void Image::Rescale(const float factor) { Rescale(factor, factor); } void Image::Rescale(const float factor_x, const float factor_y) { const size_t new_width = std::round(width_ * factor_x); const size_t new_height = std::round(height_ * factor_y); if (bitmap_.Data() != nullptr) { bitmap_.Rescale(new_width, new_height); } const float scale_x = new_width / static_cast(width_); const float scale_y = new_height / static_cast(height_); K_[0] *= scale_x; K_[2] *= scale_x; K_[4] *= scale_y; K_[5] *= scale_y; ComposeProjectionMatrix(K_, R_, T_, P_); ComposeInverseProjectionMatrix(K_, R_, T_, inv_P_); width_ = new_width; height_ = new_height; } void Image::Downsize(const size_t max_width, const size_t max_height) { if (width_ <= max_width && height_ <= max_height) { return; } const float factor_x = static_cast(max_width) / width_; const float factor_y = static_cast(max_height) / height_; Rescale(std::min(factor_x, factor_y)); } void ComputeRelativePose(const float R1[9], const float T1[3], const float R2[9], const float T2[3], float R[9], float T[3]) { const Eigen::Map> R1_m(R1); const Eigen::Map> R2_m(R2); const Eigen::Map> T1_m(T1); const Eigen::Map> T2_m(T2); Eigen::Map> R_m(R); Eigen::Map T_m(T); R_m = R2_m * R1_m.transpose(); T_m = T2_m - R_m * T1_m; } void ComposeProjectionMatrix(const float K[9], const float R[9], const float T[3], float P[12]) { Eigen::Map> P_m(P); P_m.leftCols<3>() = Eigen::Map>(R); P_m.rightCols<1>() = Eigen::Map(T); P_m = Eigen::Map>(K) * P_m; } void ComposeInverseProjectionMatrix(const float K[9], const float R[9], const float T[3], float inv_P[12]) { Eigen::Matrix P; ComposeProjectionMatrix(K, R, T, P.data()); P.row(3) = Eigen::Vector4f(0, 0, 0, 1); const Eigen::Matrix4f inv_P_temp = P.inverse(); Eigen::Map> inv_P_m(inv_P); inv_P_m = inv_P_temp.topRows<3>(); } void ComputeProjectionCenter(const float R[9], const float T[3], float C[3]) { const Eigen::Map> R_m(R); const Eigen::Map> T_m(T); Eigen::Map C_m(C); C_m = -R_m.transpose() * T_m; } void RotatePose(const float RR[9], float R[9], float T[3]) { Eigen::Map> R_m(R); Eigen::Map> T_m(T); const Eigen::Map> RR_m(RR); R_m = RR_m * R_m; T_m = RR_m * T_m; } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/image.h000066400000000000000000000103341454702036400171070ustar00rootroot00000000000000// 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. #pragma once #include "colmap/sensor/bitmap.h" #include #include #include #include #include #include namespace colmap { namespace mvs { class Image { public: Image(); Image(const std::string& path, size_t width, size_t height, const float* K, const float* R, const float* T); inline size_t GetWidth() const; inline size_t GetHeight() const; void SetBitmap(const Bitmap& bitmap); inline const Bitmap& GetBitmap() const; inline const std::string& GetPath() const; inline const float* GetR() const; inline const float* GetT() const; inline const float* GetK() const; inline const float* GetP() const; inline const float* GetInvP() const; inline const float* GetViewingDirection() const; void Rescale(float factor); void Rescale(float factor_x, float factor_y); void Downsize(size_t max_width, size_t max_height); private: std::string path_; size_t width_; size_t height_; float K_[9]; float R_[9]; float T_[3]; float P_[12]; float inv_P_[12]; Bitmap bitmap_; }; void ComputeRelativePose(const float R1[9], const float T1[3], const float R2[9], const float T2[3], float R[9], float T[3]); void ComposeProjectionMatrix(const float K[9], const float R[9], const float T[3], float P[12]); void ComposeInverseProjectionMatrix(const float K[9], const float R[9], const float T[3], float inv_P[12]); void ComputeProjectionCenter(const float R[9], const float T[3], float C[3]); void RotatePose(const float RR[9], float R[9], float T[3]); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// size_t Image::GetWidth() const { return width_; } size_t Image::GetHeight() const { return height_; } const Bitmap& Image::GetBitmap() const { return bitmap_; } const std::string& Image::GetPath() const { return path_; } const float* Image::GetR() const { return R_; } const float* Image::GetT() const { return T_; } const float* Image::GetK() const { return K_; } const float* Image::GetP() const { return P_; } const float* Image::GetInvP() const { return inv_P_; } const float* Image::GetViewingDirection() const { return &R_[6]; } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/mat.h000066400000000000000000000120341454702036400166050ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/endian.h" #include "colmap/util/logging.h" #include #include #include namespace colmap { namespace mvs { template class Mat { public: Mat(); Mat(size_t width, size_t height, size_t depth); size_t GetWidth() const; size_t GetHeight() const; size_t GetDepth() const; size_t GetNumBytes() const; T Get(size_t row, size_t col, size_t slice = 0) const; void GetSlice(size_t row, size_t col, T* values) const; T* GetPtr(); const T* GetPtr() const; const std::vector& GetData() const; void Set(size_t row, size_t col, T value); void Set(size_t row, size_t col, size_t slice, T value); void Fill(T value); void Read(const std::string& path); void Write(const std::string& path) const; protected: size_t width_ = 0; size_t height_ = 0; size_t depth_ = 0; std::vector data_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template Mat::Mat() : Mat(0, 0, 0) {} template Mat::Mat(const size_t width, const size_t height, const size_t depth) : width_(width), height_(height), depth_(depth) { data_.resize(width_ * height_ * depth_, 0); } template size_t Mat::GetWidth() const { return width_; } template size_t Mat::GetHeight() const { return height_; } template size_t Mat::GetDepth() const { return depth_; } template size_t Mat::GetNumBytes() const { return data_.size() * sizeof(T); } template T Mat::Get(const size_t row, const size_t col, const size_t slice) const { return data_.at(slice * width_ * height_ + row * width_ + col); } template void Mat::GetSlice(const size_t row, const size_t col, T* values) const { for (size_t slice = 0; slice < depth_; ++slice) { values[slice] = Get(row, col, slice); } } template T* Mat::GetPtr() { return data_.data(); } template const T* Mat::GetPtr() const { return data_.data(); } template const std::vector& Mat::GetData() const { return data_; } template void Mat::Set(const size_t row, const size_t col, const T value) { Set(row, col, 0, value); } template void Mat::Set(const size_t row, const size_t col, const size_t slice, const T value) { data_.at(slice * width_ * height_ + row * width_ + col) = value; } template void Mat::Fill(const T value) { std::fill(data_.begin(), data_.end(), value); } template void Mat::Read(const std::string& path) { std::ifstream file(path, std::ios::binary); CHECK(file.is_open()) << path; char unused_char; file >> width_ >> unused_char >> height_ >> unused_char >> depth_ >> unused_char; CHECK_GT(width_, 0) << path; CHECK_GT(height_, 0) << path; CHECK_GT(depth_, 0) << path; data_.resize(width_ * height_ * depth_); ReadBinaryLittleEndian(&file, &data_); file.close(); } template void Mat::Write(const std::string& path) const { std::ofstream file(path, std::ios::binary); CHECK(file.is_open()) << path; file << width_ << "&" << height_ << "&" << depth_ << "&"; WriteBinaryLittleEndian(&file, data_); file.close(); } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/mat_test.cc000066400000000000000000000057531454702036400200140ustar00rootroot00000000000000// 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. #include "colmap/mvs/mat.h" #include namespace colmap { namespace mvs { namespace { TEST(Mat, Empty) { Mat mat; EXPECT_EQ(mat.GetWidth(), 0); EXPECT_EQ(mat.GetHeight(), 0); EXPECT_EQ(mat.GetDepth(), 0); EXPECT_EQ(mat.GetNumBytes(), 0); } TEST(Mat, NonEmpty) { Mat mat(1, 2, 3); EXPECT_EQ(mat.GetWidth(), 1); EXPECT_EQ(mat.GetHeight(), 2); EXPECT_EQ(mat.GetDepth(), 3); EXPECT_EQ(mat.GetNumBytes(), 24); } TEST(Mat, GetSet) { Mat mat(1, 2, 3); EXPECT_EQ(mat.GetNumBytes(), 24); mat.Set(0, 0, 0, 1); mat.Set(0, 0, 1, 2); mat.Set(0, 0, 2, 3); mat.Set(1, 0, 0, 4); mat.Set(1, 0, 1, 5); mat.Set(1, 0, 2, 6); EXPECT_EQ(mat.Get(0, 0, 0), 1); EXPECT_EQ(mat.Get(0, 0, 1), 2); EXPECT_EQ(mat.Get(0, 0, 2), 3); EXPECT_EQ(mat.Get(1, 0, 0), 4); EXPECT_EQ(mat.Get(1, 0, 1), 5); EXPECT_EQ(mat.Get(1, 0, 2), 6); int slice[3]; mat.GetSlice(0, 0, slice); EXPECT_EQ(slice[0], 1); EXPECT_EQ(slice[1], 2); EXPECT_EQ(slice[2], 3); mat.GetSlice(1, 0, slice); EXPECT_EQ(slice[0], 4); EXPECT_EQ(slice[1], 5); EXPECT_EQ(slice[2], 6); } TEST(Mat, Fill) { Mat mat(1, 2, 3); EXPECT_EQ(mat.GetNumBytes(), 24); mat.Fill(10); mat.Set(0, 0, 0, 10); mat.Set(0, 0, 1, 10); mat.Set(0, 0, 2, 10); mat.Set(1, 0, 0, 10); mat.Set(1, 0, 1, 10); mat.Set(1, 0, 2, 10); } } // namespace } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/meshing.cc000066400000000000000000001110141454702036400176120ustar00rootroot00000000000000// 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. #include "colmap/mvs/meshing.h" #include #include #include #if defined(COLMAP_CGAL_ENABLED) #include #include #endif // COLMAP_CGAL_ENABLED #include "colmap/math/graph_cut.h" #include "colmap/math/random.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/endian.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include "colmap/util/ply.h" #include "colmap/util/threading.h" #include "colmap/util/timer.h" #include "thirdparty/PoissonRecon/PoissonRecon.h" #include "thirdparty/PoissonRecon/SurfaceTrimmer.h" #if defined(COLMAP_CGAL_ENABLED) typedef CGAL::Exact_predicates_inexact_constructions_kernel K; typedef CGAL::Delaunay_triangulation_3 Delaunay; namespace std { template <> struct hash { std::size_t operator()(const Delaunay::Vertex_handle& handle) const { return reinterpret_cast(&*handle); } }; template <> struct hash { std::size_t operator()(const Delaunay::Vertex_handle& handle) const { return reinterpret_cast(&*handle); } }; template <> struct hash { std::size_t operator()(const Delaunay::Cell_handle& handle) const { return reinterpret_cast(&*handle); } }; template <> struct hash { std::size_t operator()(const Delaunay::Cell_handle& handle) const { return reinterpret_cast(&*handle); } }; } // namespace std #endif // COLMAP_CGAL_ENABLED namespace colmap { namespace mvs { bool PoissonMeshingOptions::Check() const { CHECK_OPTION_GE(point_weight, 0); CHECK_OPTION_GT(depth, 0); CHECK_OPTION_GE(color, 0); CHECK_OPTION_GE(trim, 0); CHECK_OPTION_GE(num_threads, -1); CHECK_OPTION_NE(num_threads, 0); return true; } bool DelaunayMeshingOptions::Check() const { CHECK_OPTION_GE(max_proj_dist, 0); CHECK_OPTION_GE(max_depth_dist, 0); CHECK_OPTION_LE(max_depth_dist, 1); CHECK_OPTION_GT(visibility_sigma, 0); CHECK_OPTION_GT(distance_sigma_factor, 0); CHECK_OPTION_GE(quality_regularization, 0); CHECK_OPTION_GE(max_side_length_factor, 0); CHECK_OPTION_GE(max_side_length_percentile, 0); CHECK_OPTION_LE(max_side_length_percentile, 100); CHECK_OPTION_GE(num_threads, -1); CHECK_OPTION_NE(num_threads, 0); return true; } bool PoissonMeshing(const PoissonMeshingOptions& options, const std::string& input_path, const std::string& output_path) { CHECK(options.Check()); std::vector args; args.push_back("./binary"); args.push_back("--in"); args.push_back(input_path); args.push_back("--out"); args.push_back(output_path); args.push_back("--pointWeight"); args.push_back(std::to_string(options.point_weight)); args.push_back("--depth"); args.push_back(std::to_string(options.depth)); if (options.color > 0) { args.push_back("--color"); args.push_back(std::to_string(options.color)); } #if defined(COLMAP_OPENMP_ENABLED) if (options.num_threads > 0) { args.push_back("--threads"); args.push_back(std::to_string(options.num_threads)); } #endif // OPENMP_ENABLED if (options.trim > 0) { args.push_back("--density"); } std::vector args_cstr; args_cstr.reserve(args.size()); for (const auto& arg : args) { args_cstr.push_back(arg.c_str()); } if (PoissonRecon(args_cstr.size(), const_cast(args_cstr.data())) != EXIT_SUCCESS) { return false; } if (options.trim == 0) { return true; } args.clear(); args_cstr.clear(); args.push_back("./binary"); args.push_back("--in"); args.push_back(output_path); args.push_back("--out"); args.push_back(output_path); args.push_back("--trim"); args.push_back(std::to_string(options.trim)); args_cstr.reserve(args.size()); for (const auto& arg : args) { args_cstr.push_back(arg.c_str()); } return SurfaceTrimmer(args_cstr.size(), const_cast(args_cstr.data())) == EXIT_SUCCESS; } #if defined(COLMAP_CGAL_ENABLED) K::Point_3 EigenToCGAL(const Eigen::Vector3f& point) { return K::Point_3(point.x(), point.y(), point.z()); } Eigen::Vector3f CGALToEigen(const K::Point_3& point) { return Eigen::Vector3f(point.x(), point.y(), point.z()); } class DelaunayMeshingInput { public: struct Image { camera_t camera_id = kInvalidCameraId; Eigen::Matrix3x4f proj_matrix = Eigen::Matrix3x4f::Identity(); Eigen::Vector3f proj_center = Eigen::Vector3f::Zero(); std::vector point_idxs; }; struct Point { Eigen::Vector3f position = Eigen::Vector3f::Zero(); uint32_t num_visible_images = 0; }; std::unordered_map cameras; std::vector images; std::vector points; void ReadSparseReconstruction(const std::string& path) { Reconstruction reconstruction; reconstruction.Read(path); CopyFromSparseReconstruction(reconstruction); } void CopyFromSparseReconstruction(const Reconstruction& reconstruction) { images.reserve(reconstruction.NumRegImages()); points.reserve(reconstruction.NumPoints3D()); cameras = reconstruction.Cameras(); std::unordered_map point_id_to_idx; point_id_to_idx.reserve(reconstruction.NumPoints3D()); for (const auto& point3D : reconstruction.Points3D()) { point_id_to_idx.emplace(point3D.first, points.size()); DelaunayMeshingInput::Point input_point; input_point.position = point3D.second.xyz.cast(); input_point.num_visible_images = point3D.second.track.Length(); points.push_back(input_point); } for (const auto image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); DelaunayMeshingInput::Image input_image; input_image.camera_id = image.CameraId(); input_image.proj_matrix = image.CamFromWorld().ToMatrix().cast(); input_image.proj_center = image.ProjectionCenter().cast(); input_image.point_idxs.reserve(image.NumPoints3D()); for (const auto& point2D : image.Points2D()) { if (point2D.HasPoint3D()) { input_image.point_idxs.push_back( point_id_to_idx.at(point2D.point3D_id)); } } images.push_back(input_image); } } void ReadDenseReconstruction(const std::string& path) { { Reconstruction reconstruction; reconstruction.Read(JoinPaths(path, "sparse")); cameras = reconstruction.Cameras(); images.reserve(reconstruction.NumRegImages()); for (const auto& image_id : reconstruction.RegImageIds()) { const auto& image = reconstruction.Image(image_id); DelaunayMeshingInput::Image input_image; input_image.camera_id = image.CameraId(); input_image.proj_matrix = image.CamFromWorld().ToMatrix().cast(); input_image.proj_center = image.ProjectionCenter().cast(); images.push_back(input_image); } } const auto& ply_points = ReadPly(JoinPaths(path, "fused.ply")); const std::string vis_path = JoinPaths(path, "fused.ply.vis"); std::fstream vis_file(vis_path, std::ios::in | std::ios::binary); CHECK(vis_file.is_open()) << vis_path; const size_t vis_num_points = ReadBinaryLittleEndian(&vis_file); CHECK_EQ(vis_num_points, ply_points.size()); points.reserve(ply_points.size()); for (const auto& ply_point : ply_points) { const int point_idx = points.size(); DelaunayMeshingInput::Point input_point; input_point.position = Eigen::Vector3f(ply_point.x, ply_point.y, ply_point.z); input_point.num_visible_images = ReadBinaryLittleEndian(&vis_file); for (uint32_t i = 0; i < input_point.num_visible_images; ++i) { const int image_idx = ReadBinaryLittleEndian(&vis_file); images.at(image_idx).point_idxs.push_back(point_idx); } points.push_back(input_point); } } Delaunay CreateDelaunayTriangulation() const { std::vector delaunay_points(points.size()); for (size_t i = 0; i < points.size(); ++i) { delaunay_points[i] = Delaunay::Point(points[i].position.x(), points[i].position.y(), points[i].position.z()); } return Delaunay(delaunay_points.begin(), delaunay_points.end()); } Delaunay CreateSubSampledDelaunayTriangulation( const float max_proj_dist, const float max_depth_dist) const { CHECK_GE(max_proj_dist, 0); if (max_proj_dist == 0) { return CreateDelaunayTriangulation(); } std::vector> points_visible_image_idxs(points.size()); for (size_t image_idx = 0; image_idx < images.size(); ++image_idx) { for (const auto& point_idx : images[image_idx].point_idxs) { points_visible_image_idxs[point_idx].push_back(image_idx); } } std::vector point_idxs(points.size()); std::iota(point_idxs.begin(), point_idxs.end(), 0); Shuffle(point_idxs.size(), &point_idxs); Delaunay triangulation; const float max_squared_proj_dist = max_proj_dist * max_proj_dist; const float min_depth_ratio = 1.0f - max_depth_dist; const float max_depth_ratio = 1.0f + max_depth_dist; for (const auto point_idx : point_idxs) { const auto& point = points[point_idx]; const auto& visible_image_idxs = points_visible_image_idxs[point_idx]; const K::Point_3 point_position = EigenToCGAL(point.position); // Insert point into triangulation until there is one cell. if (triangulation.number_of_vertices() < 4) { triangulation.insert(point_position); continue; } const Delaunay::Cell_handle cell = triangulation.locate(point_position); // If the point is outside the current hull, then extend the hull. if (triangulation.is_infinite(cell)) { triangulation.insert(point_position); continue; } // Project point and located cell vertices to all visible images and // determine reprojection error. bool insert_point = false; for (const auto& image_idx : visible_image_idxs) { const auto& image = images[image_idx]; const auto& camera = cameras.at(image.camera_id); for (int i = 0; i < 4; ++i) { const Eigen::Vector3f cell_point = CGALToEigen(cell->vertex(i)->point()); const Eigen::Vector3f point_local = image.proj_matrix * point.position.homogeneous(); const Eigen::Vector3f cell_point_local = image.proj_matrix * cell_point.homogeneous(); // Ensure that both points are infront of camera. if (point_local.z() <= 0 || cell_point_local.z() <= 0) { insert_point = true; break; } // Check depth ratio between the two points. const float depth_ratio = point_local.z() / cell_point_local.z(); if (depth_ratio < min_depth_ratio || depth_ratio > max_depth_ratio) { insert_point = true; break; } // Check reprojection error between the two points. const Eigen::Vector2f point_proj = camera.ImgFromCam(point_local.hnormalized().cast()) .cast(); const Eigen::Vector2f cell_point_proj = camera.ImgFromCam(cell_point_local.hnormalized().cast()) .cast(); const float squared_proj_dist = (point_proj - cell_point_proj).squaredNorm(); if (squared_proj_dist > max_squared_proj_dist) { insert_point = true; break; } } if (insert_point) { break; } } if (insert_point) { triangulation.insert(point_position); } } LOG(INFO) << StringPrintf("Triangulation has %d using %d points.", triangulation.number_of_vertices(), points.size()); return triangulation; } }; struct DelaunayMeshingEdgeWeightComputer { DelaunayMeshingEdgeWeightComputer(const Delaunay& triangulation, const double visibility_sigma, const double distance_sigma_factor) : visibility_threshold_(5 * visibility_sigma), visibility_normalization_(-0.5 / (visibility_sigma * visibility_sigma)) { std::vector edge_lengths; edge_lengths.reserve(triangulation.number_of_finite_edges()); for (auto it = triangulation.finite_edges_begin(); it != triangulation.finite_edges_end(); ++it) { edge_lengths.push_back((it->first->vertex(it->second)->point() - it->first->vertex(it->third)->point()) .squared_length()); } distance_sigma_ = distance_sigma_factor * std::max(std::sqrt(Percentile(edge_lengths, 25)), 1e-7f); distance_threshold_ = 5 * distance_sigma_; distance_normalization_ = -0.5 / (distance_sigma_ * distance_sigma_); } double DistanceSigma() const { return distance_sigma_; } double ComputeVisibilityProb(const double visibility_squared) const { if (visibility_squared < visibility_threshold_) { return std::max( 0.0, 1.0 - std::exp(visibility_squared * visibility_normalization_)); } else { return 1.0; } } double ComputeDistanceProb(const double distance_squared) const { if (distance_squared < distance_threshold_) { return std::max( 0.0, 1.0 - std::exp(distance_squared * distance_normalization_)); } else { return 1.0; } } private: double visibility_threshold_; double visibility_normalization_; double distance_sigma_; double distance_threshold_; double distance_normalization_; }; // Ray caster through the cells of a Delaunay triangulation. The tracing locates // the cell of the ray origin and then iteratively intersects the ray with all // facets of the current cell and advances to the neighboring cell of the // intersected facet. Note that the ray can also pass through outside of the // hull of the triangulation, i.e. lie within the infinite cells/facets. // The ray caster collects the intersected facets along the ray. struct DelaunayTriangulationRayCaster { struct Intersection { Delaunay::Facet facet; double target_distance_squared = 0.0; }; explicit DelaunayTriangulationRayCaster(const Delaunay& triangulation) : triangulation_(triangulation) { FindHullFacets(); } void CastRaySegment(const K::Segment_3& ray_segment, std::vector* intersections) const { intersections->clear(); Delaunay::Cell_handle next_cell = triangulation_.locate(ray_segment.start()); bool next_cell_found = true; while (next_cell_found) { next_cell_found = false; if (triangulation_.is_infinite(next_cell)) { // Linearly check all hull facets for intersection. for (const auto& hull_facet : hull_facets_) { // Check if the ray origin is infront of the facet. const K::Triangle_3 triangle = triangulation_.triangle(hull_facet); if (CGAL::orientation( triangle[0], triangle[1], triangle[2], ray_segment.start()) == K::Orientation::NEGATIVE) { continue; } // Check if the segment intersects the facet. K::Point_3 intersection_point; if (!CGAL::assign(intersection_point, CGAL::intersection(ray_segment, triangle))) { continue; } // Make sure the next intersection is closer to target than previous. const double target_distance_squared = (intersection_point - ray_segment.end()).squared_length(); if (!intersections->empty() && intersections->back().target_distance_squared < target_distance_squared) { continue; } Intersection intersection; intersection.facet = Delaunay::Facet(hull_facet.first, hull_facet.second); intersection.target_distance_squared = target_distance_squared; intersections->push_back(intersection); next_cell = hull_facet.first->neighbor(hull_facet.second); next_cell_found = true; break; } } else { // Check all neighboring finite facets for intersection. for (int i = 0; i < 4; ++i) { // Check if the ray origin is infront of the facet. const K::Triangle_3 triangle = triangulation_.triangle(next_cell, i); if (CGAL::orientation( triangle[0], triangle[1], triangle[2], ray_segment.start()) == K::Orientation::NEGATIVE) { continue; } // Check if the segment intersects the facet. K::Point_3 intersection_point; if (!CGAL::assign(intersection_point, CGAL::intersection(ray_segment, triangle))) { continue; } // Make sure the next intersection is closer to target than previous. const double target_distance_squared = (intersection_point - ray_segment.end()).squared_length(); if (!intersections->empty() && intersections->back().target_distance_squared < target_distance_squared) { continue; } Intersection intersection; intersection.facet = Delaunay::Facet(next_cell, i); intersection.target_distance_squared = target_distance_squared; intersections->push_back(intersection); next_cell = next_cell->neighbor(i); next_cell_found = true; break; } } } } private: // Find all finite facets of infinite cells. void FindHullFacets() { for (auto it = triangulation_.all_cells_begin(); it != triangulation_.all_cells_end(); ++it) { if (triangulation_.is_infinite(it)) { for (int i = 0; i < 4; ++i) { if (!triangulation_.is_infinite(it, i)) { hull_facets_.emplace_back(it, i); } } } } } const Delaunay& triangulation_; std::vector hull_facets_; }; // Implementation of geometry visualized in Figure 9 in P. Labatut, J‐P. Pons, // and R. Keriven. "Robust and efficient surface reconstruction from range // data." Computer graphics forum, 2009. double ComputeCosFacetCellAngle(const Delaunay& triangulation, const Delaunay::Facet& facet) { if (triangulation.is_infinite(facet.first)) { return 1.0; } const K::Triangle_3 triangle = triangulation.triangle(facet); const K::Vector_3 facet_normal = CGAL::cross_product(triangle[1] - triangle[0], triangle[2] - triangle[0]); const double facet_normal_length_squared = facet_normal.squared_length(); if (facet_normal_length_squared == 0.0) { return 0.5; } const K::Vector_3 co_tangent = facet.first->circumcenter() - triangle[0]; const float co_tangent_length_squared = co_tangent.squared_length(); if (co_tangent_length_squared == 0.0) { return 0.5; } return (facet_normal * co_tangent) / std::sqrt(facet_normal_length_squared * co_tangent_length_squared); } void WriteDelaunayTriangulationPly(const std::string& path, const Delaunay& triangulation) { std::fstream file(path, std::ios::out); CHECK(file.is_open()); file << "ply" << std::endl; file << "format ascii 1.0" << std::endl; file << "element vertex " << triangulation.number_of_vertices() << std::endl; file << "property float x" << std::endl; file << "property float y" << std::endl; file << "property float z" << std::endl; file << "element edge " << triangulation.number_of_finite_edges() << std::endl; file << "property int vertex1" << std::endl; file << "property int vertex2" << std::endl; file << "element face " << triangulation.number_of_finite_facets() << std::endl; file << "property list uchar int vertex_index" << std::endl; file << "end_header" << std::endl; std::unordered_map vertex_indices; vertex_indices.reserve(triangulation.number_of_vertices()); for (auto it = triangulation.finite_vertices_begin(); it != triangulation.finite_vertices_end(); ++it) { vertex_indices.emplace(it, vertex_indices.size()); file << it->point().x() << " " << it->point().y() << " " << it->point().z() << std::endl; } for (auto it = triangulation.finite_edges_begin(); it != triangulation.finite_edges_end(); ++it) { file << vertex_indices.at(it->first->vertex(it->second)) << " " << vertex_indices.at(it->first->vertex(it->third)) << std::endl; } for (auto it = triangulation.finite_facets_begin(); it != triangulation.finite_facets_end(); ++it) { file << "3 " << vertex_indices.at(it->first->vertex( triangulation.vertex_triple_index(it->second, 0))) << " " << vertex_indices.at(it->first->vertex( triangulation.vertex_triple_index(it->second, 1))) << " " << vertex_indices.at(it->first->vertex( triangulation.vertex_triple_index(it->second, 2))) << std::endl; } } struct DelaunayCellData { DelaunayCellData() : DelaunayCellData(-1) {} explicit DelaunayCellData(const int index) : index(index), source_weight(0), sink_weight(0), edge_weights({{0, 0, 0, 0}}) {} int index; float source_weight; float sink_weight; std::array edge_weights; }; PlyMesh DelaunayMeshing(const DelaunayMeshingOptions& options, const DelaunayMeshingInput& input_data) { CHECK(options.Check()); // Create a delaunay triangulation of all input points. LOG(INFO) << "Triangulating points..."; const auto triangulation = input_data.CreateSubSampledDelaunayTriangulation( options.max_proj_dist, options.max_depth_dist); // Helper class to efficiently trace rays through the triangulation. LOG(INFO) << "Initializing ray tracer..."; const DelaunayTriangulationRayCaster ray_caster(triangulation); // Helper class to efficiently compute edge weights in the s-t graph. const DelaunayMeshingEdgeWeightComputer edge_weight_computer( triangulation, options.visibility_sigma, options.distance_sigma_factor); // Initialize the s-t graph with cells as nodes and oriented facets as edges. LOG(INFO) << "Initializing graph optimization..."; typedef std::unordered_map CellGraphData; CellGraphData cell_graph_data; cell_graph_data.reserve(triangulation.number_of_cells()); for (auto it = triangulation.all_cells_begin(); it != triangulation.all_cells_end(); ++it) { cell_graph_data.emplace(it, DelaunayCellData(cell_graph_data.size())); } // Spawn threads for parallelized integration of images. const int num_threads = GetEffectiveNumThreads(options.num_threads); ThreadPool thread_pool(num_threads); JobQueue result_queue(num_threads); // Function that accumulates edge weights in the s-t graph for a single image. auto IntegreateImage = [&](const size_t image_idx) { // Accumulated weights for the current image only. CellGraphData image_cell_graph_data; // Image that is integrated into s-t graph. const auto& image = input_data.images[image_idx]; const K::Point_3 image_position = EigenToCGAL(image.proj_center); // Intersections between viewing rays and Delaunay triangulation. std::vector intersections; // Iterate through all image observations and integrate them into the graph. for (const auto& point_idx : image.point_idxs) { const auto& point = input_data.points[point_idx]; // Likelihood of the point observation. const double alpha = edge_weight_computer.ComputeVisibilityProb( point.num_visible_images * point.num_visible_images); const K::Point_3 point_position = EigenToCGAL(point.position); const K::Ray_3 viewing_ray = K::Ray_3(image_position, point_position); const K::Vector_3 viewing_direction = point_position - image_position; const K::Vector_3 viewing_direction_normalized = viewing_direction / std::sqrt(viewing_direction.squared_length()); const K::Vector_3 viewing_direction_epsilon = 0.001 * edge_weight_computer.DistanceSigma() * viewing_direction_normalized; // Find intersected facets between image and point. ray_caster.CastRaySegment( K::Segment_3(image_position, point_position - viewing_direction_epsilon), &intersections); // Accumulate source weights for cell containing image. if (!intersections.empty()) { image_cell_graph_data[intersections.front().facet.first] .source_weight += alpha; } // Accumulate edge weights from image to point. for (const auto& intersection : intersections) { image_cell_graph_data[intersection.facet.first] .edge_weights[intersection.facet.second] += alpha * edge_weight_computer.ComputeDistanceProb( intersection.target_distance_squared); } // Accumulate edge weights from point to extended point // and accumulate sink weight of the cell inside the surface. { // Find the first facet that is intersected by the extended ray behind // the observed point. Then accumulate the edge weight of that facet // and accumulate the sink weight of the cell behind that facet. const Delaunay::Cell_handle behind_point_cell = triangulation.locate(point_position + viewing_direction_epsilon); int behind_neighbor_idx = -1; double behind_distance_squared = 0.0; for (int neighbor_idx = 0; neighbor_idx < 4; ++neighbor_idx) { const K::Triangle_3 triangle = triangulation.triangle(behind_point_cell, neighbor_idx); K::Point_3 inter_point; if (CGAL::assign(inter_point, CGAL::intersection(viewing_ray, triangle))) { const double distance_squared = (inter_point - point_position).squared_length(); if (distance_squared > behind_distance_squared) { behind_distance_squared = distance_squared; behind_neighbor_idx = neighbor_idx; } } } if (behind_neighbor_idx >= 0) { image_cell_graph_data[behind_point_cell] .edge_weights[behind_neighbor_idx] += alpha * edge_weight_computer.ComputeDistanceProb(behind_distance_squared); const auto& inside_cell = behind_point_cell->neighbor(behind_neighbor_idx); image_cell_graph_data[inside_cell].sink_weight += alpha; } } } CHECK(result_queue.Push(std::move(image_cell_graph_data))); }; // Add first batch of images to the thread job queue. size_t image_idx = 0; const size_t init_num_tasks = std::min(input_data.images.size(), 2 * thread_pool.NumThreads()); for (; image_idx < init_num_tasks; ++image_idx) { thread_pool.AddTask(IntegreateImage, image_idx); } // Pop the integrated images from the thread job queue and integrate their // accumulated weights into the global graph. for (size_t i = 0; i < input_data.images.size(); ++i) { Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Integrating image [%d/%d]", i + 1, input_data.images.size()) << std::flush; // Push the next image to the queue. if (image_idx < input_data.images.size()) { thread_pool.AddTask(IntegreateImage, image_idx); image_idx += 1; } // Pop the next results from the queue. auto result = result_queue.Pop(); CHECK(result.IsValid()); // Accumulate the weights of the image into the global graph. const auto& image_cell_graph_data = result.Data(); for (const auto& image_cell_data : image_cell_graph_data) { auto& cell_data = cell_graph_data.at(image_cell_data.first); cell_data.sink_weight += image_cell_data.second.sink_weight; cell_data.source_weight += image_cell_data.second.source_weight; for (size_t j = 0; j < cell_data.edge_weights.size(); ++j) { cell_data.edge_weights[j] += image_cell_data.second.edge_weights[j]; } } LOG(INFO) << StringPrintf(" in %.3fs", timer.ElapsedSeconds()); } // Setup the min-cut (max-flow) graph optimization. LOG(INFO) << "Setting up optimization..."; // Each oriented facet in the Delaunay triangulation corresponds to a directed // edge and each cell corresponds to a node in the graph. MinSTGraphCut graph_cut(cell_graph_data.size()); // Iterate all cells in the triangulation. for (auto& cell_data : cell_graph_data) { graph_cut.AddNode(cell_data.second.index, cell_data.second.source_weight, cell_data.second.sink_weight); // Iterate all facets of the current cell to accumulate edge weight. for (int i = 0; i < 4; ++i) { // Compose the current facet. const Delaunay::Facet facet = std::make_pair(cell_data.first, i); // Extract the mirrored facet of the current cell (opposite orientation). const Delaunay::Facet mirror_facet = triangulation.mirror_facet(facet); const auto& mirror_cell_data = cell_graph_data.at(mirror_facet.first); // Avoid duplicate edges in graph. if (cell_data.second.index < mirror_cell_data.index) { continue; } // Implementation of geometry visualized in Figure 9 in P. Labatut, J‐P. // Pons, and R. Keriven. "Robust and efficient surface reconstruction from // range data." Computer graphics forum, 2009. const double edge_shape_weight = options.quality_regularization * (1.0 - std::min(ComputeCosFacetCellAngle(triangulation, facet), ComputeCosFacetCellAngle(triangulation, mirror_facet))); const float forward_edge_weight = cell_data.second.edge_weights[facet.second] + edge_shape_weight; const float backward_edge_weight = mirror_cell_data.edge_weights[mirror_facet.second] + edge_shape_weight; graph_cut.AddEdge(cell_data.second.index, mirror_cell_data.index, forward_edge_weight, backward_edge_weight); } } // Extract the surface facets as the oriented min-cut of the graph. LOG(INFO) << "Running graph-cut optimization..."; graph_cut.Compute(); LOG(INFO) << "Extracting surface as min-cut..."; std::unordered_set surface_vertices; std::vector surface_facets; std::vector surface_facet_side_lengths; for (auto it = triangulation.finite_facets_begin(); it != triangulation.finite_facets_end(); ++it) { const auto& cell_data = cell_graph_data.at(it->first); const auto& mirror_cell_data = cell_graph_data.at(it->first->neighbor(it->second)); // Obtain labeling after the graph-cut. const bool cell_is_source = graph_cut.IsConnectedToSource(cell_data.index); const bool mirror_cell_is_source = graph_cut.IsConnectedToSource(mirror_cell_data.index); // The surface is equal to the location of the cut, which is at the // transition between source and sink nodes. if (cell_is_source == mirror_cell_is_source) { continue; } // Remember all unique vertices of the surface mesh. for (int i = 0; i < 3; ++i) { const auto& vertex = it->first->vertex(triangulation.vertex_triple_index(it->second, i)); surface_vertices.insert(vertex); } // Determine maximum side length of facet. const K::Triangle_3 triangle = triangulation.triangle(*it); const float max_squared_side_length = std::max({(triangle[0] - triangle[1]).squared_length(), (triangle[0] - triangle[2]).squared_length(), (triangle[1] - triangle[2]).squared_length()}); surface_facet_side_lengths.push_back(std::sqrt(max_squared_side_length)); // Remember surface mesh facet and make sure it is oriented correctly. if (cell_is_source) { surface_facets.push_back(*it); } else { surface_facets.push_back(triangulation.mirror_facet(*it)); } } LOG(INFO) << "Creating surface mesh model..."; PlyMesh mesh; std::unordered_map surface_vertex_indices; surface_vertex_indices.reserve(surface_vertices.size()); mesh.vertices.reserve(surface_vertices.size()); for (const auto& vertex : surface_vertices) { mesh.vertices.emplace_back( vertex->point().x(), vertex->point().y(), vertex->point().z()); surface_vertex_indices.emplace(vertex, surface_vertex_indices.size()); } const float max_facet_side_length = options.max_side_length_factor * Percentile(surface_facet_side_lengths, options.max_side_length_percentile); mesh.faces.reserve(surface_facets.size()); for (size_t i = 0; i < surface_facets.size(); ++i) { // Note that skipping some of the facets here means that there will be // some unused vertices in the final mesh. if (surface_facet_side_lengths[i] > max_facet_side_length) { continue; } const auto& facet = surface_facets[i]; mesh.faces.emplace_back( surface_vertex_indices.at(facet.first->vertex( triangulation.vertex_triple_index(facet.second, 0))), surface_vertex_indices.at(facet.first->vertex( triangulation.vertex_triple_index(facet.second, 1))), surface_vertex_indices.at(facet.first->vertex( triangulation.vertex_triple_index(facet.second, 2)))); } return mesh; } void SparseDelaunayMeshing(const DelaunayMeshingOptions& options, const std::string& input_path, const std::string& output_path) { Timer timer; timer.Start(); DelaunayMeshingInput input_data; input_data.ReadSparseReconstruction(input_path); const auto mesh = DelaunayMeshing(options, input_data); LOG(INFO) << "Writing surface mesh..."; WriteBinaryPlyMesh(output_path, mesh); timer.PrintSeconds(); } void DenseDelaunayMeshing(const DelaunayMeshingOptions& options, const std::string& input_path, const std::string& output_path) { Timer timer; timer.Start(); DelaunayMeshingInput input_data; input_data.ReadDenseReconstruction(input_path); const auto mesh = DelaunayMeshing(options, input_data); LOG(INFO) << "Writing surface mesh..."; WriteBinaryPlyMesh(output_path, mesh); timer.PrintSeconds(); } #endif // COLMAP_CGAL_ENABLED } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/meshing.h000066400000000000000000000131061454702036400174570ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { namespace mvs { struct PoissonMeshingOptions { // This floating point value specifies the importance that interpolation of // the point samples is given in the formulation of the screened Poisson // equation. The results of the original (unscreened) Poisson Reconstruction // can be obtained by setting this value to 0. double point_weight = 1.0; // This integer is the maximum depth of the tree that will be used for surface // reconstruction. Running at depth d corresponds to solving on a voxel grid // whose resolution is no larger than 2^d x 2^d x 2^d. Note that since the // reconstructor adapts the octree to the sampling density, the specified // reconstruction depth is only an upper bound. int depth = 13; // If specified, the reconstruction code assumes that the input is equipped // with colors and will extrapolate the color values to the vertices of the // reconstructed mesh. The floating point value specifies the relative // importance of finer color estimates over lower ones. double color = 32.0; // This floating point values specifies the value for mesh trimming. The // subset of the mesh with signal value less than the trim value is discarded. double trim = 10.0; // The number of threads used for the Poisson reconstruction. int num_threads = -1; bool Check() const; }; struct DelaunayMeshingOptions { // Unify input points into one cell in the Delaunay triangulation that fall // within a reprojected radius of the given pixels. double max_proj_dist = 20.0; // Maximum relative depth difference between input point and a vertex of an // existing cell in the Delaunay triangulation, otherwise a new vertex is // created in the triangulation. double max_depth_dist = 0.05; // The standard deviation of wrt. the number of images seen by each point. // Increasing this value decreases the influence of points seen in few images. double visibility_sigma = 3.0; // The factor that is applied to the computed distance sigma, which is // automatically computed as the 25th percentile of edge lengths. A higher // value will increase the smoothness of the surface. double distance_sigma_factor = 1.0; // A higher quality regularization leads to a smoother surface. double quality_regularization = 1.0; // Filtering thresholds for outlier surface mesh faces. If the longest side of // a mesh face (longest out of 3) exceeds the side lengths of all faces at a // certain percentile by the given factor, then it is considered an outlier // mesh face and discarded. double max_side_length_factor = 25.0; double max_side_length_percentile = 95.0; // The number of threads to use for reconstruction. Default is all threads. int num_threads = -1; bool Check() const; }; // Perform Poisson surface reconstruction and return true if successful. bool PoissonMeshing(const PoissonMeshingOptions& options, const std::string& input_path, const std::string& output_path); #if defined(COLMAP_CGAL_ENABLED) // Delaunay meshing of sparse and dense COLMAP reconstructions. This is an // implementation of the approach described in: // // P. Labatut, J‐P. Pons, and R. Keriven. "Robust and efficient surface // reconstruction from range data". Computer graphics forum, 2009. // // In case of sparse input, the path should point to a sparse COLMAP // reconstruction. In case of dense input, the path should point to a dense // COLMAP workspace folder, which has been fully processed by the stereo and // fusion pipeline. void SparseDelaunayMeshing(const DelaunayMeshingOptions& options, const std::string& input_path, const std::string& output_path); void DenseDelaunayMeshing(const DelaunayMeshingOptions& options, const std::string& input_path, const std::string& output_path); #endif // COLMAP_CGAL_ENABLED } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/model.cc000066400000000000000000000352351454702036400172720ustar00rootroot00000000000000// 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. #include "colmap/mvs/model.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/triangulation.h" #include "colmap/scene/projection.h" #include "colmap/scene/reconstruction.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" namespace colmap { namespace mvs { void Model::Read(const std::string& path, const std::string& format) { auto format_lower_case = format; StringToLower(&format_lower_case); if (format_lower_case == "colmap") { ReadFromCOLMAP(path); } else if (format_lower_case == "pmvs") { ReadFromPMVS(path); } else { LOG(FATAL) << "Invalid input format"; } } void Model::ReadFromCOLMAP(const std::string& path, const std::string& sparse_path, const std::string& images_path) { Reconstruction reconstruction; reconstruction.Read(JoinPaths(path, sparse_path)); images.reserve(reconstruction.NumRegImages()); std::unordered_map image_id_to_idx; for (size_t i = 0; i < reconstruction.NumRegImages(); ++i) { const auto image_id = reconstruction.RegImageIds()[i]; const auto& image = reconstruction.Image(image_id); const auto& camera = reconstruction.Camera(image.CameraId()); const std::string image_path = JoinPaths(path, images_path, image.Name()); const Eigen::Matrix K = camera.CalibrationMatrix().cast(); const Eigen::Matrix R = image.CamFromWorld().rotation.toRotationMatrix().cast(); const Eigen::Vector3f T = image.CamFromWorld().translation.cast(); images.emplace_back( image_path, camera.width, camera.height, K.data(), R.data(), T.data()); image_id_to_idx.emplace(image_id, i); image_names_.push_back(image.Name()); image_name_to_idx_.emplace(image.Name(), i); } points.reserve(reconstruction.NumPoints3D()); for (const auto& point3D : reconstruction.Points3D()) { Point point; point.x = point3D.second.xyz(0); point.y = point3D.second.xyz(1); point.z = point3D.second.xyz(2); point.track.reserve(point3D.second.track.Length()); for (const auto& track_el : point3D.second.track.Elements()) { point.track.push_back(image_id_to_idx.at(track_el.image_id)); } points.push_back(point); } } void Model::ReadFromPMVS(const std::string& path) { if (ReadFromBundlerPMVS(path) || ReadFromRawPMVS(path)) { return; } else { LOG(FATAL) << "Invalid PMVS format"; } } int Model::GetImageIdx(const std::string& name) const { CHECK_GT(image_name_to_idx_.count(name), 0) << "Image with name `" << name << "` does not exist"; return image_name_to_idx_.at(name); } std::string Model::GetImageName(const int image_idx) const { CHECK_GE(image_idx, 0); CHECK_LT(image_idx, image_names_.size()); return image_names_.at(image_idx); } std::vector> Model::GetMaxOverlappingImages( const size_t num_images, const double min_triangulation_angle) const { std::vector> overlapping_images(images.size()); const float min_triangulation_angle_rad = DegToRad(min_triangulation_angle); const auto shared_num_points = ComputeSharedPoints(); const float kTriangulationAnglePercentile = 75; const auto triangulation_angles = ComputeTriangulationAngles(kTriangulationAnglePercentile); for (size_t image_idx = 0; image_idx < images.size(); ++image_idx) { const auto& shared_images = shared_num_points.at(image_idx); const auto& overlapping_triangulation_angles = triangulation_angles.at(image_idx); std::vector> ordered_images; ordered_images.reserve(shared_images.size()); for (const auto& image : shared_images) { if (overlapping_triangulation_angles.at(image.first) >= min_triangulation_angle_rad) { ordered_images.emplace_back(image.first, image.second); } } const size_t eff_num_images = std::min(ordered_images.size(), num_images); if (eff_num_images < shared_images.size()) { std::partial_sort(ordered_images.begin(), ordered_images.begin() + eff_num_images, ordered_images.end(), [](const std::pair image1, const std::pair image2) { return image1.second > image2.second; }); } else { std::sort(ordered_images.begin(), ordered_images.end(), [](const std::pair image1, const std::pair image2) { return image1.second > image2.second; }); } overlapping_images[image_idx].reserve(eff_num_images); for (size_t i = 0; i < eff_num_images; ++i) { overlapping_images[image_idx].push_back(ordered_images[i].first); } } return overlapping_images; } const std::vector>& Model::GetMaxOverlappingImagesFromPMVS() const { return pmvs_vis_dat_; } std::vector> Model::ComputeDepthRanges() const { std::vector> depths(images.size()); for (const auto& point : points) { const Eigen::Vector3f X(point.x, point.y, point.z); for (const auto& image_idx : point.track) { const auto& image = images.at(image_idx); const float depth = Eigen::Map(&image.GetR()[6]).dot(X) + image.GetT()[2]; if (depth > 0) { depths[image_idx].push_back(depth); } } } std::vector> depth_ranges(depths.size()); for (size_t image_idx = 0; image_idx < depth_ranges.size(); ++image_idx) { auto& depth_range = depth_ranges[image_idx]; auto& image_depths = depths[image_idx]; if (image_depths.empty()) { depth_range.first = -1.0f; depth_range.second = -1.0f; continue; } std::sort(image_depths.begin(), image_depths.end()); const float kMinPercentile = 0.01f; const float kMaxPercentile = 0.99f; depth_range.first = image_depths[image_depths.size() * kMinPercentile]; depth_range.second = image_depths[image_depths.size() * kMaxPercentile]; const float kStretchRatio = 0.25f; depth_range.first *= (1.0f - kStretchRatio); depth_range.second *= (1.0f + kStretchRatio); } return depth_ranges; } std::vector> Model::ComputeSharedPoints() const { std::vector> shared_points(images.size()); for (const auto& point : points) { for (size_t i = 0; i < point.track.size(); ++i) { const int image_idx1 = point.track[i]; for (size_t j = 0; j < i; ++j) { const int image_idx2 = point.track[j]; if (image_idx1 != image_idx2) { shared_points.at(image_idx1)[image_idx2] += 1; shared_points.at(image_idx2)[image_idx1] += 1; } } } } return shared_points; } std::vector> Model::ComputeTriangulationAngles( const float percentile) const { std::vector proj_centers(images.size()); for (size_t image_idx = 0; image_idx < images.size(); ++image_idx) { const auto& image = images[image_idx]; Eigen::Vector3f C; ComputeProjectionCenter(image.GetR(), image.GetT(), C.data()); proj_centers[image_idx] = C.cast(); } std::vector>> all_triangulation_angles( images.size()); for (const auto& point : points) { for (size_t i = 0; i < point.track.size(); ++i) { const int image_idx1 = point.track[i]; for (size_t j = 0; j < i; ++j) { const int image_idx2 = point.track[j]; if (image_idx1 != image_idx2) { const float angle = CalculateTriangulationAngle( proj_centers.at(image_idx1), proj_centers.at(image_idx2), Eigen::Vector3d(point.x, point.y, point.z)); all_triangulation_angles.at(image_idx1)[image_idx2].push_back(angle); all_triangulation_angles.at(image_idx2)[image_idx1].push_back(angle); } } } } std::vector> triangulation_angles(images.size()); for (size_t image_idx = 0; image_idx < all_triangulation_angles.size(); ++image_idx) { const auto& overlapping_images = all_triangulation_angles[image_idx]; for (const auto& image : overlapping_images) { triangulation_angles[image_idx].emplace( image.first, Percentile(image.second, percentile)); } } return triangulation_angles; } bool Model::ReadFromBundlerPMVS(const std::string& path) { const std::string bundle_file_path = JoinPaths(path, "bundle.rd.out"); if (!ExistsFile(bundle_file_path)) { return false; } std::ifstream file(bundle_file_path); CHECK(file.is_open()) << bundle_file_path; // Header line. std::string header; std::getline(file, header); int num_images, num_points; file >> num_images >> num_points; images.reserve(num_images); for (int image_idx = 0; image_idx < num_images; ++image_idx) { const std::string image_name = StringPrintf("%08d.jpg", image_idx); const std::string image_path = JoinPaths(path, "visualize", image_name); float K[9] = {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}; file >> K[0]; K[4] = K[0]; Bitmap bitmap; CHECK(bitmap.Read(image_path)); K[2] = bitmap.Width() / 2.0f; K[5] = bitmap.Height() / 2.0f; float k1, k2; file >> k1 >> k2; CHECK_EQ(k1, 0.0f); CHECK_EQ(k2, 0.0f); float R[9]; for (size_t i = 0; i < 9; ++i) { file >> R[i]; } for (size_t i = 3; i < 9; ++i) { R[i] = -R[i]; } float T[3]; file >> T[0] >> T[1] >> T[2]; T[1] = -T[1]; T[2] = -T[2]; images.emplace_back(image_path, bitmap.Width(), bitmap.Height(), K, R, T); image_names_.push_back(image_name); image_name_to_idx_.emplace(image_name, image_idx); } points.resize(num_points); for (int point_id = 0; point_id < num_points; ++point_id) { auto& point = points[point_id]; file >> point.x >> point.y >> point.z; int color[3]; file >> color[0] >> color[1] >> color[2]; int track_len; file >> track_len; point.track.resize(track_len); for (int i = 0; i < track_len; ++i) { int feature_idx; float imx, imy; file >> point.track[i] >> feature_idx >> imx >> imy; CHECK_LT(point.track[i], images.size()); } } return true; } bool Model::ReadFromRawPMVS(const std::string& path) { const std::string vis_dat_path = JoinPaths(path, "vis.dat"); if (!ExistsFile(vis_dat_path)) { return false; } for (int image_idx = 0;; ++image_idx) { const std::string image_name = StringPrintf("%08d.jpg", image_idx); const std::string image_path = JoinPaths(path, "visualize", image_name); if (!ExistsFile(image_path)) { break; } Bitmap bitmap; CHECK(bitmap.Read(image_path)); const std::string proj_matrix_path = JoinPaths(path, "txt", StringPrintf("%08d.txt", image_idx)); std::ifstream proj_matrix_file(proj_matrix_path); CHECK(proj_matrix_file.is_open()) << proj_matrix_path; std::string contour; proj_matrix_file >> contour; CHECK_EQ(contour, "CONTOUR"); Eigen::Matrix3x4d P; for (int i = 0; i < 3; ++i) { proj_matrix_file >> P(i, 0) >> P(i, 1) >> P(i, 2) >> P(i, 3); } Eigen::Matrix3d K; Eigen::Matrix3d R; Eigen::Vector3d T; DecomposeProjectionMatrix(P, &K, &R, &T); // The COLMAP patch match algorithm requires that there is no skew. K(0, 1) = 0.0f; K(1, 0) = 0.0f; K(2, 0) = 0.0f; K(2, 1) = 0.0f; K(2, 2) = 1.0f; const Eigen::Matrix K_float = K.cast(); const Eigen::Matrix R_float = R.cast(); const Eigen::Vector3f T_float = T.cast(); images.emplace_back(image_path, bitmap.Width(), bitmap.Height(), K_float.data(), R_float.data(), T_float.data()); image_names_.push_back(image_name); image_name_to_idx_.emplace(image_name, image_idx); } std::ifstream vis_dat_file(vis_dat_path); CHECK(vis_dat_file.is_open()) << vis_dat_path; std::string visdata; vis_dat_file >> visdata; CHECK_EQ(visdata, "VISDATA"); int num_images; vis_dat_file >> num_images; CHECK_GE(num_images, 0); CHECK_EQ(num_images, images.size()); pmvs_vis_dat_.resize(num_images); for (int i = 0; i < num_images; ++i) { int image_idx; vis_dat_file >> image_idx; CHECK_GE(image_idx, 0); CHECK_LT(image_idx, num_images); int num_visible_images; vis_dat_file >> num_visible_images; auto& visible_image_idxs = pmvs_vis_dat_[image_idx]; visible_image_idxs.reserve(num_visible_images); for (int j = 0; j < num_visible_images; ++j) { int visible_image_idx; vis_dat_file >> visible_image_idx; CHECK_GE(visible_image_idx, 0); CHECK_LT(visible_image_idx, num_images); if (visible_image_idx != image_idx) { visible_image_idxs.push_back(visible_image_idx); } } } return true; } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/model.h000066400000000000000000000101561454702036400171270ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/depth_map.h" #include "colmap/mvs/image.h" #include "colmap/mvs/normal_map.h" #include #include #include #include #include #include #include namespace colmap { namespace mvs { // Simple sparse model class. struct Model { struct Point { float x = 0; float y = 0; float z = 0; std::vector track; }; // Read the model from different data formats. void Read(const std::string& path, const std::string& format); void ReadFromCOLMAP(const std::string& path, const std::string& sparse_path = "sparse", const std::string& images_path = "images"); void ReadFromPMVS(const std::string& path); // Get the image index for the given image name. int GetImageIdx(const std::string& name) const; std::string GetImageName(int image_idx) const; // For each image, determine the maximally overlapping images, sorted based on // the number of shared points subject to a minimum robust average // triangulation angle of the points. std::vector> GetMaxOverlappingImages( size_t num_images, double min_triangulation_angle) const; // Get the overlapping images defined in the vis.dat file. const std::vector>& GetMaxOverlappingImagesFromPMVS() const; // Compute the robust minimum and maximum depths from the sparse point cloud. std::vector> ComputeDepthRanges() const; // Compute the number of shared points between all overlapping images. std::vector> ComputeSharedPoints() const; // Compute the median triangulation angles between all overlapping images. std::vector> ComputeTriangulationAngles( float percentile = 50) const; // Note that in case the data is read from a COLMAP reconstruction, the index // of an image or point does not correspond to its original identifier in the // reconstruction, but it corresponds to the position in the // images.bin/points3D.bin files. This is mainly done for more efficient // access to the data, which is required during the stereo fusion stage. std::vector images; std::vector points; private: bool ReadFromBundlerPMVS(const std::string& path); bool ReadFromRawPMVS(const std::string& path); std::vector image_names_; std::unordered_map image_name_to_idx_; std::vector> pmvs_vis_dat_; }; } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/normal_map.cc000066400000000000000000000103631454702036400203120ustar00rootroot00000000000000// 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. #include "colmap/mvs/normal_map.h" #include "colmap/image/warp.h" namespace colmap { namespace mvs { NormalMap::NormalMap() : Mat(0, 0, 3) {} NormalMap::NormalMap(const size_t width, const size_t height) : Mat(width, height, 3) {} NormalMap::NormalMap(const Mat& mat) : Mat(mat.GetWidth(), mat.GetHeight(), mat.GetDepth()) { CHECK_EQ(mat.GetDepth(), 3); data_ = mat.GetData(); } void NormalMap::Rescale(const float factor) { if (width_ * height_ == 0) { return; } const size_t new_width = std::round(width_ * factor); const size_t new_height = std::round(height_ * factor); std::vector new_data(new_width * new_height * 3); // Resample the normal map. for (size_t d = 0; d < 3; ++d) { const size_t offset = d * width_ * height_; const size_t new_offset = d * new_width * new_height; DownsampleImage(data_.data() + offset, height_, width_, new_height, new_width, new_data.data() + new_offset); } data_ = new_data; width_ = new_width; height_ = new_height; data_.shrink_to_fit(); // Re-normalize the normal vectors. for (size_t r = 0; r < height_; ++r) { for (size_t c = 0; c < width_; ++c) { Eigen::Vector3f normal(Get(r, c, 0), Get(r, c, 1), Get(r, c, 2)); const float squared_norm = normal.squaredNorm(); if (squared_norm > 0) { normal /= std::sqrt(squared_norm); } Set(r, c, 0, normal(0)); Set(r, c, 1, normal(1)); Set(r, c, 2, normal(2)); } } } void NormalMap::Downsize(const size_t max_width, const size_t max_height) { if (height_ <= max_height && width_ <= max_width) { return; } const float factor_x = static_cast(max_width) / width_; const float factor_y = static_cast(max_height) / height_; Rescale(std::min(factor_x, factor_y)); } Bitmap NormalMap::ToBitmap() const { CHECK_GT(width_, 0); CHECK_GT(height_, 0); CHECK_EQ(depth_, 3); Bitmap bitmap; bitmap.Allocate(width_, height_, true); for (size_t y = 0; y < height_; ++y) { for (size_t x = 0; x < width_; ++x) { float normal[3] = {0}; GetSlice(y, x, normal); if (normal[0] != 0 || normal[1] != 0 || normal[2] != 0) { const BitmapColor color(127.5f * (-normal[0] + 1), 127.5f * (-normal[1] + 1), -255.0f * normal[2]); bitmap.SetPixel(x, y, color.Cast()); } else { bitmap.SetPixel(x, y, BitmapColor(0)); } } } return bitmap; } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/normal_map.h000066400000000000000000000041571454702036400201600ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/mat.h" #include "colmap/sensor/bitmap.h" #include #include namespace colmap { namespace mvs { // Normal map class that stores per-pixel normals as a MxNx3 image. class NormalMap : public Mat { public: NormalMap(); NormalMap(size_t width, size_t height); explicit NormalMap(const Mat& mat); void Rescale(float factor); void Downsize(size_t max_width, size_t max_height); Bitmap ToBitmap() const; }; } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/normal_map_test.cc000066400000000000000000000070601454702036400213510ustar00rootroot00000000000000// 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. #include "colmap/mvs/normal_map.h" #include namespace colmap { namespace mvs { namespace { TEST(NormalMap, Empty) { NormalMap normal_map; EXPECT_EQ(normal_map.GetWidth(), 0); EXPECT_EQ(normal_map.GetHeight(), 0); EXPECT_EQ(normal_map.GetDepth(), 3); } TEST(NormalMap, NonEmpty) { NormalMap normal_map(1, 2); EXPECT_EQ(normal_map.GetWidth(), 1); EXPECT_EQ(normal_map.GetHeight(), 2); EXPECT_EQ(normal_map.GetDepth(), 3); } TEST(NormalMap, Rescale) { NormalMap normal_map(6, 7); normal_map.Rescale(0.5); EXPECT_EQ(normal_map.GetWidth(), 3); EXPECT_EQ(normal_map.GetHeight(), 4); EXPECT_EQ(normal_map.GetDepth(), 3); } TEST(NormalMap, Downsize) { NormalMap normal_map(6, 7); normal_map.Downsize(2, 4); EXPECT_EQ(normal_map.GetWidth(), 2); EXPECT_EQ(normal_map.GetHeight(), 2); EXPECT_EQ(normal_map.GetDepth(), 3); } TEST(NormalMap, ToBitmap) { NormalMap normal_map(2, 2); normal_map.Set(0, 0, 0, 0); normal_map.Set(0, 0, 1, 0); normal_map.Set(0, 0, 2, 1); normal_map.Set(0, 1, 0, 0); normal_map.Set(0, 1, 1, 1); normal_map.Set(0, 1, 2, 0); normal_map.Set(1, 0, 0, 1); normal_map.Set(1, 0, 1, 0); normal_map.Set(1, 0, 2, 0); normal_map.Set(1, 1, 0, 1 / std::sqrt(2.0f)); normal_map.Set(1, 1, 1, 1 / std::sqrt(2.0f)); normal_map.Set(1, 1, 2, 0); const Bitmap bitmap = normal_map.ToBitmap(); EXPECT_EQ(bitmap.Width(), normal_map.GetWidth()); EXPECT_EQ(bitmap.Height(), normal_map.GetHeight()); EXPECT_TRUE(bitmap.IsRGB()); BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(0, 0, &color)); EXPECT_EQ(color, BitmapColor(128, 128, 0)); EXPECT_TRUE(bitmap.GetPixel(0, 1, &color)); EXPECT_EQ(color, BitmapColor(0, 128, 0)); EXPECT_TRUE(bitmap.GetPixel(1, 0, &color)); EXPECT_EQ(color, BitmapColor(128, 0, 0)); EXPECT_TRUE(bitmap.GetPixel(1, 1, &color)); EXPECT_EQ(color, BitmapColor(37, 37, 0)); } } // namespace } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/patch_match.cc000066400000000000000000000467551454702036400204560ustar00rootroot00000000000000// 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. #include "colmap/mvs/patch_match.h" #include "colmap/math/math.h" #include "colmap/mvs/consistency_graph.h" #include "colmap/mvs/patch_match_cuda.h" #include "colmap/mvs/workspace.h" #include "colmap/util/misc.h" #include #include #define PrintOption(option) LOG(INFO) << #option ": " << option << std::endl namespace colmap { namespace mvs { PatchMatch::PatchMatch(const PatchMatchOptions& options, const Problem& problem) : options_(options), problem_(problem) {} PatchMatch::~PatchMatch() {} void PatchMatchOptions::Print() const { PrintHeading2("PatchMatchOptions"); PrintOption(max_image_size); PrintOption(gpu_index); PrintOption(depth_min); PrintOption(depth_max); PrintOption(window_radius); PrintOption(window_step); PrintOption(sigma_spatial); PrintOption(sigma_color); PrintOption(num_samples); PrintOption(ncc_sigma); PrintOption(min_triangulation_angle); PrintOption(incident_angle_sigma); PrintOption(num_iterations); PrintOption(geom_consistency); PrintOption(geom_consistency_regularizer); PrintOption(geom_consistency_max_cost); PrintOption(filter); PrintOption(filter_min_ncc); PrintOption(filter_min_triangulation_angle); PrintOption(filter_min_num_consistent); PrintOption(filter_geom_consistency_max_cost); PrintOption(write_consistency_graph); PrintOption(allow_missing_files); } void PatchMatch::Problem::Print() const { PrintHeading2("PatchMatch::Problem"); PrintOption(ref_image_idx); LOG(INFO) << "src_image_idxs: "; if (!src_image_idxs.empty()) { for (size_t i = 0; i < src_image_idxs.size() - 1; ++i) { LOG(INFO) << src_image_idxs[i] << " "; } LOG(INFO) << src_image_idxs.back(); } else { } } void PatchMatch::Check() const { CHECK(options_.Check()); CHECK(!options_.gpu_index.empty()); const std::vector gpu_indices = CSVToVector(options_.gpu_index); CHECK_EQ(gpu_indices.size(), 1); CHECK_GE(gpu_indices[0], -1); CHECK_NOTNULL(problem_.images); if (options_.geom_consistency) { CHECK_NOTNULL(problem_.depth_maps); CHECK_NOTNULL(problem_.normal_maps); CHECK_EQ(problem_.depth_maps->size(), problem_.images->size()); CHECK_EQ(problem_.normal_maps->size(), problem_.images->size()); } CHECK_GT(problem_.src_image_idxs.size(), 0); // Check that there are no duplicate images and that the reference image // is not defined as a source image. std::set unique_image_idxs(problem_.src_image_idxs.begin(), problem_.src_image_idxs.end()); unique_image_idxs.insert(problem_.ref_image_idx); CHECK_EQ(problem_.src_image_idxs.size() + 1, unique_image_idxs.size()); // Check that input data is well-formed. for (const int image_idx : unique_image_idxs) { CHECK_GE(image_idx, 0) << image_idx; CHECK_LT(image_idx, problem_.images->size()) << image_idx; const Image& image = problem_.images->at(image_idx); CHECK_GT(image.GetBitmap().Width(), 0) << image_idx; CHECK_GT(image.GetBitmap().Height(), 0) << image_idx; CHECK(image.GetBitmap().IsGrey()) << image_idx; CHECK_EQ(image.GetWidth(), image.GetBitmap().Width()) << image_idx; CHECK_EQ(image.GetHeight(), image.GetBitmap().Height()) << image_idx; // Make sure, the calibration matrix only contains fx, fy, cx, cy. CHECK_LT(std::abs(image.GetK()[1] - 0.0f), 1e-6f) << image_idx; CHECK_LT(std::abs(image.GetK()[3] - 0.0f), 1e-6f) << image_idx; CHECK_LT(std::abs(image.GetK()[6] - 0.0f), 1e-6f) << image_idx; CHECK_LT(std::abs(image.GetK()[7] - 0.0f), 1e-6f) << image_idx; CHECK_LT(std::abs(image.GetK()[8] - 1.0f), 1e-6f) << image_idx; if (options_.geom_consistency) { CHECK_LT(image_idx, problem_.depth_maps->size()) << image_idx; const DepthMap& depth_map = problem_.depth_maps->at(image_idx); CHECK_EQ(image.GetWidth(), depth_map.GetWidth()) << image_idx; CHECK_EQ(image.GetHeight(), depth_map.GetHeight()) << image_idx; } } if (options_.geom_consistency) { const Image& ref_image = problem_.images->at(problem_.ref_image_idx); const NormalMap& ref_normal_map = problem_.normal_maps->at(problem_.ref_image_idx); CHECK_EQ(ref_image.GetWidth(), ref_normal_map.GetWidth()); CHECK_EQ(ref_image.GetHeight(), ref_normal_map.GetHeight()); } } void PatchMatch::Run() { PrintHeading2("PatchMatch::Run"); Check(); patch_match_cuda_ = std::make_unique(options_, problem_); patch_match_cuda_->Run(); } DepthMap PatchMatch::GetDepthMap() const { return patch_match_cuda_->GetDepthMap(); } NormalMap PatchMatch::GetNormalMap() const { return patch_match_cuda_->GetNormalMap(); } Mat PatchMatch::GetSelProbMap() const { return patch_match_cuda_->GetSelProbMap(); } ConsistencyGraph PatchMatch::GetConsistencyGraph() const { const auto& ref_image = problem_.images->at(problem_.ref_image_idx); return ConsistencyGraph(ref_image.GetWidth(), ref_image.GetHeight(), patch_match_cuda_->GetConsistentImageIdxs()); } PatchMatchController::PatchMatchController(const PatchMatchOptions& options, const std::string& workspace_path, const std::string& workspace_format, const std::string& pmvs_option_name, const std::string& config_path) : options_(options), workspace_path_(workspace_path), workspace_format_(workspace_format), pmvs_option_name_(pmvs_option_name), config_path_(config_path) { std::vector gpu_indices = CSVToVector(options_.gpu_index); } void PatchMatchController::Run() { ReadWorkspace(); ReadProblems(); ReadGpuIndices(); thread_pool_ = std::make_unique(gpu_indices_.size()); // If geometric consistency is enabled, then photometric output must be // computed first for all images without filtering. if (options_.geom_consistency) { auto photometric_options = options_; photometric_options.geom_consistency = false; photometric_options.filter = false; for (size_t problem_idx = 0; problem_idx < problems_.size(); ++problem_idx) { thread_pool_->AddTask(&PatchMatchController::ProcessProblem, this, photometric_options, problem_idx); } thread_pool_->Wait(); } for (size_t problem_idx = 0; problem_idx < problems_.size(); ++problem_idx) { thread_pool_->AddTask( &PatchMatchController::ProcessProblem, this, options_, problem_idx); } thread_pool_->Wait(); GetTimer().PrintMinutes(); } void PatchMatchController::ReadWorkspace() { LOG(INFO) << "Reading workspace..."; Workspace::Options workspace_options; auto workspace_format_lower_case = workspace_format_; StringToLower(&workspace_format_lower_case); if (workspace_format_lower_case == "pmvs") { workspace_options.stereo_folder = StringPrintf("stereo-%s", pmvs_option_name_.c_str()); } workspace_options.max_image_size = options_.max_image_size; workspace_options.image_as_rgb = false; workspace_options.cache_size = options_.cache_size; workspace_options.workspace_path = workspace_path_; workspace_options.workspace_format = workspace_format_; workspace_options.input_type = options_.geom_consistency ? "photometric" : ""; workspace_ = std::make_unique(workspace_options); if (workspace_format_lower_case == "pmvs") { LOG(INFO) << StringPrintf("Importing PMVS workspace (option %s)...", pmvs_option_name_.c_str()); ImportPMVSWorkspace(*workspace_, pmvs_option_name_); } depth_ranges_ = workspace_->GetModel().ComputeDepthRanges(); } void PatchMatchController::ReadProblems() { LOG(INFO) << "Reading configuration..."; problems_.clear(); const auto& model = workspace_->GetModel(); const std::string config_path = config_path_.empty() ? JoinPaths(workspace_path_, workspace_->GetOptions().stereo_folder, "patch-match.cfg") : config_path_; std::vector config = ReadTextFileLines(config_path); std::vector> shared_num_points; std::vector> triangulation_angles; const float min_triangulation_angle_rad = DegToRad(options_.min_triangulation_angle); std::string ref_image_name; std::unordered_set ref_image_idxs; struct ProblemConfig { std::string ref_image_name; std::vector src_image_names; }; std::vector problem_configs; for (size_t i = 0; i < config.size(); ++i) { std::string& config_line = config[i]; StringTrim(&config_line); if (config_line.empty() || config_line[0] == '#') { continue; } if (ref_image_name.empty()) { ref_image_name = config_line; continue; } ref_image_idxs.insert(model.GetImageIdx(ref_image_name)); ProblemConfig problem_config; problem_config.ref_image_name = ref_image_name; problem_config.src_image_names = CSVToVector(config_line); problem_configs.push_back(problem_config); ref_image_name.clear(); } for (const auto& problem_config : problem_configs) { PatchMatch::Problem problem; problem.ref_image_idx = model.GetImageIdx(problem_config.ref_image_name); if (problem_config.src_image_names.size() == 1 && problem_config.src_image_names[0] == "__all__") { // Use all images as source images. problem.src_image_idxs.clear(); problem.src_image_idxs.reserve(model.images.size() - 1); for (size_t image_idx = 0; image_idx < model.images.size(); ++image_idx) { if (static_cast(image_idx) != problem.ref_image_idx) { problem.src_image_idxs.push_back(image_idx); } } } else if (problem_config.src_image_names.size() == 2 && problem_config.src_image_names[0] == "__auto__") { // Use maximum number of overlapping images as source images. Overlapping // will be sorted based on the number of shared points to the reference // image and the top ranked images are selected. Note that images are only // selected if some points have a sufficient triangulation angle. if (shared_num_points.empty()) { shared_num_points = model.ComputeSharedPoints(); } if (triangulation_angles.empty()) { const float kTriangulationAnglePercentile = 75; triangulation_angles = model.ComputeTriangulationAngles(kTriangulationAnglePercentile); } const size_t max_num_src_images = std::stoll(problem_config.src_image_names[1]); const auto& overlapping_images = shared_num_points.at(problem.ref_image_idx); const auto& overlapping_triangulation_angles = triangulation_angles.at(problem.ref_image_idx); std::vector> src_images; src_images.reserve(overlapping_images.size()); for (const auto& image : overlapping_images) { if (overlapping_triangulation_angles.at(image.first) >= min_triangulation_angle_rad) { src_images.emplace_back(image.first, image.second); } } const size_t eff_max_num_src_images = std::min(src_images.size(), max_num_src_images); std::partial_sort(src_images.begin(), src_images.begin() + eff_max_num_src_images, src_images.end(), [](const std::pair& image1, const std::pair& image2) { return image1.second > image2.second; }); problem.src_image_idxs.reserve(eff_max_num_src_images); for (size_t i = 0; i < eff_max_num_src_images; ++i) { problem.src_image_idxs.push_back(src_images[i].first); } } else { problem.src_image_idxs.reserve(problem_config.src_image_names.size()); for (const auto& src_image_name : problem_config.src_image_names) { problem.src_image_idxs.push_back(model.GetImageIdx(src_image_name)); } } if (problem.src_image_idxs.empty()) { LOG(WARNING) << StringPrintf( "Ignoring reference image %s, because it has no " "source images.", problem_config.ref_image_name.c_str()); } else { problems_.push_back(problem); } } LOG(INFO) << StringPrintf("Configuration has %d problems...", problems_.size()); } void PatchMatchController::ReadGpuIndices() { gpu_indices_ = CSVToVector(options_.gpu_index); if (gpu_indices_.size() == 1 && gpu_indices_[0] == -1) { const int num_cuda_devices = GetNumCudaDevices(); CHECK_GT(num_cuda_devices, 0); gpu_indices_.resize(num_cuda_devices); std::iota(gpu_indices_.begin(), gpu_indices_.end(), 0); } } void PatchMatchController::ProcessProblem(const PatchMatchOptions& options, const size_t problem_idx) { if (IsStopped()) { return; } const auto& model = workspace_->GetModel(); auto& problem = problems_.at(problem_idx); const int gpu_index = gpu_indices_.at(thread_pool_->GetThreadIndex()); CHECK_GE(gpu_index, -1); const std::string& stereo_folder = workspace_->GetOptions().stereo_folder; const std::string output_type = options.geom_consistency ? "geometric" : "photometric"; const std::string image_name = model.GetImageName(problem.ref_image_idx); const std::string file_name = StringPrintf("%s.%s.bin", image_name.c_str(), output_type.c_str()); const std::string depth_map_path = JoinPaths(workspace_path_, stereo_folder, "depth_maps", file_name); const std::string normal_map_path = JoinPaths(workspace_path_, stereo_folder, "normal_maps", file_name); const std::string consistency_graph_path = JoinPaths( workspace_path_, stereo_folder, "consistency_graphs", file_name); if (ExistsFile(depth_map_path) && ExistsFile(normal_map_path) && (!options.write_consistency_graph || ExistsFile(consistency_graph_path))) { return; } PrintHeading1(StringPrintf("Processing view %d / %d for %s", problem_idx + 1, problems_.size(), image_name.c_str())); auto patch_match_options = options; if (patch_match_options.depth_min < 0 || patch_match_options.depth_max < 0) { patch_match_options.depth_min = depth_ranges_.at(problem.ref_image_idx).first; patch_match_options.depth_max = depth_ranges_.at(problem.ref_image_idx).second; CHECK(patch_match_options.depth_min > 0 && patch_match_options.depth_max > 0) << " - You must manually set the minimum and maximum depth, since no " "sparse model is provided in the workspace."; } patch_match_options.gpu_index = std::to_string(gpu_index); if (patch_match_options.sigma_spatial <= 0.0f) { patch_match_options.sigma_spatial = patch_match_options.window_radius; } std::vector images = model.images; std::vector depth_maps; std::vector normal_maps; if (options.geom_consistency) { depth_maps.resize(model.images.size()); normal_maps.resize(model.images.size()); } problem.images = &images; problem.depth_maps = &depth_maps; problem.normal_maps = &normal_maps; { // Collect all used images in current problem. std::unordered_set used_image_idxs(problem.src_image_idxs.begin(), problem.src_image_idxs.end()); used_image_idxs.insert(problem.ref_image_idx); patch_match_options.filter_min_num_consistent = std::min(static_cast(used_image_idxs.size()) - 1, patch_match_options.filter_min_num_consistent); // Only access workspace from one thread at a time and only spawn resample // threads from one master thread at a time. std::unique_lock lock(workspace_mutex_); LOG(INFO) << "Reading inputs..."; std::vector src_image_idxs; for (const auto image_idx : used_image_idxs) { std::string image_path = workspace_->GetBitmapPath(image_idx); std::string depth_path = workspace_->GetDepthMapPath(image_idx); std::string normal_path = workspace_->GetNormalMapPath(image_idx); if (!ExistsFile(image_path) || (options.geom_consistency && !ExistsFile(depth_path)) || (options.geom_consistency && !ExistsFile(normal_path))) { if (options.allow_missing_files) { LOG(WARNING) << StringPrintf( "Skipping source image %d: %s for missing " "image or depth/normal map", image_idx, model.GetImageName(image_idx).c_str()); continue; } else { LOG(ERROR) << StringPrintf( "Missing image or map dependency for image %d: %s", image_idx, model.GetImageName(image_idx).c_str()); } } if (image_idx != problem.ref_image_idx) { src_image_idxs.push_back(image_idx); } images.at(image_idx).SetBitmap(workspace_->GetBitmap(image_idx)); if (options.geom_consistency) { depth_maps.at(image_idx) = workspace_->GetDepthMap(image_idx); normal_maps.at(image_idx) = workspace_->GetNormalMap(image_idx); } } problem.src_image_idxs = src_image_idxs; } problem.Print(); patch_match_options.Print(); PatchMatch patch_match(patch_match_options, problem); patch_match.Run(); LOG(INFO) << std::endl << StringPrintf("Writing %s output for %s", output_type.c_str(), image_name.c_str()); patch_match.GetDepthMap().Write(depth_map_path); patch_match.GetNormalMap().Write(normal_map_path); if (options.write_consistency_graph) { patch_match.GetConsistencyGraph().Write(consistency_graph_path); } } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/patch_match.h000066400000000000000000000235141454702036400203040ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/depth_map.h" #include "colmap/mvs/image.h" #include "colmap/mvs/model.h" #include "colmap/mvs/normal_map.h" #ifndef __CUDACC__ #include "colmap/util/threading.h" #endif #include #include #include namespace colmap { namespace mvs { // Maximum possible window radius for the photometric consistency cost. This // value is equal to THREADS_PER_BLOCK in patch_match_cuda.cu and the limit // arises from the shared memory implementation. const static size_t kMaxPatchMatchWindowRadius = 32; class ConsistencyGraph; class PatchMatchCuda; class Workspace; struct PatchMatchOptions { // Maximum image size in either dimension. int max_image_size = -1; // Index of the GPU used for patch match. For multi-GPU usage, // you should separate multiple GPU indices by comma, e.g., "0,1,2,3". std::string gpu_index = "-1"; // Depth range in which to randomly sample depth hypotheses. double depth_min = -1.0f; double depth_max = -1.0f; // Half window size to compute NCC photo-consistency cost. int window_radius = 5; // Number of pixels to skip when computing NCC. For a value of 1, every // pixel is used to compute the NCC. For larger values, only every n-th row // and column is used and the computation speed thereby increases roughly by // a factor of window_step^2. Note that not all combinations of window sizes // and steps produce nice results, especially if the step is greather than 2. int window_step = 1; // Parameters for bilaterally weighted NCC. double sigma_spatial = -1; double sigma_color = 0.2f; // Number of random samples to draw in Monte Carlo sampling. int num_samples = 15; // Spread of the NCC likelihood function. double ncc_sigma = 0.6f; // Minimum triangulation angle in degrees. double min_triangulation_angle = 1.0f; // Spread of the incident angle likelihood function. double incident_angle_sigma = 0.9f; // Number of coordinate descent iterations. Each iteration consists // of four sweeps from left to right, top to bottom, and vice versa. int num_iterations = 5; // Whether to add a regularized geometric consistency term to the cost // function. If true, the `depth_maps` and `normal_maps` must not be null. bool geom_consistency = true; // The relative weight of the geometric consistency term w.r.t. to // the photo-consistency term. double geom_consistency_regularizer = 0.3f; // Maximum geometric consistency cost in terms of the forward-backward // reprojection error in pixels. double geom_consistency_max_cost = 3.0f; // Whether to enable filtering. bool filter = true; // Minimum NCC coefficient for pixel to be photo-consistent. double filter_min_ncc = 0.1f; // Minimum triangulation angle to be stable. double filter_min_triangulation_angle = 3.0f; // Minimum number of source images have to be consistent // for pixel not to be filtered. int filter_min_num_consistent = 2; // Maximum forward-backward reprojection error for pixel // to be geometrically consistent. double filter_geom_consistency_max_cost = 1.0f; // Cache size in gigabytes for patch match, which keeps the bitmaps, depth // maps, and normal maps of this number of images in memory. A higher value // leads to less disk access and faster computation, while a lower value // leads to reduced memory usage. Note that a single image can consume a lot // of memory, if the consistency graph is dense. double cache_size = 32.0; // Whether to tolerate missing images/maps in the problem setup bool allow_missing_files = false; // Whether to write the consistency graph. bool write_consistency_graph = false; void Print() const; bool Check() const { if (depth_min != -1.0f || depth_max != -1.0f) { CHECK_OPTION_LE(depth_min, depth_max); CHECK_OPTION_GE(depth_min, 0.0f); } CHECK_OPTION_LE(window_radius, static_cast(kMaxPatchMatchWindowRadius)); CHECK_OPTION_GT(sigma_color, 0.0f); CHECK_OPTION_GT(window_radius, 0); CHECK_OPTION_GT(window_step, 0); CHECK_OPTION_LE(window_step, 2); CHECK_OPTION_GT(num_samples, 0); CHECK_OPTION_GT(ncc_sigma, 0.0f); CHECK_OPTION_GE(min_triangulation_angle, 0.0f); CHECK_OPTION_LT(min_triangulation_angle, 180.0f); CHECK_OPTION_GT(incident_angle_sigma, 0.0f); CHECK_OPTION_GT(num_iterations, 0); CHECK_OPTION_GE(geom_consistency_regularizer, 0.0f); CHECK_OPTION_GE(geom_consistency_max_cost, 0.0f); CHECK_OPTION_GE(filter_min_ncc, -1.0f); CHECK_OPTION_LE(filter_min_ncc, 1.0f); CHECK_OPTION_GE(filter_min_triangulation_angle, 0.0f); CHECK_OPTION_LE(filter_min_triangulation_angle, 180.0f); CHECK_OPTION_GE(filter_min_num_consistent, 0); CHECK_OPTION_GE(filter_geom_consistency_max_cost, 0.0f); CHECK_OPTION_GT(cache_size, 0); return true; } }; // This is a wrapper class around the actual PatchMatchCuda implementation. This // class is necessary to hide Cuda code from any boost or Eigen code, since // NVCC/MSVC cannot compile complex C++ code. class PatchMatch { public: struct Problem { // Index of the reference image. int ref_image_idx = -1; // Indices of the source images. std::vector src_image_idxs; // Input images for the photometric consistency term. std::vector* images = nullptr; // Input depth maps for the geometric consistency term. std::vector* depth_maps = nullptr; // Input normal maps for the geometric consistency term. std::vector* normal_maps = nullptr; // Print the configuration to stdout. void Print() const; }; PatchMatch(const PatchMatchOptions& options, const Problem& problem); ~PatchMatch(); // Check the options and the problem for validity. void Check() const; // Run the patch match algorithm. void Run(); // Get the computed values after running the algorithm. DepthMap GetDepthMap() const; NormalMap GetNormalMap() const; ConsistencyGraph GetConsistencyGraph() const; Mat GetSelProbMap() const; private: const PatchMatchOptions options_; const Problem problem_; std::unique_ptr patch_match_cuda_; }; // This thread processes all problems in a workspace. A workspace has the // following file structure, if the workspace format is "COLMAP": // // images/* // sparse/{cameras.txt, images.txt, points3D.txt} // stereo/ // depth_maps/* // normal_maps/* // consistency_graphs/* // patch-match.cfg // // The `patch-match.cfg` file specifies the images to be processed as: // // image_name1.jpg // __all__ // image_name2.jpg // __auto__, 20 // image_name3.jpg // image_name1.jpg, image_name2.jpg // // Two consecutive lines specify the images used to compute one patch match // problem. The first line specifies the reference image and the second line the // source images. Image names are relative to the `images` directory. In this // example, the first reference image uses all other images as source images, // the second reference image uses the 20 most connected images as source // images, and the third reference image uses the first and second as source // images. Note that all specified images must be reconstructed in the COLMAP // reconstruction provided in the `sparse` folder. #ifndef __CUDACC__ class PatchMatchController : public Thread { public: PatchMatchController(const PatchMatchOptions& options, const std::string& workspace_path, const std::string& workspace_format, const std::string& pmvs_option_name, const std::string& config_path = ""); private: void Run(); void ReadWorkspace(); void ReadProblems(); void ReadGpuIndices(); void ProcessProblem(const PatchMatchOptions& options, size_t problem_idx); const PatchMatchOptions options_; const std::string workspace_path_; const std::string workspace_format_; const std::string pmvs_option_name_; const std::string config_path_; std::unique_ptr thread_pool_; std::mutex workspace_mutex_; std::unique_ptr workspace_; std::vector problems_; std::vector gpu_indices_; std::vector> depth_ranges_; }; #endif } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/patch_match_cuda.cu000066400000000000000000002206061454702036400214610ustar00rootroot00000000000000// 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. #define _USE_MATH_DEFINES #include "colmap/mvs/patch_match_cuda.h" #include "colmap/util/cuda.h" #include "colmap/util/cudacc.h" #include "colmap/util/logging.h" #include #include #include #include #include // The number of threads per Cuda thread. Warning: Do not change this value, // since the templated window sizes rely on this value. #define THREADS_PER_BLOCK 32 // We must not include "util/math.h" to avoid any Eigen includes here, // since Visual Studio cannot compile some of the Eigen/Boost expressions. #ifndef DEG2RAD #define DEG2RAD(deg) deg * 0.0174532925199432 #endif namespace colmap { namespace mvs { // Calibration of reference image as {fx, cx, fy, cy}. __constant__ float ref_K[4]; // Calibration of reference image as {1/fx, -cx/fx, 1/fy, -cy/fy}. __constant__ float ref_inv_K[4]; __device__ inline void Mat33DotVec3(const float mat[9], const float vec[3], float result[3]) { result[0] = mat[0] * vec[0] + mat[1] * vec[1] + mat[2] * vec[2]; result[1] = mat[3] * vec[0] + mat[4] * vec[1] + mat[5] * vec[2]; result[2] = mat[6] * vec[0] + mat[7] * vec[1] + mat[8] * vec[2]; } __device__ inline void Mat33DotVec3Homogeneous(const float mat[9], const float vec[2], float result[2]) { const float inv_z = 1.0f / (mat[6] * vec[0] + mat[7] * vec[1] + mat[8]); result[0] = inv_z * (mat[0] * vec[0] + mat[1] * vec[1] + mat[2]); result[1] = inv_z * (mat[3] * vec[0] + mat[4] * vec[1] + mat[5]); } __device__ inline float DotProduct3(const float vec1[3], const float vec2[3]) { return vec1[0] * vec2[0] + vec1[1] * vec2[1] + vec1[2] * vec2[2]; } __device__ inline float GenerateRandomDepth(const float depth_min, const float depth_max, curandState* rand_state) { return curand_uniform(rand_state) * (depth_max - depth_min) + depth_min; } __device__ inline void GenerateRandomNormal(const int row, const int col, curandState* rand_state, float normal[3]) { // Unbiased sampling of normal, according to George Marsaglia, "Choosing a // Point from the Surface of a Sphere", 1972. float v1 = 0.0f; float v2 = 0.0f; float s = 2.0f; while (s >= 1.0f) { v1 = 2.0f * curand_uniform(rand_state) - 1.0f; v2 = 2.0f * curand_uniform(rand_state) - 1.0f; s = v1 * v1 + v2 * v2; } const float s_norm = sqrt(1.0f - s); normal[0] = 2.0f * v1 * s_norm; normal[1] = 2.0f * v2 * s_norm; normal[2] = 1.0f - 2.0f * s; // Make sure normal is looking away from camera. const float view_ray[3] = {ref_inv_K[0] * col + ref_inv_K[1], ref_inv_K[2] * row + ref_inv_K[3], 1.0f}; if (DotProduct3(normal, view_ray) > 0) { normal[0] = -normal[0]; normal[1] = -normal[1]; normal[2] = -normal[2]; } } __device__ inline float PerturbDepth(const float perturbation, const float depth, curandState* rand_state) { const float depth_min = (1.0f - perturbation) * depth; const float depth_max = (1.0f + perturbation) * depth; return GenerateRandomDepth(depth_min, depth_max, rand_state); } __device__ inline void PerturbNormal(const int row, const int col, const float perturbation, const float normal[3], curandState* rand_state, float perturbed_normal[3], const int num_trials = 0) { // Perturbation rotation angles. const float a1 = (curand_uniform(rand_state) - 0.5f) * perturbation; const float a2 = (curand_uniform(rand_state) - 0.5f) * perturbation; const float a3 = (curand_uniform(rand_state) - 0.5f) * perturbation; const float sin_a1 = sin(a1); const float sin_a2 = sin(a2); const float sin_a3 = sin(a3); const float cos_a1 = cos(a1); const float cos_a2 = cos(a2); const float cos_a3 = cos(a3); // R = Rx * Ry * Rz float R[9]; R[0] = cos_a2 * cos_a3; R[1] = -cos_a2 * sin_a3; R[2] = sin_a2; R[3] = cos_a1 * sin_a3 + cos_a3 * sin_a1 * sin_a2; R[4] = cos_a1 * cos_a3 - sin_a1 * sin_a2 * sin_a3; R[5] = -cos_a2 * sin_a1; R[6] = sin_a1 * sin_a3 - cos_a1 * cos_a3 * sin_a2; R[7] = cos_a3 * sin_a1 + cos_a1 * sin_a2 * sin_a3; R[8] = cos_a1 * cos_a2; // Perturb the normal vector. Mat33DotVec3(R, normal, perturbed_normal); // Make sure the perturbed normal is still looking in the same direction as // the viewing direction, otherwise try again but with smaller perturbation. const float view_ray[3] = {ref_inv_K[0] * col + ref_inv_K[1], ref_inv_K[2] * row + ref_inv_K[3], 1.0f}; if (DotProduct3(perturbed_normal, view_ray) >= 0.0f) { const int kMaxNumTrials = 3; if (num_trials < kMaxNumTrials) { PerturbNormal(row, col, 0.5f * perturbation, normal, rand_state, perturbed_normal, num_trials + 1); return; } else { perturbed_normal[0] = normal[0]; perturbed_normal[1] = normal[1]; perturbed_normal[2] = normal[2]; return; } } // Make sure normal has unit norm. const float inv_norm = rsqrt(DotProduct3(perturbed_normal, perturbed_normal)); perturbed_normal[0] *= inv_norm; perturbed_normal[1] *= inv_norm; perturbed_normal[2] *= inv_norm; } __device__ inline void ComputePointAtDepth(const float row, const float col, const float depth, float point[3]) { point[0] = depth * (ref_inv_K[0] * col + ref_inv_K[1]); point[1] = depth * (ref_inv_K[2] * row + ref_inv_K[3]); point[2] = depth; } // Transfer depth on plane from viewing ray at row1 to row2. The returned // depth is the intersection of the viewing ray through row2 with the plane // at row1 defined by the given depth and normal. __device__ inline float PropagateDepth(const float depth1, const float normal1[3], const float row1, const float row2) { // Point along first viewing ray. const float x1 = depth1 * (ref_inv_K[2] * row1 + ref_inv_K[3]); const float y1 = depth1; // Point on plane defined by point along first viewing ray and plane normal1. const float x2 = x1 + normal1[2]; const float y2 = y1 - normal1[1]; // Origin of second viewing ray. // const float x3 = 0.0f; // const float y3 = 0.0f; // Point on second viewing ray. const float x4 = ref_inv_K[2] * row2 + ref_inv_K[3]; // const float y4 = 1.0f; // Intersection of the lines ((x1, y1), (x2, y2)) and ((x3, y3), (x4, y4)). const float denom = x2 - x1 + x4 * (y1 - y2); constexpr float kEps = 1e-5f; if (abs(denom) < kEps) { return depth1; } const float nom = y1 * x2 - x1 * y2; return nom / denom; } // First, compute triangulation angle between reference and source image for 3D // point. Second, compute incident angle between viewing direction of source // image and normal direction of 3D point. Both angles are cosine distances. __device__ inline void ComputeViewingAngles( const cudaTextureObject_t poses_texture, const float point[3], const float normal[3], const int image_idx, float* cos_triangulation_angle, float* cos_incident_angle) { *cos_triangulation_angle = 0.0f; *cos_incident_angle = 0.0f; // Projection center of source image. float C[3]; for (int i = 0; i < 3; ++i) { C[i] = tex2D(poses_texture, i + 16, image_idx); } // Ray from point to camera. const float SX[3] = {C[0] - point[0], C[1] - point[1], C[2] - point[2]}; // Length of ray from reference image to point. const float RX_inv_norm = rsqrt(DotProduct3(point, point)); // Length of ray from source image to point. const float SX_inv_norm = rsqrt(DotProduct3(SX, SX)); *cos_incident_angle = DotProduct3(SX, normal) * SX_inv_norm; *cos_triangulation_angle = DotProduct3(SX, point) * RX_inv_norm * SX_inv_norm; } __device__ inline void ComposeHomography( const cudaTextureObject_t poses_texture, const int image_idx, const int row, const int col, const float depth, const float normal[3], float H[9]) { // Calibration of source image. float K[4]; for (int i = 0; i < 4; ++i) { K[i] = tex2D(poses_texture, i, image_idx); } // Relative rotation between reference and source image. float R[9]; for (int i = 0; i < 9; ++i) { R[i] = tex2D(poses_texture, i + 4, image_idx); } // Relative translation between reference and source image. float T[3]; for (int i = 0; i < 3; ++i) { T[i] = tex2D(poses_texture, i + 13, image_idx); } // Distance to the plane. const float dist = depth * (normal[0] * (ref_inv_K[0] * col + ref_inv_K[1]) + normal[1] * (ref_inv_K[2] * row + ref_inv_K[3]) + normal[2]); const float inv_dist = 1.0f / dist; const float inv_dist_N0 = inv_dist * normal[0]; const float inv_dist_N1 = inv_dist * normal[1]; const float inv_dist_N2 = inv_dist * normal[2]; // Homography as H = K * (R - T * n' / d) * Kref^-1. H[0] = ref_inv_K[0] * (K[0] * (R[0] + inv_dist_N0 * T[0]) + K[1] * (R[6] + inv_dist_N0 * T[2])); H[1] = ref_inv_K[2] * (K[0] * (R[1] + inv_dist_N1 * T[0]) + K[1] * (R[7] + inv_dist_N1 * T[2])); H[2] = K[0] * (R[2] + inv_dist_N2 * T[0]) + K[1] * (R[8] + inv_dist_N2 * T[2]) + ref_inv_K[1] * (K[0] * (R[0] + inv_dist_N0 * T[0]) + K[1] * (R[6] + inv_dist_N0 * T[2])) + ref_inv_K[3] * (K[0] * (R[1] + inv_dist_N1 * T[0]) + K[1] * (R[7] + inv_dist_N1 * T[2])); H[3] = ref_inv_K[0] * (K[2] * (R[3] + inv_dist_N0 * T[1]) + K[3] * (R[6] + inv_dist_N0 * T[2])); H[4] = ref_inv_K[2] * (K[2] * (R[4] + inv_dist_N1 * T[1]) + K[3] * (R[7] + inv_dist_N1 * T[2])); H[5] = K[2] * (R[5] + inv_dist_N2 * T[1]) + K[3] * (R[8] + inv_dist_N2 * T[2]) + ref_inv_K[1] * (K[2] * (R[3] + inv_dist_N0 * T[1]) + K[3] * (R[6] + inv_dist_N0 * T[2])) + ref_inv_K[3] * (K[2] * (R[4] + inv_dist_N1 * T[1]) + K[3] * (R[7] + inv_dist_N1 * T[2])); H[6] = ref_inv_K[0] * (R[6] + inv_dist_N0 * T[2]); H[7] = ref_inv_K[2] * (R[7] + inv_dist_N1 * T[2]); H[8] = R[8] + ref_inv_K[1] * (R[6] + inv_dist_N0 * T[2]) + ref_inv_K[3] * (R[7] + inv_dist_N1 * T[2]) + inv_dist_N2 * T[2]; } // Each thread in the current warp / thread block reads in 3 columns of the // reference image. The shared memory holds 3 * THREADS_PER_BLOCK columns and // kWindowSize rows of the reference image. Each thread copies every // THREADS_PER_BLOCK-th column from global to shared memory offset by its ID. // For example, if THREADS_PER_BLOCK = 32, then thread 0 reads columns 0, 32, 64 // and thread 1 columns 1, 33, 65. When computing the photoconsistency, which is // shared among each thread block, each thread can then read the reference image // colors from shared memory. Note that this limits the window radius to a // maximum of THREADS_PER_BLOCK. template struct LocalRefImage { const static int kWindowRadius = kWindowSize / 2; const static int kThreadBlockRadius = 1; const static int kThreadBlockSize = 2 * kThreadBlockRadius + 1; const static int kNumRows = kWindowSize; const static int kNumColumns = kThreadBlockSize * THREADS_PER_BLOCK; const static int kDataSize = kNumRows * kNumColumns; __device__ explicit LocalRefImage(const cudaTextureObject_t ref_image_texture) : ref_image_texture_(ref_image_texture) {} float* data = nullptr; __device__ inline void Read(const int row) { // For the first row, read the entire block into shared memory. For all // consecutive rows, it is only necessary to shift the rows in shared memory // up by one element and then read in a new row at the bottom of the shared // memory. Note that this assumes that the calling loop starts with the // first row and then consecutively reads in the next row. const int thread_id = threadIdx.x; const int thread_block_first_id = blockDim.x * blockIdx.x; const int local_col_start = thread_id; const int global_col_start = thread_block_first_id - kThreadBlockRadius * THREADS_PER_BLOCK + thread_id; if (row == 0) { int global_row = row - kWindowRadius; for (int local_row = 0; local_row < kNumRows; ++local_row, ++global_row) { int local_col = local_col_start; int global_col = global_col_start; #pragma unroll for (int block = 0; block < kThreadBlockSize; ++block) { data[local_row * kNumColumns + local_col] = tex2D(ref_image_texture_, global_col, global_row); local_col += THREADS_PER_BLOCK; global_col += THREADS_PER_BLOCK; } } } else { // Move rows in shared memory up by one row. for (int local_row = 1; local_row < kNumRows; ++local_row) { int local_col = local_col_start; #pragma unroll for (int block = 0; block < kThreadBlockSize; ++block) { data[(local_row - 1) * kNumColumns + local_col] = data[local_row * kNumColumns + local_col]; local_col += THREADS_PER_BLOCK; } } // Read next row into the last row of shared memory. const int local_row = kNumRows - 1; const int global_row = row + kWindowRadius; int local_col = local_col_start; int global_col = global_col_start; #pragma unroll for (int block = 0; block < kThreadBlockSize; ++block) { data[local_row * kNumColumns + local_col] = tex2D(ref_image_texture_, global_col, global_row); local_col += THREADS_PER_BLOCK; global_col += THREADS_PER_BLOCK; } } } private: const cudaTextureObject_t ref_image_texture_; }; // The return values is 1 - NCC, so the range is [0, 2], the smaller the // value, the better the color consistency. template struct PhotoConsistencyCostComputer { const static int kWindowRadius = kWindowSize / 2; __device__ PhotoConsistencyCostComputer( const cudaTextureObject_t ref_image_texture, const cudaTextureObject_t src_images_texture, const cudaTextureObject_t poses_texture, const float sigma_spatial, const float sigma_color) : local_ref_image(ref_image_texture), src_images_texture_(src_images_texture), poses_texture_(poses_texture), bilateral_weight_computer_(sigma_spatial, sigma_color) {} // Maximum photo consistency cost as 1 - min(NCC). const float kMaxCost = 2.0f; // Thread warp local reference image data around current patch. typedef LocalRefImage LocalRefImageType; LocalRefImageType local_ref_image; // Precomputed sum of raw and squared image intensities. float local_ref_sum = 0.0f; float local_ref_squared_sum = 0.0f; // Index of source image. int src_image_idx = -1; // Center position of patch in reference image. int row = -1; int col = -1; // Depth and normal for which to warp patch. float depth = 0.0f; const float* normal = nullptr; __device__ inline void Read(const int row) { local_ref_image.Read(row); __syncthreads(); } __device__ inline float Compute() const { float tform[9]; ComposeHomography( poses_texture_, src_image_idx, row, col, depth, normal, tform); float tform_step[8]; for (int i = 0; i < 8; ++i) { tform_step[i] = kWindowStep * tform[i]; } const int thread_id = threadIdx.x; const int row_start = row - kWindowRadius; const int col_start = col - kWindowRadius; float col_src = tform[0] * col_start + tform[1] * row_start + tform[2]; float row_src = tform[3] * col_start + tform[4] * row_start + tform[5]; float z = tform[6] * col_start + tform[7] * row_start + tform[8]; float base_col_src = col_src; float base_row_src = row_src; float base_z = z; int ref_image_idx = THREADS_PER_BLOCK - kWindowRadius + thread_id; int ref_image_base_idx = ref_image_idx; const float ref_center_color = local_ref_image .data[ref_image_idx + kWindowRadius * 3 * THREADS_PER_BLOCK + kWindowRadius]; const float ref_color_sum = local_ref_sum; const float ref_color_squared_sum = local_ref_squared_sum; float src_color_sum = 0.0f; float src_color_squared_sum = 0.0f; float src_ref_color_sum = 0.0f; float bilateral_weight_sum = 0.0f; for (int row = -kWindowRadius; row <= kWindowRadius; row += kWindowStep) { for (int col = -kWindowRadius; col <= kWindowRadius; col += kWindowStep) { const float inv_z = 1.0f / z; const float norm_col_src = inv_z * col_src + 0.5f; const float norm_row_src = inv_z * row_src + 0.5f; const float ref_color = local_ref_image.data[ref_image_idx]; const float src_color = tex2DLayered( src_images_texture_, norm_col_src, norm_row_src, src_image_idx); const float bilateral_weight = bilateral_weight_computer_.Compute( row, col, ref_center_color, ref_color); const float bilateral_weight_src = bilateral_weight * src_color; src_color_sum += bilateral_weight_src; src_color_squared_sum += bilateral_weight_src * src_color; src_ref_color_sum += bilateral_weight_src * ref_color; bilateral_weight_sum += bilateral_weight; ref_image_idx += kWindowStep; // Accumulate warped source coordinates per row to reduce numerical // errors. Note that this is necessary since coordinates usually are in // the order of 1000s as opposed to the color values which are // normalized to the range [0, 1]. col_src += tform_step[0]; row_src += tform_step[3]; z += tform_step[6]; } ref_image_base_idx += kWindowStep * 3 * THREADS_PER_BLOCK; ref_image_idx = ref_image_base_idx; base_col_src += tform_step[1]; base_row_src += tform_step[4]; base_z += tform_step[7]; col_src = base_col_src; row_src = base_row_src; z = base_z; } const float inv_bilateral_weight_sum = 1.0f / bilateral_weight_sum; src_color_sum *= inv_bilateral_weight_sum; src_color_squared_sum *= inv_bilateral_weight_sum; src_ref_color_sum *= inv_bilateral_weight_sum; const float ref_color_var = ref_color_squared_sum - ref_color_sum * ref_color_sum; const float src_color_var = src_color_squared_sum - src_color_sum * src_color_sum; // Based on Jensen's Inequality for convex functions, the variance // should always be larger than 0. Do not make this threshold smaller. constexpr float kMinVar = 1e-5f; if (ref_color_var < kMinVar || src_color_var < kMinVar) { return kMaxCost; } else { const float src_ref_color_covar = src_ref_color_sum - ref_color_sum * src_color_sum; const float src_ref_color_var = sqrt(ref_color_var * src_color_var); return max(0.0f, min(kMaxCost, 1.0f - src_ref_color_covar / src_ref_color_var)); } } private: const cudaTextureObject_t src_images_texture_; const cudaTextureObject_t poses_texture_; const BilateralWeightComputer bilateral_weight_computer_; }; __device__ inline float ComputeGeomConsistencyCost( const cudaTextureObject_t poses_texture, const cudaTextureObject_t src_depth_maps_texture, const float row, const float col, const float depth, const int image_idx, const float max_cost) { // Extract projection matrices for source image. float P[12]; for (int i = 0; i < 12; ++i) { P[i] = tex2D(poses_texture, i + 19, image_idx); } float inv_P[12]; for (int i = 0; i < 12; ++i) { inv_P[i] = tex2D(poses_texture, i + 31, image_idx); } // Project point in reference image to world. float forward_point[3]; ComputePointAtDepth(row, col, depth, forward_point); // Project world point to source image. const float inv_forward_z = 1.0f / (P[8] * forward_point[0] + P[9] * forward_point[1] + P[10] * forward_point[2] + P[11]); float src_col = inv_forward_z * (P[0] * forward_point[0] + P[1] * forward_point[1] + P[2] * forward_point[2] + P[3]); float src_row = inv_forward_z * (P[4] * forward_point[0] + P[5] * forward_point[1] + P[6] * forward_point[2] + P[7]); // Extract depth in source image. const float src_depth = tex2DLayered( src_depth_maps_texture, src_col + 0.5f, src_row + 0.5f, image_idx); // Projection outside of source image. if (src_depth == 0.0f) { return max_cost; } // Project point in source image to world. src_col *= src_depth; src_row *= src_depth; const float backward_point_x = inv_P[0] * src_col + inv_P[1] * src_row + inv_P[2] * src_depth + inv_P[3]; const float backward_point_y = inv_P[4] * src_col + inv_P[5] * src_row + inv_P[6] * src_depth + inv_P[7]; const float backward_point_z = inv_P[8] * src_col + inv_P[9] * src_row + inv_P[10] * src_depth + inv_P[11]; const float inv_backward_point_z = 1.0f / backward_point_z; // Project world point back to reference image. const float backward_col = inv_backward_point_z * (ref_K[0] * backward_point_x + ref_K[1] * backward_point_z); const float backward_row = inv_backward_point_z * (ref_K[2] * backward_point_y + ref_K[3] * backward_point_z); // Return truncated reprojection error between original observation and // the forward-backward projected observation. const float diff_col = col - backward_col; const float diff_row = row - backward_row; return min(max_cost, sqrt(diff_col * diff_col + diff_row * diff_row)); } // Find index of minimum in given values. template __device__ inline int FindMinCost(const float costs[kNumCosts]) { float min_cost = costs[0]; int min_cost_idx = 0; for (int idx = 1; idx < kNumCosts; ++idx) { if (costs[idx] <= min_cost) { min_cost = costs[idx]; min_cost_idx = idx; } } return min_cost_idx; } __device__ inline void TransformPDFToCDF(float* probs, const int num_probs) { float prob_sum = 0.0f; for (int i = 0; i < num_probs; ++i) { prob_sum += probs[i]; } const float inv_prob_sum = 1.0f / prob_sum; float cum_prob = 0.0f; for (int i = 0; i < num_probs; ++i) { const float prob = probs[i] * inv_prob_sum; cum_prob += prob; probs[i] = cum_prob; } } class LikelihoodComputer { public: __device__ LikelihoodComputer(const float ncc_sigma, const float min_triangulation_angle, const float incident_angle_sigma) : cos_min_triangulation_angle_(cos(min_triangulation_angle)), inv_incident_angle_sigma_square_( -0.5f / (incident_angle_sigma * incident_angle_sigma)), inv_ncc_sigma_square_(-0.5f / (ncc_sigma * ncc_sigma)), ncc_norm_factor_(ComputeNCCCostNormFactor(ncc_sigma)) {} // Compute forward message from current cost and forward message of // previous / neighboring pixel. __device__ float ComputeForwardMessage(const float cost, const float prev) const { return ComputeMessage(cost, prev); } // Compute backward message from current cost and backward message of // previous / neighboring pixel. __device__ float ComputeBackwardMessage(const float cost, const float prev) const { return ComputeMessage(cost, prev); } // Compute the selection probability from the forward and backward message. __device__ inline float ComputeSelProb(const float alpha, const float beta, const float prev, const float prev_weight) const { const float zn0 = (1.0f - alpha) * (1.0f - beta); const float zn1 = alpha * beta; const float curr = zn1 / (zn0 + zn1); return prev_weight * prev + (1.0f - prev_weight) * curr; } // Compute NCC probability. Note that cost = 1 - NCC. __device__ inline float ComputeNCCProb(const float cost) const { return exp(cost * cost * inv_ncc_sigma_square_) * ncc_norm_factor_; } // Compute the triangulation angle probability. __device__ inline float ComputeTriProb( const float cos_triangulation_angle) const { const float abs_cos_triangulation_angle = abs(cos_triangulation_angle); if (abs_cos_triangulation_angle > cos_min_triangulation_angle_) { const float scaled = 1.0f - (1.0f - abs_cos_triangulation_angle) / (1.0f - cos_min_triangulation_angle_); const float likelihood = 1.0f - scaled * scaled; return min(1.0f, max(0.0f, likelihood)); } else { return 1.0f; } } // Compute the incident angle probability. __device__ inline float ComputeIncProb(const float cos_incident_angle) const { const float x = 1.0f - max(0.0f, cos_incident_angle); return exp(x * x * inv_incident_angle_sigma_square_); } // Compute the warping/resolution prior probability. template __device__ inline float ComputeResolutionProb(const float H[9], const float row, const float col) const { const int kWindowRadius = kWindowSize / 2; // Warp corners of patch in reference image to source image. float src1[2]; const float ref1[2] = {col - kWindowRadius, row - kWindowRadius}; Mat33DotVec3Homogeneous(H, ref1, src1); float src2[2]; const float ref2[2] = {col - kWindowRadius, row + kWindowRadius}; Mat33DotVec3Homogeneous(H, ref2, src2); float src3[2]; const float ref3[2] = {col + kWindowRadius, row + kWindowRadius}; Mat33DotVec3Homogeneous(H, ref3, src3); float src4[2]; const float ref4[2] = {col + kWindowRadius, row - kWindowRadius}; Mat33DotVec3Homogeneous(H, ref4, src4); // Compute area of patches in reference and source image. const float ref_area = kWindowSize * kWindowSize; const float src_area = abs(0.5f * (src1[0] * src2[1] - src2[0] * src1[1] - src1[0] * src4[1] + src2[0] * src3[1] - src3[0] * src2[1] + src4[0] * src1[1] + src3[0] * src4[1] - src4[0] * src3[1])); if (ref_area > src_area) { return src_area / ref_area; } else { return ref_area / src_area; } } private: // The normalization for the likelihood function, i.e. the normalization for // the prior on the matching cost. __device__ static inline float ComputeNCCCostNormFactor( const float ncc_sigma) { // A = sqrt(2pi)*sigma/2*erf(sqrt(2)/sigma) // erf(x) = 2/sqrt(pi) * integral from 0 to x of exp(-t^2) dt return 2.0f / (sqrt(2.0f * M_PI) * ncc_sigma * erff(2.0f / (ncc_sigma * 1.414213562f))); } // Compute the forward or backward message. template __device__ inline float ComputeMessage(const float cost, const float prev) const { constexpr float kUniformProb = 0.5f; constexpr float kNoChangeProb = 0.99999f; const float kChangeProb = 1.0f - kNoChangeProb; const float emission = ComputeNCCProb(cost); float zn0; // Message for selection probability = 0. float zn1; // Message for selection probability = 1. if (kForward) { zn0 = (prev * kChangeProb + (1.0f - prev) * kNoChangeProb) * kUniformProb; zn1 = (prev * kNoChangeProb + (1.0f - prev) * kChangeProb) * emission; } else { zn0 = prev * emission * kChangeProb + (1.0f - prev) * kUniformProb * kNoChangeProb; zn1 = prev * emission * kNoChangeProb + (1.0f - prev) * kUniformProb * kChangeProb; } return zn1 / (zn0 + zn1); } const float cos_min_triangulation_angle_; const float inv_incident_angle_sigma_square_; const float inv_ncc_sigma_square_; const float ncc_norm_factor_; }; // Rotate normals by 90deg around z-axis in counter-clockwise direction. __global__ void InitNormalMap(GpuMat normal_map, GpuMat rand_state_map) { const int row = blockDim.y * blockIdx.y + threadIdx.y; const int col = blockDim.x * blockIdx.x + threadIdx.x; if (col < normal_map.GetWidth() && row < normal_map.GetHeight()) { curandState rand_state = rand_state_map.Get(row, col); float normal[3]; GenerateRandomNormal(row, col, &rand_state, normal); normal_map.SetSlice(row, col, normal); rand_state_map.Set(row, col, rand_state); } } // Rotate normals by 90deg around z-axis in counter-clockwise direction. __global__ void RotateNormalMap(GpuMat normal_map) { const int row = blockDim.y * blockIdx.y + threadIdx.y; const int col = blockDim.x * blockIdx.x + threadIdx.x; if (col < normal_map.GetWidth() && row < normal_map.GetHeight()) { float normal[3]; normal_map.GetSlice(row, col, normal); float rotated_normal[3]; rotated_normal[0] = normal[1]; rotated_normal[1] = -normal[0]; rotated_normal[2] = normal[2]; normal_map.SetSlice(row, col, rotated_normal); } } template __global__ void ComputeInitialCost(GpuMat cost_map, const GpuMat depth_map, const GpuMat normal_map, const cudaTextureObject_t ref_image_texture, const GpuMat ref_sum_image, const GpuMat ref_squared_sum_image, const cudaTextureObject_t src_images_texture, const cudaTextureObject_t poses_texture, const float sigma_spatial, const float sigma_color) { const int col = blockDim.x * blockIdx.x + threadIdx.x; typedef PhotoConsistencyCostComputer PhotoConsistencyCostComputerType; PhotoConsistencyCostComputerType pcc_computer(ref_image_texture, src_images_texture, poses_texture, sigma_spatial, sigma_color); pcc_computer.col = col; __shared__ float local_ref_image_data [PhotoConsistencyCostComputerType::LocalRefImageType::kDataSize]; pcc_computer.local_ref_image.data = &local_ref_image_data[0]; float normal[3] = {0}; pcc_computer.normal = normal; for (int row = 0; row < cost_map.GetHeight(); ++row) { // Note that this must be executed even for pixels outside the borders, // since pixels are used in the local neighborhood of the current pixel. pcc_computer.Read(row); if (col < cost_map.GetWidth()) { pcc_computer.depth = depth_map.Get(row, col); normal_map.GetSlice(row, col, normal); pcc_computer.row = row; pcc_computer.local_ref_sum = ref_sum_image.Get(row, col); pcc_computer.local_ref_squared_sum = ref_squared_sum_image.Get(row, col); for (int image_idx = 0; image_idx < cost_map.GetDepth(); ++image_idx) { pcc_computer.src_image_idx = image_idx; cost_map.Set(row, col, image_idx, pcc_computer.Compute()); } } } } struct SweepOptions { float perturbation = 1.0f; float depth_min = 0.0f; float depth_max = 1.0f; int num_samples = 15; float sigma_spatial = 3.0f; float sigma_color = 0.3f; float ncc_sigma = 0.6f; float min_triangulation_angle = 0.5f; float incident_angle_sigma = 0.9f; float prev_sel_prob_weight = 0.0f; float geom_consistency_regularizer = 0.1f; float geom_consistency_max_cost = 5.0f; float filter_min_ncc = 0.1f; float filter_min_triangulation_angle = 3.0f; int filter_min_num_consistent = 2; float filter_geom_consistency_max_cost = 1.0f; }; template __global__ void SweepFromTopToBottom( GpuMat global_workspace, GpuMat rand_state_map, GpuMat cost_map, GpuMat depth_map, GpuMat normal_map, GpuMat consistency_mask, GpuMat sel_prob_map, const GpuMat prev_sel_prob_map, const cudaTextureObject_t ref_image_texture, const GpuMat ref_sum_image, const GpuMat ref_squared_sum_image, const cudaTextureObject_t src_images_texture, const cudaTextureObject_t src_depth_maps_texture, const cudaTextureObject_t poses_texture, const SweepOptions options) { const int col = blockDim.x * blockIdx.x + threadIdx.x; // Probability for boundary pixels. constexpr float kUniformProb = 0.5f; LikelihoodComputer likelihood_computer(options.ncc_sigma, options.min_triangulation_angle, options.incident_angle_sigma); float* forward_message = &global_workspace.GetPtr()[col * global_workspace.GetHeight()]; float* sampling_probs = &global_workspace.GetPtr()[global_workspace.GetWidth() * global_workspace.GetHeight() + col * global_workspace.GetHeight()]; ////////////////////////////////////////////////////////////////////////////// // Compute backward message for all rows. Note that the backward messages are // temporarily stored in the sel_prob_map and replaced row by row as the // updated forward messages are computed further below. ////////////////////////////////////////////////////////////////////////////// if (col < cost_map.GetWidth()) { for (int image_idx = 0; image_idx < cost_map.GetDepth(); ++image_idx) { // Compute backward message. float beta = kUniformProb; for (int row = cost_map.GetHeight() - 1; row >= 0; --row) { const float cost = cost_map.Get(row, col, image_idx); beta = likelihood_computer.ComputeBackwardMessage(cost, beta); sel_prob_map.Set(row, col, image_idx, beta); } // Initialize forward message. forward_message[image_idx] = kUniformProb; } } ////////////////////////////////////////////////////////////////////////////// // Estimate parameters for remaining rows and compute selection probabilities. ////////////////////////////////////////////////////////////////////////////// typedef PhotoConsistencyCostComputer PhotoConsistencyCostComputerType; PhotoConsistencyCostComputerType pcc_computer(ref_image_texture, src_images_texture, poses_texture, options.sigma_spatial, options.sigma_color); pcc_computer.col = col; __shared__ float local_ref_image_data [PhotoConsistencyCostComputerType::LocalRefImageType::kDataSize]; pcc_computer.local_ref_image.data = &local_ref_image_data[0]; struct ParamState { float depth = 0.0f; float normal[3] = {0}; }; // Parameters of previous pixel in column. ParamState prev_param_state; // Parameters of current pixel in column. ParamState curr_param_state; // Randomly sampled parameters. ParamState rand_param_state; // Cuda PRNG state for random sampling. curandState rand_state; if (col < cost_map.GetWidth()) { // Read random state for current column. rand_state = rand_state_map.Get(0, col); // Parameters for first row in column. prev_param_state.depth = depth_map.Get(0, col); normal_map.GetSlice(0, col, prev_param_state.normal); } for (int row = 0; row < cost_map.GetHeight(); ++row) { // Note that this must be executed even for pixels outside the borders, // since pixels are used in the local neighborhood of the current pixel. pcc_computer.Read(row); if (col >= cost_map.GetWidth()) { continue; } pcc_computer.row = row; pcc_computer.local_ref_sum = ref_sum_image.Get(row, col); pcc_computer.local_ref_squared_sum = ref_squared_sum_image.Get(row, col); // Propagate the depth at which the current ray intersects with the plane // of the normal of the previous ray. This helps to better estimate // the depth of very oblique structures, i.e. pixels whose normal direction // is significantly different from their viewing direction. prev_param_state.depth = PropagateDepth( prev_param_state.depth, prev_param_state.normal, row - 1, row); // Read parameters for current pixel from previous sweep. curr_param_state.depth = depth_map.Get(row, col); normal_map.GetSlice(row, col, curr_param_state.normal); // Generate random parameters. rand_param_state.depth = PerturbDepth(options.perturbation, curr_param_state.depth, &rand_state); PerturbNormal(row, col, options.perturbation * M_PI, curr_param_state.normal, &rand_state, rand_param_state.normal); // Read in the backward message, compute selection probabilities and // modulate selection probabilities with priors. float point[3]; ComputePointAtDepth(row, col, curr_param_state.depth, point); for (int image_idx = 0; image_idx < cost_map.GetDepth(); ++image_idx) { const float cost = cost_map.Get(row, col, image_idx); const float alpha = likelihood_computer.ComputeForwardMessage( cost, forward_message[image_idx]); const float beta = sel_prob_map.Get(row, col, image_idx); const float prev_prob = prev_sel_prob_map.Get(row, col, image_idx); const float sel_prob = likelihood_computer.ComputeSelProb( alpha, beta, prev_prob, options.prev_sel_prob_weight); float cos_triangulation_angle; float cos_incident_angle; ComputeViewingAngles(poses_texture, point, curr_param_state.normal, image_idx, &cos_triangulation_angle, &cos_incident_angle); const float tri_prob = likelihood_computer.ComputeTriProb(cos_triangulation_angle); const float inc_prob = likelihood_computer.ComputeIncProb(cos_incident_angle); float H[9]; ComposeHomography(poses_texture, image_idx, row, col, curr_param_state.depth, curr_param_state.normal, H); const float res_prob = likelihood_computer.ComputeResolutionProb(H, row, col); sampling_probs[image_idx] = sel_prob * tri_prob * inc_prob * res_prob; } TransformPDFToCDF(sampling_probs, cost_map.GetDepth()); // Compute matching cost using Monte Carlo sampling of source images. Images // with higher selection probability are more likely to be sampled. Hence, // if only very few source images see the reference image pixel, the same // source image is likely to be sampled many times. Instead of taking // the best K probabilities, this sampling scheme has the advantage of // being adaptive to any distribution of selection probabilities. constexpr int kNumCosts = 5; float costs[kNumCosts] = {0}; const float depths[kNumCosts] = {curr_param_state.depth, prev_param_state.depth, rand_param_state.depth, curr_param_state.depth, rand_param_state.depth}; const float* normals[kNumCosts] = {curr_param_state.normal, prev_param_state.normal, rand_param_state.normal, rand_param_state.normal, curr_param_state.normal}; for (int sample = 0; sample < options.num_samples; ++sample) { const float rand_prob = curand_uniform(&rand_state) - FLT_EPSILON; pcc_computer.src_image_idx = -1; for (int image_idx = 0; image_idx < cost_map.GetDepth(); ++image_idx) { const float prob = sampling_probs[image_idx]; if (prob > rand_prob) { pcc_computer.src_image_idx = image_idx; break; } } if (pcc_computer.src_image_idx == -1) { continue; } costs[0] += cost_map.Get(row, col, pcc_computer.src_image_idx); if (kGeomConsistencyTerm) { costs[0] += options.geom_consistency_regularizer * ComputeGeomConsistencyCost(poses_texture, src_depth_maps_texture, row, col, depths[0], pcc_computer.src_image_idx, options.geom_consistency_max_cost); } for (int i = 1; i < kNumCosts; ++i) { pcc_computer.depth = depths[i]; pcc_computer.normal = normals[i]; costs[i] += pcc_computer.Compute(); if (kGeomConsistencyTerm) { costs[i] += options.geom_consistency_regularizer * ComputeGeomConsistencyCost(poses_texture, src_depth_maps_texture, row, col, depths[i], pcc_computer.src_image_idx, options.geom_consistency_max_cost); } } } // Find the parameters of the minimum cost. const int min_cost_idx = FindMinCost(costs); const float best_depth = depths[min_cost_idx]; const float* best_normal = normals[min_cost_idx]; // Save best new parameters. depth_map.Set(row, col, best_depth); normal_map.SetSlice(row, col, best_normal); // Use the new cost to recompute the updated forward message and // the selection probability. pcc_computer.depth = best_depth; pcc_computer.normal = best_normal; for (int image_idx = 0; image_idx < cost_map.GetDepth(); ++image_idx) { // Determine the cost for best depth. float cost; if (min_cost_idx == 0) { cost = cost_map.Get(row, col, image_idx); } else { pcc_computer.src_image_idx = image_idx; cost = pcc_computer.Compute(); cost_map.Set(row, col, image_idx, cost); } const float alpha = likelihood_computer.ComputeForwardMessage( cost, forward_message[image_idx]); const float beta = sel_prob_map.Get(row, col, image_idx); const float prev_prob = prev_sel_prob_map.Get(row, col, image_idx); const float prob = likelihood_computer.ComputeSelProb( alpha, beta, prev_prob, options.prev_sel_prob_weight); forward_message[image_idx] = alpha; sel_prob_map.Set(row, col, image_idx, prob); } if (kFilterPhotoConsistency || kFilterGeomConsistency) { int num_consistent = 0; float best_point[3]; ComputePointAtDepth(row, col, best_depth, best_point); const float min_ncc_prob = likelihood_computer.ComputeNCCProb(1.0f - options.filter_min_ncc); const float cos_min_triangulation_angle = cos(options.filter_min_triangulation_angle); for (int image_idx = 0; image_idx < cost_map.GetDepth(); ++image_idx) { float cos_triangulation_angle; float cos_incident_angle; ComputeViewingAngles(poses_texture, best_point, best_normal, image_idx, &cos_triangulation_angle, &cos_incident_angle); if (cos_triangulation_angle > cos_min_triangulation_angle || cos_incident_angle <= 0.0f) { continue; } if (!kFilterGeomConsistency) { if (sel_prob_map.Get(row, col, image_idx) >= min_ncc_prob) { consistency_mask.Set(row, col, image_idx, 1); num_consistent += 1; } } else if (!kFilterPhotoConsistency) { if (ComputeGeomConsistencyCost(poses_texture, src_depth_maps_texture, row, col, best_depth, image_idx, options.geom_consistency_max_cost) <= options.filter_geom_consistency_max_cost) { consistency_mask.Set(row, col, image_idx, 1); num_consistent += 1; } } else { if (sel_prob_map.Get(row, col, image_idx) >= min_ncc_prob && ComputeGeomConsistencyCost(poses_texture, src_depth_maps_texture, row, col, best_depth, image_idx, options.geom_consistency_max_cost) <= options.filter_geom_consistency_max_cost) { consistency_mask.Set(row, col, image_idx, 1); num_consistent += 1; } } } if (num_consistent < options.filter_min_num_consistent) { depth_map.Set(row, col, 0.0f); normal_map.Set(row, col, 0, 0.0f); normal_map.Set(row, col, 1, 0.0f); normal_map.Set(row, col, 2, 0.0f); for (int image_idx = 0; image_idx < cost_map.GetDepth(); ++image_idx) { consistency_mask.Set(row, col, image_idx, 0); } } } // Update previous depth for next row. prev_param_state.depth = best_depth; for (int i = 0; i < 3; ++i) { prev_param_state.normal[i] = best_normal[i]; } } if (col < cost_map.GetWidth()) { rand_state_map.Set(0, col, rand_state); } } PatchMatchCuda::PatchMatchCuda(const PatchMatchOptions& options, const PatchMatch::Problem& problem) : options_(options), problem_(problem), ref_width_(0), ref_height_(0), rotation_in_half_pi_(0) { SetBestCudaDevice(std::stoi(options_.gpu_index)); InitRefImage(); InitSourceImages(); InitTransforms(); InitWorkspaceMemory(); } void PatchMatchCuda::Run() { #define CASE_WINDOW_RADIUS(window_radius, window_step) \ case window_radius: \ RunWithWindowSizeAndStep<2 * window_radius + 1, window_step>(); \ break; #define CASE_WINDOW_STEP(window_step) \ case window_step: \ switch (options_.window_radius) { \ CASE_WINDOW_RADIUS(1, window_step) \ CASE_WINDOW_RADIUS(2, window_step) \ CASE_WINDOW_RADIUS(3, window_step) \ CASE_WINDOW_RADIUS(4, window_step) \ CASE_WINDOW_RADIUS(5, window_step) \ CASE_WINDOW_RADIUS(6, window_step) \ CASE_WINDOW_RADIUS(7, window_step) \ CASE_WINDOW_RADIUS(8, window_step) \ CASE_WINDOW_RADIUS(9, window_step) \ CASE_WINDOW_RADIUS(10, window_step) \ CASE_WINDOW_RADIUS(11, window_step) \ CASE_WINDOW_RADIUS(12, window_step) \ CASE_WINDOW_RADIUS(13, window_step) \ CASE_WINDOW_RADIUS(14, window_step) \ CASE_WINDOW_RADIUS(15, window_step) \ CASE_WINDOW_RADIUS(16, window_step) \ CASE_WINDOW_RADIUS(17, window_step) \ CASE_WINDOW_RADIUS(18, window_step) \ CASE_WINDOW_RADIUS(19, window_step) \ CASE_WINDOW_RADIUS(20, window_step) \ default: { \ LOG(ERROR) << "Window size " << options_.window_radius \ << " not supported"; \ break; \ } \ } \ break; switch (options_.window_step) { CASE_WINDOW_STEP(1) CASE_WINDOW_STEP(2) default: { LOG(ERROR) << "Window step " << options_.window_step << " not supported"; break; } } #undef SWITCH_WINDOW_RADIUS #undef CALL_RUN_FUNC } DepthMap PatchMatchCuda::GetDepthMap() const { return DepthMap( depth_map_->CopyToMat(), options_.depth_min, options_.depth_max); } NormalMap PatchMatchCuda::GetNormalMap() const { return NormalMap(normal_map_->CopyToMat()); } Mat PatchMatchCuda::GetSelProbMap() const { return prev_sel_prob_map_->CopyToMat(); } std::vector PatchMatchCuda::GetConsistentImageIdxs() const { const Mat mask = consistency_mask_->CopyToMat(); std::vector consistent_image_idxs; std::vector pixel_consistent_image_idxs; pixel_consistent_image_idxs.reserve(mask.GetDepth()); for (size_t r = 0; r < mask.GetHeight(); ++r) { for (size_t c = 0; c < mask.GetWidth(); ++c) { pixel_consistent_image_idxs.clear(); for (size_t d = 0; d < mask.GetDepth(); ++d) { if (mask.Get(r, c, d)) { pixel_consistent_image_idxs.push_back(problem_.src_image_idxs[d]); } } if (pixel_consistent_image_idxs.size() > 0) { consistent_image_idxs.push_back(c); consistent_image_idxs.push_back(r); consistent_image_idxs.push_back(pixel_consistent_image_idxs.size()); consistent_image_idxs.insert(consistent_image_idxs.end(), pixel_consistent_image_idxs.begin(), pixel_consistent_image_idxs.end()); } } } return consistent_image_idxs; } template void PatchMatchCuda::RunWithWindowSizeAndStep() { // Wait for all initializations to finish. CUDA_SYNC_AND_CHECK(); CudaTimer total_timer; CudaTimer init_timer; ComputeCudaConfig(); ComputeInitialCost <<>>(*cost_map_, *depth_map_, *normal_map_, ref_image_texture_->GetObj(), *ref_image_->sum_image, *ref_image_->squared_sum_image, src_images_texture_->GetObj(), poses_texture_[0]->GetObj(), options_.sigma_spatial, options_.sigma_color); CUDA_SYNC_AND_CHECK(); init_timer.Print("Initialization"); const float total_num_steps = options_.num_iterations * 4; SweepOptions sweep_options; sweep_options.depth_min = options_.depth_min; sweep_options.depth_max = options_.depth_max; sweep_options.sigma_spatial = options_.sigma_spatial; sweep_options.sigma_color = options_.sigma_color; sweep_options.num_samples = options_.num_samples; sweep_options.ncc_sigma = options_.ncc_sigma; sweep_options.min_triangulation_angle = DEG2RAD(options_.min_triangulation_angle); sweep_options.incident_angle_sigma = options_.incident_angle_sigma; sweep_options.geom_consistency_regularizer = options_.geom_consistency_regularizer; sweep_options.geom_consistency_max_cost = options_.geom_consistency_max_cost; sweep_options.filter_min_ncc = options_.filter_min_ncc; sweep_options.filter_min_triangulation_angle = DEG2RAD(options_.filter_min_triangulation_angle); sweep_options.filter_min_num_consistent = options_.filter_min_num_consistent; sweep_options.filter_geom_consistency_max_cost = options_.filter_geom_consistency_max_cost; for (int iter = 0; iter < options_.num_iterations; ++iter) { CudaTimer iter_timer; for (int sweep = 0; sweep < 4; ++sweep) { CudaTimer sweep_timer; // Expenentially reduce amount of perturbation during the optimization. sweep_options.perturbation = 1.0f / std::pow(2.0f, iter + sweep / 4.0f); // Linearly increase the influence of previous selection probabilities. sweep_options.prev_sel_prob_weight = static_cast(iter * 4 + sweep) / total_num_steps; const bool last_sweep = iter == options_.num_iterations - 1 && sweep == 3; #define CALL_SWEEP_FUNC \ SweepFromTopToBottom \ <<>>( \ *global_workspace_, \ *rand_state_map_, \ *cost_map_, \ *depth_map_, \ *normal_map_, \ *consistency_mask_, \ *sel_prob_map_, \ *prev_sel_prob_map_, \ ref_image_texture_->GetObj(), \ *ref_image_->sum_image, \ *ref_image_->squared_sum_image, \ src_images_texture_->GetObj(), \ src_depth_maps_texture_ == nullptr \ ? 0 \ : src_depth_maps_texture_->GetObj(), \ poses_texture_[rotation_in_half_pi_]->GetObj(), \ sweep_options); if (last_sweep) { if (options_.filter) { consistency_mask_.reset(new GpuMat(cost_map_->GetWidth(), cost_map_->GetHeight(), cost_map_->GetDepth())); consistency_mask_->FillWithScalar(0); } if (options_.geom_consistency) { const bool kGeomConsistencyTerm = true; if (options_.filter) { const bool kFilterPhotoConsistency = true; const bool kFilterGeomConsistency = true; CALL_SWEEP_FUNC } else { const bool kFilterPhotoConsistency = false; const bool kFilterGeomConsistency = false; CALL_SWEEP_FUNC } } else { const bool kGeomConsistencyTerm = false; if (options_.filter) { const bool kFilterPhotoConsistency = true; const bool kFilterGeomConsistency = false; CALL_SWEEP_FUNC } else { const bool kFilterPhotoConsistency = false; const bool kFilterGeomConsistency = false; CALL_SWEEP_FUNC } } } else { const bool kFilterPhotoConsistency = false; const bool kFilterGeomConsistency = false; if (options_.geom_consistency) { const bool kGeomConsistencyTerm = true; CALL_SWEEP_FUNC } else { const bool kGeomConsistencyTerm = false; CALL_SWEEP_FUNC } } #undef CALL_SWEEP_FUNC CUDA_SYNC_AND_CHECK(); Rotate(); // Rotate selected image map. if (last_sweep && options_.filter) { std::unique_ptr> rot_consistency_mask_( new GpuMat(cost_map_->GetWidth(), cost_map_->GetHeight(), cost_map_->GetDepth())); consistency_mask_->Rotate(rot_consistency_mask_.get()); consistency_mask_.swap(rot_consistency_mask_); } sweep_timer.Print(" Sweep " + std::to_string(sweep + 1)); } iter_timer.Print("Iteration " + std::to_string(iter + 1)); } total_timer.Print("Total"); } void PatchMatchCuda::ComputeCudaConfig() { sweep_block_size_.x = THREADS_PER_BLOCK; sweep_block_size_.y = 1; sweep_block_size_.z = 1; sweep_grid_size_.x = (depth_map_->GetWidth() - 1) / THREADS_PER_BLOCK + 1; sweep_grid_size_.y = 1; sweep_grid_size_.z = 1; elem_wise_block_size_.x = THREADS_PER_BLOCK; elem_wise_block_size_.y = THREADS_PER_BLOCK; elem_wise_block_size_.z = 1; elem_wise_grid_size_.x = (depth_map_->GetWidth() - 1) / THREADS_PER_BLOCK + 1; elem_wise_grid_size_.y = (depth_map_->GetHeight() - 1) / THREADS_PER_BLOCK + 1; elem_wise_grid_size_.z = 1; } void PatchMatchCuda::BindRefImageTexture() { cudaTextureDesc texture_desc; memset(&texture_desc, 0, sizeof(texture_desc)); texture_desc.addressMode[0] = cudaAddressModeBorder; texture_desc.addressMode[1] = cudaAddressModeBorder; texture_desc.addressMode[2] = cudaAddressModeBorder; texture_desc.filterMode = cudaFilterModePoint; texture_desc.readMode = cudaReadModeNormalizedFloat; texture_desc.normalizedCoords = false; ref_image_texture_ = CudaArrayLayeredTexture::FromGpuMat( texture_desc, *ref_image_->image); } void PatchMatchCuda::InitRefImage() { const Image& ref_image = problem_.images->at(problem_.ref_image_idx); ref_width_ = ref_image.GetWidth(); ref_height_ = ref_image.GetHeight(); // Upload to device and filter. ref_image_.reset(new GpuMatRefImage(ref_width_, ref_height_)); const std::vector ref_image_array = ref_image.GetBitmap().ConvertToRowMajorArray(); ref_image_->Filter(ref_image_array.data(), options_.window_radius, options_.window_step, options_.sigma_spatial, options_.sigma_color); BindRefImageTexture(); } void PatchMatchCuda::InitSourceImages() { // Determine maximum image size. size_t max_width = 0; size_t max_height = 0; for (const auto image_idx : problem_.src_image_idxs) { const Image& image = problem_.images->at(image_idx); if (image.GetWidth() > max_width) { max_width = image.GetWidth(); } if (image.GetHeight() > max_height) { max_height = image.GetHeight(); } } // Upload source images to device. { // Copy source images to contiguous memory block. const uint8_t kDefaultValue = 0; std::vector src_images_host_data( static_cast(max_width * max_height * problem_.src_image_idxs.size()), kDefaultValue); for (size_t i = 0; i < problem_.src_image_idxs.size(); ++i) { const Image& image = problem_.images->at(problem_.src_image_idxs[i]); const Bitmap& bitmap = image.GetBitmap(); uint8_t* dest = src_images_host_data.data() + max_width * max_height * i; for (size_t r = 0; r < image.GetHeight(); ++r) { memcpy(dest, bitmap.GetScanline(r), image.GetWidth() * sizeof(uint8_t)); dest += max_width; } } // Create source images texture. cudaTextureDesc texture_desc; memset(&texture_desc, 0, sizeof(texture_desc)); texture_desc.addressMode[0] = cudaAddressModeBorder; texture_desc.addressMode[1] = cudaAddressModeBorder; texture_desc.addressMode[2] = cudaAddressModeBorder; texture_desc.filterMode = cudaFilterModeLinear; texture_desc.readMode = cudaReadModeNormalizedFloat; texture_desc.normalizedCoords = false; src_images_texture_ = CudaArrayLayeredTexture::FromHostArray( texture_desc, max_width, max_height, problem_.src_image_idxs.size(), src_images_host_data.data()); } // Upload source depth maps to device. if (options_.geom_consistency) { const float kDefaultValue = 0.0f; std::vector src_depth_maps_host_data( static_cast(max_width * max_height * problem_.src_image_idxs.size()), kDefaultValue); for (size_t i = 0; i < problem_.src_image_idxs.size(); ++i) { const DepthMap& depth_map = problem_.depth_maps->at(problem_.src_image_idxs[i]); float* dest = src_depth_maps_host_data.data() + max_width * max_height * i; for (size_t r = 0; r < depth_map.GetHeight(); ++r) { memcpy(dest, depth_map.GetPtr() + r * depth_map.GetWidth(), depth_map.GetWidth() * sizeof(float)); dest += max_width; } } // Create source depth maps texture. cudaTextureDesc texture_desc; memset(&texture_desc, 0, sizeof(texture_desc)); texture_desc.addressMode[0] = cudaAddressModeBorder; texture_desc.addressMode[1] = cudaAddressModeBorder; texture_desc.addressMode[2] = cudaAddressModeBorder; texture_desc.filterMode = cudaFilterModePoint; texture_desc.readMode = cudaReadModeElementType; texture_desc.normalizedCoords = false; src_depth_maps_texture_ = CudaArrayLayeredTexture::FromHostArray( texture_desc, max_width, max_height, problem_.src_image_idxs.size(), src_depth_maps_host_data.data()); } } void PatchMatchCuda::InitTransforms() { const Image& ref_image = problem_.images->at(problem_.ref_image_idx); ////////////////////////////////////////////////////////////////////////////// // Generate rotated versions (counter-clockwise) of calibration matrix. ////////////////////////////////////////////////////////////////////////////// for (size_t i = 0; i < 4; ++i) { ref_K_host_[i][0] = ref_image.GetK()[0]; ref_K_host_[i][1] = ref_image.GetK()[2]; ref_K_host_[i][2] = ref_image.GetK()[4]; ref_K_host_[i][3] = ref_image.GetK()[5]; } // Rotated by 90 degrees. std::swap(ref_K_host_[1][0], ref_K_host_[1][2]); std::swap(ref_K_host_[1][1], ref_K_host_[1][3]); ref_K_host_[1][3] = ref_width_ - 1 - ref_K_host_[1][3]; // Rotated by 180 degrees. ref_K_host_[2][1] = ref_width_ - 1 - ref_K_host_[2][1]; ref_K_host_[2][3] = ref_height_ - 1 - ref_K_host_[2][3]; // Rotated by 270 degrees. std::swap(ref_K_host_[3][0], ref_K_host_[3][2]); std::swap(ref_K_host_[3][1], ref_K_host_[3][3]); ref_K_host_[3][1] = ref_height_ - 1 - ref_K_host_[3][1]; // Extract 1/fx, -cx/fx, fy, -cy/fy. for (size_t i = 0; i < 4; ++i) { ref_inv_K_host_[i][0] = 1.0f / ref_K_host_[i][0]; ref_inv_K_host_[i][1] = -ref_K_host_[i][1] / ref_K_host_[i][0]; ref_inv_K_host_[i][2] = 1.0f / ref_K_host_[i][2]; ref_inv_K_host_[i][3] = -ref_K_host_[i][3] / ref_K_host_[i][2]; } // Bind 0 degrees version to constant global memory. CUDA_SAFE_CALL(cudaMemcpyToSymbol( ref_K, ref_K_host_[0], sizeof(float) * 4, 0, cudaMemcpyHostToDevice)); CUDA_SAFE_CALL(cudaMemcpyToSymbol(ref_inv_K, ref_inv_K_host_[0], sizeof(float) * 4, 0, cudaMemcpyHostToDevice)); ////////////////////////////////////////////////////////////////////////////// // Generate rotated versions of camera poses. ////////////////////////////////////////////////////////////////////////////// float rotated_R[9]; memcpy(rotated_R, ref_image.GetR(), 9 * sizeof(float)); float rotated_T[3]; memcpy(rotated_T, ref_image.GetT(), 3 * sizeof(float)); // Matrix for 90deg rotation around Z-axis in counter-clockwise direction. const float R_z90[9] = {0, 1, 0, -1, 0, 0, 0, 0, 1}; cudaTextureDesc texture_desc; memset(&texture_desc, 0, sizeof(texture_desc)); texture_desc.addressMode[0] = cudaAddressModeBorder; texture_desc.addressMode[1] = cudaAddressModeBorder; texture_desc.addressMode[2] = cudaAddressModeBorder; texture_desc.filterMode = cudaFilterModePoint; texture_desc.readMode = cudaReadModeElementType; texture_desc.normalizedCoords = false; for (size_t i = 0; i < 4; ++i) { const size_t kNumTformParams = 4 + 9 + 3 + 3 + 12 + 12; std::vector poses_host_data(kNumTformParams * problem_.src_image_idxs.size()); int offset = 0; for (const auto image_idx : problem_.src_image_idxs) { const Image& image = problem_.images->at(image_idx); const float K[4] = { image.GetK()[0], image.GetK()[2], image.GetK()[4], image.GetK()[5]}; memcpy(poses_host_data.data() + offset, K, 4 * sizeof(float)); offset += 4; float rel_R[9]; float rel_T[3]; ComputeRelativePose( rotated_R, rotated_T, image.GetR(), image.GetT(), rel_R, rel_T); memcpy(poses_host_data.data() + offset, rel_R, 9 * sizeof(float)); offset += 9; memcpy(poses_host_data.data() + offset, rel_T, 3 * sizeof(float)); offset += 3; float C[3]; ComputeProjectionCenter(rel_R, rel_T, C); memcpy(poses_host_data.data() + offset, C, 3 * sizeof(float)); offset += 3; float P[12]; ComposeProjectionMatrix(image.GetK(), rel_R, rel_T, P); memcpy(poses_host_data.data() + offset, P, 12 * sizeof(float)); offset += 12; float inv_P[12]; ComposeInverseProjectionMatrix(image.GetK(), rel_R, rel_T, inv_P); memcpy(poses_host_data.data() + offset, inv_P, 12 * sizeof(float)); offset += 12; } poses_texture_[i] = CudaArrayLayeredTexture::FromHostArray( texture_desc, kNumTformParams, problem_.src_image_idxs.size(), 1, poses_host_data.data()); RotatePose(R_z90, rotated_R, rotated_T); } } void PatchMatchCuda::InitWorkspaceMemory() { rand_state_map_.reset(new GpuMatPRNG(ref_width_, ref_height_)); depth_map_.reset(new GpuMat(ref_width_, ref_height_)); if (options_.geom_consistency) { const DepthMap& init_depth_map = problem_.depth_maps->at(problem_.ref_image_idx); depth_map_->CopyToDevice(init_depth_map.GetPtr(), init_depth_map.GetWidth() * sizeof(float)); } else { depth_map_->FillWithRandomNumbers( options_.depth_min, options_.depth_max, *rand_state_map_); } normal_map_.reset(new GpuMat(ref_width_, ref_height_, 3)); // Note that it is not necessary to keep the selection probability map in // memory for all pixels. Theoretically, it is possible to incorporate // the temporary selection probabilities in the global_workspace_. // However, it is useful to keep the probabilities for the entire image // in memory, so that it can be exported. sel_prob_map_.reset(new GpuMat( ref_width_, ref_height_, problem_.src_image_idxs.size())); prev_sel_prob_map_.reset(new GpuMat( ref_width_, ref_height_, problem_.src_image_idxs.size())); prev_sel_prob_map_->FillWithScalar(0.5f); cost_map_.reset(new GpuMat( ref_width_, ref_height_, problem_.src_image_idxs.size())); const int ref_max_dim = std::max(ref_width_, ref_height_); global_workspace_.reset( new GpuMat(ref_max_dim, problem_.src_image_idxs.size(), 2)); consistency_mask_.reset(new GpuMat(0, 0, 0)); ComputeCudaConfig(); if (options_.geom_consistency) { const NormalMap& init_normal_map = problem_.normal_maps->at(problem_.ref_image_idx); normal_map_->CopyToDevice(init_normal_map.GetPtr(), init_normal_map.GetWidth() * sizeof(float)); } else { InitNormalMap<<>>( *normal_map_, *rand_state_map_); } } void PatchMatchCuda::Rotate() { rotation_in_half_pi_ = (rotation_in_half_pi_ + 1) % 4; size_t width; size_t height; if (rotation_in_half_pi_ % 2 == 0) { width = ref_width_; height = ref_height_; } else { width = ref_height_; height = ref_width_; } // Rotate random map. { std::unique_ptr rotated_rand_state_map( new GpuMatPRNG(width, height)); rand_state_map_->Rotate(rotated_rand_state_map.get()); rand_state_map_.swap(rotated_rand_state_map); } // Rotate depth map. { std::unique_ptr> rotated_depth_map( new GpuMat(width, height)); depth_map_->Rotate(rotated_depth_map.get()); depth_map_.swap(rotated_depth_map); } // Rotate normal map. { RotateNormalMap<<>>( *normal_map_); std::unique_ptr> rotated_normal_map( new GpuMat(width, height, 3)); normal_map_->Rotate(rotated_normal_map.get()); normal_map_.swap(rotated_normal_map); } // Rotate reference image. { std::unique_ptr rotated_ref_image( new GpuMatRefImage(width, height)); ref_image_->image->Rotate(rotated_ref_image->image.get()); ref_image_->sum_image->Rotate(rotated_ref_image->sum_image.get()); ref_image_->squared_sum_image->Rotate( rotated_ref_image->squared_sum_image.get()); ref_image_.swap(rotated_ref_image); BindRefImageTexture(); } // Rotate selection probability map. prev_sel_prob_map_.reset( new GpuMat(width, height, problem_.src_image_idxs.size())); sel_prob_map_->Rotate(prev_sel_prob_map_.get()); sel_prob_map_.reset( new GpuMat(width, height, problem_.src_image_idxs.size())); // Rotate cost map. { std::unique_ptr> rotated_cost_map( new GpuMat(width, height, problem_.src_image_idxs.size())); cost_map_->Rotate(rotated_cost_map.get()); cost_map_.swap(rotated_cost_map); } // Rotate calibration. CUDA_SAFE_CALL(cudaMemcpyToSymbol(ref_K, ref_K_host_[rotation_in_half_pi_], sizeof(float) * 4, 0, cudaMemcpyHostToDevice)); CUDA_SAFE_CALL(cudaMemcpyToSymbol(ref_inv_K, ref_inv_K_host_[rotation_in_half_pi_], sizeof(float) * 4, 0, cudaMemcpyHostToDevice)); // Recompute Cuda configuration for rotated reference image. ComputeCudaConfig(); } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/patch_match_cuda.h000066400000000000000000000116631454702036400213020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/cuda_texture.h" #include "colmap/mvs/depth_map.h" #include "colmap/mvs/gpu_mat.h" #include "colmap/mvs/gpu_mat_prng.h" #include "colmap/mvs/gpu_mat_ref_image.h" #include "colmap/mvs/image.h" #include "colmap/mvs/normal_map.h" #include "colmap/mvs/patch_match.h" #include #include #include #include namespace colmap { namespace mvs { class PatchMatchCuda { public: PatchMatchCuda(const PatchMatchOptions& options, const PatchMatch::Problem& problem); void Run(); DepthMap GetDepthMap() const; NormalMap GetNormalMap() const; Mat GetSelProbMap() const; std::vector GetConsistentImageIdxs() const; private: template void RunWithWindowSizeAndStep(); void ComputeCudaConfig(); void BindRefImageTexture(); void InitRefImage(); void InitSourceImages(); void InitTransforms(); void InitWorkspaceMemory(); // Rotate reference image by 90 degrees in counter-clockwise direction. void Rotate(); const PatchMatchOptions options_; const PatchMatch::Problem problem_; // Dimensions for sweeping from top to bottom, i.e. one thread per column. dim3 sweep_block_size_; dim3 sweep_grid_size_; // Dimensions for element-wise operations, i.e. one thread per pixel. dim3 elem_wise_block_size_; dim3 elem_wise_grid_size_; // Original (not rotated) dimension of reference image. size_t ref_width_; size_t ref_height_; // Rotation of reference image in pi/2. This is equivalent to the number of // calls to `rotate` mod 4. int rotation_in_half_pi_; // Reference and source image input data. std::unique_ptr> ref_image_texture_; std::unique_ptr> src_images_texture_; std::unique_ptr> src_depth_maps_texture_; // Relative poses from rotated versions of reference image to source images // corresponding to _rotationInHalfPi: // // [S(1), S(2), S(3), ..., S(n)] // // where n is the number of source images and: // // S(i) = [K_i(0, 0), K_i(0, 2), K_i(1, 1), K_i(1, 2), R_i(:), T_i(:) // C_i(:), P(:), P^-1(:)] // // where i denotes the index of the source image and K is its calibration. // R, T, C, P, P^-1 denote the relative rotation, translation, camera // center, projection, and inverse projection from there reference to the // i-th source image. std::unique_ptr> poses_texture_[4]; // Calibration matrix for rotated versions of reference image // as {K[0, 0], K[0, 2], K[1, 1], K[1, 2]} corresponding to _rotationInHalfPi. float ref_K_host_[4][4]; float ref_inv_K_host_[4][4]; // Data for reference image. std::unique_ptr ref_image_; std::unique_ptr> depth_map_; std::unique_ptr> normal_map_; std::unique_ptr> sel_prob_map_; std::unique_ptr> prev_sel_prob_map_; std::unique_ptr> cost_map_; std::unique_ptr rand_state_map_; std::unique_ptr> consistency_mask_; // Shared memory is too small to hold local state for each thread, // so this is workspace memory in global memory. std::unique_ptr> global_workspace_; }; } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/workspace.cc000066400000000000000000000253421454702036400201660ustar00rootroot00000000000000// 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. #include "colmap/mvs/workspace.h" #include "colmap/util/threading.h" #include namespace colmap { namespace mvs { Workspace::Workspace(const Options& options) : options_(options) { StringToLower(&options_.input_type); model_.Read(options_.workspace_path, options_.workspace_format); if (options_.max_image_size > 0) { for (auto& image : model_.images) { image.Downsize(options_.max_image_size, options_.max_image_size); } } depth_map_path_ = EnsureTrailingSlash( JoinPaths(options_.workspace_path, options_.stereo_folder, "depth_maps")); normal_map_path_ = EnsureTrailingSlash(JoinPaths( options_.workspace_path, options_.stereo_folder, "normal_maps")); } std::string Workspace::GetFileName(const int image_idx) const { const auto& image_name = model_.GetImageName(image_idx); return StringPrintf( "%s.%s.bin", image_name.c_str(), options_.input_type.c_str()); } void Workspace::Load(const std::vector& image_names) { const size_t num_images = model_.images.size(); bitmaps_.resize(num_images); depth_maps_.resize(num_images); normal_maps_.resize(num_images); auto LoadWorkspaceData = [&, this](const int image_idx) { const size_t width = model_.images.at(image_idx).GetWidth(); const size_t height = model_.images.at(image_idx).GetHeight(); // Read and rescale bitmap bitmaps_[image_idx] = std::make_unique(); bitmaps_[image_idx]->Read(GetBitmapPath(image_idx), options_.image_as_rgb); if (options_.max_image_size > 0) { bitmaps_[image_idx]->Rescale((int)width, (int)height); } // Read and rescale depth map depth_maps_[image_idx] = std::make_unique(); depth_maps_[image_idx]->Read(GetDepthMapPath(image_idx)); if (options_.max_image_size > 0) { depth_maps_[image_idx]->Downsize(width, height); } // Read and rescale normal map normal_maps_[image_idx] = std::make_unique(); normal_maps_[image_idx]->Read(GetNormalMapPath(image_idx)); if (options_.max_image_size > 0) { normal_maps_[image_idx]->Downsize(width, height); } }; const int num_threads = GetEffectiveNumThreads(options_.num_threads); ThreadPool thread_pool(num_threads); Timer timer; timer.Start(); LOG(INFO) << StringPrintf("Loading workspace data with %d threads...", num_threads); for (size_t i = 0; i < image_names.size(); ++i) { const int image_idx = model_.GetImageIdx(image_names[i]); if (HasBitmap(image_idx) && HasDepthMap(image_idx)) { thread_pool.AddTask(LoadWorkspaceData, image_idx); } else { LOG(WARNING) << StringPrintf( "Ignoring image %s, because input does not exist.", image_names[i].c_str()); } } thread_pool.Wait(); timer.PrintMinutes(); } const Bitmap& Workspace::GetBitmap(const int image_idx) { return *bitmaps_[image_idx]; } const DepthMap& Workspace::GetDepthMap(const int image_idx) { return *depth_maps_[image_idx]; } const NormalMap& Workspace::GetNormalMap(const int image_idx) { return *normal_maps_[image_idx]; } std::string Workspace::GetBitmapPath(const int image_idx) const { return model_.images.at(image_idx).GetPath(); } std::string Workspace::GetDepthMapPath(const int image_idx) const { return depth_map_path_ + GetFileName(image_idx); } std::string Workspace::GetNormalMapPath(const int image_idx) const { return normal_map_path_ + GetFileName(image_idx); } bool Workspace::HasBitmap(const int image_idx) const { return ExistsFile(GetBitmapPath(image_idx)); } bool Workspace::HasDepthMap(const int image_idx) const { return ExistsFile(GetDepthMapPath(image_idx)); } bool Workspace::HasNormalMap(const int image_idx) const { return ExistsFile(GetNormalMapPath(image_idx)); } CachedWorkspace::CachedImage::CachedImage(CachedImage&& other) noexcept { num_bytes = other.num_bytes; bitmap = std::move(other.bitmap); depth_map = std::move(other.depth_map); normal_map = std::move(other.normal_map); } CachedWorkspace::CachedImage& CachedWorkspace::CachedImage::operator=( CachedImage&& other) noexcept { if (this != &other) { num_bytes = other.num_bytes; bitmap = std::move(other.bitmap); depth_map = std::move(other.depth_map); normal_map = std::move(other.normal_map); } return *this; } CachedWorkspace::CachedWorkspace(const Options& options) : Workspace(options), cache_((size_t)(1024.0 * 1024.0 * 1024.0 * options.cache_size), [](const int) { return CachedImage(); }) {} const Bitmap& CachedWorkspace::GetBitmap(const int image_idx) { auto& cached_image = cache_.GetMutable(image_idx); if (!cached_image.bitmap) { cached_image.bitmap = std::make_unique(); cached_image.bitmap->Read(GetBitmapPath(image_idx), options_.image_as_rgb); if (options_.max_image_size > 0) { cached_image.bitmap->Rescale(model_.images.at(image_idx).GetWidth(), model_.images.at(image_idx).GetHeight()); } cached_image.num_bytes += cached_image.bitmap->NumBytes(); cache_.UpdateNumBytes(image_idx); } return *cached_image.bitmap; } const DepthMap& CachedWorkspace::GetDepthMap(const int image_idx) { auto& cached_image = cache_.GetMutable(image_idx); if (!cached_image.depth_map) { cached_image.depth_map = std::make_unique(); cached_image.depth_map->Read(GetDepthMapPath(image_idx)); if (options_.max_image_size > 0) { cached_image.depth_map->Downsize(model_.images.at(image_idx).GetWidth(), model_.images.at(image_idx).GetHeight()); } cached_image.num_bytes += cached_image.depth_map->GetNumBytes(); cache_.UpdateNumBytes(image_idx); } return *cached_image.depth_map; } const NormalMap& CachedWorkspace::GetNormalMap(const int image_idx) { auto& cached_image = cache_.GetMutable(image_idx); if (!cached_image.normal_map) { cached_image.normal_map = std::make_unique(); cached_image.normal_map->Read(GetNormalMapPath(image_idx)); if (options_.max_image_size > 0) { cached_image.normal_map->Downsize( model_.images.at(image_idx).GetWidth(), model_.images.at(image_idx).GetHeight()); } cached_image.num_bytes += cached_image.normal_map->GetNumBytes(); cache_.UpdateNumBytes(image_idx); } return *cached_image.normal_map; } void ImportPMVSWorkspace(const Workspace& workspace, const std::string& option_name) { const std::string& workspace_path = workspace.GetOptions().workspace_path; const std::string& stereo_folder = workspace.GetOptions().stereo_folder; CreateDirIfNotExists(JoinPaths(workspace_path, stereo_folder)); CreateDirIfNotExists(JoinPaths(workspace_path, stereo_folder, "depth_maps")); CreateDirIfNotExists(JoinPaths(workspace_path, stereo_folder, "normal_maps")); CreateDirIfNotExists( JoinPaths(workspace_path, stereo_folder, "consistency_graphs")); const auto option_lines = ReadTextFileLines(JoinPaths(workspace_path, option_name)); for (const auto& line : option_lines) { if (!StringStartsWith(line, "timages")) { continue; } const auto elems = StringSplit(line, " "); int num_images = std::stoull(elems[1]); std::vector image_idxs; if (num_images == -1) { CHECK_EQ(elems.size(), 4); const int range_lower = std::stoull(elems[2]); const int range_upper = std::stoull(elems[3]); CHECK_LT(range_lower, range_upper); num_images = range_upper - range_lower; image_idxs.resize(num_images); std::iota(image_idxs.begin(), image_idxs.end(), range_lower); } else { CHECK_EQ(num_images + 2, elems.size()); image_idxs.reserve(num_images); for (size_t i = 2; i < elems.size(); ++i) { const int image_idx = std::stoull(elems[i]); image_idxs.push_back(image_idx); } } std::vector image_names; image_names.reserve(num_images); for (const auto image_idx : image_idxs) { const std::string image_name = workspace.GetModel().GetImageName(image_idx); image_names.push_back(image_name); } const auto& overlapping_images = workspace.GetModel().GetMaxOverlappingImagesFromPMVS(); const auto patch_match_path = JoinPaths(workspace_path, stereo_folder, "patch-match.cfg"); const auto fusion_path = JoinPaths(workspace_path, stereo_folder, "fusion.cfg"); std::ofstream patch_match_file(patch_match_path, std::ios::trunc); std::ofstream fusion_file(fusion_path, std::ios::trunc); CHECK(patch_match_file.is_open()) << patch_match_path; CHECK(fusion_file.is_open()) << fusion_path; for (size_t i = 0; i < image_names.size(); ++i) { const auto& ref_image_name = image_names[i]; patch_match_file << ref_image_name << "\n"; if (overlapping_images.empty()) { patch_match_file << "__auto__, 20\n"; } else { for (const int image_idx : overlapping_images[i]) { patch_match_file << workspace.GetModel().GetImageName(image_idx) << ", "; } patch_match_file << "\n"; } fusion_file << ref_image_name << "\n"; } } } } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/mvs/workspace.h000066400000000000000000000114531454702036400200260ustar00rootroot00000000000000// 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. #pragma once #include "colmap/mvs/consistency_graph.h" #include "colmap/mvs/depth_map.h" #include "colmap/mvs/model.h" #include "colmap/mvs/normal_map.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/cache.h" #include "colmap/util/misc.h" namespace colmap { namespace mvs { class Workspace { public: struct Options { // The maximum cache size in gigabytes. double cache_size = 32.0; // The number of threads to use when pre-loading workspace. int num_threads = -1; // Maximum image size in either dimension. int max_image_size = -1; // Whether to read image as RGB or gray scale. bool image_as_rgb = true; // Location and type of workspace. std::string workspace_path; std::string workspace_format; std::string input_type; std::string stereo_folder = "stereo"; }; explicit Workspace(const Options& options); virtual ~Workspace() = default; // Do nothing when we use a cache. Data is loaded as needed. virtual void Load(const std::vector& image_names); inline const Options& GetOptions() const { return options_; } inline const Model& GetModel() const { return model_; } virtual const Bitmap& GetBitmap(int image_idx); virtual const DepthMap& GetDepthMap(int image_idx); virtual const NormalMap& GetNormalMap(int image_idx); // Get paths to bitmap, depth map, normal map and consistency graph. std::string GetBitmapPath(int image_idx) const; std::string GetDepthMapPath(int image_idx) const; std::string GetNormalMapPath(int image_idx) const; // Return whether bitmap, depth map, normal map, and consistency graph exist. bool HasBitmap(int image_idx) const; bool HasDepthMap(int image_idx) const; bool HasNormalMap(int image_idx) const; protected: std::string GetFileName(int image_idx) const; Options options_; Model model_; private: std::string depth_map_path_; std::string normal_map_path_; std::vector> bitmaps_; std::vector> depth_maps_; std::vector> normal_maps_; }; class CachedWorkspace : public Workspace { public: explicit CachedWorkspace(const Options& options); void Load(const std::vector& image_names) override {} inline void ClearCache() { cache_.Clear(); } const Bitmap& GetBitmap(int image_idx) override; const DepthMap& GetDepthMap(int image_idx) override; const NormalMap& GetNormalMap(int image_idx) override; private: class CachedImage { public: CachedImage() {} CachedImage(CachedImage&& other) noexcept; CachedImage& operator=(CachedImage&& other) noexcept; inline size_t NumBytes() const { return num_bytes; } size_t num_bytes = 0; std::unique_ptr bitmap; std::unique_ptr depth_map; std::unique_ptr normal_map; private: NON_COPYABLE(CachedImage) }; MemoryConstrainedLRUCache cache_; }; // Import a PMVS workspace into the COLMAP workspace format. Only images in the // provided option file name will be imported and used for reconstruction. void ImportPMVSWorkspace(const Workspace& workspace, const std::string& option_name); } // namespace mvs } // namespace colmap colmap-3.9.1/src/colmap/optim/000077500000000000000000000000001454702036400161765ustar00rootroot00000000000000colmap-3.9.1/src/colmap/optim/CMakeLists.txt000066400000000000000000000053731454702036400207460ustar00rootroot00000000000000# 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. set(FOLDER_NAME "optim") COLMAP_ADD_LIBRARY( NAME colmap_optim SRCS combination_sampler.h combination_sampler.cc least_absolute_deviations.h least_absolute_deviations.cc progressive_sampler.h progressive_sampler.cc random_sampler.h random_sampler.cc sprt.h sprt.cc support_measurement.h support_measurement.cc PUBLIC_LINK_LIBS colmap_math Eigen3::Eigen ) COLMAP_ADD_TEST( NAME combination_sampler_test SRCS combination_sampler_test.cc LINK_LIBS colmap_optim ) COLMAP_ADD_TEST( NAME least_absolute_deviations_test SRCS least_absolute_deviations_test.cc LINK_LIBS colmap_optim ) COLMAP_ADD_TEST( NAME loransac_test SRCS loransac_test.cc LINK_LIBS colmap_optim ) COLMAP_ADD_TEST( NAME progressive_sampler_test SRCS progressive_sampler_test.cc LINK_LIBS colmap_optim ) COLMAP_ADD_TEST( NAME random_sampler_test SRCS random_sampler_test.cc LINK_LIBS colmap_optim ) COLMAP_ADD_TEST( NAME ransac_test SRCS ransac_test.cc LINK_LIBS colmap_optim ) COLMAP_ADD_TEST( NAME support_measurement_test SRCS support_measurement_test.cc LINK_LIBS colmap_optim ) colmap-3.9.1/src/colmap/optim/combination_sampler.cc000066400000000000000000000055641454702036400225440ustar00rootroot00000000000000// 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. #include "colmap/optim/combination_sampler.h" #include "colmap/math/math.h" #include "colmap/math/random.h" #include namespace colmap { CombinationSampler::CombinationSampler(const size_t num_samples) : num_samples_(num_samples) {} void CombinationSampler::Initialize(const size_t total_num_samples) { CHECK_LE(num_samples_, total_num_samples); total_sample_idxs_.resize(total_num_samples); // Note that the samples must be in increasing order for `NextCombination`. std::iota(total_sample_idxs_.begin(), total_sample_idxs_.end(), 0); } size_t CombinationSampler::MaxNumSamples() { return NChooseK(total_sample_idxs_.size(), num_samples_); } void CombinationSampler::Sample(std::vector* sampled_idxs) { sampled_idxs->resize(num_samples_); for (size_t i = 0; i < num_samples_; ++i) { (*sampled_idxs)[i] = total_sample_idxs_[i]; } if (!NextCombination(total_sample_idxs_.begin(), total_sample_idxs_.begin() + num_samples_, total_sample_idxs_.end())) { // Reached all possible combinations, so reset to original state. // Note that the samples must be in increasing order for `NextCombination`. std::iota(total_sample_idxs_.begin(), total_sample_idxs_.end(), 0); } } } // namespace colmap colmap-3.9.1/src/colmap/optim/combination_sampler.h000066400000000000000000000043361454702036400224020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/optim/sampler.h" namespace colmap { // Random sampler for RANSAC-based methods that generates unique samples. // // Note that a separate sampler should be instantiated per thread and it assumes // that the input data is shuffled in advance. class CombinationSampler : public Sampler { public: explicit CombinationSampler(size_t num_samples); void Initialize(size_t total_num_samples) override; size_t MaxNumSamples() override; void Sample(std::vector* sampled_idxs) override; private: const size_t num_samples_; std::vector total_sample_idxs_; }; } // namespace colmap colmap-3.9.1/src/colmap/optim/combination_sampler_test.cc000066400000000000000000000056471454702036400236050ustar00rootroot00000000000000// 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. #include "colmap/optim/combination_sampler.h" #include "colmap/math/math.h" #include #include namespace colmap { namespace { TEST(CombinationSampler, LessSamples) { CombinationSampler sampler(2); sampler.Initialize(5); EXPECT_EQ(sampler.MaxNumSamples(), 10); std::vector> sample_sets; for (size_t i = 0; i < 10; ++i) { std::vector samples; sampler.Sample(&samples); EXPECT_EQ(samples.size(), 2); sample_sets.emplace_back(samples.begin(), samples.end()); EXPECT_EQ(sample_sets.back().size(), 2); for (size_t j = 0; j < i; ++j) { EXPECT_TRUE(sample_sets[j].count(samples[0]) == 0 || sample_sets[j].count(samples[1]) == 0); } } std::vector samples; sampler.Sample(&samples); EXPECT_TRUE(sample_sets[0].count(samples[0]) == 1 && sample_sets[0].count(samples[1]) == 1); } TEST(CombinationSampler, EqualSamples) { CombinationSampler sampler(5); sampler.Initialize(5); EXPECT_EQ(sampler.MaxNumSamples(), 1); for (size_t i = 0; i < 100; ++i) { std::vector samples; sampler.Sample(&samples); EXPECT_EQ(samples.size(), 5); EXPECT_EQ(std::unordered_set(samples.begin(), samples.end()).size(), 5); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/optim/least_absolute_deviations.cc000066400000000000000000000076401454702036400237470ustar00rootroot00000000000000// 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. #include "colmap/optim/least_absolute_deviations.h" #include "colmap/util/eigen_alignment.h" #include namespace colmap { namespace { Eigen::VectorXd Shrinkage(const Eigen::VectorXd& a, const double kappa) { const Eigen::VectorXd a_plus_kappa = a.array() + kappa; const Eigen::VectorXd a_minus_kappa = a.array() - kappa; return a_plus_kappa.cwiseMin(0) + a_minus_kappa.cwiseMax(0); } } // namespace bool SolveLeastAbsoluteDeviations(const LeastAbsoluteDeviationsOptions& options, const Eigen::SparseMatrix& A, const Eigen::VectorXd& b, Eigen::VectorXd* x) { CHECK_NOTNULL(x); CHECK_GT(options.rho, 0); CHECK_GT(options.alpha, 0); CHECK_GT(options.max_num_iterations, 0); CHECK_GE(options.absolute_tolerance, 0); CHECK_GE(options.relative_tolerance, 0); Eigen::SimplicialLLT> linear_solver; linear_solver.compute(A.transpose() * A); Eigen::VectorXd z = Eigen::VectorXd::Zero(A.rows()); Eigen::VectorXd z_old(A.rows()); Eigen::VectorXd u = Eigen::VectorXd::Zero(A.rows()); Eigen::VectorXd Ax(A.rows()); Eigen::VectorXd Ax_hat(A.rows()); const double b_norm = b.norm(); const double eps_pri_threshold = std::sqrt(A.rows()) * options.absolute_tolerance; const double eps_dual_threshold = std::sqrt(A.cols()) * options.absolute_tolerance; for (int i = 0; i < options.max_num_iterations; ++i) { *x = linear_solver.solve(A.transpose() * (b + z - u)); if (linear_solver.info() != Eigen::Success) { return false; } Ax = A * *x; Ax_hat = options.alpha * Ax + (1 - options.alpha) * (z + b); z_old = z; z = Shrinkage(Ax_hat - b + u, 1 / options.rho); u += Ax_hat - z - b; const double r_norm = (Ax - z - b).norm(); const double s_norm = (-options.rho * A.transpose() * (z - z_old)).norm(); const double eps_pri = eps_pri_threshold + options.relative_tolerance * std::max(b_norm, std::max(Ax.norm(), z.norm())); const double eps_dual = eps_dual_threshold + options.relative_tolerance * (options.rho * A.transpose() * u).norm(); if (r_norm < eps_pri && s_norm < eps_dual) { break; } } return true; } } // namespace colmap colmap-3.9.1/src/colmap/optim/least_absolute_deviations.h000066400000000000000000000056601454702036400236110ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include #include namespace colmap { struct LeastAbsoluteDeviationsOptions { // Augmented Lagrangian parameter. double rho = 1.0; // Over-relaxation parameter, typical values are between 1.0 and 1.8. double alpha = 1.0; // Maximum solver iterations. int max_num_iterations = 1000; // Absolute and relative solution thresholds, as suggested by Boyd et al. double absolute_tolerance = 1e-4; double relative_tolerance = 1e-2; }; // Least absolute deviations (LAD) fitting via ADMM by solving the problem: // // min || A x - b ||_1 // // The solution is returned in the vector x and the iterative solver is // initialized with the given value. This implementation is based on the paper // "Distributed Optimization and Statistical Learning via the Alternating // Direction Method of Multipliers" by Boyd et al. and the Matlab implementation // at https://web.stanford.edu/~boyd/papers/admm/least_abs_deviations/lad.html bool SolveLeastAbsoluteDeviations(const LeastAbsoluteDeviationsOptions& options, const Eigen::SparseMatrix& A, const Eigen::VectorXd& b, Eigen::VectorXd* x); } // namespace colmap colmap-3.9.1/src/colmap/optim/least_absolute_deviations_test.cc000066400000000000000000000072261454702036400250060ustar00rootroot00000000000000// 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. #include "colmap/optim/least_absolute_deviations.h" #include "colmap/math/random.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(SolveLeastAbsoluteDeviations, OverDetermined) { Eigen::SparseMatrix A(4, 3); for (int i = 0; i < A.rows(); ++i) { for (int j = 0; j < A.cols(); ++j) { A.insert(i, j) = i * A.cols() + j + 1; } } A.coeffRef(0, 0) = 10; Eigen::VectorXd b(A.rows()); for (int i = 0; i < b.size(); ++i) { b(i) = i + 1; } Eigen::VectorXd x = Eigen::VectorXd::Zero(A.cols()); LeastAbsoluteDeviationsOptions options; EXPECT_TRUE(SolveLeastAbsoluteDeviations(options, A, b, &x)); // Reference solution obtained with Boyd's Matlab implementation. const Eigen::Vector3d x_ref(0, 0, 1 / 3.0); EXPECT_TRUE(x.isApprox(x_ref)); const Eigen::VectorXd residual = A * x - b; EXPECT_LE(residual.norm(), 1e-6); } TEST(SolveLeastAbsoluteDeviations, WellDetermined) { Eigen::SparseMatrix A(3, 3); for (int i = 0; i < A.rows(); ++i) { for (int j = 0; j < A.cols(); ++j) { A.insert(i, j) = i * A.cols() + j + 1; } } A.coeffRef(0, 0) = 10; Eigen::VectorXd b(A.rows()); for (int i = 0; i < b.size(); ++i) { b(i) = i + 1; } Eigen::VectorXd x = Eigen::VectorXd::Zero(A.cols()); LeastAbsoluteDeviationsOptions options; EXPECT_TRUE(SolveLeastAbsoluteDeviations(options, A, b, &x)); // Reference solution obtained with Boyd's Matlab implementation. const Eigen::Vector3d x_ref(0, 0, 1 / 3.0); EXPECT_TRUE(x.isApprox(x_ref)); const Eigen::VectorXd residual = A * x - b; EXPECT_LE(residual.norm(), 1e-6); } TEST(SolveLeastAbsoluteDeviations, UnderDetermined) { // In this case, the system is rank-deficient and not positive semi-definite. Eigen::SparseMatrix A(2, 3); Eigen::VectorXd b(A.rows()); Eigen::VectorXd x = Eigen::VectorXd::Zero(A.cols()); LeastAbsoluteDeviationsOptions options; EXPECT_FALSE(SolveLeastAbsoluteDeviations(options, A, b, &x)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/optim/loransac.h000066400000000000000000000222401454702036400201510ustar00rootroot00000000000000// 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. #pragma once #include "colmap/optim/random_sampler.h" #include "colmap/optim/ransac.h" #include "colmap/optim/support_measurement.h" #include "colmap/util/logging.h" #include #include #include #include namespace colmap { // Implementation of LO-RANSAC (Locally Optimized RANSAC). // // "Locally Optimized RANSAC" Ondrej Chum, Jiri Matas, Josef Kittler, DAGM 2003. template class LORANSAC : public RANSAC { public: using typename RANSAC::Report; explicit LORANSAC(const RANSACOptions& options); // Robustly estimate model with RANSAC (RANdom SAmple Consensus). // // @param X Independent variables. // @param Y Dependent variables. // // @return The report with the results of the estimation. Report Estimate(const std::vector& X, const std::vector& Y); // Objects used in RANSAC procedure. using RANSAC::estimator; LocalEstimator local_estimator; using RANSAC::sampler; using RANSAC::support_measurer; private: using RANSAC::options_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template LORANSAC::LORANSAC( const RANSACOptions& options) : RANSAC(options) {} template typename LORANSAC::Report LORANSAC::Estimate( const std::vector& X, const std::vector& Y) { CHECK_EQ(X.size(), Y.size()); const size_t num_samples = X.size(); typename RANSAC::Report report; report.success = false; report.num_trials = 0; if (num_samples < Estimator::kMinNumSamples) { return report; } typename SupportMeasurer::Support best_support; typename Estimator::M_t best_model; bool best_model_is_local = false; bool abort = false; const double max_residual = options_.max_error * options_.max_error; std::vector residuals; std::vector best_local_residuals; std::vector X_inlier; std::vector Y_inlier; std::vector X_rand(Estimator::kMinNumSamples); std::vector Y_rand(Estimator::kMinNumSamples); std::vector sample_models; std::vector local_models; sampler.Initialize(num_samples); size_t max_num_trials = std::min(options_.max_num_trials, sampler.MaxNumSamples()); size_t dyn_max_num_trials = max_num_trials; const size_t min_num_trials = options_.min_num_trials; for (report.num_trials = 0; report.num_trials < max_num_trials; ++report.num_trials) { if (abort) { report.num_trials += 1; break; } sampler.SampleXY(X, Y, &X_rand, &Y_rand); // Estimate model for current subset. estimator.Estimate(X_rand, Y_rand, &sample_models); // Iterate through all estimated models for (const auto& sample_model : sample_models) { estimator.Residuals(X, Y, sample_model, &residuals); CHECK_EQ(residuals.size(), num_samples); const auto support = support_measurer.Evaluate(residuals, max_residual); // Do local optimization if better than all previous subsets. if (support_measurer.Compare(support, best_support)) { best_support = support; best_model = sample_model; best_model_is_local = false; // Estimate locally optimized model from inliers. if (support.num_inliers > Estimator::kMinNumSamples && support.num_inliers >= LocalEstimator::kMinNumSamples) { // Recursive local optimization to expand inlier set. const size_t kMaxNumLocalTrials = 10; for (size_t local_num_trials = 0; local_num_trials < kMaxNumLocalTrials; ++local_num_trials) { X_inlier.clear(); Y_inlier.clear(); X_inlier.reserve(num_samples); Y_inlier.reserve(num_samples); for (size_t i = 0; i < residuals.size(); ++i) { if (residuals[i] <= max_residual) { X_inlier.push_back(X[i]); Y_inlier.push_back(Y[i]); } } local_estimator.Estimate(X_inlier, Y_inlier, &local_models); const size_t prev_best_num_inliers = best_support.num_inliers; for (const auto& local_model : local_models) { local_estimator.Residuals(X, Y, local_model, &residuals); CHECK_EQ(residuals.size(), num_samples); const auto local_support = support_measurer.Evaluate(residuals, max_residual); // Check if locally optimized model is better. if (support_measurer.Compare(local_support, best_support)) { best_support = local_support; best_model = local_model; best_model_is_local = true; std::swap(residuals, best_local_residuals); } } // Only continue recursive local optimization, if the inlier set // size increased and we thus have a chance to further improve. if (best_support.num_inliers <= prev_best_num_inliers) { break; } // Swap back the residuals, so we can extract the best inlier // set in the next recursion of local optimization. std::swap(residuals, best_local_residuals); } } dyn_max_num_trials = RANSAC::ComputeNumTrials( best_support.num_inliers, num_samples, options_.confidence, options_.dyn_num_trials_multiplier); } if (report.num_trials >= dyn_max_num_trials && report.num_trials >= min_num_trials) { abort = true; break; } } } report.support = best_support; report.model = best_model; // No valid model was found if (report.support.num_inliers < estimator.kMinNumSamples) { return report; } report.success = true; // Determine inlier mask. Note that this calculates the residuals for the // best model twice, but saves to copy and fill the inlier mask for each // evaluated model. Some benchmarking revealed that this approach is faster. if (best_model_is_local) { local_estimator.Residuals(X, Y, report.model, &residuals); } else { estimator.Residuals(X, Y, report.model, &residuals); } CHECK_EQ(residuals.size(), num_samples); report.inlier_mask.resize(num_samples); for (size_t i = 0; i < residuals.size(); ++i) { report.inlier_mask[i] = residuals[i] <= max_residual; } return report; } } // namespace colmap colmap-3.9.1/src/colmap/optim/loransac_test.cc000066400000000000000000000076401454702036400213550ustar00rootroot00000000000000// 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. #include "colmap/optim/loransac.h" #include "colmap/estimators/similarity_transform.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/sim3.h" #include "colmap/math/random.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { namespace { TEST(LORANSAC, Report) { LORANSAC, SimilarityTransformEstimator<3>>::Report report; EXPECT_FALSE(report.success); EXPECT_EQ(report.num_trials, 0); EXPECT_EQ(report.support.num_inliers, 0); EXPECT_EQ(report.support.residual_sum, std::numeric_limits::max()); EXPECT_EQ(report.inlier_mask.size(), 0); } TEST(LORANSAC, SimilarityTransform) { SetPRNGSeed(0); const size_t num_samples = 1000; const size_t num_outliers = 400; // Create some arbitrary transformation. const Sim3d expectedTgtFromSrc( 2, Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d(100, 10, 10)); // Generate exact data std::vector src; std::vector tgt; for (size_t i = 0; i < num_samples; ++i) { src.emplace_back(i, std::sqrt(i) + 2, std::sqrt(2 * i + 2)); tgt.push_back(expectedTgtFromSrc * src.back()); } // Add some faulty data. for (size_t i = 0; i < num_outliers; ++i) { tgt[i] = Eigen::Vector3d(RandomUniformReal(-3000.0, -2000.0), RandomUniformReal(-4000.0, -3000.0), RandomUniformReal(-5000.0, -4000.0)); } // Robustly estimate transformation using RANSAC. RANSACOptions options; options.max_error = 10; LORANSAC, SimilarityTransformEstimator<3>> ransac(options); const auto report = ransac.Estimate(src, tgt); EXPECT_TRUE(report.success); EXPECT_GT(report.num_trials, 0); // Make sure outliers were detected correctly. EXPECT_EQ(report.support.num_inliers, num_samples - num_outliers); for (size_t i = 0; i < num_samples; ++i) { if (i < num_outliers) { EXPECT_FALSE(report.inlier_mask[i]); } else { EXPECT_TRUE(report.inlier_mask[i]); } } // Make sure original transformation is estimated correctly. const double matrix_diff = (expectedTgtFromSrc.ToMatrix() - report.model).norm(); EXPECT_LT(matrix_diff, 1e-6); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/optim/progressive_sampler.cc000066400000000000000000000075201454702036400226040ustar00rootroot00000000000000// 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. #include "colmap/optim/progressive_sampler.h" #include "colmap/math/random.h" #include "colmap/util/misc.h" #include namespace colmap { ProgressiveSampler::ProgressiveSampler(const size_t num_samples) : num_samples_(num_samples), total_num_samples_(0), t_(0), n_(0), T_n_(0), T_n_p_(0) {} void ProgressiveSampler::Initialize(const size_t total_num_samples) { CHECK_LE(num_samples_, total_num_samples); total_num_samples_ = total_num_samples; t_ = 0; n_ = num_samples_; // Number of iterations before PROSAC behaves like RANSAC. Default value // is chosen according to the recommended value in the paper. const size_t kNumProgressiveIterations = 200000; // Compute T_n using recurrent relation in equation 3 (first part). T_n_ = kNumProgressiveIterations; T_n_p_ = 1.0; for (size_t i = 0; i < num_samples_; ++i) { T_n_ *= static_cast(num_samples_ - i) / (total_num_samples_ - i); } } size_t ProgressiveSampler::MaxNumSamples() { return std::numeric_limits::max(); } void ProgressiveSampler::Sample(std::vector* sampled_idxs) { t_ += 1; sampled_idxs->clear(); sampled_idxs->reserve(num_samples_); // Compute T_n_p_ using recurrent relation in equation 3 (second part). if (t_ == T_n_p_ && n_ < total_num_samples_) { const double T_n_plus_1 = T_n_ * (n_ + 1.0) / (n_ + 1.0 - num_samples_); T_n_p_ += std::ceil(T_n_plus_1 - T_n_); T_n_ = T_n_plus_1; n_ += 1; } // Decide how many samples to draw from which part of the data as // specified in equation 5. size_t num_random_samples = num_samples_; size_t max_random_sample_idx = n_ - 1; if (T_n_p_ >= t_) { num_random_samples -= 1; max_random_sample_idx -= 1; } // Draw semi-random samples as described in algorithm 1. for (size_t i = 0; i < num_random_samples; ++i) { while (true) { const size_t random_idx = RandomUniformInteger(0, max_random_sample_idx); if (!VectorContainsValue(*sampled_idxs, random_idx)) { sampled_idxs->push_back(random_idx); break; } } } // In progressive sampling mode, the last element is mandatory. if (T_n_p_ >= t_) { sampled_idxs->push_back(n_); } } } // namespace colmap colmap-3.9.1/src/colmap/optim/progressive_sampler.h000066400000000000000000000051541454702036400224470ustar00rootroot00000000000000// 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. #pragma once #include "colmap/optim/sampler.h" namespace colmap { // Random sampler for PROSAC (Progressive Sample Consensus), as described in: // // "Matching with PROSAC - Progressive Sample Consensus". // Ondrej Chum and Matas, CVPR 2005. // // Note that a separate sampler should be instantiated per thread and that the // data to be sampled from is assumed to be sorted according to the quality // function in descending order, i.e., higher quality data is closer to the // front of the list. class ProgressiveSampler : public Sampler { public: explicit ProgressiveSampler(size_t num_samples); void Initialize(size_t total_num_samples) override; size_t MaxNumSamples() override; void Sample(std::vector* sampled_idxs) override; private: const size_t num_samples_; size_t total_num_samples_; // The number of generated samples, i.e. the number of calls to `Sample`. size_t t_; size_t n_; // Variables defined in equation 3. double T_n_; double T_n_p_; }; } // namespace colmap colmap-3.9.1/src/colmap/optim/progressive_sampler_test.cc000066400000000000000000000060371454702036400236450ustar00rootroot00000000000000// 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. #include "colmap/optim/progressive_sampler.h" #include #include namespace colmap { namespace { TEST(ProgressiveSampler, LessSamples) { ProgressiveSampler sampler(2); sampler.Initialize(5); EXPECT_EQ(sampler.MaxNumSamples(), std::numeric_limits::max()); for (size_t i = 0; i < 100; ++i) { std::vector samples; sampler.Sample(&samples); EXPECT_EQ(samples.size(), 2); EXPECT_EQ(std::unordered_set(samples.begin(), samples.end()).size(), 2); } } TEST(ProgressiveSampler, EqualSamples) { ProgressiveSampler sampler(5); sampler.Initialize(5); EXPECT_EQ(sampler.MaxNumSamples(), std::numeric_limits::max()); for (size_t i = 0; i < 100; ++i) { std::vector samples; sampler.Sample(&samples); EXPECT_EQ(samples.size(), 5); EXPECT_EQ(std::unordered_set(samples.begin(), samples.end()).size(), 5); } } TEST(ProgressiveSampler, Progressive) { const size_t kNumSamples = 5; ProgressiveSampler sampler(kNumSamples); sampler.Initialize(50); size_t prev_last_sample = 5; for (size_t i = 0; i < 100; ++i) { std::vector samples; sampler.Sample(&samples); for (size_t i = 0; i < samples.size() - 1; ++i) { EXPECT_LT(samples[i], samples.back()); EXPECT_GE(samples.back(), prev_last_sample); prev_last_sample = samples.back(); } } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/optim/random_sampler.cc000066400000000000000000000045671454702036400215240ustar00rootroot00000000000000// 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. #include "colmap/optim/random_sampler.h" #include "colmap/math/random.h" #include namespace colmap { RandomSampler::RandomSampler(const size_t num_samples) : num_samples_(num_samples) {} void RandomSampler::Initialize(const size_t total_num_samples) { CHECK_LE(num_samples_, total_num_samples); sample_idxs_.resize(total_num_samples); std::iota(sample_idxs_.begin(), sample_idxs_.end(), 0); } size_t RandomSampler::MaxNumSamples() { return std::numeric_limits::max(); } void RandomSampler::Sample(std::vector* sampled_idxs) { Shuffle(static_cast(num_samples_), &sample_idxs_); sampled_idxs->resize(num_samples_); for (size_t i = 0; i < num_samples_; ++i) { (*sampled_idxs)[i] = sample_idxs_[i]; } } } // namespace colmap colmap-3.9.1/src/colmap/optim/random_sampler.h000066400000000000000000000041631454702036400213560ustar00rootroot00000000000000// 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. #pragma once #include "colmap/optim/sampler.h" namespace colmap { // Random sampler for RANSAC-based methods. // // Note that a separate sampler should be instantiated per thread. class RandomSampler : public Sampler { public: explicit RandomSampler(size_t num_samples); void Initialize(size_t total_num_samples) override; size_t MaxNumSamples() override; void Sample(std::vector* sampled_idxs) override; private: const size_t num_samples_; std::vector sample_idxs_; }; } // namespace colmap colmap-3.9.1/src/colmap/optim/random_sampler_test.cc000066400000000000000000000050521454702036400225510ustar00rootroot00000000000000// 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. #include "colmap/optim/random_sampler.h" #include #include namespace colmap { namespace { TEST(RandomSampler, LessSamples) { RandomSampler sampler(2); sampler.Initialize(5); EXPECT_EQ(sampler.MaxNumSamples(), std::numeric_limits::max()); for (size_t i = 0; i < 100; ++i) { std::vector samples; sampler.Sample(&samples); EXPECT_EQ(samples.size(), 2); EXPECT_EQ(std::unordered_set(samples.begin(), samples.end()).size(), 2); } } TEST(RandomSampler, EqualSamples) { RandomSampler sampler(5); sampler.Initialize(5); EXPECT_EQ(sampler.MaxNumSamples(), std::numeric_limits::max()); for (size_t i = 0; i < 100; ++i) { std::vector samples; sampler.Sample(&samples); EXPECT_EQ(samples.size(), 5); EXPECT_EQ(std::unordered_set(samples.begin(), samples.end()).size(), 5); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/optim/ransac.h000066400000000000000000000227621454702036400176270ustar00rootroot00000000000000// 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. #pragma once #include "colmap/optim/random_sampler.h" #include "colmap/optim/support_measurement.h" #include "colmap/util/logging.h" #include #include #include #include namespace colmap { struct RANSACOptions { // Maximum error for a sample to be considered as an inlier. Note that // the residual of an estimator corresponds to a squared error. double max_error = 0.0; // A priori assumed minimum inlier ratio, which determines the maximum number // of iterations. Only applies if smaller than `max_num_trials`. double min_inlier_ratio = 0.1; // Abort the iteration if minimum probability that one sample is free from // outliers is reached. double confidence = 0.99; // The num_trials_multiplier to the dynamically computed maximum number of // iterations based on the specified confidence value. double dyn_num_trials_multiplier = 3.0; // Number of random trials to estimate model from random subset. int min_num_trials = 0; int max_num_trials = std::numeric_limits::max(); void Check() const { CHECK_GT(max_error, 0); CHECK_GE(min_inlier_ratio, 0); CHECK_LE(min_inlier_ratio, 1); CHECK_GE(confidence, 0); CHECK_LE(confidence, 1); CHECK_LE(min_num_trials, max_num_trials); } }; template class RANSAC { public: struct Report { // Whether the estimation was successful. bool success = false; // The number of RANSAC trials / iterations. size_t num_trials = 0; // The support of the estimated model. typename SupportMeasurer::Support support; // Boolean mask which is true if a sample is an inlier. std::vector inlier_mask; // The estimated model. typename Estimator::M_t model; }; explicit RANSAC(const RANSACOptions& options); // Determine the maximum number of trials required to sample at least one // outlier-free random set of samples with the specified confidence, // given the inlier ratio. // // @param num_inliers The number of inliers. // @param num_samples The total number of samples. // @param confidence Confidence that one sample is // outlier-free. // @param num_trials_multiplier Multiplication factor to the computed // number of trials. // // @return The required number of iterations. static size_t ComputeNumTrials(size_t num_inliers, size_t num_samples, double confidence, double num_trials_multiplier); // Robustly estimate model with RANSAC (RANdom SAmple Consensus). // // @param X Independent variables. // @param Y Dependent variables. // // @return The report with the results of the estimation. Report Estimate(const std::vector& X, const std::vector& Y); // Objects used in RANSAC procedure. Access useful to define custom behavior // through options or e.g. to compute residuals. Estimator estimator; Sampler sampler; SupportMeasurer support_measurer; protected: RANSACOptions options_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template RANSAC::RANSAC( const RANSACOptions& options) : sampler(Sampler(Estimator::kMinNumSamples)), options_(options) { options.Check(); // Determine max_num_trials based on assumed `min_inlier_ratio`. const size_t kNumSamples = 100000; const size_t dyn_max_num_trials = ComputeNumTrials( static_cast(options_.min_inlier_ratio * kNumSamples), kNumSamples, options_.confidence, options_.dyn_num_trials_multiplier); options_.max_num_trials = std::min(options_.max_num_trials, dyn_max_num_trials); } template size_t RANSAC::ComputeNumTrials( const size_t num_inliers, const size_t num_samples, const double confidence, const double num_trials_multiplier) { const double inlier_ratio = num_inliers / static_cast(num_samples); const double nom = 1 - confidence; if (nom <= 0) { return std::numeric_limits::max(); } const double denom = 1 - std::pow(inlier_ratio, Estimator::kMinNumSamples); if (denom <= 0) { return 1; } // Prevent divide by zero below. if (denom == 1.0) { return std::numeric_limits::max(); } return static_cast( std::ceil(std::log(nom) / std::log(denom) * num_trials_multiplier)); } template typename RANSAC::Report RANSAC::Estimate( const std::vector& X, const std::vector& Y) { CHECK_EQ(X.size(), Y.size()); const size_t num_samples = X.size(); Report report; report.success = false; report.num_trials = 0; if (num_samples < Estimator::kMinNumSamples) { return report; } typename SupportMeasurer::Support best_support; typename Estimator::M_t best_model; bool abort = false; const double max_residual = options_.max_error * options_.max_error; std::vector residuals(num_samples); std::vector X_rand(Estimator::kMinNumSamples); std::vector Y_rand(Estimator::kMinNumSamples); std::vector sample_models; sampler.Initialize(num_samples); size_t max_num_trials = std::min(options_.max_num_trials, sampler.MaxNumSamples()); size_t dyn_max_num_trials = max_num_trials; const size_t min_num_trials = options_.min_num_trials; for (report.num_trials = 0; report.num_trials < max_num_trials; ++report.num_trials) { if (abort) { report.num_trials += 1; break; } sampler.SampleXY(X, Y, &X_rand, &Y_rand); // Estimate model for current subset. estimator.Estimate(X_rand, Y_rand, &sample_models); // Iterate through all estimated models. for (const auto& sample_model : sample_models) { estimator.Residuals(X, Y, sample_model, &residuals); CHECK_EQ(residuals.size(), num_samples); const auto support = support_measurer.Evaluate(residuals, max_residual); // Save as best subset if better than all previous subsets. if (support_measurer.Compare(support, best_support)) { best_support = support; best_model = sample_model; dyn_max_num_trials = ComputeNumTrials(best_support.num_inliers, num_samples, options_.confidence, options_.dyn_num_trials_multiplier); } if (report.num_trials >= dyn_max_num_trials && report.num_trials >= min_num_trials) { abort = true; break; } } } report.support = best_support; report.model = best_model; // No valid model was found. if (report.support.num_inliers < estimator.kMinNumSamples) { return report; } report.success = true; // Determine inlier mask. Note that this calculates the residuals for the // best model twice, but saves to copy and fill the inlier mask for each // evaluated model. Some benchmarking revealed that this approach is faster. estimator.Residuals(X, Y, report.model, &residuals); CHECK_EQ(residuals.size(), num_samples); report.inlier_mask.resize(num_samples); for (size_t i = 0; i < residuals.size(); ++i) { report.inlier_mask[i] = residuals[i] <= max_residual; } return report; } } // namespace colmap colmap-3.9.1/src/colmap/optim/ransac_test.cc000066400000000000000000000117561454702036400210250ustar00rootroot00000000000000// 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. #include "colmap/optim/ransac.h" #include "colmap/estimators/similarity_transform.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/sim3.h" #include "colmap/math/random.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { namespace { TEST(RANSAC, Options) { RANSACOptions options; EXPECT_EQ(options.max_error, 0); EXPECT_EQ(options.min_inlier_ratio, 0.1); EXPECT_EQ(options.confidence, 0.99); EXPECT_EQ(options.min_num_trials, 0); EXPECT_EQ(options.max_num_trials, std::numeric_limits::max()); } TEST(RANSAC, Report) { RANSAC>::Report report; EXPECT_FALSE(report.success); EXPECT_EQ(report.num_trials, 0); EXPECT_EQ(report.support.num_inliers, 0); EXPECT_EQ(report.support.residual_sum, std::numeric_limits::max()); EXPECT_EQ(report.inlier_mask.size(), 0); } TEST(RANSAC, NumTrials) { EXPECT_EQ(RANSAC>::ComputeNumTrials( 1, 100, 0.99, 1.0), 4605168); EXPECT_EQ(RANSAC>::ComputeNumTrials( 10, 100, 0.99, 1.0), 4603); EXPECT_EQ(RANSAC>::ComputeNumTrials( 10, 100, 0.999, 1.0), 6905); EXPECT_EQ(RANSAC>::ComputeNumTrials( 10, 100, 0.999, 2.0), 13809); EXPECT_EQ(RANSAC>::ComputeNumTrials( 100, 100, 0.99, 1.0), 1); EXPECT_EQ(RANSAC>::ComputeNumTrials( 100, 100, 0.999, 1.0), 1); EXPECT_EQ(RANSAC>::ComputeNumTrials( 100, 100, 0, 1.0), 1); } TEST(RANSAC, SimilarityTransform) { SetPRNGSeed(0); const size_t num_samples = 1000; const size_t num_outliers = 400; // Create some arbitrary transformation. const Sim3d expectedTgtFromSrc( 2, Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d(100, 10, 10)); // Generate exact data std::vector src; std::vector tgt; for (size_t i = 0; i < num_samples; ++i) { src.emplace_back(i, std::sqrt(i) + 2, std::sqrt(2 * i + 2)); tgt.push_back(expectedTgtFromSrc * src.back()); } // Add some faulty data. for (size_t i = 0; i < num_outliers; ++i) { tgt[i] = Eigen::Vector3d(RandomUniformReal(-3000.0, -2000.0), RandomUniformReal(-4000.0, -3000.0), RandomUniformReal(-5000.0, -4000.0)); } // Robustly estimate transformation using RANSAC. RANSACOptions options; options.max_error = 10; RANSAC> ransac(options); const auto report = ransac.Estimate(src, tgt); EXPECT_TRUE(report.success); EXPECT_GT(report.num_trials, 0); // Make sure outliers were detected correctly. EXPECT_EQ(report.support.num_inliers, num_samples - num_outliers); for (size_t i = 0; i < num_samples; ++i) { if (i < num_outliers) { EXPECT_FALSE(report.inlier_mask[i]); } else { EXPECT_TRUE(report.inlier_mask[i]); } } // Make sure original transformation is estimated correctly. const double matrix_diff = (expectedTgtFromSrc.ToMatrix() - report.model).norm(); EXPECT_LT(matrix_diff, 1e-6); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/optim/sampler.h000066400000000000000000000071131454702036400200140ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include #include namespace colmap { // Abstract base class for sampling methods. class Sampler { public: Sampler() = default; explicit Sampler(size_t num_samples); virtual ~Sampler() = default; // Initialize the sampler, before calling the `Sample` method. virtual void Initialize(size_t total_num_samples) = 0; // Maximum number of unique samples that can be generated. virtual size_t MaxNumSamples() = 0; // Sample `num_samples` elements from all samples. virtual void Sample(std::vector* sampled_idxs) = 0; // Sample elements from `X` into `X_rand`. // // Note that `X.size()` should equal `num_total_samples` and `X_rand.size()` // should equal `num_samples`. template void SampleX(const X_t& X, X_t* X_rand); // Sample elements from `X` and `Y` into `X_rand` and `Y_rand`. // // Note that `X.size()` should equal `num_total_samples` and `X_rand.size()` // should equal `num_samples`. The same applies for `Y` and `Y_rand`. template void SampleXY(const X_t& X, const Y_t& Y, X_t* X_rand, Y_t* Y_rand); }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template void Sampler::SampleX(const X_t& X, X_t* X_rand) { thread_local std::vector sampled_idxs; Sample(&sampled_idxs); for (size_t i = 0; i < X_rand->size(); ++i) { (*X_rand)[i] = X[sampled_idxs[i]]; } } template void Sampler::SampleXY(const X_t& X, const Y_t& Y, X_t* X_rand, Y_t* Y_rand) { CHECK_EQ(X.size(), Y.size()); CHECK_EQ(X_rand->size(), Y_rand->size()); thread_local std::vector sampled_idxs; Sample(&sampled_idxs); for (size_t i = 0; i < X_rand->size(); ++i) { (*X_rand)[i] = X[sampled_idxs[i]]; (*Y_rand)[i] = Y[sampled_idxs[i]]; } } } // namespace colmap colmap-3.9.1/src/colmap/optim/sprt.cc000066400000000000000000000064421454702036400175030ustar00rootroot00000000000000// 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. #include "colmap/optim/sprt.h" namespace colmap { SPRT::SPRT(const Options& options) { Update(options); } void SPRT::Update(const Options& options) { options_ = options; delta_epsilon_ = options.delta / options.epsilon; delta_1_epsilon_1_ = (1 - options.delta) / (1 - options.epsilon); UpdateDecisionThreshold(); } bool SPRT::Evaluate(const std::vector& residuals, const double max_residual, size_t* num_inliers, size_t* num_eval_samples) { *num_inliers = 0; double likelihood_ratio = 1; for (size_t i = 0; i < residuals.size(); ++i) { if (std::abs(residuals[i]) <= max_residual) { *num_inliers += 1; likelihood_ratio *= delta_epsilon_; } else { likelihood_ratio *= delta_1_epsilon_1_; } if (likelihood_ratio > decision_threshold_) { *num_eval_samples = i + 1; return false; } } *num_eval_samples = residuals.size(); return true; } void SPRT::UpdateDecisionThreshold() { // Equation 2 const double C = (1 - options_.delta) * std::log((1 - options_.delta) / (1 - options_.epsilon)) + options_.delta * std::log(options_.delta / options_.epsilon); // Equation 6 const double A0 = options_.eval_time_ratio * C / options_.num_models_per_sample + 1; double A = A0; const double kEps = 1.5e-8; // Compute A using the recursive relation // A* = lim(n->inf) A // The series typically converges within 4 iterations for (size_t i = 0; i < 100; ++i) { const double A1 = A0 + std::log(A); if (std::abs(A1 - A) < kEps) { break; } A = A1; } decision_threshold_ = A; } } // namespace colmap colmap-3.9.1/src/colmap/optim/sprt.h000066400000000000000000000056311454702036400173440ustar00rootroot00000000000000// 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. #pragma once #include #include #include namespace colmap { // Sequential Probability Ratio Test as proposed in // // "Randomized RANSAC with Sequential Probability Ratio Test", // Matas et al., 2005 class SPRT { public: struct Options { // Probability of rejecting a good model. double delta = 0.01; // A priori assumed minimum inlier ratio double epsilon = 0.1; // The ratio of the time it takes to estimate a model from a random sample // over the time it takes to decide whether one data sample is an // inlier or not. Matas et al. propose 200 for the 7-point algorithm. double eval_time_ratio = 200; // Number of models per random sample, that have to be verified. E.g. 1-3 // for the 7-point fundamental matrix algorithm, or 1-10 for the 5-point // essential matrix algorithm. int num_models_per_sample = 1; }; explicit SPRT(const Options& options); void Update(const Options& options); bool Evaluate(const std::vector& residuals, double max_residual, size_t* num_inliers, size_t* num_eval_samples); private: void UpdateDecisionThreshold(); Options options_; double delta_epsilon_; double delta_1_epsilon_1_; double decision_threshold_; }; } // namespace colmap colmap-3.9.1/src/colmap/optim/support_measurement.cc000066400000000000000000000103641454702036400226320ustar00rootroot00000000000000// 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. #include "colmap/optim/support_measurement.h" #include "colmap/util/logging.h" #include namespace colmap { InlierSupportMeasurer::Support InlierSupportMeasurer::Evaluate( const std::vector& residuals, const double max_residual) { Support support; support.num_inliers = 0; support.residual_sum = 0; for (const auto residual : residuals) { if (residual <= max_residual) { support.num_inliers += 1; support.residual_sum += residual; } } return support; } bool InlierSupportMeasurer::Compare(const Support& support1, const Support& support2) { if (support1.num_inliers > support2.num_inliers) { return true; } else { return support1.num_inliers == support2.num_inliers && support1.residual_sum < support2.residual_sum; } } UniqueInlierSupportMeasurer::Support UniqueInlierSupportMeasurer::Evaluate( const std::vector& residuals, const double max_residual) { CHECK_EQ(residuals.size(), unique_sample_ids_.size()); Support support; support.num_inliers = 0; support.num_unique_inliers = 0; support.residual_sum = 0; std::unordered_set inlier_point_ids; for (size_t idx = 0; idx < residuals.size(); ++idx) { if (residuals[idx] <= max_residual) { support.num_inliers += 1; inlier_point_ids.insert(unique_sample_ids_[idx]); support.residual_sum += residuals[idx]; } } support.num_unique_inliers = inlier_point_ids.size(); return support; } bool UniqueInlierSupportMeasurer::Compare(const Support& support1, const Support& support2) { if (support1.num_unique_inliers > support2.num_unique_inliers) { return true; } else if (support1.num_unique_inliers == support2.num_unique_inliers) { if (support1.num_inliers > support2.num_inliers) { return true; } else { return support1.num_inliers == support2.num_inliers && support1.residual_sum < support2.residual_sum; } } else { return false; } } MEstimatorSupportMeasurer::Support MEstimatorSupportMeasurer::Evaluate( const std::vector& residuals, const double max_residual) { Support support; support.num_inliers = 0; support.score = 0; for (const auto residual : residuals) { if (residual <= max_residual) { support.num_inliers += 1; support.score += residual; } else { support.score += max_residual; } } return support; } bool MEstimatorSupportMeasurer::Compare(const Support& support1, const Support& support2) { return support1.score < support2.score; } } // namespace colmap colmap-3.9.1/src/colmap/optim/support_measurement.h000066400000000000000000000101341454702036400224670ustar00rootroot00000000000000// 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. #pragma once #include #include #include namespace colmap { // Measure the support of a model by counting the number of inliers and // summing all inlier residuals. The support is better if it has more inliers // and a smaller residual sum. struct InlierSupportMeasurer { struct Support { // The number of inliers. size_t num_inliers = 0; // The sum of all inlier residuals. double residual_sum = std::numeric_limits::max(); }; // Compute the support of the residuals. Support Evaluate(const std::vector& residuals, double max_residual); // Compare the two supports and return the better support. bool Compare(const Support& support1, const Support& support2); }; // Measure the support of a model by counting the number of unique inliers // (e.g., visible 3D points), number of inliers, and summing all inlier // residuals. Each sample should have an associated id. Samples with the same id // are only counted once in num_unique_inliers. The support is better if it has // more unique inliers, more inliers, and a smaller residual sum. struct UniqueInlierSupportMeasurer { struct Support { // The number of unique inliers; size_t num_unique_inliers = 0; // The number of inliers. // This is still needed for determining the dynamic number of iterations. size_t num_inliers = 0; // The sum of all inlier residuals. double residual_sum = std::numeric_limits::max(); }; void SetUniqueSampleIds(const std::vector& sample_ids) { unique_sample_ids_ = sample_ids; } // Compute the support of the residuals. Support Evaluate(const std::vector& residuals, double max_residual); // Compare the two supports and return the better support. bool Compare(const Support& support1, const Support& support2); private: std::vector unique_sample_ids_; }; // Measure the support of a model by its fitness to the data as used in MSAC. // A support is better if it has a smaller MSAC score. struct MEstimatorSupportMeasurer { struct Support { // The number of inliers. size_t num_inliers = 0; // The MSAC score, defined as the truncated sum of residuals. double score = std::numeric_limits::max(); }; // Compute the support of the residuals. Support Evaluate(const std::vector& residuals, double max_residual); // Compare the two supports and return the better support. bool Compare(const Support& support1, const Support& support2); }; } // namespace colmap colmap-3.9.1/src/colmap/optim/support_measurement_test.cc000066400000000000000000000143261454702036400236730ustar00rootroot00000000000000// 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. #include "colmap/optim/support_measurement.h" #include "colmap/math/math.h" #include #include namespace colmap { namespace { TEST(InlierSupportMeasurer, Nominal) { InlierSupportMeasurer::Support support1; EXPECT_EQ(support1.num_inliers, 0); EXPECT_EQ(support1.residual_sum, std::numeric_limits::max()); InlierSupportMeasurer measurer; std::vector residuals = {-1.0, 0.0, 1.0, 2.0}; support1 = measurer.Evaluate(residuals, 1.0); EXPECT_EQ(support1.num_inliers, 3); EXPECT_EQ(support1.residual_sum, 0.0); InlierSupportMeasurer::Support support2; support2.num_inliers = 2; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.residual_sum = support1.residual_sum; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.num_inliers = support1.num_inliers; support2.residual_sum += 0.01; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.residual_sum -= 0.01; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.residual_sum -= 0.01; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_TRUE(measurer.Compare(support2, support1)); } TEST(UniqueInlierSupportMeasurer, Nominal) { UniqueInlierSupportMeasurer::Support support1; EXPECT_EQ(support1.num_inliers, 0); EXPECT_EQ(support1.num_unique_inliers, 0); EXPECT_EQ(support1.residual_sum, std::numeric_limits::max()); UniqueInlierSupportMeasurer measurer; const std::vector sample_ids = {1, 2, 2, 3}; measurer.SetUniqueSampleIds(sample_ids); const std::vector residuals = {-1.0, 0.0, 1.0, 2.0}; support1 = measurer.Evaluate(residuals, 1.0); EXPECT_EQ(support1.num_inliers, 3); EXPECT_EQ(support1.num_unique_inliers, 2); EXPECT_EQ(support1.residual_sum, 0.0); UniqueInlierSupportMeasurer::Support support2; support2.num_unique_inliers = support1.num_unique_inliers - 1; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.num_inliers = support1.num_inliers + 1; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.num_inliers = support1.num_inliers; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.residual_sum = support1.residual_sum - 0.01; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.residual_sum = support1.residual_sum; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.num_unique_inliers = support1.num_unique_inliers; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.residual_sum = support1.residual_sum - 0.01; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_TRUE(measurer.Compare(support2, support1)); support2.num_inliers = support1.num_inliers + 1; support2.residual_sum = support1.residual_sum + 0.01; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_TRUE(measurer.Compare(support2, support1)); support2.num_unique_inliers = support1.num_unique_inliers + 1; support2.num_inliers = support1.num_inliers - 1; support2.residual_sum = support1.residual_sum + 0.01; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_TRUE(measurer.Compare(support2, support1)); } TEST(MEstimatorSupportMeasurer, Nominal) { MEstimatorSupportMeasurer::Support support1; EXPECT_EQ(support1.num_inliers, 0); EXPECT_EQ(support1.score, std::numeric_limits::max()); MEstimatorSupportMeasurer measurer; std::vector residuals = {-1.0, 0.0, 1.0, 2.0}; support1 = measurer.Evaluate(residuals, 1.0); EXPECT_EQ(support1.num_inliers, 3); EXPECT_EQ(support1.score, 1.0); MEstimatorSupportMeasurer::Support support2 = support1; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.num_inliers -= 1; support2.score += 0.01; EXPECT_TRUE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.score -= 0.01; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_FALSE(measurer.Compare(support2, support1)); support2.score -= 0.01; EXPECT_FALSE(measurer.Compare(support1, support2)); EXPECT_TRUE(measurer.Compare(support2, support1)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/retrieval/000077500000000000000000000000001454702036400170435ustar00rootroot00000000000000colmap-3.9.1/src/colmap/retrieval/CMakeLists.txt000066400000000000000000000047121454702036400216070ustar00rootroot00000000000000# 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. set(FOLDER_NAME "retrieval") COLMAP_ADD_LIBRARY( NAME colmap_retrieval SRCS geometry.h geometry.cc inverted_file.h inverted_file_entry.h inverted_index.h utils.h visual_index.h vote_and_verify.h vote_and_verify.cc PUBLIC_LINK_LIBS Boost::boost Eigen3::Eigen flann lz4 PRIVATE_LINK_LIBS colmap_math colmap_estimators colmap_optim ) COLMAP_ADD_TEST( NAME geometry_test SRCS geometry_test.cc LINK_LIBS colmap_retrieval ) COLMAP_ADD_TEST( NAME inverted_file_entry_test SRCS inverted_file_entry_test.cc LINK_LIBS colmap_retrieval ) COLMAP_ADD_TEST( NAME visual_index_test SRCS visual_index_test.cc LINK_LIBS colmap_retrieval ) COLMAP_ADD_TEST( NAME vote_and_verify_test SRCS vote_and_verify_test.cc LINK_LIBS colmap_retrieval ) colmap-3.9.1/src/colmap/retrieval/geometry.cc000066400000000000000000000066211454702036400212120ustar00rootroot00000000000000// 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. #include "colmap/retrieval/geometry.h" namespace colmap { namespace retrieval { FeatureGeometryTransform FeatureGeometry::TransformFromMatch( const FeatureGeometry& feature1, const FeatureGeometry& feature2) { FeatureGeometryTransform tform; tform.scale = feature2.scale / feature1.scale; tform.angle = feature2.orientation - feature1.orientation; const float sin_angle = std::sin(tform.angle); const float cos_angle = std::cos(tform.angle); Eigen::Matrix2f R; R << cos_angle, -sin_angle, sin_angle, cos_angle; const Eigen::Vector2f t = Eigen::Vector2f(feature2.x, feature2.y) - tform.scale * R * Eigen::Vector2f(feature1.x, feature1.y); tform.tx = t.x(); tform.ty = t.y(); return tform; } Eigen::Matrix FeatureGeometry::TransformMatrixFromMatch( const FeatureGeometry& feature1, const FeatureGeometry& feature2) { Eigen::Matrix T; const float scale = feature2.scale / feature1.scale; const float angle = feature2.orientation - feature1.orientation; const float sin_angle = std::sin(angle); const float cos_angle = std::cos(angle); T.leftCols<2>() << cos_angle, -sin_angle, sin_angle, cos_angle; T.leftCols<2>() *= scale; T.rightCols<1>() = Eigen::Vector2f(feature2.x, feature2.y) - T.leftCols<2>() * Eigen::Vector2f(feature1.x, feature1.y); return T; } float FeatureGeometry::GetArea() const { return 1.0f / std::sqrt(4.0f / (scale * scale * scale * scale)); } float FeatureGeometry::GetAreaUnderTransform(const Eigen::Matrix2f& A) const { const Eigen::Matrix2f M = Eigen::Matrix2f::Identity() / (scale * scale); const Eigen::Matrix2f N = A.transpose() * M * A; const float B = N(1, 0) + N(0, 1); return 1.0f / std::sqrt(4.0f * N(0, 0) * N(1, 1) - B * B); } } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/geometry.h000066400000000000000000000053531454702036400210550ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace retrieval { struct FeatureGeometryTransform { float scale = 0.0f; float angle = 0.0f; float tx = 0.0f; float ty = 0.0f; }; struct FeatureGeometry { // Compute the similarity that transforms the shape of feature 1 to feature 2. static FeatureGeometryTransform TransformFromMatch( const FeatureGeometry& feature1, const FeatureGeometry& feature2); static Eigen::Matrix TransformMatrixFromMatch( const FeatureGeometry& feature1, const FeatureGeometry& feature2); // Get the approximate area occupied by the feature. float GetArea() const; // Get the approximate area occupied by the feature after applying an affine // transformation to the feature geometry. float GetAreaUnderTransform(const Eigen::Matrix2f& A) const; float x = 0.0f; float y = 0.0f; float scale = 0.0f; float orientation = 0.0f; }; // 1-to-M feature geometry match. struct FeatureGeometryMatch { FeatureGeometry geometry1; FeatureGeometry geometry2; }; } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/geometry_test.cc000066400000000000000000000124201454702036400222430ustar00rootroot00000000000000// 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. #define BOOST_TEST_MAIN #define BOOST_TEST_MODULE "retrieval/geometry" #include "colmap/retrieval/geometry.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { namespace retrieval { namespace { TEST(FeatureGeometry, Identity) { for (int x = 0; x < 3; ++x) { for (int y = 0; y < 3; ++y) { for (int scale = 1; scale < 5; ++scale) { for (int orientation = 0; orientation < 3; ++orientation) { FeatureGeometry feature1; feature1.x = x; feature1.y = y; feature1.scale = scale; feature1.orientation = orientation; FeatureGeometry feature2; feature2.x = x; feature2.y = y; feature2.scale = scale; feature2.orientation = orientation; const auto tform_matrix = FeatureGeometry::TransformMatrixFromMatch(feature1, feature2); EXPECT_TRUE( tform_matrix.isApprox(Eigen::Matrix::Identity())); const auto tform = FeatureGeometry::TransformFromMatch(feature1, feature2); EXPECT_NEAR(tform.scale, 1, 1e-6); EXPECT_NEAR(tform.angle, 0, 1e-6); EXPECT_NEAR(tform.tx, 0, 1e-6); EXPECT_NEAR(tform.ty, 0, 1e-6); } } } } } TEST(FeatureGeometry, Translation) { for (int x = 0; x < 3; ++x) { for (int y = 0; y < 3; ++y) { FeatureGeometry feature1; feature1.scale = 1; FeatureGeometry feature2; feature2.x = x; feature2.y = y; feature2.scale = 1; feature2.orientation = 0; const auto tform_matrix = FeatureGeometry::TransformMatrixFromMatch(feature1, feature2); EXPECT_TRUE( tform_matrix.leftCols<2>().isApprox(Eigen::Matrix2f::Identity())); EXPECT_TRUE(tform_matrix.rightCols<1>().isApprox(Eigen::Vector2f(x, y))); const auto tform = FeatureGeometry::TransformFromMatch(feature1, feature2); EXPECT_NEAR(tform.scale, 1, 1e-6); EXPECT_NEAR(tform.angle, 0, 1e-6); EXPECT_NEAR(tform.tx, x, 1e-6); EXPECT_NEAR(tform.ty, y, 1e-6); } } } TEST(FeatureGeometry, Scale) { for (int scale = 1; scale < 5; ++scale) { FeatureGeometry feature1; feature1.scale = 1; FeatureGeometry feature2; feature2.scale = scale; feature2.orientation = 0; const auto tform_matrix = FeatureGeometry::TransformMatrixFromMatch(feature1, feature2); EXPECT_TRUE(tform_matrix.leftCols<2>().isApprox( scale * Eigen::Matrix2f::Identity())); EXPECT_TRUE(tform_matrix.rightCols<1>().isApprox(Eigen::Vector2f(0, 0))); const auto tform = FeatureGeometry::TransformFromMatch(feature1, feature2); EXPECT_NEAR(tform.scale, scale, 1e-6); EXPECT_NEAR(tform.angle, 0, 1e-6); EXPECT_NEAR(tform.tx, 0, 1e-6); EXPECT_NEAR(tform.ty, 0, 1e-6); } } TEST(FeatureGeometry, Orientation) { for (int orientation = 0; orientation < 3; ++orientation) { FeatureGeometry feature1; feature1.scale = 1; feature1.orientation = 0; FeatureGeometry feature2; feature2.scale = 1; feature2.orientation = orientation; const auto tform_matrix = FeatureGeometry::TransformMatrixFromMatch(feature1, feature2); EXPECT_NEAR(tform_matrix.leftCols<2>().determinant(), 1, 1e-5); EXPECT_TRUE(tform_matrix.rightCols<1>().isApprox(Eigen::Vector2f(0, 0))); const auto tform = FeatureGeometry::TransformFromMatch(feature1, feature2); EXPECT_NEAR(tform.scale, 1, 1e-6); EXPECT_NEAR(tform.angle, orientation, 1e-6); EXPECT_NEAR(tform.tx, 0, 1e-6); EXPECT_NEAR(tform.ty, 0, 1e-6); } } } // namespace } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/inverted_file.h000066400000000000000000000331321454702036400220350ustar00rootroot00000000000000// 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. #pragma once #include "colmap/math/math.h" #include "colmap/retrieval/geometry.h" #include "colmap/retrieval/inverted_file_entry.h" #include "colmap/retrieval/utils.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include #include #include #include #include #include #include #include namespace colmap { namespace retrieval { // Implements an inverted file, including the ability to compute image scores // and matches. The template parameter is the length of the binary vectors // in the Hamming Embedding. // This class is based on an original implementation by Torsten Sattler. template class InvertedFile { public: typedef Eigen::VectorXf DescType; typedef FeatureGeometry GeomType; typedef InvertedFileEntry EntryType; enum Status { UNUSABLE = 0x00, HAS_EMBEDDING = 0x01, ENTRIES_SORTED = 0x02, USABLE = 0x03, }; InvertedFile(); // The number of added entries. size_t NumEntries() const; // Return all entries in the file. const std::vector& GetEntries() const; // Whether the Hamming embedding was computed for this file. bool HasHammingEmbedding() const; // Whether the entries in this file are sorted. bool EntriesSorted() const; // Whether this file is usable for scoring, i.e. the entries are sorted and // the Hamming embedding has been computed. bool IsUsable() const; // Adds an inverted file entry given a projected descriptor and its image // information stored in an inverted file entry. In particular, this function // generates the binary descriptor for the inverted file entry and then stores // the entry in the inverted file. void AddEntry(int image_id, typename DescType::Index feature_idx, const DescType& descriptor, const GeomType& geometry); // Sorts the inverted file entries in ascending order of image ids. This is // required for efficient scoring and must be called before ScoreFeature. void SortEntries(); // Clear all entries in this file. void ClearEntries(); // Reset all computed weights/thresholds and clear all entries. void Reset(); // Given a projected descriptor, returns the corresponding binary string. void ConvertToBinaryDescriptor( const DescType& descriptor, std::bitset* binary_descriptor) const; // Compute the idf-weight for this inverted file. void ComputeIDFWeight(int num_total_images); // Return the idf-weight of this inverted file. float IDFWeight() const; // Given a set of descriptors, learns the thresholds required for the Hamming // embedding. Each row in descriptors represents a single descriptor projected // into the kEmbeddingDim dimensional space used for Hamming embedding. void ComputeHammingEmbedding( const Eigen::Matrix& descriptors); // Given a query feature, performs inverted file scoring. void ScoreFeature(const DescType& descriptor, std::vector* image_scores) const; // Get the identifiers of all indexed images in this file. void GetImageIds(std::unordered_set* ids) const; // For each image in the inverted file, computes the self-similarity of each // image in the file (the part caused by this word) and adds the weight to the // entry corresponding to that image. This function is useful to determine the // normalization factor for each image that is used during retrieval. void ComputeImageSelfSimilarities( std::unordered_map* self_similarities) const; // Read/write the inverted file from/to a binary file. void Read(std::ifstream* ifs); void Write(std::ofstream* ofs) const; private: // Whether the inverted file is initialized. uint8_t status_; // The inverse document frequency weight of this inverted file. float idf_weight_; // The entries of the inverted file system. std::vector entries_; // The thresholds used for Hamming embedding. DescType thresholds_; // The functor to derive a voting weight from a Hamming distance. static const HammingDistWeightFunctor hamming_dist_weight_functor_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template const HammingDistWeightFunctor InvertedFile::hamming_dist_weight_functor_; template InvertedFile::InvertedFile() : status_(UNUSABLE), idf_weight_(0.0f) { static_assert(kEmbeddingDim % 8 == 0, "Dimensionality of projected space needs to" " be a multiple of 8."); static_assert(kEmbeddingDim > 0, "Dimensionality of projected space needs to be > 0."); thresholds_.resize(kEmbeddingDim); thresholds_.setZero(); } template size_t InvertedFile::NumEntries() const { return entries_.size(); } template const std::vector::EntryType>& InvertedFile::GetEntries() const { return entries_; } template bool InvertedFile::HasHammingEmbedding() const { return status_ & HAS_EMBEDDING; } template bool InvertedFile::EntriesSorted() const { return status_ & ENTRIES_SORTED; } template bool InvertedFile::IsUsable() const { return status_ & USABLE; } template void InvertedFile::AddEntry(const int image_id, typename DescType::Index feature_idx, const DescType& descriptor, const GeomType& geometry) { CHECK_GE(image_id, 0); CHECK_EQ(descriptor.size(), kEmbeddingDim); EntryType entry; entry.image_id = image_id; entry.feature_idx = feature_idx; entry.geometry = geometry; ConvertToBinaryDescriptor(descriptor, &entry.descriptor); entries_.push_back(entry); status_ &= ~ENTRIES_SORTED; } template void InvertedFile::SortEntries() { std::sort(entries_.begin(), entries_.end(), [](const EntryType& entry1, const EntryType& entry2) { return entry1.image_id < entry2.image_id; }); status_ |= ENTRIES_SORTED; } template void InvertedFile::ClearEntries() { entries_.clear(); status_ &= ~ENTRIES_SORTED; } template void InvertedFile::Reset() { status_ = UNUSABLE; idf_weight_ = 0.0f; entries_.clear(); thresholds_.setZero(); } template void InvertedFile::ConvertToBinaryDescriptor( const DescType& descriptor, std::bitset* binary_descriptor) const { CHECK_EQ(descriptor.size(), kEmbeddingDim); for (int i = 0; i < kEmbeddingDim; ++i) { (*binary_descriptor)[i] = descriptor[i] > thresholds_[i]; } } template void InvertedFile::ComputeIDFWeight(const int num_total_images) { if (entries_.empty()) { return; } std::unordered_set image_ids; GetImageIds(&image_ids); idf_weight_ = std::log(static_cast(num_total_images) / static_cast(image_ids.size())); } template float InvertedFile::IDFWeight() const { return idf_weight_; } template void InvertedFile::ComputeHammingEmbedding( const Eigen::Matrix& descriptors) { const int num_descriptors = static_cast(descriptors.rows()); if (num_descriptors < 2) { return; } std::vector elements(num_descriptors); for (int n = 0; n < kEmbeddingDim; ++n) { for (int i = 0; i < num_descriptors; ++i) { elements[i] = descriptors(i, n); } thresholds_[n] = Median(elements); } status_ |= HAS_EMBEDDING; } template void InvertedFile::ScoreFeature( const DescType& descriptor, std::vector* image_scores) const { CHECK_EQ(descriptor.size(), kEmbeddingDim); image_scores->clear(); if (!IsUsable()) { return; } if (entries_.size() == 0) { return; } const float squared_idf_weight = idf_weight_ * idf_weight_; std::bitset bin_descriptor; ConvertToBinaryDescriptor(descriptor, &bin_descriptor); ImageScore image_score; image_score.image_id = entries_.front().image_id; image_score.score = 0.0f; int num_image_votes = 0; // Note that this assumes that the entries are sorted using SortEntries // according to their image identifiers. for (const auto& entry : entries_) { if (image_score.image_id < entry.image_id) { if (num_image_votes > 0) { // Finalizes the voting since we now know how many features from // the database image match the current image feature. This is // required to perform burstiness normalization (cf. Eqn. 2 in // Arandjelovic, Zisserman: Scalable descriptor // distinctiveness for location recognition. ACCV 2014). // Notice that the weight from the descriptor matching is already // accumulated in image_score.score, i.e., we only need // to apply the burstiness weighting. image_score.score /= std::sqrt(static_cast(num_image_votes)); image_score.score *= squared_idf_weight; image_scores->push_back(image_score); } image_score.image_id = entry.image_id; image_score.score = 0.0f; num_image_votes = 0; } const size_t hamming_dist = (bin_descriptor ^ entry.descriptor).count(); if (hamming_dist <= hamming_dist_weight_functor_.kMaxHammingDistance) { image_score.score += hamming_dist_weight_functor_(hamming_dist); num_image_votes += 1; } } // Add the voting for the largest image_id in the entries. if (num_image_votes > 0) { image_score.score /= std::sqrt(static_cast(num_image_votes)); image_score.score *= squared_idf_weight; image_scores->push_back(image_score); } } template void InvertedFile::GetImageIds( std::unordered_set* ids) const { for (const EntryType& entry : entries_) { ids->insert(entry.image_id); } } template void InvertedFile::ComputeImageSelfSimilarities( std::unordered_map* self_similarities) const { const double squared_idf_weight = idf_weight_ * idf_weight_; for (const auto& entry : entries_) { (*self_similarities)[entry.image_id] += squared_idf_weight; } } template void InvertedFile::Read(std::ifstream* ifs) { CHECK(ifs->is_open()); ifs->read(reinterpret_cast(&status_), sizeof(uint8_t)); ifs->read(reinterpret_cast(&idf_weight_), sizeof(float)); for (int i = 0; i < kEmbeddingDim; ++i) { ifs->read(reinterpret_cast(&thresholds_[i]), sizeof(float)); } uint32_t num_entries = 0; ifs->read(reinterpret_cast(&num_entries), sizeof(uint32_t)); entries_.resize(num_entries); for (uint32_t i = 0; i < num_entries; ++i) { entries_[i].Read(ifs); } } template void InvertedFile::Write(std::ofstream* ofs) const { CHECK(ofs->is_open()); ofs->write(reinterpret_cast(&status_), sizeof(uint8_t)); ofs->write(reinterpret_cast(&idf_weight_), sizeof(float)); for (int i = 0; i < kEmbeddingDim; ++i) { ofs->write(reinterpret_cast(&thresholds_[i]), sizeof(float)); } const uint32_t num_entries = static_cast(entries_.size()); ofs->write(reinterpret_cast(&num_entries), sizeof(uint32_t)); for (uint32_t i = 0; i < num_entries; ++i) { entries_[i].Write(ofs); } } } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/inverted_file_entry.h000066400000000000000000000104261454702036400232570ustar00rootroot00000000000000// 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. #pragma once #include "colmap/retrieval/geometry.h" #include #include namespace colmap { namespace retrieval { // An inverted file entry. The template defines the dimensionality of the binary // string used to approximate the descriptor in the Hamming space. // This class is based on an original implementation by Torsten Sattler. template struct InvertedFileEntry { void Read(std::istream* ifs); void Write(std::ostream* ofs) const; // The identifier of the image this entry is associated with. int image_id = -1; // The index of the feature within the image's keypoints list. int feature_idx = -1; // The geometry of the feature, used for spatial verification. FeatureGeometry geometry; // The binary signature in the Hamming embedding. std::bitset descriptor; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template void InvertedFileEntry::Read(std::istream* ifs) { static_assert(N <= 64, "Dimensionality too large"); static_assert(sizeof(unsigned long long) >= 8, "Expected unsigned long to be at least 8 byte"); static_assert(sizeof(FeatureGeometry) == 16, "Geometry type size mismatch"); int32_t image_id_data = 0; ifs->read(reinterpret_cast(&image_id_data), sizeof(int32_t)); image_id = static_cast(image_id_data); int32_t feature_idx_data = 0; ifs->read(reinterpret_cast(&feature_idx_data), sizeof(int32_t)); feature_idx = static_cast(feature_idx_data); ifs->read(reinterpret_cast(&geometry), sizeof(FeatureGeometry)); uint64_t descriptor_data = 0; ifs->read(reinterpret_cast(&descriptor_data), sizeof(uint64_t)); descriptor = std::bitset(descriptor_data); } template void InvertedFileEntry::Write(std::ostream* ofs) const { static_assert(N <= 64, "Dimensionality too large"); static_assert(sizeof(unsigned long long) >= 8, "Expected unsigned long to be at least 8 byte"); static_assert(sizeof(FeatureGeometry) == 16, "Geometry type size mismatch"); const int32_t image_id_data = image_id; ofs->write(reinterpret_cast(&image_id_data), sizeof(int32_t)); const int32_t feature_idx_data = feature_idx; ofs->write(reinterpret_cast(&feature_idx_data), sizeof(int32_t)); ofs->write(reinterpret_cast(&geometry), sizeof(FeatureGeometry)); const uint64_t descriptor_data = static_cast(descriptor.to_ullong()); ofs->write(reinterpret_cast(&descriptor_data), sizeof(uint64_t)); } } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/inverted_file_entry_test.cc000066400000000000000000000060171454702036400244550ustar00rootroot00000000000000// 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. #include "colmap/retrieval/inverted_file_entry.h" #include namespace colmap { namespace retrieval { namespace { TEST(InvertedFileEntry, Empty) { InvertedFileEntry<10> entry; EXPECT_EQ(entry.image_id, -1); EXPECT_EQ(entry.feature_idx, -1); EXPECT_EQ(entry.geometry.x, 0); EXPECT_EQ(entry.geometry.y, 0); EXPECT_EQ(entry.geometry.scale, 0); EXPECT_EQ(entry.geometry.orientation, 0); EXPECT_EQ(entry.descriptor.size(), 10); } TEST(InvertedFileEntry, ReadWrite) { InvertedFileEntry<10> entry; entry.image_id = 99; entry.feature_idx = 100; entry.geometry.x = 0.123; entry.geometry.y = 0.456; entry.geometry.scale = 0.789; entry.geometry.orientation = -0.1; for (size_t i = 0; i < entry.descriptor.size(); ++i) { entry.descriptor[i] = (i % 2) == 0; } std::stringstream file; entry.Write(&file); InvertedFileEntry<10> read_entry; read_entry.Read(&file); EXPECT_EQ(entry.image_id, read_entry.image_id); EXPECT_EQ(entry.feature_idx, read_entry.feature_idx); EXPECT_EQ(entry.geometry.x, read_entry.geometry.x); EXPECT_EQ(entry.geometry.y, read_entry.geometry.y); EXPECT_EQ(entry.geometry.scale, read_entry.geometry.scale); EXPECT_EQ(entry.geometry.orientation, read_entry.geometry.orientation); for (size_t i = 0; i < entry.descriptor.size(); ++i) { EXPECT_EQ(entry.descriptor[i], read_entry.descriptor[i]); } } } // namespace } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/inverted_index.h000066400000000000000000000376451454702036400222420ustar00rootroot00000000000000// 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. #pragma once #include "colmap/math/random.h" #include "colmap/retrieval/inverted_file.h" #include "colmap/util/eigen_alignment.h" #include #include #include #include #include #include #include #include #include namespace colmap { namespace retrieval { // Implements an inverted index system. The template parameter is the length of // the binary vectors in the Hamming Embedding. // This class is based on an original implementation by Torsten Sattler. template class InvertedIndex { public: const static int kInvalidWordId; typedef Eigen::Matrix DescType; typedef typename InvertedFile::EntryType EntryType; typedef typename InvertedFile::GeomType GeomType; typedef Eigen::Matrix ProjMatrixType; typedef Eigen::VectorXf ProjDescType; InvertedIndex(); // The number of visual words in the index. int NumVisualWords() const; // Initializes the inverted index with num_words empty inverted files. void Initialize(int num_words); // Finalizes the inverted index by sorting each inverted file such that all // entries are in ascending order of image ids. void Finalize(); // Generate projection matrix for Hamming embedding. void GenerateHammingEmbeddingProjection(); // Compute Hamming embedding thresholds from a set of descriptors with // given visual word identifies. void ComputeHammingEmbedding(const DescType& descriptors, const Eigen::VectorXi& word_ids); // Add single entry to the index. void AddEntry(int image_id, int word_id, typename DescType::Index feature_idx, const DescType& descriptor, const GeomType& geometry); // Clear all index entries. void ClearEntries(); // Query the inverted file and return a list of sorted images. void Query(const DescType& descriptors, const Eigen::MatrixXi& word_ids, std::vector* image_scores) const; void ConvertToBinaryDescriptor( int word_id, const DescType& descriptor, std::bitset* binary_descriptor) const; float GetIDFWeight(int word_id) const; void FindMatches(int word_id, const std::unordered_set& image_ids, std::vector* matches) const; // Compute the self-similarity for the image. float ComputeSelfSimilarity(const Eigen::MatrixXi& word_ids) const; // Get the identifiers of all indexed images. void GetImageIds(std::unordered_set* image_ids) const; // Read/write the inverted index from/to a binary file. void Read(std::ifstream* ifs); void Write(std::ofstream* ofs) const; private: void ComputeWeightsAndNormalizationConstants(); // The individual inverted indices. std::vector, Eigen::aligned_allocator>> inverted_files_; // For each image in the database, a normalization factor to be used to // normalize the votes. std::unordered_map normalization_constants_; // The projection matrix used to project SIFT descriptors. ProjMatrixType proj_matrix_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template const int InvertedIndex::kInvalidWordId = std::numeric_limits::max(); template InvertedIndex::InvertedIndex() { proj_matrix_.resize(kEmbeddingDim, kDescDim); proj_matrix_.setIdentity(); } template int InvertedIndex::NumVisualWords() const { return static_cast(inverted_files_.size()); } template void InvertedIndex::Initialize( const int num_words) { CHECK_GT(num_words, 0); inverted_files_.resize(num_words); for (auto& inverted_file : inverted_files_) { inverted_file.Reset(); } } template void InvertedIndex::Finalize() { CHECK_GT(NumVisualWords(), 0); for (auto& inverted_file : inverted_files_) { inverted_file.SortEntries(); } ComputeWeightsAndNormalizationConstants(); } template void InvertedIndex:: GenerateHammingEmbeddingProjection() { Eigen::MatrixXf random_matrix(kDescDim, kDescDim); for (Eigen::MatrixXf::Index i = 0; i < random_matrix.size(); ++i) { random_matrix(i) = RandomGaussian(0.0f, 1.0f); } const Eigen::MatrixXf Q = random_matrix.colPivHouseholderQr().matrixQ(); proj_matrix_ = Q.topRows(); } template void InvertedIndex::ComputeHammingEmbedding( const DescType& descriptors, const Eigen::VectorXi& word_ids) { CHECK_EQ(descriptors.rows(), word_ids.rows()); CHECK_EQ(descriptors.cols(), kDescDim); // Skip every inverted file with less than kMinEntries entries. const size_t kMinEntries = 5; // Determines for each word the corresponding descriptors. std::vector> indices_per_word(NumVisualWords()); for (Eigen::MatrixXi::Index i = 0; i < word_ids.rows(); ++i) { indices_per_word.at(word_ids(i)).push_back(i); } // For each word, learn the Hamming embedding threshold and the local // descriptor space densities. for (int i = 0; i < NumVisualWords(); ++i) { const auto& indices = indices_per_word[i]; if (indices.size() < kMinEntries) { continue; } Eigen::Matrix proj_desc( indices.size(), kEmbeddingDim); for (size_t j = 0; j < indices.size(); ++j) { proj_desc.row(j) = proj_matrix_ * descriptors.row(indices[j]).transpose().template cast(); } inverted_files_[i].ComputeHammingEmbedding(proj_desc); } } template void InvertedIndex::AddEntry( const int image_id, const int word_id, typename DescType::Index feature_idx, const DescType& descriptor, const GeomType& geometry) { CHECK_EQ(descriptor.size(), kDescDim); const ProjDescType proj_desc = proj_matrix_ * descriptor.transpose().template cast(); inverted_files_.at(word_id).AddEntry( image_id, feature_idx, proj_desc, geometry); } template void InvertedIndex::ClearEntries() { for (auto& inverted_file : inverted_files_) { inverted_file.ClearEntries(); } } template void InvertedIndex::Query( const DescType& descriptors, const Eigen::MatrixXi& word_ids, std::vector* image_scores) const { CHECK_EQ(descriptors.cols(), kDescDim); image_scores->clear(); // Computes the self-similarity score for the query image. const float self_similarity = ComputeSelfSimilarity(word_ids); float normalization_weight = 1.0f; if (self_similarity > 0.0f) { normalization_weight = 1.0f / std::sqrt(self_similarity); } std::unordered_map score_map; std::vector inverted_file_scores; for (typename DescType::Index i = 0; i < descriptors.rows(); ++i) { const ProjDescType proj_descriptor = proj_matrix_ * descriptors.row(i).transpose().template cast(); for (Eigen::MatrixXi::Index n = 0; n < word_ids.cols(); ++n) { const int word_id = word_ids(i, n); if (word_id == kInvalidWordId) { continue; } inverted_files_.at(word_id).ScoreFeature(proj_descriptor, &inverted_file_scores); for (const ImageScore& score : inverted_file_scores) { const auto score_map_it = score_map.find(score.image_id); if (score_map_it == score_map.end()) { // Image not found in another inverted file. score_map.emplace(score.image_id, static_cast(image_scores->size())); image_scores->push_back(score); } else { // Image already found in another inverted file, so accumulate. (*image_scores).at(score_map_it->second).score += score.score; } } } } // Normalization. for (ImageScore& score : *image_scores) { score.score *= normalization_weight * normalization_constants_.at(score.image_id); } } template void InvertedIndex:: ConvertToBinaryDescriptor( const int word_id, const DescType& descriptor, std::bitset* binary_descriptor) const { const ProjDescType proj_desc = proj_matrix_ * descriptor.transpose().template cast(); inverted_files_.at(word_id).ConvertToBinaryDescriptor(proj_desc, binary_descriptor); } template float InvertedIndex::GetIDFWeight( const int word_id) const { return inverted_files_.at(word_id).IDFWeight(); } template void InvertedIndex::FindMatches( const int word_id, const std::unordered_set& image_ids, std::vector* matches) const { matches->clear(); const auto& entries = inverted_files_.at(word_id).GetEntries(); for (const auto& entry : entries) { if (image_ids.count(entry.image_id)) { matches->emplace_back(&entry); } } } template float InvertedIndex::ComputeSelfSimilarity( const Eigen::MatrixXi& word_ids) const { double self_similarity = 0.0; for (Eigen::MatrixXi::Index i = 0; i < word_ids.size(); ++i) { const int word_id = word_ids(i); if (word_id != kInvalidWordId) { const auto& inverted_file = inverted_files_.at(word_id); self_similarity += inverted_file.IDFWeight() * inverted_file.IDFWeight(); } } return static_cast(self_similarity); } template void InvertedIndex::GetImageIds( std::unordered_set* image_ids) const { for (const auto& inverted_file : inverted_files_) { inverted_file.GetImageIds(image_ids); } } template void InvertedIndex::Read( std::ifstream* ifs) { CHECK(ifs->is_open()); int32_t num_words = 0; ifs->read(reinterpret_cast(&num_words), sizeof(int32_t)); CHECK_GT(num_words, 0); Initialize(num_words); int32_t N_t = 0; ifs->read(reinterpret_cast(&N_t), sizeof(int32_t)); CHECK_EQ(N_t, kEmbeddingDim) << "The length of the binary strings should be " << kEmbeddingDim << " but is " << N_t << ". The indices are not compatible!"; for (int i = 0; i < kEmbeddingDim; ++i) { for (int j = 0; j < kDescDim; ++j) { ifs->read(reinterpret_cast(&proj_matrix_(i, j)), sizeof(float)); } } for (auto& inverted_file : inverted_files_) { inverted_file.Read(ifs); } int32_t num_images = 0; ifs->read(reinterpret_cast(&num_images), sizeof(int32_t)); CHECK_GE(num_images, 0); normalization_constants_.clear(); normalization_constants_.reserve(num_images); for (int32_t i = 0; i < num_images; ++i) { int image_id; float value; ifs->read(reinterpret_cast(&image_id), sizeof(int)); ifs->read(reinterpret_cast(&value), sizeof(float)); normalization_constants_[image_id] = value; } } template void InvertedIndex::Write( std::ofstream* ofs) const { CHECK(ofs->is_open()); int32_t num_words = static_cast(NumVisualWords()); ofs->write(reinterpret_cast(&num_words), sizeof(int32_t)); CHECK_GT(num_words, 0); const int32_t N_t = static_cast(kEmbeddingDim); ofs->write(reinterpret_cast(&N_t), sizeof(int32_t)); for (int i = 0; i < kEmbeddingDim; ++i) { for (int j = 0; j < kDescDim; ++j) { ofs->write(reinterpret_cast(&proj_matrix_(i, j)), sizeof(float)); } } for (const auto& inverted_file : inverted_files_) { inverted_file.Write(ofs); } const int32_t num_images = normalization_constants_.size(); ofs->write(reinterpret_cast(&num_images), sizeof(int32_t)); for (const auto& constant : normalization_constants_) { ofs->write(reinterpret_cast(&constant.first), sizeof(int)); ofs->write(reinterpret_cast(&constant.second), sizeof(float)); } } template void InvertedIndex:: ComputeWeightsAndNormalizationConstants() { std::unordered_set image_ids; GetImageIds(&image_ids); for (auto& inverted_file : inverted_files_) { inverted_file.ComputeIDFWeight(image_ids.size()); } std::unordered_map self_similarities(image_ids.size()); for (const auto& inverted_file : inverted_files_) { inverted_file.ComputeImageSelfSimilarities(&self_similarities); } normalization_constants_.clear(); normalization_constants_.reserve(image_ids.size()); for (const auto& self_similarity : self_similarities) { if (self_similarity.second > 0.0) { normalization_constants_[self_similarity.first] = static_cast(1.0 / std::sqrt(self_similarity.second)); } else { normalization_constants_[self_similarity.first] = 0.0f; } } } } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/utils.h000066400000000000000000000064041454702036400203600ustar00rootroot00000000000000// 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. #pragma once #include #include namespace colmap { namespace retrieval { struct ImageScore { int image_id = -1; float score = 0.0f; }; // Implements the weighting function used to derive a voting weight from the // Hamming distance of two binary signatures. See Eqn. 4 in // Arandjelovic, Zisserman. DisLocation: Scalable descriptor distinctiveness for // location recognition. ACCV 2014. // The template is the length of the Hamming embedding vectors. // This class is based on an original implementation by Torsten Sattler. template class HammingDistWeightFunctor { public: static const size_t kMaxHammingDistance = static_cast(1.5f * kSigma); HammingDistWeightFunctor() { // Fills the look-up table. const float sigma_squared = kSigma * kSigma; for (int n = 0; n <= N; ++n) { const float hamming_dist = static_cast(n); if (hamming_dist <= kMaxHammingDistance) { look_up_table_.at(n) = std::exp(-hamming_dist * hamming_dist / sigma_squared); } else { look_up_table_.at(n) = 0.0f; } } } // Returns the weight for Hamming distance h and standard deviation sigma. // Does not perform a range check when performing the look-up. inline float operator()(const size_t hamming_dist) const { return look_up_table_.at(hamming_dist); } private: // In order to avoid wasting computations, we once compute a look-up table // storing all function values for all possible values of the standard // deviation \sigma. This is implemented as a (N + 1) vector. std::array look_up_table_; }; } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/visual_index.h000066400000000000000000000653561454702036400217250ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/types.h" #include "colmap/math/math.h" #include "colmap/retrieval/inverted_file.h" #include "colmap/retrieval/inverted_index.h" #include "colmap/retrieval/vote_and_verify.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/endian.h" #include "colmap/util/logging.h" #include #include #include namespace colmap { namespace retrieval { // Visual index for image retrieval using a vocabulary tree with Hamming // embedding, based on the papers: // // Schönberger, Price, Sattler, Pollefeys, Frahm. "A Vote-and-Verify Strategy // for Fast Spatial Verification in Image Retrieval". ACCV 2016. // // Arandjelovic, Zisserman: Scalable descriptor // distinctiveness for location recognition. ACCV 2014. template class VisualIndex { public: static const int kMaxNumThreads = -1; typedef InvertedIndex InvertedIndexType; typedef FeatureKeypoints GeomType; typedef typename InvertedIndexType::DescType DescType; typedef typename InvertedIndexType::EntryType EntryType; struct IndexOptions { // The number of nearest neighbor visual words that each feature descriptor // is assigned to. int num_neighbors = 1; // The number of checks in the nearest neighbor search. int num_checks = 256; // The number of threads used in the index. int num_threads = kMaxNumThreads; }; struct QueryOptions { // The maximum number of most similar images to retrieve. int max_num_images = -1; // The number of nearest neighbor visual words that each feature descriptor // is assigned to. int num_neighbors = 5; // The number of checks in the nearest neighbor search. int num_checks = 256; // Whether to perform spatial verification after image retrieval. int num_images_after_verification = 0; // The number of threads used in the index. int num_threads = kMaxNumThreads; }; struct BuildOptions { // The desired number of visual words, i.e. the number of leaf node // clusters. Note that the actual number of visual words might be less. int num_visual_words = 256 * 256; // The branching factor of the hierarchical k-means tree. int branching = 256; // The number of iterations for the clustering. int num_iterations = 11; // The target precision of the visual word search index. double target_precision = 0.95; // The number of checks in the nearest neighbor search. int num_checks = 256; // The number of threads used in the index. int num_threads = kMaxNumThreads; }; VisualIndex(); ~VisualIndex(); size_t NumVisualWords() const; // Add image to the visual index. void Add(const IndexOptions& options, int image_id, const GeomType& geometries, const DescType& descriptors); // Check if an image has been indexed. bool ImageIndexed(int image_id) const; // Query for most similar images in the visual index. void Query(const QueryOptions& options, const DescType& descriptors, std::vector* image_scores) const; // Query for most similar images in the visual index. void Query(const QueryOptions& options, const GeomType& geometries, const DescType& descriptors, std::vector* image_scores) const; // Prepare the index after adding images and before querying. void Prepare(); // Build a visual index from a set of training descriptors by quantizing the // descriptor space into visual words and compute their Hamming embedding. void Build(const BuildOptions& options, const DescType& descriptors); // Read and write the visual index. This can be done for an index with and // without indexed images. void Read(const std::string& path); void Write(const std::string& path); private: // Quantize the descriptor space into visual words. void Quantize(const BuildOptions& options, const DescType& descriptors); // Query for nearest neighbor images and return nearest neighbor visual word // identifiers for each descriptor. void QueryAndFindWordIds(const QueryOptions& options, const DescType& descriptors, std::vector* image_scores, Eigen::MatrixXi* word_ids) const; // Find the nearest neighbor visual words for the given descriptors. Eigen::MatrixXi FindWordIds(const DescType& descriptors, int num_neighbors, int num_checks, int num_threads) const; // The search structure on the quantized descriptor space. flann::AutotunedIndex> visual_word_index_; // The centroids of the visual words. flann::Matrix visual_words_; // The inverted index of the database. InvertedIndexType inverted_index_; // Identifiers of all indexed images. std::unordered_set image_ids_; // Whether the index is prepared. bool prepared_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template VisualIndex::VisualIndex() : prepared_(false) {} template VisualIndex::~VisualIndex() { if (visual_words_.ptr() != nullptr) { delete[] visual_words_.ptr(); } } template size_t VisualIndex::NumVisualWords() const { return visual_words_.rows; } template void VisualIndex::Add( const IndexOptions& options, const int image_id, const GeomType& geometries, const DescType& descriptors) { CHECK_EQ(geometries.size(), descriptors.rows()); // If the image is already indexed, do nothing. if (ImageIndexed(image_id)) { return; } image_ids_.insert(image_id); prepared_ = false; if (descriptors.rows() == 0) { return; } const Eigen::MatrixXi word_ids = FindWordIds(descriptors, options.num_neighbors, options.num_checks, options.num_threads); for (typename DescType::Index i = 0; i < descriptors.rows(); ++i) { const auto& descriptor = descriptors.row(i); typename InvertedIndexType::GeomType geometry; geometry.x = geometries[i].x; geometry.y = geometries[i].y; geometry.scale = geometries[i].ComputeScale(); geometry.orientation = geometries[i].ComputeOrientation(); for (int n = 0; n < options.num_neighbors; ++n) { const int word_id = word_ids(i, n); if (word_id != InvertedIndexType::kInvalidWordId) { inverted_index_.AddEntry(image_id, word_id, i, descriptor, geometry); } } } } template bool VisualIndex::ImageIndexed( const int image_id) const { return image_ids_.count(image_id) != 0; } template void VisualIndex::Query( const QueryOptions& options, const DescType& descriptors, std::vector* image_scores) const { const GeomType geometries; Query(options, geometries, descriptors, image_scores); } template void VisualIndex::Query( const QueryOptions& options, const GeomType& geometries, const DescType& descriptors, std::vector* image_scores) const { Eigen::MatrixXi word_ids; QueryAndFindWordIds(options, descriptors, image_scores, &word_ids); if (options.num_images_after_verification <= 0) { return; } CHECK_EQ(descriptors.rows(), geometries.size()); // Extract top-ranked images to verify. std::unordered_set image_ids; for (const auto& image_score : *image_scores) { image_ids.insert(image_score.image_id); } // Find matches for top-ranked images typedef std::vector< std::pair>> OrderedMatchListType; // Reference our matches (with their lowest distance) for both // {query feature => db feature} and vice versa. std::unordered_map> query_to_db_matches; std::unordered_map> db_to_query_matches; std::vector word_matches; std::vector query_entries; // Convert query features, too. query_entries.reserve(descriptors.rows()); // NOTE: Currently, we are redundantly computing the feature weighting. const HammingDistWeightFunctor hamming_dist_weight_functor; for (typename DescType::Index i = 0; i < descriptors.rows(); ++i) { const auto& descriptor = descriptors.row(i); EntryType query_entry; query_entry.feature_idx = i; query_entry.geometry.x = geometries[i].x; query_entry.geometry.y = geometries[i].y; query_entry.geometry.scale = geometries[i].ComputeScale(); query_entry.geometry.orientation = geometries[i].ComputeOrientation(); query_entries.push_back(query_entry); // For each db feature, keep track of the lowest distance (if db features // are mapped to more than one visual word). std::unordered_map< int, std::unordered_map>> image_matches; for (int j = 0; j < word_ids.cols(); ++j) { const int word_id = word_ids(i, j); if (word_id != InvertedIndexType::kInvalidWordId) { inverted_index_.ConvertToBinaryDescriptor( word_id, descriptor, &query_entries[i].descriptor); const auto idf_weight = inverted_index_.GetIDFWeight(word_id); const auto squared_idf_weight = idf_weight * idf_weight; inverted_index_.FindMatches(word_id, image_ids, &word_matches); for (const auto& match : word_matches) { const size_t hamming_dist = (query_entries[i].descriptor ^ match->descriptor).count(); if (hamming_dist <= hamming_dist_weight_functor.kMaxHammingDistance) { const float dist = hamming_dist_weight_functor(hamming_dist) * squared_idf_weight; auto& feature_matches = image_matches[match->image_id]; const auto feature_match = feature_matches.find(match->feature_idx); if (feature_match == feature_matches.end() || feature_match->first < dist) { feature_matches[match->feature_idx] = std::make_pair(dist, match); } } } } } // Finally, cross-reference the query and db feature matches. for (const auto& feature_matches : image_matches) { const auto image_id = feature_matches.first; for (const auto& feature_match : feature_matches.second) { const auto feature_idx = feature_match.first; const auto dist = feature_match.second.first; const auto db_match = feature_match.second.second; const auto entry_pair = std::make_pair(&query_entries[i], db_match); query_to_db_matches[image_id][i].emplace_back(dist, entry_pair); db_to_query_matches[image_id][feature_idx].emplace_back(dist, entry_pair); } } } // Verify top-ranked images using the found matches. for (auto& image_score : *image_scores) { auto& query_matches = query_to_db_matches[image_score.image_id]; auto& db_matches = db_to_query_matches[image_score.image_id]; // No matches found. if (query_matches.empty()) { continue; } // Enforce 1-to-1 matching: Build Fibonacci heaps for the query and database // features, ordered by the minimum number of matches per feature. We'll // select these matches one at a time. For convenience, we'll also pre-sort // the matched feature lists by matching score. typedef boost::heap::fibonacci_heap> FibonacciHeapType; FibonacciHeapType query_heap; FibonacciHeapType db_heap; std::unordered_map query_heap_handles; std::unordered_map db_heap_handles; for (auto& match_data : query_matches) { std::sort( match_data.second.begin(), match_data.second.end(), std::greater< std::pair>>()); query_heap_handles[match_data.first] = query_heap.push(std::make_pair( -static_cast(match_data.second.size()), match_data.first)); } for (auto& match_data : db_matches) { std::sort( match_data.second.begin(), match_data.second.end(), std::greater< std::pair>>()); db_heap_handles[match_data.first] = db_heap.push(std::make_pair( -static_cast(match_data.second.size()), match_data.first)); } // Keep tabs on what features have been already matched. std::vector matches; auto db_top = db_heap.top(); // (-num_available_matches, feature_idx) auto query_top = query_heap.top(); while (!db_heap.empty() && !query_heap.empty()) { // Take the query or database feature with the smallest number of // available matches. const bool use_query = (query_top.first >= db_top.first) && !query_heap.empty(); // Find the best matching feature that hasn't already been matched. auto& heap1 = (use_query) ? query_heap : db_heap; auto& heap2 = (use_query) ? db_heap : query_heap; auto& handles1 = (use_query) ? query_heap_handles : db_heap_handles; auto& handles2 = (use_query) ? db_heap_handles : query_heap_handles; auto& matches1 = (use_query) ? query_matches : db_matches; auto& matches2 = (use_query) ? db_matches : query_matches; const auto idx1 = heap1.top().second; heap1.pop(); // Entries that have been matched (or processed and subsequently ignored) // get their handles removed. if (handles1.count(idx1) > 0) { handles1.erase(idx1); bool match_found = false; // The matches have been ordered by Hamming distance, already -- // select the lowest available match. for (auto& entry2 : matches1[idx1]) { const auto idx2 = (use_query) ? entry2.second.second->feature_idx : entry2.second.first->feature_idx; if (handles2.count(idx2) > 0) { if (!match_found) { match_found = true; FeatureGeometryMatch match; match.geometry1 = entry2.second.first->geometry; match.geometry2 = entry2.second.second->geometry; matches.push_back(match); handles2.erase(idx2); // Remove this feature from consideration for all other features // that matched to it. for (auto& entry1 : matches2[idx2]) { const auto other_idx1 = (use_query) ? entry1.second.first->feature_idx : entry1.second.second->feature_idx; if (handles1.count(other_idx1) > 0) { (*handles1[other_idx1]).first += 1; heap1.increase(handles1[other_idx1]); } } } else { (*handles2[idx2]).first += 1; heap2.increase(handles2[idx2]); } } } } if (!query_heap.empty()) { query_top = query_heap.top(); } if (!db_heap.empty()) { db_top = db_heap.top(); } } // Finally, run verification for the current image. VoteAndVerifyOptions vote_and_verify_options; image_score.score += VoteAndVerify(vote_and_verify_options, matches); } // Re-rank the images using the spatial verification scores. const size_t num_images = std::min( image_scores->size(), options.num_images_after_verification); auto SortFunc = [](const ImageScore& score1, const ImageScore& score2) { return score1.score > score2.score; }; if (num_images == image_scores->size()) { std::sort(image_scores->begin(), image_scores->end(), SortFunc); } else { std::partial_sort(image_scores->begin(), image_scores->begin() + num_images, image_scores->end(), SortFunc); image_scores->resize(num_images); } } template void VisualIndex::Prepare() { inverted_index_.Finalize(); prepared_ = true; } template void VisualIndex::Build( const BuildOptions& options, const DescType& descriptors) { // Quantize the descriptor space into visual words. Quantize(options, descriptors); // Build the search index on the visual words. flann::AutotunedIndexParams index_params; index_params["target_precision"] = static_cast(options.target_precision); visual_word_index_ = flann::AutotunedIndex>(index_params); visual_word_index_.buildIndex(visual_words_); // Initialize a new inverted index. inverted_index_ = InvertedIndexType(); inverted_index_.Initialize(NumVisualWords()); // Generate descriptor projection matrix. inverted_index_.GenerateHammingEmbeddingProjection(); // Learn the Hamming embedding. const int kNumNeighbors = 1; const Eigen::MatrixXi word_ids = FindWordIds( descriptors, kNumNeighbors, options.num_checks, options.num_threads); inverted_index_.ComputeHammingEmbedding(descriptors, word_ids); } template void VisualIndex::Read( const std::string& path) { long int file_offset = 0; // Read the visual words. { if (visual_words_.ptr() != nullptr) { delete[] visual_words_.ptr(); } std::ifstream file(path, std::ios::binary); CHECK(file.is_open()) << path; const uint64_t rows = ReadBinaryLittleEndian(&file); const uint64_t cols = ReadBinaryLittleEndian(&file); kDescType* visual_words_data = new kDescType[rows * cols]; for (size_t i = 0; i < rows * cols; ++i) { visual_words_data[i] = ReadBinaryLittleEndian(&file); } visual_words_ = flann::Matrix(visual_words_data, rows, cols); file_offset = file.tellg(); } // Read the visual words search index. visual_word_index_ = flann::AutotunedIndex>(visual_words_); { FILE* fin = fopen(path.c_str(), "rb"); CHECK_NOTNULL(fin); fseek(fin, file_offset, SEEK_SET); visual_word_index_.loadIndex(fin); file_offset = ftell(fin); fclose(fin); } // Read the inverted index. { std::ifstream file(path, std::ios::binary); CHECK(file.is_open()) << path; file.seekg(file_offset, std::ios::beg); inverted_index_.Read(&file); } image_ids_.clear(); inverted_index_.GetImageIds(&image_ids_); } template void VisualIndex::Write( const std::string& path) { // Write the visual words. { CHECK_NOTNULL(visual_words_.ptr()); std::ofstream file(path, std::ios::binary); CHECK(file.is_open()) << path; WriteBinaryLittleEndian(&file, visual_words_.rows); WriteBinaryLittleEndian(&file, visual_words_.cols); for (size_t i = 0; i < visual_words_.rows * visual_words_.cols; ++i) { WriteBinaryLittleEndian(&file, visual_words_.ptr()[i]); } } // Write the visual words search index. { FILE* fout = fopen(path.c_str(), "ab"); CHECK_NOTNULL(fout); visual_word_index_.saveIndex(fout); fclose(fout); } // Write the inverted index. { std::ofstream file(path, std::ios::binary | std::ios::app); CHECK(file.is_open()) << path; inverted_index_.Write(&file); } } template void VisualIndex::Quantize( const BuildOptions& options, const DescType& descriptors) { static_assert(DescType::IsRowMajor, "Descriptors must be row-major."); CHECK_GE(options.num_visual_words, options.branching); CHECK_GE(descriptors.rows(), options.num_visual_words); const flann::Matrix descriptor_matrix( const_cast(descriptors.data()), descriptors.rows(), descriptors.cols()); std::vector::ResultType> centers_data( options.num_visual_words * descriptors.cols()); flann::Matrix::ResultType> centers( centers_data.data(), options.num_visual_words, descriptors.cols()); flann::KMeansIndexParams index_params; index_params["branching"] = options.branching; index_params["iterations"] = options.num_iterations; index_params["centers_init"] = flann::FLANN_CENTERS_KMEANSPP; // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) const int num_centers = flann::hierarchicalClustering>( descriptor_matrix, centers, index_params); CHECK_LE(num_centers, options.num_visual_words); const size_t visual_word_data_size = num_centers * descriptors.cols(); kDescType* visual_words_data = new kDescType[visual_word_data_size]; for (size_t i = 0; i < visual_word_data_size; ++i) { if (std::is_integral::value) { visual_words_data[i] = std::round(centers_data[i]); } else { visual_words_data[i] = centers_data[i]; } } if (visual_words_.ptr() != nullptr) { delete[] visual_words_.ptr(); } visual_words_ = flann::Matrix( visual_words_data, num_centers, descriptors.cols()); } template void VisualIndex::QueryAndFindWordIds( const QueryOptions& options, const DescType& descriptors, std::vector* image_scores, Eigen::MatrixXi* word_ids) const { CHECK(prepared_); if (descriptors.rows() == 0) { image_scores->clear(); return; } *word_ids = FindWordIds(descriptors, options.num_neighbors, options.num_checks, options.num_threads); inverted_index_.Query(descriptors, *word_ids, image_scores); auto SortFunc = [](const ImageScore& score1, const ImageScore& score2) { return score1.score > score2.score; }; size_t num_images = image_scores->size(); if (options.max_num_images >= 0) { num_images = std::min(image_scores->size(), options.max_num_images); } if (num_images == image_scores->size()) { std::sort(image_scores->begin(), image_scores->end(), SortFunc); } else { std::partial_sort(image_scores->begin(), image_scores->begin() + num_images, image_scores->end(), SortFunc); image_scores->resize(num_images); } } template Eigen::MatrixXi VisualIndex::FindWordIds( const DescType& descriptors, const int num_neighbors, const int num_checks, const int num_threads) const { static_assert(DescType::IsRowMajor, "Descriptors must be row-major"); CHECK_GT(descriptors.rows(), 0); CHECK_GT(num_neighbors, 0); Eigen::Matrix word_ids(descriptors.rows(), num_neighbors); word_ids.setConstant(InvertedIndexType::kInvalidWordId); flann::Matrix indices( word_ids.data(), descriptors.rows(), num_neighbors); Eigen::Matrix::ResultType, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> distance_matrix(descriptors.rows(), num_neighbors); flann::Matrix::ResultType> distances( distance_matrix.data(), descriptors.rows(), num_neighbors); const flann::Matrix query( const_cast(descriptors.data()), descriptors.rows(), descriptors.cols()); flann::SearchParams search_params(num_checks); if (num_threads < 0) { search_params.cores = std::thread::hardware_concurrency(); } else { search_params.cores = num_threads; } if (search_params.cores <= 0) { search_params.cores = 1; } visual_word_index_.knnSearch( query, indices, distances, num_neighbors, search_params); return word_ids.cast(); } } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/visual_index_test.cc000066400000000000000000000121301454702036400231000ustar00rootroot00000000000000// 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. #include "colmap/retrieval/visual_index.h" #include namespace colmap { namespace retrieval { namespace { template void TestVocabTreeType() { typedef VisualIndex VisualIndexType; SetPRNGSeed(0); { VisualIndexType visual_index; EXPECT_EQ(visual_index.NumVisualWords(), 0); } { typename VisualIndexType::DescType descriptors = VisualIndexType::DescType::Random(50, kDescDim); VisualIndexType visual_index; EXPECT_EQ(visual_index.NumVisualWords(), 0); typename VisualIndexType::BuildOptions build_options; build_options.num_visual_words = 5; build_options.branching = 5; visual_index.Build(build_options, descriptors); EXPECT_EQ(visual_index.NumVisualWords(), 5); } { typename VisualIndexType::DescType descriptors = VisualIndexType::DescType::Random(1000, kDescDim); VisualIndexType visual_index; EXPECT_EQ(visual_index.NumVisualWords(), 0); typename VisualIndexType::BuildOptions build_options; build_options.num_visual_words = 100; build_options.branching = 10; visual_index.Build(build_options, descriptors); EXPECT_EQ(visual_index.NumVisualWords(), 100); typename VisualIndexType::IndexOptions index_options; typename VisualIndexType::GeomType keypoints1(50); typename VisualIndexType::DescType descriptors1 = VisualIndexType::DescType::Random(50, kDescDim); visual_index.Add(index_options, 1, keypoints1, descriptors1); typename VisualIndexType::GeomType keypoints2(50); typename VisualIndexType::DescType descriptors2 = VisualIndexType::DescType::Random(50, kDescDim); visual_index.Add(index_options, 2, keypoints2, descriptors2); visual_index.Prepare(); typename VisualIndexType::QueryOptions query_options; std::vector image_scores; visual_index.Query(query_options, descriptors1, &image_scores); EXPECT_EQ(image_scores.size(), 2); EXPECT_EQ(image_scores[0].image_id, 1); EXPECT_EQ(image_scores[1].image_id, 2); EXPECT_GT(image_scores[0].score, image_scores[1].score); query_options.max_num_images = 1; visual_index.Query(query_options, descriptors1, &image_scores); EXPECT_EQ(image_scores.size(), 1); EXPECT_EQ(image_scores[0].image_id, 1); query_options.max_num_images = 3; visual_index.Query(query_options, descriptors1, &image_scores); EXPECT_EQ(image_scores.size(), 2); EXPECT_EQ(image_scores[0].image_id, 1); EXPECT_EQ(image_scores[1].image_id, 2); EXPECT_GT(image_scores[0].score, image_scores[1].score); } } TEST(VisualIndex, uint8_t_128_64) { // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) TestVocabTreeType(); } TEST(VisualIndex, uint8_t_64_64) { // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) TestVocabTreeType(); } TEST(VisualIndex, uint8_t_32_16) { // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) TestVocabTreeType(); } TEST(VisualIndex, int_32_16) { // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) TestVocabTreeType(); } TEST(VisualIndex, float_32_16) { // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) TestVocabTreeType(); } TEST(VisualIndex, double_32_16) { // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) TestVocabTreeType(); } } // namespace } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/vote_and_verify.cc000066400000000000000000000403151454702036400225400ustar00rootroot00000000000000// 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. #include "colmap/retrieval/vote_and_verify.h" #include "colmap/estimators/affine_transform.h" #include "colmap/math/math.h" #include "colmap/optim/ransac.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include #include #include namespace colmap { namespace retrieval { namespace { // Affine transformation from left to right and from left to right image. struct TwoWayTransform { TwoWayTransform() : A12(Eigen::Matrix2f::Zero()), t12(Eigen::Vector2f::Zero()), A21(Eigen::Matrix2f::Zero()), t21(Eigen::Vector2f::Zero()) {} explicit TwoWayTransform(const FeatureGeometryTransform& tform) { const float sin_angle = std::sin(tform.angle); const float cos_angle = std::cos(tform.angle); Eigen::Matrix2f R; R << cos_angle, -sin_angle, sin_angle, cos_angle; A12 = tform.scale * R; t12 << tform.tx, tform.ty; A21 = R.transpose() / tform.scale; t21 = -A21 * t12; } Eigen::Matrix2f A12; Eigen::Vector2f t12; Eigen::Matrix2f A21; Eigen::Vector2f t21; }; // Class representing a single bin in the voting space of a similarity // transformation. Keeps track of the mean transformation described by the bin. class VotingBin { public: void SetCoord(const Eigen::Vector4i& coord) { coord_ = coord; } void Vote(const FeatureGeometryTransform& tform) { num_votes_ += 1; sum_tform_.scale += tform.scale; sum_tform_.angle += tform.angle; sum_tform_.tx += tform.tx; sum_tform_.ty += tform.ty; } inline const Eigen::Vector4i& GetCoord() const { return coord_; } inline int GetNumVotes() const { return num_votes_; } // Compute the mean transformation of the voting bin. FeatureGeometryTransform GetMeanTransformation() const { const float inv_num_votes = 1.0f / static_cast(num_votes_); FeatureGeometryTransform tform = sum_tform_; tform.scale *= inv_num_votes; tform.angle *= inv_num_votes; tform.tx *= inv_num_votes; tform.ty *= inv_num_votes; return tform; } private: Eigen::Vector4i coord_; int num_votes_ = 0; FeatureGeometryTransform sum_tform_; }; // Compute the difference in scale between the two features when aligning them // with the given transformation. float ComputeScaleError(const FeatureGeometry& feature1, const FeatureGeometry& feature2, const TwoWayTransform& tform) { const float area_transformed = feature1.GetAreaUnderTransform(tform.A21); const float area_measured = feature2.GetArea(); if (area_transformed > area_measured) { return area_transformed / area_measured; } else { return area_measured / area_transformed; } } // Compute the two-way transfer error between two features. float ComputeTransferError(const FeatureGeometry& feature1, const FeatureGeometry& feature2, const TwoWayTransform& tform) { const Eigen::Vector2f xy1(feature1.x, feature1.y); const Eigen::Vector2f xy2(feature2.x, feature2.y); const float error1 = (xy2 - tform.A12 * xy1 - tform.t12).squaredNorm(); const float error2 = (xy1 - tform.A21 * xy2 - tform.t21).squaredNorm(); return error1 + error2; } // Compute inlier matches that satisfy the transfer, scale thresholds. void ComputeInliers(const TwoWayTransform& tform, const std::vector& matches, float max_transfer_error, float max_scale_error, size_t best_num_inliers, std::vector* inlier_idxs) { CHECK_GT(max_transfer_error, 0); CHECK_GT(max_scale_error, 0); const size_t num_matches = matches.size(); const size_t max_num_outliers = num_matches - best_num_inliers; inlier_idxs->clear(); inlier_idxs->reserve(num_matches); size_t num_outliers = 0; for (size_t i = 0; i < num_matches; ++i) { const auto& match = matches[i]; if (ComputeScaleError(match.geometry1, match.geometry2, tform) <= max_scale_error && ComputeTransferError(match.geometry1, match.geometry2, tform) <= max_transfer_error) { inlier_idxs->emplace_back(i); } else { num_outliers += 1; if (num_outliers > max_num_outliers) { break; } } } } // Compute effective inlier count that satisfy the transfer, scale thresholds. size_t ComputeEffectiveInlierCount( const TwoWayTransform& tform, const std::vector& matches, const float max_transfer_error, const float max_scale_error, const int num_bins) { CHECK_GT(max_transfer_error, 0); CHECK_GT(max_scale_error, 0); CHECK_GT(num_bins, 0); std::vector> inlier_coords; inlier_coords.reserve(matches.size()); float min_x = std::numeric_limits::max(); float min_y = std::numeric_limits::max(); float max_x = 0; float max_y = 0; for (const auto& match : matches) { if (ComputeScaleError(match.geometry1, match.geometry2, tform) <= max_scale_error && ComputeTransferError(match.geometry1, match.geometry2, tform) <= max_transfer_error) { inlier_coords.emplace_back(match.geometry1.x, match.geometry1.y); min_x = std::min(min_x, match.geometry1.x); min_y = std::min(min_y, match.geometry1.y); max_x = std::max(max_x, match.geometry1.x); max_y = std::max(max_y, match.geometry1.y); } } if (inlier_coords.empty()) { return 0; } const float scale_x = num_bins / (max_x - min_x); const float scale_y = num_bins / (max_y - min_y); Eigen::Matrix counter(num_bins, num_bins); counter.setZero(); for (const auto& coord : inlier_coords) { const int c_x = (coord.first - min_x) * scale_x; const int c_y = (coord.second - min_y) * scale_y; counter(std::max(0, std::min(num_bins - 1, c_x)), std::max(0, std::min(num_bins - 1, c_y))) = 1; } return counter.sum(); } } // namespace int VoteAndVerify(const VoteAndVerifyOptions& options, const std::vector& matches) { CHECK_GT(options.num_levels, 0); CHECK_GT(options.num_transformations, 0); CHECK_GT(options.num_trans_bins, 0); CHECK_EQ(options.num_trans_bins % 2, 0); CHECK_GT(options.num_scale_bins, 0); CHECK_EQ(options.num_scale_bins % 2, 0); CHECK_GT(options.num_angle_bins, 0); CHECK_EQ(options.num_angle_bins % 2, 0); CHECK_GT(options.max_image_size, 0); CHECK_GT(options.min_num_votes, 0); CHECK_GE(options.confidence, 0); CHECK_LE(options.confidence, 1); CHECK_GT(options.num_eff_inlier_bins, 0); const size_t num_matches = matches.size(); if (num_matches < AffineTransformEstimator::kMinNumSamples) { return 0; } const float max_trans = options.max_image_size; const float kMaxScale = 10.0f; const float max_log_scale = std::log2(kMaxScale); const float trans_norm = 1.0f / (2.0f * max_trans); const float scale_norm = 1.0f / (2.0f * max_log_scale); const float angle_norm = 1.0f / (2.0f * M_PI); ////////////////////////////////////////////////////////////////////////////// // Fill the multi-resolution voting histogram. ////////////////////////////////////////////////////////////////////////////// std::vector> bins(options.num_levels); for (auto& levelBins : bins) { levelBins.reserve(num_matches); } for (const auto& match : matches) { const auto T = FeatureGeometry::TransformFromMatch(match.geometry1, match.geometry2); if (std::abs(T.tx) > max_trans || std::abs(T.ty) > max_trans) { continue; } const float log_scale = std::log2(T.scale); if (std::abs(log_scale) > max_log_scale) { continue; } const float x = (T.tx + max_trans) * trans_norm; const float y = (T.ty + max_trans) * trans_norm; const float s = (log_scale + max_log_scale) * scale_norm; const float a = (T.angle + M_PI) * angle_norm; int n_x = std::min(static_cast(x * options.num_trans_bins), static_cast(options.num_trans_bins - 1)); int n_y = std::min(static_cast(y * options.num_trans_bins), static_cast(options.num_trans_bins - 1)); int n_s = std::min(static_cast(s * options.num_scale_bins), static_cast(options.num_scale_bins - 1)); int n_a = std::min(static_cast(a * options.num_angle_bins), static_cast(options.num_angle_bins - 1)); for (int level = 0; level < options.num_levels; ++level) { const size_t index = n_a + options.num_angle_bins * (n_s + options.num_scale_bins * (n_x + options.num_trans_bins * n_y)); if (level == 0) { bins[level][index].SetCoord(Eigen::Vector4i(n_a, n_s, n_x, n_y)); } bins[level][index].Vote(T); n_x >>= 1; n_y >>= 1; n_s >>= 1; n_a >>= 1; } } ////////////////////////////////////////////////////////////////////////////// // Compute the multi-resolution scores for all occupied bins. ////////////////////////////////////////////////////////////////////////////// std::vector> bin_scores; bin_scores.reserve(bins[0].size()); for (const auto& bin : bins[0]) { if (bin.second.GetNumVotes() >= options.min_num_votes) { const Eigen::Vector4i& coord = bin.second.GetCoord(); int n_a = coord(0); int n_s = coord(1); int n_x = coord(2); int n_y = coord(3); float score = bin.second.GetNumVotes(); float level_weight = 0.5f; for (int level = 1; level < options.num_levels; ++level) { n_x >>= 1; n_y >>= 1; n_s >>= 1; n_a >>= 1; const uint64_t index = n_a + options.num_angle_bins * (n_s + options.num_scale_bins * (n_x + options.num_trans_bins * n_y)); score += bins[level][index].GetNumVotes() * level_weight; level_weight *= 0.5f; } bin_scores.emplace_back(bin.first, score); } } ////////////////////////////////////////////////////////////////////////////// // Extract the top transformations. ////////////////////////////////////////////////////////////////////////////// const size_t num_transformations = std::min( static_cast(options.num_transformations), bin_scores.size()); std::partial_sort(bin_scores.begin(), bin_scores.begin() + num_transformations, bin_scores.end(), [](const std::pair& score1, const std::pair& score2) { return score1.second > score2.second; }); ////////////////////////////////////////////////////////////////////////////// // Verify the top transformations. ////////////////////////////////////////////////////////////////////////////// size_t max_num_trials = std::numeric_limits::max(); TwoWayTransform best_tform; std::vector inlier_idxs; size_t best_num_inliers = 0; std::vector best_inlier_idxs; for (size_t i = 0; i < num_transformations && i < max_num_trials; ++i) { const VotingBin& bin = bins[0].at(bin_scores[i].first); const TwoWayTransform tform(bin.GetMeanTransformation()); ComputeInliers(tform, matches, options.max_transfer_error, options.max_scale_error, best_num_inliers, &inlier_idxs); if (inlier_idxs.size() < best_num_inliers || inlier_idxs.size() < AffineTransformEstimator::kMinNumSamples) { continue; } best_num_inliers = inlier_idxs.size(); if (options.local_optimization) { best_inlier_idxs = inlier_idxs; } best_tform = tform; if (best_num_inliers == num_matches) { break; } max_num_trials = RANSAC::ComputeNumTrials( best_num_inliers, num_matches, options.confidence, /*num_trials_multiplier=*/1.0); } ////////////////////////////////////////////////////////////////////////////// // Local optimization of best transformation. ////////////////////////////////////////////////////////////////////////////// if (options.local_optimization && best_num_inliers > 0) { // Collect matching inlier points. const size_t num_inliers = best_inlier_idxs.size(); std::vector best_inlier_points1(num_inliers); std::vector best_inlier_points2(num_inliers); for (size_t i = 0; i < num_inliers; ++i) { const auto& match = matches.at(best_inlier_idxs[i]); best_inlier_points1[i] = Eigen::Vector2d(match.geometry1.x, match.geometry1.y); best_inlier_points2[i] = Eigen::Vector2d(match.geometry2.x, match.geometry2.y); } // Local optimization on matching inlier points. std::vector> models; AffineTransformEstimator::Estimate( best_inlier_points1, best_inlier_points2, &models); CHECK_EQ(models.size(), 1); const Eigen::Matrix& A = models[0]; Eigen::Matrix3d A_homogeneous = Eigen::Matrix3d::Identity(); A_homogeneous.topRows<2>() = A; const Eigen::Matrix inv_A = A_homogeneous.inverse().topRows<2>(); TwoWayTransform local_tform; local_tform.A12 = A.leftCols<2>().cast(); local_tform.t12 = A.rightCols<1>().cast(); local_tform.A21 = inv_A.leftCols<2>().cast(); local_tform.t21 = inv_A.rightCols<1>().cast(); ComputeInliers(local_tform, matches, options.max_transfer_error, options.max_scale_error, best_num_inliers, &inlier_idxs); if (inlier_idxs.size() > best_num_inliers) { best_num_inliers = inlier_idxs.size(); best_tform = local_tform; } } ////////////////////////////////////////////////////////////////////////////// // Effective inlier counting. ////////////////////////////////////////////////////////////////////////////// if (options.eff_inlier_count && best_num_inliers > 0) { best_num_inliers = ComputeEffectiveInlierCount(best_tform, matches, options.max_transfer_error, options.max_scale_error, options.num_eff_inlier_bins); } return best_num_inliers; } } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/vote_and_verify.h000066400000000000000000000063341454702036400224050ustar00rootroot00000000000000// 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. #pragma once #include "colmap/retrieval/geometry.h" namespace colmap { namespace retrieval { struct VoteAndVerifyOptions { // Number of levels in the multi-resolution histogram. int num_levels = 3; // Number of top transformations to generate. int num_transformations = 10; // Number of voting bins in the translation dimension. int num_trans_bins = 64; // Number of voting bins in the scale dimension. int num_scale_bins = 32; // Number of voting bins in the orientation dimension. int num_angle_bins = 8; // Maximum image dimension that bounds the range of the translation bins. int max_image_size = 4096; // Minimum number of votes for a transformation to be considered. int min_num_votes = 1; // RANSAC confidence level used to abort the iteration. double confidence = 0.99; // Thresholds for considering a match an inlier. double max_transfer_error = 100.0 * 100.0; double max_scale_error = 2.0; // Whether to enable local optimization of top transformations. bool local_optimization = true; // Whether to enable effective inlier counting for best transformation. bool eff_inlier_count = true; int num_eff_inlier_bins = 32; }; // Compute effective inlier count using Vote-and-Verify by estimating an affine // transformation from 2D-2D image matches. The method is described in: // "A Vote­-and­-Verify Strategy for // Fast Spatial Verification in Image Retrieval", // Schönberger et al., ACCV 2016. int VoteAndVerify(const VoteAndVerifyOptions& options, const std::vector& matches); } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/retrieval/vote_and_verify_test.cc000066400000000000000000000111511454702036400235730ustar00rootroot00000000000000// 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. #include "colmap/retrieval/visual_index.h" #include namespace colmap { namespace retrieval { namespace { struct SyntheticData { FeatureGeometryTransform img2_from_img1; std::vector matches; }; SyntheticData SynthesizeData(size_t num_inliers, size_t num_outliers) { SyntheticData data; data.img2_from_img1.scale = RandomUniformReal(0.1, 10); data.img2_from_img1.angle = RandomUniformReal(0, 2 * M_PI); data.img2_from_img1.tx = RandomUniformReal(-100, 100); data.img2_from_img1.ty = RandomUniformReal(-100, 100); const float sin_angle = std::sin(data.img2_from_img1.angle); const float cos_angle = std::cos(data.img2_from_img1.angle); data.matches.resize(num_inliers + num_outliers); for (size_t i = 0; i < num_inliers; ++i) { FeatureGeometryMatch& match = data.matches[i]; match.geometry1.scale = RandomUniformReal(0.5, 2); match.geometry1.orientation = RandomUniformReal(0, 2 * M_PI); match.geometry1.x = RandomUniformReal(-100, 100); match.geometry1.y = RandomUniformReal(-100, 100); match.geometry2.scale = data.img2_from_img1.scale * match.geometry1.scale; match.geometry2.orientation = data.img2_from_img1.angle + match.geometry1.orientation; match.geometry2.x = data.img2_from_img1.scale * (cos_angle * match.geometry1.x - sin_angle * match.geometry1.y); match.geometry2.y = data.img2_from_img1.scale * (sin_angle * match.geometry1.x + cos_angle * match.geometry1.y); } for (size_t i = 0; i < num_outliers; ++i) { FeatureGeometryMatch& match = data.matches[i + num_inliers]; match.geometry1.scale = RandomUniformReal(0.5, 2); match.geometry1.orientation = RandomUniformReal(0, 2 * M_PI); match.geometry1.x = RandomUniformReal(-100, 100); match.geometry1.y = RandomUniformReal(-100, 100); match.geometry2.scale = RandomUniformReal(0.5, 2); match.geometry2.orientation = RandomUniformReal(0, 2 * M_PI); match.geometry2.x = RandomUniformReal(-100, 100); match.geometry2.y = RandomUniformReal(-100, 100); } return data; } TEST(VoteAndVerify, NoMatches) { EXPECT_EQ(VoteAndVerify(VoteAndVerifyOptions(), {}), 0); } TEST(VoteAndVerify, NoEffectiveInliers) { const size_t kNumInliers = 100; const size_t kNumOutliers = 50; const auto data = SynthesizeData(kNumInliers, kNumOutliers); VoteAndVerifyOptions options; options.eff_inlier_count = false; const int num_inliers = VoteAndVerify(options, data.matches); EXPECT_EQ(num_inliers, kNumInliers); } TEST(VoteAndVerify, EffectiveInliers) { const size_t kNumInliers = 100; const size_t kNumOutliers = 50; const auto data = SynthesizeData(kNumInliers, kNumOutliers); VoteAndVerifyOptions options; options.eff_inlier_count = true; const int num_inliers = VoteAndVerify(options, data.matches); EXPECT_GT(num_inliers, 0.8 * kNumInliers); } } // namespace } // namespace retrieval } // namespace colmap colmap-3.9.1/src/colmap/scene/000077500000000000000000000000001454702036400161435ustar00rootroot00000000000000colmap-3.9.1/src/colmap/scene/CMakeLists.txt000066400000000000000000000077531454702036400207170ustar00rootroot00000000000000# 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. set(FOLDER_NAME "scene") COLMAP_ADD_LIBRARY( NAME colmap_scene SRCS camera.h camera.cc camera_rig.h camera_rig.cc correspondence_graph.h correspondence_graph.cc database.h database.cc database_cache.h database_cache.cc image.h image.cc point2d.h point3d.h projection.h projection.cc reconstruction.h reconstruction.cc reconstruction_manager.h reconstruction_manager.cc scene_clustering.h scene_clustering.cc synthetic.h synthetic.cc track.h track.cc two_view_geometry.h two_view_geometry.cc visibility_pyramid.h visibility_pyramid.cc PUBLIC_LINK_LIBS colmap_sensor colmap_feature colmap_geometry colmap_util Eigen3::Eigen SQLite::SQLite3 ) COLMAP_ADD_TEST( NAME camera_rig_test SRCS camera_rig_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME camera_test SRCS camera_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME correspondence_graph_test SRCS correspondence_graph_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME database_cache_test SRCS database_cache_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME database_test SRCS database_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME image_test SRCS image_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME point2d_test SRCS point2d_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME point3d_test SRCS point3d_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME projection_test SRCS projection_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME reconstruction_test SRCS reconstruction_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME reconstruction_manager_test SRCS reconstruction_manager_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME scene_clustering_test SRCS scene_clustering_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME synthetic_test SRCS synthetic_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME track_test SRCS track_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME two_view_geometry_test SRCS two_view_geometry_test.cc LINK_LIBS colmap_scene ) COLMAP_ADD_TEST( NAME visibility_pyramid_test SRCS visibility_pyramid_test.cc LINK_LIBS colmap_scene ) colmap-3.9.1/src/colmap/scene/camera.cc000066400000000000000000000130661454702036400177100ustar00rootroot00000000000000// 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. #include "colmap/scene/camera.h" #include "colmap/sensor/models.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include namespace colmap { Camera Camera::CreateFromModelId(camera_t camera_id, const CameraModelId model_id, const double focal_length, const size_t width, const size_t height) { CHECK(ExistsCameraModelWithId(model_id)); Camera camera; camera.camera_id = camera_id; camera.model_id = model_id; camera.width = width; camera.height = height; camera.params = CameraModelInitializeParams(model_id, focal_length, width, height); return camera; } Camera Camera::CreateFromModelName(camera_t camera_id, const std::string& model_name, const double focal_length, const size_t width, const size_t height) { return CreateFromModelId( camera_id, CameraModelNameToId(model_name), focal_length, width, height); } Eigen::Matrix3d Camera::CalibrationMatrix() const { Eigen::Matrix3d K = Eigen::Matrix3d::Identity(); const span idxs = FocalLengthIdxs(); if (idxs.size() == 1) { K(0, 0) = params[idxs[0]]; K(1, 1) = params[idxs[0]]; } else if (idxs.size() == 2) { K(0, 0) = params[idxs[0]]; K(1, 1) = params[idxs[1]]; } else { LOG(FATAL) << "Camera model must either have 1 or 2 focal length parameters."; } K(0, 2) = PrincipalPointX(); K(1, 2) = PrincipalPointY(); return K; } double Camera::MeanFocalLength() const { const span focal_length_idxs = FocalLengthIdxs(); double focal_length = 0; for (const auto idx : focal_length_idxs) { focal_length += params[idx]; } return focal_length / focal_length_idxs.size(); } std::string Camera::ParamsToString() const { return VectorToCSV(params); } bool Camera::SetParamsFromString(const std::string& string) { std::vector new_camera_params = CSVToVector(string); if (!CameraModelVerifyParams(model_id, new_camera_params)) { return false; } params = std::move(new_camera_params); return true; } bool Camera::IsUndistorted() const { for (const size_t idx : ExtraParamsIdxs()) { if (std::abs(params[idx]) > 1e-8) { return false; } } return true; } void Camera::Rescale(const double scale) { CHECK_GT(scale, 0.0); const double scale_x = std::round(scale * width) / static_cast(width); const double scale_y = std::round(scale * height) / static_cast(height); width = static_cast(std::round(scale * width)); height = static_cast(std::round(scale * height)); SetPrincipalPointX(scale_x * PrincipalPointX()); SetPrincipalPointY(scale_y * PrincipalPointY()); if (FocalLengthIdxs().size() == 1) { SetFocalLength((scale_x + scale_y) / 2.0 * FocalLength()); } else if (FocalLengthIdxs().size() == 2) { SetFocalLengthX(scale_x * FocalLengthX()); SetFocalLengthY(scale_y * FocalLengthY()); } else { LOG(FATAL) << "Camera model must either have 1 or 2 focal length parameters."; } } void Camera::Rescale(const size_t new_width, const size_t new_height) { const double scale_x = static_cast(new_width) / static_cast(width); const double scale_y = static_cast(new_height) / static_cast(height); width = new_width; height = new_height; SetPrincipalPointX(scale_x * PrincipalPointX()); SetPrincipalPointY(scale_y * PrincipalPointY()); if (FocalLengthIdxs().size() == 1) { SetFocalLength((scale_x + scale_y) / 2.0 * FocalLength()); } else if (FocalLengthIdxs().size() == 2) { SetFocalLengthX(scale_x * FocalLengthX()); SetFocalLengthY(scale_y * FocalLengthY()); } else { LOG(FATAL) << "Camera model must either have 1 or 2 focal length parameters."; } } } // namespace colmap colmap-3.9.1/src/colmap/scene/camera.h000066400000000000000000000217141454702036400175510ustar00rootroot00000000000000// 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. #pragma once #include "colmap/sensor/models.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include namespace colmap { // Camera class that holds the intrinsic parameters. Cameras may be shared // between multiple images, e.g., if the same "physical" camera took multiple // pictures with the exact same lens and intrinsics (focal length, etc.). // This class has a specific distortion model defined by a camera model class. struct Camera { // The unique identifier of the camera. camera_t camera_id = kInvalidCameraId; // The identifier of the camera model. CameraModelId model_id = CameraModelId::kInvalid; // The dimensions of the image, 0 if not initialized. size_t width = 0; size_t height = 0; // The focal length, principal point, and extra parameters. If the camera // model is not specified, this vector is empty. std::vector params; // Whether there is a safe prior for the focal length, // e.g. manually provided or extracted from EXIF bool has_prior_focal_length = false; // Initialize parameters for given camera model and focal length, and set // the principal point to be the image center. static Camera CreateFromModelId(camera_t camera_id, CameraModelId model_id, double focal_length, size_t width, size_t height); static Camera CreateFromModelName(camera_t camera_id, const std::string& model_name, double focal_length, size_t width, size_t height); inline const std::string& ModelName() const; // Access focal length parameters. double MeanFocalLength() const; inline double FocalLength() const; inline double FocalLengthX() const; inline double FocalLengthY() const; inline void SetFocalLength(double f); inline void SetFocalLengthX(double fx); inline void SetFocalLengthY(double fy); // Access principal point parameters. Only works if there are two // principal point parameters. inline double PrincipalPointX() const; inline double PrincipalPointY() const; inline void SetPrincipalPointX(double cx); inline void SetPrincipalPointY(double cy); // Get the indices of the parameter groups in the parameter vector. inline span FocalLengthIdxs() const; inline span PrincipalPointIdxs() const; inline span ExtraParamsIdxs() const; // Get intrinsic calibration matrix composed from focal length and principal // point parameters, excluding distortion parameters. Eigen::Matrix3d CalibrationMatrix() const; // Get human-readable information about the parameter vector ordering. inline const std::string& ParamsInfo() const; // Concatenate parameters as comma-separated list. std::string ParamsToString() const; // Set camera parameters from comma-separated list. bool SetParamsFromString(const std::string& string); // Check whether parameters are valid, i.e. the parameter vector has // the correct dimensions that match the specified camera model. inline bool VerifyParams() const; // Check whether camera is already undistorted. bool IsUndistorted() const; // Check whether camera has bogus parameters. inline bool HasBogusParams(double min_focal_length_ratio, double max_focal_length_ratio, double max_extra_param) const; // Project point in image plane to world / infinity. inline Eigen::Vector2d CamFromImg(const Eigen::Vector2d& image_point) const; // Convert pixel threshold in image plane to camera frame. inline double CamFromImgThreshold(double threshold) const; // Project point from camera frame to image plane. inline Eigen::Vector2d ImgFromCam(const Eigen::Vector2d& cam_point) const; // Rescale camera dimensions and accordingly the focal length and // and the principal point. void Rescale(double scale); void Rescale(size_t new_width, size_t new_height); }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// const std::string& Camera::ModelName() const { return CameraModelIdToName(model_id); } double Camera::FocalLength() const { const span idxs = FocalLengthIdxs(); DCHECK_EQ(idxs.size(), 1); return params[idxs[0]]; } double Camera::FocalLengthX() const { const span idxs = FocalLengthIdxs(); return params[idxs[0]]; } double Camera::FocalLengthY() const { const span idxs = FocalLengthIdxs(); return params[idxs[(idxs.size() == 1) ? 0 : 1]]; } void Camera::SetFocalLength(const double f) { const span idxs = FocalLengthIdxs(); for (const size_t idx : idxs) { params[idx] = f; } } void Camera::SetFocalLengthX(const double fx) { const span idxs = FocalLengthIdxs(); DCHECK_EQ(idxs.size(), 2); params[idxs[0]] = fx; } void Camera::SetFocalLengthY(const double fy) { const span idxs = FocalLengthIdxs(); DCHECK_EQ(idxs.size(), 2); params[idxs[1]] = fy; } double Camera::PrincipalPointX() const { const span idxs = PrincipalPointIdxs(); DCHECK_EQ(idxs.size(), 2); return params[idxs[0]]; } double Camera::PrincipalPointY() const { const span idxs = PrincipalPointIdxs(); DCHECK_EQ(idxs.size(), 2); return params[idxs[1]]; } void Camera::SetPrincipalPointX(const double cx) { const span idxs = PrincipalPointIdxs(); DCHECK_EQ(idxs.size(), 2); params[idxs[0]] = cx; } void Camera::SetPrincipalPointY(const double cy) { const span idxs = PrincipalPointIdxs(); DCHECK_EQ(idxs.size(), 2); params[idxs[1]] = cy; } const std::string& Camera::ParamsInfo() const { return CameraModelParamsInfo(model_id); } span Camera::FocalLengthIdxs() const { return CameraModelFocalLengthIdxs(model_id); } span Camera::PrincipalPointIdxs() const { return CameraModelPrincipalPointIdxs(model_id); } span Camera::ExtraParamsIdxs() const { return CameraModelExtraParamsIdxs(model_id); } bool Camera::VerifyParams() const { return CameraModelVerifyParams(model_id, params); } bool Camera::HasBogusParams(const double min_focal_length_ratio, const double max_focal_length_ratio, const double max_extra_param) const { return CameraModelHasBogusParams(model_id, params, width, height, min_focal_length_ratio, max_focal_length_ratio, max_extra_param); } Eigen::Vector2d Camera::CamFromImg(const Eigen::Vector2d& image_point) const { return CameraModelCamFromImg(model_id, params, image_point).hnormalized(); } double Camera::CamFromImgThreshold(const double threshold) const { return CameraModelCamFromImgThreshold(model_id, params, threshold); } Eigen::Vector2d Camera::ImgFromCam(const Eigen::Vector2d& cam_point) const { return CameraModelImgFromCam(model_id, params, cam_point.homogeneous()); } } // namespace colmap colmap-3.9.1/src/colmap/scene/camera_rig.cc000066400000000000000000000210471454702036400205470ustar00rootroot00000000000000// 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. #include "colmap/scene/camera_rig.h" #include "colmap/util/misc.h" namespace colmap { CameraRig::CameraRig() {} size_t CameraRig::NumCameras() const { return cams_from_rigs_.size(); } size_t CameraRig::NumSnapshots() const { return snapshots_.size(); } bool CameraRig::HasCamera(const camera_t camera_id) const { return cams_from_rigs_.count(camera_id); } camera_t CameraRig::RefCameraId() const { return ref_camera_id_; } void CameraRig::SetRefCameraId(const camera_t camera_id) { CHECK(HasCamera(camera_id)); ref_camera_id_ = camera_id; } std::vector CameraRig::GetCameraIds() const { std::vector rig_camera_ids; rig_camera_ids.reserve(cams_from_rigs_.size()); for (const auto& rig_camera : cams_from_rigs_) { rig_camera_ids.push_back(rig_camera.first); } return rig_camera_ids; } const std::vector>& CameraRig::Snapshots() const { return snapshots_; } void CameraRig::AddCamera(const camera_t camera_id, const Rigid3d& cam_from_rig) { CHECK(!HasCamera(camera_id)); CHECK_EQ(NumSnapshots(), 0); cams_from_rigs_.emplace(camera_id, cam_from_rig); } void CameraRig::AddSnapshot(const std::vector& image_ids) { CHECK(!image_ids.empty()); CHECK_LE(image_ids.size(), NumCameras()); CHECK(!VectorContainsDuplicateValues(image_ids)); snapshots_.push_back(image_ids); } void CameraRig::Check(const Reconstruction& reconstruction) const { CHECK(HasCamera(ref_camera_id_)); for (const auto& rig_camera : cams_from_rigs_) { CHECK(reconstruction.ExistsCamera(rig_camera.first)); } std::unordered_set all_image_ids; for (const auto& snapshot : snapshots_) { CHECK(!snapshot.empty()); CHECK_LE(snapshot.size(), NumCameras()); bool has_ref_camera = false; for (const auto image_id : snapshot) { CHECK(reconstruction.ExistsImage(image_id)); CHECK_EQ(all_image_ids.count(image_id), 0); all_image_ids.insert(image_id); const auto& image = reconstruction.Image(image_id); CHECK(HasCamera(image.CameraId())); if (image.CameraId() == ref_camera_id_) { has_ref_camera = true; } } CHECK(has_ref_camera); } } const Rigid3d& CameraRig::CamFromRig(const camera_t camera_id) const { return cams_from_rigs_.at(camera_id); } Rigid3d& CameraRig::CamFromRig(const camera_t camera_id) { return cams_from_rigs_.at(camera_id); } double CameraRig::ComputeRigFromWorldScale( const Reconstruction& reconstruction) const { CHECK_GT(NumSnapshots(), 0); const size_t num_cameras = NumCameras(); CHECK_GT(num_cameras, 0); double rig_from_world_scale = 0; size_t num_dists = 0; std::vector proj_centers_in_rig(num_cameras); std::vector proj_centers_in_world(num_cameras); for (const auto& snapshot : snapshots_) { for (size_t i = 0; i < num_cameras; ++i) { const auto& image = reconstruction.Image(snapshot[i]); proj_centers_in_rig[i] = Inverse(CamFromRig(image.CameraId())).translation; proj_centers_in_world[i] = image.ProjectionCenter(); } // Accumulate the relative scale for all pairs of camera distances. for (size_t i = 0; i < num_cameras; ++i) { for (size_t j = 0; j < i; ++j) { const double rig_dist = (proj_centers_in_rig[i] - proj_centers_in_rig[j]).norm(); const double world_dist = (proj_centers_in_world[i] - proj_centers_in_world[j]).norm(); const double kMinDist = 1e-6; if (rig_dist > kMinDist && world_dist > kMinDist) { rig_from_world_scale += rig_dist / world_dist; num_dists += 1; } } } } if (num_dists == 0) { return std::numeric_limits::quiet_NaN(); } return rig_from_world_scale / num_dists; } bool CameraRig::ComputeCamsFromRigs(const Reconstruction& reconstruction) { CHECK_GT(NumSnapshots(), 0); CHECK_NE(ref_camera_id_, kInvalidCameraId); for (auto& cam_from_rig : cams_from_rigs_) { cam_from_rig.second.translation = Eigen::Vector3d::Zero(); } std::unordered_map> cam_from_ref_cam_rotations; for (const auto& snapshot : snapshots_) { // Find the image of the reference camera in the current snapshot. const Image* ref_image = nullptr; for (const auto image_id : snapshot) { const auto& image = reconstruction.Image(image_id); if (image.CameraId() == ref_camera_id_) { ref_image = ℑ break; } } const Rigid3d world_from_ref_cam = Inverse(CHECK_NOTNULL(ref_image)->CamFromWorld()); // Compute the relative poses from all cameras in the current snapshot to // the reference camera. for (const auto image_id : snapshot) { const auto& image = reconstruction.Image(image_id); if (image.CameraId() != ref_camera_id_) { const Rigid3d cam_from_ref_cam = image.CamFromWorld() * world_from_ref_cam; cam_from_ref_cam_rotations[image.CameraId()].push_back( cam_from_ref_cam.rotation); CamFromRig(image.CameraId()).translation += cam_from_ref_cam.translation; } } } cams_from_rigs_.at(ref_camera_id_) = Rigid3d(); // Compute the average relative poses. for (auto& cam_from_rig : cams_from_rigs_) { if (cam_from_rig.first != ref_camera_id_) { if (cam_from_ref_cam_rotations.count(cam_from_rig.first) == 0) { LOG(INFO) << "Need at least one snapshot with an image of camera " << cam_from_rig.first << " and the reference camera " << ref_camera_id_ << " to compute its relative pose in the camera rig"; return false; } const std::vector& cam_from_rig_rotations = cam_from_ref_cam_rotations.at(cam_from_rig.first); const std::vector weights(cam_from_rig_rotations.size(), 1.0); cam_from_rig.second.rotation = AverageQuaternions(cam_from_rig_rotations, weights); cam_from_rig.second.translation /= cam_from_rig_rotations.size(); } } return true; } Rigid3d CameraRig::ComputeRigFromWorld( const size_t snapshot_idx, const Reconstruction& reconstruction) const { const auto& snapshot = snapshots_.at(snapshot_idx); std::vector rig_from_world_rotations; rig_from_world_rotations.reserve(snapshot.size()); Eigen::Vector3d rig_from_world_translations = Eigen::Vector3d::Zero(); for (const auto image_id : snapshot) { const auto& image = reconstruction.Image(image_id); const Rigid3d rig_from_world = Inverse(CamFromRig(image.CameraId())) * image.CamFromWorld(); rig_from_world_rotations.push_back(rig_from_world.rotation); rig_from_world_translations += rig_from_world.translation; } const std::vector rotation_weights(snapshot.size(), 1); return Rigid3d(AverageQuaternions(rig_from_world_rotations, rotation_weights), rig_from_world_translations /= snapshot.size()); } } // namespace colmap colmap-3.9.1/src/colmap/scene/camera_rig.h000066400000000000000000000114011454702036400204020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/pose.h" #include "colmap/scene/camera.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/types.h" #include #include namespace colmap { // This class holds information about the relative configuration of camera rigs. // Camera rigs are composed of multiple cameras with a rigid relative extrinsic // configuration over multiple snapshots. Snapshots are defined as the // collection of images captured simultaneously by all cameras in the rig. class CameraRig { public: CameraRig(); // The number of cameras in the rig. size_t NumCameras() const; // The number of snapshots captured by this rig. size_t NumSnapshots() const; // Check whether the given camera is part of the rig. bool HasCamera(camera_t camera_id) const; // Access the reference camera. camera_t RefCameraId() const; void SetRefCameraId(camera_t camera_id); // Get the identifiers of the cameras in the rig. std::vector GetCameraIds() const; // Get the snapshots of the camera rig. const std::vector>& Snapshots() const; // Add a new camera to the rig. The relative pose may contain invalid values // and can then be computed automatically from a given reconstruction using // the method `ComputeCamsFromRigs`. void AddCamera(camera_t camera_id, const Rigid3d& cam_from_rig); // Add the images of a single snapshot to rig. A snapshot consists of the // captured images of all cameras of the rig. All images of a snapshot share // the same global camera rig pose, i.e. all images in the camera rig are // captured simultaneously. void AddSnapshot(const std::vector& image_ids); // Check whether the camera rig setup is valid. void Check(const Reconstruction& reconstruction) const; // Get the relative poses of the cameras in the rig. const Rigid3d& CamFromRig(camera_t camera_id) const; Rigid3d& CamFromRig(camera_t camera_id); // Compute the scaling factor from the world scale of the reconstruction to // the camera rig scale by averaging over the distances between the projection // centers. Note that this assumes that there is at least one camera pair in // the rig with non-zero baseline, otherwise the function returns NaN. double ComputeRigFromWorldScale(const Reconstruction& reconstruction) const; // Compute the camera rig poses from the reconstruction by averaging // the relative poses over all snapshots. The pose of the reference camera // will have the identity transformation. This assumes that the camera rig has // snapshots that are registered in the reconstruction. bool ComputeCamsFromRigs(const Reconstruction& reconstruction); // Compute the pose of the camera rig. The rig pose is computed as the average // of all relative camera poses in the rig and their corresponding image poses // in the reconstruction. Rigid3d ComputeRigFromWorld(size_t snapshot_idx, const Reconstruction& reconstruction) const; private: camera_t ref_camera_id_ = kInvalidCameraId; std::unordered_map cams_from_rigs_; std::vector> snapshots_; }; } // namespace colmap colmap-3.9.1/src/colmap/scene/camera_rig_test.cc000066400000000000000000000261511454702036400216070ustar00rootroot00000000000000// 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. #include "colmap/scene/camera_rig.h" #include namespace colmap { namespace { TEST(CameraRig, Empty) { CameraRig camera_rig; EXPECT_EQ(camera_rig.NumCameras(), 0); EXPECT_EQ(camera_rig.NumSnapshots(), 0); EXPECT_EQ(camera_rig.GetCameraIds().size(), 0); EXPECT_FALSE(camera_rig.HasCamera(0)); } TEST(CameraRig, AddCamera) { CameraRig camera_rig; EXPECT_EQ(camera_rig.NumCameras(), 0); EXPECT_EQ(camera_rig.NumSnapshots(), 0); EXPECT_EQ(camera_rig.GetCameraIds().size(), 0); EXPECT_FALSE(camera_rig.HasCamera(0)); const Rigid3d cam_from_rig1(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 1, 2)); camera_rig.AddCamera(0, cam_from_rig1); EXPECT_EQ(camera_rig.NumCameras(), 1); EXPECT_EQ(camera_rig.NumSnapshots(), 0); EXPECT_EQ(camera_rig.GetCameraIds().size(), 1); EXPECT_EQ(camera_rig.GetCameraIds()[0], 0); EXPECT_TRUE(camera_rig.HasCamera(0)); EXPECT_EQ(camera_rig.CamFromRig(0).rotation.coeffs(), cam_from_rig1.rotation.coeffs()); EXPECT_EQ(camera_rig.CamFromRig(0).translation, cam_from_rig1.translation); const Rigid3d cam_from_rig2(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 1, 2)); camera_rig.AddCamera(1, cam_from_rig2); EXPECT_EQ(camera_rig.NumCameras(), 2); EXPECT_EQ(camera_rig.NumSnapshots(), 0); EXPECT_EQ(camera_rig.GetCameraIds().size(), 2); EXPECT_TRUE(camera_rig.HasCamera(0)); EXPECT_TRUE(camera_rig.HasCamera(1)); EXPECT_EQ(camera_rig.CamFromRig(1).rotation.coeffs(), cam_from_rig2.rotation.coeffs()); EXPECT_EQ(camera_rig.CamFromRig(1).translation, cam_from_rig2.translation); } TEST(CameraRig, AddSnapshot) { CameraRig camera_rig; EXPECT_EQ(camera_rig.NumCameras(), 0); EXPECT_EQ(camera_rig.NumSnapshots(), 0); EXPECT_EQ(camera_rig.GetCameraIds().size(), 0); EXPECT_EQ(camera_rig.Snapshots().size(), 0); camera_rig.AddCamera( 0, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 1, 2))); camera_rig.AddCamera( 1, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(3, 4, 5))); EXPECT_EQ(camera_rig.NumCameras(), 2); EXPECT_EQ(camera_rig.NumSnapshots(), 0); EXPECT_EQ(camera_rig.Snapshots().size(), 0); const std::vector image_ids1 = {0, 1}; camera_rig.AddSnapshot(image_ids1); EXPECT_EQ(camera_rig.NumCameras(), 2); EXPECT_EQ(camera_rig.NumSnapshots(), 1); EXPECT_EQ(camera_rig.Snapshots().size(), 1); EXPECT_EQ(camera_rig.Snapshots()[0].size(), 2); EXPECT_EQ(camera_rig.Snapshots()[0][0], 0); EXPECT_EQ(camera_rig.Snapshots()[0][1], 1); const std::vector image_ids2 = {2, 3}; camera_rig.AddSnapshot(image_ids2); EXPECT_EQ(camera_rig.NumCameras(), 2); EXPECT_EQ(camera_rig.NumSnapshots(), 2); EXPECT_EQ(camera_rig.Snapshots().size(), 2); EXPECT_EQ(camera_rig.Snapshots()[0].size(), 2); EXPECT_EQ(camera_rig.Snapshots()[0][0], 0); EXPECT_EQ(camera_rig.Snapshots()[0][1], 1); EXPECT_EQ(camera_rig.Snapshots()[1].size(), 2); EXPECT_EQ(camera_rig.Snapshots()[1][0], 2); EXPECT_EQ(camera_rig.Snapshots()[1][1], 3); } TEST(CameraRig, Check) { CameraRig camera_rig; camera_rig.AddCamera( 0, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 1, 2))); camera_rig.AddCamera( 1, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(3, 4, 5))); const std::vector image_ids1 = {0, 1}; camera_rig.AddSnapshot(image_ids1); const std::vector image_ids2 = {2, 3}; camera_rig.AddSnapshot(image_ids2); Reconstruction reconstruction; Camera camera1 = Camera::CreateFromModelName(0, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera1); Camera camera2 = Camera::CreateFromModelName(1, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera2); Image image1; image1.SetImageId(0); image1.SetCameraId(camera1.camera_id); reconstruction.AddImage(image1); Image image2; image2.SetImageId(1); image2.SetCameraId(camera2.camera_id); reconstruction.AddImage(image2); Image image3; image3.SetImageId(2); image3.SetCameraId(camera1.camera_id); reconstruction.AddImage(image3); Image image4; image4.SetImageId(3); image4.SetCameraId(camera2.camera_id); reconstruction.AddImage(image4); camera_rig.SetRefCameraId(0); camera_rig.Check(reconstruction); } TEST(CameraRig, ComputeRigFromWorldScale) { CameraRig camera_rig; camera_rig.AddCamera( 0, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 0, 0))); camera_rig.AddCamera( 1, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(2, 4, 6))); const std::vector image_ids1 = {0, 1}; camera_rig.AddSnapshot(image_ids1); Reconstruction reconstruction; Camera camera1 = Camera::CreateFromModelName(0, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera1); Camera camera2 = Camera::CreateFromModelName(1, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera2); Image image1; image1.SetImageId(0); image1.SetCameraId(camera1.camera_id); reconstruction.AddImage(image1); Image image2; image2.SetImageId(1); image2.SetCameraId(camera2.camera_id); image2.CamFromWorld().translation = Eigen::Vector3d(1, 2, 3); reconstruction.AddImage(image2); camera_rig.SetRefCameraId(0); camera_rig.Check(reconstruction); EXPECT_EQ(camera_rig.ComputeRigFromWorldScale(reconstruction), 2.0); reconstruction.Image(1).CamFromWorld().translation = Eigen::Vector3d(0, 0, 0); EXPECT_TRUE(std::isnan(camera_rig.ComputeRigFromWorldScale(reconstruction))); } TEST(CameraRig, ComputeCamsFromRigs) { CameraRig camera_rig; camera_rig.AddCamera( 0, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 0, 0))); camera_rig.AddCamera( 1, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 0, 0))); const std::vector image_ids1 = {0, 1}; camera_rig.AddSnapshot(image_ids1); Reconstruction reconstruction; Camera camera1 = Camera::CreateFromModelName(0, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera1); Camera camera2 = Camera::CreateFromModelName(1, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera2); Image image1; image1.SetImageId(0); image1.SetCameraId(camera1.camera_id); reconstruction.AddImage(image1); Image image2; image2.SetImageId(1); image2.SetCameraId(camera2.camera_id); image2.CamFromWorld().translation = Eigen::Vector3d(1, 2, 3); reconstruction.AddImage(image2); camera_rig.SetRefCameraId(0); camera_rig.Check(reconstruction); camera_rig.ComputeCamsFromRigs(reconstruction); EXPECT_EQ(camera_rig.CamFromRig(0).rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(camera_rig.CamFromRig(0).translation, Eigen::Vector3d(0, 0, 0)); EXPECT_EQ(camera_rig.CamFromRig(1).rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(camera_rig.CamFromRig(1).translation, Eigen::Vector3d(1, 2, 3)); const std::vector image_ids2 = {2, 3}; camera_rig.AddSnapshot(image_ids2); Image image3; image3.SetImageId(2); image3.SetCameraId(camera1.camera_id); reconstruction.AddImage(image3); Image image4; image4.SetImageId(3); image4.SetCameraId(camera2.camera_id); image4.CamFromWorld().translation = Eigen::Vector3d(2, 4, 6); reconstruction.AddImage(image4); camera_rig.Check(reconstruction); camera_rig.ComputeCamsFromRigs(reconstruction); EXPECT_EQ(camera_rig.CamFromRig(0).rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(camera_rig.CamFromRig(0).translation, Eigen::Vector3d(0, 0, 0)); EXPECT_EQ(camera_rig.CamFromRig(1).rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(camera_rig.CamFromRig(1).translation, Eigen::Vector3d(1.5, 3, 4.5)); const std::vector image_ids3 = {4}; camera_rig.AddSnapshot(image_ids3); Image image5; image5.SetImageId(4); image5.SetCameraId(camera1.camera_id); reconstruction.AddImage(image5); camera_rig.Check(reconstruction); camera_rig.ComputeCamsFromRigs(reconstruction); EXPECT_EQ(camera_rig.CamFromRig(0).rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(camera_rig.CamFromRig(0).translation, Eigen::Vector3d(0, 0, 0)); EXPECT_EQ(camera_rig.CamFromRig(1).rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(camera_rig.CamFromRig(1).translation, Eigen::Vector3d(1.5, 3, 4.5)); } TEST(CameraRig, ComputeRigFromWorld) { CameraRig camera_rig; camera_rig.AddCamera( 0, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 1, 2))); camera_rig.AddCamera( 1, Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(3, 4, 5))); const std::vector image_ids1 = {0, 1}; camera_rig.AddSnapshot(image_ids1); Reconstruction reconstruction; Camera camera1 = Camera::CreateFromModelName(0, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera1); Camera camera2 = Camera::CreateFromModelName(1, "PINHOLE", 1, 1, 1); reconstruction.AddCamera(camera2); Image image1; image1.SetImageId(0); image1.SetCameraId(camera1.camera_id); reconstruction.AddImage(image1); Image image2; image2.SetImageId(1); image2.SetCameraId(camera2.camera_id); image2.CamFromWorld().translation = Eigen::Vector3d(3, 3, 3); reconstruction.AddImage(image2); camera_rig.SetRefCameraId(0); camera_rig.Check(reconstruction); const Rigid3d rig_from_world = camera_rig.ComputeRigFromWorld(0, reconstruction); EXPECT_EQ(rig_from_world.rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(rig_from_world.translation, Eigen::Vector3d(0, -1, -2)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/camera_test.cc000066400000000000000000000310001454702036400207330ustar00rootroot00000000000000// 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. #include "colmap/scene/camera.h" #include "colmap/sensor/models.h" #include namespace colmap { namespace { TEST(Camera, Empty) { Camera camera; EXPECT_EQ(camera.camera_id, kInvalidCameraId); EXPECT_EQ(camera.model_id, CameraModelId::kInvalid); EXPECT_EQ(camera.ModelName(), ""); EXPECT_EQ(camera.width, 0); EXPECT_EQ(camera.height, 0); EXPECT_FALSE(camera.has_prior_focal_length); EXPECT_THROW(camera.FocalLengthIdxs(), std::domain_error); EXPECT_THROW(camera.ParamsInfo(), std::domain_error); EXPECT_EQ(camera.ParamsToString(), ""); EXPECT_EQ(camera.params.size(), 0); EXPECT_EQ(camera.params.size(), 0); EXPECT_EQ(camera.params.data(), camera.params.data()); } TEST(Camera, CameraId) { Camera camera; EXPECT_EQ(camera.camera_id, kInvalidCameraId); camera.camera_id = 1; EXPECT_EQ(camera.camera_id, 1); } TEST(Camera, FocalLength) { Camera camera = Camera::CreateFromModelId( 1, SimplePinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_EQ(camera.FocalLength(), 1.0); EXPECT_EQ(camera.FocalLengthX(), 1.0); EXPECT_EQ(camera.FocalLengthY(), 1.0); camera.SetFocalLength(2.0); EXPECT_EQ(camera.FocalLength(), 2.0); EXPECT_EQ(camera.FocalLengthX(), 2.0); EXPECT_EQ(camera.FocalLengthY(), 2.0); camera = Camera::CreateFromModelId(1, PinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_EQ(camera.FocalLengthX(), 1.0); EXPECT_EQ(camera.FocalLengthY(), 1.0); camera.SetFocalLengthX(2.0); EXPECT_EQ(camera.FocalLengthX(), 2.0); EXPECT_EQ(camera.FocalLengthY(), 1.0); camera.SetFocalLengthY(2.0); EXPECT_EQ(camera.FocalLengthX(), 2.0); EXPECT_EQ(camera.FocalLengthY(), 2.0); } TEST(Camera, PrincipalPoint) { Camera camera = Camera::CreateFromModelId(1, PinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_EQ(camera.PrincipalPointX(), 0.5); EXPECT_EQ(camera.PrincipalPointY(), 0.5); camera.SetPrincipalPointX(2.0); EXPECT_EQ(camera.PrincipalPointX(), 2.0); EXPECT_EQ(camera.PrincipalPointY(), 0.5); camera.SetPrincipalPointY(2.0); EXPECT_EQ(camera.PrincipalPointX(), 2.0); EXPECT_EQ(camera.PrincipalPointY(), 2.0); } TEST(Camera, ParamIdxs) { Camera camera; EXPECT_THROW(camera.FocalLengthIdxs(), std::domain_error); EXPECT_THROW(camera.PrincipalPointIdxs(), std::domain_error); EXPECT_THROW(camera.ExtraParamsIdxs(), std::domain_error); camera.model_id = FullOpenCVCameraModel::model_id; EXPECT_EQ(camera.FocalLengthIdxs().size(), 2); EXPECT_EQ(camera.PrincipalPointIdxs().size(), 2); EXPECT_EQ(camera.ExtraParamsIdxs().size(), 8); } TEST(Camera, CalibrationMatrix) { Camera camera = Camera::CreateFromModelId(1, PinholeCameraModel::model_id, 1.0, 1, 1); const Eigen::Matrix3d K = camera.CalibrationMatrix(); Eigen::Matrix3d K_ref; K_ref << 1, 0, 0.5, 0, 1, 0.5, 0, 0, 1; EXPECT_EQ(K, K_ref); } TEST(Camera, ParamsInfo) { Camera camera; EXPECT_THROW(camera.ParamsInfo(), std::domain_error); camera.model_id = SimpleRadialCameraModel::model_id; EXPECT_EQ(camera.ParamsInfo(), "f, cx, cy, k"); } TEST(Camera, ParamsToString) { Camera camera = Camera::CreateFromModelId( 1, SimplePinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_EQ(camera.ParamsToString(), "1.000000, 0.500000, 0.500000"); } TEST(Camera, ParamsFromString) { Camera camera; camera.model_id = SimplePinholeCameraModel::model_id; EXPECT_TRUE(camera.SetParamsFromString("1.000000, 0.500000, 0.500000")); const std::vector params{1.0, 0.5, 0.5}; EXPECT_EQ(camera.params, params); EXPECT_FALSE(camera.SetParamsFromString("1.000000, 0.500000")); EXPECT_EQ(camera.params, params); } TEST(Camera, VerifyParams) { Camera camera; EXPECT_THROW(camera.VerifyParams(), std::domain_error); camera = Camera::CreateFromModelId( 1, SimplePinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_TRUE(camera.VerifyParams()); camera.params.resize(2); EXPECT_FALSE(camera.VerifyParams()); } TEST(Camera, IsUndistorted) { Camera camera = Camera::CreateFromModelId( 1, SimplePinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_TRUE(camera.IsUndistorted()); camera = Camera::CreateFromModelId( 1, SimpleRadialCameraModel::model_id, 1.0, 1, 1); EXPECT_TRUE(camera.IsUndistorted()); camera.params = {1.0, 0.5, 0.5, 0.005}; EXPECT_FALSE(camera.IsUndistorted()); camera = Camera::CreateFromModelId(1, RadialCameraModel::model_id, 1.0, 1, 1); EXPECT_TRUE(camera.IsUndistorted()); camera.params = {1.0, 0.5, 0.5, 0.0, 0.005}; EXPECT_FALSE(camera.IsUndistorted()); camera = Camera::CreateFromModelId(1, OpenCVCameraModel::model_id, 1.0, 1, 1); EXPECT_TRUE(camera.IsUndistorted()); camera.params = {1.0, 1.0, 0.5, 0.5, 0.0, 0.0, 0.0, 0.001}; EXPECT_FALSE(camera.IsUndistorted()); camera = Camera::CreateFromModelId(1, FullOpenCVCameraModel::model_id, 1.0, 1, 1); EXPECT_TRUE(camera.IsUndistorted()); camera.params = { 1.0, 1.0, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001}; EXPECT_FALSE(camera.IsUndistorted()); } TEST(Camera, HasBogusParams) { Camera camera; EXPECT_THROW(camera.HasBogusParams(0.0, 0.0, 0.0), std::domain_error); camera = Camera::CreateFromModelId( 1, SimplePinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_FALSE(camera.HasBogusParams(0.1, 1.1, 1.0)); EXPECT_FALSE(camera.HasBogusParams(0.1, 1.1, 0.0)); EXPECT_TRUE(camera.HasBogusParams(0.1, 0.99, 1.0)); EXPECT_TRUE(camera.HasBogusParams(1.01, 1.1, 1.0)); camera = Camera::CreateFromModelId( 1, SimpleRadialCameraModel::model_id, 1.0, 1, 1); EXPECT_FALSE(camera.HasBogusParams(0.1, 1.1, 1.0)); camera.params[3] = 1.01; EXPECT_TRUE(camera.HasBogusParams(0.1, 1.1, 1.0)); camera.params[3] = -0.5; EXPECT_FALSE(camera.HasBogusParams(0.1, 1.1, 1.0)); camera.params[3] = -1.01; EXPECT_TRUE(camera.HasBogusParams(0.1, 1.1, 1.0)); } TEST(Camera, CreateFromModelId) { Camera camera = Camera::CreateFromModelId( 1, SimplePinholeCameraModel::model_id, 1.0, 1, 1); EXPECT_EQ(camera.camera_id, 1); EXPECT_EQ(camera.model_id, SimplePinholeCameraModel::model_id); EXPECT_EQ(camera.ModelName(), "SIMPLE_PINHOLE"); EXPECT_EQ(camera.width, 1); EXPECT_EQ(camera.height, 1); EXPECT_FALSE(camera.has_prior_focal_length); EXPECT_EQ(camera.FocalLengthIdxs().size(), 1); EXPECT_EQ(camera.PrincipalPointIdxs().size(), 2); EXPECT_EQ(camera.ExtraParamsIdxs().size(), 0); EXPECT_EQ(camera.ParamsInfo(), "f, cx, cy"); EXPECT_EQ(camera.ParamsToString(), "1.000000, 0.500000, 0.500000"); EXPECT_EQ(camera.FocalLength(), 1.0); EXPECT_EQ(camera.PrincipalPointX(), 0.5); EXPECT_EQ(camera.PrincipalPointY(), 0.5); EXPECT_TRUE(camera.VerifyParams()); EXPECT_FALSE(camera.HasBogusParams(0.1, 2.0, 1.0)); EXPECT_TRUE(camera.HasBogusParams(0.1, 0.5, 1.0)); EXPECT_EQ(camera.params.size(), static_cast(SimplePinholeCameraModel::num_params)); EXPECT_EQ(camera.params.size(), static_cast(SimplePinholeCameraModel::num_params)); } TEST(Camera, CreateFromModelName) { Camera camera = Camera::CreateFromModelName(1, "SIMPLE_PINHOLE", 1.0, 1, 1); EXPECT_EQ(camera.camera_id, 1); EXPECT_EQ(camera.model_id, SimplePinholeCameraModel::model_id); EXPECT_EQ(camera.ModelName(), "SIMPLE_PINHOLE"); EXPECT_EQ(camera.width, 1); EXPECT_EQ(camera.height, 1); EXPECT_FALSE(camera.has_prior_focal_length); EXPECT_EQ(camera.FocalLengthIdxs().size(), 1); EXPECT_EQ(camera.PrincipalPointIdxs().size(), 2); EXPECT_EQ(camera.ExtraParamsIdxs().size(), 0); EXPECT_EQ(camera.ParamsInfo(), "f, cx, cy"); EXPECT_EQ(camera.ParamsToString(), "1.000000, 0.500000, 0.500000"); EXPECT_EQ(camera.FocalLength(), 1.0); EXPECT_EQ(camera.PrincipalPointX(), 0.5); EXPECT_EQ(camera.PrincipalPointY(), 0.5); EXPECT_TRUE(camera.VerifyParams()); EXPECT_FALSE(camera.HasBogusParams(0.1, 2.0, 1.0)); EXPECT_TRUE(camera.HasBogusParams(0.1, 0.5, 1.0)); EXPECT_EQ(camera.params.size(), static_cast(SimplePinholeCameraModel::num_params)); EXPECT_EQ(camera.params.size(), static_cast(SimplePinholeCameraModel::num_params)); } TEST(Camera, CamFromImg) { Camera camera; EXPECT_THROW(camera.CamFromImg(Eigen::Vector2d::Zero()), std::domain_error); camera = Camera::CreateFromModelName(1, "SIMPLE_PINHOLE", 1.0, 1, 1); EXPECT_EQ(camera.CamFromImg(Eigen::Vector2d(0.0, 0.0))(0), -0.5); EXPECT_EQ(camera.CamFromImg(Eigen::Vector2d(0.0, 0.0))(1), -0.5); EXPECT_EQ(camera.CamFromImg(Eigen::Vector2d(0.5, 0.5))(0), 0.0); EXPECT_EQ(camera.CamFromImg(Eigen::Vector2d(0.5, 0.5))(1), 0.0); } TEST(Camera, CamFromImgThreshold) { Camera camera; EXPECT_THROW(camera.CamFromImgThreshold(0), std::domain_error); camera = Camera::CreateFromModelName(1, "SIMPLE_PINHOLE", 1.0, 1, 1); EXPECT_EQ(camera.CamFromImgThreshold(0), 0); EXPECT_EQ(camera.CamFromImgThreshold(1), 1); camera.SetFocalLength(2.0); EXPECT_EQ(camera.CamFromImgThreshold(1), 0.5); camera = Camera::CreateFromModelName(1, "PINHOLE", 1.0, 1, 1); camera.SetFocalLengthY(3.0); EXPECT_EQ(camera.CamFromImgThreshold(1), 0.5); } TEST(Camera, ImgFromCam) { Camera camera; EXPECT_THROW(camera.ImgFromCam(Eigen::Vector2d::Zero()), std::domain_error); camera = Camera::CreateFromModelName(1, "SIMPLE_PINHOLE", 1.0, 1, 1); EXPECT_EQ(camera.ImgFromCam(Eigen::Vector2d(0.0, 0.0))(0), 0.5); EXPECT_EQ(camera.ImgFromCam(Eigen::Vector2d(0.0, 0.0))(1), 0.5); EXPECT_EQ(camera.ImgFromCam(Eigen::Vector2d(-0.5, -0.5))(0), 0.0); EXPECT_EQ(camera.ImgFromCam(Eigen::Vector2d(-0.5, -0.5))(1), 0.0); } TEST(Camera, Rescale) { Camera camera = Camera::CreateFromModelName(1, "SIMPLE_PINHOLE", 1.0, 1, 1); camera.Rescale(2.0); EXPECT_EQ(camera.width, 2); EXPECT_EQ(camera.height, 2); EXPECT_EQ(camera.FocalLength(), 2); EXPECT_EQ(camera.PrincipalPointX(), 1); EXPECT_EQ(camera.PrincipalPointY(), 1); camera = Camera::CreateFromModelName(1, "PINHOLE", 1.0, 1, 1); camera.Rescale(2.0); EXPECT_EQ(camera.width, 2); EXPECT_EQ(camera.height, 2); EXPECT_EQ(camera.FocalLengthX(), 2); EXPECT_EQ(camera.FocalLengthY(), 2); EXPECT_EQ(camera.PrincipalPointX(), 1); EXPECT_EQ(camera.PrincipalPointY(), 1); camera = Camera::CreateFromModelName(1, "PINHOLE", 1.0, 2, 2); camera.Rescale(0.5); EXPECT_EQ(camera.width, 1); EXPECT_EQ(camera.height, 1); EXPECT_EQ(camera.FocalLengthX(), 0.5); EXPECT_EQ(camera.FocalLengthY(), 0.5); EXPECT_EQ(camera.PrincipalPointX(), 0.5); EXPECT_EQ(camera.PrincipalPointY(), 0.5); camera = Camera::CreateFromModelName(1, "PINHOLE", 1.0, 2, 2); camera.Rescale(1, 1); EXPECT_EQ(camera.width, 1); EXPECT_EQ(camera.height, 1); EXPECT_EQ(camera.FocalLengthX(), 0.5); EXPECT_EQ(camera.FocalLengthY(), 0.5); EXPECT_EQ(camera.PrincipalPointX(), 0.5); EXPECT_EQ(camera.PrincipalPointY(), 0.5); camera = Camera::CreateFromModelName(1, "PINHOLE", 1.0, 2, 2); camera.Rescale(4, 4); EXPECT_EQ(camera.width, 4); EXPECT_EQ(camera.height, 4); EXPECT_EQ(camera.FocalLengthX(), 2); EXPECT_EQ(camera.FocalLengthY(), 2); EXPECT_EQ(camera.PrincipalPointX(), 2); EXPECT_EQ(camera.PrincipalPointY(), 2); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/correspondence_graph.cc000066400000000000000000000263711454702036400226550ustar00rootroot00000000000000// 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. #include "colmap/scene/correspondence_graph.h" #include "colmap/geometry/pose.h" #include "colmap/util/string.h" #include #include namespace colmap { std::unordered_map CorrespondenceGraph::NumCorrespondencesBetweenImages() const { std::unordered_map num_corrs_between_images; num_corrs_between_images.reserve(image_pairs_.size()); for (const auto& image_pair : image_pairs_) { num_corrs_between_images.emplace(image_pair.first, image_pair.second.num_correspondences); } return num_corrs_between_images; } void CorrespondenceGraph::Finalize() { CHECK(!finalized_); finalized_ = true; // Flatten all correspondences, remove images without observations. for (auto it = images_.begin(); it != images_.end();) { // Count number of correspondences and observations. it->second.num_observations = 0; size_t num_total_corrs = 0; for (auto& corr : it->second.corrs) { num_total_corrs += corr.size(); if (!corr.empty()) { it->second.num_observations += 1; } } // Erase image without observations. if (num_total_corrs == 0) { images_.erase(it++); continue; } // Reshuffle correspondences into flattened vector. const point2D_t num_points2D = it->second.corrs.size(); it->second.flat_corrs.reserve(num_total_corrs); it->second.flat_corr_begs.resize(num_points2D + 1); for (point2D_t point2D_idx = 0; point2D_idx < num_points2D; ++point2D_idx) { it->second.flat_corr_begs[point2D_idx] = it->second.flat_corrs.size(); std::vector& corrs = it->second.corrs[point2D_idx]; it->second.flat_corrs.insert( it->second.flat_corrs.end(), corrs.begin(), corrs.end()); } it->second.flat_corr_begs[num_points2D] = it->second.flat_corrs.size(); // Ensure we reserved enough space before insertion. CHECK_EQ(it->second.flat_corrs.size(), num_total_corrs); // Deallocate original data. it->second.corrs.clear(); it->second.corrs.shrink_to_fit(); ++it; } } void CorrespondenceGraph::AddImage(const image_t image_id, const size_t num_points) { CHECK(!ExistsImage(image_id)); images_[image_id].corrs.resize(num_points); } void CorrespondenceGraph::AddCorrespondences(const image_t image_id1, const image_t image_id2, const FeatureMatches& matches) { // Avoid self-matches - should only happen, if user provides custom matches. if (image_id1 == image_id2) { LOG(WARNING) << "Cannot use self-matches for image_id=" << image_id1; return; } // Corresponding images. struct Image& image1 = images_.at(image_id1); struct Image& image2 = images_.at(image_id2); // Store number of correspondences for each image to find good initial pair. image1.num_correspondences += matches.size(); image2.num_correspondences += matches.size(); // Set the number of all correspondences for this image pair. Further below, // we will make sure that only unique correspondences are counted. const image_pair_t pair_id = Database::ImagePairToPairId(image_id1, image_id2); auto& image_pair = image_pairs_[pair_id]; image_pair.num_correspondences += static_cast(matches.size()); // Store all matches in correspondence graph data structure. This data- // structure uses more memory than storing the raw match matrices, but is // significantly more efficient when updating the correspondences in case an // observation is triangulated. for (const auto& match : matches) { const bool valid_idx1 = match.point2D_idx1 < image1.corrs.size(); const bool valid_idx2 = match.point2D_idx2 < image2.corrs.size(); if (valid_idx1 && valid_idx2) { auto& corrs1 = image1.corrs[match.point2D_idx1]; auto& corrs2 = image2.corrs[match.point2D_idx2]; const bool duplicate1 = std::find_if(corrs1.begin(), corrs1.end(), [image_id2](const Correspondence& corr) { return corr.image_id == image_id2; }) != corrs1.end(); const bool duplicate2 = std::find_if(corrs2.begin(), corrs2.end(), [image_id1](const Correspondence& corr) { return corr.image_id == image_id1; }) != corrs2.end(); if (duplicate1 || duplicate2) { image1.num_correspondences -= 1; image2.num_correspondences -= 1; image_pair.num_correspondences -= 1; LOG(WARNING) << StringPrintf( "Duplicate correspondence between " "point2D_idx=%d in image_id=%d and point2D_idx=%d in " "image_id=%d", match.point2D_idx1, image_id1, match.point2D_idx2, image_id2); } else { corrs1.emplace_back(image_id2, match.point2D_idx2); corrs2.emplace_back(image_id1, match.point2D_idx1); } } else { image1.num_correspondences -= 1; image2.num_correspondences -= 1; image_pair.num_correspondences -= 1; if (!valid_idx1) { LOG(WARNING) << StringPrintf( "point2D_idx=%d in image_id=%d does not exist", match.point2D_idx1, image_id1); } if (!valid_idx2) { LOG(WARNING) << StringPrintf( "point2D_idx=%d in image_id=%d does not exist", match.point2D_idx2, image_id2); } } } } CorrespondenceGraph::CorrespondenceRange CorrespondenceGraph::FindCorrespondences(const image_t image_id, const point2D_t point2D_idx) const { CHECK(finalized_); const point2D_t next_point2D_idx = point2D_idx + 1; const Image& image = images_.at(image_id); const Correspondence* beg = image.flat_corrs.data() + image.flat_corr_begs.at(point2D_idx); const Correspondence* end = image.flat_corrs.data() + image.flat_corr_begs.at(next_point2D_idx); return CorrespondenceRange{beg, end}; } void CorrespondenceGraph::ExtractCorrespondences( const image_t image_id, const point2D_t point2D_idx, std::vector* corrs) const { const auto range = FindCorrespondences(image_id, point2D_idx); corrs->clear(); corrs->reserve(range.end - range.beg); for (const Correspondence* corr = range.beg; corr < range.end; ++corr) { corrs->push_back(*corr); } } void CorrespondenceGraph::ExtractTransitiveCorrespondences( const image_t image_id, const point2D_t point2D_idx, const size_t transitivity, std::vector* corrs) const { if (transitivity == 1) { ExtractCorrespondences(image_id, point2D_idx, corrs); return; } corrs->clear(); if (!HasCorrespondences(image_id, point2D_idx)) { return; } // Push requested image point on queue to visit. Will be removed later. corrs->emplace_back(image_id, point2D_idx); std::map> image_corrs; image_corrs[image_id].insert(point2D_idx); size_t corr_queue_beg = 0; size_t corr_queue_end = 1; for (size_t t = 0; t < transitivity; ++t) { // Collect correspondences at transitive level t to all // correspondences that were collected at transitive level t - 1. for (size_t i = corr_queue_beg; i < corr_queue_end; ++i) { const Correspondence ref_corr = (*corrs)[i]; const CorrespondenceRange ref_corr_range = FindCorrespondences(ref_corr.image_id, ref_corr.point2D_idx); for (const Correspondence* corr = ref_corr_range.beg; corr < ref_corr_range.end; ++corr) { // Check if correspondence already collected, otherwise collect. auto& corr_image_corrs = image_corrs[corr->image_id]; if (corr_image_corrs.insert(corr->point2D_idx).second) { corrs->emplace_back(corr->image_id, corr->point2D_idx); } } } // Move on to the next block of correspondences at next transitive level. corr_queue_beg = corr_queue_end; corr_queue_end = corrs->size(); // No new correspondences collected in last transitivity level. if (corr_queue_beg == corr_queue_end) { break; } } // Remove first element, which is the given observation by swapping it // with the last collected correspondence. if (corrs->size() > 1) { corrs->front() = corrs->back(); } corrs->pop_back(); } FeatureMatches CorrespondenceGraph::FindCorrespondencesBetweenImages( const image_t image_id1, const image_t image_id2) const { const point2D_t num_correspondences = NumCorrespondencesBetweenImages(image_id1, image_id2); if (num_correspondences == 0) { return {}; } FeatureMatches corrs; corrs.reserve(num_correspondences); const point2D_t num_points2D1 = images_.at(image_id1).flat_corr_begs.size() - 1; for (point2D_t point2D_idx1 = 0; point2D_idx1 < num_points2D1; ++point2D_idx1) { const CorrespondenceRange range = FindCorrespondences(image_id1, point2D_idx1); for (const Correspondence* corr = range.beg; corr < range.end; ++corr) { if (corr->image_id == image_id2) { corrs.emplace_back(point2D_idx1, corr->point2D_idx); } } } return corrs; } bool CorrespondenceGraph::IsTwoViewObservation( const image_t image_id, const point2D_t point2D_idx) const { const CorrespondenceRange range = FindCorrespondences(image_id, point2D_idx); if (range.end - range.beg != 1) { return false; } const CorrespondenceRange other_range = FindCorrespondences(range.beg->image_id, range.beg->point2D_idx); return (other_range.end - other_range.beg) == 1; } } // namespace colmap colmap-3.9.1/src/colmap/scene/correspondence_graph.h000066400000000000000000000207551454702036400225170ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/database.h" #include "colmap/util/types.h" #include #include namespace colmap { // Scene graph represents the graph of image to image and feature to feature // correspondences of a dataset. It should be accessed from the DatabaseCache. class CorrespondenceGraph { public: struct Correspondence { Correspondence() : image_id(kInvalidImageId), point2D_idx(kInvalidPoint2DIdx) {} Correspondence(const image_t image_id, const point2D_t point2D_idx) : image_id(image_id), point2D_idx(point2D_idx) {} // The identifier of the corresponding image. image_t image_id; // The index of the corresponding point in the corresponding image. point2D_t point2D_idx; }; // Range of correspondences from [beg, end). Empty if beg == end. struct CorrespondenceRange { const Correspondence* beg = nullptr; const Correspondence* end = nullptr; }; CorrespondenceGraph() = default; // Number of added images. inline size_t NumImages() const; // Number of added images. inline size_t NumImagePairs() const; // Check whether image exists. inline bool ExistsImage(image_t image_id) const; // Get the number of observations in an image. An observation is an image // point that has at least one correspondence. inline point2D_t NumObservationsForImage(image_t image_id) const; // Get the number of correspondences per image. inline point2D_t NumCorrespondencesForImage(image_t image_id) const; // Get the number of correspondences between a pair of images. inline point2D_t NumCorrespondencesBetweenImages(image_t image_id1, image_t image_id2) const; // Get the number of correspondences between all images. std::unordered_map NumCorrespondencesBetweenImages() const; // Finalize the database manager. // // - Calculates the number of observations per image by counting the number // of image points that have at least one correspondence. // - Deletes images without observations, as they are useless for SfM. // - Shrinks the correspondence vectors to their size to save memory. void Finalize(); // Add new image to the correspondence graph. void AddImage(image_t image_id, size_t num_points2D); // Add correspondences between images. This function ignores invalid // correspondences where the point indices are out of bounds or duplicate // correspondences between the same image points. Whenever either of the two // cases occur this function prints a warning to the standard output. void AddCorrespondences(image_t image_id1, image_t image_id2, const FeatureMatches& matches); // Find range of correspondences of an image observation to all other images. CorrespondenceRange FindCorrespondences(image_t image_id, point2D_t point2D_idx) const; // Helper method to extract found correspondences into a vector. void ExtractCorrespondences(image_t image_id, point2D_t point2D_idx, std::vector* corrs) const; // Extract correspondences to the given observation. // // Transitively collects correspondences to the given observation by first // finding correspondences to the given observation, then looking for // correspondences to the collected correspondences in the first step, and so // forth until the transitivity is exhausted or no more correspondences are // found. The returned list does not contain duplicates and contains // the given observation. void ExtractTransitiveCorrespondences( image_t image_id, point2D_t point2D_idx, size_t transitivity, std::vector* corrs) const; // Find all correspondences between two images. FeatureMatches FindCorrespondencesBetweenImages(image_t image_id1, image_t image_id2) const; // Check whether the image point has correspondences. inline bool HasCorrespondences(image_t image_id, point2D_t point2D_idx) const; // Check whether the given observation is part of a two-view track, i.e. // it only has one correspondence and that correspondence has the given // observation as its only correspondence. bool IsTwoViewObservation(image_t image_id, point2D_t point2D_idx) const; private: struct Image { // Number of 2D points with at least one correspondence to another image. point2D_t num_observations = 0; // Total number of correspondences to other images. This measure is useful // to find a good initial pair, that is connected to many images. point2D_t num_correspondences = 0; // Correspondences to other images per image point. // Added correspondences before Finalize(). std::vector> corrs; // Flattened correspondences after Finalize(). std::vector flat_corrs; // For each point, determines the beginning of the correspondences in the // flat_corrs vector. The end of point i is determined by the beginning of // the next point. The length of this vector is num_points2D + 1, where the // last element is equivalent to the size of flat_corrs. std::vector flat_corr_begs; }; struct ImagePair { // The number of correspondences between pairs of images. point2D_t num_correspondences = 0; }; bool finalized_ = false; std::unordered_map images_; std::unordered_map image_pairs_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// size_t CorrespondenceGraph::NumImages() const { return images_.size(); } size_t CorrespondenceGraph::NumImagePairs() const { return image_pairs_.size(); } bool CorrespondenceGraph::ExistsImage(const image_t image_id) const { return images_.find(image_id) != images_.end(); } point2D_t CorrespondenceGraph::NumObservationsForImage( const image_t image_id) const { return images_.at(image_id).num_observations; } point2D_t CorrespondenceGraph::NumCorrespondencesForImage( const image_t image_id) const { return images_.at(image_id).num_correspondences; } point2D_t CorrespondenceGraph::NumCorrespondencesBetweenImages( const image_t image_id1, const image_t image_id2) const { const image_pair_t pair_id = Database::ImagePairToPairId(image_id1, image_id2); const auto it = image_pairs_.find(pair_id); if (it == image_pairs_.end()) { return 0; } else { return it->second.num_correspondences; } } bool CorrespondenceGraph::HasCorrespondences( const image_t image_id, const point2D_t point2D_idx) const { const CorrespondenceRange range = FindCorrespondences(image_id, point2D_idx); return range.beg != range.end; } } // namespace colmap colmap-3.9.1/src/colmap/scene/correspondence_graph_test.cc000066400000000000000000000274411454702036400237130ustar00rootroot00000000000000// 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. #include "colmap/scene/correspondence_graph.h" #include namespace colmap { namespace { int CountNumTransitiveCorrespondences(const CorrespondenceGraph& graph, const image_t image_id, const point2D_t point2D_idx, const size_t transitivity) { std::vector corrs; graph.ExtractTransitiveCorrespondences( image_id, point2D_idx, transitivity, &corrs); return corrs.size(); } TEST(CorrespondenceGraph, Empty) { CorrespondenceGraph correspondence_graph; EXPECT_EQ(correspondence_graph.NumImages(), 0); EXPECT_EQ(correspondence_graph.NumImagePairs(), 0); EXPECT_EQ(correspondence_graph.NumCorrespondencesBetweenImages().size(), 0); } TEST(CorrespondenceGraph, TwoView) { CorrespondenceGraph correspondence_graph; correspondence_graph.AddImage(0, 10); correspondence_graph.AddImage(1, 10); EXPECT_TRUE(correspondence_graph.ExistsImage(0)); EXPECT_TRUE(correspondence_graph.ExistsImage(1)); EXPECT_FALSE(correspondence_graph.ExistsImage(2)); EXPECT_EQ(correspondence_graph.NumImages(), 2); const FeatureMatches matches = { {0, 0}, {1, 2}, {3, 7}, {4, 8}, }; correspondence_graph.AddCorrespondences(0, 1, matches); correspondence_graph.Finalize(); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(0), 4); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(1), 4); const image_pair_t pair_id = Database::ImagePairToPairId(0, 1); EXPECT_EQ(correspondence_graph.NumCorrespondencesBetweenImages().size(), 1); EXPECT_EQ(correspondence_graph.NumCorrespondencesBetweenImages().at(pair_id), 4); std::vector corrs; correspondence_graph.ExtractCorrespondences(0, 0, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(0, 0)); EXPECT_TRUE(correspondence_graph.IsTwoViewObservation(0, 0)); EXPECT_EQ(corrs.at(0).image_id, 1); EXPECT_EQ(corrs.at(0).point2D_idx, 0); correspondence_graph.ExtractCorrespondences(1, 0, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(1, 0)); EXPECT_TRUE(correspondence_graph.IsTwoViewObservation(1, 0)); EXPECT_EQ(corrs.at(0).image_id, 0); EXPECT_EQ(corrs.at(0).point2D_idx, 0); correspondence_graph.ExtractCorrespondences(0, 1, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(0, 1)); EXPECT_TRUE(correspondence_graph.IsTwoViewObservation(0, 1)); EXPECT_EQ(corrs.at(0).image_id, 1); EXPECT_EQ(corrs.at(0).point2D_idx, 2); correspondence_graph.ExtractCorrespondences(1, 2, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(1, 2)); EXPECT_TRUE(correspondence_graph.IsTwoViewObservation(1, 2)); EXPECT_EQ(corrs.at(0).image_id, 0); EXPECT_EQ(corrs.at(0).point2D_idx, 1); correspondence_graph.ExtractCorrespondences(0, 4, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(0, 4)); EXPECT_TRUE(correspondence_graph.IsTwoViewObservation(0, 4)); EXPECT_EQ(corrs.at(0).image_id, 1); EXPECT_EQ(corrs.at(0).point2D_idx, 8); correspondence_graph.ExtractCorrespondences(0, 3, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(0, 3)); EXPECT_EQ(corrs.at(0).image_id, 1); EXPECT_EQ(corrs.at(0).point2D_idx, 7); correspondence_graph.ExtractCorrespondences(1, 7, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(1, 7)); EXPECT_TRUE(correspondence_graph.IsTwoViewObservation(1, 7)); EXPECT_EQ(corrs.at(0).image_id, 0); EXPECT_EQ(corrs.at(0).point2D_idx, 3); correspondence_graph.ExtractCorrespondences(1, 8, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_TRUE(correspondence_graph.HasCorrespondences(1, 8)); EXPECT_TRUE(correspondence_graph.IsTwoViewObservation(1, 8)); EXPECT_EQ(corrs.at(0).image_id, 0); EXPECT_EQ(corrs.at(0).point2D_idx, 4); for (size_t i = 0; i < 10; ++i) { EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 0, i, 0), 0); correspondence_graph.ExtractCorrespondences(0, i, &corrs); EXPECT_EQ(corrs.size(), CountNumTransitiveCorrespondences(correspondence_graph, 0, i, 2)); EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 1, i, 0), 0); correspondence_graph.ExtractCorrespondences(1, i, &corrs); EXPECT_EQ(corrs.size(), CountNumTransitiveCorrespondences(correspondence_graph, 1, i, 2)); } const auto corrs01 = correspondence_graph.FindCorrespondencesBetweenImages(0, 1); const auto corrs10 = correspondence_graph.FindCorrespondencesBetweenImages(1, 0); EXPECT_EQ(corrs01.size(), matches.size()); EXPECT_EQ(corrs10.size(), matches.size()); for (size_t i = 0; i < corrs01.size(); ++i) { EXPECT_EQ(corrs01[i].point2D_idx1, corrs10[i].point2D_idx2); EXPECT_EQ(corrs01[i].point2D_idx2, corrs10[i].point2D_idx1); EXPECT_EQ(matches[i].point2D_idx1, corrs01[i].point2D_idx1); EXPECT_EQ(matches[i].point2D_idx2, corrs01[i].point2D_idx2); } EXPECT_EQ(correspondence_graph.NumObservationsForImage(0), 4); EXPECT_EQ(correspondence_graph.NumObservationsForImage(1), 4); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(0), 4); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(1), 4); } TEST(CorrespondenceGraph, ThreeView) { CorrespondenceGraph correspondence_graph; correspondence_graph.AddImage(0, 10); correspondence_graph.AddImage(1, 10); correspondence_graph.AddImage(2, 10); const FeatureMatches matches01 = {{0, 0}}; correspondence_graph.AddCorrespondences(0, 1, matches01); const FeatureMatches matches02 = {{0, 0}}; correspondence_graph.AddCorrespondences(0, 2, matches02); const FeatureMatches matches12 = {{0, 0}, {5, 5}}; correspondence_graph.AddCorrespondences(1, 2, matches12); correspondence_graph.Finalize(); EXPECT_EQ(correspondence_graph.NumObservationsForImage(0), 1); EXPECT_EQ(correspondence_graph.NumObservationsForImage(1), 2); EXPECT_EQ(correspondence_graph.NumObservationsForImage(2), 2); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(0), 2); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(1), 3); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(2), 3); const image_pair_t pair_id01 = Database::ImagePairToPairId(0, 1); const image_pair_t pair_id02 = Database::ImagePairToPairId(0, 2); const image_pair_t pair_id12 = Database::ImagePairToPairId(1, 2); EXPECT_EQ(correspondence_graph.NumCorrespondencesBetweenImages().size(), 3); EXPECT_EQ( correspondence_graph.NumCorrespondencesBetweenImages().at(pair_id01), 1); EXPECT_EQ( correspondence_graph.NumCorrespondencesBetweenImages().at(pair_id02), 1); EXPECT_EQ( correspondence_graph.NumCorrespondencesBetweenImages().at(pair_id12), 2); std::vector corrs; correspondence_graph.ExtractCorrespondences(0, 0, &corrs); EXPECT_EQ(corrs.size(), 2); EXPECT_EQ(corrs.at(0).image_id, 1); EXPECT_EQ(corrs.at(0).point2D_idx, 0); EXPECT_EQ(corrs.at(1).image_id, 2); EXPECT_EQ(corrs.at(1).point2D_idx, 0); correspondence_graph.ExtractCorrespondences(1, 0, &corrs); EXPECT_EQ(corrs.size(), 2); EXPECT_EQ(corrs.at(0).image_id, 0); EXPECT_EQ(corrs.at(0).point2D_idx, 0); EXPECT_EQ(corrs.at(1).image_id, 2); EXPECT_EQ(corrs.at(1).point2D_idx, 0); correspondence_graph.ExtractCorrespondences(2, 0, &corrs); EXPECT_EQ(corrs.size(), 2); EXPECT_EQ(corrs.at(0).image_id, 0); EXPECT_EQ(corrs.at(0).point2D_idx, 0); EXPECT_EQ(corrs.at(1).image_id, 1); EXPECT_EQ(corrs.at(1).point2D_idx, 0); correspondence_graph.ExtractCorrespondences(1, 5, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_EQ(corrs.at(0).image_id, 2); EXPECT_EQ(corrs.at(0).point2D_idx, 5); correspondence_graph.ExtractCorrespondences(2, 5, &corrs); EXPECT_EQ(corrs.size(), 1); EXPECT_EQ(corrs.at(0).image_id, 1); EXPECT_EQ(corrs.at(0).point2D_idx, 5); EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 0, 0, 2), 2); EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 1, 0, 2), 2); EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 2, 0, 2), 2); EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 0, 0, 3), 2); EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 1, 0, 3), 2); EXPECT_EQ(CountNumTransitiveCorrespondences(correspondence_graph, 2, 0, 3), 2); } TEST(CorrespondenceGraph, OutOfBounds) { CorrespondenceGraph correspondence_graph; correspondence_graph.AddImage(0, 10); correspondence_graph.AddImage(1, 4); FeatureMatches matches(3); matches[0].point2D_idx1 = 9; matches[0].point2D_idx2 = 3; matches[1].point2D_idx1 = 10; matches[1].point2D_idx2 = 3; matches[2].point2D_idx1 = 9; matches[2].point2D_idx2 = 4; correspondence_graph.AddCorrespondences(0, 1, matches); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(0), 1); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(1), 1); const image_pair_t pair_id = Database::ImagePairToPairId(0, 1); EXPECT_EQ(correspondence_graph.NumCorrespondencesBetweenImages().at(pair_id), 1); } TEST(CorrespondenceGraph, Duplicate) { CorrespondenceGraph correspondence_graph; correspondence_graph.AddImage(0, 10); correspondence_graph.AddImage(1, 10); FeatureMatches matches(5); matches[0].point2D_idx1 = 0; matches[0].point2D_idx2 = 0; matches[1].point2D_idx1 = 1; matches[1].point2D_idx2 = 1; matches[2].point2D_idx1 = 1; matches[2].point2D_idx2 = 1; matches[3].point2D_idx1 = 3; matches[3].point2D_idx2 = 3; matches[4].point2D_idx1 = 3; matches[4].point2D_idx2 = 4; correspondence_graph.AddCorrespondences(0, 1, matches); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(0), 3); EXPECT_EQ(correspondence_graph.NumCorrespondencesForImage(1), 3); const image_pair_t pair_id = Database::ImagePairToPairId(0, 1); EXPECT_EQ(correspondence_graph.NumCorrespondencesBetweenImages().at(pair_id), 3); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/database.cc000066400000000000000000001622301454702036400202220ustar00rootroot00000000000000// 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. #include "colmap/scene/database.h" #include "colmap/util/sqlite3_utils.h" #include "colmap/util/string.h" #include "colmap/util/version.h" #include #include namespace colmap { namespace { typedef Eigen::Matrix FeatureKeypointsBlob; typedef Eigen::Matrix FeatureDescriptorsBlob; typedef Eigen::Matrix FeatureMatchesBlob; void SwapFeatureMatchesBlob(FeatureMatchesBlob* matches) { matches->col(0).swap(matches->col(1)); } FeatureKeypointsBlob FeatureKeypointsToBlob(const FeatureKeypoints& keypoints) { const FeatureKeypointsBlob::Index kNumCols = 6; FeatureKeypointsBlob blob(keypoints.size(), kNumCols); for (size_t i = 0; i < keypoints.size(); ++i) { blob(i, 0) = keypoints[i].x; blob(i, 1) = keypoints[i].y; blob(i, 2) = keypoints[i].a11; blob(i, 3) = keypoints[i].a12; blob(i, 4) = keypoints[i].a21; blob(i, 5) = keypoints[i].a22; } return blob; } FeatureKeypoints FeatureKeypointsFromBlob(const FeatureKeypointsBlob& blob) { FeatureKeypoints keypoints(static_cast(blob.rows())); if (blob.cols() == 2) { for (FeatureKeypointsBlob::Index i = 0; i < blob.rows(); ++i) { keypoints[i] = FeatureKeypoint(blob(i, 0), blob(i, 1)); } } else if (blob.cols() == 4) { for (FeatureKeypointsBlob::Index i = 0; i < blob.rows(); ++i) { keypoints[i] = FeatureKeypoint(blob(i, 0), blob(i, 1), blob(i, 2), blob(i, 3)); } } else if (blob.cols() == 6) { for (FeatureKeypointsBlob::Index i = 0; i < blob.rows(); ++i) { keypoints[i] = FeatureKeypoint(blob(i, 0), blob(i, 1), blob(i, 2), blob(i, 3), blob(i, 4), blob(i, 5)); } } else { LOG(FATAL) << "Keypoint format not supported"; } return keypoints; } FeatureMatchesBlob FeatureMatchesToBlob(const FeatureMatches& matches) { const FeatureMatchesBlob::Index kNumCols = 2; FeatureMatchesBlob blob(matches.size(), kNumCols); for (size_t i = 0; i < matches.size(); ++i) { blob(i, 0) = matches[i].point2D_idx1; blob(i, 1) = matches[i].point2D_idx2; } return blob; } FeatureMatches FeatureMatchesFromBlob(const FeatureMatchesBlob& blob) { CHECK_EQ(blob.cols(), 2); FeatureMatches matches(static_cast(blob.rows())); for (FeatureMatchesBlob::Index i = 0; i < blob.rows(); ++i) { matches[i].point2D_idx1 = blob(i, 0); matches[i].point2D_idx2 = blob(i, 1); } return matches; } template MatrixType ReadStaticMatrixBlob(sqlite3_stmt* sql_stmt, const int rc, const int col) { CHECK_GE(col, 0); MatrixType matrix; if (rc == SQLITE_ROW) { const size_t num_bytes = static_cast(sqlite3_column_bytes(sql_stmt, col)); if (num_bytes > 0) { CHECK_EQ(num_bytes, matrix.size() * sizeof(typename MatrixType::Scalar)); memcpy(reinterpret_cast(matrix.data()), sqlite3_column_blob(sql_stmt, col), num_bytes); } else { matrix = MatrixType::Zero(); } } else { matrix = MatrixType::Zero(); } return matrix; } template MatrixType ReadDynamicMatrixBlob(sqlite3_stmt* sql_stmt, const int rc, const int col) { CHECK_GE(col, 0); MatrixType matrix; if (rc == SQLITE_ROW) { const size_t rows = static_cast(sqlite3_column_int64(sql_stmt, col + 0)); const size_t cols = static_cast(sqlite3_column_int64(sql_stmt, col + 1)); CHECK_GE(rows, 0); CHECK_GE(cols, 0); matrix = MatrixType(rows, cols); const size_t num_bytes = static_cast(sqlite3_column_bytes(sql_stmt, col + 2)); CHECK_EQ(matrix.size() * sizeof(typename MatrixType::Scalar), num_bytes); memcpy(reinterpret_cast(matrix.data()), sqlite3_column_blob(sql_stmt, col + 2), num_bytes); } else { const typename MatrixType::Index rows = (MatrixType::RowsAtCompileTime == Eigen::Dynamic) ? 0 : MatrixType::RowsAtCompileTime; const typename MatrixType::Index cols = (MatrixType::ColsAtCompileTime == Eigen::Dynamic) ? 0 : MatrixType::ColsAtCompileTime; matrix = MatrixType(rows, cols); } return matrix; } template void WriteStaticMatrixBlob(sqlite3_stmt* sql_stmt, const MatrixType& matrix, const int col) { SQLITE3_CALL(sqlite3_bind_blob( sql_stmt, col, reinterpret_cast(matrix.data()), static_cast(matrix.size() * sizeof(typename MatrixType::Scalar)), SQLITE_STATIC)); } template void WriteDynamicMatrixBlob(sqlite3_stmt* sql_stmt, const MatrixType& matrix, const int col) { CHECK_GE(matrix.rows(), 0); CHECK_GE(matrix.cols(), 0); CHECK_GE(col, 0); const size_t num_bytes = matrix.size() * sizeof(typename MatrixType::Scalar); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt, col + 0, matrix.rows())); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt, col + 1, matrix.cols())); SQLITE3_CALL(sqlite3_bind_blob(sql_stmt, col + 2, reinterpret_cast(matrix.data()), static_cast(num_bytes), SQLITE_STATIC)); } Camera ReadCameraRow(sqlite3_stmt* sql_stmt) { Camera camera; camera.camera_id = static_cast(sqlite3_column_int64(sql_stmt, 0)); camera.model_id = static_cast(sqlite3_column_int64(sql_stmt, 1)); camera.width = static_cast(sqlite3_column_int64(sql_stmt, 2)); camera.height = static_cast(sqlite3_column_int64(sql_stmt, 3)); const size_t num_params_bytes = static_cast(sqlite3_column_bytes(sql_stmt, 4)); const size_t num_params = num_params_bytes / sizeof(double); CHECK_EQ(num_params, CameraModelNumParams(camera.model_id)); camera.params.resize(num_params, 0.); memcpy( camera.params.data(), sqlite3_column_blob(sql_stmt, 4), num_params_bytes); camera.has_prior_focal_length = sqlite3_column_int64(sql_stmt, 5) != 0; return camera; } Image ReadImageRow(sqlite3_stmt* sql_stmt) { Image image; image.SetImageId(static_cast(sqlite3_column_int64(sql_stmt, 0))); image.SetName(std::string( reinterpret_cast(sqlite3_column_text(sql_stmt, 1)))); image.SetCameraId(static_cast(sqlite3_column_int64(sql_stmt, 2))); auto ExtractDoubleColumnOrNaN = [sql_stmt](size_t column) { // NaNs are automatically converted to NULLs in SQLite. if (sqlite3_column_type(sql_stmt, column) != SQLITE_NULL) { return sqlite3_column_double(sql_stmt, column); } return std::numeric_limits::quiet_NaN(); }; image.CamFromWorldPrior().rotation.w() = ExtractDoubleColumnOrNaN(3); image.CamFromWorldPrior().rotation.x() = ExtractDoubleColumnOrNaN(4); image.CamFromWorldPrior().rotation.y() = ExtractDoubleColumnOrNaN(5); image.CamFromWorldPrior().rotation.z() = ExtractDoubleColumnOrNaN(6); image.CamFromWorldPrior().translation.x() = ExtractDoubleColumnOrNaN(7); image.CamFromWorldPrior().translation.y() = ExtractDoubleColumnOrNaN(8); image.CamFromWorldPrior().translation.z() = ExtractDoubleColumnOrNaN(9); return image; } } // namespace const size_t Database::kMaxNumImages = static_cast(std::numeric_limits::max()); const std::string Database::kInMemoryDatabasePath = ":memory:"; std::mutex Database::update_schema_mutex_; Database::Database() : database_(nullptr) {} Database::Database(const std::string& path) : Database() { Open(path); } Database::~Database() { Close(); } void Database::Open(const std::string& path) { Close(); // SQLITE_OPEN_NOMUTEX specifies that the connection should not have a // mutex (so that we don't serialize the connection's operations). // Modifications to the database will still be serialized, but multiple // connections can read concurrently. SQLITE3_CALL(sqlite3_open_v2( path.c_str(), &database_, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, nullptr)); // Don't wait for the operating system to write the changes to disk SQLITE3_EXEC(database_, "PRAGMA synchronous=OFF", nullptr); // Use faster journaling mode SQLITE3_EXEC(database_, "PRAGMA journal_mode=WAL", nullptr); // Store temporary tables and indices in memory SQLITE3_EXEC(database_, "PRAGMA temp_store=MEMORY", nullptr); // Disabled by default SQLITE3_EXEC(database_, "PRAGMA foreign_keys=ON", nullptr); // Enable auto vacuum to reduce DB file size SQLITE3_EXEC(database_, "PRAGMA auto_vacuum=1", nullptr); CreateTables(); UpdateSchema(); PrepareSQLStatements(); } void Database::Close() { if (database_ != nullptr) { FinalizeSQLStatements(); if (database_cleared_) { SQLITE3_EXEC(database_, "VACUUM", nullptr); database_cleared_ = false; } sqlite3_close_v2(database_); database_ = nullptr; } } bool Database::ExistsCamera(const camera_t camera_id) const { return ExistsRowId(sql_stmt_exists_camera_, camera_id); } bool Database::ExistsImage(const image_t image_id) const { return ExistsRowId(sql_stmt_exists_image_id_, image_id); } bool Database::ExistsImageWithName(const std::string& name) const { return ExistsRowString(sql_stmt_exists_image_name_, name); } bool Database::ExistsKeypoints(const image_t image_id) const { return ExistsRowId(sql_stmt_exists_keypoints_, image_id); } bool Database::ExistsDescriptors(const image_t image_id) const { return ExistsRowId(sql_stmt_exists_descriptors_, image_id); } bool Database::ExistsMatches(const image_t image_id1, const image_t image_id2) const { return ExistsRowId(sql_stmt_exists_matches_, ImagePairToPairId(image_id1, image_id2)); } bool Database::ExistsInlierMatches(const image_t image_id1, const image_t image_id2) const { return ExistsRowId(sql_stmt_exists_two_view_geometry_, ImagePairToPairId(image_id1, image_id2)); } size_t Database::NumCameras() const { return CountRows("cameras"); } size_t Database::NumImages() const { return CountRows("images"); } size_t Database::NumKeypoints() const { return SumColumn("rows", "keypoints"); } size_t Database::MaxNumKeypoints() const { return MaxColumn("rows", "keypoints"); } size_t Database::NumKeypointsForImage(const image_t image_id) const { return CountRowsForEntry(sql_stmt_num_keypoints_, image_id); } size_t Database::NumDescriptors() const { return SumColumn("rows", "descriptors"); } size_t Database::MaxNumDescriptors() const { return MaxColumn("rows", "descriptors"); } size_t Database::NumDescriptorsForImage(const image_t image_id) const { return CountRowsForEntry(sql_stmt_num_descriptors_, image_id); } size_t Database::NumMatches() const { return SumColumn("rows", "matches"); } size_t Database::NumInlierMatches() const { return SumColumn("rows", "two_view_geometries"); } size_t Database::NumMatchedImagePairs() const { return CountRows("matches"); } size_t Database::NumVerifiedImagePairs() const { return CountRows("two_view_geometries"); } Camera Database::ReadCamera(const camera_t camera_id) const { SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_read_camera_, 1, camera_id)); Camera camera; const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_camera_)); if (rc == SQLITE_ROW) { camera = ReadCameraRow(sql_stmt_read_camera_); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_camera_)); return camera; } std::vector Database::ReadAllCameras() const { std::vector cameras; while (SQLITE3_CALL(sqlite3_step(sql_stmt_read_cameras_)) == SQLITE_ROW) { cameras.push_back(ReadCameraRow(sql_stmt_read_cameras_)); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_cameras_)); return cameras; } Image Database::ReadImage(const image_t image_id) const { SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_read_image_id_, 1, image_id)); Image image; const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_image_id_)); if (rc == SQLITE_ROW) { image = ReadImageRow(sql_stmt_read_image_id_); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_image_id_)); return image; } Image Database::ReadImageWithName(const std::string& name) const { SQLITE3_CALL(sqlite3_bind_text(sql_stmt_read_image_name_, 1, name.c_str(), static_cast(name.size()), SQLITE_STATIC)); Image image; const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_image_name_)); if (rc == SQLITE_ROW) { image = ReadImageRow(sql_stmt_read_image_name_); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_image_name_)); return image; } std::vector Database::ReadAllImages() const { std::vector images; images.reserve(NumImages()); while (SQLITE3_CALL(sqlite3_step(sql_stmt_read_images_)) == SQLITE_ROW) { images.push_back(ReadImageRow(sql_stmt_read_images_)); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_images_)); return images; } FeatureKeypoints Database::ReadKeypoints(const image_t image_id) const { SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_read_keypoints_, 1, image_id)); const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_keypoints_)); const FeatureKeypointsBlob blob = ReadDynamicMatrixBlob( sql_stmt_read_keypoints_, rc, 0); SQLITE3_CALL(sqlite3_reset(sql_stmt_read_keypoints_)); return FeatureKeypointsFromBlob(blob); } FeatureDescriptors Database::ReadDescriptors(const image_t image_id) const { SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_read_descriptors_, 1, image_id)); const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_descriptors_)); FeatureDescriptors descriptors = ReadDynamicMatrixBlob( sql_stmt_read_descriptors_, rc, 0); SQLITE3_CALL(sqlite3_reset(sql_stmt_read_descriptors_)); return descriptors; } FeatureMatches Database::ReadMatches(image_t image_id1, image_t image_id2) const { const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_read_matches_, 1, pair_id)); const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_matches_)); FeatureMatchesBlob blob = ReadDynamicMatrixBlob(sql_stmt_read_matches_, rc, 0); SQLITE3_CALL(sqlite3_reset(sql_stmt_read_matches_)); if (SwapImagePair(image_id1, image_id2)) { SwapFeatureMatchesBlob(&blob); } return FeatureMatchesFromBlob(blob); } std::vector> Database::ReadAllMatches() const { std::vector> all_matches; int rc; while ((rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_matches_all_))) == SQLITE_ROW) { const image_pair_t pair_id = static_cast( sqlite3_column_int64(sql_stmt_read_matches_all_, 0)); const FeatureMatchesBlob blob = ReadDynamicMatrixBlob( sql_stmt_read_matches_all_, rc, 1); all_matches.emplace_back(pair_id, FeatureMatchesFromBlob(blob)); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_matches_all_)); return all_matches; } TwoViewGeometry Database::ReadTwoViewGeometry(const image_t image_id1, const image_t image_id2) const { const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); SQLITE3_CALL( sqlite3_bind_int64(sql_stmt_read_two_view_geometry_, 1, pair_id)); const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt_read_two_view_geometry_)); TwoViewGeometry two_view_geometry; FeatureMatchesBlob blob = ReadDynamicMatrixBlob( sql_stmt_read_two_view_geometry_, rc, 0); two_view_geometry.config = static_cast( sqlite3_column_int64(sql_stmt_read_two_view_geometry_, 3)); two_view_geometry.F = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometry_, rc, 4); two_view_geometry.E = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometry_, rc, 5); two_view_geometry.H = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometry_, rc, 6); const Eigen::Vector4d quat_wxyz = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometry_, rc, 7); two_view_geometry.cam2_from_cam1.rotation = Eigen::Quaterniond( quat_wxyz(0), quat_wxyz(1), quat_wxyz(2), quat_wxyz(3)); two_view_geometry.cam2_from_cam1.translation = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometry_, rc, 8); SQLITE3_CALL(sqlite3_reset(sql_stmt_read_two_view_geometry_)); two_view_geometry.inlier_matches = FeatureMatchesFromBlob(blob); two_view_geometry.F.transposeInPlace(); two_view_geometry.E.transposeInPlace(); two_view_geometry.H.transposeInPlace(); if (SwapImagePair(image_id1, image_id2)) { two_view_geometry.Invert(); } return two_view_geometry; } void Database::ReadTwoViewGeometries( std::vector* image_pair_ids, std::vector* two_view_geometries) const { int rc; while ((rc = SQLITE3_CALL(sqlite3_step( sql_stmt_read_two_view_geometries_))) == SQLITE_ROW) { const image_pair_t pair_id = static_cast( sqlite3_column_int64(sql_stmt_read_two_view_geometries_, 0)); image_pair_ids->push_back(pair_id); TwoViewGeometry two_view_geometry; const FeatureMatchesBlob blob = ReadDynamicMatrixBlob( sql_stmt_read_two_view_geometries_, rc, 1); two_view_geometry.inlier_matches = FeatureMatchesFromBlob(blob); two_view_geometry.config = static_cast( sqlite3_column_int64(sql_stmt_read_two_view_geometries_, 4)); two_view_geometry.F = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometries_, rc, 5); two_view_geometry.E = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometries_, rc, 6); two_view_geometry.H = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometries_, rc, 7); const Eigen::Vector4d quat_wxyz = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometries_, rc, 8); two_view_geometry.cam2_from_cam1.rotation = Eigen::Quaterniond( quat_wxyz(0), quat_wxyz(1), quat_wxyz(2), quat_wxyz(3)); two_view_geometry.cam2_from_cam1.translation = ReadStaticMatrixBlob( sql_stmt_read_two_view_geometries_, rc, 9); two_view_geometry.F.transposeInPlace(); two_view_geometry.E.transposeInPlace(); two_view_geometry.H.transposeInPlace(); two_view_geometries->push_back(two_view_geometry); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_two_view_geometries_)); } void Database::ReadTwoViewGeometryNumInliers( std::vector>* image_pairs, std::vector* num_inliers) const { const auto num_inlier_matches = NumInlierMatches(); image_pairs->reserve(num_inlier_matches); num_inliers->reserve(num_inlier_matches); while (SQLITE3_CALL(sqlite3_step( sql_stmt_read_two_view_geometry_num_inliers_)) == SQLITE_ROW) { image_t image_id1; image_t image_id2; const image_pair_t pair_id = static_cast( sqlite3_column_int64(sql_stmt_read_two_view_geometry_num_inliers_, 0)); PairIdToImagePair(pair_id, &image_id1, &image_id2); image_pairs->emplace_back(image_id1, image_id2); const int rows = static_cast( sqlite3_column_int64(sql_stmt_read_two_view_geometry_num_inliers_, 1)); num_inliers->push_back(rows); } SQLITE3_CALL(sqlite3_reset(sql_stmt_read_two_view_geometry_num_inliers_)); } camera_t Database::WriteCamera(const Camera& camera, const bool use_camera_id) const { if (use_camera_id) { CHECK(!ExistsCamera(camera.camera_id)) << "camera_id must be unique"; SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_add_camera_, 1, camera.camera_id)); } else { SQLITE3_CALL(sqlite3_bind_null(sql_stmt_add_camera_, 1)); } SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_add_camera_, 2, static_cast(camera.model_id))); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_add_camera_, 3, static_cast(camera.width))); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_add_camera_, 4, static_cast(camera.height))); const size_t num_params_bytes = sizeof(double) * camera.params.size(); SQLITE3_CALL(sqlite3_bind_blob(sql_stmt_add_camera_, 5, camera.params.data(), static_cast(num_params_bytes), SQLITE_STATIC)); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_add_camera_, 6, camera.has_prior_focal_length)); SQLITE3_CALL(sqlite3_step(sql_stmt_add_camera_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_add_camera_)); return static_cast(sqlite3_last_insert_rowid(database_)); } image_t Database::WriteImage(const Image& image, const bool use_image_id) const { if (use_image_id) { CHECK(!ExistsImage(image.ImageId())) << "image_id must be unique"; SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_add_image_, 1, image.ImageId())); } else { SQLITE3_CALL(sqlite3_bind_null(sql_stmt_add_image_, 1)); } SQLITE3_CALL(sqlite3_bind_text(sql_stmt_add_image_, 2, image.Name().c_str(), static_cast(image.Name().size()), SQLITE_STATIC)); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_add_image_, 3, image.CameraId())); // NaNs are automatically converted to NULLs in SQLite. SQLITE3_CALL(sqlite3_bind_double( sql_stmt_add_image_, 4, image.CamFromWorldPrior().rotation.w())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_add_image_, 5, image.CamFromWorldPrior().rotation.x())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_add_image_, 6, image.CamFromWorldPrior().rotation.y())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_add_image_, 7, image.CamFromWorldPrior().rotation.z())); // NaNs are automatically converted to NULLs in SQLite. SQLITE3_CALL(sqlite3_bind_double( sql_stmt_add_image_, 8, image.CamFromWorldPrior().translation.x())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_add_image_, 9, image.CamFromWorldPrior().translation.y())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_add_image_, 10, image.CamFromWorldPrior().translation.z())); SQLITE3_CALL(sqlite3_step(sql_stmt_add_image_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_add_image_)); return static_cast(sqlite3_last_insert_rowid(database_)); } void Database::WriteKeypoints(const image_t image_id, const FeatureKeypoints& keypoints) const { const FeatureKeypointsBlob blob = FeatureKeypointsToBlob(keypoints); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_write_keypoints_, 1, image_id)); WriteDynamicMatrixBlob(sql_stmt_write_keypoints_, blob, 2); SQLITE3_CALL(sqlite3_step(sql_stmt_write_keypoints_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_write_keypoints_)); } void Database::WriteDescriptors(const image_t image_id, const FeatureDescriptors& descriptors) const { SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_write_descriptors_, 1, image_id)); WriteDynamicMatrixBlob(sql_stmt_write_descriptors_, descriptors, 2); SQLITE3_CALL(sqlite3_step(sql_stmt_write_descriptors_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_write_descriptors_)); } void Database::WriteMatches(const image_t image_id1, const image_t image_id2, const FeatureMatches& matches) const { const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_write_matches_, 1, pair_id)); // Important: the swapped data must live until the query is executed. FeatureMatchesBlob blob = FeatureMatchesToBlob(matches); if (SwapImagePair(image_id1, image_id2)) { SwapFeatureMatchesBlob(&blob); WriteDynamicMatrixBlob(sql_stmt_write_matches_, blob, 2); } else { WriteDynamicMatrixBlob(sql_stmt_write_matches_, blob, 2); } SQLITE3_CALL(sqlite3_step(sql_stmt_write_matches_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_write_matches_)); } void Database::WriteTwoViewGeometry( const image_t image_id1, const image_t image_id2, const TwoViewGeometry& two_view_geometry) const { const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); SQLITE3_CALL( sqlite3_bind_int64(sql_stmt_write_two_view_geometry_, 1, pair_id)); const TwoViewGeometry* two_view_geometry_ptr = &two_view_geometry; // Invert the two-view geometry if the image pair has to be swapped. std::unique_ptr swapped_two_view_geometry; if (SwapImagePair(image_id1, image_id2)) { swapped_two_view_geometry = std::make_unique(); *swapped_two_view_geometry = two_view_geometry; swapped_two_view_geometry->Invert(); two_view_geometry_ptr = swapped_two_view_geometry.get(); } const FeatureMatchesBlob inlier_matches = FeatureMatchesToBlob(two_view_geometry_ptr->inlier_matches); WriteDynamicMatrixBlob(sql_stmt_write_two_view_geometry_, inlier_matches, 2); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_write_two_view_geometry_, 5, two_view_geometry_ptr->config)); // Transpose the matrices to obtain row-major data layout. // Important: Do not move these objects inside the if-statement, because // the objects must live until `sqlite3_step` is called on the statement. const Eigen::Matrix3d Ft = two_view_geometry_ptr->F.transpose(); const Eigen::Matrix3d Et = two_view_geometry_ptr->E.transpose(); const Eigen::Matrix3d Ht = two_view_geometry_ptr->H.transpose(); const Eigen::Vector4d quat_wxyz( two_view_geometry_ptr->cam2_from_cam1.rotation.w(), two_view_geometry_ptr->cam2_from_cam1.rotation.x(), two_view_geometry_ptr->cam2_from_cam1.rotation.y(), two_view_geometry_ptr->cam2_from_cam1.rotation.z()); if (two_view_geometry_ptr->inlier_matches.size() > 0) { WriteStaticMatrixBlob(sql_stmt_write_two_view_geometry_, Ft, 6); WriteStaticMatrixBlob(sql_stmt_write_two_view_geometry_, Et, 7); WriteStaticMatrixBlob(sql_stmt_write_two_view_geometry_, Ht, 8); WriteStaticMatrixBlob(sql_stmt_write_two_view_geometry_, quat_wxyz, 9); WriteStaticMatrixBlob(sql_stmt_write_two_view_geometry_, two_view_geometry_ptr->cam2_from_cam1.translation, 10); } else { WriteStaticMatrixBlob( sql_stmt_write_two_view_geometry_, Eigen::MatrixXd(0, 0), 6); WriteStaticMatrixBlob( sql_stmt_write_two_view_geometry_, Eigen::MatrixXd(0, 0), 7); WriteStaticMatrixBlob( sql_stmt_write_two_view_geometry_, Eigen::MatrixXd(0, 0), 8); WriteStaticMatrixBlob( sql_stmt_write_two_view_geometry_, Eigen::MatrixXd(0, 0), 9); WriteStaticMatrixBlob( sql_stmt_write_two_view_geometry_, Eigen::MatrixXd(0, 0), 10); } SQLITE3_CALL(sqlite3_step(sql_stmt_write_two_view_geometry_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_write_two_view_geometry_)); } void Database::UpdateCamera(const Camera& camera) const { SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_update_camera_, 1, static_cast(camera.model_id))); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_update_camera_, 2, static_cast(camera.width))); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_update_camera_, 3, static_cast(camera.height))); const size_t num_params_bytes = sizeof(double) * camera.params.size(); SQLITE3_CALL(sqlite3_bind_blob(sql_stmt_update_camera_, 4, camera.params.data(), static_cast(num_params_bytes), SQLITE_STATIC)); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_update_camera_, 5, camera.has_prior_focal_length)); SQLITE3_CALL( sqlite3_bind_int64(sql_stmt_update_camera_, 6, camera.camera_id)); SQLITE3_CALL(sqlite3_step(sql_stmt_update_camera_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_update_camera_)); } void Database::UpdateImage(const Image& image) const { SQLITE3_CALL(sqlite3_bind_text(sql_stmt_update_image_, 1, image.Name().c_str(), static_cast(image.Name().size()), SQLITE_STATIC)); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_update_image_, 2, image.CameraId())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_update_image_, 3, image.CamFromWorldPrior().rotation.w())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_update_image_, 4, image.CamFromWorldPrior().rotation.x())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_update_image_, 5, image.CamFromWorldPrior().rotation.y())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_update_image_, 6, image.CamFromWorldPrior().rotation.z())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_update_image_, 7, image.CamFromWorldPrior().translation.x())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_update_image_, 8, image.CamFromWorldPrior().translation.y())); SQLITE3_CALL(sqlite3_bind_double( sql_stmt_update_image_, 9, image.CamFromWorldPrior().translation.z())); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_update_image_, 10, image.ImageId())); SQLITE3_CALL(sqlite3_step(sql_stmt_update_image_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_update_image_)); } void Database::DeleteMatches(const image_t image_id1, const image_t image_id2) const { const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); SQLITE3_CALL(sqlite3_bind_int64( sql_stmt_delete_matches_, 1, static_cast(pair_id))); SQLITE3_CALL(sqlite3_step(sql_stmt_delete_matches_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_delete_matches_)); database_cleared_ = true; } void Database::DeleteInlierMatches(const image_t image_id1, const image_t image_id2) const { const image_pair_t pair_id = ImagePairToPairId(image_id1, image_id2); SQLITE3_CALL(sqlite3_bind_int64(sql_stmt_delete_two_view_geometry_, 1, static_cast(pair_id))); SQLITE3_CALL(sqlite3_step(sql_stmt_delete_two_view_geometry_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_delete_two_view_geometry_)); database_cleared_ = true; } void Database::ClearAllTables() const { ClearMatches(); ClearTwoViewGeometries(); ClearDescriptors(); ClearKeypoints(); ClearImages(); ClearCameras(); } void Database::ClearCameras() const { SQLITE3_CALL(sqlite3_step(sql_stmt_clear_cameras_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_clear_cameras_)); database_cleared_ = true; } void Database::ClearImages() const { SQLITE3_CALL(sqlite3_step(sql_stmt_clear_images_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_clear_images_)); database_cleared_ = true; } void Database::ClearDescriptors() const { SQLITE3_CALL(sqlite3_step(sql_stmt_clear_descriptors_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_clear_descriptors_)); database_cleared_ = true; } void Database::ClearKeypoints() const { SQLITE3_CALL(sqlite3_step(sql_stmt_clear_keypoints_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_clear_keypoints_)); database_cleared_ = true; } void Database::ClearMatches() const { SQLITE3_CALL(sqlite3_step(sql_stmt_clear_matches_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_clear_matches_)); database_cleared_ = true; } void Database::ClearTwoViewGeometries() const { SQLITE3_CALL(sqlite3_step(sql_stmt_clear_two_view_geometries_)); SQLITE3_CALL(sqlite3_reset(sql_stmt_clear_two_view_geometries_)); database_cleared_ = true; } void Database::Merge(const Database& database1, const Database& database2, Database* merged_database) { // Merge the cameras. std::unordered_map new_camera_ids1; for (const auto& camera : database1.ReadAllCameras()) { const camera_t new_camera_id = merged_database->WriteCamera(camera); new_camera_ids1.emplace(camera.camera_id, new_camera_id); } std::unordered_map new_camera_ids2; for (const auto& camera : database2.ReadAllCameras()) { const camera_t new_camera_id = merged_database->WriteCamera(camera); new_camera_ids2.emplace(camera.camera_id, new_camera_id); } // Merge the images. std::unordered_map new_image_ids1; for (auto& image : database1.ReadAllImages()) { image.SetCameraId(new_camera_ids1.at(image.CameraId())); CHECK(!merged_database->ExistsImageWithName(image.Name())) << "The two databases must not contain images with the same name, but " "the there are images with name " << image.Name() << " in both databases"; const image_t new_image_id = merged_database->WriteImage(image); new_image_ids1.emplace(image.ImageId(), new_image_id); const auto keypoints = database1.ReadKeypoints(image.ImageId()); const auto descriptors = database1.ReadDescriptors(image.ImageId()); merged_database->WriteKeypoints(new_image_id, keypoints); merged_database->WriteDescriptors(new_image_id, descriptors); } std::unordered_map new_image_ids2; for (auto& image : database2.ReadAllImages()) { image.SetCameraId(new_camera_ids2.at(image.CameraId())); CHECK(!merged_database->ExistsImageWithName(image.Name())) << "The two databases must not contain images with the same name, but " "the there are images with name " << image.Name() << " in both databases"; const image_t new_image_id = merged_database->WriteImage(image); new_image_ids2.emplace(image.ImageId(), new_image_id); const auto keypoints = database2.ReadKeypoints(image.ImageId()); const auto descriptors = database2.ReadDescriptors(image.ImageId()); merged_database->WriteKeypoints(new_image_id, keypoints); merged_database->WriteDescriptors(new_image_id, descriptors); } // Merge the matches. for (const auto& matches : database1.ReadAllMatches()) { image_t image_id1, image_id2; Database::PairIdToImagePair(matches.first, &image_id1, &image_id2); const image_t new_image_id1 = new_image_ids1.at(image_id1); const image_t new_image_id2 = new_image_ids1.at(image_id2); merged_database->WriteMatches(new_image_id1, new_image_id2, matches.second); } for (const auto& matches : database2.ReadAllMatches()) { image_t image_id1, image_id2; Database::PairIdToImagePair(matches.first, &image_id1, &image_id2); const image_t new_image_id1 = new_image_ids2.at(image_id1); const image_t new_image_id2 = new_image_ids2.at(image_id2); merged_database->WriteMatches(new_image_id1, new_image_id2, matches.second); } // Merge the two-view geometries. { std::vector image_pair_ids; std::vector two_view_geometries; database1.ReadTwoViewGeometries(&image_pair_ids, &two_view_geometries); for (size_t i = 0; i < two_view_geometries.size(); ++i) { image_t image_id1, image_id2; Database::PairIdToImagePair(image_pair_ids[i], &image_id1, &image_id2); const image_t new_image_id1 = new_image_ids1.at(image_id1); const image_t new_image_id2 = new_image_ids1.at(image_id2); merged_database->WriteTwoViewGeometry( new_image_id1, new_image_id2, two_view_geometries[i]); } } { std::vector image_pair_ids; std::vector two_view_geometries; database2.ReadTwoViewGeometries(&image_pair_ids, &two_view_geometries); for (size_t i = 0; i < two_view_geometries.size(); ++i) { image_t image_id1, image_id2; Database::PairIdToImagePair(image_pair_ids[i], &image_id1, &image_id2); const image_t new_image_id1 = new_image_ids2.at(image_id1); const image_t new_image_id2 = new_image_ids2.at(image_id2); merged_database->WriteTwoViewGeometry( new_image_id1, new_image_id2, two_view_geometries[i]); } } } void Database::BeginTransaction() const { SQLITE3_EXEC(database_, "BEGIN TRANSACTION", nullptr); } void Database::EndTransaction() const { SQLITE3_EXEC(database_, "END TRANSACTION", nullptr); } void Database::PrepareSQLStatements() { sql_stmts_.clear(); std::string sql; ////////////////////////////////////////////////////////////////////////////// // num_* ////////////////////////////////////////////////////////////////////////////// sql = "SELECT rows FROM keypoints WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_num_keypoints_, 0)); sql_stmts_.push_back(sql_stmt_num_keypoints_); sql = "SELECT rows FROM descriptors WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_num_descriptors_, 0)); sql_stmts_.push_back(sql_stmt_num_descriptors_); ////////////////////////////////////////////////////////////////////////////// // exists_* ////////////////////////////////////////////////////////////////////////////// sql = "SELECT 1 FROM cameras WHERE camera_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_exists_camera_, 0)); sql_stmts_.push_back(sql_stmt_exists_camera_); sql = "SELECT 1 FROM images WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_exists_image_id_, 0)); sql_stmts_.push_back(sql_stmt_exists_image_id_); sql = "SELECT 1 FROM images WHERE name = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_exists_image_name_, 0)); sql_stmts_.push_back(sql_stmt_exists_image_name_); sql = "SELECT 1 FROM keypoints WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_exists_keypoints_, 0)); sql_stmts_.push_back(sql_stmt_exists_keypoints_); sql = "SELECT 1 FROM descriptors WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_exists_descriptors_, 0)); sql_stmts_.push_back(sql_stmt_exists_descriptors_); sql = "SELECT 1 FROM matches WHERE pair_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_exists_matches_, 0)); sql_stmts_.push_back(sql_stmt_exists_matches_); sql = "SELECT 1 FROM two_view_geometries WHERE pair_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_exists_two_view_geometry_, 0)); sql_stmts_.push_back(sql_stmt_exists_two_view_geometry_); ////////////////////////////////////////////////////////////////////////////// // add_* ////////////////////////////////////////////////////////////////////////////// sql = "INSERT INTO cameras(camera_id, model, width, height, params, " "prior_focal_length) VALUES(?, ?, ?, ?, ?, ?);"; SQLITE3_CALL( sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt_add_camera_, 0)); sql_stmts_.push_back(sql_stmt_add_camera_); sql = "INSERT INTO images(image_id, name, camera_id, prior_qw, prior_qx, " "prior_qy, prior_qz, prior_tx, prior_ty, prior_tz) VALUES(?, ?, ?, ?, ?, " "?, ?, ?, ?, ?);"; SQLITE3_CALL( sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt_add_image_, 0)); sql_stmts_.push_back(sql_stmt_add_image_); ////////////////////////////////////////////////////////////////////////////// // update_* ////////////////////////////////////////////////////////////////////////////// sql = "UPDATE cameras SET model=?, width=?, height=?, params=?, " "prior_focal_length=? WHERE camera_id=?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_update_camera_, 0)); sql_stmts_.push_back(sql_stmt_update_camera_); sql = "UPDATE images SET name=?, camera_id=?, prior_qw=?, prior_qx=?, " "prior_qy=?, prior_qz=?, prior_tx=?, prior_ty=?, prior_tz=? WHERE " "image_id=?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_update_image_, 0)); sql_stmts_.push_back(sql_stmt_update_image_); ////////////////////////////////////////////////////////////////////////////// // read_* ////////////////////////////////////////////////////////////////////////////// sql = "SELECT * FROM cameras WHERE camera_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_camera_, 0)); sql_stmts_.push_back(sql_stmt_read_camera_); sql = "SELECT * FROM cameras;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_cameras_, 0)); sql_stmts_.push_back(sql_stmt_read_cameras_); sql = "SELECT * FROM images WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_image_id_, 0)); sql_stmts_.push_back(sql_stmt_read_image_id_); sql = "SELECT * FROM images WHERE name = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_image_name_, 0)); sql_stmts_.push_back(sql_stmt_read_image_name_); sql = "SELECT * FROM images;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_images_, 0)); sql_stmts_.push_back(sql_stmt_read_images_); sql = "SELECT rows, cols, data FROM keypoints WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_keypoints_, 0)); sql_stmts_.push_back(sql_stmt_read_keypoints_); sql = "SELECT rows, cols, data FROM descriptors WHERE image_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_descriptors_, 0)); sql_stmts_.push_back(sql_stmt_read_descriptors_); sql = "SELECT rows, cols, data FROM matches WHERE pair_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_matches_, 0)); sql_stmts_.push_back(sql_stmt_read_matches_); sql = "SELECT * FROM matches WHERE rows > 0;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_matches_all_, 0)); sql_stmts_.push_back(sql_stmt_read_matches_all_); sql = "SELECT rows, cols, data, config, F, E, H, qvec, tvec FROM " "two_view_geometries WHERE pair_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_two_view_geometry_, 0)); sql_stmts_.push_back(sql_stmt_read_two_view_geometry_); sql = "SELECT * FROM two_view_geometries WHERE rows > 0;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_read_two_view_geometries_, 0)); sql_stmts_.push_back(sql_stmt_read_two_view_geometries_); sql = "SELECT pair_id, rows FROM two_view_geometries WHERE rows > 0;"; SQLITE3_CALL(sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt_read_two_view_geometry_num_inliers_, 0)); sql_stmts_.push_back(sql_stmt_read_two_view_geometry_num_inliers_); ////////////////////////////////////////////////////////////////////////////// // write_* ////////////////////////////////////////////////////////////////////////////// sql = "INSERT INTO keypoints(image_id, rows, cols, data) VALUES(?, ?, ?, ?);"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_write_keypoints_, 0)); sql_stmts_.push_back(sql_stmt_write_keypoints_); sql = "INSERT INTO descriptors(image_id, rows, cols, data) VALUES(?, ?, ?, ?);"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_write_descriptors_, 0)); sql_stmts_.push_back(sql_stmt_write_descriptors_); sql = "INSERT INTO matches(pair_id, rows, cols, data) VALUES(?, ?, ?, ?);"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_write_matches_, 0)); sql_stmts_.push_back(sql_stmt_write_matches_); sql = "INSERT INTO two_view_geometries(pair_id, rows, cols, data, config, F, " "E, H, qvec, tvec) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_write_two_view_geometry_, 0)); sql_stmts_.push_back(sql_stmt_write_two_view_geometry_); ////////////////////////////////////////////////////////////////////////////// // delete_* ////////////////////////////////////////////////////////////////////////////// sql = "DELETE FROM matches WHERE pair_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_delete_matches_, 0)); sql_stmts_.push_back(sql_stmt_delete_matches_); sql = "DELETE FROM two_view_geometries WHERE pair_id = ?;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_delete_two_view_geometry_, 0)); sql_stmts_.push_back(sql_stmt_delete_two_view_geometry_); ////////////////////////////////////////////////////////////////////////////// // clear_* ////////////////////////////////////////////////////////////////////////////// sql = "DELETE FROM cameras;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_clear_cameras_, 0)); sql_stmts_.push_back(sql_stmt_clear_cameras_); sql = "DELETE FROM images;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_clear_images_, 0)); sql_stmts_.push_back(sql_stmt_clear_images_); sql = "DELETE FROM descriptors;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_clear_descriptors_, 0)); sql_stmts_.push_back(sql_stmt_clear_descriptors_); sql = "DELETE FROM keypoints;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_clear_keypoints_, 0)); sql_stmts_.push_back(sql_stmt_clear_keypoints_); sql = "DELETE FROM matches;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_clear_matches_, 0)); sql_stmts_.push_back(sql_stmt_clear_matches_); sql = "DELETE FROM two_view_geometries;"; SQLITE3_CALL(sqlite3_prepare_v2( database_, sql.c_str(), -1, &sql_stmt_clear_two_view_geometries_, 0)); sql_stmts_.push_back(sql_stmt_clear_two_view_geometries_); } void Database::FinalizeSQLStatements() { for (const auto& sql_stmt : sql_stmts_) { SQLITE3_CALL(sqlite3_finalize(sql_stmt)); } } void Database::CreateTables() const { CreateCameraTable(); CreateImageTable(); CreateKeypointsTable(); CreateDescriptorsTable(); CreateMatchesTable(); CreateTwoViewGeometriesTable(); } void Database::CreateCameraTable() const { const std::string sql = "CREATE TABLE IF NOT EXISTS cameras" " (camera_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," " model INTEGER NOT NULL," " width INTEGER NOT NULL," " height INTEGER NOT NULL," " params BLOB," " prior_focal_length INTEGER NOT NULL);"; SQLITE3_EXEC(database_, sql.c_str(), nullptr); } void Database::CreateImageTable() const { const std::string sql = StringPrintf( "CREATE TABLE IF NOT EXISTS images" " (image_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," " name TEXT NOT NULL UNIQUE," " camera_id INTEGER NOT NULL," " prior_qw REAL," " prior_qx REAL," " prior_qy REAL," " prior_qz REAL," " prior_tx REAL," " prior_ty REAL," " prior_tz REAL," "CONSTRAINT image_id_check CHECK(image_id >= 0 and image_id < %d)," "FOREIGN KEY(camera_id) REFERENCES cameras(camera_id));" "CREATE UNIQUE INDEX IF NOT EXISTS index_name ON images(name);", kMaxNumImages); SQLITE3_EXEC(database_, sql.c_str(), nullptr); } void Database::CreateKeypointsTable() const { const std::string sql = "CREATE TABLE IF NOT EXISTS keypoints" " (image_id INTEGER PRIMARY KEY NOT NULL," " rows INTEGER NOT NULL," " cols INTEGER NOT NULL," " data BLOB," "FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE);"; SQLITE3_EXEC(database_, sql.c_str(), nullptr); } void Database::CreateDescriptorsTable() const { const std::string sql = "CREATE TABLE IF NOT EXISTS descriptors" " (image_id INTEGER PRIMARY KEY NOT NULL," " rows INTEGER NOT NULL," " cols INTEGER NOT NULL," " data BLOB," "FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE);"; SQLITE3_EXEC(database_, sql.c_str(), nullptr); } void Database::CreateMatchesTable() const { const std::string sql = "CREATE TABLE IF NOT EXISTS matches" " (pair_id INTEGER PRIMARY KEY NOT NULL," " rows INTEGER NOT NULL," " cols INTEGER NOT NULL," " data BLOB);"; SQLITE3_EXEC(database_, sql.c_str(), nullptr); } void Database::CreateTwoViewGeometriesTable() const { if (ExistsTable("inlier_matches")) { SQLITE3_EXEC(database_, "ALTER TABLE inlier_matches RENAME TO two_view_geometries;", nullptr); } else { const std::string sql = "CREATE TABLE IF NOT EXISTS two_view_geometries" " (pair_id INTEGER PRIMARY KEY NOT NULL," " rows INTEGER NOT NULL," " cols INTEGER NOT NULL," " data BLOB," " config INTEGER NOT NULL," " F BLOB," " E BLOB," " H BLOB," " qvec BLOB," " tvec BLOB);"; SQLITE3_EXEC(database_, sql.c_str(), nullptr); } } void Database::UpdateSchema() const { if (!ExistsColumn("two_view_geometries", "F")) { SQLITE3_EXEC(database_, "ALTER TABLE two_view_geometries ADD COLUMN F BLOB;", nullptr); } if (!ExistsColumn("two_view_geometries", "E")) { SQLITE3_EXEC(database_, "ALTER TABLE two_view_geometries ADD COLUMN E BLOB;", nullptr); } if (!ExistsColumn("two_view_geometries", "H")) { SQLITE3_EXEC(database_, "ALTER TABLE two_view_geometries ADD COLUMN H BLOB;", nullptr); } if (!ExistsColumn("two_view_geometries", "qvec")) { SQLITE3_EXEC(database_, "ALTER TABLE two_view_geometries ADD COLUMN qvec BLOB;", nullptr); } if (!ExistsColumn("two_view_geometries", "tvec")) { SQLITE3_EXEC(database_, "ALTER TABLE two_view_geometries ADD COLUMN tvec BLOB;", nullptr); } // Update user version number. std::unique_lock lock(update_schema_mutex_); const std::string update_user_version_sql = StringPrintf("PRAGMA user_version = 3900;"); SQLITE3_EXEC(database_, update_user_version_sql.c_str(), nullptr); } bool Database::ExistsTable(const std::string& table_name) const { const std::string sql = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?;"; sqlite3_stmt* sql_stmt; SQLITE3_CALL(sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt, 0)); SQLITE3_CALL(sqlite3_bind_text(sql_stmt, 1, table_name.c_str(), static_cast(table_name.size()), SQLITE_STATIC)); const bool exists = SQLITE3_CALL(sqlite3_step(sql_stmt)) == SQLITE_ROW; SQLITE3_CALL(sqlite3_finalize(sql_stmt)); return exists; } bool Database::ExistsColumn(const std::string& table_name, const std::string& column_name) const { const std::string sql = StringPrintf("PRAGMA table_info(%s);", table_name.c_str()); sqlite3_stmt* sql_stmt; SQLITE3_CALL(sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt, 0)); bool exists_column = false; while (SQLITE3_CALL(sqlite3_step(sql_stmt)) == SQLITE_ROW) { const std::string result = reinterpret_cast(sqlite3_column_text(sql_stmt, 1)); if (column_name == result) { exists_column = true; break; } } SQLITE3_CALL(sqlite3_finalize(sql_stmt)); return exists_column; } bool Database::ExistsRowId(sqlite3_stmt* sql_stmt, const sqlite3_int64 row_id) const { SQLITE3_CALL( sqlite3_bind_int64(sql_stmt, 1, static_cast(row_id))); const bool exists = SQLITE3_CALL(sqlite3_step(sql_stmt)) == SQLITE_ROW; SQLITE3_CALL(sqlite3_reset(sql_stmt)); return exists; } bool Database::ExistsRowString(sqlite3_stmt* sql_stmt, const std::string& row_entry) const { SQLITE3_CALL(sqlite3_bind_text(sql_stmt, 1, row_entry.c_str(), static_cast(row_entry.size()), SQLITE_STATIC)); const bool exists = SQLITE3_CALL(sqlite3_step(sql_stmt)) == SQLITE_ROW; SQLITE3_CALL(sqlite3_reset(sql_stmt)); return exists; } size_t Database::CountRows(const std::string& table) const { const std::string sql = StringPrintf("SELECT COUNT(*) FROM %s;", table.c_str()); sqlite3_stmt* sql_stmt; SQLITE3_CALL(sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt, 0)); size_t count = 0; const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt)); if (rc == SQLITE_ROW) { count = static_cast(sqlite3_column_int64(sql_stmt, 0)); } SQLITE3_CALL(sqlite3_finalize(sql_stmt)); return count; } size_t Database::CountRowsForEntry(sqlite3_stmt* sql_stmt, const sqlite3_int64 row_id) const { SQLITE3_CALL(sqlite3_bind_int64(sql_stmt, 1, row_id)); size_t count = 0; const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt)); if (rc == SQLITE_ROW) { count = static_cast(sqlite3_column_int64(sql_stmt, 0)); } SQLITE3_CALL(sqlite3_reset(sql_stmt)); return count; } size_t Database::SumColumn(const std::string& column, const std::string& table) const { const std::string sql = StringPrintf("SELECT SUM(%s) FROM %s;", column.c_str(), table.c_str()); sqlite3_stmt* sql_stmt; SQLITE3_CALL(sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt, 0)); size_t sum = 0; const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt)); if (rc == SQLITE_ROW) { sum = static_cast(sqlite3_column_int64(sql_stmt, 0)); } SQLITE3_CALL(sqlite3_finalize(sql_stmt)); return sum; } size_t Database::MaxColumn(const std::string& column, const std::string& table) const { const std::string sql = StringPrintf("SELECT MAX(%s) FROM %s;", column.c_str(), table.c_str()); sqlite3_stmt* sql_stmt; SQLITE3_CALL(sqlite3_prepare_v2(database_, sql.c_str(), -1, &sql_stmt, 0)); size_t max = 0; const int rc = SQLITE3_CALL(sqlite3_step(sql_stmt)); if (rc == SQLITE_ROW) { max = static_cast(sqlite3_column_int64(sql_stmt, 0)); } SQLITE3_CALL(sqlite3_finalize(sql_stmt)); return max; } DatabaseTransaction::DatabaseTransaction(Database* database) : database_(database), database_lock_(database->transaction_mutex_) { CHECK_NOTNULL(database_); database_->BeginTransaction(); } DatabaseTransaction::~DatabaseTransaction() { database_->EndTransaction(); } } // namespace colmap colmap-3.9.1/src/colmap/scene/database.h000066400000000000000000000365251454702036400200730ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/types.h" #include "colmap/scene/camera.h" #include "colmap/scene/image.h" #include "colmap/scene/two_view_geometry.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include #include #include namespace colmap { // Database class to read and write images, features, cameras, matches, etc. // from a SQLite database. The class is not thread-safe and must not be accessed // concurrently. The class is optimized for single-thread speed and for optimal // performance, wrap multiple method calls inside a leading `BeginTransaction` // and trailing `EndTransaction`. class Database { public: const static int kSchemaVersion = 1; // The maximum number of images, that can be stored in the database. // This limitation arises due to the fact, that we generate unique IDs for // image pairs manually. Note: do not change this to // another type than `size_t`. const static size_t kMaxNumImages; // Can be used to construct temporary in-memory database. const static std::string kInMemoryDatabasePath; Database(); explicit Database(const std::string& path); ~Database(); // Open and close database. The same database should not be opened // concurrently in multiple threads or processes. void Open(const std::string& path); void Close(); // Check if entry already exists in database. For image pairs, the order of // `image_id1` and `image_id2` does not matter. bool ExistsCamera(camera_t camera_id) const; bool ExistsImage(image_t image_id) const; bool ExistsImageWithName(const std::string& name) const; bool ExistsKeypoints(image_t image_id) const; bool ExistsDescriptors(image_t image_id) const; bool ExistsMatches(image_t image_id1, image_t image_id2) const; bool ExistsInlierMatches(image_t image_id1, image_t image_id2) const; // Number of rows in `cameras` table. size_t NumCameras() const; // Number of rows in `images` table. size_t NumImages() const; // Sum of `rows` column in `keypoints` table, i.e. number of total keypoints. size_t NumKeypoints() const; // The number of keypoints for the image with most features. size_t MaxNumKeypoints() const; // Number of descriptors for specific image. size_t NumKeypointsForImage(image_t image_id) const; // Sum of `rows` column in `descriptors` table, // i.e. number of total descriptors. size_t NumDescriptors() const; // The number of descriptors for the image with most features. size_t MaxNumDescriptors() const; // Number of descriptors for specific image. size_t NumDescriptorsForImage(image_t image_id) const; // Sum of `rows` column in `matches` table, i.e. number of total matches. size_t NumMatches() const; // Sum of `rows` column in `two_view_geometries` table, // i.e. number of total inlier matches. size_t NumInlierMatches() const; // Number of rows in `matches` table. size_t NumMatchedImagePairs() const; // Number of rows in `two_view_geometries` table. size_t NumVerifiedImagePairs() const; // Each image pair is assigned an unique ID in the `matches` and // `two_view_geometries` table. We intentionally avoid to store the pairs in a // separate table by using e.g. AUTOINCREMENT, since the overhead of querying // the unique pair ID is significant. inline static image_pair_t ImagePairToPairId(image_t image_id1, image_t image_id2); inline static void PairIdToImagePair(image_pair_t pair_id, image_t* image_id1, image_t* image_id2); // Return true if image pairs should be swapped. Used to enforce a specific // image order to generate unique image pair identifiers independent of the // order in which the image identifiers are used. inline static bool SwapImagePair(image_t image_id1, image_t image_id2); // Read an existing entry in the database. The user is responsible for making // sure that the entry actually exists. For image pairs, the order of // `image_id1` and `image_id2` does not matter. Camera ReadCamera(camera_t camera_id) const; std::vector ReadAllCameras() const; Image ReadImage(image_t image_id) const; Image ReadImageWithName(const std::string& name) const; std::vector ReadAllImages() const; FeatureKeypoints ReadKeypoints(image_t image_id) const; FeatureDescriptors ReadDescriptors(image_t image_id) const; FeatureMatches ReadMatches(image_t image_id1, image_t image_id2) const; std::vector> ReadAllMatches() const; TwoViewGeometry ReadTwoViewGeometry(image_t image_id1, image_t image_id2) const; void ReadTwoViewGeometries( std::vector* image_pair_ids, std::vector* two_view_geometries) const; // Read all image pairs that have an entry in the `NumVerifiedImagePairs` // table with at least one inlier match and their number of inlier matches. void ReadTwoViewGeometryNumInliers( std::vector>* image_pairs, std::vector* num_inliers) const; // Add new camera and return its database identifier. If `use_camera_id` // is false a new identifier is automatically generated. camera_t WriteCamera(const Camera& camera, bool use_camera_id = false) const; // Add new image and return its database identifier. If `use_image_id` // is false a new identifier is automatically generated. image_t WriteImage(const Image& image, bool use_image_id = false) const; // Write a new entry in the database. The user is responsible for making sure // that the entry does not yet exist. For image pairs, the order of // `image_id1` and `image_id2` does not matter. void WriteKeypoints(image_t image_id, const FeatureKeypoints& keypoints) const; void WriteDescriptors(image_t image_id, const FeatureDescriptors& descriptors) const; void WriteMatches(image_t image_id1, image_t image_id2, const FeatureMatches& matches) const; void WriteTwoViewGeometry(image_t image_id1, image_t image_id2, const TwoViewGeometry& two_view_geometry) const; // Update an existing camera in the database. The user is responsible for // making sure that the entry already exists. void UpdateCamera(const Camera& camera) const; // Update an existing image in the database. The user is responsible for // making sure that the entry already exists. void UpdateImage(const Image& image) const; // Delete matches of an image pair. void DeleteMatches(image_t image_id1, image_t image_id2) const; // Delete inlier matches of an image pair. void DeleteInlierMatches(image_t image_id1, image_t image_id2) const; // Clear all database tables void ClearAllTables() const; // Clear the entire cameras table void ClearCameras() const; // Clear the entire images, keypoints, and descriptors tables void ClearImages() const; // Clear the entire descriptors table void ClearDescriptors() const; // Clear the entire keypoints table void ClearKeypoints() const; // Clear the entire matches table. void ClearMatches() const; // Clear the entire inlier matches table. void ClearTwoViewGeometries() const; // Merge two databases into a single, new database. static void Merge(const Database& database1, const Database& database2, Database* merged_database); private: friend class DatabaseTransaction; // Combine multiple queries into one transaction by wrapping a code section // into a `BeginTransaction` and `EndTransaction`. You can create a scoped // transaction with `DatabaseTransaction` that ends when the transaction // object is destructed. Combining queries results in faster transaction time // due to reduced locking of the database etc. void BeginTransaction() const; void EndTransaction() const; // Prepare SQL statements once at construction of the database, and reuse // the statements for multiple queries by resetting their states. void PrepareSQLStatements(); void FinalizeSQLStatements(); // Create database tables, if not existing, called when opening a database. void CreateTables() const; void CreateCameraTable() const; void CreateImageTable() const; void CreateKeypointsTable() const; void CreateDescriptorsTable() const; void CreateMatchesTable() const; void CreateTwoViewGeometriesTable() const; void UpdateSchema() const; bool ExistsTable(const std::string& table_name) const; bool ExistsColumn(const std::string& table_name, const std::string& column_name) const; bool ExistsRowId(sqlite3_stmt* sql_stmt, sqlite3_int64 row_id) const; bool ExistsRowString(sqlite3_stmt* sql_stmt, const std::string& row_entry) const; size_t CountRows(const std::string& table) const; size_t CountRowsForEntry(sqlite3_stmt* sql_stmt, sqlite3_int64 row_id) const; size_t SumColumn(const std::string& column, const std::string& table) const; size_t MaxColumn(const std::string& column, const std::string& table) const; sqlite3* database_ = nullptr; // Check if elements got removed from the database to only apply // the VACUUM command in such case mutable bool database_cleared_ = false; // Ensure that only one database object at a time updates the schema of a // database. Since the schema is updated every time a database is opened, this // is to ensure that there are no race conditions ("database locked" error // messages) when the user actually only intends to read from the database, // which requires to open it. static std::mutex update_schema_mutex_; // Used to ensure that only one transaction is active at the same time. std::mutex transaction_mutex_; // A collection of all `sqlite3_stmt` objects for deletion in the destructor. std::vector sql_stmts_; // num_* sqlite3_stmt* sql_stmt_num_keypoints_ = nullptr; sqlite3_stmt* sql_stmt_num_descriptors_ = nullptr; // exists_* sqlite3_stmt* sql_stmt_exists_camera_ = nullptr; sqlite3_stmt* sql_stmt_exists_image_id_ = nullptr; sqlite3_stmt* sql_stmt_exists_image_name_ = nullptr; sqlite3_stmt* sql_stmt_exists_keypoints_ = nullptr; sqlite3_stmt* sql_stmt_exists_descriptors_ = nullptr; sqlite3_stmt* sql_stmt_exists_matches_ = nullptr; sqlite3_stmt* sql_stmt_exists_two_view_geometry_ = nullptr; // add_* sqlite3_stmt* sql_stmt_add_camera_ = nullptr; sqlite3_stmt* sql_stmt_add_image_ = nullptr; // update_* sqlite3_stmt* sql_stmt_update_camera_ = nullptr; sqlite3_stmt* sql_stmt_update_image_ = nullptr; // read_* sqlite3_stmt* sql_stmt_read_camera_ = nullptr; sqlite3_stmt* sql_stmt_read_cameras_ = nullptr; sqlite3_stmt* sql_stmt_read_image_id_ = nullptr; sqlite3_stmt* sql_stmt_read_image_name_ = nullptr; sqlite3_stmt* sql_stmt_read_images_ = nullptr; sqlite3_stmt* sql_stmt_read_keypoints_ = nullptr; sqlite3_stmt* sql_stmt_read_descriptors_ = nullptr; sqlite3_stmt* sql_stmt_read_matches_ = nullptr; sqlite3_stmt* sql_stmt_read_matches_all_ = nullptr; sqlite3_stmt* sql_stmt_read_two_view_geometry_ = nullptr; sqlite3_stmt* sql_stmt_read_two_view_geometries_ = nullptr; sqlite3_stmt* sql_stmt_read_two_view_geometry_num_inliers_ = nullptr; // write_* sqlite3_stmt* sql_stmt_write_keypoints_ = nullptr; sqlite3_stmt* sql_stmt_write_descriptors_ = nullptr; sqlite3_stmt* sql_stmt_write_matches_ = nullptr; sqlite3_stmt* sql_stmt_write_two_view_geometry_ = nullptr; // delete_* sqlite3_stmt* sql_stmt_delete_matches_ = nullptr; sqlite3_stmt* sql_stmt_delete_two_view_geometry_ = nullptr; // clear_* sqlite3_stmt* sql_stmt_clear_cameras_ = nullptr; sqlite3_stmt* sql_stmt_clear_images_ = nullptr; sqlite3_stmt* sql_stmt_clear_descriptors_ = nullptr; sqlite3_stmt* sql_stmt_clear_keypoints_ = nullptr; sqlite3_stmt* sql_stmt_clear_matches_ = nullptr; sqlite3_stmt* sql_stmt_clear_two_view_geometries_ = nullptr; }; // This class automatically manages the scope of a database transaction by // calling `BeginTransaction` and `EndTransaction` during construction and // destruction, respectively. class DatabaseTransaction { public: explicit DatabaseTransaction(Database* database); ~DatabaseTransaction(); private: NON_COPYABLE(DatabaseTransaction) NON_MOVABLE(DatabaseTransaction) Database* database_; std::unique_lock database_lock_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// image_pair_t Database::ImagePairToPairId(const image_t image_id1, const image_t image_id2) { CHECK_LT(image_id1, kMaxNumImages); CHECK_LT(image_id2, kMaxNumImages); if (SwapImagePair(image_id1, image_id2)) { return static_cast(kMaxNumImages) * image_id2 + image_id1; } else { return static_cast(kMaxNumImages) * image_id1 + image_id2; } } void Database::PairIdToImagePair(const image_pair_t pair_id, image_t* image_id1, image_t* image_id2) { *image_id2 = static_cast(pair_id % kMaxNumImages); *image_id1 = static_cast((pair_id - *image_id2) / kMaxNumImages); CHECK_LT(*image_id1, kMaxNumImages); CHECK_LT(*image_id2, kMaxNumImages); } // Return true if image pairs should be swapped. Used to enforce a specific // image order to generate unique image pair identifiers independent of the // order in which the image identifiers are used. bool Database::SwapImagePair(const image_t image_id1, const image_t image_id2) { return image_id1 > image_id2; } } // namespace colmap colmap-3.9.1/src/colmap/scene/database_cache.cc000066400000000000000000000167141454702036400213520ustar00rootroot00000000000000// 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. #include "colmap/scene/database_cache.h" #include "colmap/feature/utils.h" #include "colmap/util/string.h" #include "colmap/util/timer.h" #include namespace colmap { std::shared_ptr DatabaseCache::Create( const Database& database, const size_t min_num_matches, const bool ignore_watermarks, const std::unordered_set& image_names) { auto cache = std::make_shared(); ////////////////////////////////////////////////////////////////////////////// // Load cameras ////////////////////////////////////////////////////////////////////////////// Timer timer; timer.Start(); LOG(INFO) << "Loading cameras..."; { std::vector cameras = database.ReadAllCameras(); cache->cameras_.reserve(cameras.size()); for (auto& camera : cameras) { cache->cameras_.emplace(camera.camera_id, std::move(camera)); } } LOG(INFO) << StringPrintf( " %d in %.3fs", cache->cameras_.size(), timer.ElapsedSeconds()); ////////////////////////////////////////////////////////////////////////////// // Load matches ////////////////////////////////////////////////////////////////////////////// timer.Restart(); LOG(INFO) << "Loading matches..."; std::vector image_pair_ids; std::vector two_view_geometries; database.ReadTwoViewGeometries(&image_pair_ids, &two_view_geometries); LOG(INFO) << StringPrintf( " %d in %.3fs", image_pair_ids.size(), timer.ElapsedSeconds()); auto UseInlierMatchesCheck = [min_num_matches, ignore_watermarks]( const TwoViewGeometry& two_view_geometry) { return static_cast(two_view_geometry.inlier_matches.size()) >= min_num_matches && (!ignore_watermarks || two_view_geometry.config != TwoViewGeometry::WATERMARK); }; ////////////////////////////////////////////////////////////////////////////// // Load images ////////////////////////////////////////////////////////////////////////////// timer.Restart(); LOG(INFO) << "Loading images..."; std::unordered_set image_ids; { std::vector images = database.ReadAllImages(); const size_t num_images = images.size(); // Determines for which images data should be loaded. if (image_names.empty()) { for (const auto& image : images) { image_ids.insert(image.ImageId()); } } else { for (const auto& image : images) { if (image_names.count(image.Name()) > 0) { image_ids.insert(image.ImageId()); } } } // Collect all images that are connected in the correspondence graph. std::unordered_set connected_image_ids; connected_image_ids.reserve(image_ids.size()); for (size_t i = 0; i < image_pair_ids.size(); ++i) { if (UseInlierMatchesCheck(two_view_geometries[i])) { image_t image_id1; image_t image_id2; Database::PairIdToImagePair(image_pair_ids[i], &image_id1, &image_id2); if (image_ids.count(image_id1) > 0 && image_ids.count(image_id2) > 0) { connected_image_ids.insert(image_id1); connected_image_ids.insert(image_id2); } } } // Load images with correspondences and discard images without // correspondences, as those images are useless for SfM. cache->images_.reserve(connected_image_ids.size()); for (auto& image : images) { const image_t image_id = image.ImageId(); if (image_ids.count(image_id) > 0 && connected_image_ids.count(image_id) > 0) { image.SetPoints2D( FeatureKeypointsToPointsVector(database.ReadKeypoints(image_id))); cache->images_.emplace(image_id, std::move(image)); } } LOG(INFO) << StringPrintf(" %d in %.3fs (connected %d)", num_images, timer.ElapsedSeconds(), connected_image_ids.size()); } ////////////////////////////////////////////////////////////////////////////// // Build correspondence graph ////////////////////////////////////////////////////////////////////////////// timer.Restart(); LOG(INFO) << "Building correspondence graph..."; cache->correspondence_graph_ = std::make_shared(); for (const auto& image : cache->images_) { cache->correspondence_graph_->AddImage(image.first, image.second.NumPoints2D()); } size_t num_ignored_image_pairs = 0; for (size_t i = 0; i < image_pair_ids.size(); ++i) { if (UseInlierMatchesCheck(two_view_geometries[i])) { image_t image_id1; image_t image_id2; Database::PairIdToImagePair(image_pair_ids[i], &image_id1, &image_id2); if (image_ids.count(image_id1) > 0 && image_ids.count(image_id2) > 0) { cache->correspondence_graph_->AddCorrespondences( image_id1, image_id2, two_view_geometries[i].inlier_matches); } else { num_ignored_image_pairs += 1; } } else { num_ignored_image_pairs += 1; } } cache->correspondence_graph_->Finalize(); // Set number of observations and correspondences per image. for (auto& image : cache->images_) { image.second.SetNumObservations( cache->correspondence_graph_->NumObservationsForImage(image.first)); image.second.SetNumCorrespondences( cache->correspondence_graph_->NumCorrespondencesForImage(image.first)); } LOG(INFO) << StringPrintf(" in %.3fs (ignored %d)", timer.ElapsedSeconds(), num_ignored_image_pairs); return cache; } const class Image* DatabaseCache::FindImageWithName( const std::string& name) const { for (const auto& image : images_) { if (image.second.Name() == name) { return &image.second; } } return nullptr; } } // namespace colmap colmap-3.9.1/src/colmap/scene/database_cache.h000066400000000000000000000124571454702036400212140ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/camera.h" #include "colmap/scene/correspondence_graph.h" #include "colmap/scene/database.h" #include "colmap/scene/image.h" #include "colmap/sensor/models.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include #include #include #include namespace colmap { // A class that caches the contents of the database in memory, used to quickly // create new reconstruction instances when multiple models are reconstructed. class DatabaseCache { public: // Load cameras, images, features, and matches from database. // // @param database Source database from which to load data. // @param min_num_matches Only load image pairs with a minimum number // of matches. // @param ignore_watermarks Whether to ignore watermark image pairs. // @param image_names Whether to use only load the data for a subset // of the images. All images are used if empty. static std::shared_ptr Create( const Database& database, size_t min_num_matches, bool ignore_watermarks, const std::unordered_set& image_names); // Get number of objects. inline size_t NumCameras() const; inline size_t NumImages() const; // Get specific objects. inline struct Camera& Camera(camera_t camera_id); inline const struct Camera& Camera(camera_t camera_id) const; inline class Image& Image(image_t image_id); inline const class Image& Image(image_t image_id) const; // Get all objects. inline const std::unordered_map& Cameras() const; inline const std::unordered_map& Images() const; // Check whether specific object exists. inline bool ExistsCamera(camera_t camera_id) const; inline bool ExistsImage(image_t image_id) const; // Get reference to const correspondence graph. inline std::shared_ptr CorrespondenceGraph() const; // Find specific image by name. Note that this uses linear search. const class Image* FindImageWithName(const std::string& name) const; private: std::shared_ptr correspondence_graph_; std::unordered_map cameras_; std::unordered_map images_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// size_t DatabaseCache::NumCameras() const { return cameras_.size(); } size_t DatabaseCache::NumImages() const { return images_.size(); } struct Camera& DatabaseCache::Camera(const camera_t camera_id) { return cameras_.at(camera_id); } const struct Camera& DatabaseCache::Camera(const camera_t camera_id) const { return cameras_.at(camera_id); } class Image& DatabaseCache::Image(const image_t image_id) { return images_.at(image_id); } const class Image& DatabaseCache::Image(const image_t image_id) const { return images_.at(image_id); } const std::unordered_map& DatabaseCache::Cameras() const { return cameras_; } const std::unordered_map& DatabaseCache::Images() const { return images_; } bool DatabaseCache::ExistsCamera(const camera_t camera_id) const { return cameras_.find(camera_id) != cameras_.end(); } bool DatabaseCache::ExistsImage(const image_t image_id) const { return images_.find(image_id) != images_.end(); } std::shared_ptr DatabaseCache::CorrespondenceGraph() const { return correspondence_graph_; } } // namespace colmap colmap-3.9.1/src/colmap/scene/database_cache_test.cc000066400000000000000000000102611454702036400224000ustar00rootroot00000000000000// 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. #include "colmap/scene/database_cache.h" #include namespace colmap { namespace { TEST(DatabaseCache, Empty) { DatabaseCache cache; EXPECT_EQ(cache.NumCameras(), 0); EXPECT_EQ(cache.NumImages(), 0); } TEST(DatabaseCache, Nominal) { Database database(Database::kInMemoryDatabasePath); const Camera camera = Camera::CreateFromModelId( kInvalidCameraId, SimplePinholeCameraModel::model_id, 1, 1, 1); const camera_t camera_id = database.WriteCamera(camera); Image image1; image1.SetName("image1"); image1.SetCameraId(camera_id); Image image2; image2.SetName("image2"); image2.SetCameraId(camera_id); const image_t image_id1 = database.WriteImage(image1); const image_t image_id2 = database.WriteImage(image2); database.WriteKeypoints(image_id1, FeatureKeypoints(10)); database.WriteKeypoints(image_id2, FeatureKeypoints(5)); TwoViewGeometry two_view_geometry; two_view_geometry.inlier_matches = {{0, 1}}; two_view_geometry.config = TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC; two_view_geometry.F = Eigen::Matrix3d::Random(); two_view_geometry.E = Eigen::Matrix3d::Random(); two_view_geometry.H = Eigen::Matrix3d::Random(); two_view_geometry.cam2_from_cam1 = Rigid3d(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); database.WriteTwoViewGeometry(image_id1, image_id2, two_view_geometry); auto cache = DatabaseCache::Create(database, /*min_num_matches=*/0, /*ignore_watermarks=*/false, /*image_names=*/{}); EXPECT_EQ(cache->NumCameras(), 1); EXPECT_EQ(cache->NumImages(), 2); EXPECT_TRUE(cache->ExistsCamera(camera_id)); EXPECT_EQ(cache->Camera(camera_id).model_id, camera.model_id); EXPECT_TRUE(cache->ExistsImage(image_id1)); EXPECT_TRUE(cache->ExistsImage(image_id2)); EXPECT_EQ(cache->Image(image_id1).NumPoints2D(), 10); EXPECT_EQ(cache->Image(image_id2).NumPoints2D(), 5); const auto correspondence_graph = cache->CorrespondenceGraph(); EXPECT_TRUE(cache->CorrespondenceGraph()->ExistsImage(image_id1)); EXPECT_EQ(cache->CorrespondenceGraph()->NumCorrespondencesForImage(image_id1), 1); EXPECT_EQ(cache->CorrespondenceGraph()->NumObservationsForImage(image_id1), 1); EXPECT_TRUE(cache->CorrespondenceGraph()->ExistsImage(image_id2)); EXPECT_EQ(cache->CorrespondenceGraph()->NumCorrespondencesForImage(image_id2), 1); EXPECT_EQ(cache->CorrespondenceGraph()->NumObservationsForImage(image_id2), 1); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/database_test.cc000066400000000000000000000503531454702036400212630ustar00rootroot00000000000000// 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. #include "colmap/scene/database.h" #include "colmap/geometry/pose.h" #include "colmap/util/eigen_alignment.h" #include #include #include namespace colmap { namespace { TEST(Database, OpenCloseConstructorDestructor) { Database database(Database::kInMemoryDatabasePath); } TEST(Database, OpenClose) { Database database(Database::kInMemoryDatabasePath); database.Close(); } TEST(Database, Transaction) { Database database(Database::kInMemoryDatabasePath); DatabaseTransaction database_transaction(&database); } TEST(Database, TransactionMultiThreaded) { Database database(Database::kInMemoryDatabasePath); std::thread thread1([&database]() { DatabaseTransaction database_transaction(&database); std::this_thread::sleep_for(std::chrono::milliseconds(100)); }); std::thread thread2([&database]() { DatabaseTransaction database_transaction(&database); std::this_thread::sleep_for(std::chrono::milliseconds(100)); }); thread1.join(); thread2.join(); } TEST(Database, Empty) { Database database(Database::kInMemoryDatabasePath); EXPECT_EQ(database.NumCameras(), 0); EXPECT_EQ(database.NumImages(), 0); EXPECT_EQ(database.NumKeypoints(), 0); EXPECT_EQ(database.MaxNumKeypoints(), 0); EXPECT_EQ(database.NumDescriptors(), 0); EXPECT_EQ(database.MaxNumDescriptors(), 0); EXPECT_EQ(database.NumMatches(), 0); EXPECT_EQ(database.NumMatchedImagePairs(), 0); EXPECT_EQ(database.NumVerifiedImagePairs(), 0); } TEST(Database, ImagePairToPairId) { EXPECT_EQ(Database::ImagePairToPairId(0, 0), 0); EXPECT_EQ(Database::ImagePairToPairId(0, 1), 1); EXPECT_EQ(Database::ImagePairToPairId(0, 2), 2); EXPECT_EQ(Database::ImagePairToPairId(0, 3), 3); EXPECT_EQ(Database::ImagePairToPairId(1, 2), Database::kMaxNumImages + 2); for (image_t i = 0; i < 20; ++i) { for (image_t j = 0; j < 20; ++j) { const image_pair_t pair_id = Database::ImagePairToPairId(i, j); image_t image_id1; image_t image_id2; Database::PairIdToImagePair(pair_id, &image_id1, &image_id2); if (i < j) { EXPECT_EQ(i, image_id1); EXPECT_EQ(j, image_id2); } else { EXPECT_EQ(i, image_id2); EXPECT_EQ(j, image_id1); } } } } TEST(Database, SwapImagePair) { EXPECT_FALSE(Database::SwapImagePair(0, 0)); EXPECT_FALSE(Database::SwapImagePair(0, 1)); EXPECT_TRUE(Database::SwapImagePair(1, 0)); EXPECT_FALSE(Database::SwapImagePair(1, 1)); } TEST(Database, Camera) { Database database(Database::kInMemoryDatabasePath); EXPECT_EQ(database.NumCameras(), 0); Camera camera = Camera::CreateFromModelName( kInvalidCameraId, "SIMPLE_PINHOLE", 1.0, 1, 1); camera.camera_id = database.WriteCamera(camera); EXPECT_EQ(database.NumCameras(), 1); EXPECT_TRUE(database.ExistsCamera(camera.camera_id)); EXPECT_EQ(database.ReadCamera(camera.camera_id).camera_id, camera.camera_id); EXPECT_EQ(database.ReadCamera(camera.camera_id).model_id, camera.model_id); EXPECT_EQ(database.ReadCamera(camera.camera_id).FocalLength(), camera.FocalLength()); EXPECT_EQ(database.ReadCamera(camera.camera_id).PrincipalPointX(), camera.PrincipalPointX()); EXPECT_EQ(database.ReadCamera(camera.camera_id).PrincipalPointY(), camera.PrincipalPointY()); camera.SetFocalLength(2.0); database.UpdateCamera(camera); EXPECT_EQ(database.ReadCamera(camera.camera_id).FocalLength(), camera.FocalLength()); Camera camera2 = camera; camera2.camera_id = camera.camera_id + 1; database.WriteCamera(camera2, true); EXPECT_EQ(database.NumCameras(), 2); EXPECT_TRUE(database.ExistsCamera(camera.camera_id)); EXPECT_TRUE(database.ExistsCamera(camera2.camera_id)); EXPECT_EQ(database.ReadAllCameras().size(), 2); EXPECT_EQ(database.ReadAllCameras()[0].camera_id, camera.camera_id); EXPECT_EQ(database.ReadAllCameras()[1].camera_id, camera2.camera_id); database.ClearCameras(); EXPECT_EQ(database.NumCameras(), 0); } TEST(Database, Image) { Database database(Database::kInMemoryDatabasePath); Camera camera = Camera::CreateFromModelName( kInvalidCameraId, "SIMPLE_PINHOLE", 1.0, 1, 1); camera.camera_id = database.WriteCamera(camera); EXPECT_EQ(database.NumImages(), 0); Image image; image.SetName("test"); image.SetCameraId(camera.camera_id); image.CamFromWorldPrior() = Rigid3d(Eigen::Quaterniond(0.1, 0.2, 0.3, 0.4), Eigen::Vector3d(0.1, 0.2, 0.3)); image.SetImageId(database.WriteImage(image)); EXPECT_EQ(database.NumImages(), 1); EXPECT_TRUE(database.ExistsImage(image.ImageId())); auto read_image = database.ReadImage(image.ImageId()); EXPECT_EQ(read_image.ImageId(), image.ImageId()); EXPECT_EQ(read_image.CameraId(), image.CameraId()); EXPECT_EQ(read_image.CamFromWorldPrior().rotation.coeffs(), image.CamFromWorldPrior().rotation.coeffs()); EXPECT_EQ(read_image.CamFromWorldPrior().translation, image.CamFromWorldPrior().translation); image.CamFromWorldPrior().translation.x() += 2; database.UpdateImage(image); read_image = database.ReadImage(image.ImageId()); EXPECT_EQ(read_image.ImageId(), image.ImageId()); EXPECT_EQ(read_image.CameraId(), image.CameraId()); EXPECT_EQ(read_image.CamFromWorldPrior().rotation.coeffs(), image.CamFromWorldPrior().rotation.coeffs()); EXPECT_EQ(read_image.CamFromWorldPrior().translation, image.CamFromWorldPrior().translation); Image image2 = image; image2.SetName("test2"); image2.SetImageId(image.ImageId() + 1); database.WriteImage(image2, true); EXPECT_EQ(database.NumImages(), 2); EXPECT_TRUE(database.ExistsImage(image.ImageId())); EXPECT_TRUE(database.ExistsImage(image2.ImageId())); EXPECT_EQ(database.ReadAllImages().size(), 2); database.ClearImages(); EXPECT_EQ(database.NumImages(), 0); } TEST(Database, Keypoints) { Database database(Database::kInMemoryDatabasePath); Camera camera; camera.camera_id = database.WriteCamera(camera); Image image; image.SetName("test"); image.SetCameraId(camera.camera_id); image.SetImageId(database.WriteImage(image)); EXPECT_EQ(database.NumKeypoints(), 0); EXPECT_EQ(database.NumKeypointsForImage(image.ImageId()), 0); const FeatureKeypoints keypoints = FeatureKeypoints(10); database.WriteKeypoints(image.ImageId(), keypoints); const FeatureKeypoints keypoints_read = database.ReadKeypoints(image.ImageId()); EXPECT_EQ(keypoints.size(), keypoints_read.size()); for (size_t i = 0; i < keypoints.size(); ++i) { EXPECT_EQ(keypoints[i].x, keypoints_read[i].x); EXPECT_EQ(keypoints[i].y, keypoints_read[i].y); EXPECT_EQ(keypoints[i].a11, keypoints_read[i].a11); EXPECT_EQ(keypoints[i].a12, keypoints_read[i].a12); EXPECT_EQ(keypoints[i].a21, keypoints_read[i].a21); EXPECT_EQ(keypoints[i].a22, keypoints_read[i].a22); } EXPECT_EQ(database.NumKeypoints(), 10); EXPECT_EQ(database.MaxNumKeypoints(), 10); EXPECT_EQ(database.NumKeypointsForImage(image.ImageId()), 10); const FeatureKeypoints keypoints2 = FeatureKeypoints(20); image.SetName("test2"); image.SetImageId(database.WriteImage(image)); database.WriteKeypoints(image.ImageId(), keypoints2); EXPECT_EQ(database.NumKeypoints(), 30); EXPECT_EQ(database.MaxNumKeypoints(), 20); EXPECT_EQ(database.NumKeypointsForImage(image.ImageId()), 20); database.ClearKeypoints(); EXPECT_EQ(database.NumKeypoints(), 0); EXPECT_EQ(database.MaxNumKeypoints(), 0); EXPECT_EQ(database.NumKeypointsForImage(image.ImageId()), 0); } TEST(Database, Descriptors) { Database database(Database::kInMemoryDatabasePath); Camera camera; camera.camera_id = database.WriteCamera(camera); Image image; image.SetName("test"); image.SetCameraId(camera.camera_id); image.SetImageId(database.WriteImage(image)); EXPECT_EQ(database.NumDescriptors(), 0); EXPECT_EQ(database.NumDescriptorsForImage(image.ImageId()), 0); const FeatureDescriptors descriptors = FeatureDescriptors::Random(10, 128); database.WriteDescriptors(image.ImageId(), descriptors); const FeatureDescriptors descriptors_read = database.ReadDescriptors(image.ImageId()); EXPECT_EQ(descriptors.rows(), descriptors_read.rows()); EXPECT_EQ(descriptors.cols(), descriptors_read.cols()); for (FeatureDescriptors::Index r = 0; r < descriptors.rows(); ++r) { for (FeatureDescriptors::Index c = 0; c < descriptors.cols(); ++c) { EXPECT_EQ(descriptors(r, c), descriptors_read(r, c)); } } EXPECT_EQ(database.NumDescriptors(), 10); EXPECT_EQ(database.MaxNumDescriptors(), 10); EXPECT_EQ(database.NumDescriptorsForImage(image.ImageId()), 10); const FeatureDescriptors descriptors2 = FeatureDescriptors(20, 128); image.SetName("test2"); image.SetImageId(database.WriteImage(image)); database.WriteDescriptors(image.ImageId(), descriptors2); EXPECT_EQ(database.NumDescriptors(), 30); EXPECT_EQ(database.MaxNumDescriptors(), 20); EXPECT_EQ(database.NumDescriptorsForImage(image.ImageId()), 20); database.ClearDescriptors(); EXPECT_EQ(database.NumDescriptors(), 0); EXPECT_EQ(database.MaxNumDescriptors(), 0); EXPECT_EQ(database.NumDescriptorsForImage(image.ImageId()), 0); } TEST(Database, Matches) { Database database(Database::kInMemoryDatabasePath); const image_t image_id1 = 1; const image_t image_id2 = 2; const FeatureMatches matches = FeatureMatches(1000); database.WriteMatches(image_id1, image_id2, matches); const FeatureMatches matches_read = database.ReadMatches(image_id1, image_id2); EXPECT_EQ(matches.size(), matches_read.size()); for (size_t i = 0; i < matches.size(); ++i) { EXPECT_EQ(matches[i].point2D_idx1, matches_read[i].point2D_idx1); EXPECT_EQ(matches[i].point2D_idx2, matches_read[i].point2D_idx2); } EXPECT_EQ(database.ReadAllMatches().size(), 1); EXPECT_EQ(database.ReadAllMatches()[0].first, Database::ImagePairToPairId(image_id1, image_id2)); EXPECT_EQ(database.NumMatches(), 1000); database.DeleteMatches(image_id1, image_id2); EXPECT_EQ(database.NumMatches(), 0); database.WriteMatches(image_id1, image_id2, matches); EXPECT_EQ(database.NumMatches(), 1000); database.ClearMatches(); EXPECT_EQ(database.NumMatches(), 0); } TEST(Database, TwoViewGeometry) { Database database(Database::kInMemoryDatabasePath); const image_t image_id1 = 1; const image_t image_id2 = 2; TwoViewGeometry two_view_geometry; two_view_geometry.inlier_matches = FeatureMatches(1000); two_view_geometry.config = TwoViewGeometry::ConfigurationType::PLANAR_OR_PANORAMIC; two_view_geometry.F = Eigen::Matrix3d::Random(); two_view_geometry.E = Eigen::Matrix3d::Random(); two_view_geometry.H = Eigen::Matrix3d::Random(); two_view_geometry.cam2_from_cam1 = Rigid3d(Eigen::Quaterniond::UnitRandom(), Eigen::Vector3d::Random()); database.WriteTwoViewGeometry(image_id1, image_id2, two_view_geometry); const TwoViewGeometry two_view_geometry_read = database.ReadTwoViewGeometry(image_id1, image_id2); EXPECT_EQ(two_view_geometry.inlier_matches.size(), two_view_geometry_read.inlier_matches.size()); for (size_t i = 0; i < two_view_geometry_read.inlier_matches.size(); ++i) { EXPECT_EQ(two_view_geometry.inlier_matches[i].point2D_idx1, two_view_geometry_read.inlier_matches[i].point2D_idx1); EXPECT_EQ(two_view_geometry.inlier_matches[i].point2D_idx2, two_view_geometry_read.inlier_matches[i].point2D_idx2); } EXPECT_EQ(two_view_geometry.config, two_view_geometry_read.config); EXPECT_EQ(two_view_geometry.F, two_view_geometry_read.F); EXPECT_EQ(two_view_geometry.E, two_view_geometry_read.E); EXPECT_EQ(two_view_geometry.H, two_view_geometry_read.H); EXPECT_EQ(two_view_geometry.cam2_from_cam1.rotation.coeffs(), two_view_geometry_read.cam2_from_cam1.rotation.coeffs()); EXPECT_EQ(two_view_geometry.cam2_from_cam1.translation, two_view_geometry_read.cam2_from_cam1.translation); const TwoViewGeometry two_view_geometry_read_inv = database.ReadTwoViewGeometry(image_id2, image_id1); EXPECT_EQ(two_view_geometry_read_inv.inlier_matches.size(), two_view_geometry_read.inlier_matches.size()); for (size_t i = 0; i < two_view_geometry_read.inlier_matches.size(); ++i) { EXPECT_EQ(two_view_geometry_read_inv.inlier_matches[i].point2D_idx2, two_view_geometry_read.inlier_matches[i].point2D_idx1); EXPECT_EQ(two_view_geometry_read_inv.inlier_matches[i].point2D_idx1, two_view_geometry_read.inlier_matches[i].point2D_idx2); } EXPECT_EQ(two_view_geometry_read_inv.config, two_view_geometry_read.config); EXPECT_EQ(two_view_geometry_read_inv.F.transpose(), two_view_geometry_read.F); EXPECT_EQ(two_view_geometry_read_inv.E.transpose(), two_view_geometry_read.E); EXPECT_TRUE(two_view_geometry_read_inv.H.inverse().eval().isApprox( two_view_geometry_read.H)); EXPECT_TRUE(two_view_geometry_read_inv.cam2_from_cam1.rotation.isApprox( Inverse(two_view_geometry_read.cam2_from_cam1).rotation)); EXPECT_TRUE(two_view_geometry_read_inv.cam2_from_cam1.translation.isApprox( Inverse(two_view_geometry_read.cam2_from_cam1).translation)); std::vector image_pair_ids; std::vector two_view_geometries; database.ReadTwoViewGeometries(&image_pair_ids, &two_view_geometries); EXPECT_EQ(image_pair_ids.size(), 1); EXPECT_EQ(two_view_geometries.size(), 1); EXPECT_EQ(image_pair_ids[0], Database::ImagePairToPairId(image_id1, image_id2)); EXPECT_EQ(two_view_geometry.config, two_view_geometries[0].config); EXPECT_EQ(two_view_geometry.F, two_view_geometries[0].F); EXPECT_EQ(two_view_geometry.E, two_view_geometries[0].E); EXPECT_EQ(two_view_geometry.H, two_view_geometries[0].H); EXPECT_EQ(two_view_geometry.cam2_from_cam1.rotation.coeffs(), two_view_geometries[0].cam2_from_cam1.rotation.coeffs()); EXPECT_EQ(two_view_geometry.cam2_from_cam1.translation, two_view_geometries[0].cam2_from_cam1.translation); EXPECT_EQ(two_view_geometry.inlier_matches.size(), two_view_geometries[0].inlier_matches.size()); std::vector> image_pairs; std::vector num_inliers; database.ReadTwoViewGeometryNumInliers(&image_pairs, &num_inliers); EXPECT_EQ(image_pairs.size(), 1); EXPECT_EQ(num_inliers.size(), 1); EXPECT_EQ(image_pairs[0].first, image_id1); EXPECT_EQ(image_pairs[0].second, image_id2); EXPECT_EQ(num_inliers[0], two_view_geometry.inlier_matches.size()); EXPECT_EQ(database.NumInlierMatches(), 1000); database.DeleteInlierMatches(image_id1, image_id2); EXPECT_EQ(database.NumInlierMatches(), 0); database.WriteTwoViewGeometry(image_id1, image_id2, two_view_geometry); EXPECT_EQ(database.NumInlierMatches(), 1000); database.ClearTwoViewGeometries(); EXPECT_EQ(database.NumInlierMatches(), 0); } TEST(Database, Merge) { Database database1(Database::kInMemoryDatabasePath); Database database2(Database::kInMemoryDatabasePath); Camera camera = Camera::CreateFromModelName( kInvalidCameraId, "SIMPLE_PINHOLE", 1.0, 1, 1); camera.camera_id = database1.WriteCamera(camera); camera.camera_id = database2.WriteCamera(camera); Image image; image.SetCameraId(camera.camera_id); image.CamFromWorldPrior() = Rigid3d(Eigen::Quaterniond(0.1, 0.2, 0.3, 0.4), Eigen::Vector3d(0.1, 0.2, 0.3)); image.SetName("test1"); const image_t image_id1 = database1.WriteImage(image); image.SetName("test2"); const image_t image_id2 = database1.WriteImage(image); image.SetName("test3"); const image_t image_id3 = database2.WriteImage(image); image.SetName("test4"); const image_t image_id4 = database2.WriteImage(image); auto keypoints1 = FeatureKeypoints(10); keypoints1[0].x = 100; auto keypoints2 = FeatureKeypoints(20); keypoints2[0].x = 200; auto keypoints3 = FeatureKeypoints(30); keypoints3[0].x = 300; auto keypoints4 = FeatureKeypoints(40); keypoints4[0].x = 400; const auto descriptors1 = FeatureDescriptors::Random(10, 128); const auto descriptors2 = FeatureDescriptors::Random(20, 128); const auto descriptors3 = FeatureDescriptors::Random(30, 128); const auto descriptors4 = FeatureDescriptors::Random(40, 128); database1.WriteKeypoints(image_id1, keypoints1); database1.WriteKeypoints(image_id2, keypoints2); database2.WriteKeypoints(image_id3, keypoints3); database2.WriteKeypoints(image_id4, keypoints4); database1.WriteDescriptors(image_id1, descriptors1); database1.WriteDescriptors(image_id2, descriptors2); database2.WriteDescriptors(image_id3, descriptors3); database2.WriteDescriptors(image_id4, descriptors4); database1.WriteMatches(image_id1, image_id2, FeatureMatches(10)); database2.WriteMatches(image_id3, image_id4, FeatureMatches(10)); database1.WriteTwoViewGeometry(image_id1, image_id2, TwoViewGeometry()); database2.WriteTwoViewGeometry(image_id3, image_id4, TwoViewGeometry()); Database merged_database(Database::kInMemoryDatabasePath); Database::Merge(database1, database2, &merged_database); EXPECT_EQ(merged_database.NumCameras(), 2); EXPECT_EQ(merged_database.NumImages(), 4); EXPECT_EQ(merged_database.NumKeypoints(), 100); EXPECT_EQ(merged_database.NumDescriptors(), 100); EXPECT_EQ(merged_database.NumMatches(), 20); EXPECT_EQ(merged_database.NumInlierMatches(), 0); EXPECT_EQ(merged_database.ReadAllImages()[0].CameraId(), 1); EXPECT_EQ(merged_database.ReadAllImages()[1].CameraId(), 1); EXPECT_EQ(merged_database.ReadAllImages()[2].CameraId(), 2); EXPECT_EQ(merged_database.ReadAllImages()[3].CameraId(), 2); EXPECT_EQ(merged_database.ReadKeypoints(1).size(), 10); EXPECT_EQ(merged_database.ReadKeypoints(2).size(), 20); EXPECT_EQ(merged_database.ReadKeypoints(3).size(), 30); EXPECT_EQ(merged_database.ReadKeypoints(4).size(), 40); EXPECT_EQ(merged_database.ReadKeypoints(1)[0].x, 100); EXPECT_EQ(merged_database.ReadKeypoints(2)[0].x, 200); EXPECT_EQ(merged_database.ReadKeypoints(3)[0].x, 300); EXPECT_EQ(merged_database.ReadKeypoints(4)[0].x, 400); EXPECT_EQ(merged_database.ReadDescriptors(1).size(), descriptors1.size()); EXPECT_EQ(merged_database.ReadDescriptors(2).size(), descriptors2.size()); EXPECT_EQ(merged_database.ReadDescriptors(3).size(), descriptors3.size()); EXPECT_EQ(merged_database.ReadDescriptors(4).size(), descriptors4.size()); EXPECT_TRUE(merged_database.ExistsMatches(1, 2)); EXPECT_FALSE(merged_database.ExistsMatches(2, 3)); EXPECT_FALSE(merged_database.ExistsMatches(2, 4)); EXPECT_TRUE(merged_database.ExistsMatches(3, 4)); merged_database.ClearAllTables(); EXPECT_EQ(merged_database.NumCameras(), 0); EXPECT_EQ(merged_database.NumImages(), 0); EXPECT_EQ(merged_database.NumKeypoints(), 0); EXPECT_EQ(merged_database.NumDescriptors(), 0); EXPECT_EQ(merged_database.NumMatches(), 0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/image.cc000066400000000000000000000121551454702036400175400ustar00rootroot00000000000000// 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. #include "colmap/scene/image.h" #include "colmap/geometry/pose.h" #include "colmap/scene/projection.h" namespace colmap { namespace { static constexpr double kNaN = std::numeric_limits::quiet_NaN(); } // namespace const int Image::kNumPoint3DVisibilityPyramidLevels = 6; Image::Image() : image_id_(kInvalidImageId), name_(""), camera_id_(kInvalidCameraId), registered_(false), num_points3D_(0), num_observations_(0), num_correspondences_(0), num_visible_points3D_(0), cam_from_world_prior_(Eigen::Quaterniond(kNaN, kNaN, kNaN, kNaN), Eigen::Vector3d(kNaN, kNaN, kNaN)) {} void Image::SetUp(const struct Camera& camera) { CHECK_EQ(camera_id_, camera.camera_id); point3D_visibility_pyramid_ = VisibilityPyramid( kNumPoint3DVisibilityPyramidLevels, camera.width, camera.height); } void Image::TearDown() { point3D_visibility_pyramid_ = VisibilityPyramid(0, 0, 0); } void Image::SetPoints2D(const std::vector& points) { CHECK(points2D_.empty()); points2D_.resize(points.size()); num_correspondences_have_point3D_.resize(points.size(), 0); for (point2D_t point2D_idx = 0; point2D_idx < points.size(); ++point2D_idx) { points2D_[point2D_idx].xy = points[point2D_idx]; } } void Image::SetPoints2D(const std::vector& points) { CHECK(points2D_.empty()); points2D_ = points; num_correspondences_have_point3D_.resize(points.size(), 0); num_points3D_ = 0; for (const auto& point2D : points2D_) { if (point2D.HasPoint3D()) { num_points3D_ += 1; } } } void Image::SetPoint3DForPoint2D(const point2D_t point2D_idx, const point3D_t point3D_id) { CHECK_NE(point3D_id, kInvalidPoint3DId); struct Point2D& point2D = points2D_.at(point2D_idx); if (!point2D.HasPoint3D()) { num_points3D_ += 1; } point2D.point3D_id = point3D_id; } void Image::ResetPoint3DForPoint2D(const point2D_t point2D_idx) { struct Point2D& point2D = points2D_.at(point2D_idx); if (point2D.HasPoint3D()) { point2D.point3D_id = kInvalidPoint3DId; num_points3D_ -= 1; } } bool Image::HasPoint3D(const point3D_t point3D_id) const { return std::find_if(points2D_.begin(), points2D_.end(), [point3D_id](const struct Point2D& point2D) { return point2D.point3D_id == point3D_id; }) != points2D_.end(); } void Image::IncrementCorrespondenceHasPoint3D(const point2D_t point2D_idx) { const struct Point2D& point2D = points2D_.at(point2D_idx); num_correspondences_have_point3D_[point2D_idx] += 1; if (num_correspondences_have_point3D_[point2D_idx] == 1) { num_visible_points3D_ += 1; } point3D_visibility_pyramid_.SetPoint(point2D.xy(0), point2D.xy(1)); assert(num_visible_points3D_ <= num_observations_); } void Image::DecrementCorrespondenceHasPoint3D(const point2D_t point2D_idx) { const struct Point2D& point2D = points2D_.at(point2D_idx); num_correspondences_have_point3D_[point2D_idx] -= 1; if (num_correspondences_have_point3D_[point2D_idx] == 0) { num_visible_points3D_ -= 1; } point3D_visibility_pyramid_.ResetPoint(point2D.xy(0), point2D.xy(1)); assert(num_visible_points3D_ <= num_observations_); } Eigen::Vector3d Image::ProjectionCenter() const { return cam_from_world_.rotation.inverse() * -cam_from_world_.translation; } Eigen::Vector3d Image::ViewingDirection() const { return cam_from_world_.rotation.toRotationMatrix().row(2); } } // namespace colmap colmap-3.9.1/src/colmap/scene/image.h000066400000000000000000000247631454702036400174120ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/math/math.h" #include "colmap/scene/camera.h" #include "colmap/scene/point2d.h" #include "colmap/scene/visibility_pyramid.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include "colmap/util/types.h" #include #include #include namespace colmap { // Class that holds information about an image. An image is the product of one // camera shot at a certain location (parameterized as the pose). An image may // share a camera with multiple other images, if its intrinsics are the same. class Image { public: Image(); // Setup / tear down the image and necessary internal data structures before // and after being used in reconstruction. void SetUp(const Camera& camera); void TearDown(); // Access the unique identifier of the image. inline image_t ImageId() const; inline void SetImageId(image_t image_id); // Access the name of the image. inline const std::string& Name() const; inline std::string& Name(); inline void SetName(const std::string& name); // Access the unique identifier of the camera. Note that multiple images // might share the same camera. inline camera_t CameraId() const; inline void SetCameraId(camera_t camera_id); // Check whether identifier of camera has been set. inline bool HasCamera() const; // Check if image is registered. inline bool IsRegistered() const; inline void SetRegistered(bool registered); // Get the number of image points. inline point2D_t NumPoints2D() const; // Get the number of triangulations, i.e. the number of points that // are part of a 3D point track. inline point2D_t NumPoints3D() const; // Get the number of observations, i.e. the number of image points that // have at least one correspondence to another image. inline point2D_t NumObservations() const; inline void SetNumObservations(point2D_t num_observations); // Get the number of correspondences for all image points. inline point2D_t NumCorrespondences() const; inline void SetNumCorrespondences(point2D_t num_observations); // Get the number of observations that see a triangulated point, i.e. the // number of image points that have at least one correspondence to a // triangulated point in another image. inline point2D_t NumVisiblePoints3D() const; // Get the score of triangulated observations. In contrast to // `NumVisiblePoints3D`, this score also captures the distribution // of triangulated observations in the image. This is useful to select // the next best image in incremental reconstruction, because a more // uniform distribution of observations results in more robust registration. inline size_t Point3DVisibilityScore() const; // World to camera pose. inline const Rigid3d& CamFromWorld() const; inline Rigid3d& CamFromWorld(); // World to camera pose prior, e.g. given by EXIF gyroscope tag. inline const Rigid3d& CamFromWorldPrior() const; inline Rigid3d& CamFromWorldPrior(); // Access the coordinates of image points. inline const struct Point2D& Point2D(point2D_t point2D_idx) const; inline struct Point2D& Point2D(point2D_t point2D_idx); inline const std::vector& Points2D() const; inline std::vector& Points2D(); void SetPoints2D(const std::vector& points); void SetPoints2D(const std::vector& points); // Set the point as triangulated, i.e. it is part of a 3D point track. void SetPoint3DForPoint2D(point2D_t point2D_idx, point3D_t point3D_id); // Set the point as not triangulated, i.e. it is not part of a 3D point track. void ResetPoint3DForPoint2D(point2D_t point2D_idx); // Check whether an image point has a correspondence to an image point in // another image that has a 3D point. inline bool IsPoint3DVisible(point2D_t point2D_idx) const; // Check whether one of the image points is part of the 3D point track. bool HasPoint3D(point3D_t point3D_id) const; // Indicate that another image has a point that is triangulated and has // a correspondence to this image point. Note that this must only be called // after calling `SetUp`. void IncrementCorrespondenceHasPoint3D(point2D_t point2D_idx); // Indicate that another image has a point that is not triangulated any more // and has a correspondence to this image point. This assumes that // `IncrementCorrespondenceHasPoint3D` was called for the same image point // and correspondence before. Note that this must only be called // after calling `SetUp`. void DecrementCorrespondenceHasPoint3D(point2D_t point2D_idx); // Extract the projection center in world space. Eigen::Vector3d ProjectionCenter() const; // Extract the viewing direction of the image. Eigen::Vector3d ViewingDirection() const; // The number of levels in the 3D point multi-resolution visibility pyramid. static const int kNumPoint3DVisibilityPyramidLevels; private: // Identifier of the image, if not specified `kInvalidImageId`. image_t image_id_; // The name of the image, i.e. the relative path. std::string name_; // The identifier of the associated camera. Note that multiple images might // share the same camera. If not specified `kInvalidCameraId`. camera_t camera_id_; // Whether the image is successfully registered in the reconstruction. bool registered_; // The number of 3D points the image observes, i.e. the sum of its `points2D` // where `point3D_id != kInvalidPoint3DId`. point2D_t num_points3D_; // The number of image points that have at least one correspondence to // another image. point2D_t num_observations_; // The sum of correspondences per image point. point2D_t num_correspondences_; // The number of 2D points, which have at least one corresponding 2D point in // another image that is part of a 3D point track, i.e. the sum of `points2D` // where `num_tris > 0`. point2D_t num_visible_points3D_; // The pose of the image, defined as the transformation from world to camera. Rigid3d cam_from_world_; // The pose prior of the image, e.g. extracted from EXIF tags. Rigid3d cam_from_world_prior_; // All image points, including points that are not part of a 3D point track. std::vector points2D_; // Per image point, the number of correspondences that have a 3D point. std::vector num_correspondences_have_point3D_; // Data structure to compute the distribution of triangulated correspondences // in the image. Note that this structure is only usable after `SetUp`. VisibilityPyramid point3D_visibility_pyramid_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// image_t Image::ImageId() const { return image_id_; } void Image::SetImageId(const image_t image_id) { image_id_ = image_id; } const std::string& Image::Name() const { return name_; } std::string& Image::Name() { return name_; } void Image::SetName(const std::string& name) { name_ = name; } inline camera_t Image::CameraId() const { return camera_id_; } inline void Image::SetCameraId(const camera_t camera_id) { CHECK_NE(camera_id, kInvalidCameraId); camera_id_ = camera_id; } inline bool Image::HasCamera() const { return camera_id_ != kInvalidCameraId; } bool Image::IsRegistered() const { return registered_; } void Image::SetRegistered(const bool registered) { registered_ = registered; } point2D_t Image::NumPoints2D() const { return static_cast(points2D_.size()); } point2D_t Image::NumPoints3D() const { return num_points3D_; } point2D_t Image::NumObservations() const { return num_observations_; } void Image::SetNumObservations(const point2D_t num_observations) { num_observations_ = num_observations; } point2D_t Image::NumCorrespondences() const { return num_correspondences_; } void Image::SetNumCorrespondences(const point2D_t num_correspondences) { num_correspondences_ = num_correspondences; } point2D_t Image::NumVisiblePoints3D() const { return num_visible_points3D_; } size_t Image::Point3DVisibilityScore() const { return point3D_visibility_pyramid_.Score(); } const Rigid3d& Image::CamFromWorld() const { return cam_from_world_; } Rigid3d& Image::CamFromWorld() { return cam_from_world_; } const Rigid3d& Image::CamFromWorldPrior() const { return cam_from_world_prior_; } Rigid3d& Image::CamFromWorldPrior() { return cam_from_world_prior_; } const struct Point2D& Image::Point2D(const point2D_t point2D_idx) const { return points2D_.at(point2D_idx); } struct Point2D& Image::Point2D(const point2D_t point2D_idx) { return points2D_.at(point2D_idx); } const std::vector& Image::Points2D() const { return points2D_; } std::vector& Image::Points2D() { return points2D_; } bool Image::IsPoint3DVisible(const point2D_t point2D_idx) const { return num_correspondences_have_point3D_.at(point2D_idx) > 0; } } // namespace colmap colmap-3.9.1/src/colmap/scene/image_test.cc000066400000000000000000000224571454702036400206050ustar00rootroot00000000000000// 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. #include "colmap/scene/image.h" #include namespace colmap { namespace { TEST(Image, Default) { Image image; EXPECT_EQ(image.ImageId(), kInvalidImageId); EXPECT_EQ(image.Name(), ""); EXPECT_EQ(image.CameraId(), kInvalidCameraId); EXPECT_FALSE(image.HasCamera()); EXPECT_FALSE(image.IsRegistered()); EXPECT_EQ(image.NumPoints2D(), 0); EXPECT_EQ(image.NumPoints3D(), 0); EXPECT_EQ(image.NumObservations(), 0); EXPECT_EQ(image.NumCorrespondences(), 0); EXPECT_EQ(image.NumVisiblePoints3D(), 0); EXPECT_EQ(image.Point3DVisibilityScore(), 0); EXPECT_EQ(image.CamFromWorld().rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(image.CamFromWorld().translation, Eigen::Vector3d::Zero()); EXPECT_TRUE( image.CamFromWorldPrior().rotation.coeffs().array().isNaN().all()); EXPECT_TRUE(image.CamFromWorldPrior().translation.array().isNaN().all()); EXPECT_EQ(image.Points2D().size(), 0); } TEST(Image, ImageId) { Image image; EXPECT_EQ(image.ImageId(), kInvalidImageId); image.SetImageId(1); EXPECT_EQ(image.ImageId(), 1); } TEST(Image, Name) { Image image; EXPECT_EQ(image.Name(), ""); image.SetName("test1"); EXPECT_EQ(image.Name(), "test1"); image.Name() = "test2"; EXPECT_EQ(image.Name(), "test2"); } TEST(Image, CameraId) { Image image; EXPECT_EQ(image.CameraId(), kInvalidCameraId); image.SetCameraId(1); EXPECT_EQ(image.CameraId(), 1); } TEST(Image, Registered) { Image image; EXPECT_FALSE(image.IsRegistered()); image.SetRegistered(true); EXPECT_TRUE(image.IsRegistered()); image.SetRegistered(false); EXPECT_FALSE(image.IsRegistered()); } TEST(Image, NumPoints2D) { Image image; EXPECT_EQ(image.NumPoints2D(), 0); image.SetPoints2D(std::vector(10)); EXPECT_EQ(image.NumPoints2D(), 10); } TEST(Image, NumPoints3D) { Image image; image.SetPoints2D(std::vector(10)); EXPECT_EQ(image.NumPoints3D(), 0); image.SetPoint3DForPoint2D(0, 0); EXPECT_EQ(image.NumPoints3D(), 1); image.SetPoint3DForPoint2D(0, 1); image.SetPoint3DForPoint2D(1, 2); EXPECT_EQ(image.NumPoints3D(), 2); } TEST(Image, NumObservations) { Image image; EXPECT_EQ(image.NumObservations(), 0); image.SetNumObservations(10); EXPECT_EQ(image.NumObservations(), 10); } TEST(Image, NumCorrespondences) { Image image; EXPECT_EQ(image.NumCorrespondences(), 0); image.SetNumCorrespondences(10); EXPECT_EQ(image.NumCorrespondences(), 10); } TEST(Image, NumVisiblePoints3D) { Image image; image.SetPoints2D(std::vector(10)); image.SetNumObservations(10); Camera camera; camera.width = 10; camera.height = 10; image.SetUp(camera); EXPECT_EQ(image.NumVisiblePoints3D(), 0); image.IncrementCorrespondenceHasPoint3D(0); EXPECT_EQ(image.NumVisiblePoints3D(), 1); image.IncrementCorrespondenceHasPoint3D(0); image.IncrementCorrespondenceHasPoint3D(1); EXPECT_EQ(image.NumVisiblePoints3D(), 2); image.DecrementCorrespondenceHasPoint3D(0); EXPECT_EQ(image.NumVisiblePoints3D(), 2); image.DecrementCorrespondenceHasPoint3D(0); EXPECT_EQ(image.NumVisiblePoints3D(), 1); image.DecrementCorrespondenceHasPoint3D(1); EXPECT_EQ(image.NumVisiblePoints3D(), 0); } TEST(Image, Point3DVisibilityScore) { Image image; std::vector points2D; for (size_t i = 0; i < 4; ++i) { for (size_t j = 0; j < 4; ++j) { points2D.emplace_back(i, j); } } image.SetPoints2D(points2D); image.SetNumObservations(16); Camera camera; camera.width = 4; camera.height = 4; image.SetUp(camera); Eigen::Matrix scores( image.kNumPoint3DVisibilityPyramidLevels, 1); for (int i = 1; i <= image.kNumPoint3DVisibilityPyramidLevels; ++i) { scores(i - 1) = (1 << i) * (1 << i); } EXPECT_EQ(image.Point3DVisibilityScore(), 0); image.IncrementCorrespondenceHasPoint3D(0); EXPECT_EQ(image.Point3DVisibilityScore(), scores.sum()); image.IncrementCorrespondenceHasPoint3D(0); EXPECT_EQ(image.Point3DVisibilityScore(), scores.sum()); image.IncrementCorrespondenceHasPoint3D(1); EXPECT_EQ(image.Point3DVisibilityScore(), scores.sum() + scores.bottomRows(scores.size() - 1).sum()); image.IncrementCorrespondenceHasPoint3D(1); image.IncrementCorrespondenceHasPoint3D(1); image.IncrementCorrespondenceHasPoint3D(4); EXPECT_EQ(image.Point3DVisibilityScore(), scores.sum() + 2 * scores.bottomRows(scores.size() - 1).sum()); image.IncrementCorrespondenceHasPoint3D(4); image.IncrementCorrespondenceHasPoint3D(5); EXPECT_EQ(image.Point3DVisibilityScore(), scores.sum() + 3 * scores.bottomRows(scores.size() - 1).sum()); image.DecrementCorrespondenceHasPoint3D(0); EXPECT_EQ(image.Point3DVisibilityScore(), scores.sum() + 3 * scores.bottomRows(scores.size() - 1).sum()); image.DecrementCorrespondenceHasPoint3D(0); EXPECT_EQ(image.Point3DVisibilityScore(), scores.sum() + 2 * scores.bottomRows(scores.size() - 1).sum()); image.IncrementCorrespondenceHasPoint3D(2); EXPECT_EQ(image.Point3DVisibilityScore(), 2 * scores.sum() + 2 * scores.bottomRows(scores.size() - 1).sum()); } TEST(Image, Points2D) { Image image; EXPECT_EQ(image.Points2D().size(), 0); std::vector points2D(10); points2D[0] = Eigen::Vector2d(1.0, 2.0); image.SetPoints2D(points2D); EXPECT_EQ(image.Points2D().size(), 10); EXPECT_EQ(image.Point2D(0).xy(0), 1.0); EXPECT_EQ(image.Point2D(0).xy(1), 2.0); EXPECT_EQ(image.NumPoints3D(), 0); } TEST(Image, Points2DWith3D) { Image image; EXPECT_EQ(image.Points2D().size(), 0); std::vector points2D(10); points2D[0].xy = Eigen::Vector2d(1.0, 2.0); points2D[0].point3D_id = 1; image.SetPoints2D(points2D); EXPECT_EQ(image.Points2D().size(), 10); EXPECT_EQ(image.Point2D(0).xy(0), 1.0); EXPECT_EQ(image.Point2D(0).xy(1), 2.0); EXPECT_EQ(image.NumPoints3D(), 1); } TEST(Image, Points3D) { Image image; image.SetPoints2D(std::vector(2)); EXPECT_FALSE(image.Point2D(0).HasPoint3D()); EXPECT_FALSE(image.Point2D(1).HasPoint3D()); EXPECT_EQ(image.NumPoints3D(), 0); image.SetPoint3DForPoint2D(0, 0); EXPECT_TRUE(image.Point2D(0).HasPoint3D()); EXPECT_FALSE(image.Point2D(1).HasPoint3D()); EXPECT_EQ(image.NumPoints3D(), 1); EXPECT_TRUE(image.HasPoint3D(0)); image.SetPoint3DForPoint2D(0, 1); EXPECT_TRUE(image.Point2D(0).HasPoint3D()); EXPECT_FALSE(image.Point2D(1).HasPoint3D()); EXPECT_EQ(image.NumPoints3D(), 1); EXPECT_FALSE(image.HasPoint3D(0)); EXPECT_TRUE(image.HasPoint3D(1)); image.SetPoint3DForPoint2D(1, 0); EXPECT_TRUE(image.Point2D(0).HasPoint3D()); EXPECT_TRUE(image.Point2D(1).HasPoint3D()); EXPECT_EQ(image.NumPoints3D(), 2); EXPECT_TRUE(image.HasPoint3D(0)); EXPECT_TRUE(image.HasPoint3D(1)); image.ResetPoint3DForPoint2D(0); EXPECT_FALSE(image.Point2D(0).HasPoint3D()); EXPECT_TRUE(image.Point2D(1).HasPoint3D()); EXPECT_EQ(image.NumPoints3D(), 1); EXPECT_TRUE(image.HasPoint3D(0)); EXPECT_FALSE(image.HasPoint3D(1)); image.ResetPoint3DForPoint2D(1); EXPECT_FALSE(image.Point2D(0).HasPoint3D()); EXPECT_FALSE(image.Point2D(1).HasPoint3D()); EXPECT_EQ(image.NumPoints3D(), 0); EXPECT_FALSE(image.HasPoint3D(0)); EXPECT_FALSE(image.HasPoint3D(1)); image.ResetPoint3DForPoint2D(0); EXPECT_FALSE(image.Point2D(0).HasPoint3D()); EXPECT_FALSE(image.Point2D(1).HasPoint3D()); EXPECT_EQ(image.NumPoints3D(), 0); EXPECT_FALSE(image.HasPoint3D(0)); EXPECT_FALSE(image.HasPoint3D(1)); } TEST(Image, ProjectionCenter) { Image image; EXPECT_EQ(image.ProjectionCenter(), Eigen::Vector3d::Zero()); } TEST(Image, ViewingDirection) { Image image; EXPECT_EQ(image.ViewingDirection(), Eigen::Vector3d(0, 0, 1)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/point2d.h000066400000000000000000000045531454702036400177020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include namespace colmap { // 2D point class corresponds to a feature in an image. It may or may not have a // corresponding 3D point if it is part of a triangulated track. struct Point2D { // The image coordinates in pixels, starting at upper left corner with 0. Eigen::Vector2d xy = Eigen::Vector2d::Zero(); // The identifier of the 3D point. If the 2D point is not part of a 3D point // track the identifier is `kInvalidPoint3DId` and `HasPoint3D() = false`. point3D_t point3D_id = kInvalidPoint3DId; // Determin whether the 2D point observes a 3D point. inline bool HasPoint3D() const { return point3D_id != kInvalidPoint3DId; } }; } // namespace colmap colmap-3.9.1/src/colmap/scene/point2d_test.cc000066400000000000000000000044211454702036400210710ustar00rootroot00000000000000// 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. #include "colmap/scene/point2d.h" #include namespace colmap { namespace { TEST(Point2D, Default) { Point2D point2D; EXPECT_EQ(point2D.xy, Eigen::Vector2d::Zero()); EXPECT_EQ(point2D.point3D_id, kInvalidPoint3DId); EXPECT_FALSE(point2D.HasPoint3D()); } TEST(Point2D, Point3DId) { Point2D point2D; EXPECT_EQ(point2D.point3D_id, kInvalidPoint3DId); EXPECT_FALSE(point2D.HasPoint3D()); point2D.point3D_id = 1; EXPECT_EQ(point2D.point3D_id, 1); EXPECT_TRUE(point2D.HasPoint3D()); point2D.point3D_id = kInvalidPoint3DId; EXPECT_EQ(point2D.point3D_id, kInvalidPoint3DId); EXPECT_FALSE(point2D.HasPoint3D()); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/point3d.h000066400000000000000000000044571454702036400177060ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/track.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include "colmap/util/types.h" #include #include namespace colmap { // 3D point class that holds information about triangulated 2D points. struct Point3D { // The 3D position of the point. Eigen::Vector3d xyz = Eigen::Vector3d::Zero(); // The color of the point in the range [0, 255]. Eigen::Vector3ub color = Eigen::Vector3ub::Zero(); // The mean reprojection error in pixels. double error = -1.; // The track of the point as a list of image observations. Track track; inline bool HasError() const { return error != -1.; } }; } // namespace colmap colmap-3.9.1/src/colmap/scene/point3d_test.cc000066400000000000000000000042701454702036400210740ustar00rootroot00000000000000// 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. #include "colmap/scene/point3d.h" #include namespace colmap { namespace { TEST(Point3D, Default) { Point3D point3D; EXPECT_EQ(point3D.xyz, Eigen::Vector3d::Zero()); EXPECT_EQ(point3D.color, Eigen::Vector3ub::Zero()); EXPECT_EQ(point3D.error, -1.0); EXPECT_FALSE(point3D.HasError()); EXPECT_EQ(point3D.track.Length(), 0); } TEST(Point3D, Error) { Point3D point3D; EXPECT_EQ(point3D.error, -1.0); EXPECT_FALSE(point3D.HasError()); point3D.error = 1.0; EXPECT_EQ(point3D.error, 1.0); EXPECT_TRUE(point3D.HasError()); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/projection.cc000066400000000000000000000113761454702036400206360ustar00rootroot00000000000000// 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. #include "colmap/scene/projection.h" #include "colmap/geometry/pose.h" #include "colmap/math/matrix.h" namespace colmap { double CalculateSquaredReprojectionError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Rigid3d& cam_from_world, const Camera& camera) { const Eigen::Vector3d point3D_in_cam = cam_from_world * point3D; // Check that point is infront of camera. if (point3D_in_cam.z() < std::numeric_limits::epsilon()) { return std::numeric_limits::max(); } return (camera.ImgFromCam(point3D_in_cam.hnormalized()) - point2D) .squaredNorm(); } double CalculateSquaredReprojectionError( const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Eigen::Matrix3x4d& cam_from_world, const Camera& camera) { const double proj_z = cam_from_world.row(2).dot(point3D.homogeneous()); // Check that point is infront of camera. if (proj_z < std::numeric_limits::epsilon()) { return std::numeric_limits::max(); } const double proj_x = cam_from_world.row(0).dot(point3D.homogeneous()); const double proj_y = cam_from_world.row(1).dot(point3D.homogeneous()); const double inv_proj_z = 1.0 / proj_z; const Eigen::Vector2d proj_point2D = camera.ImgFromCam( Eigen::Vector2d(inv_proj_z * proj_x, inv_proj_z * proj_y)); return (proj_point2D - point2D).squaredNorm(); } double CalculateAngularError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Rigid3d& cam_from_world, const Camera& camera) { return CalculateNormalizedAngularError( camera.CamFromImg(point2D), point3D, cam_from_world); } double CalculateAngularError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Eigen::Matrix3x4d& cam_from_world, const Camera& camera) { return CalculateNormalizedAngularError( camera.CamFromImg(point2D), point3D, cam_from_world); } double CalculateNormalizedAngularError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Rigid3d& cam_from_world) { const Eigen::Vector3d ray1 = point2D.homogeneous(); const Eigen::Vector3d ray2 = cam_from_world * point3D; return std::acos(ray1.normalized().transpose() * ray2.normalized()); } double CalculateNormalizedAngularError( const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Eigen::Matrix3x4d& cam_from_world) { const Eigen::Vector3d ray1 = point2D.homogeneous(); const Eigen::Vector3d ray2 = cam_from_world * point3D.homogeneous(); return std::acos(ray1.normalized().transpose() * ray2.normalized()); } bool HasPointPositiveDepth(const Eigen::Matrix3x4d& cam_from_world, const Eigen::Vector3d& point3D) { return cam_from_world.row(2).dot(point3D.homogeneous()) >= std::numeric_limits::epsilon(); } } // namespace colmap colmap-3.9.1/src/colmap/scene/projection.h000066400000000000000000000103371454702036400204740ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/rigid3.h" #include "colmap/scene/camera.h" #include "colmap/util/eigen_alignment.h" #include #include #include #include namespace colmap { // Calculate the reprojection error. // // The reprojection error is the Euclidean distance between the observation // in the image and the projection of the 3D point into the image. If the // 3D point is behind the camera, then this function returns DBL_MAX. double CalculateSquaredReprojectionError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Rigid3d& cam_from_world, const Camera& camera); double CalculateSquaredReprojectionError( const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Eigen::Matrix3x4d& cam_from_world, const Camera& camera); // Calculate the angular error. // // The angular error is the angle between the observed viewing ray and the // actual viewing ray from the camera center to the 3D point. double CalculateAngularError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Rigid3d& cam_from_world, const Camera& camera); double CalculateAngularError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Eigen::Matrix3x4d& cam_from_world, const Camera& camera); // Calculate angulate error using normalized image points. // // The angular error is the angle between the observed viewing ray and the // actual viewing ray from the camera center to the 3D point. double CalculateNormalizedAngularError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Rigid3d& cam_from_world); double CalculateNormalizedAngularError(const Eigen::Vector2d& point2D, const Eigen::Vector3d& point3D, const Eigen::Matrix3x4d& cam_from_world); // Check if 3D point passes cheirality constraint, // i.e. it lies in front of the camera and not in the image plane. // // @param cam_from_world 3x4 projection matrix. // @param point3D 3D point as 3x1 vector. // // @return True if point lies in front of camera. bool HasPointPositiveDepth(const Eigen::Matrix3x4d& cam_from_world, const Eigen::Vector3d& point3D); } // namespace colmap colmap-3.9.1/src/colmap/scene/projection_test.cc000066400000000000000000000166651454702036400217030ustar00rootroot00000000000000// 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. #include "colmap/scene/projection.h" #include "colmap/geometry/pose.h" #include "colmap/math/math.h" #include "colmap/sensor/models.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(CalculateSquaredReprojectionError, Nominal) { const Rigid3d cam_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); const Eigen::Matrix3x4d cam_from_world_mat = cam_from_world.ToMatrix(); const Eigen::Vector3d point3D = Eigen::Vector3d::Random().cwiseAbs(); const Eigen::Vector3d point2D_h = cam_from_world_mat * point3D.homogeneous(); const Eigen::Vector2d point2D = point2D_h.hnormalized(); Camera camera = Camera::CreateFromModelId(1, SimplePinholeCameraModel::model_id, 1, 0, 0); EXPECT_NEAR(CalculateSquaredReprojectionError( point2D, point3D, cam_from_world, camera), 0, 1e-6); EXPECT_NEAR(CalculateSquaredReprojectionError( point2D, point3D, cam_from_world_mat, camera), 0, 1e-6); EXPECT_NEAR(CalculateSquaredReprojectionError( point2D.array() + 1, point3D, cam_from_world, camera), 2, 1e-6); EXPECT_NEAR(CalculateSquaredReprojectionError( point2D.array() + 1, point3D, cam_from_world_mat, camera), 2, 1e-6); } TEST(CalculateAngularError, Nominal) { const Rigid3d cam_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); const Eigen::Matrix3x4d cam_from_world_mat = cam_from_world.ToMatrix(); Camera camera; camera.model_id = SimplePinholeCameraModel::model_id; camera.params = {1, 0, 0}; const double error1 = CalculateAngularError(Eigen::Vector2d(0, 0), Eigen::Vector3d(0, 0, 1), cam_from_world_mat, camera); EXPECT_NEAR(error1, 0, 1e-6); const double error2 = CalculateAngularError(Eigen::Vector2d(0, 0), Eigen::Vector3d(0, 1, 1), cam_from_world_mat, camera); EXPECT_NEAR(error2, M_PI / 4, 1e-6); const double error3 = CalculateAngularError(Eigen::Vector2d(0, 0), Eigen::Vector3d(0, 5, 5), cam_from_world_mat, camera); EXPECT_NEAR(error3, M_PI / 4, 1e-6); const double error4 = CalculateAngularError(Eigen::Vector2d(1, 0), Eigen::Vector3d(0, 0, 1), cam_from_world_mat, camera); EXPECT_NEAR(error4, M_PI / 4, 1e-6); const double error5 = CalculateAngularError(Eigen::Vector2d(2, 0), Eigen::Vector3d(0, 0, 1), cam_from_world_mat, camera); EXPECT_NEAR(error5, 1.10714872, 1e-6); const double error6 = CalculateAngularError(Eigen::Vector2d(2, 0), Eigen::Vector3d(1, 0, 1), cam_from_world_mat, camera); EXPECT_NEAR(error6, 1.10714872 - M_PI / 4, 1e-6); const double error7 = CalculateAngularError(Eigen::Vector2d(2, 0), Eigen::Vector3d(5, 0, 5), cam_from_world_mat, camera); EXPECT_NEAR(error7, 1.10714872 - M_PI / 4, 1e-6); const double error8 = CalculateAngularError(Eigen::Vector2d(1, 0), Eigen::Vector3d(-1, 0, 1), cam_from_world_mat, camera); EXPECT_NEAR(error8, M_PI / 2, 1e-6); const double error9 = CalculateAngularError(Eigen::Vector2d(1, 0), Eigen::Vector3d(-1, 0, 0), cam_from_world_mat, camera); EXPECT_NEAR(error9, M_PI * 3 / 4, 1e-6); const double error10 = CalculateAngularError(Eigen::Vector2d(1, 0), Eigen::Vector3d(-1, 0, -1), cam_from_world_mat, camera); EXPECT_NEAR(error10, M_PI, 1e-6); const double error11 = CalculateAngularError(Eigen::Vector2d(1, 0), Eigen::Vector3d(0, 0, -1), cam_from_world_mat, camera); EXPECT_NEAR(error11, M_PI * 3 / 4, 1e-6); } TEST(HasPointPositiveDepth, Nominal) { const Rigid3d cam_from_world(Eigen::Quaterniond::Identity(), Eigen::Vector3d::Zero()); const Eigen::Matrix3x4d cam_from_world_mat = cam_from_world.ToMatrix(); // In the image plane const bool check1 = HasPointPositiveDepth(cam_from_world_mat, Eigen::Vector3d(0, 0, 0)); EXPECT_FALSE(check1); const bool check2 = HasPointPositiveDepth(cam_from_world_mat, Eigen::Vector3d(0, 2, 0)); EXPECT_FALSE(check2); // Infront of camera const bool check3 = HasPointPositiveDepth(cam_from_world_mat, Eigen::Vector3d(0, 0, 1)); EXPECT_TRUE(check3); // Behind camera const bool check4 = HasPointPositiveDepth(cam_from_world_mat, Eigen::Vector3d(0, 0, -1)); EXPECT_FALSE(check4); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/reconstruction.cc000066400000000000000000002126271454702036400215450ustar00rootroot00000000000000// 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. #include "colmap/scene/reconstruction.h" #include "colmap/geometry/gps.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/triangulation.h" #include "colmap/scene/database_cache.h" #include "colmap/scene/projection.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/misc.h" #include "colmap/util/ply.h" #include namespace colmap { Reconstruction::Reconstruction() : correspondence_graph_(nullptr), num_added_points3D_(0) {} std::unordered_set Reconstruction::Point3DIds() const { std::unordered_set point3D_ids; point3D_ids.reserve(points3D_.size()); for (const auto& point3D : points3D_) { point3D_ids.insert(point3D.first); } return point3D_ids; } void Reconstruction::Load(const DatabaseCache& database_cache) { correspondence_graph_ = nullptr; // Add cameras. cameras_.reserve(database_cache.NumCameras()); for (const auto& camera : database_cache.Cameras()) { if (!ExistsCamera(camera.first)) { AddCamera(camera.second); } // Else: camera was added before, e.g. with `ReadAllCameras`. } // Add images. images_.reserve(database_cache.NumImages()); for (const auto& image : database_cache.Images()) { if (ExistsImage(image.second.ImageId())) { class Image& existing_image = Image(image.second.ImageId()); CHECK_EQ(existing_image.Name(), image.second.Name()); if (existing_image.NumPoints2D() == 0) { existing_image.SetPoints2D(image.second.Points2D()); } else { CHECK_EQ(image.second.NumPoints2D(), existing_image.NumPoints2D()); } existing_image.SetNumObservations(image.second.NumObservations()); existing_image.SetNumCorrespondences(image.second.NumCorrespondences()); } else { AddImage(image.second); } } // Add image pairs. for (const auto& image_pair : database_cache.CorrespondenceGraph() ->NumCorrespondencesBetweenImages()) { ImagePairStat image_pair_stat; image_pair_stat.num_total_corrs = image_pair.second; image_pair_stats_.emplace(image_pair.first, image_pair_stat); } } void Reconstruction::SetUp( std::shared_ptr correspondence_graph) { correspondence_graph_ = std::move(CHECK_NOTNULL(correspondence_graph)); for (auto& image : images_) { image.second.SetUp(Camera(image.second.CameraId())); } // If an existing model was loaded from disk and there were already images // registered previously, we need to set observations as triangulated. for (const auto image_id : reg_image_ids_) { const class Image& image = Image(image_id); for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { if (image.Point2D(point2D_idx).HasPoint3D()) { const bool kIsContinuedPoint3D = false; SetObservationAsTriangulated( image_id, point2D_idx, kIsContinuedPoint3D); } } } } void Reconstruction::TearDown() { correspondence_graph_ = nullptr; image_pair_stats_.clear(); // Remove all not yet registered images. std::unordered_set keep_camera_ids; for (auto it = images_.begin(); it != images_.end();) { if (it->second.IsRegistered()) { keep_camera_ids.insert(it->second.CameraId()); it->second.TearDown(); ++it; } else { it = images_.erase(it); } } // Remove all unused cameras. for (auto it = cameras_.begin(); it != cameras_.end();) { if (keep_camera_ids.count(it->first) == 0) { it = cameras_.erase(it); } else { ++it; } } // Compress tracks. for (auto& point3D : points3D_) { point3D.second.track.Compress(); } } void Reconstruction::AddCamera(struct Camera camera) { const camera_t camera_id = camera.camera_id; CHECK(camera.VerifyParams()); CHECK(cameras_.emplace(camera_id, std::move(camera)).second); } void Reconstruction::AddImage(class Image image) { const image_t image_id = image.ImageId(); const bool is_registered = image.IsRegistered(); CHECK(images_.emplace(image_id, std::move(image)).second); if (is_registered) { reg_image_ids_.push_back(image_id); } } point3D_t Reconstruction::AddPoint3D(const Eigen::Vector3d& xyz, Track track, const Eigen::Vector3ub& color) { const point3D_t point3D_id = ++num_added_points3D_; CHECK(!ExistsPoint3D(point3D_id)); for (const auto& track_el : track.Elements()) { class Image& image = Image(track_el.image_id); CHECK(!image.Point2D(track_el.point2D_idx).HasPoint3D()); image.SetPoint3DForPoint2D(track_el.point2D_idx, point3D_id); CHECK_LE(image.NumPoints3D(), image.NumPoints2D()); } const bool kIsContinuedPoint3D = false; for (const auto& track_el : track.Elements()) { SetObservationAsTriangulated( track_el.image_id, track_el.point2D_idx, kIsContinuedPoint3D); } struct Point3D& point3D = points3D_[point3D_id]; point3D.xyz = xyz; point3D.track = std::move(track); point3D.color = color; return point3D_id; } void Reconstruction::AddObservation(const point3D_t point3D_id, const TrackElement& track_el) { class Image& image = Image(track_el.image_id); CHECK(!image.Point2D(track_el.point2D_idx).HasPoint3D()); image.SetPoint3DForPoint2D(track_el.point2D_idx, point3D_id); CHECK_LE(image.NumPoints3D(), image.NumPoints2D()); struct Point3D& point3D = Point3D(point3D_id); point3D.track.AddElement(track_el); const bool kIsContinuedPoint3D = true; SetObservationAsTriangulated( track_el.image_id, track_el.point2D_idx, kIsContinuedPoint3D); } point3D_t Reconstruction::MergePoints3D(const point3D_t point3D_id1, const point3D_t point3D_id2) { const struct Point3D& point3D1 = Point3D(point3D_id1); const struct Point3D& point3D2 = Point3D(point3D_id2); const Eigen::Vector3d merged_xyz = (point3D1.track.Length() * point3D1.xyz + point3D2.track.Length() * point3D2.xyz) / (point3D1.track.Length() + point3D2.track.Length()); const Eigen::Vector3d merged_rgb = (point3D1.track.Length() * point3D1.color.cast() + point3D2.track.Length() * point3D2.color.cast()) / (point3D1.track.Length() + point3D2.track.Length()); Track merged_track; merged_track.Reserve(point3D1.track.Length() + point3D2.track.Length()); merged_track.AddElements(point3D1.track.Elements()); merged_track.AddElements(point3D2.track.Elements()); DeletePoint3D(point3D_id1); DeletePoint3D(point3D_id2); const point3D_t merged_point3D_id = AddPoint3D(merged_xyz, merged_track, merged_rgb.cast()); return merged_point3D_id; } void Reconstruction::DeletePoint3D(const point3D_t point3D_id) { // Note: Do not change order of these instructions, especially with respect to // `Reconstruction::ResetTriObservations` const class Track& track = Point3D(point3D_id).track; const bool kIsDeletedPoint3D = true; for (const auto& track_el : track.Elements()) { ResetTriObservations( track_el.image_id, track_el.point2D_idx, kIsDeletedPoint3D); } for (const auto& track_el : track.Elements()) { class Image& image = Image(track_el.image_id); image.ResetPoint3DForPoint2D(track_el.point2D_idx); } points3D_.erase(point3D_id); } void Reconstruction::DeleteObservation(const image_t image_id, const point2D_t point2D_idx) { // Note: Do not change order of these instructions, especially with respect to // `Reconstruction::ResetTriObservations` class Image& image = Image(image_id); const point3D_t point3D_id = image.Point2D(point2D_idx).point3D_id; struct Point3D& point3D = Point3D(point3D_id); if (point3D.track.Length() <= 2) { DeletePoint3D(point3D_id); return; } point3D.track.DeleteElement(image_id, point2D_idx); const bool kIsDeletedPoint3D = false; ResetTriObservations(image_id, point2D_idx, kIsDeletedPoint3D); image.ResetPoint3DForPoint2D(point2D_idx); } void Reconstruction::DeleteAllPoints2DAndPoints3D() { points3D_.clear(); for (auto& image : images_) { class Image new_image; new_image.SetImageId(image.second.ImageId()); new_image.SetName(image.second.Name()); new_image.SetCameraId(image.second.CameraId()); new_image.SetRegistered(image.second.IsRegistered()); new_image.SetNumCorrespondences(image.second.NumCorrespondences()); new_image.CamFromWorld() = image.second.CamFromWorld(); new_image.CamFromWorldPrior() = image.second.CamFromWorldPrior(); image.second = std::move(new_image); } } void Reconstruction::RegisterImage(const image_t image_id) { class Image& image = Image(image_id); if (!image.IsRegistered()) { image.SetRegistered(true); reg_image_ids_.push_back(image_id); } } void Reconstruction::DeRegisterImage(const image_t image_id) { class Image& image = Image(image_id); const auto num_points2D = image.NumPoints2D(); for (point2D_t point2D_idx = 0; point2D_idx < num_points2D; ++point2D_idx) { if (image.Point2D(point2D_idx).HasPoint3D()) { DeleteObservation(image_id, point2D_idx); } } image.SetRegistered(false); reg_image_ids_.erase( std::remove(reg_image_ids_.begin(), reg_image_ids_.end(), image_id), reg_image_ids_.end()); } void Reconstruction::Normalize(const double extent, const double p0, const double p1, const bool use_images) { CHECK_GT(extent, 0); if ((use_images && reg_image_ids_.size() < 2) || (!use_images && points3D_.size() < 2)) { return; } auto bound = ComputeBoundsAndCentroid(p0, p1, use_images); // Calculate scale and translation, such that // translation is applied before scaling. const double old_extent = (std::get<1>(bound) - std::get<0>(bound)).norm(); double scale; if (old_extent < std::numeric_limits::epsilon()) { scale = 1; } else { scale = extent / old_extent; } Sim3d tform( scale, Eigen::Quaterniond::Identity(), -scale * std::get<2>(bound)); Transform(tform); } Eigen::Vector3d Reconstruction::ComputeCentroid(const double p0, const double p1) const { return std::get<2>(ComputeBoundsAndCentroid(p0, p1, false)); } std::pair Reconstruction::ComputeBoundingBox( const double p0, const double p1) const { auto bound = ComputeBoundsAndCentroid(p0, p1, false); return std::make_pair(std::get<0>(bound), std::get<1>(bound)); } std::tuple Reconstruction::ComputeBoundsAndCentroid(const double p0, const double p1, const bool use_images) const { CHECK_GE(p0, 0); CHECK_LE(p0, 1); CHECK_GE(p1, 0); CHECK_LE(p1, 1); CHECK_LE(p0, p1); const size_t num_elements = use_images ? reg_image_ids_.size() : points3D_.size(); if (num_elements == 0) { return std::make_tuple(Eigen::Vector3d(0, 0, 0), Eigen::Vector3d(0, 0, 0), Eigen::Vector3d(0, 0, 0)); } // Coordinates of image centers or point locations. std::vector coords_x; std::vector coords_y; std::vector coords_z; if (use_images) { coords_x.reserve(reg_image_ids_.size()); coords_y.reserve(reg_image_ids_.size()); coords_z.reserve(reg_image_ids_.size()); for (const image_t im_id : reg_image_ids_) { const Eigen::Vector3d proj_center = Image(im_id).ProjectionCenter(); coords_x.push_back(static_cast(proj_center(0))); coords_y.push_back(static_cast(proj_center(1))); coords_z.push_back(static_cast(proj_center(2))); } } else { coords_x.reserve(points3D_.size()); coords_y.reserve(points3D_.size()); coords_z.reserve(points3D_.size()); for (const auto& point3D : points3D_) { coords_x.push_back(static_cast(point3D.second.xyz(0))); coords_y.push_back(static_cast(point3D.second.xyz(1))); coords_z.push_back(static_cast(point3D.second.xyz(2))); } } // Determine robust bounding box and mean. std::sort(coords_x.begin(), coords_x.end()); std::sort(coords_y.begin(), coords_y.end()); std::sort(coords_z.begin(), coords_z.end()); const size_t P0 = static_cast( (coords_x.size() > 3) ? p0 * (coords_x.size() - 1) : 0); const size_t P1 = static_cast( (coords_x.size() > 3) ? p1 * (coords_x.size() - 1) : coords_x.size() - 1); const Eigen::Vector3d bbox_min(coords_x[P0], coords_y[P0], coords_z[P0]); const Eigen::Vector3d bbox_max(coords_x[P1], coords_y[P1], coords_z[P1]); Eigen::Vector3d mean_coord(0, 0, 0); for (size_t i = P0; i <= P1; ++i) { mean_coord(0) += coords_x[i]; mean_coord(1) += coords_y[i]; mean_coord(2) += coords_z[i]; } mean_coord /= P1 - P0 + 1; return std::make_tuple(bbox_min, bbox_max, mean_coord); } void Reconstruction::Transform(const Sim3d& new_from_old_world) { for (auto& image : images_) { image.second.CamFromWorld() = TransformCameraWorld(new_from_old_world, image.second.CamFromWorld()); } for (auto& point3D : points3D_) { point3D.second.xyz = new_from_old_world * point3D.second.xyz; } } Reconstruction Reconstruction::Crop( const std::pair& bbox) const { Reconstruction cropped_reconstruction; for (const auto& camera : cameras_) { cropped_reconstruction.AddCamera(camera.second); } for (const auto& image : images_) { auto new_image = image.second; new_image.SetRegistered(false); for (auto& point2D : new_image.Points2D()) { point2D.point3D_id = kInvalidPoint3DId; } cropped_reconstruction.AddImage(std::move(new_image)); } std::unordered_set registered_image_ids; for (const auto& point3D : points3D_) { if ((point3D.second.xyz.array() >= bbox.first.array()).all() && (point3D.second.xyz.array() <= bbox.second.array()).all()) { for (const auto& track_el : point3D.second.track.Elements()) { if (registered_image_ids.count(track_el.image_id) == 0) { cropped_reconstruction.RegisterImage(track_el.image_id); registered_image_ids.insert(track_el.image_id); } } cropped_reconstruction.AddPoint3D( point3D.second.xyz, point3D.second.track, point3D.second.color); } } return cropped_reconstruction; } const class Image* Reconstruction::FindImageWithName( const std::string& name) const { for (const auto& image : images_) { if (image.second.Name() == name) { return &image.second; } } return nullptr; } std::vector> Reconstruction::FindCommonRegImageIds( const Reconstruction& other) const { std::vector> common_reg_image_ids; for (const auto image_id : reg_image_ids_) { const auto& image = Image(image_id); const auto* other_image = other.FindImageWithName(image.Name()); if (other_image != nullptr && other_image->IsRegistered()) { common_reg_image_ids.emplace_back(image_id, other_image->ImageId()); } } return common_reg_image_ids; } void Reconstruction::TranscribeImageIdsToDatabase(const Database& database) { std::unordered_map old_to_new_image_ids; old_to_new_image_ids.reserve(NumImages()); std::unordered_map new_images; new_images.reserve(NumImages()); for (auto& image : images_) { if (!database.ExistsImageWithName(image.second.Name())) { LOG(FATAL) << "Image with name " << image.second.Name() << " does not exist in database"; } const auto database_image = database.ReadImageWithName(image.second.Name()); old_to_new_image_ids.emplace(image.second.ImageId(), database_image.ImageId()); image.second.SetImageId(database_image.ImageId()); new_images.emplace(database_image.ImageId(), image.second); } images_ = std::move(new_images); for (auto& image_id : reg_image_ids_) { image_id = old_to_new_image_ids.at(image_id); } for (auto& point3D : points3D_) { for (auto& track_el : point3D.second.track.Elements()) { track_el.image_id = old_to_new_image_ids.at(track_el.image_id); } } } size_t Reconstruction::FilterPoints3D( const double max_reproj_error, const double min_tri_angle, const std::unordered_set& point3D_ids) { size_t num_filtered = 0; num_filtered += FilterPoints3DWithLargeReprojectionError(max_reproj_error, point3D_ids); num_filtered += FilterPoints3DWithSmallTriangulationAngle(min_tri_angle, point3D_ids); return num_filtered; } size_t Reconstruction::FilterPoints3DInImages( const double max_reproj_error, const double min_tri_angle, const std::unordered_set& image_ids) { std::unordered_set point3D_ids; for (const image_t image_id : image_ids) { const class Image& image = Image(image_id); for (const Point2D& point2D : image.Points2D()) { if (point2D.HasPoint3D()) { point3D_ids.insert(point2D.point3D_id); } } } return FilterPoints3D(max_reproj_error, min_tri_angle, point3D_ids); } size_t Reconstruction::FilterAllPoints3D(const double max_reproj_error, const double min_tri_angle) { // Important: First filter observations and points with large reprojection // error, so that observations with large reprojection error do not make // a point stable through a large triangulation angle. const std::unordered_set& point3D_ids = Point3DIds(); size_t num_filtered = 0; num_filtered += FilterPoints3DWithLargeReprojectionError(max_reproj_error, point3D_ids); num_filtered += FilterPoints3DWithSmallTriangulationAngle(min_tri_angle, point3D_ids); return num_filtered; } size_t Reconstruction::FilterObservationsWithNegativeDepth() { size_t num_filtered = 0; for (const auto image_id : reg_image_ids_) { const class Image& image = Image(image_id); const Eigen::Matrix3x4d cam_from_world = image.CamFromWorld().ToMatrix(); for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { const Point2D& point2D = image.Point2D(point2D_idx); if (point2D.HasPoint3D()) { const struct Point3D& point3D = Point3D(point2D.point3D_id); if (!HasPointPositiveDepth(cam_from_world, point3D.xyz)) { DeleteObservation(image_id, point2D_idx); num_filtered += 1; } } } } return num_filtered; } std::vector Reconstruction::FilterImages( const double min_focal_length_ratio, const double max_focal_length_ratio, const double max_extra_param) { std::vector filtered_image_ids; for (const image_t image_id : RegImageIds()) { const class Image& image = Image(image_id); if (image.NumPoints3D() == 0 || Camera(image.CameraId()) .HasBogusParams(min_focal_length_ratio, max_focal_length_ratio, max_extra_param)) { filtered_image_ids.push_back(image_id); } } // Only de-register after iterating over reg_image_ids_ to avoid // simultaneous iteration and modification of the vector. for (const image_t image_id : filtered_image_ids) { DeRegisterImage(image_id); } return filtered_image_ids; } size_t Reconstruction::ComputeNumObservations() const { size_t num_obs = 0; for (const image_t image_id : reg_image_ids_) { num_obs += Image(image_id).NumPoints3D(); } return num_obs; } double Reconstruction::ComputeMeanTrackLength() const { if (points3D_.empty()) { return 0.0; } else { return ComputeNumObservations() / static_cast(points3D_.size()); } } double Reconstruction::ComputeMeanObservationsPerRegImage() const { if (reg_image_ids_.empty()) { return 0.0; } else { return ComputeNumObservations() / static_cast(reg_image_ids_.size()); } } double Reconstruction::ComputeMeanReprojectionError() const { double error_sum = 0.0; size_t num_valid_errors = 0; for (const auto& point3D : points3D_) { if (point3D.second.HasError()) { error_sum += point3D.second.error; num_valid_errors += 1; } } if (num_valid_errors == 0) { return 0.0; } else { return error_sum / num_valid_errors; } } void Reconstruction::UpdatePoint3DErrors() { for (auto& point3D : points3D_) { if (point3D.second.track.Length() == 0) { point3D.second.error = 0; continue; } point3D.second.error = 0; for (const auto& track_el : point3D.second.track.Elements()) { const auto& image = Image(track_el.image_id); const auto& point2D = image.Point2D(track_el.point2D_idx); const auto& camera = Camera(image.CameraId()); point3D.second.error += std::sqrt(CalculateSquaredReprojectionError( point2D.xy, point3D.second.xyz, image.CamFromWorld(), camera)); } point3D.second.error /= point3D.second.track.Length(); } } void Reconstruction::Read(const std::string& path) { if (ExistsFile(JoinPaths(path, "cameras.bin")) && ExistsFile(JoinPaths(path, "images.bin")) && ExistsFile(JoinPaths(path, "points3D.bin"))) { ReadBinary(path); } else if (ExistsFile(JoinPaths(path, "cameras.txt")) && ExistsFile(JoinPaths(path, "images.txt")) && ExistsFile(JoinPaths(path, "points3D.txt"))) { ReadText(path); } else { LOG(FATAL) << "cameras, images, points3D files do not exist at " << path; } } void Reconstruction::Write(const std::string& path) const { WriteBinary(path); } void Reconstruction::ReadText(const std::string& path) { ReadCamerasText(JoinPaths(path, "cameras.txt")); ReadImagesText(JoinPaths(path, "images.txt")); ReadPoints3DText(JoinPaths(path, "points3D.txt")); } void Reconstruction::ReadBinary(const std::string& path) { ReadCamerasBinary(JoinPaths(path, "cameras.bin")); ReadImagesBinary(JoinPaths(path, "images.bin")); ReadPoints3DBinary(JoinPaths(path, "points3D.bin")); } void Reconstruction::WriteText(const std::string& path) const { WriteCamerasText(JoinPaths(path, "cameras.txt")); WriteImagesText(JoinPaths(path, "images.txt")); WritePoints3DText(JoinPaths(path, "points3D.txt")); } void Reconstruction::WriteBinary(const std::string& path) const { WriteCamerasBinary(JoinPaths(path, "cameras.bin")); WriteImagesBinary(JoinPaths(path, "images.bin")); WritePoints3DBinary(JoinPaths(path, "points3D.bin")); } std::vector Reconstruction::ConvertToPLY() const { std::vector ply_points; ply_points.reserve(points3D_.size()); for (const auto& point3D : points3D_) { PlyPoint ply_point; ply_point.x = point3D.second.xyz(0); ply_point.y = point3D.second.xyz(1); ply_point.z = point3D.second.xyz(2); ply_point.r = point3D.second.color(0); ply_point.g = point3D.second.color(1); ply_point.b = point3D.second.color(2); ply_points.push_back(ply_point); } return ply_points; } void Reconstruction::ImportPLY(const std::string& path) { points3D_.clear(); const auto ply_points = ReadPly(path); points3D_.reserve(ply_points.size()); for (const auto& ply_point : ply_points) { AddPoint3D(Eigen::Vector3d(ply_point.x, ply_point.y, ply_point.z), Track(), Eigen::Vector3ub(ply_point.r, ply_point.g, ply_point.b)); } } void Reconstruction::ImportPLY(const std::vector& ply_points) { points3D_.clear(); points3D_.reserve(ply_points.size()); for (const auto& ply_point : ply_points) { AddPoint3D(Eigen::Vector3d(ply_point.x, ply_point.y, ply_point.z), Track(), Eigen::Vector3ub(ply_point.r, ply_point.g, ply_point.b)); } } bool Reconstruction::ExportNVM(const std::string& path, bool skip_distortion) const { std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; // Ensure that we don't lose any precision by storing in text. file.precision(17); // White space added for compatibility with Meshlab. file << "NVM_V3 " << std::endl << " " << std::endl; file << reg_image_ids_.size() << " " << std::endl; std::unordered_map image_id_to_idx_; size_t image_idx = 0; for (const auto image_id : reg_image_ids_) { const class Image& image = Image(image_id); const struct Camera& camera = Camera(image.CameraId()); double k; if (skip_distortion || camera.model_id == SimplePinholeCameraModel::model_id || camera.model_id == PinholeCameraModel::model_id) { k = 0.0; } else if (camera.model_id == SimpleRadialCameraModel::model_id) { k = -1 * camera.params[SimpleRadialCameraModel::extra_params_idxs[0]]; } else { LOG(WARNING) << "NVM only supports `SIMPLE_RADIAL` " "and pinhole camera models." << std::endl; return false; } const Eigen::Vector3d proj_center = image.ProjectionCenter(); file << image.Name() << " "; file << camera.MeanFocalLength() << " "; file << image.CamFromWorld().rotation.w() << " "; file << image.CamFromWorld().rotation.x() << " "; file << image.CamFromWorld().rotation.y() << " "; file << image.CamFromWorld().rotation.z() << " "; file << proj_center.x() << " "; file << proj_center.y() << " "; file << proj_center.z() << " "; file << k << " "; file << 0 << std::endl; image_id_to_idx_[image_id] = image_idx; image_idx += 1; } file << std::endl << points3D_.size() << std::endl; for (const auto& point3D : points3D_) { file << point3D.second.xyz(0) << " "; file << point3D.second.xyz(1) << " "; file << point3D.second.xyz(2) << " "; file << static_cast(point3D.second.color(0)) << " "; file << static_cast(point3D.second.color(1)) << " "; file << static_cast(point3D.second.color(2)) << " "; std::ostringstream line; std::unordered_set image_ids; for (const auto& track_el : point3D.second.track.Elements()) { // Make sure that each point only has a single observation per image, // since VisualSfM does not support with multiple observations. if (image_ids.count(track_el.image_id) == 0) { const class Image& image = Image(track_el.image_id); const Point2D& point2D = image.Point2D(track_el.point2D_idx); line << image_id_to_idx_[track_el.image_id] << " "; line << track_el.point2D_idx << " "; line << point2D.xy(0) << " "; line << point2D.xy(1) << " "; image_ids.insert(track_el.image_id); } } std::string line_string = line.str(); line_string = line_string.substr(0, line_string.size() - 1); file << image_ids.size() << " "; file << line_string << std::endl; } return true; } bool Reconstruction::ExportCam(const std::string& path, bool skip_distortion) const { CreateImageDirs(path); for (const auto image_id : reg_image_ids_) { std::string name, ext; const class Image& image = Image(image_id); const struct Camera& camera = Camera(image.CameraId()); SplitFileExtension(image.Name(), &name, &ext); name = JoinPaths(path, name.append(".cam")); std::ofstream file(name, std::ios::trunc); CHECK(file.is_open()) << name; // Ensure that we don't lose any precision by storing in text. file.precision(17); double k1, k2; if (skip_distortion || camera.model_id == SimplePinholeCameraModel::model_id || camera.model_id == PinholeCameraModel::model_id) { k1 = 0.0; k2 = 0.0; } else if (camera.model_id == SimpleRadialCameraModel::model_id) { k1 = camera.params[SimpleRadialCameraModel::extra_params_idxs[0]]; k2 = 0.0; } else if (camera.model_id == RadialCameraModel::model_id) { k1 = camera.params[RadialCameraModel::extra_params_idxs[0]]; k2 = camera.params[RadialCameraModel::extra_params_idxs[1]]; } else { LOG(WARNING) << "CAM only supports `SIMPLE_RADIAL`, `RADIAL`, " "and pinhole camera models." << std::endl; return false; } // If both k1 and k2 values are non-zero, then the CAM format assumes // a Bundler-like radial distortion model, which converts well from // COLMAP. However, if k2 is zero, then a different model is used // that does not translate as well, so we avoid setting k2 to zero. if (k1 != 0.0 && k2 == 0.0) { k2 = 1e-10; } const double fx = camera.FocalLengthX(); const double fy = camera.FocalLengthY(); double focal_length; if (camera.width * fy < camera.height * fx) { focal_length = fy / camera.height; } else { focal_length = fx / camera.width; } const Eigen::Matrix3d R = image.CamFromWorld().rotation.toRotationMatrix(); file << image.CamFromWorld().translation.x() << " " << image.CamFromWorld().translation.y() << " " << image.CamFromWorld().translation.z() << " " << R(0, 0) << " " << R(0, 1) << " " << R(0, 2) << " " << R(1, 0) << " " << R(1, 1) << " " << R(1, 2) << " " << R(2, 0) << " " << R(2, 1) << " " << R(2, 2) << std::endl; file << focal_length << " " << k1 << " " << k2 << " " << fy / fx << " " << camera.PrincipalPointX() / camera.width << " " << camera.PrincipalPointY() / camera.height << std::endl; } return true; } bool Reconstruction::ExportRecon3D(const std::string& path, bool skip_distortion) const { std::string base_path = EnsureTrailingSlash(StringReplace(path, "\\", "/")); CreateDirIfNotExists(base_path); base_path = base_path.append("Recon/"); CreateDirIfNotExists(base_path); std::string synth_path = base_path + "synth_0.out"; std::string image_list_path = base_path + "urd-images.txt"; std::string image_map_path = base_path + "imagemap_0.txt"; std::ofstream synth_file(synth_path, std::ios::trunc); CHECK(synth_file.is_open()) << synth_path; std::ofstream image_list_file(image_list_path, std::ios::trunc); CHECK(image_list_file.is_open()) << image_list_path; std::ofstream image_map_file(image_map_path, std::ios::trunc); CHECK(image_map_file.is_open()) << image_map_path; // Ensure that we don't lose any precision by storing in text. synth_file.precision(17); // Write header info synth_file << "colmap 1.0" << std::endl; synth_file << reg_image_ids_.size() << " " << points3D_.size() << std::endl; std::unordered_map image_id_to_idx_; size_t image_idx = 0; // Write image/camera info for (const auto image_id : reg_image_ids_) { const class Image& image = Image(image_id); const struct Camera& camera = Camera(image.CameraId()); double k1, k2; if (skip_distortion || camera.model_id == SimplePinholeCameraModel::model_id || camera.model_id == PinholeCameraModel::model_id) { k1 = 0.0; k2 = 0.0; } else if (camera.model_id == SimpleRadialCameraModel::model_id) { k1 = -1 * camera.params[SimpleRadialCameraModel::extra_params_idxs[0]]; k2 = 0.0; } else if (camera.model_id == RadialCameraModel::model_id) { k1 = -1 * camera.params[RadialCameraModel::extra_params_idxs[0]]; k2 = -1 * camera.params[RadialCameraModel::extra_params_idxs[1]]; } else { LOG(WARNING) << "Recon3D only supports `SIMPLE_RADIAL`, " "`RADIAL`, and pinhole camera models."; return false; } const double scale = 1.0 / (double)std::max(camera.width, camera.height); synth_file << scale * camera.MeanFocalLength() << " " << k1 << " " << k2 << std::endl; synth_file << image.CamFromWorld().rotation.toRotationMatrix() << std::endl; synth_file << image.CamFromWorld().translation.transpose() << std::endl; image_id_to_idx_[image_id] = image_idx; image_list_file << image.Name() << std::endl << camera.width << " " << camera.height << std::endl; image_map_file << image_idx << std::endl; image_idx += 1; } image_list_file.close(); image_map_file.close(); // Write point info for (const auto& point3D : points3D_) { auto& p = point3D.second; synth_file << p.xyz(0) << " " << p.xyz(1) << " " << p.xyz(2) << std::endl; synth_file << static_cast(p.color(0)) << " " << static_cast(p.color(1)) << " " << static_cast(p.color(2)) << std::endl; std::ostringstream line; std::unordered_set image_ids; for (const auto& track_el : p.track.Elements()) { // Make sure that each point only has a single observation per image, // since VisualSfM does not support with multiple observations. if (image_ids.count(track_el.image_id) == 0) { const class Image& image = Image(track_el.image_id); const struct Camera& camera = Camera(image.CameraId()); const Point2D& point2D = image.Point2D(track_el.point2D_idx); const double scale = 1.0 / (double)std::max(camera.width, camera.height); line << image_id_to_idx_[track_el.image_id] << " "; line << track_el.point2D_idx << " "; // Use a scale of -1.0 to mark as invalid as it is not needed currently line << "-1.0 "; line << (point2D.xy(0) - camera.PrincipalPointX()) * scale << " "; line << (point2D.xy(1) - camera.PrincipalPointY()) * scale << " "; image_ids.insert(track_el.image_id); } } std::string line_string = line.str(); line_string = line_string.substr(0, line_string.size() - 1); synth_file << image_ids.size() << " "; synth_file << line_string << std::endl; } synth_file.close(); return true; } bool Reconstruction::ExportBundler(const std::string& path, const std::string& list_path, bool skip_distortion) const { std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; std::ofstream list_file(list_path, std::ios::trunc); CHECK(list_file.is_open()) << list_path; // Ensure that we don't lose any precision by storing in text. file.precision(17); file << "# Bundle file v0.3" << std::endl; file << reg_image_ids_.size() << " " << points3D_.size() << std::endl; std::unordered_map image_id_to_idx_; size_t image_idx = 0; for (const image_t image_id : reg_image_ids_) { const class Image& image = Image(image_id); const struct Camera& camera = Camera(image.CameraId()); double k1, k2; if (skip_distortion || camera.model_id == SimplePinholeCameraModel::model_id || camera.model_id == PinholeCameraModel::model_id) { k1 = 0.0; k2 = 0.0; } else if (camera.model_id == SimpleRadialCameraModel::model_id) { k1 = camera.params[SimpleRadialCameraModel::extra_params_idxs[0]]; k2 = 0.0; } else if (camera.model_id == RadialCameraModel::model_id) { k1 = camera.params[RadialCameraModel::extra_params_idxs[0]]; k2 = camera.params[RadialCameraModel::extra_params_idxs[1]]; } else { LOG(WARNING) << "Bundler only supports `SIMPLE_RADIAL`, " "`RADIAL`, and pinhole camera models." << std::endl; return false; } file << camera.MeanFocalLength() << " " << k1 << " " << k2 << std::endl; const Eigen::Matrix3d R = image.CamFromWorld().rotation.toRotationMatrix(); file << R(0, 0) << " " << R(0, 1) << " " << R(0, 2) << std::endl; file << -R(1, 0) << " " << -R(1, 1) << " " << -R(1, 2) << std::endl; file << -R(2, 0) << " " << -R(2, 1) << " " << -R(2, 2) << std::endl; file << image.CamFromWorld().translation.x() << " "; file << -image.CamFromWorld().translation.y() << " "; file << -image.CamFromWorld().translation.z() << std::endl; list_file << image.Name() << std::endl; image_id_to_idx_[image_id] = image_idx; image_idx += 1; } for (const auto& point3D : points3D_) { file << point3D.second.xyz(0) << " "; file << point3D.second.xyz(1) << " "; file << point3D.second.xyz(2) << std::endl; file << static_cast(point3D.second.color(0)) << " "; file << static_cast(point3D.second.color(1)) << " "; file << static_cast(point3D.second.color(2)) << std::endl; std::ostringstream line; line << point3D.second.track.Length() << " "; for (const auto& track_el : point3D.second.track.Elements()) { const class Image& image = Image(track_el.image_id); const struct Camera& camera = Camera(image.CameraId()); // Bundler output assumes image coordinate system origin // in the lower left corner of the image with the center of // the lower left pixel being (0, 0). Our coordinate system // starts in the upper left corner with the center of the // upper left pixel being (0.5, 0.5). const Point2D& point2D = image.Point2D(track_el.point2D_idx); line << image_id_to_idx_.at(track_el.image_id) << " "; line << track_el.point2D_idx << " "; line << point2D.xy(0) - camera.PrincipalPointX() << " "; line << camera.PrincipalPointY() - point2D.xy(1) << " "; } std::string line_string = line.str(); line_string = line_string.substr(0, line_string.size() - 1); file << line_string << std::endl; } return true; } void Reconstruction::ExportPLY(const std::string& path) const { const auto ply_points = ConvertToPLY(); const bool kWriteNormal = false; const bool kWriteRGB = true; WriteBinaryPlyPoints(path, ply_points, kWriteNormal, kWriteRGB); } void Reconstruction::ExportVRML(const std::string& images_path, const std::string& points3D_path, const double image_scale, const Eigen::Vector3d& image_rgb) const { std::ofstream images_file(images_path, std::ios::trunc); CHECK(images_file.is_open()) << images_path; const double six = image_scale * 0.15; const double siy = image_scale * 0.1; std::vector points; points.emplace_back(-six, -siy, six * 1.0 * 2.0); points.emplace_back(+six, -siy, six * 1.0 * 2.0); points.emplace_back(+six, +siy, six * 1.0 * 2.0); points.emplace_back(-six, +siy, six * 1.0 * 2.0); points.emplace_back(0, 0, 0); points.emplace_back(-six / 3.0, -siy / 3.0, six * 1.0 * 2.0); points.emplace_back(+six / 3.0, -siy / 3.0, six * 1.0 * 2.0); points.emplace_back(+six / 3.0, +siy / 3.0, six * 1.0 * 2.0); points.emplace_back(-six / 3.0, +siy / 3.0, six * 1.0 * 2.0); for (const auto& image : images_) { if (!image.second.IsRegistered()) { continue; } images_file << "Shape{\n"; images_file << " appearance Appearance {\n"; images_file << " material DEF Default-ffRffGffB Material {\n"; images_file << " ambientIntensity 0\n"; images_file << " diffuseColor " << " " << image_rgb(0) << " " << image_rgb(1) << " " << image_rgb(2) << "\n"; images_file << " emissiveColor 0.1 0.1 0.1 } }\n"; images_file << " geometry IndexedFaceSet {\n"; images_file << " solid FALSE \n"; images_file << " colorPerVertex TRUE \n"; images_file << " ccw TRUE \n"; images_file << " coord Coordinate {\n"; images_file << " point [\n"; // Move camera base model to camera pose. const Eigen::Matrix3x4d world_from_cam = Inverse(image.second.CamFromWorld()).ToMatrix(); for (size_t i = 0; i < points.size(); i++) { const Eigen::Vector3d point = world_from_cam * points[i].homogeneous(); images_file << point(0) << " " << point(1) << " " << point(2) << "\n"; } images_file << " ] }\n"; images_file << "color Color {color [\n"; for (size_t p = 0; p < points.size(); p++) { images_file << " " << image_rgb(0) << " " << image_rgb(1) << " " << image_rgb(2) << "\n"; } images_file << "\n] }\n"; images_file << "coordIndex [\n"; images_file << " 0, 1, 2, 3, -1\n"; images_file << " 5, 6, 4, -1\n"; images_file << " 6, 7, 4, -1\n"; images_file << " 7, 8, 4, -1\n"; images_file << " 8, 5, 4, -1\n"; images_file << " \n] \n"; images_file << " texCoord TextureCoordinate { point [\n"; images_file << " 1 1,\n"; images_file << " 0 1,\n"; images_file << " 0 0,\n"; images_file << " 1 0,\n"; images_file << " 0 0,\n"; images_file << " 0 0,\n"; images_file << " 0 0,\n"; images_file << " 0 0,\n"; images_file << " 0 0,\n"; images_file << " ] }\n"; images_file << "} }\n"; } // Write 3D points std::ofstream points3D_file(points3D_path, std::ios::trunc); CHECK(points3D_file.is_open()) << points3D_path; points3D_file << "#VRML V2.0 utf8\n"; points3D_file << "Background { skyColor [1.0 1.0 1.0] } \n"; points3D_file << "Shape{ appearance Appearance {\n"; points3D_file << " material Material {emissiveColor 1 1 1} }\n"; points3D_file << " geometry PointSet {\n"; points3D_file << " coord Coordinate {\n"; points3D_file << " point [\n"; for (const auto& point3D : points3D_) { points3D_file << point3D.second.xyz(0) << ", "; points3D_file << point3D.second.xyz(1) << ", "; points3D_file << point3D.second.xyz(2) << std::endl; } points3D_file << " ] }\n"; points3D_file << " color Color { color [\n"; for (const auto& point3D : points3D_) { points3D_file << point3D.second.color(0) / 255.0 << ", "; points3D_file << point3D.second.color(1) / 255.0 << ", "; points3D_file << point3D.second.color(2) / 255.0 << std::endl; } points3D_file << " ] } } }\n"; } bool Reconstruction::ExtractColorsForImage(const image_t image_id, const std::string& path) { const class Image& image = Image(image_id); Bitmap bitmap; if (!bitmap.Read(JoinPaths(path, image.Name()))) { return false; } const Eigen::Vector3ub kBlackColor(0, 0, 0); for (const Point2D& point2D : image.Points2D()) { if (point2D.HasPoint3D()) { struct Point3D& point3D = Point3D(point2D.point3D_id); if (point3D.color == kBlackColor) { BitmapColor color; // COLMAP assumes that the upper left pixel center is (0.5, 0.5). if (bitmap.InterpolateBilinear( point2D.xy(0) - 0.5, point2D.xy(1) - 0.5, &color)) { const BitmapColor color_ub = color.Cast(); point3D.color = Eigen::Vector3ub(color_ub.r, color_ub.g, color_ub.b); } } } } return true; } void Reconstruction::ExtractColorsForAllImages(const std::string& path) { std::unordered_map color_sums; std::unordered_map color_counts; for (size_t i = 0; i < reg_image_ids_.size(); ++i) { const class Image& image = Image(reg_image_ids_[i]); const std::string image_path = JoinPaths(path, image.Name()); Bitmap bitmap; if (!bitmap.Read(image_path)) { LOG(WARNING) << StringPrintf("Could not read image %s at path %s.", image.Name().c_str(), image_path.c_str()) << std::endl; continue; } for (const Point2D& point2D : image.Points2D()) { if (point2D.HasPoint3D()) { BitmapColor color; // COLMAP assumes that the upper left pixel center is (0.5, 0.5). if (bitmap.InterpolateBilinear( point2D.xy(0) - 0.5, point2D.xy(1) - 0.5, &color)) { if (color_sums.count(point2D.point3D_id)) { Eigen::Vector3d& color_sum = color_sums[point2D.point3D_id]; color_sum(0) += color.r; color_sum(1) += color.g; color_sum(2) += color.b; color_counts[point2D.point3D_id] += 1; } else { color_sums.emplace(point2D.point3D_id, Eigen::Vector3d(color.r, color.g, color.b)); color_counts.emplace(point2D.point3D_id, 1); } } } } } const Eigen::Vector3ub kBlackColor = Eigen::Vector3ub::Zero(); for (auto& point3D : points3D_) { if (color_sums.count(point3D.first)) { Eigen::Vector3d color = color_sums[point3D.first] / color_counts[point3D.first]; for (Eigen::Index i = 0; i < color.size(); ++i) { color[i] = std::round(color[i]); } point3D.second.color = color.cast(); } else { point3D.second.color = kBlackColor; } } } void Reconstruction::CreateImageDirs(const std::string& path) const { std::unordered_set image_dirs; for (const auto& image : images_) { const std::vector name_split = StringSplit(image.second.Name(), "/"); if (name_split.size() > 1) { std::string dir = path; for (size_t i = 0; i < name_split.size() - 1; ++i) { dir = JoinPaths(dir, name_split[i]); image_dirs.insert(dir); } } } for (const auto& dir : image_dirs) { CreateDirIfNotExists(dir, /*recursive=*/true); } } size_t Reconstruction::FilterPoints3DWithSmallTriangulationAngle( const double min_tri_angle, const std::unordered_set& point3D_ids) { // Number of filtered points. size_t num_filtered = 0; // Minimum triangulation angle in radians. const double min_tri_angle_rad = DegToRad(min_tri_angle); // Cache for image projection centers. std::unordered_map proj_centers; for (const auto point3D_id : point3D_ids) { if (!ExistsPoint3D(point3D_id)) { continue; } const struct Point3D& point3D = Point3D(point3D_id); // Calculate triangulation angle for all pairwise combinations of image // poses in the track. Only delete point if none of the combinations // has a sufficient triangulation angle. bool keep_point = false; for (size_t i1 = 0; i1 < point3D.track.Length(); ++i1) { const image_t image_id1 = point3D.track.Element(i1).image_id; Eigen::Vector3d proj_center1; if (proj_centers.count(image_id1) == 0) { const class Image& image1 = Image(image_id1); proj_center1 = image1.ProjectionCenter(); proj_centers.emplace(image_id1, proj_center1); } else { proj_center1 = proj_centers.at(image_id1); } for (size_t i2 = 0; i2 < i1; ++i2) { const image_t image_id2 = point3D.track.Element(i2).image_id; const Eigen::Vector3d proj_center2 = proj_centers.at(image_id2); const double tri_angle = CalculateTriangulationAngle( proj_center1, proj_center2, point3D.xyz); if (tri_angle >= min_tri_angle_rad) { keep_point = true; break; } } if (keep_point) { break; } } if (!keep_point) { num_filtered += 1; DeletePoint3D(point3D_id); } } return num_filtered; } size_t Reconstruction::FilterPoints3DWithLargeReprojectionError( const double max_reproj_error, const std::unordered_set& point3D_ids) { const double max_squared_reproj_error = max_reproj_error * max_reproj_error; // Number of filtered points. size_t num_filtered = 0; for (const auto point3D_id : point3D_ids) { if (!ExistsPoint3D(point3D_id)) { continue; } struct Point3D& point3D = Point3D(point3D_id); if (point3D.track.Length() < 2) { num_filtered += point3D.track.Length(); DeletePoint3D(point3D_id); continue; } double reproj_error_sum = 0.0; std::vector track_els_to_delete; for (const auto& track_el : point3D.track.Elements()) { const class Image& image = Image(track_el.image_id); const struct Camera& camera = Camera(image.CameraId()); const Point2D& point2D = image.Point2D(track_el.point2D_idx); const double squared_reproj_error = CalculateSquaredReprojectionError( point2D.xy, point3D.xyz, image.CamFromWorld(), camera); if (squared_reproj_error > max_squared_reproj_error) { track_els_to_delete.push_back(track_el); } else { reproj_error_sum += std::sqrt(squared_reproj_error); } } if (track_els_to_delete.size() >= point3D.track.Length() - 1) { num_filtered += point3D.track.Length(); DeletePoint3D(point3D_id); } else { num_filtered += track_els_to_delete.size(); for (const auto& track_el : track_els_to_delete) { DeleteObservation(track_el.image_id, track_el.point2D_idx); } point3D.error = reproj_error_sum / point3D.track.Length(); } } return num_filtered; } void Reconstruction::ReadCamerasText(const std::string& path) { cameras_.clear(); std::ifstream file(path); CHECK(file.is_open()) << path; std::string line; std::string item; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } std::stringstream line_stream(line); struct Camera camera; // ID std::getline(line_stream, item, ' '); camera.camera_id = std::stoul(item); // MODEL std::getline(line_stream, item, ' '); camera.model_id = CameraModelNameToId(item); // WIDTH std::getline(line_stream, item, ' '); camera.width = std::stoll(item); // HEIGHT std::getline(line_stream, item, ' '); camera.height = std::stoll(item); // PARAMS camera.params.reserve(CameraModelNumParams(camera.model_id)); while (!line_stream.eof()) { std::getline(line_stream, item, ' '); camera.params.push_back(std::stold(item)); } CHECK(camera.VerifyParams()); cameras_.emplace(camera.camera_id, std::move(camera)); } } void Reconstruction::ReadImagesText(const std::string& path) { images_.clear(); std::ifstream file(path); CHECK(file.is_open()) << path; std::string line; std::string item; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } std::stringstream line_stream1(line); // ID std::getline(line_stream1, item, ' '); const image_t image_id = std::stoul(item); class Image image; image.SetImageId(image_id); image.SetRegistered(true); reg_image_ids_.push_back(image_id); Rigid3d& cam_from_world = image.CamFromWorld(); std::getline(line_stream1, item, ' '); cam_from_world.rotation.w() = std::stold(item); std::getline(line_stream1, item, ' '); cam_from_world.rotation.x() = std::stold(item); std::getline(line_stream1, item, ' '); cam_from_world.rotation.y() = std::stold(item); std::getline(line_stream1, item, ' '); cam_from_world.rotation.z() = std::stold(item); cam_from_world.rotation.normalize(); std::getline(line_stream1, item, ' '); cam_from_world.translation.x() = std::stold(item); std::getline(line_stream1, item, ' '); cam_from_world.translation.y() = std::stold(item); std::getline(line_stream1, item, ' '); cam_from_world.translation.z() = std::stold(item); // CAMERA_ID std::getline(line_stream1, item, ' '); image.SetCameraId(std::stoul(item)); // NAME std::getline(line_stream1, item, ' '); image.SetName(item); // POINTS2D if (!std::getline(file, line)) { break; } StringTrim(&line); std::stringstream line_stream2(line); std::vector points2D; std::vector point3D_ids; if (!line.empty()) { while (!line_stream2.eof()) { Eigen::Vector2d point; std::getline(line_stream2, item, ' '); point.x() = std::stold(item); std::getline(line_stream2, item, ' '); point.y() = std::stold(item); points2D.push_back(point); std::getline(line_stream2, item, ' '); if (item == "-1") { point3D_ids.push_back(kInvalidPoint3DId); } else { point3D_ids.push_back(std::stoll(item)); } } } image.SetUp(Camera(image.CameraId())); image.SetPoints2D(points2D); for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { if (point3D_ids[point2D_idx] != kInvalidPoint3DId) { image.SetPoint3DForPoint2D(point2D_idx, point3D_ids[point2D_idx]); } } images_.emplace(image.ImageId(), image); } } void Reconstruction::ReadPoints3DText(const std::string& path) { points3D_.clear(); std::ifstream file(path); CHECK(file.is_open()) << path; std::string line; std::string item; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } std::stringstream line_stream(line); // ID std::getline(line_stream, item, ' '); const point3D_t point3D_id = std::stoll(item); // Make sure, that we can add new 3D points after reading 3D points // without overwriting existing 3D points. num_added_points3D_ = std::max(num_added_points3D_, point3D_id); struct Point3D point3D; // XYZ std::getline(line_stream, item, ' '); point3D.xyz(0) = std::stold(item); std::getline(line_stream, item, ' '); point3D.xyz(1) = std::stold(item); std::getline(line_stream, item, ' '); point3D.xyz(2) = std::stold(item); // Color std::getline(line_stream, item, ' '); point3D.color(0) = static_cast(std::stoi(item)); std::getline(line_stream, item, ' '); point3D.color(1) = static_cast(std::stoi(item)); std::getline(line_stream, item, ' '); point3D.color(2) = static_cast(std::stoi(item)); // ERROR std::getline(line_stream, item, ' '); point3D.error = std::stold(item); // TRACK while (!line_stream.eof()) { TrackElement track_el; std::getline(line_stream, item, ' '); StringTrim(&item); if (item.empty()) { break; } track_el.image_id = std::stoul(item); std::getline(line_stream, item, ' '); track_el.point2D_idx = std::stoul(item); point3D.track.AddElement(track_el); } point3D.track.Compress(); points3D_.emplace(point3D_id, point3D); } } void Reconstruction::ReadCamerasBinary(const std::string& path) { std::ifstream file(path, std::ios::binary); CHECK(file.is_open()) << path; const size_t num_cameras = ReadBinaryLittleEndian(&file); for (size_t i = 0; i < num_cameras; ++i) { struct Camera camera; camera.camera_id = ReadBinaryLittleEndian(&file); camera.model_id = static_cast(ReadBinaryLittleEndian(&file)); camera.width = ReadBinaryLittleEndian(&file); camera.height = ReadBinaryLittleEndian(&file); camera.params.resize(CameraModelNumParams(camera.model_id), 0.); ReadBinaryLittleEndian(&file, &camera.params); CHECK(camera.VerifyParams()); cameras_.emplace(camera.camera_id, std::move(camera)); } } void Reconstruction::ReadImagesBinary(const std::string& path) { std::ifstream file(path, std::ios::binary); CHECK(file.is_open()) << path; const size_t num_reg_images = ReadBinaryLittleEndian(&file); for (size_t i = 0; i < num_reg_images; ++i) { class Image image; image.SetImageId(ReadBinaryLittleEndian(&file)); Rigid3d& cam_from_world = image.CamFromWorld(); cam_from_world.rotation.w() = ReadBinaryLittleEndian(&file); cam_from_world.rotation.x() = ReadBinaryLittleEndian(&file); cam_from_world.rotation.y() = ReadBinaryLittleEndian(&file); cam_from_world.rotation.z() = ReadBinaryLittleEndian(&file); cam_from_world.rotation.normalize(); cam_from_world.translation.x() = ReadBinaryLittleEndian(&file); cam_from_world.translation.y() = ReadBinaryLittleEndian(&file); cam_from_world.translation.z() = ReadBinaryLittleEndian(&file); image.SetCameraId(ReadBinaryLittleEndian(&file)); char name_char; do { file.read(&name_char, 1); if (name_char != '\0') { image.Name() += name_char; } } while (name_char != '\0'); const size_t num_points2D = ReadBinaryLittleEndian(&file); std::vector points2D; points2D.reserve(num_points2D); std::vector point3D_ids; point3D_ids.reserve(num_points2D); for (size_t j = 0; j < num_points2D; ++j) { const double x = ReadBinaryLittleEndian(&file); const double y = ReadBinaryLittleEndian(&file); points2D.emplace_back(x, y); point3D_ids.push_back(ReadBinaryLittleEndian(&file)); } image.SetUp(Camera(image.CameraId())); image.SetPoints2D(points2D); for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { if (point3D_ids[point2D_idx] != kInvalidPoint3DId) { image.SetPoint3DForPoint2D(point2D_idx, point3D_ids[point2D_idx]); } } image.SetRegistered(true); reg_image_ids_.push_back(image.ImageId()); images_.emplace(image.ImageId(), image); } } void Reconstruction::ReadPoints3DBinary(const std::string& path) { std::ifstream file(path, std::ios::binary); CHECK(file.is_open()) << path; const size_t num_points3D = ReadBinaryLittleEndian(&file); for (size_t i = 0; i < num_points3D; ++i) { struct Point3D point3D; const point3D_t point3D_id = ReadBinaryLittleEndian(&file); num_added_points3D_ = std::max(num_added_points3D_, point3D_id); point3D.xyz(0) = ReadBinaryLittleEndian(&file); point3D.xyz(1) = ReadBinaryLittleEndian(&file); point3D.xyz(2) = ReadBinaryLittleEndian(&file); point3D.color(0) = ReadBinaryLittleEndian(&file); point3D.color(1) = ReadBinaryLittleEndian(&file); point3D.color(2) = ReadBinaryLittleEndian(&file); point3D.error = ReadBinaryLittleEndian(&file); const size_t track_length = ReadBinaryLittleEndian(&file); for (size_t j = 0; j < track_length; ++j) { const image_t image_id = ReadBinaryLittleEndian(&file); const point2D_t point2D_idx = ReadBinaryLittleEndian(&file); point3D.track.AddElement(image_id, point2D_idx); } point3D.track.Compress(); points3D_.emplace(point3D_id, point3D); } } void Reconstruction::WriteCamerasText(const std::string& path) const { std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; // Ensure that we don't loose any precision by storing in text. file.precision(17); file << "# Camera list with one line of data per camera:" << std::endl; file << "# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]" << std::endl; file << "# Number of cameras: " << cameras_.size() << std::endl; for (const auto& camera : cameras_) { std::ostringstream line; line.precision(17); line << camera.first << " "; line << camera.second.ModelName() << " "; line << camera.second.width << " "; line << camera.second.height << " "; for (const double param : camera.second.params) { line << param << " "; } std::string line_string = line.str(); line_string = line_string.substr(0, line_string.size() - 1); file << line_string << std::endl; } } void Reconstruction::WriteImagesText(const std::string& path) const { std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; // Ensure that we don't loose any precision by storing in text. file.precision(17); file << "# Image list with two lines of data per image:" << std::endl; file << "# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, " "NAME" << std::endl; file << "# POINTS2D[] as (X, Y, POINT3D_ID)" << std::endl; file << "# Number of images: " << reg_image_ids_.size() << ", mean observations per image: " << ComputeMeanObservationsPerRegImage() << std::endl; for (const auto& image : images_) { if (!image.second.IsRegistered()) { continue; } std::ostringstream line; line.precision(17); std::string line_string; line << image.first << " "; const Rigid3d& cam_from_world = image.second.CamFromWorld(); line << cam_from_world.rotation.w() << " "; line << cam_from_world.rotation.x() << " "; line << cam_from_world.rotation.y() << " "; line << cam_from_world.rotation.z() << " "; line << cam_from_world.translation.x() << " "; line << cam_from_world.translation.y() << " "; line << cam_from_world.translation.z() << " "; line << image.second.CameraId() << " "; line << image.second.Name(); file << line.str() << std::endl; line.str(""); line.clear(); for (const Point2D& point2D : image.second.Points2D()) { line << point2D.xy(0) << " "; line << point2D.xy(1) << " "; if (point2D.HasPoint3D()) { line << point2D.point3D_id << " "; } else { line << -1 << " "; } } line_string = line.str(); line_string = line_string.substr(0, line_string.size() - 1); file << line_string << std::endl; } } void Reconstruction::WritePoints3DText(const std::string& path) const { std::ofstream file(path, std::ios::trunc); CHECK(file.is_open()) << path; // Ensure that we don't loose any precision by storing in text. file.precision(17); file << "# 3D point list with one line of data per point:" << std::endl; file << "# POINT3D_ID, X, Y, Z, R, G, B, ERROR, " "TRACK[] as (IMAGE_ID, POINT2D_IDX)" << std::endl; file << "# Number of points: " << points3D_.size() << ", mean track length: " << ComputeMeanTrackLength() << std::endl; for (const auto& point3D : points3D_) { file << point3D.first << " "; file << point3D.second.xyz(0) << " "; file << point3D.second.xyz(1) << " "; file << point3D.second.xyz(2) << " "; file << static_cast(point3D.second.color(0)) << " "; file << static_cast(point3D.second.color(1)) << " "; file << static_cast(point3D.second.color(2)) << " "; file << point3D.second.error << " "; std::ostringstream line; line.precision(17); for (const auto& track_el : point3D.second.track.Elements()) { line << track_el.image_id << " "; line << track_el.point2D_idx << " "; } std::string line_string = line.str(); line_string = line_string.substr(0, line_string.size() - 1); file << line_string << std::endl; } } void Reconstruction::WriteCamerasBinary(const std::string& path) const { std::ofstream file(path, std::ios::trunc | std::ios::binary); CHECK(file.is_open()) << path; WriteBinaryLittleEndian(&file, cameras_.size()); for (const auto& camera : cameras_) { WriteBinaryLittleEndian(&file, camera.first); WriteBinaryLittleEndian(&file, static_cast(camera.second.model_id)); WriteBinaryLittleEndian(&file, camera.second.width); WriteBinaryLittleEndian(&file, camera.second.height); for (const double param : camera.second.params) { WriteBinaryLittleEndian(&file, param); } } } void Reconstruction::WriteImagesBinary(const std::string& path) const { std::ofstream file(path, std::ios::trunc | std::ios::binary); CHECK(file.is_open()) << path; WriteBinaryLittleEndian(&file, reg_image_ids_.size()); for (const auto& image : images_) { if (!image.second.IsRegistered()) { continue; } WriteBinaryLittleEndian(&file, image.first); const Rigid3d& cam_from_world = image.second.CamFromWorld(); WriteBinaryLittleEndian(&file, cam_from_world.rotation.w()); WriteBinaryLittleEndian(&file, cam_from_world.rotation.x()); WriteBinaryLittleEndian(&file, cam_from_world.rotation.y()); WriteBinaryLittleEndian(&file, cam_from_world.rotation.z()); WriteBinaryLittleEndian(&file, cam_from_world.translation.x()); WriteBinaryLittleEndian(&file, cam_from_world.translation.y()); WriteBinaryLittleEndian(&file, cam_from_world.translation.z()); WriteBinaryLittleEndian(&file, image.second.CameraId()); const std::string name = image.second.Name() + '\0'; file.write(name.c_str(), name.size()); WriteBinaryLittleEndian(&file, image.second.NumPoints2D()); for (const Point2D& point2D : image.second.Points2D()) { WriteBinaryLittleEndian(&file, point2D.xy(0)); WriteBinaryLittleEndian(&file, point2D.xy(1)); WriteBinaryLittleEndian(&file, point2D.point3D_id); } } } void Reconstruction::WritePoints3DBinary(const std::string& path) const { std::ofstream file(path, std::ios::trunc | std::ios::binary); CHECK(file.is_open()) << path; WriteBinaryLittleEndian(&file, points3D_.size()); for (const auto& point3D : points3D_) { WriteBinaryLittleEndian(&file, point3D.first); WriteBinaryLittleEndian(&file, point3D.second.xyz(0)); WriteBinaryLittleEndian(&file, point3D.second.xyz(1)); WriteBinaryLittleEndian(&file, point3D.second.xyz(2)); WriteBinaryLittleEndian(&file, point3D.second.color(0)); WriteBinaryLittleEndian(&file, point3D.second.color(1)); WriteBinaryLittleEndian(&file, point3D.second.color(2)); WriteBinaryLittleEndian(&file, point3D.second.error); WriteBinaryLittleEndian(&file, point3D.second.track.Length()); for (const auto& track_el : point3D.second.track.Elements()) { WriteBinaryLittleEndian(&file, track_el.image_id); WriteBinaryLittleEndian(&file, track_el.point2D_idx); } } } void Reconstruction::SetObservationAsTriangulated( const image_t image_id, const point2D_t point2D_idx, const bool is_continued_point3D) { if (correspondence_graph_ == nullptr) { return; } const class Image& image = Image(image_id); CHECK(image.IsRegistered()); const Point2D& point2D = image.Point2D(point2D_idx); CHECK(point2D.HasPoint3D()); const auto corr_range = correspondence_graph_->FindCorrespondences(image_id, point2D_idx); for (const auto* corr = corr_range.beg; corr < corr_range.end; ++corr) { class Image& corr_image = Image(corr->image_id); const Point2D& corr_point2D = corr_image.Point2D(corr->point2D_idx); corr_image.IncrementCorrespondenceHasPoint3D(corr->point2D_idx); // Update number of shared 3D points between image pairs and make sure to // only count the correspondences once (not twice forward and backward). if (point2D.point3D_id == corr_point2D.point3D_id && (is_continued_point3D || image_id < corr->image_id)) { const image_pair_t pair_id = Database::ImagePairToPairId(image_id, corr->image_id); auto& stats = image_pair_stats_[pair_id]; stats.num_tri_corrs += 1; CHECK_LE(stats.num_tri_corrs, stats.num_total_corrs) << "The correspondence graph must not contain duplicate matches: " << corr->image_id << " " << corr->point2D_idx; } } } void Reconstruction::ResetTriObservations(const image_t image_id, const point2D_t point2D_idx, const bool is_deleted_point3D) { if (correspondence_graph_ == nullptr) { return; } const class Image& image = Image(image_id); CHECK(image.IsRegistered()); const Point2D& point2D = image.Point2D(point2D_idx); CHECK(point2D.HasPoint3D()); const auto corr_range = correspondence_graph_->FindCorrespondences(image_id, point2D_idx); for (const auto* corr = corr_range.beg; corr < corr_range.end; ++corr) { class Image& corr_image = Image(corr->image_id); const Point2D& corr_point2D = corr_image.Point2D(corr->point2D_idx); corr_image.DecrementCorrespondenceHasPoint3D(corr->point2D_idx); // Update number of shared 3D points between image pairs and make sure to // only count the correspondences once (not twice forward and backward). if (point2D.point3D_id == corr_point2D.point3D_id && (!is_deleted_point3D || image_id < corr->image_id)) { const image_pair_t pair_id = Database::ImagePairToPairId(image_id, corr->image_id); CHECK_GT(image_pair_stats_[pair_id].num_tri_corrs, 0) << "The scene graph graph must not contain duplicate matches"; image_pair_stats_[pair_id].num_tri_corrs -= 1; } } } } // namespace colmap colmap-3.9.1/src/colmap/scene/reconstruction.h000066400000000000000000000505731454702036400214070ustar00rootroot00000000000000// 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. #pragma once #include "colmap/geometry/sim3.h" #include "colmap/scene/camera.h" #include "colmap/scene/database.h" #include "colmap/scene/image.h" #include "colmap/scene/point2d.h" #include "colmap/scene/point3d.h" #include "colmap/scene/track.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include #include #include #include namespace colmap { struct PlyPoint; struct RANSACOptions; class DatabaseCache; class CorrespondenceGraph; // Reconstruction class holds all information about a single reconstructed // model. It is used by the mapping and bundle adjustment classes and can be // written to and read from disk. class Reconstruction { public: struct ImagePairStat { // The number of triangulated correspondences between two images. size_t num_tri_corrs = 0; // The number of total correspondences/matches between two images. size_t num_total_corrs = 0; }; Reconstruction(); // Get number of objects. inline size_t NumCameras() const; inline size_t NumImages() const; inline size_t NumRegImages() const; inline size_t NumPoints3D() const; inline size_t NumImagePairs() const; // Get const objects. inline const struct Camera& Camera(camera_t camera_id) const; inline const class Image& Image(image_t image_id) const; inline const struct Point3D& Point3D(point3D_t point3D_id) const; inline const ImagePairStat& ImagePair(image_pair_t pair_id) const; inline ImagePairStat& ImagePair(image_t image_id1, image_t image_id2); // Get mutable objects. inline struct Camera& Camera(camera_t camera_id); inline class Image& Image(image_t image_id); inline struct Point3D& Point3D(point3D_t point3D_id); inline ImagePairStat& ImagePair(image_pair_t pair_id); inline const ImagePairStat& ImagePair(image_t image_id1, image_t image_id2) const; // Get reference to all objects. inline const std::unordered_map& Cameras() const; inline const std::unordered_map& Images() const; inline const std::vector& RegImageIds() const; inline const std::unordered_map& Points3D() const; inline const std::unordered_map& ImagePairs() const; // Identifiers of all 3D points. std::unordered_set Point3DIds() const; // Check whether specific object exists. inline bool ExistsCamera(camera_t camera_id) const; inline bool ExistsImage(image_t image_id) const; inline bool ExistsPoint3D(point3D_t point3D_id) const; inline bool ExistsImagePair(image_pair_t pair_id) const; // Load data from given `DatabaseCache`. void Load(const DatabaseCache& database_cache); // Setup all relevant data structures before reconstruction. Note the // correspondence graph object must live until `TearDown` is called. void SetUp(std::shared_ptr correspondence_graph); // Finalize the Reconstruction after the reconstruction has finished. // // Once a scene has been finalized, it cannot be used for reconstruction. // // This removes all not yet registered images and unused cameras, in order to // save memory. void TearDown(); // Add new camera. There is only one camera per image, while multiple images // might be taken by the same camera. void AddCamera(struct Camera camera); // Add new image. void AddImage(class Image image); // Add new 3D object, and return its unique ID. point3D_t AddPoint3D( const Eigen::Vector3d& xyz, Track track, const Eigen::Vector3ub& color = Eigen::Vector3ub::Zero()); // Add observation to existing 3D point. void AddObservation(point3D_t point3D_id, const TrackElement& track_el); // Merge two 3D points and return new identifier of new 3D point. // The location of the merged 3D point is a weighted average of the two // original 3D point's locations according to their track lengths. point3D_t MergePoints3D(point3D_t point3D_id1, point3D_t point3D_id2); // Delete a 3D point, and all its references in the observed images. void DeletePoint3D(point3D_t point3D_id); // Delete one observation from an image and the corresponding 3D point. // Note that this deletes the entire 3D point, if the track has two elements // prior to calling this method. void DeleteObservation(image_t image_id, point2D_t point2D_idx); // Delete all 2D points of all images and all 3D points. void DeleteAllPoints2DAndPoints3D(); // Register an existing image. void RegisterImage(image_t image_id); // De-register an existing image, and all its references. void DeRegisterImage(image_t image_id); // Check if image is registered. inline bool IsImageRegistered(image_t image_id) const; // Normalize scene by scaling and translation to avoid degenerate // visualization after bundle adjustment and to improve numerical // stability of algorithms. // // Translates scene such that the mean of the camera centers or point // locations are at the origin of the coordinate system. // // Scales scene such that the minimum and maximum camera centers are at the // given `extent`, whereas `p0` and `p1` determine the minimum and // maximum percentiles of the camera centers considered. void Normalize(double extent = 10.0, double p0 = 0.1, double p1 = 0.9, bool use_images = true); // Compute the centroid of the 3D points Eigen::Vector3d ComputeCentroid(double p0 = 0.1, double p1 = 0.9) const; // Compute the bounding box corners of the 3D points std::pair ComputeBoundingBox( double p0 = 0.0, double p1 = 1.0) const; // Apply the 3D similarity transformation to all images and points. void Transform(const Sim3d& new_from_old_world); // Creates a cropped reconstruction using the input bounds as corner points // of the bounding box containing the included 3D points of the new // reconstruction. Only the cameras and images of the included points are // registered. Reconstruction Crop( const std::pair& bbox) const; // Find specific image by name. Note that this uses linear search. const class Image* FindImageWithName(const std::string& name) const; // Find images that are both present in this and the given reconstruction. // Matching of images is performed based on common image names. std::vector> FindCommonRegImageIds( const Reconstruction& other) const; // Update the image identifiers to match the ones in the database by matching // the names of the images. void TranscribeImageIdsToDatabase(const Database& database); // Filter 3D points with large reprojection error, negative depth, or // insufficient triangulation angle. // // @param max_reproj_error The maximum reprojection error. // @param min_tri_angle The minimum triangulation angle. // @param point3D_ids The points to be filtered. // // @return The number of filtered observations. size_t FilterPoints3D(double max_reproj_error, double min_tri_angle, const std::unordered_set& point3D_ids); size_t FilterPoints3DInImages(double max_reproj_error, double min_tri_angle, const std::unordered_set& image_ids); size_t FilterAllPoints3D(double max_reproj_error, double min_tri_angle); // Filter observations that have negative depth. // // @return The number of filtered observations. size_t FilterObservationsWithNegativeDepth(); // Filter images without observations or bogus camera parameters. // // @return The identifiers of the filtered images. std::vector FilterImages(double min_focal_length_ratio, double max_focal_length_ratio, double max_extra_param); // Compute statistics for scene. size_t ComputeNumObservations() const; double ComputeMeanTrackLength() const; double ComputeMeanObservationsPerRegImage() const; double ComputeMeanReprojectionError() const; // Updates mean reprojection errors for all 3D points. void UpdatePoint3DErrors(); // Read data from text or binary file. Prefer binary data if it exists. void Read(const std::string& path); void Write(const std::string& path) const; // Read data from binary/text file. void ReadText(const std::string& path); void ReadBinary(const std::string& path); // Write data from binary/text file. void WriteText(const std::string& path) const; void WriteBinary(const std::string& path) const; // Convert 3D points in reconstruction to PLY point cloud. std::vector ConvertToPLY() const; // Import from other data formats. Note that these import functions are // only intended for visualization of data and usable for reconstruction. void ImportPLY(const std::string& path); void ImportPLY(const std::vector& ply_points); // Export to other data formats. // Exports in NVM format http://ccwu.me/vsfm/doc.html#nvm. Only supports // SIMPLE_RADIAL camera model when exporting distortion parameters. When // skip_distortion == true it supports all camera models with the caveat that // it's using the mean focal length which will be inaccurate for camera models // with two focal lengths and distortion. bool ExportNVM(const std::string& path, bool skip_distortion = false) const; // Exports in CAM format which is a simple text file that contains pose // information and camera intrinsics for each image and exports one file per // image; it does not include information on the 3D points. The format is as // follows (2 lines of text with space separated numbers): // // 1.0 // Note that focal length is relative to the image max(width, height), // and principal points x and y are relative to width and height respectively. // // Only supports SIMPLE_RADIAL and RADIAL camera models when exporting // distortion parameters. When skip_distortion == true it supports all camera // models with the caveat that it's using the mean focal length which will be // inaccurate for camera models with two focal lengths and distortion. bool ExportCam(const std::string& path, bool skip_distortion = false) const; // Exports in Recon3D format which consists of three text files with the // following format and content: // 1) imagemap_0.txt: a list of image numeric IDs with one entry per line. // 2) urd-images.txt: A list of images with one entry per line as: // // 3) synth_0.out: Contains information for image poses, camera intrinsics, // and 3D points as: // // // // // Each image entry consists of 5 lines as: // // // // Note that the focal length is scaled by 1 / max(width, height) // // Each point entry consists of 3 lines as: // // // ... // // Each track elemenet is a sequence of 5 values as: // <2D point ID> -1.0 // Note that the 2D point coordinates are centered around the principal // point and scaled by 1 / max(width, height). // // When skip_distortion == true it supports all camera models with the // caveat that it's using the mean focal length which will be inaccurate // for camera models with two focal lengths and distortion. bool ExportRecon3D(const std::string& path, bool skip_distortion = false) const; // Exports in Bundler format https://www.cs.cornell.edu/~snavely/bundler/. // Supports SIMPLE_PINHOLE, PINHOLE, SIMPLE_RADIAL and RADIAL camera models // when exporting distortion parameters. When skip_distortion == true it // supports all camera models with the caveat that it's using the mean focal // length which will be inaccurate for camera models with two focal lengths // and distortion. bool ExportBundler(const std::string& path, const std::string& list_path, bool skip_distortion = false) const; // Exports 3D points only in PLY format. void ExportPLY(const std::string& path) const; // Exports in VRML format https://en.wikipedia.org/wiki/VRML. void ExportVRML(const std::string& images_path, const std::string& points3D_path, double image_scale, const Eigen::Vector3d& image_rgb) const; // Extract colors for 3D points of given image. Colors will be extracted // only for 3D points which are completely black. // // @param image_id Identifier of the image for which to extract colors. // @param path Absolute or relative path to root folder of image. // The image path is determined by concatenating the // root path and the name of the image. // // @return True if image could be read at given path. bool ExtractColorsForImage(image_t image_id, const std::string& path); // Extract colors for all 3D points by computing the mean color of all images. // // @param path Absolute or relative path to root folder of image. // The image path is determined by concatenating the // root path and the name of the image. void ExtractColorsForAllImages(const std::string& path); // Create all image sub-directories in the given path. void CreateImageDirs(const std::string& path) const; private: size_t FilterPoints3DWithSmallTriangulationAngle( double min_tri_angle, const std::unordered_set& point3D_ids); size_t FilterPoints3DWithLargeReprojectionError( double max_reproj_error, const std::unordered_set& point3D_ids); std::tuple ComputeBoundsAndCentroid(double p0, double p1, bool use_images) const; void ReadCamerasText(const std::string& path); void ReadImagesText(const std::string& path); void ReadPoints3DText(const std::string& path); void ReadCamerasBinary(const std::string& path); void ReadImagesBinary(const std::string& path); void ReadPoints3DBinary(const std::string& path); void WriteCamerasText(const std::string& path) const; void WriteImagesText(const std::string& path) const; void WritePoints3DText(const std::string& path) const; void WriteCamerasBinary(const std::string& path) const; void WriteImagesBinary(const std::string& path) const; void WritePoints3DBinary(const std::string& path) const; void SetObservationAsTriangulated(image_t image_id, point2D_t point2D_idx, bool is_continued_point3D); void ResetTriObservations(image_t image_id, point2D_t point2D_idx, bool is_deleted_point3D); std::shared_ptr correspondence_graph_; std::unordered_map cameras_; std::unordered_map images_; std::unordered_map points3D_; std::unordered_map image_pair_stats_; // { image_id, ... } where `images_.at(image_id).registered == true`. std::vector reg_image_ids_; // Total number of added 3D points, used to generate unique identifiers. point3D_t num_added_points3D_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// size_t Reconstruction::NumCameras() const { return cameras_.size(); } size_t Reconstruction::NumImages() const { return images_.size(); } size_t Reconstruction::NumRegImages() const { return reg_image_ids_.size(); } size_t Reconstruction::NumPoints3D() const { return points3D_.size(); } size_t Reconstruction::NumImagePairs() const { return image_pair_stats_.size(); } const struct Camera& Reconstruction::Camera(const camera_t camera_id) const { return cameras_.at(camera_id); } const class Image& Reconstruction::Image(const image_t image_id) const { return images_.at(image_id); } const struct Point3D& Reconstruction::Point3D( const point3D_t point3D_id) const { return points3D_.at(point3D_id); } const Reconstruction::ImagePairStat& Reconstruction::ImagePair( const image_pair_t pair_id) const { return image_pair_stats_.at(pair_id); } const Reconstruction::ImagePairStat& Reconstruction::ImagePair( const image_t image_id1, const image_t image_id2) const { const auto pair_id = Database::ImagePairToPairId(image_id1, image_id2); return image_pair_stats_.at(pair_id); } struct Camera& Reconstruction::Camera(const camera_t camera_id) { return cameras_.at(camera_id); } class Image& Reconstruction::Image(const image_t image_id) { return images_.at(image_id); } struct Point3D& Reconstruction::Point3D(const point3D_t point3D_id) { return points3D_.at(point3D_id); } Reconstruction::ImagePairStat& Reconstruction::ImagePair( const image_pair_t pair_id) { return image_pair_stats_.at(pair_id); } Reconstruction::ImagePairStat& Reconstruction::ImagePair( const image_t image_id1, const image_t image_id2) { const auto pair_id = Database::ImagePairToPairId(image_id1, image_id2); return image_pair_stats_.at(pair_id); } const std::unordered_map& Reconstruction::Cameras() const { return cameras_; } const std::unordered_map& Reconstruction::Images() const { return images_; } const std::vector& Reconstruction::RegImageIds() const { return reg_image_ids_; } const std::unordered_map& Reconstruction::Points3D() const { return points3D_; } const std::unordered_map& Reconstruction::ImagePairs() const { return image_pair_stats_; } bool Reconstruction::ExistsCamera(const camera_t camera_id) const { return cameras_.find(camera_id) != cameras_.end(); } bool Reconstruction::ExistsImage(const image_t image_id) const { return images_.find(image_id) != images_.end(); } bool Reconstruction::ExistsPoint3D(const point3D_t point3D_id) const { return points3D_.find(point3D_id) != points3D_.end(); } bool Reconstruction::ExistsImagePair(const image_pair_t pair_id) const { return image_pair_stats_.find(pair_id) != image_pair_stats_.end(); } bool Reconstruction::IsImageRegistered(const image_t image_id) const { return Image(image_id).IsRegistered(); } } // namespace colmap colmap-3.9.1/src/colmap/scene/reconstruction_manager.cc000066400000000000000000000065061454702036400232340ustar00rootroot00000000000000// 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. #include "colmap/scene/reconstruction_manager.h" #include "colmap/util/misc.h" namespace colmap { size_t ReconstructionManager::Size() const { return reconstructions_.size(); } std::shared_ptr ReconstructionManager::Get( const size_t idx) const { return reconstructions_.at(idx); } std::shared_ptr& ReconstructionManager::Get(const size_t idx) { return reconstructions_.at(idx); } size_t ReconstructionManager::Add() { const size_t idx = Size(); reconstructions_.push_back(std::make_shared()); return idx; } void ReconstructionManager::Delete(const size_t idx) { CHECK_LT(idx, reconstructions_.size()); reconstructions_.erase(reconstructions_.begin() + idx); } void ReconstructionManager::Clear() { reconstructions_.clear(); } size_t ReconstructionManager::Read(const std::string& path) { const size_t idx = Add(); reconstructions_[idx]->Read(path); return idx; } void ReconstructionManager::Write(const std::string& path) const { std::vector> recon_sizes(reconstructions_.size()); for (size_t i = 0; i < reconstructions_.size(); ++i) { recon_sizes[i] = std::make_pair(i, reconstructions_[i]->NumPoints3D()); } std::sort(recon_sizes.begin(), recon_sizes.end(), [](const std::pair& first, const std::pair& second) { return first.second > second.second; }); for (size_t i = 0; i < reconstructions_.size(); ++i) { const std::string reconstruction_path = JoinPaths(path, std::to_string(i)); CreateDirIfNotExists(reconstruction_path); reconstructions_[recon_sizes[i].first]->Write(reconstruction_path); } } } // namespace colmap colmap-3.9.1/src/colmap/scene/reconstruction_manager.h000066400000000000000000000047541454702036400231010ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/reconstruction.h" #include namespace colmap { class OptionManager; class ReconstructionManager { public: // The number of reconstructions managed. size_t Size() const; // Get a reference to a specific reconstruction. std::shared_ptr Get(size_t idx) const; std::shared_ptr& Get(size_t idx); // Add a new empty reconstruction and return its index. size_t Add(); // Delete a specific reconstruction. void Delete(size_t idx); // Delete all reconstructions. void Clear(); // Read and add a new reconstruction and return its index. size_t Read(const std::string& path); // Write all managed reconstructions into sub-folders "0", "1", "2", ... void Write(const std::string& path) const; private: std::vector> reconstructions_; }; } // namespace colmap colmap-3.9.1/src/colmap/scene/reconstruction_manager_test.cc000066400000000000000000000062131454702036400242660ustar00rootroot00000000000000// 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. #include "colmap/scene/reconstruction_manager.h" #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { namespace { TEST(ReconstructionManager, Empty) { ReconstructionManager reconstruction_manager; EXPECT_EQ(reconstruction_manager.Size(), 0); } TEST(ReconstructionManager, AddGet) { ReconstructionManager reconstruction_manager; EXPECT_EQ(reconstruction_manager.Size(), 0); for (size_t i = 0; i < 10; ++i) { const size_t idx = reconstruction_manager.Add(); EXPECT_EQ(reconstruction_manager.Size(), i + 1); EXPECT_EQ(idx, i); EXPECT_EQ(reconstruction_manager.Get(idx)->NumCameras(), 0); EXPECT_EQ(reconstruction_manager.Get(idx)->NumImages(), 0); EXPECT_EQ(reconstruction_manager.Get(idx)->NumPoints3D(), 0); } } TEST(ReconstructionManager, Delete) { ReconstructionManager reconstruction_manager; EXPECT_EQ(reconstruction_manager.Size(), 0); for (size_t i = 0; i < 10; ++i) { reconstruction_manager.Add(); } EXPECT_EQ(reconstruction_manager.Size(), 10); for (size_t i = 0; i < 10; ++i) { reconstruction_manager.Delete(0); EXPECT_EQ(reconstruction_manager.Size(), 9 - i); } } TEST(ReconstructionManager, Clear) { ReconstructionManager reconstruction_manager; EXPECT_EQ(reconstruction_manager.Size(), 0); for (size_t i = 0; i < 10; ++i) { reconstruction_manager.Add(); } EXPECT_EQ(reconstruction_manager.Size(), 10); reconstruction_manager.Clear(); EXPECT_EQ(reconstruction_manager.Size(), 0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/reconstruction_test.cc000066400000000000000000000671531454702036400226060ustar00rootroot00000000000000// 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. #include "colmap/scene/reconstruction.h" #include "colmap/geometry/pose.h" #include "colmap/geometry/sim3.h" #include "colmap/scene/correspondence_graph.h" #include "colmap/sensor/models.h" #include namespace colmap { namespace { void GenerateReconstruction(const image_t num_images, Reconstruction* reconstruction) { const size_t kNumPoints2D = 10; Camera camera = Camera::CreateFromModelName(1, "PINHOLE", 1, 1, 1); reconstruction->AddCamera(camera); for (image_t image_id = 1; image_id <= num_images; ++image_id) { Image image; image.SetImageId(image_id); image.SetCameraId(camera.camera_id); image.SetName("image" + std::to_string(image_id)); image.SetPoints2D( std::vector(kNumPoints2D, Eigen::Vector2d::Zero())); image.SetRegistered(true); reconstruction->AddImage(std::move(image)); } } TEST(Reconstruction, Empty) { Reconstruction reconstruction; EXPECT_EQ(reconstruction.NumCameras(), 0); EXPECT_EQ(reconstruction.NumImages(), 0); EXPECT_EQ(reconstruction.NumRegImages(), 0); EXPECT_EQ(reconstruction.NumPoints3D(), 0); EXPECT_EQ(reconstruction.NumImagePairs(), 0); } TEST(Reconstruction, AddCamera) { Reconstruction reconstruction; Camera camera = Camera::CreateFromModelId(1, SimplePinholeCameraModel::model_id, 1, 1, 1); reconstruction.AddCamera(camera); EXPECT_TRUE(reconstruction.ExistsCamera(camera.camera_id)); EXPECT_EQ(reconstruction.Camera(camera.camera_id).camera_id, camera.camera_id); EXPECT_EQ(reconstruction.Cameras().count(camera.camera_id), 1); EXPECT_EQ(reconstruction.Cameras().size(), 1); EXPECT_EQ(reconstruction.NumCameras(), 1); EXPECT_EQ(reconstruction.NumImages(), 0); EXPECT_EQ(reconstruction.NumRegImages(), 0); EXPECT_EQ(reconstruction.NumPoints3D(), 0); EXPECT_EQ(reconstruction.NumImagePairs(), 0); } TEST(Reconstruction, AddImage) { Reconstruction reconstruction; Image image; image.SetImageId(1); reconstruction.AddImage(image); EXPECT_TRUE(reconstruction.ExistsImage(1)); EXPECT_EQ(reconstruction.Image(1).ImageId(), 1); EXPECT_FALSE(reconstruction.Image(1).IsRegistered()); EXPECT_EQ(reconstruction.Images().count(1), 1); EXPECT_EQ(reconstruction.Images().size(), 1); EXPECT_EQ(reconstruction.NumCameras(), 0); EXPECT_EQ(reconstruction.NumImages(), 1); EXPECT_EQ(reconstruction.NumRegImages(), 0); EXPECT_EQ(reconstruction.NumPoints3D(), 0); EXPECT_EQ(reconstruction.NumImagePairs(), 0); } TEST(Reconstruction, AddPoint3D) { Reconstruction reconstruction; const point3D_t point3D_id = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_TRUE(reconstruction.ExistsPoint3D(point3D_id)); EXPECT_EQ(reconstruction.Point3D(point3D_id).track.Length(), 0); EXPECT_EQ(reconstruction.Points3D().count(point3D_id), 1); EXPECT_EQ(reconstruction.Points3D().size(), 1); EXPECT_EQ(reconstruction.NumCameras(), 0); EXPECT_EQ(reconstruction.NumImages(), 0); EXPECT_EQ(reconstruction.NumRegImages(), 0); EXPECT_EQ(reconstruction.NumPoints3D(), 1); EXPECT_EQ(reconstruction.NumImagePairs(), 0); EXPECT_EQ(reconstruction.Point3DIds().count(point3D_id), 1); } TEST(Reconstruction, AddObservation) { Reconstruction reconstruction; GenerateReconstruction(3, &reconstruction); Track track; track.AddElement(1, 0); track.AddElement(2, 1); const point3D_t point3D_id = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), track); EXPECT_EQ(reconstruction.Image(1).NumPoints3D(), 1); EXPECT_TRUE(reconstruction.Image(1).Point2D(0).HasPoint3D()); EXPECT_FALSE(reconstruction.Image(1).Point2D(1).HasPoint3D()); EXPECT_EQ(reconstruction.Image(2).NumPoints3D(), 1); EXPECT_FALSE(reconstruction.Image(2).Point2D(0).HasPoint3D()); EXPECT_TRUE(reconstruction.Image(2).Point2D(1).HasPoint3D()); EXPECT_EQ(reconstruction.Point3D(point3D_id).track.Length(), 2); reconstruction.AddObservation(point3D_id, TrackElement(3, 2)); EXPECT_EQ(reconstruction.Image(3).NumPoints3D(), 1); EXPECT_TRUE(reconstruction.Image(3).Point2D(2).HasPoint3D()); EXPECT_EQ(reconstruction.Point3D(point3D_id).track.Length(), 3); } TEST(Reconstruction, MergePoints3D) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, 0), Track()); reconstruction.AddObservation(point3D_id1, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id1, TrackElement(2, 0)); reconstruction.Point3D(point3D_id1).color = Eigen::Matrix(0, 0, 0); const point3D_t point3D_id2 = reconstruction.AddPoint3D(Eigen::Vector3d(1, 1, 1), Track()); reconstruction.AddObservation(point3D_id2, TrackElement(1, 1)); reconstruction.AddObservation(point3D_id2, TrackElement(2, 1)); reconstruction.Point3D(point3D_id2).color = Eigen::Matrix(20, 20, 20); const point3D_t merged_point3D_id = reconstruction.MergePoints3D(point3D_id1, point3D_id2); EXPECT_FALSE(reconstruction.ExistsPoint3D(point3D_id1)); EXPECT_FALSE(reconstruction.ExistsPoint3D(point3D_id2)); EXPECT_TRUE(reconstruction.ExistsPoint3D(merged_point3D_id)); EXPECT_EQ(reconstruction.Image(1).Point2D(0).point3D_id, merged_point3D_id); EXPECT_EQ(reconstruction.Image(1).Point2D(1).point3D_id, merged_point3D_id); EXPECT_EQ(reconstruction.Image(2).Point2D(0).point3D_id, merged_point3D_id); EXPECT_EQ(reconstruction.Image(2).Point2D(1).point3D_id, merged_point3D_id); EXPECT_TRUE(reconstruction.Point3D(merged_point3D_id) .xyz.isApprox(Eigen::Vector3d(0.5, 0.5, 0.5))); EXPECT_EQ(reconstruction.Point3D(merged_point3D_id).color, Eigen::Vector3ub(10, 10, 10)); } TEST(Reconstruction, DeletePoint3D) { Reconstruction reconstruction; GenerateReconstruction(1, &reconstruction); const point3D_t point3D_id = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); reconstruction.AddObservation(point3D_id, TrackElement(1, 0)); reconstruction.DeletePoint3D(point3D_id); EXPECT_FALSE(reconstruction.ExistsPoint3D(point3D_id)); EXPECT_EQ(reconstruction.Image(1).NumPoints3D(), 0); } TEST(Reconstruction, DeleteObservation) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); const point3D_t point3D_id = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, 0), Track()); reconstruction.AddObservation(point3D_id, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id, TrackElement(1, 1)); reconstruction.AddObservation(point3D_id, TrackElement(1, 2)); reconstruction.DeleteObservation(1, 0); EXPECT_EQ(reconstruction.Point3D(point3D_id).track.Length(), 2); EXPECT_FALSE(reconstruction.Image(point3D_id).Point2D(0).HasPoint3D()); reconstruction.DeleteObservation(1, 1); EXPECT_FALSE(reconstruction.ExistsPoint3D(point3D_id)); EXPECT_FALSE(reconstruction.Image(point3D_id).Point2D(1).HasPoint3D()); EXPECT_FALSE(reconstruction.Image(point3D_id).Point2D(2).HasPoint3D()); } TEST(Reconstruction, RegisterImage) { Reconstruction reconstruction; GenerateReconstruction(1, &reconstruction); EXPECT_EQ(reconstruction.NumRegImages(), 1); EXPECT_TRUE(reconstruction.Image(1).IsRegistered()); EXPECT_TRUE(reconstruction.IsImageRegistered(1)); reconstruction.RegisterImage(1); EXPECT_EQ(reconstruction.NumRegImages(), 1); EXPECT_TRUE(reconstruction.Image(1).IsRegistered()); EXPECT_TRUE(reconstruction.IsImageRegistered(1)); reconstruction.DeRegisterImage(1); EXPECT_EQ(reconstruction.NumRegImages(), 0); EXPECT_FALSE(reconstruction.Image(1).IsRegistered()); EXPECT_FALSE(reconstruction.IsImageRegistered(1)); } TEST(Reconstruction, Normalize) { Reconstruction reconstruction; GenerateReconstruction(3, &reconstruction); reconstruction.Image(1).CamFromWorld().translation.z() = -10.0; reconstruction.Image(2).CamFromWorld().translation.z() = 0.0; reconstruction.Image(3).CamFromWorld().translation.z() = 10.0; reconstruction.DeRegisterImage(1); reconstruction.DeRegisterImage(2); reconstruction.DeRegisterImage(3); reconstruction.Normalize(); EXPECT_NEAR( reconstruction.Image(1).CamFromWorld().translation.z(), -10, 1e-6); EXPECT_NEAR(reconstruction.Image(2).CamFromWorld().translation.z(), 0, 1e-6); EXPECT_NEAR(reconstruction.Image(3).CamFromWorld().translation.z(), 10, 1e-6); reconstruction.RegisterImage(1); reconstruction.RegisterImage(2); reconstruction.RegisterImage(3); reconstruction.Normalize(); EXPECT_NEAR(reconstruction.Image(1).CamFromWorld().translation.z(), -5, 1e-6); EXPECT_NEAR(reconstruction.Image(2).CamFromWorld().translation.z(), 0, 1e-6); EXPECT_NEAR(reconstruction.Image(3).CamFromWorld().translation.z(), 5, 1e-6); reconstruction.Normalize(5); EXPECT_NEAR( reconstruction.Image(1).CamFromWorld().translation.z(), -2.5, 1e-6); EXPECT_NEAR(reconstruction.Image(2).CamFromWorld().translation.z(), 0, 1e-6); EXPECT_NEAR( reconstruction.Image(3).CamFromWorld().translation.z(), 2.5, 1e-6); reconstruction.Normalize(10, 0.0, 1.0); EXPECT_NEAR(reconstruction.Image(1).CamFromWorld().translation.z(), -5, 1e-6); EXPECT_NEAR(reconstruction.Image(2).CamFromWorld().translation.z(), 0, 1e-6); EXPECT_NEAR(reconstruction.Image(3).CamFromWorld().translation.z(), 5, 1e-6); reconstruction.Normalize(20); Image image; image.SetImageId(4); reconstruction.AddImage(image); reconstruction.RegisterImage(4); image.SetImageId(5); reconstruction.AddImage(image); reconstruction.RegisterImage(5); image.SetImageId(6); reconstruction.AddImage(image); reconstruction.RegisterImage(6); image.SetImageId(7); reconstruction.AddImage(image); reconstruction.RegisterImage(7); reconstruction.Image(4).CamFromWorld().translation.z() = -7.5; reconstruction.Image(5).CamFromWorld().translation.z() = -5.0; reconstruction.Image(6).CamFromWorld().translation.z() = 5.0; reconstruction.Image(7).CamFromWorld().translation.z() = 7.5; reconstruction.RegisterImage(7); reconstruction.Normalize(10, 0.0, 1.0); EXPECT_NEAR(reconstruction.Image(1).CamFromWorld().translation.z(), -5, 1e-6); EXPECT_NEAR(reconstruction.Image(2).CamFromWorld().translation.z(), 0, 1e-6); EXPECT_NEAR(reconstruction.Image(3).CamFromWorld().translation.z(), 5, 1e-6); EXPECT_NEAR( reconstruction.Image(4).CamFromWorld().translation.z(), -3.75, 1e-6); EXPECT_NEAR( reconstruction.Image(5).CamFromWorld().translation.z(), -2.5, 1e-6); EXPECT_NEAR( reconstruction.Image(6).CamFromWorld().translation.z(), 2.5, 1e-6); EXPECT_NEAR( reconstruction.Image(7).CamFromWorld().translation.z(), 3.75, 1e-6); } TEST(Reconstruction, ComputeBoundsAndCentroid) { Reconstruction reconstruction; // Test emtpy reconstruction first auto centroid = reconstruction.ComputeCentroid(0.0, 1.0); auto bbox = reconstruction.ComputeBoundingBox(0.0, 1.0); EXPECT_LT(std::abs(centroid(0)), 1e-6); EXPECT_LT(std::abs(centroid(1)), 1e-6); EXPECT_LT(std::abs(centroid(2)), 1e-6); EXPECT_LT(std::abs(bbox.first(0)), 1e-6); EXPECT_LT(std::abs(bbox.first(1)), 1e-6); EXPECT_LT(std::abs(bbox.first(2)), 1e-6); EXPECT_LT(std::abs(bbox.second(0)), 1e-6); EXPECT_LT(std::abs(bbox.second(1)), 1e-6); EXPECT_LT(std::abs(bbox.second(2)), 1e-6); // Test reconstruction with 3D points reconstruction.AddPoint3D(Eigen::Vector3d(3.0, 0.0, 0.0), Track()); reconstruction.AddPoint3D(Eigen::Vector3d(0.0, 3.0, 0.0), Track()); reconstruction.AddPoint3D(Eigen::Vector3d(0.0, 0.0, 3.0), Track()); centroid = reconstruction.ComputeCentroid(0.0, 1.0); bbox = reconstruction.ComputeBoundingBox(0.0, 1.0); EXPECT_LT(std::abs(centroid(0) - 1.0), 1e-6); EXPECT_LT(std::abs(centroid(1) - 1.0), 1e-6); EXPECT_LT(std::abs(centroid(2) - 1.0), 1e-6); EXPECT_LT(std::abs(bbox.first(0)), 1e-6); EXPECT_LT(std::abs(bbox.first(1)), 1e-6); EXPECT_LT(std::abs(bbox.first(2)), 1e-6); EXPECT_LT(std::abs(bbox.second(0) - 3.0), 1e-6); EXPECT_LT(std::abs(bbox.second(1) - 3.0), 1e-6); EXPECT_LT(std::abs(bbox.second(2) - 3.0), 1e-6); } TEST(Reconstruction, Crop) { Reconstruction reconstruction; GenerateReconstruction(3, &reconstruction); point3D_t point_id = reconstruction.AddPoint3D(Eigen::Vector3d(0.0, 0.0, 0.0), Track()); reconstruction.AddObservation(point_id, TrackElement(1, 1)); point_id = reconstruction.AddPoint3D(Eigen::Vector3d(0.5, 0.5, 0.0), Track()); reconstruction.AddObservation(point_id, TrackElement(1, 2)); point_id = reconstruction.AddPoint3D(Eigen::Vector3d(1.0, 1.0, 0.0), Track()); reconstruction.AddObservation(point_id, TrackElement(2, 3)); point_id = reconstruction.AddPoint3D(Eigen::Vector3d(0.0, 0.0, 0.5), Track()); reconstruction.AddObservation(point_id, TrackElement(2, 4)); point_id = reconstruction.AddPoint3D(Eigen::Vector3d(0.5, 0.5, 1.0), Track()); reconstruction.AddObservation(point_id, TrackElement(3, 5)); // Check correct reconstruction setup EXPECT_EQ(reconstruction.NumCameras(), 1); EXPECT_EQ(reconstruction.NumImages(), 3); EXPECT_EQ(reconstruction.NumRegImages(), 3); EXPECT_EQ(reconstruction.NumPoints3D(), 5); std::pair bbox; // Test emtpy reconstruction after cropping. bbox.first = Eigen::Vector3d(-1, -1, -1); bbox.second = Eigen::Vector3d(-0.5, -0.5, -0.5); Reconstruction cropped1 = reconstruction.Crop(bbox); EXPECT_EQ(cropped1.NumCameras(), 1); EXPECT_EQ(cropped1.NumImages(), 3); EXPECT_EQ(cropped1.NumRegImages(), 0); EXPECT_EQ(cropped1.NumPoints3D(), 0); // Test reconstruction with contents after cropping bbox.first = Eigen::Vector3d(0.0, 0.0, 0.0); bbox.second = Eigen::Vector3d(0.75, 0.75, 0.75); Reconstruction recon2 = reconstruction.Crop(bbox); EXPECT_EQ(recon2.NumCameras(), 1); EXPECT_EQ(recon2.NumImages(), 3); EXPECT_EQ(recon2.NumRegImages(), 2); EXPECT_EQ(recon2.NumPoints3D(), 3); EXPECT_TRUE(recon2.IsImageRegistered(1)); EXPECT_TRUE(recon2.IsImageRegistered(2)); EXPECT_FALSE(recon2.IsImageRegistered(3)); } TEST(Reconstruction, Transform) { Reconstruction reconstruction; GenerateReconstruction(3, &reconstruction); const point3D_t point3D_id = reconstruction.AddPoint3D(Eigen::Vector3d(1, 1, 1), Track()); reconstruction.AddObservation(point3D_id, TrackElement(1, 1)); reconstruction.AddObservation(point3D_id, TrackElement(2, 1)); reconstruction.Transform( Sim3d(2, Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 1, 2))); EXPECT_EQ(reconstruction.Image(1).ProjectionCenter(), Eigen::Vector3d(0, 1, 2)); EXPECT_EQ(reconstruction.Point3D(point3D_id).xyz, Eigen::Vector3d(2, 3, 4)); } TEST(Reconstruction, FindImageWithName) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); EXPECT_EQ(reconstruction.FindImageWithName("image1"), &reconstruction.Image(1)); EXPECT_EQ(reconstruction.FindImageWithName("image2"), &reconstruction.Image(2)); EXPECT_TRUE(reconstruction.FindImageWithName("image3") == nullptr); } TEST(Reconstruction, FindCommonRegImageIds) { Reconstruction reconstruction1; GenerateReconstruction(5, &reconstruction1); Reconstruction reconstruction2; GenerateReconstruction(5, &reconstruction2); reconstruction1.DeRegisterImage(1); reconstruction1.Image(2).SetName("foo"); reconstruction2.DeRegisterImage(3); reconstruction2.Image(4).SetName("bar"); const auto common_image_ids = reconstruction1.FindCommonRegImageIds(reconstruction2); ASSERT_EQ(common_image_ids.size(), 1); EXPECT_EQ(common_image_ids[0].first, 5); EXPECT_EQ(common_image_ids[0].second, 5); EXPECT_EQ(common_image_ids, reconstruction2.FindCommonRegImageIds(reconstruction1)); } TEST(Reconstruction, FilterPoints3D) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3D(0.0, 0.0, std::unordered_set{}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3D( 0.0, 0.0, std::unordered_set{point3D_id1 + 1}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3D( 0.0, 0.0, std::unordered_set{point3D_id1}); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id2 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); reconstruction.AddObservation(point3D_id2, TrackElement(1, 0)); reconstruction.FilterPoints3D( 0.0, 0.0, std::unordered_set{point3D_id2}); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id3 = reconstruction.AddPoint3D(Eigen::Vector3d(-0.5, -0.5, 1), Track()); reconstruction.AddObservation(point3D_id3, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id3, TrackElement(2, 0)); reconstruction.FilterPoints3D( 0.0, 0.0, std::unordered_set{point3D_id3}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3D( 0.0, 1e-3, std::unordered_set{point3D_id3}); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id4 = reconstruction.AddPoint3D(Eigen::Vector3d(-0.6, -0.5, 1), Track()); reconstruction.AddObservation(point3D_id4, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id4, TrackElement(2, 0)); reconstruction.FilterPoints3D( 0.1, 0.0, std::unordered_set{point3D_id4}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3D( 0.09, 0.0, std::unordered_set{point3D_id4}); EXPECT_EQ(reconstruction.NumPoints3D(), 0); } TEST(Reconstruction, FilterPoints3DInImages) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3DInImages( 0.0, 0.0, std::unordered_set{}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3DInImages( 0.0, 0.0, std::unordered_set{1}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.AddObservation(point3D_id1, TrackElement(1, 0)); reconstruction.FilterPoints3DInImages( 0.0, 0.0, std::unordered_set{2}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3DInImages( 0.0, 0.0, std::unordered_set{1}); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id3 = reconstruction.AddPoint3D(Eigen::Vector3d(-0.5, -0.5, 1), Track()); reconstruction.AddObservation(point3D_id3, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id3, TrackElement(2, 0)); reconstruction.FilterPoints3DInImages( 0.0, 0.0, std::unordered_set{1}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3DInImages( 0.0, 1e-3, std::unordered_set{1}); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id4 = reconstruction.AddPoint3D(Eigen::Vector3d(-0.6, -0.5, 1), Track()); reconstruction.AddObservation(point3D_id4, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id4, TrackElement(2, 0)); reconstruction.FilterPoints3DInImages( 0.1, 0.0, std::unordered_set{1}); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterPoints3DInImages( 0.09, 0.0, std::unordered_set{1}); EXPECT_EQ(reconstruction.NumPoints3D(), 0); } TEST(Reconstruction, FilterAllPoints) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterAllPoints3D(0.0, 0.0); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id2 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); reconstruction.AddObservation(point3D_id2, TrackElement(1, 0)); reconstruction.FilterAllPoints3D(0.0, 0.0); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id3 = reconstruction.AddPoint3D(Eigen::Vector3d(-0.5, -0.5, 1), Track()); reconstruction.AddObservation(point3D_id3, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id3, TrackElement(2, 0)); reconstruction.FilterAllPoints3D(0.0, 0.0); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterAllPoints3D(0.0, 1e-3); EXPECT_EQ(reconstruction.NumPoints3D(), 0); const point3D_t point3D_id4 = reconstruction.AddPoint3D(Eigen::Vector3d(-0.6, -0.5, 1), Track()); reconstruction.AddObservation(point3D_id4, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id4, TrackElement(2, 0)); reconstruction.FilterAllPoints3D(0.1, 0.0); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterAllPoints3D(0.09, 0.0); EXPECT_EQ(reconstruction.NumPoints3D(), 0); } TEST(Reconstruction, FilterObservationsWithNegativeDepth) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, 1), Track()); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.FilterObservationsWithNegativeDepth(); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.Point3D(point3D_id1).xyz(2) = 0.001; reconstruction.FilterObservationsWithNegativeDepth(); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.Point3D(point3D_id1).xyz(2) = 0.0; reconstruction.FilterObservationsWithNegativeDepth(); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.AddObservation(point3D_id1, TrackElement(1, 0)); reconstruction.Point3D(point3D_id1).xyz(2) = 0.001; reconstruction.FilterObservationsWithNegativeDepth(); EXPECT_EQ(reconstruction.NumPoints3D(), 1); reconstruction.Point3D(point3D_id1).xyz(2) = 0.0; reconstruction.FilterObservationsWithNegativeDepth(); EXPECT_EQ(reconstruction.NumPoints3D(), 0); } TEST(Reconstruction, FilterImages) { Reconstruction reconstruction; GenerateReconstruction(4, &reconstruction); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); reconstruction.AddObservation(point3D_id1, TrackElement(1, 0)); reconstruction.AddObservation(point3D_id1, TrackElement(2, 0)); reconstruction.AddObservation(point3D_id1, TrackElement(3, 0)); reconstruction.FilterImages(0.0, 10.0, 1.0); EXPECT_EQ(reconstruction.NumRegImages(), 3); reconstruction.DeleteObservation(3, 0); reconstruction.FilterImages(0.0, 10.0, 1.0); EXPECT_EQ(reconstruction.NumRegImages(), 2); reconstruction.FilterImages(0.0, 0.9, 1.0); EXPECT_EQ(reconstruction.NumRegImages(), 0); } TEST(Reconstruction, ComputeNumObservations) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_EQ(reconstruction.ComputeNumObservations(), 0); reconstruction.AddObservation(point3D_id1, TrackElement(1, 0)); EXPECT_EQ(reconstruction.ComputeNumObservations(), 1); reconstruction.AddObservation(point3D_id1, TrackElement(1, 1)); EXPECT_EQ(reconstruction.ComputeNumObservations(), 2); reconstruction.AddObservation(point3D_id1, TrackElement(2, 0)); EXPECT_EQ(reconstruction.ComputeNumObservations(), 3); } TEST(Reconstruction, ComputeMeanTrackLength) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); EXPECT_EQ(reconstruction.ComputeMeanTrackLength(), 0); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_EQ(reconstruction.ComputeMeanTrackLength(), 0); reconstruction.AddObservation(point3D_id1, TrackElement(1, 0)); EXPECT_EQ(reconstruction.ComputeMeanTrackLength(), 1); reconstruction.AddObservation(point3D_id1, TrackElement(1, 1)); EXPECT_EQ(reconstruction.ComputeMeanTrackLength(), 2); reconstruction.AddObservation(point3D_id1, TrackElement(2, 0)); EXPECT_EQ(reconstruction.ComputeMeanTrackLength(), 3); } TEST(Reconstruction, ComputeMeanObservationsPerRegImage) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); EXPECT_EQ(reconstruction.ComputeMeanObservationsPerRegImage(), 0); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_EQ(reconstruction.ComputeMeanObservationsPerRegImage(), 0); reconstruction.AddObservation(point3D_id1, TrackElement(1, 0)); EXPECT_EQ(reconstruction.ComputeMeanObservationsPerRegImage(), 0.5); reconstruction.AddObservation(point3D_id1, TrackElement(1, 1)); EXPECT_EQ(reconstruction.ComputeMeanObservationsPerRegImage(), 1.0); reconstruction.AddObservation(point3D_id1, TrackElement(2, 0)); EXPECT_EQ(reconstruction.ComputeMeanObservationsPerRegImage(), 1.5); } TEST(Reconstruction, ComputeMeanReprojectionError) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); EXPECT_EQ(reconstruction.ComputeMeanReprojectionError(), 0); const point3D_t point3D_id1 = reconstruction.AddPoint3D(Eigen::Vector3d::Random(), Track()); EXPECT_EQ(reconstruction.ComputeMeanReprojectionError(), 0); reconstruction.Point3D(point3D_id1).error = 0.0; EXPECT_EQ(reconstruction.ComputeMeanReprojectionError(), 0); reconstruction.Point3D(point3D_id1).error = 1.0; EXPECT_EQ(reconstruction.ComputeMeanReprojectionError(), 1); reconstruction.Point3D(point3D_id1).error = 2.0; EXPECT_EQ(reconstruction.ComputeMeanReprojectionError(), 2.0); } TEST(Reconstruction, UpdatePoint3DErrors) { Reconstruction reconstruction; GenerateReconstruction(2, &reconstruction); EXPECT_EQ(reconstruction.ComputeMeanReprojectionError(), 0); Track track; track.AddElement(1, 0); reconstruction.Image(1).Point2D(0).xy = Eigen::Vector2d(0.5, 0.5); const point3D_t point3D_id = reconstruction.AddPoint3D(Eigen::Vector3d(0, 0, 1), track); EXPECT_EQ(reconstruction.Point3D(point3D_id).error, -1); reconstruction.UpdatePoint3DErrors(); EXPECT_EQ(reconstruction.Point3D(point3D_id).error, 0); reconstruction.Point3D(point3D_id).xyz = Eigen::Vector3d(0, 1, 1); reconstruction.UpdatePoint3DErrors(); EXPECT_EQ(reconstruction.Point3D(point3D_id).error, 1); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/scene_clustering.cc000066400000000000000000000304471454702036400220160ustar00rootroot00000000000000// 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. #include "colmap/scene/scene_clustering.h" #include "colmap/math/graph_cut.h" #include "colmap/math/random.h" #include namespace colmap { bool SceneClustering::Options::Check() const { CHECK_OPTION_GT(branching, 0); CHECK_OPTION_GE(image_overlap, 0); return true; } SceneClustering::SceneClustering(const Options& options) : options_(options) { CHECK(options_.Check()); } void SceneClustering::Partition( const std::vector>& image_pairs, const std::vector& num_inliers) { CHECK(!root_cluster_); CHECK_EQ(image_pairs.size(), num_inliers.size()); std::set image_ids; std::vector> edges; edges.reserve(image_pairs.size()); for (const auto& image_pair : image_pairs) { image_ids.insert(image_pair.first); image_ids.insert(image_pair.second); edges.emplace_back(image_pair.first, image_pair.second); } root_cluster_ = std::make_unique(); root_cluster_->image_ids.insert( root_cluster_->image_ids.end(), image_ids.begin(), image_ids.end()); if (options_.is_hierarchical) { PartitionHierarchicalCluster(edges, num_inliers, root_cluster_.get()); } else { PartitionFlatCluster(edges, num_inliers); } } void SceneClustering::PartitionHierarchicalCluster( const std::vector>& edges, const std::vector& weights, Cluster* cluster) { CHECK_EQ(edges.size(), weights.size()); // If the cluster is small enough, we return from the recursive clustering. if (edges.empty() || cluster->image_ids.size() <= static_cast(options_.leaf_max_num_images)) { return; } // Partition the cluster using a normalized cut on the scene graph. const auto labels = ComputeNormalizedMinGraphCut(edges, weights, options_.branching); // Assign the images to the clustered child clusters. cluster->child_clusters.resize(options_.branching); for (const auto image_id : cluster->image_ids) { if (labels.count(image_id)) { auto& child_cluster = cluster->child_clusters.at(labels.at(image_id)); child_cluster.image_ids.push_back(image_id); } else { LOG(WARNING) << "Graph cut failed to assign cluster label to image " << image_id << "; assigning to cluster 0"; cluster->child_clusters.at(0).image_ids.push_back(image_id); } } // Collect the edges based on whether they are inter or intra child clusters. std::vector>> child_edges(options_.branching); std::vector> child_weights(options_.branching); std::vector, int>>> overlapping_edges(options_.branching); for (size_t i = 0; i < edges.size(); ++i) { const int label1 = labels.at(edges[i].first); const int label2 = labels.at(edges[i].second); if (label1 == label2) { child_edges.at(label1).push_back(edges[i]); child_weights.at(label1).push_back(weights[i]); } else { overlapping_edges.at(label1).emplace_back(edges[i], weights[i]); overlapping_edges.at(label2).emplace_back(edges[i], weights[i]); } } // Recursively partition all the child clusters. for (int i = 0; i < options_.branching; ++i) { // Skip empty clusters or clusters where the current cluster has as many // images as its child to avoid infinite loops. This can happen because // the normalized cut sometimes decides to put all images into one // cluster. if (cluster->child_clusters[i].image_ids.empty() || cluster->child_clusters[i].image_ids.size() == cluster->image_ids.size()) { continue; } PartitionHierarchicalCluster( child_edges[i], child_weights[i], &cluster->child_clusters[i]); } // Remove empty clusters. cluster->child_clusters.erase( std::remove_if(cluster->child_clusters.begin(), cluster->child_clusters.end(), [](const Cluster& childCluster) { return childCluster.image_ids.empty(); }), cluster->child_clusters.end()); // If the child cluster is the same as the current cluster, it is redundant // and we can remove it. if (cluster->child_clusters.size() == 1 && cluster->image_ids.size() == cluster->child_clusters[0].image_ids.size()) { cluster->child_clusters = {}; } if (options_.image_overlap > 0) { for (int i = 0; i < options_.branching; ++i) { // Sort the overlapping edges by the number of inlier matches, such // that we add overlapping images with many common observations. std::sort(overlapping_edges[i].begin(), overlapping_edges[i].end(), [](const std::pair, int>& edge1, const std::pair, int>& edge2) { return edge1.second > edge2.second; }); // Select overlapping edges at random and add image to cluster. std::set overlapping_image_ids; for (const auto& edge : overlapping_edges[i]) { if (labels.at(edge.first.first) == i) { overlapping_image_ids.insert(edge.first.second); } else { overlapping_image_ids.insert(edge.first.first); } if (overlapping_image_ids.size() >= static_cast(options_.image_overlap)) { break; } } // Recursively append the overlapping images to cluster and its children. std::function InsertOverlappingImageIds = [&](Cluster* cluster) { cluster->image_ids.insert(cluster->image_ids.end(), overlapping_image_ids.begin(), overlapping_image_ids.end()); for (auto& child_cluster : cluster->child_clusters) { InsertOverlappingImageIds(&child_cluster); } }; InsertOverlappingImageIds(&cluster->child_clusters[i]); } } } void SceneClustering::PartitionFlatCluster( const std::vector>& edges, const std::vector& weights) { CHECK_EQ(edges.size(), weights.size()); // Partition the cluster using a normalized cut on the scene graph. const auto labels = ComputeNormalizedMinGraphCut(edges, weights, options_.branching); // Assign the images to the clustered child clusters. root_cluster_->child_clusters.resize(options_.branching); for (const auto image_id : root_cluster_->image_ids) { if (labels.count(image_id)) { auto& child_cluster = root_cluster_->child_clusters.at(labels.at(image_id)); child_cluster.image_ids.push_back(image_id); } } // Sort child clusters by descending size of images and secondarily by lowest // image id. std::sort(root_cluster_->child_clusters.begin(), root_cluster_->child_clusters.end(), [](const Cluster& first, const Cluster& second) { return first.image_ids.size() >= second.image_ids.size() && *std::min_element(first.image_ids.begin(), first.image_ids.end()) < *std::min_element(second.image_ids.begin(), second.image_ids.end()); }); // For each image find all related images with their weights std::unordered_map>> related_images; for (size_t i = 0; i < edges.size(); ++i) { related_images[edges[i].first].emplace_back(edges[i].second, weights[i]); related_images[edges[i].second].emplace_back(edges[i].first, weights[i]); } // Sort related images by decreasing weights for (auto& image : related_images) { std::sort(image.second.begin(), image.second.end(), [](const std::pair& first, const std::pair& second) { return first.second > second.second; }); } // For each cluster add as many of the needed matching images up to // the max image overal allowance // We do the process sequentially for each image to ensure that at // least we get the best matches firat for (int i = 0; i < options_.branching; ++i) { auto& orig_image_ids = root_cluster_->child_clusters[i].image_ids; std::set cluster_images( root_cluster_->child_clusters[i].image_ids.begin(), root_cluster_->child_clusters[i].image_ids.end()); const size_t max_size = cluster_images.size() + options_.image_overlap; // check up to all the desired matches for (size_t j = 0; j < static_cast(options_.num_image_matches) && cluster_images.size() < max_size; ++j) { for (const image_t image_id : orig_image_ids) { const auto& images = related_images[image_id]; if (j >= images.size()) { continue; } // image not exists in cluster so we add it in the overlap set const int related_id = images[j].first; if (cluster_images.count(related_id) == 0) { cluster_images.insert(related_id); } if (cluster_images.size() >= max_size) { break; } } } orig_image_ids.clear(); orig_image_ids.insert( orig_image_ids.end(), cluster_images.begin(), cluster_images.end()); } } const SceneClustering::Cluster* SceneClustering::GetRootCluster() const { return root_cluster_.get(); } std::vector SceneClustering::GetLeafClusters() const { CHECK(root_cluster_); std::vector leaf_clusters; if (!root_cluster_) { return leaf_clusters; } else if (root_cluster_->child_clusters.empty()) { leaf_clusters.push_back(root_cluster_.get()); return leaf_clusters; } std::vector non_leaf_clusters; non_leaf_clusters.push_back(root_cluster_.get()); while (!non_leaf_clusters.empty()) { const auto cluster = non_leaf_clusters.back(); non_leaf_clusters.pop_back(); for (const auto& child_cluster : cluster->child_clusters) { if (child_cluster.child_clusters.empty()) { leaf_clusters.push_back(&child_cluster); } else { non_leaf_clusters.push_back(&child_cluster); } } } return leaf_clusters; } SceneClustering SceneClustering::Create(const Options& options, const Database& database) { LOG(INFO) << "Reading scene graph..."; std::vector> image_pairs; std::vector num_inliers; database.ReadTwoViewGeometryNumInliers(&image_pairs, &num_inliers); LOG(INFO) << "Partitioning scene graph..."; SceneClustering scene_clustering(options); scene_clustering.Partition(image_pairs, num_inliers); return scene_clustering; } } // namespace colmap colmap-3.9.1/src/colmap/scene/scene_clustering.h000066400000000000000000000070511454702036400216530ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/database.h" #include "colmap/util/types.h" #include #include namespace colmap { // Scene clustering approach using normalized cuts on the scene graph. The scene // is hierarchically partitioned into overlapping clusters until a maximum // number of images is in a leaf node. class SceneClustering { public: struct Options { // Flag for hierarchical vs flat clustering bool is_hierarchical = true; // The branching factor of the hierarchical clustering. int branching = 2; // The number of overlapping images between child clusters. int image_overlap = 50; // The max related images matches to look for in a flat cluster int num_image_matches = 20; // The maximum number of images in a leaf node cluster, otherwise the // cluster is further partitioned using the given branching factor. Note // that a cluster leaf node will have at most `leaf_max_num_images + // overlap` images to satisfy the overlap constraint. int leaf_max_num_images = 500; bool Check() const; }; struct Cluster { std::vector image_ids; std::vector child_clusters; }; explicit SceneClustering(const Options& options); void Partition(const std::vector>& image_pairs, const std::vector& num_inliers); const Cluster* GetRootCluster() const; std::vector GetLeafClusters() const; static SceneClustering Create(const Options& options, const Database& database); private: void PartitionHierarchicalCluster( const std::vector>& edges, const std::vector& weights, Cluster* cluster); void PartitionFlatCluster(const std::vector>& edges, const std::vector& weights); const Options options_; std::unique_ptr root_cluster_; }; } // namespace colmap colmap-3.9.1/src/colmap/scene/scene_clustering_test.cc000066400000000000000000000166611454702036400230570ustar00rootroot00000000000000// 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. #include "colmap/scene/scene_clustering.h" #include "colmap/scene/database.h" #include #include namespace colmap { namespace { TEST(SceneClustering, Empty) { const std::vector> image_pairs; const std::vector num_inliers; SceneClustering::Options options; options.branching = 2; options.image_overlap = 0; options.leaf_max_num_images = 2; SceneClustering scene_clustering(options); EXPECT_TRUE(scene_clustering.GetRootCluster() == nullptr); scene_clustering.Partition(image_pairs, num_inliers); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids.size(), 0); EXPECT_EQ(scene_clustering.GetRootCluster()->child_clusters.size(), 0); EXPECT_EQ(scene_clustering.GetLeafClusters().size(), 1); } TEST(SceneClustering, OneLevel) { const std::vector> image_pairs = {{0, 1}}; const std::vector num_inliers = {10}; SceneClustering::Options options; options.branching = 2; options.image_overlap = 0; options.leaf_max_num_images = 2; SceneClustering scene_clustering(options); EXPECT_TRUE(scene_clustering.GetRootCluster() == nullptr); scene_clustering.Partition(image_pairs, num_inliers); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids.size(), 2); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[0], 0); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[1], 1); EXPECT_EQ(scene_clustering.GetRootCluster()->child_clusters.size(), 0); EXPECT_EQ(scene_clustering.GetLeafClusters().size(), 1); EXPECT_EQ(scene_clustering.GetRootCluster(), scene_clustering.GetLeafClusters()[0]); } TEST(SceneClustering, ThreeFlatClusters) { const std::vector> image_pairs = { {0, 1}, {2, 3}, {4, 5}, {1, 2}, {3, 4}, {5, 0}, {0, 3}, {2, 5}, {4, 1}}; const std::vector num_inliers = {100, 100, 100, 10, 10, 10, 1, 1, 1}; SceneClustering::Options options; options.branching = 3; options.image_overlap = 0; options.branching = 3; options.is_hierarchical = false; SceneClustering scene_clustering(options); EXPECT_TRUE(scene_clustering.GetRootCluster() == nullptr); scene_clustering.Partition(image_pairs, num_inliers); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids.size(), 6); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[0], 0); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[1], 1); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[2], 2); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[3], 3); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[4], 4); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[5], 5); EXPECT_EQ(scene_clustering.GetLeafClusters().size(), 3); EXPECT_EQ(scene_clustering.GetLeafClusters()[0]->image_ids.size(), 2); const std::set image_ids0( scene_clustering.GetLeafClusters()[0]->image_ids.begin(), scene_clustering.GetLeafClusters()[0]->image_ids.end()); EXPECT_TRUE(image_ids0.count(0)); EXPECT_TRUE(image_ids0.count(1)); EXPECT_EQ(scene_clustering.GetLeafClusters()[1]->image_ids.size(), 2); const std::set image_ids1( scene_clustering.GetLeafClusters()[1]->image_ids.begin(), scene_clustering.GetLeafClusters()[1]->image_ids.end()); EXPECT_TRUE(image_ids1.count(2)); EXPECT_TRUE(image_ids1.count(3)); EXPECT_EQ(scene_clustering.GetLeafClusters()[2]->image_ids.size(), 2); const std::set image_ids2( scene_clustering.GetLeafClusters()[2]->image_ids.begin(), scene_clustering.GetLeafClusters()[2]->image_ids.end()); EXPECT_TRUE(image_ids2.count(4)); EXPECT_TRUE(image_ids2.count(5)); } TEST(SceneClustering, ThreeFlatClustersTwoOverlap) { const std::vector> image_pairs = { {0, 1}, {2, 3}, {4, 5}, {1, 2}, {3, 4}, {5, 0}, {0, 3}, {2, 5}, {4, 1}}; const std::vector num_inliers = {100, 100, 100, 10, 10, 10, 1, 1, 1}; SceneClustering::Options options; options.branching = 3; options.image_overlap = 2; options.branching = 3; options.is_hierarchical = false; SceneClustering scene_clustering(options); EXPECT_TRUE(scene_clustering.GetRootCluster() == nullptr); scene_clustering.Partition(image_pairs, num_inliers); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids.size(), 6); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[0], 0); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[1], 1); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[2], 2); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[3], 3); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[4], 4); EXPECT_EQ(scene_clustering.GetRootCluster()->image_ids[5], 5); EXPECT_EQ(scene_clustering.GetLeafClusters().size(), 3); EXPECT_EQ(scene_clustering.GetLeafClusters()[0]->image_ids.size(), 4); const std::set image_ids0( scene_clustering.GetLeafClusters()[0]->image_ids.begin(), scene_clustering.GetLeafClusters()[0]->image_ids.end()); EXPECT_TRUE(image_ids0.count(0)); EXPECT_TRUE(image_ids0.count(1)); EXPECT_TRUE(image_ids0.count(2)); EXPECT_TRUE(image_ids0.count(5)); EXPECT_EQ(scene_clustering.GetLeafClusters()[1]->image_ids.size(), 4); const std::set image_ids1( scene_clustering.GetLeafClusters()[1]->image_ids.begin(), scene_clustering.GetLeafClusters()[1]->image_ids.end()); EXPECT_TRUE(image_ids1.count(1)); EXPECT_TRUE(image_ids1.count(2)); EXPECT_TRUE(image_ids1.count(3)); EXPECT_TRUE(image_ids1.count(4)); EXPECT_EQ(scene_clustering.GetLeafClusters()[2]->image_ids.size(), 4); const std::set image_ids2( scene_clustering.GetLeafClusters()[2]->image_ids.begin(), scene_clustering.GetLeafClusters()[2]->image_ids.end()); EXPECT_TRUE(image_ids2.count(0)); EXPECT_TRUE(image_ids2.count(3)); EXPECT_TRUE(image_ids2.count(4)); EXPECT_TRUE(image_ids2.count(5)); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/synthetic.cc000066400000000000000000000241001454702036400204610ustar00rootroot00000000000000// 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. #include "colmap/scene/synthetic.h" #include "colmap/geometry/essential_matrix.h" #include "colmap/geometry/pose.h" #include "colmap/math/random.h" #include "colmap/scene/projection.h" #include "colmap/util/eigen_alignment.h" #include namespace colmap { namespace { void SynthesizeExhaustiveMatches(Reconstruction* reconstruction, Database* database) { const std::vector& reg_image_ids = reconstruction->RegImageIds(); for (size_t image_idx1 = 0; image_idx1 < reg_image_ids.size(); ++image_idx1) { const auto& image1 = reconstruction->Image(reg_image_ids[image_idx1]); const auto num_points2D1 = image1.NumPoints2D(); for (size_t image_idx2 = 0; image_idx2 < image_idx1; ++image_idx2) { const auto& image2 = reconstruction->Image(reg_image_ids[image_idx2]); const auto num_points2D2 = image2.NumPoints2D(); TwoViewGeometry two_view_geometry; two_view_geometry.config = TwoViewGeometry::CALIBRATED; const Rigid3d cam2_from_cam1 = image2.CamFromWorld() * Inverse(image1.CamFromWorld()); two_view_geometry.E = EssentialMatrixFromPose(cam2_from_cam1); for (point2D_t point2D_idx1 = 0; point2D_idx1 < num_points2D1; ++point2D_idx1) { const auto& point2D1 = image1.Point2D(point2D_idx1); if (!point2D1.HasPoint3D()) { continue; } for (point2D_t point2D_idx2 = 0; point2D_idx2 < num_points2D2; ++point2D_idx2) { const auto& point2D2 = image2.Point2D(point2D_idx2); if (point2D1.point3D_id == point2D2.point3D_id) { two_view_geometry.inlier_matches.emplace_back(point2D_idx1, point2D_idx2); break; } } } database->WriteTwoViewGeometry( image1.ImageId(), image2.ImageId(), two_view_geometry); } } } void SynthesizeChainedMatches(Reconstruction* reconstruction, Database* database) { std::unordered_map two_view_geometries; for (const auto& point3D : reconstruction->Points3D()) { std::vector track_elements = point3D.second.track.Elements(); std::shuffle(track_elements.begin(), track_elements.end(), *PRNG); for (size_t i = 1; i < track_elements.size(); ++i) { const auto& prev_track_el = track_elements[i - 1]; const auto& curr_track_el = track_elements[i]; const image_pair_t pair_id = Database::ImagePairToPairId( prev_track_el.image_id, curr_track_el.image_id); if (Database::SwapImagePair(prev_track_el.image_id, curr_track_el.image_id)) { two_view_geometries[pair_id].inlier_matches.emplace_back( curr_track_el.point2D_idx, prev_track_el.point2D_idx); } else { two_view_geometries[pair_id].inlier_matches.emplace_back( prev_track_el.point2D_idx, curr_track_el.point2D_idx); } } } for (auto& two_view_geometry : two_view_geometries) { image_t image_id1; image_t image_id2; Database::PairIdToImagePair( two_view_geometry.first, &image_id1, &image_id2); const auto& image1 = reconstruction->Image(image_id1); const auto& image2 = reconstruction->Image(image_id2); two_view_geometry.second.config = TwoViewGeometry::CALIBRATED; const Rigid3d cam2_from_cam1 = image2.CamFromWorld() * Inverse(image1.CamFromWorld()); two_view_geometry.second.E = EssentialMatrixFromPose(cam2_from_cam1); database->WriteTwoViewGeometry( image1.ImageId(), image2.ImageId(), two_view_geometry.second); } } } // namespace void SynthesizeDataset(const SyntheticDatasetOptions& options, Reconstruction* reconstruction, Database* database) { CHECK_GT(options.num_cameras, 0); CHECK_GT(options.num_images, 0); CHECK_LE(options.num_cameras, options.num_images); CHECK_GE(options.num_points3D, 0); CHECK_GE(options.num_points2D_without_point3D, 0); CHECK_GE(options.point2D_stddev, 0); // Synthesize cameras. std::vector camera_ids(options.num_cameras); for (int camera_idx = 0; camera_idx < options.num_cameras; ++camera_idx) { Camera camera; camera.width = options.camera_width; camera.height = options.camera_height; camera.model_id = options.camera_model_id; camera.params = options.camera_params; CHECK(camera.VerifyParams()); const camera_t camera_id = (database == nullptr) ? camera_idx + 1 : database->WriteCamera(camera); camera_ids[camera_idx] = camera_id; camera.camera_id = camera_id; reconstruction->AddCamera(std::move(camera)); } // Synthesize 3D points on unit sphere centered at origin. for (int point3D_idx = 0; point3D_idx < options.num_points3D; ++point3D_idx) { reconstruction->AddPoint3D(Eigen::Vector3d::Random().normalized(), /*track=*/{}); } // Synthesize images. const int existing_num_images = (database == nullptr) ? 0 : database->NumImages(); for (int image_idx = 0; image_idx < options.num_images; ++image_idx) { Image image; image.SetName("image" + std::to_string(existing_num_images + image_idx)); image.SetCameraId(camera_ids[image_idx % options.num_cameras]); // Synthesize image poses with projection centers on sphere with radious 5 // centered at origin. const Eigen::Vector3d view_dir = -Eigen::Vector3d::Random().normalized(); const Eigen::Vector3d proj_center = -5 * view_dir; image.CamFromWorld().rotation = Eigen::Quaterniond::FromTwoVectors(view_dir, Eigen::Vector3d(0, 0, 1)); image.CamFromWorld().translation = image.CamFromWorld().rotation * -proj_center; const Camera& camera = reconstruction->Camera(image.CameraId()); std::vector points2D; points2D.reserve(options.num_points3D + options.num_points2D_without_point3D); // Create 3D point observations by project all 3D points to the image. for (auto& point3D : reconstruction->Points3D()) { Point2D point2D; point2D.xy = camera.ImgFromCam( (image.CamFromWorld() * point3D.second.xyz).hnormalized()); if (options.point2D_stddev > 0) { const Eigen::Vector2d noise( RandomGaussian(0, options.point2D_stddev), RandomGaussian(0, options.point2D_stddev)); point2D.xy += noise; } if (point2D.xy(0) >= 0 && point2D.xy(1) >= 0 && point2D.xy(0) <= camera.width && point2D.xy(1) <= camera.height) { point2D.point3D_id = point3D.first; points2D.push_back(point2D); } } // Synthesize uniform random 2D points without 3D points. for (int i = 0; i < options.num_points2D_without_point3D; ++i) { Point2D point2D; point2D.xy = Eigen::Vector2d(RandomUniformReal(0, camera.width), RandomUniformReal(0, camera.height)); points2D.push_back(point2D); } // Shuffle 2D points, so each image has another order of observed 3D points. std::shuffle(points2D.begin(), points2D.end(), *PRNG); const image_t image_id = (database == nullptr) ? image_idx + 1 : database->WriteImage(image); if (database != nullptr) { // Create keypoints to add to database. FeatureKeypoints keypoints; keypoints.reserve(points2D.size()); for (const auto& point2D : points2D) { keypoints.emplace_back(point2D.xy(0), point2D.xy(1)); } database->WriteKeypoints(image_id, keypoints); } for (point2D_t point2D_idx = 0; point2D_idx < points2D.size(); ++point2D_idx) { const auto& point2D = points2D[point2D_idx]; if (point2D.HasPoint3D()) { auto& point3D = reconstruction->Point3D(point2D.point3D_id); point3D.track.AddElement(image_id, point2D_idx); } } image.SetImageId(image_id); image.SetPoints2D(points2D); reconstruction->AddImage(std::move(image)); reconstruction->RegisterImage(image_id); } if (database != nullptr) { switch (options.match_config) { case SyntheticDatasetOptions::MatchConfig::EXHAUSTIVE: SynthesizeExhaustiveMatches(reconstruction, database); break; case SyntheticDatasetOptions::MatchConfig::CHAINED: SynthesizeChainedMatches(reconstruction, database); break; default: LOG(FATAL) << "Invalid MatchConfig specified"; } } reconstruction->UpdatePoint3DErrors(); } } // namespace colmap colmap-3.9.1/src/colmap/scene/synthetic.h000066400000000000000000000051071454702036400203310ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/database.h" #include "colmap/scene/reconstruction.h" #include "colmap/sensor/models.h" #include "colmap/util/types.h" namespace colmap { struct SyntheticDatasetOptions { int num_cameras = 2; int num_images = 10; int num_points3D = 100; int camera_width = 1024; int camera_height = 768; CameraModelId camera_model_id = SimpleRadialCameraModel::model_id; std::vector camera_params = {1280, 512, 384, 0.05}; int num_points2D_without_point3D = 10; double point2D_stddev = 0.0; enum class MatchConfig { // Exhaustive matches between all pairs of observations of a 3D point. EXHAUSTIVE = 1, // Chain of matches with random start/end observations. CHAINED = 2, }; MatchConfig match_config = MatchConfig::EXHAUSTIVE; }; void SynthesizeDataset(const SyntheticDatasetOptions& options, Reconstruction* reconstruction, Database* database = nullptr); } // namespace colmap colmap-3.9.1/src/colmap/scene/synthetic_test.cc000066400000000000000000000152561454702036400215340ustar00rootroot00000000000000// 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. #include "colmap/scene/synthetic.h" #include "colmap/util/misc.h" #include "colmap/util/testing.h" #include namespace colmap { namespace { TEST(SynthesizeDataset, Nominal) { Database database(Database::kInMemoryDatabasePath); Reconstruction reconstruction; SyntheticDatasetOptions options; SynthesizeDataset(options, &reconstruction, &database); const std::string test_dir = CreateTestDir(); const std::string sparse_path = test_dir + "/sparse"; CreateDirIfNotExists(sparse_path); reconstruction.Write(sparse_path); EXPECT_EQ(database.NumCameras(), options.num_cameras); EXPECT_EQ(reconstruction.NumCameras(), options.num_cameras); for (const auto& camera : reconstruction.Cameras()) { EXPECT_EQ(camera.second.ParamsToString(), database.ReadCamera(camera.first).ParamsToString()); } EXPECT_EQ(database.NumImages(), options.num_images); EXPECT_EQ(reconstruction.NumImages(), options.num_images); EXPECT_EQ(reconstruction.NumRegImages(), options.num_images); for (const auto& image : reconstruction.Images()) { EXPECT_EQ(image.second.Name(), database.ReadImage(image.first).Name()); EXPECT_EQ(image.second.NumPoints2D(), database.ReadKeypoints(image.first).size()); EXPECT_EQ(image.second.NumPoints2D(), options.num_points3D + options.num_points2D_without_point3D); EXPECT_EQ(image.second.NumPoints3D(), options.num_points3D); } const int num_image_pairs = options.num_images * (options.num_images - 1) / 2; EXPECT_EQ(database.NumVerifiedImagePairs(), num_image_pairs); EXPECT_EQ(database.NumInlierMatches(), num_image_pairs * options.num_points3D); EXPECT_NEAR(reconstruction.ComputeMeanReprojectionError(), 0, 1e-6); EXPECT_NEAR(reconstruction.ComputeCentroid(0, 1).norm(), 0, 0.2); EXPECT_NEAR(reconstruction.ComputeMeanTrackLength(), options.num_images, 0.1); EXPECT_EQ(reconstruction.ComputeNumObservations(), options.num_images * options.num_points3D); // All observations should be perfect and have sufficient triangulation angle. // No points or observations should be filtered. EXPECT_EQ(reconstruction.FilterAllPoints3D(/*max_reproj_error=*/1e-3, /*min_tri_angle=*/1), 0); } TEST(SynthesizeDataset, WithNoise) { Database database(Database::kInMemoryDatabasePath); Reconstruction reconstruction; SyntheticDatasetOptions options; options.point2D_stddev = 2.0; SynthesizeDataset(options, &reconstruction, &database); EXPECT_NEAR(reconstruction.ComputeMeanReprojectionError(), options.point2D_stddev, 0.5 * options.point2D_stddev); EXPECT_NEAR(reconstruction.ComputeMeanTrackLength(), options.num_images, 0.1); } TEST(SynthesizeDataset, MultiReconstruction) { Database database(Database::kInMemoryDatabasePath); Reconstruction reconstruction1; Reconstruction reconstruction2; SyntheticDatasetOptions options; SynthesizeDataset(options, &reconstruction1, &database); SynthesizeDataset(options, &reconstruction2, &database); EXPECT_EQ(database.NumCameras(), 2 * options.num_cameras); EXPECT_EQ(reconstruction1.NumCameras(), options.num_cameras); EXPECT_EQ(reconstruction1.NumCameras(), options.num_cameras); EXPECT_EQ(database.NumImages(), 2 * options.num_images); EXPECT_EQ(reconstruction1.NumImages(), options.num_images); EXPECT_EQ(reconstruction2.NumImages(), options.num_images); EXPECT_EQ(reconstruction1.NumRegImages(), options.num_images); EXPECT_EQ(reconstruction2.NumRegImages(), options.num_images); const int num_image_pairs = options.num_images * (options.num_images - 1) / 2; EXPECT_EQ(database.NumVerifiedImagePairs(), 2 * num_image_pairs); EXPECT_EQ(database.NumInlierMatches(), 2 * num_image_pairs * options.num_points3D); } TEST(SynthesizeDataset, ExhaustiveMatches) { Database database(Database::kInMemoryDatabasePath); Reconstruction reconstruction; SyntheticDatasetOptions options; options.match_config = SyntheticDatasetOptions::MatchConfig::EXHAUSTIVE; SynthesizeDataset(options, &reconstruction, &database); const int num_image_pairs = options.num_images * (options.num_images - 1) / 2; EXPECT_EQ(database.NumVerifiedImagePairs(), num_image_pairs); EXPECT_EQ(database.NumInlierMatches(), num_image_pairs * options.num_points3D); } TEST(SynthesizeDataset, ChainedMatches) { Database database(Database::kInMemoryDatabasePath); Reconstruction reconstruction; SyntheticDatasetOptions options; options.match_config = SyntheticDatasetOptions::MatchConfig::CHAINED; SynthesizeDataset(options, &reconstruction, &database); const int num_image_pairs = options.num_images * (options.num_images - 1) / 2; EXPECT_EQ(database.NumVerifiedImagePairs(), num_image_pairs); EXPECT_EQ(database.NumInlierMatches(), (options.num_images - 1) * options.num_points3D); } TEST(SynthesizeDataset, NoDatabase) { Database database(Database::kInMemoryDatabasePath); SyntheticDatasetOptions options; Reconstruction reconstruction; SynthesizeDataset(options, &reconstruction); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/track.cc000066400000000000000000000045321454702036400175620ustar00rootroot00000000000000// 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. #include "colmap/scene/track.h" namespace colmap { Track::Track() {} TrackElement::TrackElement() : image_id(kInvalidImageId), point2D_idx(kInvalidPoint2DIdx) {} TrackElement::TrackElement(const image_t image_id, const point2D_t point2D_idx) : image_id(image_id), point2D_idx(point2D_idx) {} void Track::DeleteElement(const image_t image_id, const point2D_t point2D_idx) { elements_.erase( std::remove_if(elements_.begin(), elements_.end(), [image_id, point2D_idx](const TrackElement& element) { return element.image_id == image_id && element.point2D_idx == point2D_idx; }), elements_.end()); } } // namespace colmap colmap-3.9.1/src/colmap/scene/track.h000066400000000000000000000111041454702036400174150ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include "colmap/util/types.h" #include namespace colmap { // Track class stores all observations of a 3D point. struct TrackElement { TrackElement(); TrackElement(image_t image_id, point2D_t point2D_idx); // The image in which the track element is observed. image_t image_id; // The point in the image that the track element is observed. point2D_t point2D_idx; }; class Track { public: Track(); // The number of track elements. inline size_t Length() const; // Access all elements. inline const std::vector& Elements() const; inline std::vector& Elements(); inline void SetElements(std::vector elements); // Access specific elements. inline const TrackElement& Element(size_t idx) const; inline TrackElement& Element(size_t idx); inline void SetElement(size_t idx, const TrackElement& element); // Append new elements. inline void AddElement(const TrackElement& element); inline void AddElement(image_t image_id, point2D_t point2D_idx); inline void AddElements(const std::vector& elements); // Delete existing element. inline void DeleteElement(size_t idx); void DeleteElement(image_t image_id, point2D_t point2D_idx); // Requests that the track capacity be at least enough to contain the // specified number of elements. inline void Reserve(size_t num_elements); // Shrink the capacity of track vector to fit its size to save memory. inline void Compress(); private: std::vector elements_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// size_t Track::Length() const { return elements_.size(); } const std::vector& Track::Elements() const { return elements_; } std::vector& Track::Elements() { return elements_; } void Track::SetElements(std::vector elements) { elements_ = std::move(elements); } // Access specific elements. const TrackElement& Track::Element(const size_t idx) const { return elements_.at(idx); } TrackElement& Track::Element(const size_t idx) { return elements_.at(idx); } void Track::SetElement(const size_t idx, const TrackElement& element) { elements_.at(idx) = element; } void Track::AddElement(const TrackElement& element) { elements_.push_back(element); } void Track::AddElement(const image_t image_id, const point2D_t point2D_idx) { elements_.emplace_back(image_id, point2D_idx); } void Track::AddElements(const std::vector& elements) { elements_.insert(elements_.end(), elements.begin(), elements.end()); } void Track::DeleteElement(const size_t idx) { CHECK_LT(idx, elements_.size()); elements_.erase(elements_.begin() + idx); } void Track::Reserve(const size_t num_elements) { elements_.reserve(num_elements); } void Track::Compress() { elements_.shrink_to_fit(); } } // namespace colmap colmap-3.9.1/src/colmap/scene/track_test.cc000066400000000000000000000113671454702036400206250ustar00rootroot00000000000000// 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. #include "colmap/scene/track.h" #include namespace colmap { namespace { TEST(TrackElement, Empty) { TrackElement track_el; EXPECT_EQ(track_el.image_id, kInvalidImageId); EXPECT_EQ(track_el.point2D_idx, kInvalidPoint2DIdx); } TEST(Track, Default) { Track track; EXPECT_EQ(track.Length(), 0); EXPECT_EQ(track.Elements().size(), track.Length()); } TEST(Track, SetElements) { Track track; std::vector elements; elements.emplace_back(0, 1); elements.emplace_back(0, 2); track.SetElements(elements); EXPECT_EQ(track.Length(), 2); EXPECT_EQ(track.Elements().size(), track.Length()); EXPECT_EQ(track.Element(0).image_id, 0); EXPECT_EQ(track.Element(0).point2D_idx, 1); EXPECT_EQ(track.Element(1).image_id, 0); EXPECT_EQ(track.Element(1).point2D_idx, 2); for (size_t i = 0; i < track.Length(); ++i) { EXPECT_EQ(track.Element(i).image_id, track.Elements()[i].image_id); EXPECT_EQ(track.Element(i).point2D_idx, track.Elements()[i].point2D_idx); } } TEST(Track, AddElement) { Track track; track.AddElement(0, 1); track.AddElement(TrackElement(0, 2)); std::vector elements; elements.emplace_back(0, 1); elements.emplace_back(0, 2); track.AddElements(elements); EXPECT_EQ(track.Length(), 4); EXPECT_EQ(track.Elements().size(), track.Length()); EXPECT_EQ(track.Element(0).image_id, 0); EXPECT_EQ(track.Element(0).point2D_idx, 1); EXPECT_EQ(track.Element(1).image_id, 0); EXPECT_EQ(track.Element(1).point2D_idx, 2); EXPECT_EQ(track.Element(2).image_id, 0); EXPECT_EQ(track.Element(2).point2D_idx, 1); EXPECT_EQ(track.Element(3).image_id, 0); EXPECT_EQ(track.Element(3).point2D_idx, 2); for (size_t i = 0; i < track.Length(); ++i) { EXPECT_EQ(track.Element(i).image_id, track.Elements()[i].image_id); EXPECT_EQ(track.Element(i).point2D_idx, track.Elements()[i].point2D_idx); } } TEST(Track, DeleteElement) { Track track; track.AddElement(0, 1); track.AddElement(0, 2); track.AddElement(0, 3); track.AddElement(0, 3); EXPECT_EQ(track.Length(), 4); EXPECT_EQ(track.Elements().size(), track.Length()); track.DeleteElement(0); EXPECT_EQ(track.Length(), 3); EXPECT_EQ(track.Elements().size(), track.Length()); EXPECT_EQ(track.Element(0).image_id, 0); EXPECT_EQ(track.Element(0).point2D_idx, 2); EXPECT_EQ(track.Element(1).image_id, 0); EXPECT_EQ(track.Element(1).point2D_idx, 3); EXPECT_EQ(track.Element(2).image_id, 0); EXPECT_EQ(track.Element(2).point2D_idx, 3); track.DeleteElement(0, 3); EXPECT_EQ(track.Length(), 1); EXPECT_EQ(track.Elements().size(), track.Length()); EXPECT_EQ(track.Element(0).image_id, 0); EXPECT_EQ(track.Element(0).point2D_idx, 2); } TEST(Track, Reserve) { Track track; track.Reserve(2); EXPECT_EQ(track.Elements().capacity(), 2); } TEST(Track, Compress) { Track track; track.AddElement(0, 1); track.AddElement(0, 2); track.AddElement(0, 3); track.AddElement(0, 3); EXPECT_EQ(track.Elements().capacity(), 4); track.DeleteElement(0); track.DeleteElement(0); EXPECT_EQ(track.Elements().capacity(), 4); track.Compress(); EXPECT_EQ(track.Elements().capacity(), 2); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/two_view_geometry.cc000066400000000000000000000036641454702036400222410ustar00rootroot00000000000000// 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. #include "colmap/scene/two_view_geometry.h" namespace colmap { void TwoViewGeometry::Invert() { F.transposeInPlace(); E.transposeInPlace(); H = H.inverse().eval(); cam2_from_cam1 = Inverse(cam2_from_cam1); for (auto& match : inlier_matches) { std::swap(match.point2D_idx1, match.point2D_idx2); } } } // namespace colmap colmap-3.9.1/src/colmap/scene/two_view_geometry.h000066400000000000000000000057731454702036400221060ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/types.h" #include "colmap/geometry/rigid3.h" namespace colmap { // Two-view geometry. struct TwoViewGeometry { // The configuration of the two-view geometry. enum ConfigurationType { UNDEFINED = 0, // Degenerate configuration (e.g., no overlap or not enough inliers). DEGENERATE = 1, // Essential matrix. CALIBRATED = 2, // Fundamental matrix. UNCALIBRATED = 3, // Homography, planar scene with baseline. PLANAR = 4, // Homography, pure rotation without baseline. PANORAMIC = 5, // Homography, planar or panoramic. PLANAR_OR_PANORAMIC = 6, // Watermark, pure 2D translation in image borders. WATERMARK = 7, // Multi-model configuration, i.e. the inlier matches result from multiple // individual, non-degenerate configurations. MULTIPLE = 8, }; // One of `ConfigurationType`. int config = ConfigurationType::UNDEFINED; // Essential matrix. Eigen::Matrix3d E = Eigen::Matrix3d::Zero(); // Fundamental matrix. Eigen::Matrix3d F = Eigen::Matrix3d::Zero(); // Homography matrix. Eigen::Matrix3d H = Eigen::Matrix3d::Zero(); // Relative pose. Rigid3d cam2_from_cam1; // Inlier matches of the configuration. FeatureMatches inlier_matches; // Median triangulation angle. double tri_angle = -1; // Invert the geometry to match swapped cameras. void Invert(); }; } // namespace colmap colmap-3.9.1/src/colmap/scene/two_view_geometry_test.cc000066400000000000000000000106211454702036400232670ustar00rootroot00000000000000// 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. #include "colmap/scene/two_view_geometry.h" #include "colmap/geometry/pose.h" #include namespace colmap { namespace { TEST(TwoViewGeometry, Default) { TwoViewGeometry two_view_geometry; EXPECT_EQ(two_view_geometry.config, TwoViewGeometry::UNDEFINED); EXPECT_EQ(two_view_geometry.F, Eigen::Matrix3d::Zero()); EXPECT_EQ(two_view_geometry.E, Eigen::Matrix3d::Zero()); EXPECT_EQ(two_view_geometry.H, Eigen::Matrix3d::Zero()); EXPECT_EQ(two_view_geometry.cam2_from_cam1.rotation.coeffs(), Eigen::Quaterniond::Identity().coeffs()); EXPECT_EQ(two_view_geometry.cam2_from_cam1.translation, Eigen::Vector3d::Zero()); EXPECT_TRUE(two_view_geometry.inlier_matches.empty()); } TEST(TwoViewGeometry, Invert) { TwoViewGeometry two_view_geometry; two_view_geometry.config = TwoViewGeometry::CALIBRATED; two_view_geometry.F = two_view_geometry.E = two_view_geometry.H = Eigen::Matrix3d::Identity(); two_view_geometry.cam2_from_cam1 = Rigid3d(Eigen::Quaterniond::Identity(), Eigen::Vector3d(0, 1, 2)); two_view_geometry.inlier_matches.resize(2); two_view_geometry.inlier_matches[0] = FeatureMatch(0, 1); two_view_geometry.inlier_matches[1] = FeatureMatch(2, 3); two_view_geometry.Invert(); EXPECT_EQ(two_view_geometry.config, TwoViewGeometry::CALIBRATED); EXPECT_TRUE(two_view_geometry.F.isApprox(Eigen::Matrix3d::Identity())); EXPECT_TRUE(two_view_geometry.E.isApprox(Eigen::Matrix3d::Identity())); EXPECT_TRUE(two_view_geometry.H.isApprox(Eigen::Matrix3d::Identity())); EXPECT_TRUE(two_view_geometry.cam2_from_cam1.rotation.isApprox( Eigen::Quaterniond::Identity())); EXPECT_TRUE(two_view_geometry.cam2_from_cam1.translation.isApprox( Eigen::Vector3d(-0, -1, -2))); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 1); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 0); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx1, 3); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx2, 2); two_view_geometry.Invert(); EXPECT_EQ(two_view_geometry.config, TwoViewGeometry::CALIBRATED); EXPECT_TRUE(two_view_geometry.F.isApprox(Eigen::Matrix3d::Identity())); EXPECT_TRUE(two_view_geometry.E.isApprox(Eigen::Matrix3d::Identity())); EXPECT_TRUE(two_view_geometry.H.isApprox(Eigen::Matrix3d::Identity())); EXPECT_TRUE(two_view_geometry.cam2_from_cam1.rotation.isApprox( Eigen::Quaterniond::Identity())); EXPECT_TRUE(two_view_geometry.cam2_from_cam1.translation.isApprox( Eigen::Vector3d(0, 1, 2))); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx1, 0); EXPECT_EQ(two_view_geometry.inlier_matches[0].point2D_idx2, 1); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx1, 2); EXPECT_EQ(two_view_geometry.inlier_matches[1].point2D_idx2, 3); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/scene/visibility_pyramid.cc000066400000000000000000000071141454702036400223710ustar00rootroot00000000000000// 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. #include "colmap/scene/visibility_pyramid.h" #include "colmap/math/math.h" #include "colmap/util/logging.h" namespace colmap { VisibilityPyramid::VisibilityPyramid() : VisibilityPyramid(0, 0, 0) {} VisibilityPyramid::VisibilityPyramid(const size_t num_levels, const size_t width, const size_t height) : width_(width), height_(height), score_(0), max_score_(0) { pyramid_.resize(num_levels); for (size_t level = 0; level < num_levels; ++level) { const size_t level_plus_one = level + 1; const int dim = 1 << level_plus_one; pyramid_[level].setZero(dim, dim); max_score_ += dim * dim * dim * dim; } } void VisibilityPyramid::SetPoint(const double x, const double y) { CHECK_GT(pyramid_.size(), 0); size_t cx = 0; size_t cy = 0; CellForPoint(x, y, &cx, &cy); for (int i = static_cast(pyramid_.size() - 1); i >= 0; --i) { auto& level = pyramid_[i]; level(cy, cx) += 1; if (level(cy, cx) == 1) { score_ += level.size(); } cx = cx >> 1; cy = cy >> 1; } CHECK_LE(score_, max_score_); } void VisibilityPyramid::ResetPoint(const double x, const double y) { CHECK_GT(pyramid_.size(), 0); size_t cx = 0; size_t cy = 0; CellForPoint(x, y, &cx, &cy); for (int i = static_cast(pyramid_.size() - 1); i >= 0; --i) { auto& level = pyramid_[i]; level(cy, cx) -= 1; if (level(cy, cx) == 0) { score_ -= level.size(); } cx = cx >> 1; cy = cy >> 1; } CHECK_LE(score_, max_score_); } void VisibilityPyramid::CellForPoint(const double x, const double y, size_t* cx, size_t* cy) const { CHECK_GT(width_, 0); CHECK_GT(height_, 0); const int max_dim = 1 << pyramid_.size(); *cx = Clamp(max_dim * x / width_, 0, max_dim - 1); *cy = Clamp(max_dim * y / height_, 0, max_dim - 1); } } // namespace colmap colmap-3.9.1/src/colmap/scene/visibility_pyramid.h000066400000000000000000000071231454702036400222330ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include #include namespace colmap { // A class that captures the distribution of points in a 2D grid. // For example, to capture the distribution of visible 3D points in an image. // // The class captures the distribution of points by a score. A higher score // corresponds to a more uniform distribution of the points in the grid. // // The score is computed by the number of populated cells in a multi-resolution // pyramid. A populated cell contributes to the overall score if it is // populated by at least one point and the contributed score is according // to its resolution in the pyramid. A cell in a higher resolution level // contributes a higher score to the overall score. class VisibilityPyramid { public: VisibilityPyramid(); VisibilityPyramid(size_t num_levels, size_t width, size_t height); void SetPoint(double x, double y); void ResetPoint(double x, double y); inline size_t NumLevels() const; inline size_t Width() const; inline size_t Height() const; inline size_t Score() const; inline size_t MaxScore() const; private: void CellForPoint(double x, double y, size_t* cx, size_t* cy) const; // Range of the input points. size_t width_; size_t height_; // The overall visibility score. size_t score_; // The maximum score when all cells are populated. size_t max_score_; // The visibilty pyramid with multiple levels. std::vector pyramid_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// size_t VisibilityPyramid::NumLevels() const { return pyramid_.size(); } size_t VisibilityPyramid::Width() const { return width_; } size_t VisibilityPyramid::Height() const { return height_; } size_t VisibilityPyramid::Score() const { return score_; } size_t VisibilityPyramid::MaxScore() const { return max_score_; } } // namespace colmap colmap-3.9.1/src/colmap/scene/visibility_pyramid_test.cc000066400000000000000000000070401454702036400234260ustar00rootroot00000000000000// 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. #include "colmap/scene/visibility_pyramid.h" #include namespace colmap { namespace { TEST(VisibilityPyramid, Default) { VisibilityPyramid pyramid; EXPECT_EQ(pyramid.NumLevels(), 0); EXPECT_EQ(pyramid.Width(), 0); EXPECT_EQ(pyramid.Height(), 0); EXPECT_EQ(pyramid.Score(), 0); } TEST(VisibilityPyramid, Score) { for (int num_levels = 1; num_levels < 8; ++num_levels) { Eigen::VectorXi scores(num_levels); size_t max_score = 0; for (int i = 1; i <= num_levels; ++i) { scores(i - 1) = (1 << i) * (1 << i); max_score += scores(i - 1) * scores(i - 1); } VisibilityPyramid pyramid(static_cast(num_levels), 4, 4); EXPECT_EQ(pyramid.NumLevels(), num_levels); EXPECT_EQ(pyramid.Width(), 4); EXPECT_EQ(pyramid.Height(), 4); EXPECT_EQ(pyramid.Score(), 0); EXPECT_EQ(pyramid.MaxScore(), max_score); EXPECT_EQ(pyramid.Score(), 0); pyramid.SetPoint(0, 0); EXPECT_EQ(pyramid.Score(), scores.sum()); pyramid.SetPoint(0, 0); EXPECT_EQ(pyramid.Score(), scores.sum()); pyramid.SetPoint(0, 1); EXPECT_EQ(pyramid.Score(), scores.sum() + scores.tail(scores.size() - 1).sum()); pyramid.SetPoint(0, 1); pyramid.SetPoint(0, 1); pyramid.SetPoint(1, 0); EXPECT_EQ(pyramid.Score(), scores.sum() + 2 * scores.tail(scores.size() - 1).sum()); pyramid.SetPoint(1, 0); pyramid.SetPoint(1, 1); EXPECT_EQ(pyramid.Score(), scores.sum() + 3 * scores.tail(scores.size() - 1).sum()); pyramid.ResetPoint(0, 0); EXPECT_EQ(pyramid.Score(), scores.sum() + 3 * scores.tail(scores.size() - 1).sum()); pyramid.ResetPoint(0, 0); EXPECT_EQ(pyramid.Score(), scores.sum() + 2 * scores.tail(scores.size() - 1).sum()); pyramid.SetPoint(0, 2); EXPECT_EQ(pyramid.Score(), 2 * scores.sum() + 2 * scores.tail(scores.size() - 1).sum()); } } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/sensor/000077500000000000000000000000001454702036400163575ustar00rootroot00000000000000colmap-3.9.1/src/colmap/sensor/CMakeLists.txt000066400000000000000000000043411454702036400211210ustar00rootroot00000000000000# 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. set(FOLDER_NAME "sensor") COLMAP_ADD_LIBRARY( NAME colmap_sensor SRCS bitmap.h bitmap.cc database.h database.cc models.h models.cc specs.h specs.cc PUBLIC_LINK_LIBS Ceres::ceres Eigen3::Eigen PRIVATE_LINK_LIBS colmap_util colmap_vlfeat freeimage::FreeImage ) COLMAP_ADD_TEST( NAME bitmap_test SRCS bitmap_test.cc LINK_LIBS colmap_sensor freeimage::FreeImage ) COLMAP_ADD_TEST( NAME database_test SRCS database_test.cc LINK_LIBS colmap_sensor ) COLMAP_ADD_TEST( NAME models_test SRCS models_test.cc LINK_LIBS colmap_sensor ) colmap-3.9.1/src/colmap/sensor/bitmap.cc000066400000000000000000000546031454702036400201520ustar00rootroot00000000000000// 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. #include "colmap/sensor/bitmap.h" #include "colmap/math/math.h" #include "colmap/sensor/database.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include "thirdparty/VLFeat/imopv.h" #include #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include #endif #include namespace colmap { namespace { #ifdef FREEIMAGE_LIB // Only needed for static FreeImage. struct FreeImageInitializer { FreeImageInitializer() { FreeImage_Initialise(); } ~FreeImageInitializer() { FreeImage_DeInitialise(); } }; const static auto initializer = FreeImageInitializer(); #endif // FREEIMAGE_LIB bool ReadExifTag(FIBITMAP* ptr, const FREE_IMAGE_MDMODEL model, const std::string& tag_name, std::string* result) { FITAG* tag = nullptr; FreeImage_GetMetadata(model, ptr, tag_name.c_str(), &tag); if (tag == nullptr) { *result = ""; return false; } else { if (tag_name == "FocalPlaneXResolution") { // This tag seems to be in the wrong category. *result = std::string(FreeImage_TagToString(FIMD_EXIF_INTEROP, tag)); } else { *result = FreeImage_TagToString(model, tag); } return true; } } bool IsPtrGrey(FIBITMAP* ptr) { return FreeImage_GetColorType(ptr) == FIC_MINISBLACK && FreeImage_GetBPP(ptr) == 8; } bool IsPtrRGB(FIBITMAP* ptr) { return FreeImage_GetColorType(ptr) == FIC_RGB && FreeImage_GetBPP(ptr) == 24; } bool IsPtrSupported(FIBITMAP* ptr) { return IsPtrGrey(ptr) || IsPtrRGB(ptr); } } // namespace Bitmap::Bitmap() : width_(0), height_(0), channels_(0) {} Bitmap::Bitmap(const Bitmap& other) : Bitmap() { if (other.handle_.ptr != nullptr) { SetPtr(FreeImage_Clone(other.handle_.ptr)); } } Bitmap::Bitmap(Bitmap&& other) noexcept : Bitmap() { handle_ = std::move(other.handle_); width_ = other.width_; height_ = other.height_; channels_ = other.channels_; other.width_ = 0; other.height_ = 0; other.channels_ = 0; } Bitmap::Bitmap(FIBITMAP* data) : Bitmap() { SetPtr(data); } Bitmap& Bitmap::operator=(const Bitmap& other) { if (other.handle_.ptr != nullptr) { SetPtr(FreeImage_Clone(other.handle_.ptr)); } return *this; } Bitmap& Bitmap::operator=(Bitmap&& other) noexcept { if (this != &other) { handle_ = std::move(other.handle_); width_ = other.width_; height_ = other.height_; channels_ = other.channels_; other.width_ = 0; other.height_ = 0; other.channels_ = 0; } return *this; } bool Bitmap::Allocate(const int width, const int height, const bool as_rgb) { width_ = width; height_ = height; if (as_rgb) { const int kNumBitsPerPixel = 24; handle_ = FreeImageHandle(FreeImage_Allocate(width, height, kNumBitsPerPixel)); channels_ = 3; } else { const int kNumBitsPerPixel = 8; handle_ = FreeImageHandle(FreeImage_Allocate(width, height, kNumBitsPerPixel)); channels_ = 1; } return handle_.ptr != nullptr; } void Bitmap::Deallocate() { handle_ = FreeImageHandle(); width_ = 0; height_ = 0; channels_ = 0; } size_t Bitmap::NumBytes() const { if (handle_.ptr != nullptr) { return ScanWidth() * height_; } else { return 0; } } unsigned int Bitmap::BitsPerPixel() const { return FreeImage_GetBPP(handle_.ptr); } unsigned int Bitmap::ScanWidth() const { return FreeImage_GetPitch(handle_.ptr); } std::vector Bitmap::ConvertToRawBits() const { const unsigned int scan_width = ScanWidth(); const unsigned int bpp = BitsPerPixel(); const bool kTopDown = true; std::vector raw_bits(scan_width * height_, 0); FreeImage_ConvertToRawBits(raw_bits.data(), handle_.ptr, scan_width, bpp, FI_RGBA_RED_MASK, FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK, kTopDown); return raw_bits; } std::vector Bitmap::ConvertToRowMajorArray() const { std::vector array(width_ * height_ * channels_); size_t i = 0; for (int y = 0; y < height_; ++y) { const uint8_t* line = FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); for (int x = 0; x < width_; ++x) { for (int d = 0; d < channels_; ++d) { array[i] = line[x * channels_ + d]; i += 1; } } } return array; } std::vector Bitmap::ConvertToColMajorArray() const { std::vector array(width_ * height_ * channels_); size_t i = 0; for (int d = 0; d < channels_; ++d) { for (int x = 0; x < width_; ++x) { for (int y = 0; y < height_; ++y) { const uint8_t* line = FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); array[i] = line[x * channels_ + d]; i += 1; } } } return array; } bool Bitmap::GetPixel(const int x, const int y, BitmapColor* color) const { if (x < 0 || x >= width_ || y < 0 || y >= height_) { return false; } const uint8_t* line = FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); if (IsGrey()) { color->r = line[x]; return true; } else if (IsRGB()) { color->r = line[3 * x + FI_RGBA_RED]; color->g = line[3 * x + FI_RGBA_GREEN]; color->b = line[3 * x + FI_RGBA_BLUE]; return true; } return false; } bool Bitmap::SetPixel(const int x, const int y, const BitmapColor& color) { if (x < 0 || x >= width_ || y < 0 || y >= height_) { return false; } uint8_t* line = FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); if (IsGrey()) { line[x] = color.r; return true; } else if (IsRGB()) { line[3 * x + FI_RGBA_RED] = color.r; line[3 * x + FI_RGBA_GREEN] = color.g; line[3 * x + FI_RGBA_BLUE] = color.b; return true; } return false; } const uint8_t* Bitmap::GetScanline(const int y) const { CHECK_GE(y, 0); CHECK_LT(y, height_); return FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); } void Bitmap::Fill(const BitmapColor& color) { for (int y = 0; y < height_; ++y) { uint8_t* line = FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); for (int x = 0; x < width_; ++x) { if (IsGrey()) { line[x] = color.r; } else if (IsRGB()) { line[3 * x + FI_RGBA_RED] = color.r; line[3 * x + FI_RGBA_GREEN] = color.g; line[3 * x + FI_RGBA_BLUE] = color.b; } } } } bool Bitmap::InterpolateNearestNeighbor(const double x, const double y, BitmapColor* color) const { const int xx = static_cast(std::round(x)); const int yy = static_cast(std::round(y)); return GetPixel(xx, yy, color); } bool Bitmap::InterpolateBilinear(const double x, const double y, BitmapColor* color) const { // FreeImage's coordinate system origin is in the lower left of the image. const double inv_y = height_ - 1 - y; const int x0 = static_cast(std::floor(x)); const int x1 = x0 + 1; const int y0 = static_cast(std::floor(inv_y)); const int y1 = y0 + 1; if (x0 < 0 || x1 >= width_ || y0 < 0 || y1 >= height_) { return false; } const double dx = x - x0; const double dy = inv_y - y0; const double dx_1 = 1 - dx; const double dy_1 = 1 - dy; const uint8_t* line0 = FreeImage_GetScanLine(handle_.ptr, y0); const uint8_t* line1 = FreeImage_GetScanLine(handle_.ptr, y1); if (IsGrey()) { // Top row, column-wise linear interpolation. const double v0 = dx_1 * line0[x0] + dx * line0[x1]; // Bottom row, column-wise linear interpolation. const double v1 = dx_1 * line1[x0] + dx * line1[x1]; // Row-wise linear interpolation. color->r = dy_1 * v0 + dy * v1; return true; } else if (IsRGB()) { const uint8_t* p00 = &line0[3 * x0]; const uint8_t* p01 = &line0[3 * x1]; const uint8_t* p10 = &line1[3 * x0]; const uint8_t* p11 = &line1[3 * x1]; // Top row, column-wise linear interpolation. const double v0_r = dx_1 * p00[FI_RGBA_RED] + dx * p01[FI_RGBA_RED]; const double v0_g = dx_1 * p00[FI_RGBA_GREEN] + dx * p01[FI_RGBA_GREEN]; const double v0_b = dx_1 * p00[FI_RGBA_BLUE] + dx * p01[FI_RGBA_BLUE]; // Bottom row, column-wise linear interpolation. const double v1_r = dx_1 * p10[FI_RGBA_RED] + dx * p11[FI_RGBA_RED]; const double v1_g = dx_1 * p10[FI_RGBA_GREEN] + dx * p11[FI_RGBA_GREEN]; const double v1_b = dx_1 * p10[FI_RGBA_BLUE] + dx * p11[FI_RGBA_BLUE]; // Row-wise linear interpolation. color->r = dy_1 * v0_r + dy * v1_r; color->g = dy_1 * v0_g + dy * v1_g; color->b = dy_1 * v0_b + dy * v1_b; return true; } return false; } bool Bitmap::ExifCameraModel(std::string* camera_model) const { // Read camera make and model std::string make_str; std::string model_str; std::string focal_length; *camera_model = ""; if (ReadExifTag(handle_.ptr, FIMD_EXIF_MAIN, "Make", &make_str)) { *camera_model += (make_str + "-"); } else { *camera_model = ""; return false; } if (ReadExifTag(handle_.ptr, FIMD_EXIF_MAIN, "Model", &model_str)) { *camera_model += (model_str + "-"); } else { *camera_model = ""; return false; } if (ReadExifTag(handle_.ptr, FIMD_EXIF_EXIF, "FocalLengthIn35mmFilm", &focal_length) || ReadExifTag(handle_.ptr, FIMD_EXIF_EXIF, "FocalLength", &focal_length)) { *camera_model += (focal_length + "-"); } else { *camera_model = ""; return false; } *camera_model += (std::to_string(width_) + "x" + std::to_string(height_)); return true; } bool Bitmap::ExifFocalLength(double* focal_length) const { const double max_size = std::max(width_, height_); ////////////////////////////////////////////////////////////////////////////// // Focal length in 35mm equivalent ////////////////////////////////////////////////////////////////////////////// std::string focal_length_35mm_str; if (ReadExifTag(handle_.ptr, FIMD_EXIF_EXIF, "FocalLengthIn35mmFilm", &focal_length_35mm_str)) { const std::regex regex(".*?([0-9.]+).*?mm.*?"); std::cmatch result; if (std::regex_search(focal_length_35mm_str.c_str(), result, regex)) { const double focal_length_35 = std::stold(result[1]); if (focal_length_35 > 0) { *focal_length = focal_length_35 / 35.0 * max_size; return true; } } } ////////////////////////////////////////////////////////////////////////////// // Focal length in mm ////////////////////////////////////////////////////////////////////////////// std::string focal_length_str; if (ReadExifTag( handle_.ptr, FIMD_EXIF_EXIF, "FocalLength", &focal_length_str)) { std::regex regex(".*?([0-9.]+).*?mm"); std::cmatch result; if (std::regex_search(focal_length_str.c_str(), result, regex)) { const double focal_length_mm = std::stold(result[1]); // Lookup sensor width in database. std::string make_str; std::string model_str; if (ReadExifTag(handle_.ptr, FIMD_EXIF_MAIN, "Make", &make_str) && ReadExifTag(handle_.ptr, FIMD_EXIF_MAIN, "Model", &model_str)) { CameraDatabase database; double sensor_width; if (database.QuerySensorWidth(make_str, model_str, &sensor_width)) { *focal_length = focal_length_mm / sensor_width * max_size; return true; } } // Extract sensor width from EXIF. std::string pixel_x_dim_str; std::string x_res_str; std::string res_unit_str; if (ReadExifTag(handle_.ptr, FIMD_EXIF_EXIF, "PixelXDimension", &pixel_x_dim_str) && ReadExifTag(handle_.ptr, FIMD_EXIF_EXIF, "FocalPlaneXResolution", &x_res_str) && ReadExifTag(handle_.ptr, FIMD_EXIF_EXIF, "FocalPlaneResolutionUnit", &res_unit_str)) { regex = std::regex(".*?([0-9.]+).*?"); if (std::regex_search(pixel_x_dim_str.c_str(), result, regex)) { const double pixel_x_dim = std::stold(result[1]); regex = std::regex(".*?([0-9.]+).*?/.*?([0-9.]+).*?"); if (std::regex_search(x_res_str.c_str(), result, regex)) { const double x_res = std::stold(result[2]) / std::stold(result[1]); // Use PixelXDimension instead of actual width of image, since // the image might have been resized, but the EXIF data preserved. const double ccd_width = x_res * pixel_x_dim; if (ccd_width > 0 && focal_length_mm > 0) { if (res_unit_str == "cm") { *focal_length = focal_length_mm / (ccd_width * 10.0) * max_size; return true; } else if (res_unit_str == "inches") { *focal_length = focal_length_mm / (ccd_width * 25.4) * max_size; return true; } } } } } } } return false; } bool Bitmap::ExifLatitude(double* latitude) const { std::string str; double sign = 1.0; if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLatitudeRef", &str)) { StringTrim(&str); StringToLower(&str); if (!str.empty() && str[0] == 's') { sign = -1.0; } } if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLatitude", &str)) { const std::regex regex(".*?([0-9.]+):([0-9.]+):([0-9.]+).*?"); std::cmatch result; if (std::regex_search(str.c_str(), result, regex)) { const double hours = std::stold(result[1]); const double minutes = std::stold(result[2]); const double seconds = std::stold(result[3]); double value = hours + minutes / 60.0 + seconds / 3600.0; if (value > 0 && sign < 0) { value *= sign; } *latitude = value; return true; } } return false; } bool Bitmap::ExifLongitude(double* longitude) const { std::string str; double sign = 1.0; if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLongitudeRef", &str)) { StringTrim(&str); StringToLower(&str); if (!str.empty() && str[0] == 'w') { sign = -1.0; } } if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLongitude", &str)) { const std::regex regex(".*?([0-9.]+):([0-9.]+):([0-9.]+).*?"); std::cmatch result; if (std::regex_search(str.c_str(), result, regex)) { const double hours = std::stold(result[1]); const double minutes = std::stold(result[2]); const double seconds = std::stold(result[3]); double value = hours + minutes / 60.0 + seconds / 3600.0; if (value > 0 && sign < 0) { value *= sign; } *longitude = value; return true; } } return false; } bool Bitmap::ExifAltitude(double* altitude) const { std::string str; if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSAltitude", &str)) { const std::regex regex(".*?([0-9.]+).*?/.*?([0-9.]+).*?"); std::cmatch result; if (std::regex_search(str.c_str(), result, regex)) { *altitude = std::stold(result[1]) / std::stold(result[2]); return true; } } return false; } bool Bitmap::Read(const std::string& path, const bool as_rgb) { if (!ExistsFile(path)) { return false; } const FREE_IMAGE_FORMAT format = FreeImage_GetFileType(path.c_str(), 0); if (format == FIF_UNKNOWN) { return false; } handle_ = FreeImageHandle(FreeImage_Load(format, path.c_str())); if (handle_.ptr == nullptr) { return false; } if (!IsPtrRGB(handle_.ptr) && as_rgb) { FIBITMAP* converted_bitmap = FreeImage_ConvertTo24Bits(handle_.ptr); handle_ = FreeImageHandle(converted_bitmap); } else if (!IsPtrGrey(handle_.ptr) && !as_rgb) { FIBITMAP* converted_bitmap = FreeImage_ConvertToGreyscale(handle_.ptr); handle_ = FreeImageHandle(converted_bitmap); } if (!IsPtrSupported(handle_.ptr)) { handle_ = FreeImageHandle(); return false; } width_ = FreeImage_GetWidth(handle_.ptr); height_ = FreeImage_GetHeight(handle_.ptr); channels_ = as_rgb ? 3 : 1; return true; } bool Bitmap::Write(const std::string& path, const int flags) const { FREE_IMAGE_FORMAT save_format = FreeImage_GetFIFFromFilename(path.c_str()); if (save_format == FIF_UNKNOWN) { // If format could not be deduced, save as PNG by default. save_format = FIF_PNG; } int save_flags = flags; if (save_format == FIF_JPEG && flags == 0) { // Use superb JPEG quality by default to avoid artifacts. save_flags = JPEG_QUALITYSUPERB; } bool success = false; if (save_flags == 0) { success = FreeImage_Save(save_format, handle_.ptr, path.c_str()); } else { success = FreeImage_Save(save_format, handle_.ptr, path.c_str(), save_flags); } return success; } void Bitmap::Smooth(const float sigma_x, const float sigma_y) { std::vector array(width_ * height_); std::vector array_smoothed(width_ * height_); for (int d = 0; d < channels_; ++d) { size_t i = 0; for (int y = 0; y < height_; ++y) { const uint8_t* line = FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); for (int x = 0; x < width_; ++x) { array[i] = line[x * channels_ + d]; i += 1; } } vl_imsmooth_f(array_smoothed.data(), width_, array.data(), width_, height_, width_, sigma_x, sigma_y); i = 0; for (int y = 0; y < height_; ++y) { uint8_t* line = FreeImage_GetScanLine(handle_.ptr, height_ - 1 - y); for (int x = 0; x < width_; ++x) { line[x * channels_ + d] = TruncateCast(array_smoothed[i]); i += 1; } } } } void Bitmap::Rescale(const int new_width, const int new_height, RescaleFilter filter) { FREE_IMAGE_FILTER fi_filter = FILTER_BILINEAR; switch (filter) { case RescaleFilter::kBilinear: fi_filter = FILTER_BILINEAR; break; case RescaleFilter::kBox: fi_filter = FILTER_BOX; break; default: LOG(FATAL) << "Filter not implemented"; } SetPtr(FreeImage_Rescale(handle_.ptr, new_width, new_height, fi_filter)); } Bitmap Bitmap::Clone() const { FIBITMAP* cloned = FreeImage_Clone(handle_.ptr); return Bitmap(cloned); } Bitmap Bitmap::CloneAsGrey() const { if (IsGrey()) { return Clone(); } else { return Bitmap(FreeImage_ConvertToGreyscale(handle_.ptr)); } } Bitmap Bitmap::CloneAsRGB() const { if (IsRGB()) { return Clone(); } else { return Bitmap(FreeImage_ConvertTo24Bits(handle_.ptr)); } } void Bitmap::CloneMetadata(Bitmap* target) const { CHECK_NOTNULL(target); CHECK_NOTNULL(target->Data()); FreeImage_CloneMetadata(handle_.ptr, target->Data()); } void Bitmap::SetPtr(FIBITMAP* ptr) { CHECK_NOTNULL(ptr); if (!IsPtrSupported(ptr)) { FreeImageHandle temp_handle(ptr); ptr = FreeImage_ConvertTo24Bits(temp_handle.ptr); CHECK(IsPtrSupported(ptr)); } handle_ = FreeImageHandle(ptr); width_ = FreeImage_GetWidth(handle_.ptr); height_ = FreeImage_GetHeight(handle_.ptr); channels_ = IsPtrRGB(handle_.ptr) ? 3 : 1; } Bitmap::FreeImageHandle::FreeImageHandle() : ptr(nullptr) {} Bitmap::FreeImageHandle::FreeImageHandle(FIBITMAP* ptr) : ptr(ptr) {} Bitmap::FreeImageHandle::~FreeImageHandle() { if (ptr != nullptr) { FreeImage_Unload(ptr); ptr = nullptr; } } Bitmap::FreeImageHandle::FreeImageHandle( Bitmap::FreeImageHandle&& other) noexcept { ptr = other.ptr; other.ptr = nullptr; } Bitmap::FreeImageHandle& Bitmap::FreeImageHandle::operator=( Bitmap::FreeImageHandle&& other) noexcept { if (this != &other) { if (ptr != nullptr) { FreeImage_Unload(ptr); } ptr = other.ptr; other.ptr = nullptr; } return *this; } float JetColormap::Red(const float gray) { return Base(gray - 0.25f); } float JetColormap::Green(const float gray) { return Base(gray); } float JetColormap::Blue(const float gray) { return Base(gray + 0.25f); } float JetColormap::Base(const float val) { // NOLINTNEXTLINE(bugprone-branch-clone) if (val <= 0.125f) { return 0.0f; } else if (val <= 0.375f) { return Interpolate(2.0f * val - 1.0f, 0.0f, -0.75f, 1.0f, -0.25f); } else if (val <= 0.625f) { return 1.0f; } else if (val <= 0.87f) { return Interpolate(2.0f * val - 1.0f, 1.0f, 0.25f, 0.0f, 0.75f); } else { return 0.0f; } } float JetColormap::Interpolate(const float val, const float y0, const float x0, const float y1, const float x1) { return (val - x0) * (y1 - y0) / (x1 - x0) + y0; } } // namespace colmap colmap-3.9.1/src/colmap/sensor/bitmap.h000066400000000000000000000213131454702036400200040ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/string.h" #include #include #include #include #include #include #include struct FIBITMAP; namespace colmap { // Templated bitmap color class. template struct BitmapColor { BitmapColor(); explicit BitmapColor(T gray); BitmapColor(T r, T g, T b); template BitmapColor Cast() const; bool operator==(const BitmapColor& rhs) const; bool operator!=(const BitmapColor& rhs) const; template friend std::ostream& operator<<(std::ostream& output, const BitmapColor& color); T r; T g; T b; }; // Wrapper class around FreeImage bitmaps. class Bitmap { public: Bitmap(); // Copy constructor. Bitmap(const Bitmap& other); // Move constructor. Bitmap(Bitmap&& other) noexcept; // Create bitmap object from existing FreeImage bitmap object. Note that // this class takes ownership of the object. explicit Bitmap(FIBITMAP* data); // Copy assignment. Bitmap& operator=(const Bitmap& other); // Move assignment. Bitmap& operator=(Bitmap&& other) noexcept; // Allocate bitmap by overwriting the existing data. bool Allocate(int width, int height, bool as_rgb); // Deallocate the bitmap by releasing the existing data. void Deallocate(); // Get pointer to underlying FreeImage object. inline const FIBITMAP* Data() const; inline FIBITMAP* Data(); // Dimensions of bitmap. inline int Width() const; inline int Height() const; inline int Channels() const; // Number of bits per pixel. This is 8 for grey and 24 for RGB image. unsigned int BitsPerPixel() const; // Scan width of bitmap which differs from the actual image width to achieve // 32 bit aligned memory. Also known as pitch or stride. unsigned int ScanWidth() const; // Check whether image is grey- or colorscale. inline bool IsRGB() const; inline bool IsGrey() const; // Number of bytes required to store image. size_t NumBytes() const; // Copy raw image data to array. std::vector ConvertToRawBits() const; std::vector ConvertToRowMajorArray() const; std::vector ConvertToColMajorArray() const; // Manipulate individual pixels. For grayscale images, only the red element // of the RGB color is used. bool GetPixel(int x, int y, BitmapColor* color) const; bool SetPixel(int x, int y, const BitmapColor& color); // Get pointer to y-th scanline, where the 0-th scanline is at the top. const uint8_t* GetScanline(int y) const; // Fill entire bitmap with uniform color. For grayscale images, the first // element of the vector is used. void Fill(const BitmapColor& color); // Interpolate color at given floating point position. bool InterpolateNearestNeighbor(double x, double y, BitmapColor* color) const; bool InterpolateBilinear(double x, double y, BitmapColor* color) const; // Extract EXIF information from bitmap. Returns false if no EXIF information // is embedded in the bitmap. bool ExifCameraModel(std::string* camera_model) const; bool ExifFocalLength(double* focal_length) const; bool ExifLatitude(double* latitude) const; bool ExifLongitude(double* longitude) const; bool ExifAltitude(double* altitude) const; // Read bitmap at given path and convert to grey- or colorscale. bool Read(const std::string& path, bool as_rgb = true); // Write image to file. Flags can be used to set e.g. the JPEG quality. // Consult the FreeImage documentation for all available flags. bool Write(const std::string& path, int flags = 0) const; // Smooth the image using a Gaussian kernel. void Smooth(float sigma_x, float sigma_y); // Rescale image to the new dimensions. enum class RescaleFilter { kBilinear, kBox, }; void Rescale(int new_width, int new_height, RescaleFilter filter = RescaleFilter::kBilinear); // Clone the image to a new bitmap object. Bitmap Clone() const; Bitmap CloneAsGrey() const; Bitmap CloneAsRGB() const; // Clone metadata from this bitmap object to another target bitmap object. void CloneMetadata(Bitmap* target) const; private: struct FreeImageHandle { FreeImageHandle(); explicit FreeImageHandle(FIBITMAP* ptr); ~FreeImageHandle(); FreeImageHandle(FreeImageHandle&&) noexcept; FreeImageHandle& operator=(FreeImageHandle&&) noexcept; FreeImageHandle(const FreeImageHandle&) = delete; FreeImageHandle& operator=(const FreeImageHandle&) = delete; FIBITMAP* ptr; }; void SetPtr(FIBITMAP* ptr); FreeImageHandle handle_; int width_; int height_; int channels_; }; // Jet colormap inspired by Matlab. Grayvalues are expected in the range [0, 1] // and are converted to RGB values in the same range. class JetColormap { public: static float Red(float gray); static float Green(float gray); static float Blue(float gray); private: static float Interpolate(float val, float y0, float x0, float y1, float x1); static float Base(float val); }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// namespace internal { template T2 BitmapColorCast(const T1 value) { return std::min(static_cast(std::numeric_limits::max()), std::max(static_cast(std::numeric_limits::min()), std::round(value))); } } // namespace internal template BitmapColor::BitmapColor() : r(0), g(0), b(0) {} template BitmapColor::BitmapColor(const T gray) : r(gray), g(gray), b(gray) {} template BitmapColor::BitmapColor(const T r, const T g, const T b) : r(r), g(g), b(b) {} template template BitmapColor BitmapColor::Cast() const { BitmapColor color; color.r = internal::BitmapColorCast(r); color.g = internal::BitmapColorCast(g); color.b = internal::BitmapColorCast(b); return color; } template bool BitmapColor::operator==(const BitmapColor& rhs) const { return r == rhs.r && g == rhs.g && b == rhs.b; } template bool BitmapColor::operator!=(const BitmapColor& rhs) const { return r != rhs.r || g != rhs.g || b != rhs.b; } template std::ostream& operator<<(std::ostream& output, const BitmapColor& color) { output << StringPrintf("RGB(%f, %f, %f)", static_cast(color.r), static_cast(color.g), static_cast(color.b)); return output; } FIBITMAP* Bitmap::Data() { return handle_.ptr; } const FIBITMAP* Bitmap::Data() const { return handle_.ptr; } int Bitmap::Width() const { return width_; } int Bitmap::Height() const { return height_; } int Bitmap::Channels() const { return channels_; } bool Bitmap::IsRGB() const { return channels_ == 3; } bool Bitmap::IsGrey() const { return channels_ == 1; } } // namespace colmap colmap-3.9.1/src/colmap/sensor/bitmap_test.cc000066400000000000000000000400631454702036400212040ustar00rootroot00000000000000// 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. #include "colmap/sensor/bitmap.h" #include "colmap/util/testing.h" #include #include namespace colmap { namespace { TEST(Bitmap, BitmapColorEmpty) { BitmapColor color; EXPECT_EQ(color.r, 0); EXPECT_EQ(color.g, 0); EXPECT_EQ(color.b, 0); EXPECT_EQ(color, BitmapColor(0)); EXPECT_EQ(color, BitmapColor(0, 0, 0)); } TEST(Bitmap, BitmapGrayColor) { BitmapColor color(5); EXPECT_EQ(color.r, 5); EXPECT_EQ(color.g, 5); EXPECT_EQ(color.b, 5); } TEST(Bitmap, BitmapColorCast) { BitmapColor color1(1.1, 2.9, -3.0); BitmapColor color2 = color1.Cast(); EXPECT_EQ(color2.r, 1); EXPECT_EQ(color2.g, 3); EXPECT_EQ(color2.b, 0); } TEST(Bitmap, Empty) { Bitmap bitmap; EXPECT_EQ(bitmap.Width(), 0); EXPECT_EQ(bitmap.Height(), 0); EXPECT_EQ(bitmap.Channels(), 0); EXPECT_FALSE(bitmap.IsRGB()); EXPECT_FALSE(bitmap.IsGrey()); } TEST(Bitmap, AllocateRGB) { Bitmap bitmap; bitmap.Allocate(100, 100, true); EXPECT_EQ(bitmap.Width(), 100); EXPECT_EQ(bitmap.Height(), 100); EXPECT_EQ(bitmap.Channels(), 3); EXPECT_TRUE(bitmap.IsRGB()); EXPECT_FALSE(bitmap.IsGrey()); } TEST(Bitmap, AllocateGrey) { Bitmap bitmap; bitmap.Allocate(100, 100, false); EXPECT_EQ(bitmap.Width(), 100); EXPECT_EQ(bitmap.Height(), 100); EXPECT_EQ(bitmap.Channels(), 1); EXPECT_FALSE(bitmap.IsRGB()); EXPECT_TRUE(bitmap.IsGrey()); } TEST(Bitmap, Deallocate) { Bitmap bitmap; bitmap.Allocate(100, 100, false); bitmap.Deallocate(); EXPECT_EQ(bitmap.Width(), 0); EXPECT_EQ(bitmap.Height(), 0); EXPECT_EQ(bitmap.Channels(), 0); EXPECT_EQ(bitmap.NumBytes(), 0); EXPECT_FALSE(bitmap.IsRGB()); EXPECT_FALSE(bitmap.IsGrey()); } TEST(Bitmap, MoveConstruct) { Bitmap bitmap; bitmap.Allocate(2, 1, true); const auto* data = bitmap.Data(); Bitmap moved_bitmap(std::move(bitmap)); EXPECT_EQ(moved_bitmap.Width(), 2); EXPECT_EQ(moved_bitmap.Height(), 1); EXPECT_EQ(moved_bitmap.Channels(), 3); EXPECT_EQ(moved_bitmap.Data(), data); // NOLINTBEGIN(bugprone-use-after-move,clang-analyzer-cplusplus.Move) EXPECT_EQ(bitmap.Width(), 0); EXPECT_EQ(bitmap.Height(), 0); EXPECT_EQ(bitmap.Channels(), 0); EXPECT_EQ(bitmap.NumBytes(), 0); EXPECT_EQ(bitmap.Data(), nullptr); // NOLINTEND(bugprone-use-after-move,clang-analyzer-cplusplus.Move) } TEST(Bitmap, MoveAssign) { Bitmap bitmap; bitmap.Allocate(2, 1, true); const auto* data = bitmap.Data(); Bitmap moved_bitmap = std::move(bitmap); EXPECT_EQ(moved_bitmap.Width(), 2); EXPECT_EQ(moved_bitmap.Height(), 1); EXPECT_EQ(moved_bitmap.Channels(), 3); EXPECT_EQ(moved_bitmap.Data(), data); // NOLINTBEGIN(bugprone-use-after-move,clang-analyzer-cplusplus.Move) EXPECT_EQ(bitmap.Width(), 0); EXPECT_EQ(bitmap.Height(), 0); EXPECT_EQ(bitmap.Channels(), 0); EXPECT_EQ(bitmap.NumBytes(), 0); EXPECT_EQ(bitmap.Data(), nullptr); // NOLINTEND(bugprone-use-after-move,clang-analyzer-cplusplus.Move) } TEST(Bitmap, BitsPerPixel) { Bitmap bitmap; bitmap.Allocate(100, 100, true); EXPECT_EQ(bitmap.BitsPerPixel(), 24); bitmap.Allocate(100, 100, false); EXPECT_EQ(bitmap.BitsPerPixel(), 8); } TEST(Bitmap, NumBytes) { Bitmap bitmap; EXPECT_EQ(bitmap.NumBytes(), 0); bitmap.Allocate(100, 100, true); EXPECT_EQ(bitmap.NumBytes(), 3 * 100 * 100); bitmap.Allocate(100, 100, false); EXPECT_EQ(bitmap.NumBytes(), 100 * 100); } TEST(Bitmap, ConvertToRowMajorArrayRGB) { Bitmap bitmap; bitmap.Allocate(2, 2, true); bitmap.SetPixel(0, 0, BitmapColor(0, 0, 0)); bitmap.SetPixel(0, 1, BitmapColor(1, 0, 0)); bitmap.SetPixel(1, 0, BitmapColor(2, 0, 0)); bitmap.SetPixel(1, 1, BitmapColor(3, 0, 0)); const std::vector array = bitmap.ConvertToRowMajorArray(); EXPECT_EQ(array.size(), 12); EXPECT_EQ(array[0], 0); EXPECT_EQ(array[1], 0); EXPECT_EQ(array[2], 0); EXPECT_EQ(array[3], 0); EXPECT_EQ(array[4], 0); EXPECT_EQ(array[5], 2); EXPECT_EQ(array[6], 0); EXPECT_EQ(array[7], 0); EXPECT_EQ(array[8], 1); EXPECT_EQ(array[9], 0); EXPECT_EQ(array[10], 0); EXPECT_EQ(array[11], 3); } TEST(Bitmap, ConvertToRowMajorArrayGrey) { Bitmap bitmap; bitmap.Allocate(2, 2, false); bitmap.SetPixel(0, 0, BitmapColor(0, 0, 0)); bitmap.SetPixel(0, 1, BitmapColor(1, 0, 0)); bitmap.SetPixel(1, 0, BitmapColor(2, 0, 0)); bitmap.SetPixel(1, 1, BitmapColor(3, 0, 0)); const std::vector array = bitmap.ConvertToRowMajorArray(); EXPECT_EQ(array.size(), 4); EXPECT_EQ(array[0], 0); EXPECT_EQ(array[1], 2); EXPECT_EQ(array[2], 1); EXPECT_EQ(array[3], 3); } TEST(Bitmap, ConvertToColMajorArrayRGB) { Bitmap bitmap; bitmap.Allocate(2, 2, true); bitmap.SetPixel(0, 0, BitmapColor(0, 0, 0)); bitmap.SetPixel(0, 1, BitmapColor(1, 0, 0)); bitmap.SetPixel(1, 0, BitmapColor(2, 0, 0)); bitmap.SetPixel(1, 1, BitmapColor(3, 0, 0)); const std::vector array = bitmap.ConvertToColMajorArray(); EXPECT_EQ(array.size(), 12); EXPECT_EQ(array[0], 0); EXPECT_EQ(array[1], 0); EXPECT_EQ(array[2], 0); EXPECT_EQ(array[3], 0); EXPECT_EQ(array[4], 0); EXPECT_EQ(array[5], 0); EXPECT_EQ(array[6], 0); EXPECT_EQ(array[7], 0); EXPECT_EQ(array[8], 0); EXPECT_EQ(array[9], 1); EXPECT_EQ(array[10], 2); EXPECT_EQ(array[11], 3); } TEST(Bitmap, ConvertToColMajorArrayGrey) { Bitmap bitmap; bitmap.Allocate(2, 2, false); bitmap.SetPixel(0, 0, BitmapColor(0, 0, 0)); bitmap.SetPixel(0, 1, BitmapColor(1, 0, 0)); bitmap.SetPixel(1, 0, BitmapColor(2, 0, 0)); bitmap.SetPixel(1, 1, BitmapColor(3, 0, 0)); const std::vector array = bitmap.ConvertToColMajorArray(); EXPECT_EQ(array.size(), 4); EXPECT_EQ(array[0], 0); EXPECT_EQ(array[1], 1); EXPECT_EQ(array[2], 2); EXPECT_EQ(array[3], 3); } TEST(Bitmap, GetAndSetPixelRGB) { Bitmap bitmap; bitmap.Allocate(1, 1, true); bitmap.SetPixel(0, 0, BitmapColor(1, 2, 3)); BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(0, 0, &color)); EXPECT_EQ(color, BitmapColor(1, 2, 3)); } TEST(Bitmap, GetAndSetPixelGrey) { Bitmap bitmap; bitmap.Allocate(1, 1, false); bitmap.SetPixel(0, 0, BitmapColor(0, 2, 3)); BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(0, 0, &color)); EXPECT_EQ(color, BitmapColor(0, 0, 0)); bitmap.SetPixel(0, 0, BitmapColor(1, 2, 3)); EXPECT_TRUE(bitmap.GetPixel(0, 0, &color)); EXPECT_EQ(color, BitmapColor(1, 0, 0)); } TEST(Bitmap, GetScanlineRGB) { Bitmap bitmap; bitmap.Allocate(3, 3, true); bitmap.Fill(BitmapColor(1, 2, 3)); for (size_t r = 0; r < 3; ++r) { const uint8_t* scanline = bitmap.GetScanline(r); for (size_t c = 0; c < 3; ++c) { BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(r, c, &color)); EXPECT_EQ(scanline[c * 3 + FI_RGBA_RED], color.r); EXPECT_EQ(scanline[c * 3 + FI_RGBA_GREEN], color.g); EXPECT_EQ(scanline[c * 3 + FI_RGBA_BLUE], color.b); } } } TEST(Bitmap, GetScanlineGrey) { Bitmap bitmap; bitmap.Allocate(3, 3, false); bitmap.Fill(BitmapColor(1, 2, 3)); for (size_t r = 0; r < 3; ++r) { const uint8_t* scanline = bitmap.GetScanline(r); for (size_t c = 0; c < 3; ++c) { BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(r, c, &color)); EXPECT_EQ(scanline[c], color.r); } } } TEST(Bitmap, Fill) { Bitmap bitmap; bitmap.Allocate(100, 100, true); bitmap.Fill(BitmapColor(1, 2, 3)); for (int y = 0; y < bitmap.Height(); ++y) { for (int x = 0; x < bitmap.Width(); ++x) { BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(x, y, &color)); EXPECT_EQ(color, BitmapColor(1, 2, 3)); } } } TEST(Bitmap, InterpolateNearestNeighbor) { Bitmap bitmap; bitmap.Allocate(11, 11, true); bitmap.Fill(BitmapColor(0, 0, 0)); bitmap.SetPixel(5, 5, BitmapColor(1, 2, 3)); BitmapColor color; EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5, 5, &color)); EXPECT_EQ(color, BitmapColor(1, 2, 3)); EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5.4999, 5.4999, &color)); EXPECT_EQ(color, BitmapColor(1, 2, 3)); EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5.5, 5.5, &color)); EXPECT_EQ(color, BitmapColor(0, 0, 0)); EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(4.5, 5.4999, &color)); EXPECT_EQ(color, BitmapColor(1, 2, 3)); } TEST(Bitmap, InterpolateBilinear) { Bitmap bitmap; bitmap.Allocate(11, 11, true); bitmap.Fill(BitmapColor(0, 0, 0)); bitmap.SetPixel(5, 5, BitmapColor(1, 2, 3)); BitmapColor color; EXPECT_TRUE(bitmap.InterpolateBilinear(5, 5, &color)); EXPECT_EQ(color, BitmapColor(1, 2, 3)); EXPECT_TRUE(bitmap.InterpolateBilinear(5.5, 5, &color)); EXPECT_EQ(color, BitmapColor(0.5, 1, 1.5)); EXPECT_TRUE(bitmap.InterpolateBilinear(5.5, 5.5, &color)); EXPECT_EQ(color, BitmapColor(0.25, 0.5, 0.75)); } TEST(Bitmap, SmoothRGB) { Bitmap bitmap; bitmap.Allocate(50, 50, true); for (int x = 0; x < 50; ++x) { for (int y = 0; y < 50; ++y) { bitmap.SetPixel( x, y, BitmapColor(y * 50 + x, y * 50 + x, y * 50 + x)); } } bitmap.Smooth(1, 1); EXPECT_EQ(bitmap.Width(), 50); EXPECT_EQ(bitmap.Height(), 50); EXPECT_EQ(bitmap.Channels(), 3); for (int x = 0; x < 50; ++x) { for (int y = 0; y < 50; ++y) { BitmapColor color; EXPECT_TRUE(bitmap.GetPixel(x, y, &color)); EXPECT_EQ(color.r, color.g); EXPECT_EQ(color.r, color.b); } } } TEST(Bitmap, SmoothGrey) { Bitmap bitmap; bitmap.Allocate(50, 50, false); for (int x = 0; x < 50; ++x) { for (int y = 0; y < 50; ++y) { bitmap.SetPixel( x, y, BitmapColor(y * 50 + x, y * 50 + x, y * 50 + x)); } } bitmap.Smooth(1, 1); EXPECT_EQ(bitmap.Width(), 50); EXPECT_EQ(bitmap.Height(), 50); EXPECT_EQ(bitmap.Channels(), 1); } TEST(Bitmap, RescaleRGB) { Bitmap bitmap; bitmap.Allocate(100, 100, true); Bitmap bitmap1 = bitmap.Clone(); bitmap1.Rescale(50, 25); EXPECT_EQ(bitmap1.Width(), 50); EXPECT_EQ(bitmap1.Height(), 25); EXPECT_EQ(bitmap1.Channels(), 3); Bitmap bitmap2 = bitmap.Clone(); bitmap2.Rescale(150, 20); EXPECT_EQ(bitmap2.Width(), 150); EXPECT_EQ(bitmap2.Height(), 20); EXPECT_EQ(bitmap2.Channels(), 3); } TEST(Bitmap, RescaleGrey) { Bitmap bitmap; bitmap.Allocate(100, 100, false); Bitmap bitmap1 = bitmap.Clone(); bitmap1.Rescale(50, 25); EXPECT_EQ(bitmap1.Width(), 50); EXPECT_EQ(bitmap1.Height(), 25); EXPECT_EQ(bitmap1.Channels(), 1); Bitmap bitmap2 = bitmap.Clone(); bitmap2.Rescale(150, 20); EXPECT_EQ(bitmap2.Width(), 150); EXPECT_EQ(bitmap2.Height(), 20); EXPECT_EQ(bitmap2.Channels(), 1); } TEST(Bitmap, Clone) { Bitmap bitmap; bitmap.Allocate(100, 100, true); const Bitmap cloned_bitmap = bitmap.Clone(); EXPECT_EQ(cloned_bitmap.Width(), 100); EXPECT_EQ(cloned_bitmap.Height(), 100); EXPECT_EQ(cloned_bitmap.Channels(), 3); EXPECT_NE(bitmap.Data(), cloned_bitmap.Data()); } TEST(Bitmap, CloneAsRGB) { Bitmap bitmap; bitmap.Allocate(100, 100, false); const Bitmap cloned_bitmap = bitmap.CloneAsRGB(); EXPECT_EQ(cloned_bitmap.Width(), 100); EXPECT_EQ(cloned_bitmap.Height(), 100); EXPECT_EQ(cloned_bitmap.Channels(), 3); EXPECT_NE(bitmap.Data(), cloned_bitmap.Data()); } TEST(Bitmap, CloneAsGrey) { Bitmap bitmap; bitmap.Allocate(100, 100, true); const Bitmap cloned_bitmap = bitmap.CloneAsGrey(); EXPECT_EQ(cloned_bitmap.Width(), 100); EXPECT_EQ(cloned_bitmap.Height(), 100); EXPECT_EQ(cloned_bitmap.Channels(), 1); EXPECT_NE(bitmap.Data(), cloned_bitmap.Data()); } TEST(Bitmap, ReadWriteAsRGB) { Bitmap bitmap; bitmap.Allocate(2, 3, true); bitmap.SetPixel(0, 0, BitmapColor(0, 0, 0)); bitmap.SetPixel(0, 1, BitmapColor(1, 0, 0)); bitmap.SetPixel(1, 0, BitmapColor(2, 0, 0)); bitmap.SetPixel(1, 1, BitmapColor(3, 0, 0)); bitmap.SetPixel(0, 2, BitmapColor(4, 2, 0)); bitmap.SetPixel(1, 2, BitmapColor(5, 2, 1)); const std::string test_dir = CreateTestDir(); const std::string filename = test_dir + "/bitmap.png"; EXPECT_TRUE(bitmap.Write(filename)); Bitmap read_bitmap; // Allocate bitmap with different size to test read overwrites existing data. read_bitmap.Allocate(bitmap.Width() + 1, bitmap.Height() + 2, true); EXPECT_TRUE(read_bitmap.Read(filename)); EXPECT_EQ(read_bitmap.Width(), bitmap.Width()); EXPECT_EQ(read_bitmap.Height(), bitmap.Height()); EXPECT_EQ(read_bitmap.Channels(), 3); EXPECT_EQ(read_bitmap.BitsPerPixel(), 24); EXPECT_EQ(read_bitmap.ConvertToRowMajorArray(), bitmap.ConvertToRowMajorArray()); EXPECT_TRUE(read_bitmap.Read(filename, /*as_rgb=*/false)); EXPECT_EQ(read_bitmap.Width(), bitmap.Width()); EXPECT_EQ(read_bitmap.Height(), bitmap.Height()); EXPECT_EQ(read_bitmap.Channels(), 1); EXPECT_EQ(read_bitmap.BitsPerPixel(), 8); EXPECT_EQ(read_bitmap.ConvertToRowMajorArray(), bitmap.CloneAsGrey().ConvertToRowMajorArray()); } TEST(Bitmap, ReadWriteAsGrey) { Bitmap bitmap; bitmap.Allocate(2, 3, false); bitmap.SetPixel(0, 0, BitmapColor(0)); bitmap.SetPixel(0, 1, BitmapColor(1)); bitmap.SetPixel(1, 0, BitmapColor(2)); bitmap.SetPixel(1, 1, BitmapColor(3)); bitmap.SetPixel(0, 2, BitmapColor(4)); bitmap.SetPixel(1, 2, BitmapColor(5)); const std::string test_dir = CreateTestDir(); const std::string filename = test_dir + "/bitmap.png"; EXPECT_TRUE(bitmap.Write(filename)); Bitmap read_bitmap; // Allocate bitmap with different size to test read overwrites existing data. read_bitmap.Allocate(bitmap.Width() + 1, bitmap.Height() + 2, true); EXPECT_TRUE(read_bitmap.Read(filename)); EXPECT_EQ(read_bitmap.Width(), bitmap.Width()); EXPECT_EQ(read_bitmap.Height(), bitmap.Height()); EXPECT_EQ(read_bitmap.Channels(), 3); EXPECT_EQ(read_bitmap.BitsPerPixel(), 24); EXPECT_EQ(read_bitmap.ConvertToRowMajorArray(), bitmap.CloneAsRGB().ConvertToRowMajorArray()); EXPECT_TRUE(read_bitmap.Read(filename, /*as_rgb=*/false)); EXPECT_EQ(read_bitmap.Width(), bitmap.Width()); EXPECT_EQ(read_bitmap.Height(), bitmap.Height()); EXPECT_EQ(read_bitmap.Channels(), 1); EXPECT_EQ(read_bitmap.BitsPerPixel(), 8); EXPECT_EQ(read_bitmap.ConvertToRowMajorArray(), bitmap.ConvertToRowMajorArray()); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/sensor/database.cc000066400000000000000000000065431454702036400204420ustar00rootroot00000000000000// 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. #include "colmap/sensor/database.h" #include "colmap/util/string.h" namespace colmap { const camera_specs_t CameraDatabase::specs_ = InitializeCameraSpecs(); bool CameraDatabase::QuerySensorWidth(const std::string& make, const std::string& model, double* sensor_width) { // Clean the strings from all separators. std::string cleaned_make = make; std::string cleaned_model = model; cleaned_make = StringReplace(cleaned_make, " ", ""); cleaned_model = StringReplace(cleaned_model, " ", ""); cleaned_make = StringReplace(cleaned_make, "-", ""); cleaned_model = StringReplace(cleaned_model, "-", ""); StringToLower(&cleaned_make); StringToLower(&cleaned_model); // Make sure that make name is not duplicated. cleaned_model = StringReplace(cleaned_model, cleaned_make, ""); // Check if cleaned_make exists in database: Test whether EXIF string is // substring of database entry and vice versa. size_t spec_matches = 0; for (const auto& make_elem : specs_) { if (StringContains(cleaned_make, make_elem.first) || StringContains(make_elem.first, cleaned_make)) { for (const auto& model_elem : make_elem.second) { if (StringContains(cleaned_model, model_elem.first) || StringContains(model_elem.first, cleaned_model)) { *sensor_width = model_elem.second; if (cleaned_model == model_elem.first) { // Model exactly matches, return immediately. return true; } spec_matches += 1; if (spec_matches > 1) { break; } } } } } // Only return unique results, if model does not exactly match. return spec_matches == 1; } } // namespace colmap colmap-3.9.1/src/colmap/sensor/database.h000066400000000000000000000042461454702036400203020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/sensor/specs.h" #include namespace colmap { // Database that contains sensor widths for many cameras, which is useful // to automatically extract the focal length if EXIF information is incomplete. struct CameraDatabase { public: CameraDatabase() = default; size_t NumEntries() const { return specs_.size(); } bool QuerySensorWidth(const std::string& make, const std::string& model, double* sensor_width); private: static const camera_specs_t specs_; }; } // namespace colmap colmap-3.9.1/src/colmap/sensor/database_test.cc000066400000000000000000000045011454702036400214710ustar00rootroot00000000000000// 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. #include "colmap/sensor/database.h" #include namespace colmap { namespace { TEST(CameraDatabase, Initialization) { CameraDatabase database; camera_specs_t specs = InitializeCameraSpecs(); EXPECT_EQ(database.NumEntries(), specs.size()); } TEST(CameraDatabase, ExactMatch) { CameraDatabase database; double sensor_width; EXPECT_TRUE( database.QuerySensorWidth("canon", "digitalixus100is", &sensor_width)); EXPECT_EQ(sensor_width, 6.1600f); } TEST(CameraDatabase, AmbiguousMatch) { CameraDatabase database; double sensor_width; EXPECT_TRUE( !database.QuerySensorWidth("canon", "digitalixus", &sensor_width)); EXPECT_EQ(sensor_width, 6.1600f); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/sensor/models.cc000066400000000000000000000214621454702036400201560ustar00rootroot00000000000000// 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. #include "colmap/sensor/models.h" #include namespace colmap { // Initialize params_info, focal_length_idxs, principal_point_idxs, // extra_params_idxs #define CAMERA_MODEL_CASE(CameraModel) \ constexpr CameraModelId CameraModel::model_id; \ const std::string CameraModel::model_name = \ CameraModel::InitializeModelName(); \ constexpr size_t CameraModel::num_params; \ const std::string CameraModel::params_info = \ CameraModel::InitializeParamsInfo(); \ const std::array \ CameraModel::focal_length_idxs = \ CameraModel::InitializeFocalLengthIdxs(); \ const std::array \ CameraModel::principal_point_idxs = \ CameraModel::InitializePrincipalPointIdxs(); \ const std::array \ CameraModel::extra_params_idxs = \ CameraModel::InitializeExtraParamsIdxs(); CAMERA_MODEL_CASES #undef CAMERA_MODEL_CASE std::unordered_map InitialzeCameraModelNameToId() { std::unordered_map camera_model_name_to_id; #define CAMERA_MODEL_CASE(CameraModel) \ camera_model_name_to_id.emplace(CameraModel::model_name, \ CameraModel::model_id); CAMERA_MODEL_CASES #undef CAMERA_MODEL_CASE return camera_model_name_to_id; } std::unordered_map InitialzeCameraModelIdToName() { std::unordered_map camera_model_id_to_name; #define CAMERA_MODEL_CASE(CameraModel) \ camera_model_id_to_name.emplace(CameraModel::model_id, \ &CameraModel::model_name); CAMERA_MODEL_CASES #undef CAMERA_MODEL_CASE return camera_model_id_to_name; } static const std::unordered_map kCameraModelNameToId = InitialzeCameraModelNameToId(); static const std::unordered_map kCameraModelIdToName = InitialzeCameraModelIdToName(); bool ExistsCameraModelWithName(const std::string& model_name) { return kCameraModelNameToId.count(model_name) > 0; } bool ExistsCameraModelWithId(const CameraModelId model_id) { return kCameraModelIdToName.count(model_id) > 0; } CameraModelId CameraModelNameToId(const std::string& model_name) { const auto it = kCameraModelNameToId.find(model_name); if (it == kCameraModelNameToId.end()) { return CameraModelId::kInvalid; } else { return it->second; } } const std::string& CameraModelIdToName(const CameraModelId model_id) { const auto it = kCameraModelIdToName.find(model_id); if (it == kCameraModelIdToName.end()) { const static std::string kEmptyModelName = ""; return kEmptyModelName; } else { return *(it->second); } } std::vector CameraModelInitializeParams(const CameraModelId model_id, const double focal_length, const size_t width, const size_t height) { // Assuming that image measurements are within [0, dim], i.e. that the // upper left corner is the (0, 0) coordinate (rather than the center of // the upper left pixel). This complies with the default SiftGPU convention. switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return CameraModel::InitializeParams(focal_length, width, height); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } } const std::string& CameraModelParamsInfo(const CameraModelId model_id) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return CameraModel::params_info; \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } const static std::string kEmptyParamsInfo = ""; return kEmptyParamsInfo; } span CameraModelFocalLengthIdxs(const CameraModelId model_id) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return {CameraModel::focal_length_idxs.data(), \ CameraModel::focal_length_idxs.size()}; \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return {nullptr, 0}; } span CameraModelPrincipalPointIdxs(const CameraModelId model_id) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return {CameraModel::principal_point_idxs.data(), \ CameraModel::principal_point_idxs.size()}; \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return {nullptr, 0}; } span CameraModelExtraParamsIdxs(const CameraModelId model_id) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return {CameraModel::extra_params_idxs.data(), \ CameraModel::extra_params_idxs.size()}; \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return {nullptr, 0}; } size_t CameraModelNumParams(const CameraModelId model_id) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return CameraModel::num_params; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return 0; } bool CameraModelVerifyParams(const CameraModelId model_id, const std::vector& params) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ if (params.size() == CameraModel::num_params) { \ return true; \ } \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return false; } bool CameraModelHasBogusParams(const CameraModelId model_id, const std::vector& params, const size_t width, const size_t height, const double min_focal_length_ratio, const double max_focal_length_ratio, const double max_extra_param) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return CameraModel::HasBogusParams(params, \ width, \ height, \ min_focal_length_ratio, \ max_focal_length_ratio, \ max_extra_param); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return false; } } // namespace colmap colmap-3.9.1/src/colmap/sensor/models.h000066400000000000000000001424541454702036400200250ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include #include #include #include #include namespace colmap { // This file defines several different camera models and arbitrary new camera // models can be added by the following steps: // // 1. Add a new struct in this file which implements all the necessary methods. // 2. Define an unique model_name and model_id for the camera model. // 3. Add camera model to `CAMERA_MODEL_CASES` macro in this file. // 4. Add new template specialization of test case for camera model to // `camera_models_test.cc`. // // A camera model can have three different types of camera parameters: focal // length, principal point, extra parameters (distortion parameters). The // parameter array is split into different groups, so that we can enable or // disable the refinement of the individual groups during bundle adjustment. It // is up to the camera model to access the parameters correctly (it is free to // do so in an arbitrary manner) - the parameters are not accessed from outside. // // A camera model must have the following methods: // // - `ImgFromCam`: transform normalized camera coordinates to image // coordinates (the inverse of `CamFromImg`). Assumes that the camera // coordinates are given as (u, v, 1). // - `CamFromImg`: transform image coordinates to normalized camera // coordinates (the inverse of `ImgFromCam`). Produces camera coordinates // as (u, v, 1). // - `CamFromImgThreshold`: transform a threshold given in pixels to // normalized units (e.g. useful for reprojection error thresholds). // // Whenever you specify the camera parameters in a list, they must appear // exactly in the order as they are accessed in the defined model struct. // // The camera models follow the convention that the upper left image corner has // the coordinate (0, 0), the lower right corner (width, height), i.e. that // the upper left pixel center has coordinate (0.5, 0.5) and the lower right // pixel center has the coordinate (width - 0.5, height - 0.5). enum class CameraModelId { kInvalid = -1, kSimplePinhole = 0, kPinhole = 1, kSimpleRadial = 2, kRadial = 3, kOpenCV = 4, kOpenCVFisheye = 5, kFullOpenCV = 6, kFOV = 7, kSimpleRadialFisheye = 8, kRadialFisheye = 9, kThinPrismFisheye = 10, }; #ifndef CAMERA_MODEL_DEFINITIONS #define CAMERA_MODEL_DEFINITIONS(model_id_val, \ model_name_val, \ num_focal_params_val, \ num_pp_params_val, \ num_extra_params_val) \ static constexpr size_t num_params = \ (num_focal_params_val) + (num_pp_params_val) + (num_extra_params_val); \ static constexpr size_t num_focal_params = num_focal_params_val; \ static constexpr size_t num_pp_params = num_pp_params_val; \ static constexpr size_t num_extra_params = num_extra_params_val; \ static constexpr CameraModelId model_id = model_id_val; \ static const std::string model_name; \ static const std::string params_info; \ static const std::array focal_length_idxs; \ static const std::array principal_point_idxs; \ static const std::array extra_params_idxs; \ \ static inline CameraModelId InitializeModelId() { return model_id_val; }; \ static inline std::string InitializeModelName() { return model_name_val; }; \ static inline std::string InitializeParamsInfo(); \ static inline std::array \ InitializeFocalLengthIdxs(); \ static inline std::array \ InitializePrincipalPointIdxs(); \ static inline std::array \ InitializeExtraParamsIdxs(); \ \ static inline std::vector InitializeParams( \ double focal_length, size_t width, size_t height); \ template \ static void ImgFromCam(const T* params, T u, T v, T w, T* x, T* y); \ template \ static void CamFromImg(const T* params, T x, T y, T* u, T* v, T* w); \ template \ static void Distortion(const T* extra_params, T u, T v, T* du, T* dv); #endif #ifndef CAMERA_MODEL_CASES #define CAMERA_MODEL_CASES \ CAMERA_MODEL_CASE(SimplePinholeCameraModel) \ CAMERA_MODEL_CASE(PinholeCameraModel) \ CAMERA_MODEL_CASE(SimpleRadialCameraModel) \ CAMERA_MODEL_CASE(SimpleRadialFisheyeCameraModel) \ CAMERA_MODEL_CASE(RadialCameraModel) \ CAMERA_MODEL_CASE(RadialFisheyeCameraModel) \ CAMERA_MODEL_CASE(OpenCVCameraModel) \ CAMERA_MODEL_CASE(OpenCVFisheyeCameraModel) \ CAMERA_MODEL_CASE(FullOpenCVCameraModel) \ CAMERA_MODEL_CASE(FOVCameraModel) \ CAMERA_MODEL_CASE(ThinPrismFisheyeCameraModel) #endif #ifndef CAMERA_MODEL_SWITCH_CASES #define CAMERA_MODEL_SWITCH_CASES \ CAMERA_MODEL_CASES \ default: \ CAMERA_MODEL_DOES_NOT_EXIST_EXCEPTION \ break; #endif #define CAMERA_MODEL_DOES_NOT_EXIST_EXCEPTION \ throw std::domain_error("Camera model does not exist"); // The "Curiously Recurring Template Pattern" (CRTP) is used here, so that we // can reuse some shared functionality between all camera models - // defined in the BaseCameraModel. template struct BaseCameraModel { template static inline bool HasBogusParams(const std::vector& params, size_t width, size_t height, T min_focal_length_ratio, T max_focal_length_ratio, T max_extra_param); template static inline bool HasBogusFocalLength(const std::vector& params, size_t width, size_t height, T min_focal_length_ratio, T max_focal_length_ratio); template static inline bool HasBogusPrincipalPoint(const std::vector& params, size_t width, size_t height); template static inline bool HasBogusExtraParams(const std::vector& params, T max_extra_param); template static inline T CamFromImgThreshold(const T* params, T threshold); template static inline void IterativeUndistortion(const T* params, T* u, T* v); }; // Simple Pinhole camera model. // // No Distortion is assumed. Only focal length and principal point is modeled. // // Parameter list is expected in the following order: // // f, cx, cy // // See https://en.wikipedia.org/wiki/Pinhole_camera_model struct SimplePinholeCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS( CameraModelId::kSimplePinhole, "SIMPLE_PINHOLE", 1, 2, 0) }; // Pinhole camera model. // // No Distortion is assumed. Only focal length and principal point is modeled. // // Parameter list is expected in the following order: // // fx, fy, cx, cy // // See https://en.wikipedia.org/wiki/Pinhole_camera_model struct PinholeCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS(CameraModelId::kPinhole, "PINHOLE", 2, 2, 0) }; // Simple camera model with one focal length and one radial distortion // parameter. // // This model is similar to the camera model that VisualSfM uses with the // difference that the distortion here is applied to the projections and // not to the measurements. // // Parameter list is expected in the following order: // // f, cx, cy, k // struct SimpleRadialCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS( CameraModelId::kSimpleRadial, "SIMPLE_RADIAL", 1, 2, 1) }; // Simple camera model with one focal length and two radial distortion // parameters. // // This model is equivalent to the camera model that Bundler uses // (except for an inverse z-axis in the camera coordinate system). // // Parameter list is expected in the following order: // // f, cx, cy, k1, k2 // struct RadialCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS(CameraModelId::kRadial, "RADIAL", 1, 2, 2) }; // OpenCV camera model. // // Based on the pinhole camera model. Additionally models radial and // tangential distortion (up to 2nd degree of coefficients). Not suitable for // large radial distortions of fish-eye cameras. // // Parameter list is expected in the following order: // // fx, fy, cx, cy, k1, k2, p1, p2 // // See // http://docs.opencv.org/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html struct OpenCVCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS(CameraModelId::kOpenCV, "OPENCV", 2, 2, 4) }; // OpenCV fish-eye camera model. // // Based on the pinhole camera model. Additionally models radial distortion // (up to 4th degree of coefficients). Suitable for // large radial distortions of fish-eye cameras. // // Parameter list is expected in the following order: // // fx, fy, cx, cy, k1, k2, k3, k4 // // See // http://docs.opencv.org/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html struct OpenCVFisheyeCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS( CameraModelId::kOpenCVFisheye, "OPENCV_FISHEYE", 2, 2, 4) }; // Full OpenCV camera model. // // Based on the pinhole camera model. Additionally models radial and // tangential Distortion. // // Parameter list is expected in the following order: // // fx, fy, cx, cy, k1, k2, p1, p2, k3, k4, k5, k6 // // See // http://docs.opencv.org/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html struct FullOpenCVCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS(CameraModelId::kFullOpenCV, "FULL_OPENCV", 2, 2, 8) }; // FOV camera model. // // Based on the pinhole camera model. Additionally models radial distortion. // This model is for example used by Project Tango for its equidistant // calibration type. // // Parameter list is expected in the following order: // // fx, fy, cx, cy, omega // // See: // Frederic Devernay, Olivier Faugeras. Straight lines have to be straight: // Automatic calibration and removal of distortion from scenes of structured // environments. Machine vision and applications, 2001. struct FOVCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS(CameraModelId::kFOV, "FOV", 2, 2, 1) template static void Undistortion(const T* extra_params, T u, T v, T* du, T* dv); }; // Simple camera model with one focal length and one radial distortion // parameter, suitable for fish-eye cameras. // // This model is equivalent to the OpenCVFisheyeCameraModel but has only one // radial distortion coefficient. // // Parameter list is expected in the following order: // // f, cx, cy, k // struct SimpleRadialFisheyeCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS( CameraModelId::kSimpleRadialFisheye, "SIMPLE_RADIAL_FISHEYE", 1, 2, 1) }; // Simple camera model with one focal length and two radial distortion // parameters, suitable for fish-eye cameras. // // This model is equivalent to the OpenCVFisheyeCameraModel but has only two // radial distortion coefficients. // // Parameter list is expected in the following order: // // f, cx, cy, k1, k2 // struct RadialFisheyeCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS( CameraModelId::kRadialFisheye, "RADIAL_FISHEYE", 1, 2, 2) }; // Camera model with radial and tangential distortion coefficients and // additional coefficients accounting for thin-prism distortion. // // This camera model is described in // // "Camera Calibration with Distortion Models and Accuracy Evaluation", // J Weng et al., TPAMI, 1992. // // Parameter list is expected in the following order: // // fx, fy, cx, cy, k1, k2, p1, p2, k3, k4, sx1, sy1 // struct ThinPrismFisheyeCameraModel : public BaseCameraModel { CAMERA_MODEL_DEFINITIONS( CameraModelId::kThinPrismFisheye, "THIN_PRISM_FISHEYE", 2, 2, 8) }; // Check whether camera model with given name or identifier exists. bool ExistsCameraModelWithName(const std::string& model_name); bool ExistsCameraModelWithId(CameraModelId model_id); // Convert camera name to unique camera model identifier. // // @param name Unique name of camera model. // // @return Unique identifier of camera model. CameraModelId CameraModelNameToId(const std::string& model_name); // Convert camera model identifier to unique camera model name. // // @param model_id Unique identifier of camera model. // // @return Unique name of camera model. const std::string& CameraModelIdToName(CameraModelId model_id); // Initialize camera parameters using given image properties. // // Initializes all focal length parameters to the same given focal length and // sets the principal point to the image center. // // @param model_id Unique identifier of camera model. // @param focal_length Focal length, equal for all focal length parameters. // @param width Sensor width of the camera. // @param height Sensor height of the camera. std::vector CameraModelInitializeParams(CameraModelId model_id, double focal_length, size_t width, size_t height); // Get human-readable information about the parameter vector order. // // @param model_id Unique identifier of camera model. const std::string& CameraModelParamsInfo(CameraModelId model_id); // Get the indices of the parameter groups in the parameter vector. // // @param model_id Unique identifier of camera model. span CameraModelFocalLengthIdxs(CameraModelId model_id); span CameraModelPrincipalPointIdxs(CameraModelId model_id); span CameraModelExtraParamsIdxs(CameraModelId model_id); // Get the total number of parameters of a camera model. size_t CameraModelNumParams(CameraModelId model_id); // Check whether parameters are valid, i.e. the parameter vector has // the correct dimensions that match the specified camera model. // // @param model_id Unique identifier of camera model. // @param params Array of camera parameters. bool CameraModelVerifyParams(CameraModelId model_id, const std::vector& params); // Check whether camera has bogus parameters. // // @param model_id Unique identifier of camera model. // @param params Array of camera parameters. // @param width Sensor width of the camera. // @param height Sensor height of the camera. // @param min_focal_length_ratio Minimum ratio of focal length over // maximum sensor dimension. // @param min_focal_length_ratio Maximum ratio of focal length over // maximum sensor dimension. // @param max_extra_param Maximum magnitude of each extra parameter. bool CameraModelHasBogusParams(CameraModelId model_id, const std::vector& params, size_t width, size_t height, double min_focal_length_ratio, double max_focal_length_ratio, double max_extra_param); // Transform camera to image coordinates. // // This is the inverse of `CameraModelCamFromImg`. // // @param model_id Unique model_id of camera model as defined in // `CAMERA_MODEL_NAME_TO_CODE`. // @param params Array of camera parameters. // @param u, v Coordinates in camera system as (u, v, 1). // @param x, y Output image coordinates in pixels. inline Eigen::Vector2d CameraModelImgFromCam(CameraModelId model_id, const std::vector& params, const Eigen::Vector3d& uvw); // Transform image to camera coordinates. // // This is the inverse of `CameraModelImgFromCam`. // // @param model_id Unique identifier of camera model. // @param params Array of camera parameters. // @param xy Image coordinates in pixels. // // @return Output Coordinates in camera system as (u, v, w=1). inline Eigen::Vector3d CameraModelCamFromImg(CameraModelId model_id, const std::vector& params, const Eigen::Vector2d& xy); // Convert pixel threshold in image plane to camera space by dividing // the threshold through the mean focal length. // // @param model_id Unique identifier of camera model. // @param params Array of camera parameters. // @param threshold Image space threshold in pixels. // // @return Camera space threshold. inline double CameraModelCamFromImgThreshold(CameraModelId model_id, const std::vector& params, double threshold); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // BaseCameraModel template template bool BaseCameraModel::HasBogusParams( const std::vector& params, const size_t width, const size_t height, const T min_focal_length_ratio, const T max_focal_length_ratio, const T max_extra_param) { return HasBogusPrincipalPoint(params, width, height) || HasBogusFocalLength(params, width, height, min_focal_length_ratio, max_focal_length_ratio) || HasBogusExtraParams(params, max_extra_param); } template template bool BaseCameraModel::HasBogusFocalLength( const std::vector& params, const size_t width, const size_t height, const T min_focal_length_ratio, const T max_focal_length_ratio) { const T inv_max_size = 1.0 / std::max(width, height); for (const size_t idx : CameraModel::focal_length_idxs) { const T focal_length_ratio = params[idx] * inv_max_size; if (focal_length_ratio < min_focal_length_ratio || focal_length_ratio > max_focal_length_ratio) { return true; } } return false; } template template bool BaseCameraModel::HasBogusPrincipalPoint( const std::vector& params, const size_t width, const size_t height) { const T cx = params[CameraModel::principal_point_idxs[0]]; const T cy = params[CameraModel::principal_point_idxs[1]]; return cx < 0 || cx > width || cy < 0 || cy > height; } template template bool BaseCameraModel::HasBogusExtraParams( const std::vector& params, const T max_extra_param) { for (const size_t idx : CameraModel::extra_params_idxs) { if (std::abs(params[idx]) > max_extra_param) { return true; } } return false; } template template T BaseCameraModel::CamFromImgThreshold(const T* params, const T threshold) { T mean_focal_length = 0; for (const size_t idx : CameraModel::focal_length_idxs) { mean_focal_length += params[idx]; } mean_focal_length /= CameraModel::focal_length_idxs.size(); return threshold / mean_focal_length; } template template void BaseCameraModel::IterativeUndistortion(const T* params, T* u, T* v) { // Parameters for Newton iteration using numerical differentiation with // central differences, 100 iterations should be enough even for complex // camera models with higher order terms. const size_t kNumIterations = 100; const double kMaxStepNorm = 1e-10; const double kRelStepSize = 1e-6; Eigen::Matrix2d J; const Eigen::Vector2d x0(*u, *v); Eigen::Vector2d x(*u, *v); Eigen::Vector2d dx; Eigen::Vector2d dx_0b; Eigen::Vector2d dx_0f; Eigen::Vector2d dx_1b; Eigen::Vector2d dx_1f; for (size_t i = 0; i < kNumIterations; ++i) { const double step0 = std::max(std::numeric_limits::epsilon(), std::abs(kRelStepSize * x(0))); const double step1 = std::max(std::numeric_limits::epsilon(), std::abs(kRelStepSize * x(1))); CameraModel::Distortion(params, x(0), x(1), &dx(0), &dx(1)); CameraModel::Distortion(params, x(0) - step0, x(1), &dx_0b(0), &dx_0b(1)); CameraModel::Distortion(params, x(0) + step0, x(1), &dx_0f(0), &dx_0f(1)); CameraModel::Distortion(params, x(0), x(1) - step1, &dx_1b(0), &dx_1b(1)); CameraModel::Distortion(params, x(0), x(1) + step1, &dx_1f(0), &dx_1f(1)); J(0, 0) = 1 + (dx_0f(0) - dx_0b(0)) / (2 * step0); J(0, 1) = (dx_1f(0) - dx_1b(0)) / (2 * step1); J(1, 0) = (dx_0f(1) - dx_0b(1)) / (2 * step0); J(1, 1) = 1 + (dx_1f(1) - dx_1b(1)) / (2 * step1); const Eigen::Vector2d step_x = J.partialPivLu().solve(x + dx - x0); x -= step_x; if (step_x.squaredNorm() < kMaxStepNorm) { break; } } *u = x(0); *v = x(1); } //////////////////////////////////////////////////////////////////////////////// // SimplePinholeCameraModel std::string SimplePinholeCameraModel::InitializeParamsInfo() { return "f, cx, cy"; } std::array SimplePinholeCameraModel::InitializeFocalLengthIdxs() { return {0}; } std::array SimplePinholeCameraModel::InitializePrincipalPointIdxs() { return {1, 2}; } std::array SimplePinholeCameraModel::InitializeExtraParamsIdxs() { return {}; } std::vector SimplePinholeCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, width / 2.0, height / 2.0}; } template void SimplePinholeCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; // No Distortion // Transform to image coordinates *x = f * u / w + c1; *y = f * v / w + c2; } template void SimplePinholeCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; *u = (x - c1) / f; *v = (y - c2) / f; *w = 1; } //////////////////////////////////////////////////////////////////////////////// // PinholeCameraModel std::string PinholeCameraModel::InitializeParamsInfo() { return "fx, fy, cx, cy"; } std::array PinholeCameraModel::InitializeFocalLengthIdxs() { return {0, 1}; } std::array PinholeCameraModel::InitializePrincipalPointIdxs() { return {2, 3}; } std::array PinholeCameraModel::InitializeExtraParamsIdxs() { return {}; } std::vector PinholeCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, focal_length, width / 2.0, height / 2.0}; } template void PinholeCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; // No Distortion // Transform to image coordinates *x = f1 * u / w + c1; *y = f2 * v / w + c2; } template void PinholeCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; *u = (x - c1) / f1; *v = (y - c2) / f2; *w = 1; } //////////////////////////////////////////////////////////////////////////////// // SimpleRadialCameraModel std::string SimpleRadialCameraModel::InitializeParamsInfo() { return "f, cx, cy, k"; } std::array SimpleRadialCameraModel::InitializeFocalLengthIdxs() { return {0}; } std::array SimpleRadialCameraModel::InitializePrincipalPointIdxs() { return {1, 2}; } std::array SimpleRadialCameraModel::InitializeExtraParamsIdxs() { return {3}; } std::vector SimpleRadialCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, width / 2.0, height / 2.0, 0}; } template void SimpleRadialCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; u /= w; v /= w; // Distortion T du, dv; Distortion(¶ms[3], u, v, &du, &dv); *x = u + du; *y = v + dv; // Transform to image coordinates *x = f * *x + c1; *y = f * *y + c2; } template void SimpleRadialCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; // Lift points to normalized plane *u = (x - c1) / f; *v = (y - c2) / f; *w = 1; IterativeUndistortion(¶ms[3], u, v); } template void SimpleRadialCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k = extra_params[0]; const T u2 = u * u; const T v2 = v * v; const T r2 = u2 + v2; const T radial = k * r2; *du = u * radial; *dv = v * radial; } //////////////////////////////////////////////////////////////////////////////// // RadialCameraModel std::string RadialCameraModel::InitializeParamsInfo() { return "f, cx, cy, k1, k2"; } std::array RadialCameraModel::InitializeFocalLengthIdxs() { return {0}; } std::array RadialCameraModel::InitializePrincipalPointIdxs() { return {1, 2}; } std::array RadialCameraModel::InitializeExtraParamsIdxs() { return {3, 4}; } std::vector RadialCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, width / 2.0, height / 2.0, 0, 0}; } template void RadialCameraModel::ImgFromCam(const T* params, T u, T v, T w, T* x, T* y) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; u /= w; v /= w; // Distortion T du, dv; Distortion(¶ms[3], u, v, &du, &dv); *x = u + du; *y = v + dv; // Transform to image coordinates *x = f * *x + c1; *y = f * *y + c2; } template void RadialCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; // Lift points to normalized plane *u = (x - c1) / f; *v = (y - c2) / f; *w = 1; IterativeUndistortion(¶ms[3], u, v); } template void RadialCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k1 = extra_params[0]; const T k2 = extra_params[1]; const T u2 = u * u; const T v2 = v * v; const T r2 = u2 + v2; const T radial = k1 * r2 + k2 * r2 * r2; *du = u * radial; *dv = v * radial; } //////////////////////////////////////////////////////////////////////////////// // OpenCVCameraModel std::string OpenCVCameraModel::InitializeParamsInfo() { return "fx, fy, cx, cy, k1, k2, p1, p2"; } std::array OpenCVCameraModel::InitializeFocalLengthIdxs() { return {0, 1}; } std::array OpenCVCameraModel::InitializePrincipalPointIdxs() { return {2, 3}; } std::array OpenCVCameraModel::InitializeExtraParamsIdxs() { return {4, 5, 6, 7}; } std::vector OpenCVCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, focal_length, width / 2.0, height / 2.0, 0, 0, 0, 0}; } template void OpenCVCameraModel::ImgFromCam(const T* params, T u, T v, T w, T* x, T* y) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; u /= w; v /= w; // Distortion T du, dv; Distortion(¶ms[4], u, v, &du, &dv); *x = u + du; *y = v + dv; // Transform to image coordinates *x = f1 * *x + c1; *y = f2 * *y + c2; } template void OpenCVCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; // Lift points to normalized plane *u = (x - c1) / f1; *v = (y - c2) / f2; *w = 1; IterativeUndistortion(¶ms[4], u, v); } template void OpenCVCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k1 = extra_params[0]; const T k2 = extra_params[1]; const T p1 = extra_params[2]; const T p2 = extra_params[3]; const T u2 = u * u; const T uv = u * v; const T v2 = v * v; const T r2 = u2 + v2; const T radial = k1 * r2 + k2 * r2 * r2; *du = u * radial + T(2) * p1 * uv + p2 * (r2 + T(2) * u2); *dv = v * radial + T(2) * p2 * uv + p1 * (r2 + T(2) * v2); } //////////////////////////////////////////////////////////////////////////////// // OpenCVFisheyeCameraModel std::string OpenCVFisheyeCameraModel::InitializeParamsInfo() { return "fx, fy, cx, cy, k1, k2, k3, k4"; } std::array OpenCVFisheyeCameraModel::InitializeFocalLengthIdxs() { return {0, 1}; } std::array OpenCVFisheyeCameraModel::InitializePrincipalPointIdxs() { return {2, 3}; } std::array OpenCVFisheyeCameraModel::InitializeExtraParamsIdxs() { return {4, 5, 6, 7}; } std::vector OpenCVFisheyeCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, focal_length, width / 2.0, height / 2.0, 0, 0, 0, 0}; } template void OpenCVFisheyeCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; u /= w; v /= w; // Distortion T du, dv; Distortion(¶ms[4], u, v, &du, &dv); *x = u + du; *y = v + dv; // Transform to image coordinates *x = f1 * *x + c1; *y = f2 * *y + c2; } template void OpenCVFisheyeCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; // Lift points to normalized plane *u = (x - c1) / f1; *v = (y - c2) / f2; *w = 1; IterativeUndistortion(¶ms[4], u, v); } template void OpenCVFisheyeCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k1 = extra_params[0]; const T k2 = extra_params[1]; const T k3 = extra_params[2]; const T k4 = extra_params[3]; const T r = ceres::sqrt(u * u + v * v); if (r > T(std::numeric_limits::epsilon())) { const T theta = ceres::atan(r); const T theta2 = theta * theta; const T theta4 = theta2 * theta2; const T theta6 = theta4 * theta2; const T theta8 = theta4 * theta4; const T thetad = theta * (T(1) + k1 * theta2 + k2 * theta4 + k3 * theta6 + k4 * theta8); *du = u * thetad / r - u; *dv = v * thetad / r - v; } else { *du = T(0); *dv = T(0); } } //////////////////////////////////////////////////////////////////////////////// // FullOpenCVCameraModel std::string FullOpenCVCameraModel::InitializeParamsInfo() { return "fx, fy, cx, cy, k1, k2, p1, p2, k3, k4, k5, k6"; } std::array FullOpenCVCameraModel::InitializeFocalLengthIdxs() { return {0, 1}; } std::array FullOpenCVCameraModel::InitializePrincipalPointIdxs() { return {2, 3}; } std::array FullOpenCVCameraModel::InitializeExtraParamsIdxs() { return {4, 5, 6, 7, 8, 9, 10, 11}; } std::vector FullOpenCVCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, focal_length, width / 2.0, height / 2.0, 0, 0, 0, 0, 0, 0, 0, 0}; } template void FullOpenCVCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; u /= w; v /= w; // Distortion T du, dv; Distortion(¶ms[4], u, v, &du, &dv); *x = u + du; *y = v + dv; // Transform to image coordinates *x = f1 * *x + c1; *y = f2 * *y + c2; } template void FullOpenCVCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; // Lift points to normalized plane *u = (x - c1) / f1; *v = (y - c2) / f2; *w = 1; IterativeUndistortion(¶ms[4], u, v); } template void FullOpenCVCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k1 = extra_params[0]; const T k2 = extra_params[1]; const T p1 = extra_params[2]; const T p2 = extra_params[3]; const T k3 = extra_params[4]; const T k4 = extra_params[5]; const T k5 = extra_params[6]; const T k6 = extra_params[7]; const T u2 = u * u; const T uv = u * v; const T v2 = v * v; const T r2 = u2 + v2; const T r4 = r2 * r2; const T r6 = r4 * r2; const T radial = (T(1) + k1 * r2 + k2 * r4 + k3 * r6) / (T(1) + k4 * r2 + k5 * r4 + k6 * r6); *du = u * radial + T(2) * p1 * uv + p2 * (r2 + T(2) * u2) - u; *dv = v * radial + T(2) * p2 * uv + p1 * (r2 + T(2) * v2) - v; } //////////////////////////////////////////////////////////////////////////////// // FOVCameraModel std::string FOVCameraModel::InitializeParamsInfo() { return "fx, fy, cx, cy, omega"; } std::array FOVCameraModel::InitializeFocalLengthIdxs() { return {0, 1}; } std::array FOVCameraModel::InitializePrincipalPointIdxs() { return {2, 3}; } std::array FOVCameraModel::InitializeExtraParamsIdxs() { return {4}; } std::vector FOVCameraModel::InitializeParams(const double focal_length, const size_t width, const size_t height) { return {focal_length, focal_length, width / 2.0, height / 2.0, 1e-2}; } template void FOVCameraModel::ImgFromCam(const T* params, T u, T v, T w, T* x, T* y) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; u /= w; v /= w; // Distortion Distortion(¶ms[4], u, v, x, y); // Transform to image coordinates *x = f1 * *x + c1; *y = f2 * *y + c2; } template void FOVCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; // Lift points to normalized plane const T uu = (x - c1) / f1; const T vv = (y - c2) / f2; *w = 1; // Undistortion Undistortion(¶ms[4], uu, vv, u, v); } template void FOVCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T omega = extra_params[0]; // Chosen arbitrarily. const T kEpsilon = T(1e-4); const T radius2 = u * u + v * v; const T omega2 = omega * omega; T factor; if (omega2 < kEpsilon) { // Derivation of this case with Matlab: // syms radius omega; // factor(radius) = atan(radius * 2 * tan(omega / 2)) / ... // (radius * omega); // simplify(taylor(factor, omega, 'order', 3)) factor = (omega2 * radius2) / T(3) - omega2 / T(12) + T(1); } else if (radius2 < kEpsilon) { // Derivation of this case with Matlab: // syms radius omega; // factor(radius) = atan(radius * 2 * tan(omega / 2)) / ... // (radius * omega); // simplify(taylor(factor, radius, 'order', 3)) const T tan_half_omega = ceres::tan(omega / T(2)); factor = (T(-2) * tan_half_omega * (T(4) * radius2 * tan_half_omega * tan_half_omega - T(3))) / (T(3) * omega); } else { const T radius = ceres::sqrt(radius2); const T numerator = ceres::atan(radius * T(2) * ceres::tan(omega / T(2))); factor = numerator / (radius * omega); } *du = u * factor; *dv = v * factor; } template void FOVCameraModel::Undistortion( const T* extra_params, const T u, const T v, T* du, T* dv) { T omega = extra_params[0]; // Chosen arbitrarily. const T kEpsilon = T(1e-4); const T radius2 = u * u + v * v; const T omega2 = omega * omega; T factor; if (omega2 < kEpsilon) { // Derivation of this case with Matlab: // syms radius omega; // factor(radius) = tan(radius * omega) / ... // (radius * 2*tan(omega/2)); // simplify(taylor(factor, omega, 'order', 3)) factor = (omega2 * radius2) / T(3) - omega2 / T(12) + T(1); } else if (radius2 < kEpsilon) { // Derivation of this case with Matlab: // syms radius omega; // factor(radius) = tan(radius * omega) / ... // (radius * 2*tan(omega/2)); // simplify(taylor(factor, radius, 'order', 3)) factor = (omega * (omega * omega * radius2 + T(3))) / (T(6) * ceres::tan(omega / T(2))); } else { const T radius = ceres::sqrt(radius2); const T numerator = ceres::tan(radius * omega); factor = numerator / (radius * T(2) * ceres::tan(omega / T(2))); } *du = u * factor; *dv = v * factor; } //////////////////////////////////////////////////////////////////////////////// // SimpleRadialFisheyeCameraModel std::string SimpleRadialFisheyeCameraModel::InitializeParamsInfo() { return "f, cx, cy, k"; } std::array SimpleRadialFisheyeCameraModel::InitializeFocalLengthIdxs() { return {0}; } std::array SimpleRadialFisheyeCameraModel::InitializePrincipalPointIdxs() { return {1, 2}; } std::array SimpleRadialFisheyeCameraModel::InitializeExtraParamsIdxs() { return {3}; } std::vector SimpleRadialFisheyeCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, width / 2.0, height / 2.0, 0}; } template void SimpleRadialFisheyeCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; u /= w; v /= w; // Distortion T du, dv; Distortion(¶ms[3], u, v, &du, &dv); *x = u + du; *y = v + dv; // Transform to image coordinates *x = f * *x + c1; *y = f * *y + c2; } template void SimpleRadialFisheyeCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; // Lift points to normalized plane *u = (x - c1) / f; *v = (y - c2) / f; *w = 1; IterativeUndistortion(¶ms[3], u, v); } template void SimpleRadialFisheyeCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k = extra_params[0]; const T r = ceres::sqrt(u * u + v * v); if (r > T(std::numeric_limits::epsilon())) { const T theta = ceres::atan(r); const T theta2 = theta * theta; const T thetad = theta * (T(1) + k * theta2); *du = u * thetad / r - u; *dv = v * thetad / r - v; } else { *du = T(0); *dv = T(0); } } //////////////////////////////////////////////////////////////////////////////// // RadialFisheyeCameraModel std::string RadialFisheyeCameraModel::InitializeParamsInfo() { return "f, cx, cy, k1, k2"; } std::array RadialFisheyeCameraModel::InitializeFocalLengthIdxs() { return {0}; } std::array RadialFisheyeCameraModel::InitializePrincipalPointIdxs() { return {1, 2}; } std::array RadialFisheyeCameraModel::InitializeExtraParamsIdxs() { return {3, 4}; } std::vector RadialFisheyeCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, width / 2.0, height / 2.0, 0, 0}; } template void RadialFisheyeCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; u /= w; v /= w; // Distortion T du, dv; Distortion(¶ms[3], u, v, &du, &dv); *x = u + du; *y = v + dv; // Transform to image coordinates *x = f * *x + c1; *y = f * *y + c2; } template void RadialFisheyeCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f = params[0]; const T c1 = params[1]; const T c2 = params[2]; // Lift points to normalized plane *u = (x - c1) / f; *v = (y - c2) / f; *w = 1; IterativeUndistortion(¶ms[3], u, v); } template void RadialFisheyeCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k1 = extra_params[0]; const T k2 = extra_params[1]; const T r = ceres::sqrt(u * u + v * v); if (r > T(std::numeric_limits::epsilon())) { const T theta = ceres::atan(r); const T theta2 = theta * theta; const T theta4 = theta2 * theta2; const T thetad = theta * (T(1) + k1 * theta2 + k2 * theta4); *du = u * thetad / r - u; *dv = v * thetad / r - v; } else { *du = T(0); *dv = T(0); } } //////////////////////////////////////////////////////////////////////////////// // ThinPrismFisheyeCameraModel std::string ThinPrismFisheyeCameraModel::InitializeParamsInfo() { return "fx, fy, cx, cy, k1, k2, p1, p2, k3, k4, sx1, sy1"; } std::array ThinPrismFisheyeCameraModel::InitializeFocalLengthIdxs() { return {0, 1}; } std::array ThinPrismFisheyeCameraModel::InitializePrincipalPointIdxs() { return {2, 3}; } std::array ThinPrismFisheyeCameraModel::InitializeExtraParamsIdxs() { return {4, 5, 6, 7, 8, 9, 10, 11}; } std::vector ThinPrismFisheyeCameraModel::InitializeParams( const double focal_length, const size_t width, const size_t height) { return {focal_length, focal_length, width / 2.0, height / 2.0, 0, 0, 0, 0, 0, 0, 0, 0}; } template void ThinPrismFisheyeCameraModel::ImgFromCam( const T* params, T u, T v, T w, T* x, T* y) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; u /= w; v /= w; const T r = ceres::sqrt(u * u + v * v); T uu, vv; if (r > T(std::numeric_limits::epsilon())) { const T theta = ceres::atan(r); uu = theta * u / r; vv = theta * v / r; } else { uu = u; vv = v; } // Distortion T du, dv; Distortion(¶ms[4], uu, vv, &du, &dv); *x = uu + du; *y = vv + dv; // Transform to image coordinates *x = f1 * *x + c1; *y = f2 * *y + c2; } template void ThinPrismFisheyeCameraModel::CamFromImg( const T* params, const T x, const T y, T* u, T* v, T* w) { const T f1 = params[0]; const T f2 = params[1]; const T c1 = params[2]; const T c2 = params[3]; // Lift points to normalized plane *u = (x - c1) / f1; *v = (y - c2) / f2; *w = 1; IterativeUndistortion(¶ms[4], u, v); const T theta = ceres::sqrt(*u * *u + *v * *v); const T theta_cos_theta = theta * ceres::cos(theta); if (theta_cos_theta > T(std::numeric_limits::epsilon())) { const T scale = ceres::sin(theta) / theta_cos_theta; *u *= scale; *v *= scale; } } template void ThinPrismFisheyeCameraModel::Distortion( const T* extra_params, const T u, const T v, T* du, T* dv) { const T k1 = extra_params[0]; const T k2 = extra_params[1]; const T p1 = extra_params[2]; const T p2 = extra_params[3]; const T k3 = extra_params[4]; const T k4 = extra_params[5]; const T sx1 = extra_params[6]; const T sy1 = extra_params[7]; const T u2 = u * u; const T uv = u * v; const T v2 = v * v; const T r2 = u2 + v2; const T r4 = r2 * r2; const T r6 = r4 * r2; const T r8 = r6 * r2; const T radial = k1 * r2 + k2 * r4 + k3 * r6 + k4 * r8; *du = u * radial + T(2) * p1 * uv + p2 * (r2 + T(2) * u2) + sx1 * r2; *dv = v * radial + T(2) * p2 * uv + p1 * (r2 + T(2) * v2) + sy1 * r2; } //////////////////////////////////////////////////////////////////////////////// Eigen::Vector2d CameraModelImgFromCam(const CameraModelId model_id, const std::vector& params, const Eigen::Vector3d& uvw) { Eigen::Vector2d xy; switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ CameraModel::ImgFromCam( \ params.data(), uvw.x(), uvw.y(), uvw.z(), &xy.x(), &xy.y()); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return xy; } Eigen::Vector3d CameraModelCamFromImg(const CameraModelId model_id, const std::vector& params, const Eigen::Vector2d& xy) { Eigen::Vector3d uvw; switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ CameraModel::CamFromImg( \ params.data(), xy.x(), xy.y(), &uvw.x(), &uvw.y(), &uvw.z()); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return uvw; } double CameraModelCamFromImgThreshold(const CameraModelId model_id, const std::vector& params, const double threshold) { switch (model_id) { #define CAMERA_MODEL_CASE(CameraModel) \ case CameraModel::model_id: \ return CameraModel::CamFromImgThreshold(params.data(), threshold); \ break; CAMERA_MODEL_SWITCH_CASES #undef CAMERA_MODEL_CASE } return -1; } } // namespace colmap colmap-3.9.1/src/colmap/sensor/models_test.cc000066400000000000000000000226431454702036400212170ustar00rootroot00000000000000// 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. #include "colmap/sensor/models.h" #include namespace colmap { namespace { template void TestCamToCamFromImg(const std::vector& params, const double u0, const double v0, const double w0) { double u, v, w, x, y; CameraModel::ImgFromCam(params.data(), u0, v0, w0, &x, &y); const Eigen::Vector2d xy = CameraModelImgFromCam( CameraModel::model_id, params, Eigen::Vector3d(u0, v0, w0)); EXPECT_EQ(x, xy.x()); EXPECT_EQ(y, xy.y()); CameraModel::CamFromImg(params.data(), x, y, &u, &v, &w); EXPECT_NEAR(u, u0 / w0, 1e-6); EXPECT_NEAR(v, v0 / w0, 1e-6); } template void TestCamFromImgToImg(const std::vector& params, const double x0, const double y0) { double u, v, w, x, y; CameraModel::CamFromImg(params.data(), x0, y0, &u, &v, &w); const Eigen::Vector3d uvw = CameraModelCamFromImg( CameraModel::model_id, params, Eigen::Vector2d(x0, y0)); EXPECT_EQ(u, uvw.x()); EXPECT_EQ(v, uvw.y()); EXPECT_EQ(w, uvw.z()); CameraModel::ImgFromCam(params.data(), u, v, w, &x, &y); EXPECT_NEAR(x, x0, 1e-6); EXPECT_NEAR(y, y0, 1e-6); } template void TestModel(const std::vector& params) { EXPECT_TRUE(CameraModelVerifyParams(CameraModel::model_id, params)); const std::vector default_params = CameraModelInitializeParams(CameraModel::model_id, 100, 100, 100); EXPECT_TRUE(CameraModelVerifyParams(CameraModel::model_id, default_params)); EXPECT_EQ(CameraModelParamsInfo(CameraModel::model_id), CameraModel::params_info); EXPECT_EQ(std::vector( CameraModelFocalLengthIdxs(CameraModel::model_id).begin(), CameraModelFocalLengthIdxs(CameraModel::model_id).end()), std::vector(CameraModel::focal_length_idxs.begin(), CameraModel::focal_length_idxs.end())); EXPECT_EQ(std::vector( CameraModelPrincipalPointIdxs(CameraModel::model_id).begin(), CameraModelPrincipalPointIdxs(CameraModel::model_id).end()), std::vector(CameraModel::principal_point_idxs.begin(), CameraModel::principal_point_idxs.end())); EXPECT_EQ(std::vector( CameraModelExtraParamsIdxs(CameraModel::model_id).begin(), CameraModelExtraParamsIdxs(CameraModel::model_id).end()), std::vector(CameraModel::extra_params_idxs.begin(), CameraModel::extra_params_idxs.end())); EXPECT_EQ(CameraModelNumParams(CameraModel::model_id), CameraModel::num_params); EXPECT_FALSE(CameraModelHasBogusParams( CameraModel::model_id, default_params, 100, 100, 0.1, 2.0, 1.0)); EXPECT_TRUE(CameraModelHasBogusParams( CameraModel::model_id, default_params, 100, 100, 0.1, 0.5, 1.0)); EXPECT_TRUE(CameraModelHasBogusParams( CameraModel::model_id, default_params, 100, 100, 1.5, 2.0, 1.0)); if (CameraModel::extra_params_idxs.size() > 0) { EXPECT_TRUE(CameraModelHasBogusParams( CameraModel::model_id, default_params, 100, 100, 0.1, 2.0, -0.1)); } EXPECT_EQ(CameraModelCamFromImgThreshold(CameraModel::model_id, params, 0), 0); EXPECT_GT(CameraModelCamFromImgThreshold(CameraModel::model_id, params, 1), 0); EXPECT_EQ( CameraModelCamFromImgThreshold(CameraModel::model_id, default_params, 1), 1.0 / 100.0); EXPECT_TRUE(ExistsCameraModelWithName(CameraModel::model_name)); EXPECT_FALSE(ExistsCameraModelWithName(CameraModel::model_name + "FOO")); EXPECT_TRUE(ExistsCameraModelWithId(CameraModel::model_id)); EXPECT_FALSE(ExistsCameraModelWithId(static_cast(123456789))); EXPECT_EQ(CameraModelNameToId(CameraModelIdToName(CameraModel::model_id)), CameraModel::model_id); EXPECT_EQ(CameraModelIdToName(CameraModelNameToId(CameraModel::model_name)), CameraModel::model_name); // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double u = -0.5; u <= 0.5; u += 0.1) { // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) for (double v = -0.5; v <= 0.5; v += 0.1) { for (double w : {1, 2}) { TestCamToCamFromImg(params, u, v, w); } } } for (int x = 0; x <= 800; x += 50) { for (int y = 0; y <= 800; y += 50) { TestCamFromImgToImg(params, x, y); } } const auto pp_idxs = CameraModel::principal_point_idxs; TestCamFromImgToImg( params, params[pp_idxs.at(0)], params[pp_idxs.at(1)]); } TEST(SimplePinhole, Nominal) { TestModel({655.123, 386.123, 511.123}); } TEST(Pinhole, Nominal) { TestModel({651.123, 655.123, 386.123, 511.123}); } TEST(SimpleRadial, Nominal) { TestModel({651.123, 386.123, 511.123, 0}); TestModel({651.123, 386.123, 511.123, 0.1}); } TEST(Radial, Nominal) { TestModel({651.123, 386.123, 511.123, 0, 0}); TestModel({651.123, 386.123, 511.123, 0.1, 0}); TestModel({651.123, 386.123, 511.12, 0, 0.05}); TestModel({651.123, 386.123, 511.123, 0.05, 0.03}); } TEST(OpenCV, Nominal) { TestModel( {651.123, 655.123, 386.123, 511.123, -0.471, 0.223, -0.001, 0.001}); } TEST(OpenCVFisheye, Nominal) { TestModel( {651.123, 655.123, 386.123, 511.123, -0.471, 0.223, -0.001, 0.001}); } TEST(FullOpenCV, Nominal) { TestModel({651.123, 655.123, 386.123, 511.123, -0.471, 0.223, -0.001, 0.001, 0.001, 0.02, -0.02, 0.001}); } TEST(FOV, Nominal) { TestModel({651.123, 655.123, 386.123, 511.123, 0}); TestModel({651.123, 655.123, 386.123, 511.123, 0.9}); TestModel({651.123, 655.123, 386.123, 511.123, 1e-6}); TestModel({651.123, 655.123, 386.123, 511.123, 1e-2}); EXPECT_EQ(CameraModelInitializeParams(FOVCameraModel::model_id, 100, 100, 100) .back(), 1e-2); } TEST(SimpleRadialFisheye, Nominal) { std::vector params = {651.123, 386.123, 511.123, 0}; TestModel({651.123, 386.123, 511.123, 0}); TestModel({651.123, 386.123, 511.123, 0.1}); } TEST(RadialFisheye, Nominal) { TestModel({651.123, 386.123, 511.123, 0, 0}); TestModel({651.123, 386.123, 511.123, 0, 0.1}); TestModel({651.123, 386.123, 511.123, 0, 0.05}); TestModel({651.123, 386.123, 511.123, 0, 0.03}); } TEST(ThinPrismFisheye, Nominal) { TestModel({651.123, 655.123, 386.123, 511.123, -0.471, 0.223, -0.001, 0.001, 0.001, 0.02, -0.02, 0.001}); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/sensor/specs.cc000066400000000000000000005752721454702036400200250ustar00rootroot00000000000000// 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. #include "colmap/sensor/specs.h" namespace colmap { camera_specs_t InitializeCameraSpecs() { camera_specs_t specs; { auto& make_specs = specs["acer"]; make_specs.reserve(17); make_specs.emplace_back("ce5330", 5.7500f); make_specs.emplace_back("ce5430", 5.7500f); make_specs.emplace_back("ce6430", 5.7500f); make_specs.emplace_back("ci6330", 7.1100f); make_specs.emplace_back("ci6530", 7.1100f); make_specs.emplace_back("ci8330", 7.1100f); make_specs.emplace_back("cl5300", 5.7500f); make_specs.emplace_back("cp8531", 7.1100f); make_specs.emplace_back("cp8660", 7.1100f); make_specs.emplace_back("cr5130", 7.1100f); make_specs.emplace_back("cr6530", 7.1100f); make_specs.emplace_back("cr8530", 7.1100f); make_specs.emplace_back("cs5530", 5.7500f); make_specs.emplace_back("cs5531", 5.7500f); make_specs.emplace_back("cs6530", 5.7500f); make_specs.emplace_back("cs6531", 5.7500f); make_specs.emplace_back("cu6530", 5.7500f); } { auto& make_specs = specs["agfaphoto"]; make_specs.reserve(53); make_specs.emplace_back("compact100", 5.7500f); make_specs.emplace_back("compact102", 6.1600f); make_specs.emplace_back("compact103", 6.0800f); make_specs.emplace_back("dc1030i", 7.1100f); make_specs.emplace_back("dc1033m", 5.7500f); make_specs.emplace_back("dc1033x", 7.1100f); make_specs.emplace_back("dc1338i", 7.1100f); make_specs.emplace_back("dc1338st", 7.1100f); make_specs.emplace_back("dc2030m", 6.1600f); make_specs.emplace_back("dc302", 4.8000f); make_specs.emplace_back("dc500", 4.8000f); make_specs.emplace_back("dc530i", 5.7500f); make_specs.emplace_back("dc533", 5.7500f); make_specs.emplace_back("dc600uw", 6.1600f); make_specs.emplace_back("dc630i", 5.7500f); make_specs.emplace_back("dc630x", 5.7500f); make_specs.emplace_back("dc630", 5.7500f); make_specs.emplace_back("dc633xs", 5.7500f); make_specs.emplace_back("dc633x", 5.7500f); make_specs.emplace_back("dc730i", 5.7500f); make_specs.emplace_back("dc733i", 5.7500f); make_specs.emplace_back("dc733s", 5.7500f); make_specs.emplace_back("dc735i", 5.7500f); make_specs.emplace_back("dc735", 5.7500f); make_specs.emplace_back("dc738i", 5.7500f); make_specs.emplace_back("dc830i", 7.1100f); make_specs.emplace_back("dc830", 5.7500f); make_specs.emplace_back("dc8330i", 5.7500f); make_specs.emplace_back("dc8338i", 5.7500f); make_specs.emplace_back("dc833m", 5.7500f); make_specs.emplace_back("dc8428s", 7.1100f); make_specs.emplace_back("ephoto1280", 6.1600f); make_specs.emplace_back("ephoto1680", 6.1600f); make_specs.emplace_back("ephotocl18", 6.1600f); make_specs.emplace_back("ephotocl30clik!", 6.1600f); make_specs.emplace_back("ephotocl30", 6.1600f); make_specs.emplace_back("ephotocl45", 6.1600f); make_specs.emplace_back("ephotocl50", 6.1600f); make_specs.emplace_back("optima100", 6.0800f); make_specs.emplace_back("optima102", 6.0800f); make_specs.emplace_back("optima103", 6.1600f); make_specs.emplace_back("optima104", 6.0800f); make_specs.emplace_back("optima105", 6.1600f); make_specs.emplace_back("optima1338mt", 6.1600f); make_specs.emplace_back("optima1438m", 6.0800f); make_specs.emplace_back("optima1", 6.0800f); make_specs.emplace_back("optima3", 6.1600f); make_specs.emplace_back("optima830uw", 5.7500f); make_specs.emplace_back("optima8328m", 5.7500f); make_specs.emplace_back("sensor505d", 5.7500f); make_specs.emplace_back("sensor505x", 5.7500f); make_specs.emplace_back("sensor530s", 5.7500f); make_specs.emplace_back("sensor830s", 5.7500f); } { auto& make_specs = specs["benq"]; make_specs.reserve(107); make_specs.emplace_back("ac100", 6.1600f); make_specs.emplace_back("ae100", 6.1600f); make_specs.emplace_back("c1420", 6.0800f); make_specs.emplace_back("dc2300", 4.5000f); make_specs.emplace_back("dc2410", 5.3300f); make_specs.emplace_back("dc3400", 4.5000f); make_specs.emplace_back("dc3410", 4.5000f); make_specs.emplace_back("dc4330", 5.7500f); make_specs.emplace_back("dc4500", 5.7500f); make_specs.emplace_back("dc5330", 5.7500f); make_specs.emplace_back("dcc1000", 7.1100f); make_specs.emplace_back("dcc1020", 6.1600f); make_specs.emplace_back("dcc1030eco", 6.1600f); make_specs.emplace_back("dcc1035", 6.1600f); make_specs.emplace_back("dcc1050", 7.5300f); make_specs.emplace_back("dcc1060", 6.1600f); make_specs.emplace_back("dcc1220", 6.1600f); make_specs.emplace_back("dcc1230", 6.1600f); make_specs.emplace_back("dcc1250", 6.1600f); make_specs.emplace_back("dcc1255", 6.1600f); make_specs.emplace_back("dcc1430", 6.1600f); make_specs.emplace_back("dcc1450", 6.1600f); make_specs.emplace_back("dcc1460", 6.0800f); make_specs.emplace_back("dcc1480", 6.1600f); make_specs.emplace_back("dcc30", 5.7500f); make_specs.emplace_back("dcc35", 5.7500f); make_specs.emplace_back("dcc40", 5.7500f); make_specs.emplace_back("dcc420", 5.7500f); make_specs.emplace_back("dcc500", 5.7500f); make_specs.emplace_back("dcc50", 7.1100f); make_specs.emplace_back("dcc510", 5.7500f); make_specs.emplace_back("dcc51", 5.7500f); make_specs.emplace_back("dcc520", 5.7500f); make_specs.emplace_back("dcc530", 5.7500f); make_specs.emplace_back("dcc540", 5.7500f); make_specs.emplace_back("dcc60", 7.1100f); make_specs.emplace_back("dcc610", 5.7500f); make_specs.emplace_back("dcc62", 7.1100f); make_specs.emplace_back("dcc630", 5.7500f); make_specs.emplace_back("dcc640", 7.5300f); make_specs.emplace_back("dcc740i", 5.7500f); make_specs.emplace_back("dcc740", 5.7500f); make_specs.emplace_back("dcc750", 5.7500f); make_specs.emplace_back("dcc800", 7.1100f); make_specs.emplace_back("dcc840", 5.7500f); make_specs.emplace_back("dcc850", 5.7500f); make_specs.emplace_back("dce1000", 7.1100f); make_specs.emplace_back("dce1020", 6.1600f); make_specs.emplace_back("dce1030", 6.1600f); make_specs.emplace_back("dce1035", 6.1600f); make_specs.emplace_back("dce1050t", 6.1600f); make_specs.emplace_back("dce1050", 7.5300f); make_specs.emplace_back("dce1220", 6.1600f); make_specs.emplace_back("dce1230", 6.1600f); make_specs.emplace_back("dce1240", 6.1600f); make_specs.emplace_back("dce1250", 6.1600f); make_specs.emplace_back("dce1260", 6.1600f); make_specs.emplace_back("dce1280", 6.1600f); make_specs.emplace_back("dce1420", 6.1600f); make_specs.emplace_back("dce1430", 6.1600f); make_specs.emplace_back("dce1460", 6.1600f); make_specs.emplace_back("dce1465", 6.1600f); make_specs.emplace_back("dce300", 6.4000f); make_specs.emplace_back("dce30", 6.4000f); make_specs.emplace_back("dce310", 6.4000f); make_specs.emplace_back("dce40", 5.7500f); make_specs.emplace_back("dce41", 5.7500f); make_specs.emplace_back("dce43", 5.7500f); make_specs.emplace_back("dce510", 5.7500f); make_specs.emplace_back("dce520plus", 5.7500f); make_specs.emplace_back("dce520", 5.7500f); make_specs.emplace_back("dce53", 5.7500f); make_specs.emplace_back("dce600", 5.7500f); make_specs.emplace_back("dce605", 5.7500f); make_specs.emplace_back("dce610", 5.7500f); make_specs.emplace_back("dce63plus", 5.7500f); make_specs.emplace_back("dce720", 5.7500f); make_specs.emplace_back("dce800", 5.7500f); make_specs.emplace_back("dce820", 5.7500f); make_specs.emplace_back("dcl1020", 6.1600f); make_specs.emplace_back("dcp1410", 6.1600f); make_specs.emplace_back("dcp500", 5.7500f); make_specs.emplace_back("dcp860", 7.1100f); make_specs.emplace_back("dcs1430", 6.0800f); make_specs.emplace_back("dcs30", 5.3300f); make_specs.emplace_back("dcs40", 5.7500f); make_specs.emplace_back("dct1260", 6.1600f); make_specs.emplace_back("dct700", 5.7500f); make_specs.emplace_back("dct800", 6.0300f); make_specs.emplace_back("dct850", 5.7500f); make_specs.emplace_back("dcw1220", 6.1600f); make_specs.emplace_back("dcx600", 5.7500f); make_specs.emplace_back("dcx710", 5.7500f); make_specs.emplace_back("dcx720", 5.7500f); make_specs.emplace_back("dcx725", 5.7500f); make_specs.emplace_back("dcx735", 5.7500f); make_specs.emplace_back("dcx800", 5.7500f); make_specs.emplace_back("dcx835", 5.7500f); make_specs.emplace_back("e1480", 6.1600f); make_specs.emplace_back("g1", 6.1600f); make_specs.emplace_back("gh200", 6.1600f); make_specs.emplace_back("gh600", 6.1600f); make_specs.emplace_back("gh700", 6.1600f); make_specs.emplace_back("lm100", 6.1600f); make_specs.emplace_back("s1410", 6.0800f); make_specs.emplace_back("s1420", 6.1600f); make_specs.emplace_back("t1460", 6.1600f); } { auto& make_specs = specs["canon"]; make_specs.reserve(314); make_specs.emplace_back("digitalixus100is", 6.1600f); make_specs.emplace_back("digitalixus110is", 6.1600f); make_specs.emplace_back("digitalixus200is", 6.1600f); make_specs.emplace_back("digitalixus300", 5.3300f); make_specs.emplace_back("digitalixus330", 5.3300f); make_specs.emplace_back("digitalixus400", 7.1100f); make_specs.emplace_back("digitalixus40", 5.7500f); make_specs.emplace_back("digitalixus430", 7.1100f); make_specs.emplace_back("digitalixus500", 7.1100f); make_specs.emplace_back("digitalixus50", 5.7500f); make_specs.emplace_back("digitalixus60", 5.7500f); make_specs.emplace_back("digitalixus65", 5.7500f); make_specs.emplace_back("digitalixus80is", 5.7500f); make_specs.emplace_back("digitalixus800is", 5.7500f); make_specs.emplace_back("digitalixus85is", 6.1600f); make_specs.emplace_back("digitalixus850is", 5.7500f); make_specs.emplace_back("digitalixus860is", 5.7500f); make_specs.emplace_back("digitalixus870is", 6.1600f); make_specs.emplace_back("digitalixus90is", 6.1600f); make_specs.emplace_back("digitalixus900ti", 7.1100f); make_specs.emplace_back("digitalixus95is", 6.1600f); make_specs.emplace_back("digitalixus950is", 5.7500f); make_specs.emplace_back("digitalixus960is", 7.5300f); make_specs.emplace_back("digitalixus970is", 6.1600f); make_specs.emplace_back("digitalixus980is", 7.5300f); make_specs.emplace_back("digitalixus990is", 6.1600f); make_specs.emplace_back("digitalixusizoom", 5.7500f); make_specs.emplace_back("digitalixusi7", 5.7500f); make_specs.emplace_back("digitalixusiis", 5.3300f); make_specs.emplace_back("digitalixusii", 5.3300f); make_specs.emplace_back("digitalixusi", 5.7500f); make_specs.emplace_back("digitalixusv2", 5.3300f); make_specs.emplace_back("digitalixusv3", 5.3300f); make_specs.emplace_back("digitalixusv", 5.3300f); make_specs.emplace_back("digitalixus", 5.3300f); make_specs.emplace_back("elph135/ixus145", 6.1600f); make_specs.emplace_back("elph140is/ixus150", 6.1600f); make_specs.emplace_back("elph150is/ixus155", 6.1600f); make_specs.emplace_back("elph160/ixus160", 6.1600f); make_specs.emplace_back("elph170is/ixus170", 6.1600f); make_specs.emplace_back("eos1000d", 22.2000f); make_specs.emplace_back("eos10d", 22.7000f); make_specs.emplace_back("eos1dc", 36.0000f); make_specs.emplace_back("eos1dmarkiin", 28.7000f); make_specs.emplace_back("eos1dmarkiii", 28.7000f); make_specs.emplace_back("eos1dmarkii", 28.7000f); make_specs.emplace_back("eos1dmarkiv", 27.9000f); make_specs.emplace_back("eos1dx", 36.0000f); make_specs.emplace_back("eos1dsmarkiii", 36.0000f); make_specs.emplace_back("eos1dsmarkii", 36.0000f); make_specs.emplace_back("eos1ds", 35.8000f); make_specs.emplace_back("eos1d", 28.7000f); make_specs.emplace_back("eos20da", 22.5000f); make_specs.emplace_back("eos20d", 22.5000f); make_specs.emplace_back("eos300d", 22.7000f); make_specs.emplace_back("eos30d", 22.5000f); make_specs.emplace_back("eos350d", 22.2000f); make_specs.emplace_back("eos400d", 22.2000f); make_specs.emplace_back("eos40d", 22.2000f); make_specs.emplace_back("eos450d", 22.2000f); make_specs.emplace_back("eos500d", 22.3000f); make_specs.emplace_back("eos50d", 22.3000f); make_specs.emplace_back("eos5dmarkiii", 36.0000f); make_specs.emplace_back("eos5dmarkii", 36.0000f); make_specs.emplace_back("eos5dsr", 36.0000f); make_specs.emplace_back("eos5ds", 36.0000f); make_specs.emplace_back("eos5d", 35.8000f); make_specs.emplace_back("eos60da", 22.3000f); make_specs.emplace_back("eos60d", 22.3000f); make_specs.emplace_back("eos6d", 35.8000f); make_specs.emplace_back("eos70d", 22.5000f); make_specs.emplace_back("eos7dmarkii", 22.4000f); make_specs.emplace_back("eos7d", 22.3000f); make_specs.emplace_back("eosd30", 22.7000f); make_specs.emplace_back("eosd60", 22.7000f); make_specs.emplace_back("eosm10", 22.3000f); make_specs.emplace_back("eosm3", 22.3000f); make_specs.emplace_back("eosm", 22.3000f); make_specs.emplace_back("eosrebelsl1/100d", 22.3000f); make_specs.emplace_back("eosrebelt2i/550d", 22.3000f); make_specs.emplace_back("eosrebelt3i/600d", 22.3000f); make_specs.emplace_back("eosrebelt3/1100d", 22.2000f); make_specs.emplace_back("eosrebelt4i/650d", 22.3000f); make_specs.emplace_back("eosrebelt5i/700d", 22.3000f); make_specs.emplace_back("eosrebelt5/1200d", 22.3000f); make_specs.emplace_back("eosrebelt6i/750d", 22.3000f); make_specs.emplace_back("eosrebelt6s/760d", 22.3000f); make_specs.emplace_back("ixus1000hs", 6.1600f); make_specs.emplace_back("ixus105", 6.1600f); make_specs.emplace_back("ixus1100hs", 6.1600f); make_specs.emplace_back("ixus115hs", 6.1600f); make_specs.emplace_back("ixus125hs", 6.1600f); make_specs.emplace_back("ixus130", 6.1600f); make_specs.emplace_back("ixus132", 6.1600f); make_specs.emplace_back("ixus165", 6.1600f); make_specs.emplace_back("ixus210", 6.1600f); make_specs.emplace_back("ixus220hs", 6.1600f); make_specs.emplace_back("ixus230hs", 6.1600f); make_specs.emplace_back("ixus240hs", 6.1600f); make_specs.emplace_back("ixus300hs", 6.1600f); make_specs.emplace_back("ixus310hs", 6.1600f); make_specs.emplace_back("ixus500hs", 6.1600f); make_specs.emplace_back("ixus510hs", 6.1600f); make_specs.emplace_back("powershot350", 4.8000f); make_specs.emplace_back("powershot600", 4.8000f); make_specs.emplace_back("powershota1000is", 6.1600f); make_specs.emplace_back("powershota100", 4.5000f); make_specs.emplace_back("powershota10", 5.3300f); make_specs.emplace_back("powershota1100is", 6.1600f); make_specs.emplace_back("powershota1200", 6.1600f); make_specs.emplace_back("powershota1300", 6.1600f); make_specs.emplace_back("powershota1400", 6.1600f); make_specs.emplace_back("powershota2000is", 6.1600f); make_specs.emplace_back("powershota200", 4.5000f); make_specs.emplace_back("powershota20", 5.3300f); make_specs.emplace_back("powershota2100is", 6.1600f); make_specs.emplace_back("powershota2200", 6.1600f); make_specs.emplace_back("powershota2300", 6.1600f); make_specs.emplace_back("powershota2400is", 6.1600f); make_specs.emplace_back("powershota2500", 6.1600f); make_specs.emplace_back("powershota2600", 6.1600f); make_specs.emplace_back("powershota3000is", 6.1600f); make_specs.emplace_back("powershota300", 5.3300f); make_specs.emplace_back("powershota30", 5.3300f); make_specs.emplace_back("powershota3100is", 6.1600f); make_specs.emplace_back("powershota310", 5.3300f); make_specs.emplace_back("powershota3200is", 6.1600f); make_specs.emplace_back("powershota3300is", 6.1600f); make_specs.emplace_back("powershota3400is", 6.1600f); make_specs.emplace_back("powershota3500is", 6.1600f); make_specs.emplace_back("powershota4000is", 6.1600f); make_specs.emplace_back("powershota400", 4.5000f); make_specs.emplace_back("powershota40", 5.3300f); make_specs.emplace_back("powershota410", 4.5000f); make_specs.emplace_back("powershota420", 4.8000f); make_specs.emplace_back("powershota430", 4.8000f); make_specs.emplace_back("powershota450", 4.8000f); make_specs.emplace_back("powershota460", 4.8000f); make_specs.emplace_back("powershota470", 5.7500f); make_specs.emplace_back("powershota480", 6.1600f); make_specs.emplace_back("powershota490", 6.1600f); make_specs.emplace_back("powershota495", 6.1600f); make_specs.emplace_back("powershota5zoom", 4.8000f); make_specs.emplace_back("powershota50", 4.8000f); make_specs.emplace_back("powershota510", 5.3300f); make_specs.emplace_back("powershota520", 5.7500f); make_specs.emplace_back("powershota530", 5.7500f); make_specs.emplace_back("powershota540", 5.7500f); make_specs.emplace_back("powershota550", 5.7500f); make_specs.emplace_back("powershota560", 5.7500f); make_specs.emplace_back("powershota570is", 5.7500f); make_specs.emplace_back("powershota580", 5.7500f); make_specs.emplace_back("powershota590is", 5.7500f); make_specs.emplace_back("powershota5", 4.8000f); make_specs.emplace_back("powershota60", 5.3300f); make_specs.emplace_back("powershota610", 7.1100f); make_specs.emplace_back("powershota620", 7.1100f); make_specs.emplace_back("powershota630", 7.1100f); make_specs.emplace_back("powershota640", 7.1100f); make_specs.emplace_back("powershota650is", 7.5300f); make_specs.emplace_back("powershota700", 5.7500f); make_specs.emplace_back("powershota70", 5.3300f); make_specs.emplace_back("powershota710is", 5.7500f); make_specs.emplace_back("powershota720is", 5.7500f); make_specs.emplace_back("powershota75", 5.3300f); make_specs.emplace_back("powershota800", 6.1600f); make_specs.emplace_back("powershota80", 7.1100f); make_specs.emplace_back("powershota810", 6.1600f); make_specs.emplace_back("powershota85", 5.3300f); make_specs.emplace_back("powershota95", 7.1100f); make_specs.emplace_back("powershotd10", 6.1600f); make_specs.emplace_back("powershotd20", 6.1600f); make_specs.emplace_back("powershotd30", 6.1600f); make_specs.emplace_back("powershote1", 6.1600f); make_specs.emplace_back("powershotelph100hs", 6.1600f); make_specs.emplace_back("powershotelph110hs", 6.1600f); make_specs.emplace_back("powershotelph115is", 6.1600f); make_specs.emplace_back("powershotelph130is", 6.1600f); make_specs.emplace_back("powershotelph300hs", 6.1600f); make_specs.emplace_back("powershotelph310hs", 6.1600f); make_specs.emplace_back("powershotelph320hs", 6.1600f); make_specs.emplace_back("powershotelph330hs", 6.1600f); make_specs.emplace_back("powershotelph340hs", 6.1600f); make_specs.emplace_back("powershotelph350hs", 6.1600f); make_specs.emplace_back("powershotelph500hs", 6.1600f); make_specs.emplace_back("powershotelph510hs", 6.1600f); make_specs.emplace_back("powershotelph520hs", 6.1600f); make_specs.emplace_back("powershotelph530hs", 6.1600f); make_specs.emplace_back("powershotg1xmarkii", 18.7000f); make_specs.emplace_back("powershotg1x", 18.7000f); make_specs.emplace_back("powershotg10", 7.5300f); make_specs.emplace_back("powershotg11", 7.5300f); make_specs.emplace_back("powershotg12", 7.5300f); make_specs.emplace_back("powershotg15", 7.5300f); make_specs.emplace_back("powershotg16", 7.5300f); make_specs.emplace_back("powershotg1", 7.1100f); make_specs.emplace_back("powershotg2", 7.1100f); make_specs.emplace_back("powershotg3x", 13.2000f); make_specs.emplace_back("powershotg3", 7.1100f); make_specs.emplace_back("powershotg5x", 13.2000f); make_specs.emplace_back("powershotg5", 7.1100f); make_specs.emplace_back("powershotg6", 7.1100f); make_specs.emplace_back("powershotg7x", 13.2000f); make_specs.emplace_back("powershotg7", 7.1100f); make_specs.emplace_back("powershotg9x", 13.2000f); make_specs.emplace_back("powershotg9", 7.5300f); make_specs.emplace_back("powershotn100", 7.5300f); make_specs.emplace_back("powershotn2", 6.1600f); make_specs.emplace_back("powershotn", 6.1600f); make_specs.emplace_back("powershotpro1", 8.8000f); make_specs.emplace_back("powershotpro70", 6.4000f); make_specs.emplace_back("powershotpro90is", 7.1100f); make_specs.emplace_back("powershots1is", 5.3300f); make_specs.emplace_back("powershots100digitalixus", 5.3300f); make_specs.emplace_back("powershots100", 7.5300f); make_specs.emplace_back("powershots10", 6.4000f); make_specs.emplace_back("powershots110", 7.5300f); make_specs.emplace_back("powershots120", 7.5300f); make_specs.emplace_back("powershots2is", 5.7500f); make_specs.emplace_back("powershots200", 5.3300f); make_specs.emplace_back("powershots20", 7.1100f); make_specs.emplace_back("powershots230", 5.3300f); make_specs.emplace_back("powershots3is", 5.7500f); make_specs.emplace_back("powershots300", 5.3300f); make_specs.emplace_back("powershots30", 7.1100f); make_specs.emplace_back("powershots330", 5.3300f); make_specs.emplace_back("powershots400", 7.1100f); make_specs.emplace_back("powershots40", 7.1100f); make_specs.emplace_back("powershots410", 7.1100f); make_specs.emplace_back("powershots45", 7.1100f); make_specs.emplace_back("powershots5is", 5.7500f); make_specs.emplace_back("powershots500", 7.1100f); make_specs.emplace_back("powershots50", 7.1100f); make_specs.emplace_back("powershots60", 7.1100f); make_specs.emplace_back("powershots70", 7.1100f); make_specs.emplace_back("powershots80", 7.1100f); make_specs.emplace_back("powershots90", 7.5300f); make_specs.emplace_back("powershots95", 7.5300f); make_specs.emplace_back("powershotsd1000", 5.7500f); make_specs.emplace_back("powershotsd100", 5.3300f); make_specs.emplace_back("powershotsd10", 5.7500f); make_specs.emplace_back("powershotsd1100is", 5.7500f); make_specs.emplace_back("powershotsd110", 5.3300f); make_specs.emplace_back("powershotsd1200is", 6.1600f); make_specs.emplace_back("powershotsd1300is", 6.1600f); make_specs.emplace_back("powershotsd1400is", 6.1600f); make_specs.emplace_back("powershotsd200", 5.7500f); make_specs.emplace_back("powershotsd20", 5.7500f); make_specs.emplace_back("powershotsd300", 5.7500f); make_specs.emplace_back("powershotsd30", 5.7500f); make_specs.emplace_back("powershotsd3500is", 6.1600f); make_specs.emplace_back("powershotsd4000is", 6.1600f); make_specs.emplace_back("powershotsd400", 5.7500f); make_specs.emplace_back("powershotsd40", 5.7500f); make_specs.emplace_back("powershotsd430wireless", 5.7500f); make_specs.emplace_back("powershotsd4500is", 6.1600f); make_specs.emplace_back("powershotsd450", 5.7500f); make_specs.emplace_back("powershotsd500", 7.1100f); make_specs.emplace_back("powershotsd550", 7.1100f); make_specs.emplace_back("powershotsd600", 5.7500f); make_specs.emplace_back("powershotsd630", 5.7500f); make_specs.emplace_back("powershotsd700is", 5.7500f); make_specs.emplace_back("powershotsd750", 5.7500f); make_specs.emplace_back("powershotsd770is", 6.1600f); make_specs.emplace_back("powershotsd780is", 6.1600f); make_specs.emplace_back("powershotsd790is", 6.1600f); make_specs.emplace_back("powershotsd800is", 5.7500f); make_specs.emplace_back("powershotsd850is", 5.7500f); make_specs.emplace_back("powershotsd870is", 5.7500f); make_specs.emplace_back("powershotsd880is", 6.1600f); make_specs.emplace_back("powershotsd890is", 6.1600f); make_specs.emplace_back("powershotsd900", 7.1100f); make_specs.emplace_back("powershotsd940is", 6.1600f); make_specs.emplace_back("powershotsd950is", 7.5300f); make_specs.emplace_back("powershotsd960is", 6.1600f); make_specs.emplace_back("powershotsd970is", 6.1600f); make_specs.emplace_back("powershotsd980is", 6.1600f); make_specs.emplace_back("powershotsd990is", 7.5300f); make_specs.emplace_back("powershotsx1is", 6.1600f); make_specs.emplace_back("powershotsx10is", 6.1600f); make_specs.emplace_back("powershotsx100is", 5.7500f); make_specs.emplace_back("powershotsx110is", 6.1600f); make_specs.emplace_back("powershotsx120is", 5.7500f); make_specs.emplace_back("powershotsx130is", 6.1600f); make_specs.emplace_back("powershotsx150is", 6.1600f); make_specs.emplace_back("powershotsx160is", 6.1600f); make_specs.emplace_back("powershotsx170is", 6.1600f); make_specs.emplace_back("powershotsx20is", 6.1600f); make_specs.emplace_back("powershotsx200is", 6.1600f); make_specs.emplace_back("powershotsx210is", 6.1600f); make_specs.emplace_back("powershotsx220hs", 6.1600f); make_specs.emplace_back("powershotsx230hs", 6.1600f); make_specs.emplace_back("powershotsx240hs", 6.1600f); make_specs.emplace_back("powershotsx260hs", 6.1600f); make_specs.emplace_back("powershotsx270hs", 6.1600f); make_specs.emplace_back("powershotsx280hs", 6.1600f); make_specs.emplace_back("powershotsx30is", 6.1600f); make_specs.emplace_back("powershotsx40hs", 6.1600f); make_specs.emplace_back("powershotsx400is", 6.1600f); make_specs.emplace_back("powershotsx410is", 6.1600f); make_specs.emplace_back("powershotsx50hs", 6.1600f); make_specs.emplace_back("powershotsx500is", 6.1600f); make_specs.emplace_back("powershotsx510hs", 6.1600f); make_specs.emplace_back("powershotsx520hs", 6.1600f); make_specs.emplace_back("powershotsx530hs", 6.1600f); make_specs.emplace_back("powershotsx60hs", 6.1600f); make_specs.emplace_back("powershotsx600hs", 6.1600f); make_specs.emplace_back("powershotsx610hs", 6.1600f); make_specs.emplace_back("powershotsx700hs", 6.1600f); make_specs.emplace_back("powershotsx710hs", 6.1600f); make_specs.emplace_back("powershottx1", 5.7500f); make_specs.emplace_back("pro90is", 7.1100f); make_specs.emplace_back("s200", 7.5300f); make_specs.emplace_back("sx220hs", 6.1600f); } { auto& make_specs = specs["casio"]; make_specs.reserve(161); make_specs.emplace_back("exfr10", 6.1600f); make_specs.emplace_back("exilimexje10", 6.1600f); make_specs.emplace_back("exn10", 6.1600f); make_specs.emplace_back("exn1", 6.1600f); make_specs.emplace_back("exn20", 6.1600f); make_specs.emplace_back("exn50", 6.1600f); make_specs.emplace_back("exn5", 6.1600f); make_specs.emplace_back("extr10", 6.1600f); make_specs.emplace_back("extr15", 6.1600f); make_specs.emplace_back("exzr400", 6.1600f); make_specs.emplace_back("exzr700", 6.1600f); make_specs.emplace_back("exzr800", 6.1600f); make_specs.emplace_back("exzs30", 6.1600f); make_specs.emplace_back("exilimex100", 7.5300f); make_specs.emplace_back("exilimex10", 7.5300f); make_specs.emplace_back("exilimexfc100", 6.1600f); make_specs.emplace_back("exilimexfc150", 6.1600f); make_specs.emplace_back("exilimexfc160s", 6.1600f); make_specs.emplace_back("exilimexfh100", 6.1600f); make_specs.emplace_back("exilimexfh150", 6.1600f); make_specs.emplace_back("exilimexfh20", 6.1600f); make_specs.emplace_back("exilimexfh25", 6.1600f); make_specs.emplace_back("exilimexfs10", 6.1600f); make_specs.emplace_back("exilimexg1", 6.1600f); make_specs.emplace_back("exilimexh10", 6.1600f); make_specs.emplace_back("exilimexh15", 6.1600f); make_specs.emplace_back("exilimexh20g", 6.1600f); make_specs.emplace_back("exilimexh30", 6.1600f); make_specs.emplace_back("exilimexh50", 6.1600f); make_specs.emplace_back("exilimexh5", 6.1600f); make_specs.emplace_back("exilimexm1", 5.3300f); make_specs.emplace_back("exilimexm20", 5.3300f); make_specs.emplace_back("exilimexm2", 7.1100f); make_specs.emplace_back("exilimexp505", 5.7500f); make_specs.emplace_back("exilimexp600", 7.1100f); make_specs.emplace_back("exilimexp700", 7.1100f); make_specs.emplace_back("exilimexs100", 4.5000f); make_specs.emplace_back("exilimexs10", 6.1600f); make_specs.emplace_back("exilimexs12", 6.1600f); make_specs.emplace_back("exilimexs1", 5.3300f); make_specs.emplace_back("exilimexs200", 6.1600f); make_specs.emplace_back("exilimexs20", 5.3300f); make_specs.emplace_back("exilimexs2", 7.1100f); make_specs.emplace_back("exilimexs3", 7.1100f); make_specs.emplace_back("exilimexs500", 5.7500f); make_specs.emplace_back("exilimexs5", 6.1600f); make_specs.emplace_back("exilimexs600d", 5.7500f); make_specs.emplace_back("exilimexs600", 5.7500f); make_specs.emplace_back("exilimexs770d", 5.7500f); make_specs.emplace_back("exilimexs770", 5.7500f); make_specs.emplace_back("exilimexs7", 6.1600f); make_specs.emplace_back("exilimexs880", 5.7500f); make_specs.emplace_back("exilimexs8", 6.0800f); make_specs.emplace_back("exilimextr100", 6.1600f); make_specs.emplace_back("exilimextr150", 6.1600f); make_specs.emplace_back("exilimexv7", 5.7500f); make_specs.emplace_back("exilimexv8", 5.7500f); make_specs.emplace_back("exilimexz1000", 7.1100f); make_specs.emplace_back("exilimexz100", 6.1600f); make_specs.emplace_back("exilimexz1050", 7.3100f); make_specs.emplace_back("exilimexz1080", 7.3100f); make_specs.emplace_back("exilimexz10", 5.7500f); make_specs.emplace_back("exilimexz110", 5.7500f); make_specs.emplace_back("exilimexz1200sr", 7.5300f); make_specs.emplace_back("exilimexz120", 7.1100f); make_specs.emplace_back("exilimexz150", 5.7500f); make_specs.emplace_back("exilimexz16", 6.1600f); make_specs.emplace_back("exilimexz19", 5.7500f); make_specs.emplace_back("exilimexz1", 6.1600f); make_specs.emplace_back("exilimexz2000", 6.1600f); make_specs.emplace_back("exilimexz200", 6.1600f); make_specs.emplace_back("exilimexz20", 5.7500f); make_specs.emplace_back("exilimexz2300", 6.1600f); make_specs.emplace_back("exilimexz250", 5.7500f); make_specs.emplace_back("exilimexz25", 6.1600f); make_specs.emplace_back("exilimexz270", 5.7500f); make_specs.emplace_back("exilimexz280", 5.7500f); make_specs.emplace_back("exilimexz29", 5.7500f); make_specs.emplace_back("exilimexz2", 6.1600f); make_specs.emplace_back("exilimexz3000", 6.1600f); make_specs.emplace_back("exilimexz300", 6.1600f); make_specs.emplace_back("exilimexz30", 5.7500f); make_specs.emplace_back("exilimexz330", 6.1600f); make_specs.emplace_back("exilimexz33", 6.1600f); make_specs.emplace_back("exilimexz350", 6.1600f); make_specs.emplace_back("exilimexz35", 6.1600f); make_specs.emplace_back("exilimexz3", 5.7500f); make_specs.emplace_back("exilimexz400", 6.1600f); make_specs.emplace_back("exilimexz40", 5.7500f); make_specs.emplace_back("exilimexz450", 6.1600f); make_specs.emplace_back("exilimexz4", 5.7500f); make_specs.emplace_back("exilimexz500", 5.7500f); make_specs.emplace_back("exilimexz50", 5.7500f); make_specs.emplace_back("exilimexz550", 6.1600f); make_specs.emplace_back("exilimexz55", 5.7500f); make_specs.emplace_back("exilimexz57", 5.7500f); make_specs.emplace_back("exilimexz5", 5.7500f); make_specs.emplace_back("exilimexz600", 5.7500f); make_specs.emplace_back("exilimexz60", 7.1100f); make_specs.emplace_back("exilimexz65", 5.7500f); make_specs.emplace_back("exilimexz6", 5.7500f); make_specs.emplace_back("exilimexz700", 5.7500f); make_specs.emplace_back("exilimexz70", 5.7500f); make_specs.emplace_back("exilimexz750", 7.1100f); make_specs.emplace_back("exilimexz75", 5.7500f); make_specs.emplace_back("exilimexz77", 5.7500f); make_specs.emplace_back("exilimexz7", 5.7500f); make_specs.emplace_back("exilimexz800", 6.1600f); make_specs.emplace_back("exilimexz80", 5.7500f); make_specs.emplace_back("exilimexz850", 7.1100f); make_specs.emplace_back("exilimexz85", 5.7500f); make_specs.emplace_back("exilimexz8", 5.7500f); make_specs.emplace_back("exilimexz90", 6.1600f); make_specs.emplace_back("exilimexz9", 5.7500f); make_specs.emplace_back("exilimexzr1000", 6.1600f); make_specs.emplace_back("exilimexzr100", 6.1600f); make_specs.emplace_back("exilimexzr10", 6.1600f); make_specs.emplace_back("exilimexzr1100", 6.1600f); make_specs.emplace_back("exilimexzr15", 6.1600f); make_specs.emplace_back("exilimexzr200", 6.1600f); make_specs.emplace_back("exilimexzr20", 6.1600f); make_specs.emplace_back("exilimexzr300", 6.1600f); make_specs.emplace_back("exilimexzs100", 6.1600f); make_specs.emplace_back("exilimexzs10", 6.1600f); make_specs.emplace_back("exilimexzs12", 6.1600f); make_specs.emplace_back("exilimexzs150", 6.1600f); make_specs.emplace_back("exilimexzs15", 6.1600f); make_specs.emplace_back("exilimexzs20", 6.1600f); make_specs.emplace_back("exilimexzs5", 6.1600f); make_specs.emplace_back("exilimexzs6", 6.1600f); make_specs.emplace_back("exilimproexf1", 7.1100f); make_specs.emplace_back("exilimqvr100", 6.1600f); make_specs.emplace_back("exilimtryx", 6.1600f); make_specs.emplace_back("gv10", 4.5000f); make_specs.emplace_back("gv20", 4.5000f); make_specs.emplace_back("qv2000ux", 6.4000f); make_specs.emplace_back("qv2100", 5.3300f); make_specs.emplace_back("qv2300ux", 5.3300f); make_specs.emplace_back("qv2400ux", 5.3300f); make_specs.emplace_back("qv2800ux", 5.3300f); make_specs.emplace_back("qv2900ux", 5.3300f); make_specs.emplace_back("qv3000ex", 7.1100f); make_specs.emplace_back("qv300", 4.8000f); make_specs.emplace_back("qv3500ex", 7.1100f); make_specs.emplace_back("qv3ex/xv3", 7.1100f); make_specs.emplace_back("qv4000", 7.1100f); make_specs.emplace_back("qv5000sx", 4.8000f); make_specs.emplace_back("qv5500sx", 4.8000f); make_specs.emplace_back("qv5700", 7.1100f); make_specs.emplace_back("qv7000sx", 4.8000f); make_specs.emplace_back("qv700", 4.8000f); make_specs.emplace_back("qv770", 4.8000f); make_specs.emplace_back("qv8000sx", 4.8000f); make_specs.emplace_back("qvr3", 7.1100f); make_specs.emplace_back("qvr40", 7.1100f); make_specs.emplace_back("qvr41", 7.1100f); make_specs.emplace_back("qvr4", 7.1100f); make_specs.emplace_back("qvr51", 7.1100f); make_specs.emplace_back("qvr52", 7.1100f); make_specs.emplace_back("qvr61", 7.1100f); make_specs.emplace_back("qvr62", 7.1100f); } { auto& make_specs = specs["concord"]; make_specs.reserve(40); make_specs.emplace_back("1500", 6.4000f); make_specs.emplace_back("3043", 6.4000f); make_specs.emplace_back("3045", 6.4000f); make_specs.emplace_back("3046", 6.4000f); make_specs.emplace_back("3047", 6.4000f); make_specs.emplace_back("3345z", 6.4000f); make_specs.emplace_back("3346z", 6.4000f); make_specs.emplace_back("4042", 5.7500f); make_specs.emplace_back("4340z", 5.7500f); make_specs.emplace_back("5040", 7.1100f); make_specs.emplace_back("5340z", 5.7500f); make_specs.emplace_back("5345z", 7.1100f); make_specs.emplace_back("6340z", 7.1100f); make_specs.emplace_back("642", 6.4000f); make_specs.emplace_back("dvx", 6.4000f); make_specs.emplace_back("es500z", 5.7500f); make_specs.emplace_back("es510z", 7.1100f); make_specs.emplace_back("eyeq1000", 6.4000f); make_specs.emplace_back("eyeq1300", 6.4000f); make_specs.emplace_back("eyeq2040", 6.4000f); make_specs.emplace_back("eyeq2133z", 6.4000f); make_specs.emplace_back("eyeq3040af", 6.4000f); make_specs.emplace_back("eyeq3103", 6.4000f); make_specs.emplace_back("eyeq3120af", 4.0000f); make_specs.emplace_back("eyeq3132z", 6.4000f); make_specs.emplace_back("eyeq3340z", 5.3300f); make_specs.emplace_back("eyeq3341z", 7.1100f); make_specs.emplace_back("eyeq3343z", 5.3300f); make_specs.emplace_back("eyeq4060af", 7.1100f); make_specs.emplace_back("eyeq4330z", 7.1100f); make_specs.emplace_back("eyeq4342z", 7.1100f); make_specs.emplace_back("eyeq4360z", 7.1100f); make_specs.emplace_back("eyeq4363z", 7.1100f); make_specs.emplace_back("eyeq5062af", 7.1100f); make_specs.emplace_back("eyeq5330z", 7.1100f); make_specs.emplace_back("eyeqduo2000", 7.1100f); make_specs.emplace_back("eyeqduolcd", 6.4000f); make_specs.emplace_back("eyeqgo2000", 7.1100f); make_specs.emplace_back("eyeqgolcd", 6.4000f); make_specs.emplace_back("eyeqgowireless", 7.1100f); } { auto& make_specs = specs["contax"]; make_specs.reserve(5); make_specs.emplace_back("i4r", 5.3300f); make_specs.emplace_back("ndigital", 36.0000f); make_specs.emplace_back("sl300rt", 5.3300f); make_specs.emplace_back("tvsdigital", 7.1100f); make_specs.emplace_back("u4r", 5.3300f); } { auto& make_specs = specs["epson"]; make_specs.reserve(19); make_specs.emplace_back("l500v", 5.7500f); make_specs.emplace_back("photopc3000zoom", 7.1100f); make_specs.emplace_back("photopc3100zoom", 7.1100f); make_specs.emplace_back("photopc500", 4.8000f); make_specs.emplace_back("photopc550", 4.8000f); make_specs.emplace_back("photopc600", 4.8000f); make_specs.emplace_back("photopc650", 4.8000f); make_specs.emplace_back("photopc700", 4.8000f); make_specs.emplace_back("photopc750zoom", 6.4000f); make_specs.emplace_back("photopc800", 6.4000f); make_specs.emplace_back("photopc850zoom", 6.4000f); make_specs.emplace_back("photopcl200", 5.7500f); make_specs.emplace_back("photopcl300", 5.7500f); make_specs.emplace_back("photopcl400", 5.7500f); make_specs.emplace_back("photopcl410", 5.7500f); make_specs.emplace_back("photopcl500v", 5.7500f); make_specs.emplace_back("rd1xg", 23.7000f); make_specs.emplace_back("rd1", 23.7000f); make_specs.emplace_back("rd1s", 23.7000f); } { auto& make_specs = specs["fujifilm"]; make_specs.reserve(365); make_specs.emplace_back("a850", 5.7500f); make_specs.emplace_back("bigjobhd3w", 5.7500f); make_specs.emplace_back("bigjobhd1", 5.3300f); make_specs.emplace_back("digitalq1", 6.4000f); make_specs.emplace_back("ds260hd", 6.4000f); make_specs.emplace_back("ds300", 8.8000f); make_specs.emplace_back("finepix1300", 5.3300f); make_specs.emplace_back("finepix1400z", 5.3300f); make_specs.emplace_back("finepix2300", 5.3300f); make_specs.emplace_back("finepix2400zoom", 5.3300f); make_specs.emplace_back("finepix2600zoom", 5.3300f); make_specs.emplace_back("finepix2650", 5.3300f); make_specs.emplace_back("finepix2800zoom", 5.3300f); make_specs.emplace_back("finepix30i", 5.3300f); make_specs.emplace_back("finepix3800", 5.3300f); make_specs.emplace_back("finepix40i", 5.3300f); make_specs.emplace_back("finepix4700zoom", 7.5300f); make_specs.emplace_back("finepix4800zoom", 7.5300f); make_specs.emplace_back("finepix4900zoom", 7.5300f); make_specs.emplace_back("finepix50i", 7.5300f); make_specs.emplace_back("finepix6800zoom", 7.5300f); make_specs.emplace_back("finepix6900zoom", 7.5300f); make_specs.emplace_back("finepixa100", 6.1600f); make_specs.emplace_back("finepixa101", 5.3300f); make_specs.emplace_back("finepixa120", 5.3300f); make_specs.emplace_back("finepixa150", 6.1600f); make_specs.emplace_back("finepixa160", 6.1600f); make_specs.emplace_back("finepixa170", 6.1600f); make_specs.emplace_back("finepixa175", 6.1600f); make_specs.emplace_back("finepixa180", 6.1600f); make_specs.emplace_back("finepixa200", 5.3300f); make_specs.emplace_back("finepixa201", 5.3300f); make_specs.emplace_back("finepixa202", 5.3300f); make_specs.emplace_back("finepixa203", 5.3300f); make_specs.emplace_back("finepixa204", 5.3300f); make_specs.emplace_back("finepixa205zoom", 5.3300f); make_specs.emplace_back("finepixa210zoom", 5.3300f); make_specs.emplace_back("finepixa220", 6.1600f); make_specs.emplace_back("finepixa225", 6.1600f); make_specs.emplace_back("finepixa235", 6.1600f); make_specs.emplace_back("finepixa303", 5.3300f); make_specs.emplace_back("finepixa310zoom", 5.3300f); make_specs.emplace_back("finepixa330", 5.3300f); make_specs.emplace_back("finepixa340", 5.3300f); make_specs.emplace_back("finepixa345zoom", 5.7500f); make_specs.emplace_back("finepixa350zoom", 5.7500f); make_specs.emplace_back("finepixa400zoom", 5.7500f); make_specs.emplace_back("finepixa500zoom", 5.7500f); make_specs.emplace_back("finepixa510", 5.7500f); make_specs.emplace_back("finepixa600zoom", 7.5300f); make_specs.emplace_back("finepixa610", 5.7500f); make_specs.emplace_back("finepixa700", 8.0000f); make_specs.emplace_back("finepixa800", 8.0000f); make_specs.emplace_back("finepixa820", 8.0000f); make_specs.emplace_back("finepixa825", 8.0000f); make_specs.emplace_back("finepixa900", 8.0000f); make_specs.emplace_back("finepixa920", 8.0000f); make_specs.emplace_back("finepixav100", 6.1600f); make_specs.emplace_back("finepixav105", 6.1600f); make_specs.emplace_back("finepixav110", 6.1600f); make_specs.emplace_back("finepixav130", 6.1600f); make_specs.emplace_back("finepixav140", 6.1600f); make_specs.emplace_back("finepixav150", 6.1600f); make_specs.emplace_back("finepixav180", 6.1600f); make_specs.emplace_back("finepixav200", 6.1600f); make_specs.emplace_back("finepixav205", 6.1600f); make_specs.emplace_back("finepixav250", 6.1600f); make_specs.emplace_back("finepixav255", 6.1600f); make_specs.emplace_back("finepixax200", 6.1600f); make_specs.emplace_back("finepixax205", 6.1600f); make_specs.emplace_back("finepixax230", 6.1600f); make_specs.emplace_back("finepixax245w", 6.1600f); make_specs.emplace_back("finepixax250", 6.1600f); make_specs.emplace_back("finepixax280", 6.1600f); make_specs.emplace_back("finepixax300", 6.1600f); make_specs.emplace_back("finepixax305", 6.1600f); make_specs.emplace_back("finepixax350", 6.1600f); make_specs.emplace_back("finepixax355", 6.1600f); make_specs.emplace_back("finepixax500", 6.1600f); make_specs.emplace_back("finepixax550", 6.1600f); make_specs.emplace_back("finepixax650", 6.1600f); make_specs.emplace_back("finepixe500zoom", 5.7500f); make_specs.emplace_back("finepixe510zoom", 5.7500f); make_specs.emplace_back("finepixe550zoom", 7.5300f); make_specs.emplace_back("finepixe900zoom", 8.0000f); make_specs.emplace_back("finepixex20", 5.3300f); make_specs.emplace_back("finepixf10zoom", 7.5300f); make_specs.emplace_back("finepixf100fd", 8.0000f); make_specs.emplace_back("finepixf11zoom", 7.5300f); make_specs.emplace_back("finepixf20zoom", 7.5300f); make_specs.emplace_back("finepixf200exr", 8.0000f); make_specs.emplace_back("finepixf30zoom", 7.5300f); make_specs.emplace_back("finepixf300exr", 6.4000f); make_specs.emplace_back("finepixf305exr", 6.4000f); make_specs.emplace_back("finepixf31fd", 7.5300f); make_specs.emplace_back("finepixf401zoom", 5.3300f); make_specs.emplace_back("finepixf402", 5.3300f); make_specs.emplace_back("finepixf40fd", 8.0000f); make_specs.emplace_back("finepixf410zoom", 5.3300f); make_specs.emplace_back("finepixf420zoom", 5.3300f); make_specs.emplace_back("finepixf440zoom", 5.7500f); make_specs.emplace_back("finepixf450zoom", 5.7500f); make_specs.emplace_back("finepixf455zoom", 5.7500f); make_specs.emplace_back("finepixf45fd", 8.0000f); make_specs.emplace_back("finepixf460", 5.7500f); make_specs.emplace_back("finepixf470zoom", 5.7500f); make_specs.emplace_back("finepixf47fd", 8.0000f); make_specs.emplace_back("finepixf480zoom", 5.7500f); make_specs.emplace_back("finepixf500exr", 6.4000f); make_specs.emplace_back("finepixf50fd", 8.0000f); make_specs.emplace_back("finepixf550exr", 6.4000f); make_specs.emplace_back("finepixf600exr", 6.4000f); make_specs.emplace_back("finepixf601zoom", 7.5300f); make_specs.emplace_back("finepixf605exr", 6.4000f); make_specs.emplace_back("finepixf60fd", 8.0000f); make_specs.emplace_back("finepixf610", 7.5300f); make_specs.emplace_back("finepixf650zoom", 5.7500f); make_specs.emplace_back("finepixf660exr", 6.4000f); make_specs.emplace_back("finepixf700", 7.5300f); make_specs.emplace_back("finepixf70exr", 6.4000f); make_specs.emplace_back("finepixf710", 7.5300f); make_specs.emplace_back("finepixf72exr", 6.4000f); make_specs.emplace_back("finepixf750exr", 6.4000f); make_specs.emplace_back("finepixf75exr", 6.4000f); make_specs.emplace_back("finepixf770exr", 6.4000f); make_specs.emplace_back("finepixf800exr", 6.4000f); make_specs.emplace_back("finepixf80exr", 6.4000f); make_specs.emplace_back("finepixf810zoom", 7.5300f); make_specs.emplace_back("finepixf850exr", 6.4000f); make_specs.emplace_back("finepixf85exr", 6.4000f); make_specs.emplace_back("finepixf900exr", 6.4000f); make_specs.emplace_back("finepixhs10", 6.1600f); make_specs.emplace_back("finepixhs11", 6.1600f); make_specs.emplace_back("finepixhs20exr", 6.4000f); make_specs.emplace_back("finepixhs22exr", 6.4000f); make_specs.emplace_back("finepixhs25exr", 6.4000f); make_specs.emplace_back("finepixhs30exr", 6.4000f); make_specs.emplace_back("finepixhs35exr", 6.4000f); make_specs.emplace_back("finepixhs50exr", 6.4000f); make_specs.emplace_back("finepixis1", 8.0000f); make_specs.emplace_back("finepixispro", 23.0000f); make_specs.emplace_back("finepixj100", 6.1600f); make_specs.emplace_back("finepixj10", 5.7500f); make_specs.emplace_back("finepixj110w", 6.1600f); make_specs.emplace_back("finepixj120", 6.1600f); make_specs.emplace_back("finepixj12", 5.7500f); make_specs.emplace_back("finepixj150w", 6.1600f); make_specs.emplace_back("finepixj15", 5.7500f); make_specs.emplace_back("finepixj20", 6.1600f); make_specs.emplace_back("finepixj210", 6.1600f); make_specs.emplace_back("finepixj22", 6.1600f); make_specs.emplace_back("finepixj250", 6.1600f); make_specs.emplace_back("finepixj25", 6.1600f); make_specs.emplace_back("finepixj26", 6.1600f); make_specs.emplace_back("finepixj27", 6.1600f); make_specs.emplace_back("finepixj28", 6.1600f); make_specs.emplace_back("finepixj29", 6.1600f); make_specs.emplace_back("finepixj30", 6.1600f); make_specs.emplace_back("finepixj32", 6.1600f); make_specs.emplace_back("finepixj35", 6.1600f); make_specs.emplace_back("finepixj37", 6.1600f); make_specs.emplace_back("finepixj38", 6.1600f); make_specs.emplace_back("finepixj50", 5.7500f); make_specs.emplace_back("finepixjv100", 6.1600f); make_specs.emplace_back("finepixjv105", 6.1600f); make_specs.emplace_back("finepixjv110", 6.1600f); make_specs.emplace_back("finepixjv150", 6.1600f); make_specs.emplace_back("finepixjv200", 6.1600f); make_specs.emplace_back("finepixjv205", 6.1600f); make_specs.emplace_back("finepixjv250", 6.1600f); make_specs.emplace_back("finepixjv255", 6.1600f); make_specs.emplace_back("finepixjx200", 6.1600f); make_specs.emplace_back("finepixjx205", 6.1600f); make_specs.emplace_back("finepixjx210", 6.1600f); make_specs.emplace_back("finepixjx250", 6.1600f); make_specs.emplace_back("finepixjx280", 6.1600f); make_specs.emplace_back("finepixjx300", 6.1600f); make_specs.emplace_back("finepixjx305", 6.1600f); make_specs.emplace_back("finepixjx350", 6.1600f); make_specs.emplace_back("finepixjx355", 6.1600f); make_specs.emplace_back("finepixjx370", 6.1600f); make_specs.emplace_back("finepixjx375", 6.1600f); make_specs.emplace_back("finepixjx400", 6.1600f); make_specs.emplace_back("finepixjx405", 6.1600f); make_specs.emplace_back("finepixjx420", 6.1600f); make_specs.emplace_back("finepixjx500", 6.1600f); make_specs.emplace_back("finepixjx520", 6.1600f); make_specs.emplace_back("finepixjx530", 6.1600f); make_specs.emplace_back("finepixjx550", 6.1600f); make_specs.emplace_back("finepixjx580", 6.1600f); make_specs.emplace_back("finepixjx700", 6.1600f); make_specs.emplace_back("finepixjz100", 6.1600f); make_specs.emplace_back("finepixjz200", 6.1600f); make_specs.emplace_back("finepixjz250", 6.1600f); make_specs.emplace_back("finepixjz300", 6.1600f); make_specs.emplace_back("finepixjz305", 6.1600f); make_specs.emplace_back("finepixjz310", 6.1600f); make_specs.emplace_back("finepixjz500", 6.1600f); make_specs.emplace_back("finepixjz505", 6.1600f); make_specs.emplace_back("finepixjz510", 6.1600f); make_specs.emplace_back("finepixjz700", 6.1600f); make_specs.emplace_back("finepixm603", 7.5300f); make_specs.emplace_back("finepixpr21", 6.4000f); make_specs.emplace_back("finepixreal3dw1", 6.1600f); make_specs.emplace_back("finepixreal3dw3", 6.1600f); make_specs.emplace_back("finepixs1pro", 23.0000f); make_specs.emplace_back("finepixs1000fd", 6.1600f); make_specs.emplace_back("finepixs100fs", 8.8000f); make_specs.emplace_back("finepixs1500", 6.1600f); make_specs.emplace_back("finepixs1600", 6.1600f); make_specs.emplace_back("finepixs1700", 6.1600f); make_specs.emplace_back("finepixs1730", 6.1600f); make_specs.emplace_back("finepixs1770", 6.1600f); make_specs.emplace_back("finepixs1800", 6.1600f); make_specs.emplace_back("finepixs1850", 6.1600f); make_specs.emplace_back("finepixs1880", 6.1600f); make_specs.emplace_back("finepixs1900", 6.1600f); make_specs.emplace_back("finepixs1", 6.1600f); make_specs.emplace_back("finepixs2pro", 23.0000f); make_specs.emplace_back("finepixs20pro", 7.5300f); make_specs.emplace_back("finepixs2000hd", 6.1600f); make_specs.emplace_back("finepixs200exr", 8.0000f); make_specs.emplace_back("finepixs205exr", 8.0000f); make_specs.emplace_back("finepixs2500hd", 6.1600f); make_specs.emplace_back("finepixs2550hd", 6.1600f); make_specs.emplace_back("finepixs2600hd", 6.1600f); make_specs.emplace_back("finepixs2800hd", 6.1600f); make_specs.emplace_back("finepixs2900hd", 6.1600f); make_specs.emplace_back("finepixs2950", 6.1600f); make_specs.emplace_back("finepixs2980", 6.1600f); make_specs.emplace_back("finepixs2990", 6.1600f); make_specs.emplace_back("finepixs3pro", 23.5000f); make_specs.emplace_back("finepixs3000z", 5.3300f); make_specs.emplace_back("finepixs304", 5.3300f); make_specs.emplace_back("finepixs3200", 6.1600f); make_specs.emplace_back("finepixs3250", 6.1600f); make_specs.emplace_back("finepixs3300", 6.1600f); make_specs.emplace_back("finepixs3350", 6.1600f); make_specs.emplace_back("finepixs3400", 6.1600f); make_specs.emplace_back("finepixs3450", 6.1600f); make_specs.emplace_back("finepixs3500zoom", 5.3300f); make_specs.emplace_back("finepixs4000", 6.1600f); make_specs.emplace_back("finepixs4050", 6.1600f); make_specs.emplace_back("finepixs4200", 6.1600f); make_specs.emplace_back("finepixs4300", 6.1600f); make_specs.emplace_back("finepixs4400", 6.1600f); make_specs.emplace_back("finepixs4500", 6.1600f); make_specs.emplace_back("finepixs4600", 6.1600f); make_specs.emplace_back("finepixs4700", 6.1600f); make_specs.emplace_back("finepixs4800", 6.1600f); make_specs.emplace_back("finepixs5pro", 23.0000f); make_specs.emplace_back("finepixs5000zoom", 5.3300f); make_specs.emplace_back("finepixs5100zoom", 5.3300f); make_specs.emplace_back("finepixs5200zoom", 5.7500f); make_specs.emplace_back("finepixs5500zoom", 5.3300f); make_specs.emplace_back("finepixs5600zoom", 5.7500f); make_specs.emplace_back("finepixs5700zoom", 5.7500f); make_specs.emplace_back("finepixs5800", 5.7500f); make_specs.emplace_back("finepixs6000fd", 7.5300f); make_specs.emplace_back("finepixs602zoom", 7.5300f); make_specs.emplace_back("finepixs602zpro", 7.5300f); make_specs.emplace_back("finepixs6500fd", 7.5300f); make_specs.emplace_back("finepixs6600", 6.1600f); make_specs.emplace_back("finepixs6700", 6.1600f); make_specs.emplace_back("finepixs6800", 6.1600f); make_specs.emplace_back("finepixs7000zoom", 7.5300f); make_specs.emplace_back("finepixs8000fd", 6.0300f); make_specs.emplace_back("finepixs8100fd", 6.1600f); make_specs.emplace_back("finepixs8200", 6.1600f); make_specs.emplace_back("finepixs8300", 6.1600f); make_specs.emplace_back("finepixs8400", 6.1600f); make_specs.emplace_back("finepixs8500", 6.1600f); make_specs.emplace_back("finepixs8600", 6.1600f); make_specs.emplace_back("finepixs9000zoom", 8.0000f); make_specs.emplace_back("finepixs9100", 8.0000f); make_specs.emplace_back("finepixs9200", 6.1600f); make_specs.emplace_back("finepixs9400w", 6.1600f); make_specs.emplace_back("finepixs9500", 8.0000f); make_specs.emplace_back("finepixs9600", 8.0000f); make_specs.emplace_back("finepixs9800", 6.1600f); make_specs.emplace_back("finepixs9900w", 6.1600f); make_specs.emplace_back("finepixsl1000", 6.1600f); make_specs.emplace_back("finepixsl240", 6.1600f); make_specs.emplace_back("finepixsl260", 6.1600f); make_specs.emplace_back("finepixsl280", 6.1600f); make_specs.emplace_back("finepixsl300", 6.1600f); make_specs.emplace_back("finepixt200", 6.1600f); make_specs.emplace_back("finepixt205", 6.1600f); make_specs.emplace_back("finepixt300", 6.1600f); make_specs.emplace_back("finepixt305", 6.1600f); make_specs.emplace_back("finepixt350", 6.1600f); make_specs.emplace_back("finepixt400", 6.1600f); make_specs.emplace_back("finepixt500", 6.1600f); make_specs.emplace_back("finepixt550", 6.1600f); make_specs.emplace_back("finepixv10zoom", 5.7500f); make_specs.emplace_back("finepixx100", 23.6000f); make_specs.emplace_back("finepixxp100", 6.1600f); make_specs.emplace_back("finepixxp10", 6.1600f); make_specs.emplace_back("finepixxp11", 6.1600f); make_specs.emplace_back("finepixxp150", 6.1600f); make_specs.emplace_back("finepixxp170", 6.1600f); make_specs.emplace_back("finepixxp200", 6.1600f); make_specs.emplace_back("finepixxp20", 6.1600f); make_specs.emplace_back("finepixxp22", 6.1600f); make_specs.emplace_back("finepixxp30", 6.1600f); make_specs.emplace_back("finepixxp33", 6.1600f); make_specs.emplace_back("finepixxp50", 6.1600f); make_specs.emplace_back("finepixxp60", 6.1600f); make_specs.emplace_back("finepixxp70", 6.1600f); make_specs.emplace_back("finepixxp80", 6.1600f); make_specs.emplace_back("finepixz1000exr", 6.4000f); make_specs.emplace_back("finepixz100fd", 5.7500f); make_specs.emplace_back("finepixz10fd", 5.7500f); make_specs.emplace_back("finepixz110", 6.1600f); make_specs.emplace_back("finepixz1", 5.7500f); make_specs.emplace_back("finepixz200fd", 6.1600f); make_specs.emplace_back("finepixz20fd", 6.1600f); make_specs.emplace_back("finepixz2", 5.7500f); make_specs.emplace_back("finepixz300", 6.1600f); make_specs.emplace_back("finepixz30", 6.1600f); make_specs.emplace_back("finepixz31", 6.1600f); make_specs.emplace_back("finepixz33wp", 6.1600f); make_specs.emplace_back("finepixz35", 6.1600f); make_specs.emplace_back("finepixz37", 6.1600f); make_specs.emplace_back("finepixz3", 5.7500f); make_specs.emplace_back("finepixz5fd", 5.7500f); make_specs.emplace_back("finepixz700exr", 6.4000f); make_specs.emplace_back("finepixz707exr", 6.4000f); make_specs.emplace_back("finepixz70", 6.1600f); make_specs.emplace_back("finepixz71", 6.1600f); make_specs.emplace_back("finepixz800exr", 6.4000f); make_specs.emplace_back("finepixz808exr", 6.4000f); make_specs.emplace_back("finepixz80", 6.1600f); make_specs.emplace_back("finepixz81", 6.1600f); make_specs.emplace_back("finepixz900exr", 6.4000f); make_specs.emplace_back("finepixz909exr", 6.4000f); make_specs.emplace_back("finepixz90", 6.1600f); make_specs.emplace_back("finepixz91", 6.1600f); make_specs.emplace_back("finepixz950exr", 6.4000f); make_specs.emplace_back("mx1200", 6.4000f); make_specs.emplace_back("mx1400", 5.3300f); make_specs.emplace_back("mx1500", 6.4000f); make_specs.emplace_back("mx1700", 6.4000f); make_specs.emplace_back("mx2700", 6.4000f); make_specs.emplace_back("mx2900zoom", 6.4000f); make_specs.emplace_back("mx500", 6.4000f); make_specs.emplace_back("mx600zoom", 6.4000f); make_specs.emplace_back("mx700", 6.4000f); make_specs.emplace_back("xa1", 23.6000f); make_specs.emplace_back("xa2", 23.6000f); make_specs.emplace_back("xe1", 23.6000f); make_specs.emplace_back("xe2", 23.6000f); make_specs.emplace_back("xm1", 23.6000f); make_specs.emplace_back("xpro1", 23.6000f); make_specs.emplace_back("xs1", 8.8000f); make_specs.emplace_back("xt10", 23.6000f); make_specs.emplace_back("xt1", 23.6000f); make_specs.emplace_back("x100s", 23.6000f); make_specs.emplace_back("x100t", 23.6000f); make_specs.emplace_back("x10", 8.8000f); make_specs.emplace_back("x20", 8.8000f); make_specs.emplace_back("x30", 8.8000f); make_specs.emplace_back("xf1", 8.8000f); make_specs.emplace_back("xq1", 8.8000f); make_specs.emplace_back("xq2", 8.8000f); } { auto& make_specs = specs["ge"]; make_specs.reserve(54); make_specs.emplace_back("a1030", 7.5300f); make_specs.emplace_back("a1035", 6.0800f); make_specs.emplace_back("a1050", 6.0800f); make_specs.emplace_back("a1235", 6.1600f); make_specs.emplace_back("a1250", 6.1600f); make_specs.emplace_back("a1255", 6.0800f); make_specs.emplace_back("a1455", 6.1600f); make_specs.emplace_back("a1456w", 6.1600f); make_specs.emplace_back("a730", 5.7500f); make_specs.emplace_back("a735", 5.7500f); make_specs.emplace_back("a830", 5.7500f); make_specs.emplace_back("a835", 5.7500f); make_specs.emplace_back("a950", 6.0800f); make_specs.emplace_back("c1033", 6.0800f); make_specs.emplace_back("c1233", 6.0800f); make_specs.emplace_back("c1433", 6.1600f); make_specs.emplace_back("c1440w", 6.1600f); make_specs.emplace_back("create", 6.1600f); make_specs.emplace_back("e1030", 7.5300f); make_specs.emplace_back("e1035", 7.5300f); make_specs.emplace_back("e1040", 7.5300f); make_specs.emplace_back("e1050tw", 6.0800f); make_specs.emplace_back("e1050", 6.0800f); make_specs.emplace_back("e1055w", 6.0800f); make_specs.emplace_back("e1235", 7.4400f); make_specs.emplace_back("e1240", 7.5300f); make_specs.emplace_back("e1250tw", 6.1600f); make_specs.emplace_back("e1255w", 6.1600f); make_specs.emplace_back("e1276w", 6.1600f); make_specs.emplace_back("e1410sw", 6.1600f); make_specs.emplace_back("e1450w", 6.1600f); make_specs.emplace_back("e1480w", 6.1600f); make_specs.emplace_back("e1486tw", 6.1600f); make_specs.emplace_back("e1680w", 6.0800f); make_specs.emplace_back("e840s", 5.7500f); make_specs.emplace_back("e850", 5.7500f); make_specs.emplace_back("g100", 6.1600f); make_specs.emplace_back("g2", 6.0300f); make_specs.emplace_back("g3wp", 6.1600f); make_specs.emplace_back("g3", 6.0800f); make_specs.emplace_back("g5wp", 6.1600f); make_specs.emplace_back("g1", 5.7500f); make_specs.emplace_back("j1050", 6.0800f); make_specs.emplace_back("j1250", 6.1600f); make_specs.emplace_back("j1455", 6.1600f); make_specs.emplace_back("j1456w", 6.1600f); make_specs.emplace_back("j1458w", 6.1600f); make_specs.emplace_back("j1470s", 6.0800f); make_specs.emplace_back("pj1", 6.1600f); make_specs.emplace_back("x1", 5.7500f); make_specs.emplace_back("x3", 6.0800f); make_specs.emplace_back("x500", 6.1600f); make_specs.emplace_back("x550", 6.1600f); make_specs.emplace_back("x600", 6.1600f); } { auto& make_specs = specs["hp"]; make_specs.reserve(69); make_specs.emplace_back("ca350", 6.1600f); make_specs.emplace_back("cb350", 6.1600f); make_specs.emplace_back("cw450t", 6.1600f); make_specs.emplace_back("cw450", 6.1600f); make_specs.emplace_back("photosmart120", 6.4000f); make_specs.emplace_back("photosmart318", 5.3300f); make_specs.emplace_back("photosmart320", 5.3300f); make_specs.emplace_back("photosmart435", 5.3300f); make_specs.emplace_back("photosmart612", 5.3300f); make_specs.emplace_back("photosmart620", 4.8000f); make_specs.emplace_back("photosmart635", 4.5000f); make_specs.emplace_back("photosmart715", 7.1100f); make_specs.emplace_back("photosmart720", 7.1100f); make_specs.emplace_back("photosmart733", 5.3300f); make_specs.emplace_back("photosmart735", 5.3300f); make_specs.emplace_back("photosmart812", 7.1100f); make_specs.emplace_back("photosmart850", 7.1100f); make_specs.emplace_back("photosmart935", 7.1100f); make_specs.emplace_back("photosmart945", 7.1100f); make_specs.emplace_back("photosmartc200", 8.8000f); make_specs.emplace_back("photosmartc20", 8.8000f); make_specs.emplace_back("photosmartc215", 5.3300f); make_specs.emplace_back("photosmartc30", 8.8000f); make_specs.emplace_back("photosmartc315", 5.3300f); make_specs.emplace_back("photosmartc500", 8.8000f); make_specs.emplace_back("photosmartc618", 5.3300f); make_specs.emplace_back("photosmartc912", 8.8000f); make_specs.emplace_back("photosmarte317", 5.7500f); make_specs.emplace_back("photosmarte327", 5.7500f); make_specs.emplace_back("photosmarte337", 5.7500f); make_specs.emplace_back("photosmarte427", 5.7500f); make_specs.emplace_back("photosmartm22", 5.7500f); make_specs.emplace_back("photosmartm23", 5.7500f); make_specs.emplace_back("photosmartm307", 5.3300f); make_specs.emplace_back("photosmartm407", 5.7500f); make_specs.emplace_back("photosmartm417", 5.7500f); make_specs.emplace_back("photosmartm425", 5.7500f); make_specs.emplace_back("photosmartm437", 5.7500f); make_specs.emplace_back("photosmartm447", 5.7500f); make_specs.emplace_back("photosmartm517", 5.7500f); make_specs.emplace_back("photosmartm525", 5.7500f); make_specs.emplace_back("photosmartm527", 5.7500f); make_specs.emplace_back("photosmartm537", 5.7500f); make_specs.emplace_back("photosmartm547", 5.7500f); make_specs.emplace_back("photosmartm627", 5.7500f); make_specs.emplace_back("photosmartm637", 5.7500f); make_specs.emplace_back("photosmartm737", 5.7500f); make_specs.emplace_back("photosmartmz67", 7.1100f); make_specs.emplace_back("photosmartr507", 5.7500f); make_specs.emplace_back("photosmartr607", 5.7500f); make_specs.emplace_back("photosmartr707", 7.1100f); make_specs.emplace_back("photosmartr717", 7.1100f); make_specs.emplace_back("photosmartr725", 5.7500f); make_specs.emplace_back("photosmartr727", 5.7500f); make_specs.emplace_back("photosmartr742", 5.7500f); make_specs.emplace_back("photosmartr817", 5.7500f); make_specs.emplace_back("photosmartr818", 5.7500f); make_specs.emplace_back("photosmartr827", 5.7500f); make_specs.emplace_back("photosmartr837", 5.7500f); make_specs.emplace_back("photosmartr847", 5.7500f); make_specs.emplace_back("photosmartr927", 7.1100f); make_specs.emplace_back("photosmartr937", 5.7500f); make_specs.emplace_back("photosmartr967", 7.1100f); make_specs.emplace_back("pw460t", 6.1600f); make_specs.emplace_back("pw550", 6.1600f); make_specs.emplace_back("r607bmw", 5.7500f); make_specs.emplace_back("r607harajuku", 5.7500f); make_specs.emplace_back("sb360", 6.1600f); make_specs.emplace_back("sw450", 6.1600f); } { auto& make_specs = specs["jenoptik"]; make_specs.reserve(48); make_specs.emplace_back("jd1300d", 6.4000f); make_specs.emplace_back("jd1300f", 6.4000f); make_specs.emplace_back("jd1500z3", 6.4000f); make_specs.emplace_back("jd2.1ff", 4.5000f); make_specs.emplace_back("jd2.1xz3", 4.5000f); make_specs.emplace_back("jd2100af", 5.3300f); make_specs.emplace_back("jd2100f", 5.3300f); make_specs.emplace_back("jd2100m", 5.3300f); make_specs.emplace_back("jd2100z3s", 5.3300f); make_specs.emplace_back("jd2300z3", 7.5300f); make_specs.emplace_back("jd3.1exclusiv", 6.4000f); make_specs.emplace_back("jd3.1z3mpeg4", 5.3300f); make_specs.emplace_back("jd3.3af", 5.3300f); make_specs.emplace_back("jd3.3xz3", 5.3300f); make_specs.emplace_back("jd3.3x4ie", 5.3300f); make_specs.emplace_back("jd3.3z10", 5.3300f); make_specs.emplace_back("jd3300z3s", 7.1100f); make_specs.emplace_back("jd3300z3", 7.1100f); make_specs.emplace_back("jd4.0lcd", 7.1100f); make_specs.emplace_back("jd4.1xz3", 5.7500f); make_specs.emplace_back("jd4.1z3mpeg4", 5.7500f); make_specs.emplace_back("jd4.1z8", 5.7500f); make_specs.emplace_back("jd4.1zoom", 5.7500f); make_specs.emplace_back("jd4100z3s", 7.1100f); make_specs.emplace_back("jd4100z3", 7.5300f); make_specs.emplace_back("jd4100zoom", 7.1100f); make_specs.emplace_back("jd4360z", 7.1100f); make_specs.emplace_back("jd4363z", 7.1100f); make_specs.emplace_back("jd5.0z3easyshot", 5.7500f); make_specs.emplace_back("jd5.2z3mpeg4", 7.1100f); make_specs.emplace_back("jd5.2z3", 7.1100f); make_specs.emplace_back("jd5.2zoom", 5.7500f); make_specs.emplace_back("jd5200z3", 7.1100f); make_specs.emplace_back("jd6.0z3exclusiv", 7.1100f); make_specs.emplace_back("jd6.0z3mpeg4", 7.1100f); make_specs.emplace_back("jd6.0z3", 7.1100f); make_specs.emplace_back("jd8.0exclusiv", 7.1100f); make_specs.emplace_back("jd8.0z3easyshot", 7.1100f); make_specs.emplace_back("jdc1.3lcd", 6.4000f); make_specs.emplace_back("jdc1.3sd", 6.4000f); make_specs.emplace_back("jdc1300", 6.4000f); make_specs.emplace_back("jdc2.1lcd", 6.4000f); make_specs.emplace_back("jdc3.0s", 6.4000f); make_specs.emplace_back("jdc3.1lcd", 6.4000f); make_specs.emplace_back("jdc3.1li", 6.4000f); make_specs.emplace_back("jdc3.1sl", 6.4000f); make_specs.emplace_back("jdc3.1z3", 6.4000f); make_specs.emplace_back("jdc5.0sl", 7.1100f); } { auto& make_specs = specs["jvc"]; make_specs.reserve(2); make_specs.emplace_back("gcqx3hd", 7.1100f); make_specs.emplace_back("gcqx5hd", 7.1100f); } { auto& make_specs = specs["kodak"]; make_specs.reserve(207); make_specs.emplace_back("dc200plus", 7.2700f); make_specs.emplace_back("dc200", 7.2700f); make_specs.emplace_back("dc210plus", 7.2700f); make_specs.emplace_back("dc215", 7.2700f); make_specs.emplace_back("dc220", 7.2700f); make_specs.emplace_back("dc240", 7.2700f); make_specs.emplace_back("dc260", 7.2700f); make_specs.emplace_back("dc265", 7.2700f); make_specs.emplace_back("dc280", 7.5300f); make_specs.emplace_back("dc290", 7.2700f); make_specs.emplace_back("dc3200", 7.5300f); make_specs.emplace_back("dc3400", 7.5300f); make_specs.emplace_back("dc3800", 7.5300f); make_specs.emplace_back("dc4800", 7.2700f); make_specs.emplace_back("dc5000", 7.2700f); make_specs.emplace_back("dcspro14n", 36.0000f); make_specs.emplace_back("dcsproslr/c", 36.0000f); make_specs.emplace_back("dcsproslr/n", 36.0000f); make_specs.emplace_back("dcs315", 27.6500f); make_specs.emplace_back("dcs330", 18.1000f); make_specs.emplace_back("dcs420", 14.0000f); make_specs.emplace_back("dcs460", 27.6500f); make_specs.emplace_back("dcs520", 27.6500f); make_specs.emplace_back("dcs560", 27.6500f); make_specs.emplace_back("dcs620x", 22.8000f); make_specs.emplace_back("dcs620", 27.6500f); make_specs.emplace_back("dcs660", 27.6500f); make_specs.emplace_back("dcs720x", 22.8000f); make_specs.emplace_back("dcs760", 27.6500f); make_specs.emplace_back("dx3215", 5.3300f); make_specs.emplace_back("dx3500", 6.4000f); make_specs.emplace_back("dx3600", 6.4000f); make_specs.emplace_back("dx3700", 7.1100f); make_specs.emplace_back("dx3900", 7.1100f); make_specs.emplace_back("dx4330", 5.7500f); make_specs.emplace_back("dx4530", 5.7500f); make_specs.emplace_back("dx4900", 5.7500f); make_specs.emplace_back("dx6340", 5.3300f); make_specs.emplace_back("dx6440", 5.7500f); make_specs.emplace_back("dx6490", 5.7500f); make_specs.emplace_back("dx7440", 5.7500f); make_specs.emplace_back("dx7590", 5.7500f); make_specs.emplace_back("dx7630", 7.1100f); make_specs.emplace_back("easysharec1013", 6.1600f); make_specs.emplace_back("easysharec135", 6.1600f); make_specs.emplace_back("easysharec140", 5.7500f); make_specs.emplace_back("easysharec142", 5.7500f); make_specs.emplace_back("easysharec143", 6.1600f); make_specs.emplace_back("easysharec1505", 6.1600f); make_specs.emplace_back("easysharec1530", 6.1600f); make_specs.emplace_back("easysharec1550", 6.1600f); make_specs.emplace_back("easysharec160", 5.7500f); make_specs.emplace_back("easysharec180", 6.1600f); make_specs.emplace_back("easysharec182", 6.1600f); make_specs.emplace_back("easysharec183", 6.1600f); make_specs.emplace_back("easysharec190", 6.1600f); make_specs.emplace_back("easysharec195", 6.1600f); make_specs.emplace_back("easysharec300", 7.1100f); make_specs.emplace_back("easysharec310", 7.1100f); make_specs.emplace_back("easysharec330", 7.1100f); make_specs.emplace_back("easysharec340", 7.1100f); make_specs.emplace_back("easysharec360", 7.1100f); make_specs.emplace_back("easysharec433", 5.7500f); make_specs.emplace_back("easysharec503", 5.7500f); make_specs.emplace_back("easysharec513", 5.7500f); make_specs.emplace_back("easysharec530", 5.7500f); make_specs.emplace_back("easysharec533", 5.7500f); make_specs.emplace_back("easysharec610", 5.7500f); make_specs.emplace_back("easysharec613", 5.7500f); make_specs.emplace_back("easysharec623", 5.7500f); make_specs.emplace_back("easysharec643", 5.7500f); make_specs.emplace_back("easysharec653", 5.7500f); make_specs.emplace_back("easysharec663", 5.7500f); make_specs.emplace_back("easysharec703", 5.7500f); make_specs.emplace_back("easysharec713", 5.7500f); make_specs.emplace_back("easysharec743", 5.7500f); make_specs.emplace_back("easysharec763", 5.7500f); make_specs.emplace_back("easysharec813", 5.7500f); make_specs.emplace_back("easysharec875", 7.1100f); make_specs.emplace_back("easysharec913", 5.7500f); make_specs.emplace_back("easysharecd1013", 6.1600f); make_specs.emplace_back("easysharecd703", 5.7500f); make_specs.emplace_back("easysharecd80", 6.1600f); make_specs.emplace_back("easysharecd82", 6.0800f); make_specs.emplace_back("easysharecd90", 6.0800f); make_specs.emplace_back("easysharecd93", 5.7500f); make_specs.emplace_back("easysharecx4200", 5.3300f); make_specs.emplace_back("easysharecx4230", 5.3300f); make_specs.emplace_back("easysharecx4300", 5.3300f); make_specs.emplace_back("easysharecx6200", 5.7500f); make_specs.emplace_back("easysharecx6230", 5.7500f); make_specs.emplace_back("easysharecx6330", 5.7500f); make_specs.emplace_back("easysharecx6445", 5.7500f); make_specs.emplace_back("easysharecx7220", 5.7500f); make_specs.emplace_back("easysharecx7300", 5.3300f); make_specs.emplace_back("easysharecx7330", 5.7500f); make_specs.emplace_back("easysharecx7430", 5.7500f); make_specs.emplace_back("easysharecx7525", 5.7500f); make_specs.emplace_back("easysharecx7530", 5.7500f); make_specs.emplace_back("easysharels745", 7.1100f); make_specs.emplace_back("easysharem1033", 6.1600f); make_specs.emplace_back("easysharem1063", 5.7500f); make_specs.emplace_back("easysharem1073is", 5.7500f); make_specs.emplace_back("easysharem1093is", 6.1600f); make_specs.emplace_back("easysharem215", 4.8000f); make_specs.emplace_back("easysharem320", 5.7500f); make_specs.emplace_back("easysharem340", 6.1600f); make_specs.emplace_back("easysharem341", 6.1600f); make_specs.emplace_back("easysharem380", 6.1600f); make_specs.emplace_back("easysharem381", 6.1600f); make_specs.emplace_back("easysharem420", 6.1600f); make_specs.emplace_back("easysharem522", 6.1600f); make_specs.emplace_back("easysharem530", 6.1600f); make_specs.emplace_back("easysharem531", 6.1600f); make_specs.emplace_back("easysharem532", 6.1600f); make_specs.emplace_back("easysharem5370", 6.1600f); make_specs.emplace_back("easysharem550", 6.1600f); make_specs.emplace_back("easysharem552", 6.1600f); make_specs.emplace_back("easysharem565", 6.1600f); make_specs.emplace_back("easysharem575", 6.1600f); make_specs.emplace_back("easysharem580", 6.1600f); make_specs.emplace_back("easysharem583", 6.0800f); make_specs.emplace_back("easysharem750", 6.1600f); make_specs.emplace_back("easysharem753", 5.7500f); make_specs.emplace_back("easysharem763", 5.7500f); make_specs.emplace_back("easysharem853", 5.7500f); make_specs.emplace_back("easysharem863", 5.7500f); make_specs.emplace_back("easysharem873", 5.7500f); make_specs.emplace_back("easysharem883", 5.7500f); make_specs.emplace_back("easysharem893is", 5.7500f); make_specs.emplace_back("easysharemaxz990", 6.0800f); make_specs.emplace_back("easysharemd1063", 6.0800f); make_specs.emplace_back("easysharemd30", 6.0800f); make_specs.emplace_back("easysharemd41", 6.0800f); make_specs.emplace_back("easysharemd81", 6.0800f); make_specs.emplace_back("easysharemd853", 5.7500f); make_specs.emplace_back("easysharemd863", 5.7500f); make_specs.emplace_back("easysharemini", 4.8000f); make_specs.emplace_back("easysharemx1063", 6.0800f); make_specs.emplace_back("easyshareone6mp", 5.7500f); make_specs.emplace_back("easyshareone", 5.7500f); make_specs.emplace_back("easysharep712", 5.7500f); make_specs.emplace_back("easysharep850", 5.7500f); make_specs.emplace_back("easysharep880", 7.1100f); make_specs.emplace_back("easysharesport", 6.1600f); make_specs.emplace_back("easysharetouchm577", 6.1600f); make_specs.emplace_back("easysharev1003", 7.1100f); make_specs.emplace_back("easysharev1073", 7.8500f); make_specs.emplace_back("easysharev1233", 7.4400f); make_specs.emplace_back("easysharev1253", 7.4400f); make_specs.emplace_back("easysharev1273", 7.4400f); make_specs.emplace_back("easysharev530", 5.7500f); make_specs.emplace_back("easysharev550", 5.7500f); make_specs.emplace_back("easysharev570", 5.7500f); make_specs.emplace_back("easysharev603", 5.7500f); make_specs.emplace_back("easysharev610", 5.7500f); make_specs.emplace_back("easysharev705", 5.7500f); make_specs.emplace_back("easysharev803", 7.1100f); make_specs.emplace_back("easysharez1012is", 6.0800f); make_specs.emplace_back("easysharez1015is", 6.0800f); make_specs.emplace_back("easysharez1085is", 7.8500f); make_specs.emplace_back("easysharez1275", 7.4400f); make_specs.emplace_back("easysharez1285", 7.4400f); make_specs.emplace_back("easysharez1485is", 7.4400f); make_specs.emplace_back("easysharez5010", 6.1600f); make_specs.emplace_back("easysharez5120", 6.0800f); make_specs.emplace_back("easysharez612", 5.7500f); make_specs.emplace_back("easysharez650", 5.7500f); make_specs.emplace_back("easysharez700", 5.7500f); make_specs.emplace_back("easysharez710", 5.7500f); make_specs.emplace_back("easysharez712is", 5.7500f); make_specs.emplace_back("easysharez730", 5.7500f); make_specs.emplace_back("easysharez740", 5.7500f); make_specs.emplace_back("easysharez7590", 5.7500f); make_specs.emplace_back("easysharez760", 7.1100f); make_specs.emplace_back("easysharez812is", 5.7500f); make_specs.emplace_back("easysharez8612is", 5.7500f); make_specs.emplace_back("easysharez885", 7.1100f); make_specs.emplace_back("easysharez915", 6.1600f); make_specs.emplace_back("easysharez950", 6.0800f); make_specs.emplace_back("easysharez980", 6.0800f); make_specs.emplace_back("easysharez981", 6.0800f); make_specs.emplace_back("easysharez990", 6.0800f); make_specs.emplace_back("easysharezd15", 6.1600f); make_specs.emplace_back("easysharezd710", 5.7500f); make_specs.emplace_back("easysharezd8612is", 5.7500f); make_specs.emplace_back("ls420", 7.1100f); make_specs.emplace_back("ls443", 5.3300f); make_specs.emplace_back("ls633", 5.7500f); make_specs.emplace_back("ls743", 7.1100f); make_specs.emplace_back("ls753", 7.1100f); make_specs.emplace_back("ls755", 5.7500f); make_specs.emplace_back("m590", 4.8000f); make_specs.emplace_back("mc3", 6.4000f); make_specs.emplace_back("pixproaz251", 6.1600f); make_specs.emplace_back("pixproaz361", 6.1600f); make_specs.emplace_back("pixproaz362", 6.1600f); make_specs.emplace_back("pixproaz501", 6.1600f); make_specs.emplace_back("pixproaz521", 6.1600f); make_specs.emplace_back("pixproaz522", 6.1600f); make_specs.emplace_back("pixproaz651", 6.1600f); make_specs.emplace_back("pixprofz151", 6.1600f); make_specs.emplace_back("pixprofz201", 6.1600f); make_specs.emplace_back("pixprofz41", 6.1600f); make_specs.emplace_back("pixprofz51", 6.1600f); make_specs.emplace_back("s1", 17.3000f); make_specs.emplace_back("slice", 6.1600f); } { auto& make_specs = specs["konica"]; make_specs.reserve(18); make_specs.emplace_back("dg2", 5.3300f); make_specs.emplace_back("dg3z", 5.3300f); make_specs.emplace_back("qm100", 4.8000f); make_specs.emplace_back("qm200", 6.4000f); make_specs.emplace_back("revioc2", 4.2300f); make_specs.emplace_back("reviokd200z", 5.3300f); make_specs.emplace_back("reviokd210z", 7.1100f); make_specs.emplace_back("reviokd220z", 4.5000f); make_specs.emplace_back("reviokd25", 7.1100f); make_specs.emplace_back("reviokd300z", 7.1100f); make_specs.emplace_back("reviokd310z", 7.1100f); make_specs.emplace_back("reviokd3300z", 5.7500f); make_specs.emplace_back("reviokd4000z", 7.1100f); make_specs.emplace_back("reviokd400z", 7.1100f); make_specs.emplace_back("reviokd410z", 7.1100f); make_specs.emplace_back("reviokd420z", 5.7500f); make_specs.emplace_back("reviokd500z", 7.1100f); make_specs.emplace_back("reviokd510z", 7.1100f); } { auto& make_specs = specs["konica-minolta"]; make_specs.reserve(24); make_specs.emplace_back("dg5w", 5.7500f); make_specs.emplace_back("dimagea200", 8.8000f); make_specs.emplace_back("dimagea2", 8.8000f); make_specs.emplace_back("dimagee40", 6.4000f); make_specs.emplace_back("dimagee500", 5.7500f); make_specs.emplace_back("dimagee50", 5.7500f); make_specs.emplace_back("dimageg530", 5.7500f); make_specs.emplace_back("dimageg600", 7.2700f); make_specs.emplace_back("dimagex1", 7.1100f); make_specs.emplace_back("dimagex31", 4.5000f); make_specs.emplace_back("dimagex50", 5.7500f); make_specs.emplace_back("dimagex60", 5.7500f); make_specs.emplace_back("dimagexg", 5.3300f); make_specs.emplace_back("dimagez10", 5.7500f); make_specs.emplace_back("dimagez20", 5.7500f); make_specs.emplace_back("dimagez2", 5.7500f); make_specs.emplace_back("dimagez3", 5.7500f); make_specs.emplace_back("dimagez5", 5.7500f); make_specs.emplace_back("dimagez6", 5.7500f); make_specs.emplace_back("dynax5d", 23.5000f); make_specs.emplace_back("dynax7d", 23.5000f); make_specs.emplace_back("eminid", 8.8000f); make_specs.emplace_back("eminim", 8.8000f); make_specs.emplace_back("emini", 8.8000f); } { auto& make_specs = specs["kyocera"]; make_specs.reserve(17); make_specs.emplace_back("finecam3300", 7.1100f); make_specs.emplace_back("finecaml30", 5.3300f); make_specs.emplace_back("finecaml3v", 5.3300f); make_specs.emplace_back("finecaml3", 5.3300f); make_specs.emplace_back("finecaml4v", 7.1100f); make_specs.emplace_back("finecaml4", 5.7500f); make_specs.emplace_back("finecamm400r", 5.3300f); make_specs.emplace_back("finecamm410r", 5.3300f); make_specs.emplace_back("finecams3l", 7.1100f); make_specs.emplace_back("finecams3r", 7.1100f); make_specs.emplace_back("finecams3x", 7.1100f); make_specs.emplace_back("finecams3", 7.1100f); make_specs.emplace_back("finecams4", 7.1100f); make_specs.emplace_back("finecams5r", 7.1100f); make_specs.emplace_back("finecams5", 7.1100f); make_specs.emplace_back("finecamsl300r", 5.3300f); make_specs.emplace_back("finecamsl400r", 5.3300f); } { auto& make_specs = specs["leica"]; make_specs.reserve(47); make_specs.emplace_back("clux1", 5.7500f); make_specs.emplace_back("clux2", 5.7500f); make_specs.emplace_back("clux3", 6.0800f); make_specs.emplace_back("c(typ112)", 7.5300f); make_specs.emplace_back("dlux2", 7.7600f); make_specs.emplace_back("dlux3", 7.7600f); make_specs.emplace_back("dlux4", 7.8500f); make_specs.emplace_back("dlux5", 7.8500f); make_specs.emplace_back("dlux6", 7.5300f); make_specs.emplace_back("dlux(typ109)", 17.3000f); make_specs.emplace_back("dlux", 5.7500f); make_specs.emplace_back("digilux1", 7.5300f); make_specs.emplace_back("digilux2", 8.8000f); make_specs.emplace_back("digilux3", 17.3000f); make_specs.emplace_back("digilux4.3", 7.5300f); make_specs.emplace_back("digiluxzoom", 6.4000f); make_specs.emplace_back("digilux", 6.4000f); make_specs.emplace_back("medition60", 36.0000f); make_specs.emplace_back("mmonochrom(typ246)", 35.8000f); make_specs.emplace_back("mmonochrom", 35.8000f); make_specs.emplace_back("mp", 36.0000f); make_specs.emplace_back("mtyp240", 36.0000f); make_specs.emplace_back("m8.2", 27.0000f); make_specs.emplace_back("m8", 27.0000f); make_specs.emplace_back("m9p", 35.8000f); make_specs.emplace_back("m9titanium", 35.8000f); make_specs.emplace_back("m9", 35.8000f); make_specs.emplace_back("metyp220", 35.8000f); make_specs.emplace_back("q(typ116)", 36.0000f); make_specs.emplace_back("se", 45.0000f); make_specs.emplace_back("s(type007)", 45.0000f); make_specs.emplace_back("s2", 45.0000f); make_specs.emplace_back("sl(typ601)", 36.0000f); make_specs.emplace_back("t(typ701)", 23.6000f); make_specs.emplace_back("vlux1", 7.1100f); make_specs.emplace_back("vlux20", 6.0800f); make_specs.emplace_back("vlux2", 6.0800f); make_specs.emplace_back("vlux30", 6.0800f); make_specs.emplace_back("vlux3", 6.0800f); make_specs.emplace_back("vlux40", 6.0800f); make_specs.emplace_back("vlux4", 6.1600f); make_specs.emplace_back("vlux(typ114)", 13.2000f); make_specs.emplace_back("x1", 23.6000f); make_specs.emplace_back("xe", 23.6000f); make_specs.emplace_back("x(typ113)", 23.6000f); make_specs.emplace_back("xvario", 23.6000f); make_specs.emplace_back("x2", 23.6000f); } { auto& make_specs = specs["minolta"]; make_specs.reserve(27); make_specs.emplace_back("dimage2300", 7.5300f); make_specs.emplace_back("dimage2330", 7.5300f); make_specs.emplace_back("dimage5", 7.1100f); make_specs.emplace_back("dimage7hi", 8.8000f); make_specs.emplace_back("dimage7i", 8.8000f); make_specs.emplace_back("dimage7", 8.8000f); make_specs.emplace_back("dimagea1", 8.8000f); make_specs.emplace_back("dimagee201", 7.5300f); make_specs.emplace_back("dimagee203", 5.3300f); make_specs.emplace_back("dimagee223", 5.3300f); make_specs.emplace_back("dimagee323", 5.3300f); make_specs.emplace_back("dimageex1500wide", 6.4000f); make_specs.emplace_back("dimageex1500zoom", 6.4000f); make_specs.emplace_back("dimagef100", 7.1100f); make_specs.emplace_back("dimagef200", 7.1100f); make_specs.emplace_back("dimagef300", 7.1100f); make_specs.emplace_back("dimageg400", 5.7500f); make_specs.emplace_back("dimageg500", 7.1100f); make_specs.emplace_back("dimages304", 7.1100f); make_specs.emplace_back("dimages404", 7.1100f); make_specs.emplace_back("dimages414", 7.1100f); make_specs.emplace_back("dimagex20", 4.5000f); make_specs.emplace_back("dimagexi", 5.3300f); make_specs.emplace_back("dimagext", 5.3300f); make_specs.emplace_back("dimagex", 5.3300f); make_specs.emplace_back("dimagez1", 5.3300f); make_specs.emplace_back("rd3000", 6.4000f); } { auto& make_specs = specs["minox"]; make_specs.reserve(48); make_specs.emplace_back("classicleicam32.1", 6.4000f); make_specs.emplace_back("classicleicam33mp", 6.4000f); make_specs.emplace_back("classicleicam34mp", 6.4000f); make_specs.emplace_back("classicleicam35mp", 6.4000f); make_specs.emplace_back("dc1011carat", 7.5300f); make_specs.emplace_back("dc1011", 7.5300f); make_specs.emplace_back("dc1022", 7.5300f); make_specs.emplace_back("dc1033", 5.7500f); make_specs.emplace_back("dc1044", 6.0800f); make_specs.emplace_back("dc1055", 6.0800f); make_specs.emplace_back("dc1211", 6.0800f); make_specs.emplace_back("dc1222", 6.1600f); make_specs.emplace_back("dc1233", 6.0800f); make_specs.emplace_back("dc1311", 5.3300f); make_specs.emplace_back("dc1422", 6.0800f); make_specs.emplace_back("dc2111", 5.3300f); make_specs.emplace_back("dc2122", 5.3300f); make_specs.emplace_back("dc2133", 4.5000f); make_specs.emplace_back("dc3311", 7.1100f); make_specs.emplace_back("dc4011", 7.1100f); make_specs.emplace_back("dc4211", 5.7500f); make_specs.emplace_back("dc5011", 5.7500f); make_specs.emplace_back("dc5211", 7.1100f); make_specs.emplace_back("dc5222", 5.7500f); make_specs.emplace_back("dc6011", 5.7500f); make_specs.emplace_back("dc6033wp", 5.7500f); make_specs.emplace_back("dc6211", 5.7500f); make_specs.emplace_back("dc6311", 7.1100f); make_specs.emplace_back("dc7011", 5.7500f); make_specs.emplace_back("dc7022", 5.7500f); make_specs.emplace_back("dc7411", 5.7500f); make_specs.emplace_back("dc8011", 5.7500f); make_specs.emplace_back("dc8022wp", 5.7500f); make_specs.emplace_back("dc8111", 7.1100f); make_specs.emplace_back("dc8122", 7.1100f); make_specs.emplace_back("dc9011wp", 6.1600f); make_specs.emplace_back("dcc14.0", 6.1600f); make_specs.emplace_back("dcc5.0whiteedition", 6.1600f); make_specs.emplace_back("dcc5.1", 6.1600f); make_specs.emplace_back("dccleicam35mpgold", 6.1600f); make_specs.emplace_back("dccrolleiflexaf5.0", 6.4000f); make_specs.emplace_back("dd1diamond", 6.4000f); make_specs.emplace_back("dd100", 6.4000f); make_specs.emplace_back("dd1", 6.4000f); make_specs.emplace_back("dd200", 6.4000f); make_specs.emplace_back("dm1", 6.4000f); make_specs.emplace_back("mobidv", 6.4000f); make_specs.emplace_back("rolleiflexminidigi", 6.4000f); } { auto& make_specs = specs["nikon"]; make_specs.reserve(266); make_specs.emplace_back("1aw1", 13.2000f); make_specs.emplace_back("1j1", 13.2000f); make_specs.emplace_back("1j2", 13.2000f); make_specs.emplace_back("1j3", 13.2000f); make_specs.emplace_back("1j4", 13.2000f); make_specs.emplace_back("1j5", 13.2000f); make_specs.emplace_back("1s1", 13.2000f); make_specs.emplace_back("1s2", 13.2000f); make_specs.emplace_back("1v1", 13.2000f); make_specs.emplace_back("1v2", 13.2000f); make_specs.emplace_back("1v3", 13.2000f); make_specs.emplace_back("coolpix100", 4.8000f); make_specs.emplace_back("coolpix2000", 5.3300f); make_specs.emplace_back("coolpix2100", 4.5000f); make_specs.emplace_back("coolpix2200", 4.5000f); make_specs.emplace_back("coolpix2500", 5.3300f); make_specs.emplace_back("coolpix300", 4.8000f); make_specs.emplace_back("coolpix3100", 5.3300f); make_specs.emplace_back("coolpix3200", 5.3300f); make_specs.emplace_back("coolpix3500", 5.3300f); make_specs.emplace_back("coolpix3700", 5.3300f); make_specs.emplace_back("coolpix4100", 5.7500f); make_specs.emplace_back("coolpix4200", 7.1100f); make_specs.emplace_back("coolpix4300", 7.1100f); make_specs.emplace_back("coolpix4500", 7.1100f); make_specs.emplace_back("coolpix4600", 5.7500f); make_specs.emplace_back("coolpix4800", 5.7500f); make_specs.emplace_back("coolpix5000", 8.8000f); make_specs.emplace_back("coolpix5200", 7.1100f); make_specs.emplace_back("coolpix5400", 7.1100f); make_specs.emplace_back("coolpix5600", 5.7500f); make_specs.emplace_back("coolpix5700", 8.8000f); make_specs.emplace_back("coolpix5900", 7.1100f); make_specs.emplace_back("coolpix600", 5.3300f); make_specs.emplace_back("coolpix700", 6.4000f); make_specs.emplace_back("coolpix7600", 7.1100f); make_specs.emplace_back("coolpix775", 5.3300f); make_specs.emplace_back("coolpix7900", 7.1100f); make_specs.emplace_back("coolpix800", 6.4000f); make_specs.emplace_back("coolpix8400", 8.8000f); make_specs.emplace_back("coolpix8700", 8.8000f); make_specs.emplace_back("coolpix8800", 8.8000f); make_specs.emplace_back("coolpix880", 7.1100f); make_specs.emplace_back("coolpix885", 7.1100f); make_specs.emplace_back("coolpix900s", 5.3300f); make_specs.emplace_back("coolpix900", 5.3300f); make_specs.emplace_back("coolpix910", 6.4000f); make_specs.emplace_back("coolpix950", 6.4000f); make_specs.emplace_back("coolpix990", 7.1100f); make_specs.emplace_back("coolpix995", 7.1100f); make_specs.emplace_back("coolpixaw100s", 6.1600f); make_specs.emplace_back("coolpixaw100", 6.1600f); make_specs.emplace_back("coolpixaw110", 6.1600f); make_specs.emplace_back("coolpixaw120", 6.1600f); make_specs.emplace_back("coolpixaw130", 6.1600f); make_specs.emplace_back("coolpixa", 23.6000f); make_specs.emplace_back("coolpixl100", 6.0800f); make_specs.emplace_back("coolpixl101", 5.7500f); make_specs.emplace_back("coolpixl10", 5.7500f); make_specs.emplace_back("coolpixl110", 6.1600f); make_specs.emplace_back("coolpixl11", 5.7500f); make_specs.emplace_back("coolpixl120", 6.1600f); make_specs.emplace_back("coolpixl12", 5.7500f); make_specs.emplace_back("coolpixl14", 5.7500f); make_specs.emplace_back("coolpixl15", 5.7500f); make_specs.emplace_back("coolpixl16", 5.7500f); make_specs.emplace_back("coolpixl18", 5.7500f); make_specs.emplace_back("coolpixl19", 5.7500f); make_specs.emplace_back("coolpixl1", 5.7500f); make_specs.emplace_back("coolpixl20", 6.0800f); make_specs.emplace_back("coolpixl21", 6.0800f); make_specs.emplace_back("coolpixl22", 6.1600f); make_specs.emplace_back("coolpixl23", 4.9600f); make_specs.emplace_back("coolpixl24", 6.1600f); make_specs.emplace_back("coolpixl25", 4.8000f); make_specs.emplace_back("coolpixl26", 6.1600f); make_specs.emplace_back("coolpixl27", 6.1600f); make_specs.emplace_back("coolpixl28", 6.1600f); make_specs.emplace_back("coolpixl29", 6.1600f); make_specs.emplace_back("coolpixl30", 6.1600f); make_specs.emplace_back("coolpixl310", 6.1600f); make_specs.emplace_back("coolpixl31", 6.1600f); make_specs.emplace_back("coolpixl320", 6.1600f); make_specs.emplace_back("coolpixl32", 6.1600f); make_specs.emplace_back("coolpixl330", 6.1600f); make_specs.emplace_back("coolpixl5", 5.7500f); make_specs.emplace_back("coolpixl610", 6.1600f); make_specs.emplace_back("coolpixl620", 6.1600f); make_specs.emplace_back("coolpixl6", 5.7500f); make_specs.emplace_back("coolpixl810", 6.1600f); make_specs.emplace_back("coolpixl820", 6.1600f); make_specs.emplace_back("coolpixl830", 6.1600f); make_specs.emplace_back("coolpixl840", 6.1600f); make_specs.emplace_back("coolpixp100", 6.1600f); make_specs.emplace_back("coolpixp1", 7.1100f); make_specs.emplace_back("coolpixp2", 7.1100f); make_specs.emplace_back("coolpixp300", 6.1600f); make_specs.emplace_back("coolpixp310", 6.1600f); make_specs.emplace_back("coolpixp330", 7.5300f); make_specs.emplace_back("coolpixp340", 7.5300f); make_specs.emplace_back("coolpixp3", 7.1100f); make_specs.emplace_back("coolpixp4", 7.1100f); make_specs.emplace_back("coolpixp5000", 7.1100f); make_specs.emplace_back("coolpixp500", 6.1600f); make_specs.emplace_back("coolpixp50", 5.7500f); make_specs.emplace_back("coolpixp5100", 7.4400f); make_specs.emplace_back("coolpixp510", 6.1600f); make_specs.emplace_back("coolpixp520", 6.1600f); make_specs.emplace_back("coolpixp530", 6.1600f); make_specs.emplace_back("coolpixp6000", 7.4400f); make_specs.emplace_back("coolpixp600", 6.1600f); make_specs.emplace_back("coolpixp60", 5.7500f); make_specs.emplace_back("coolpixp610", 6.1600f); make_specs.emplace_back("coolpixp7000", 7.5300f); make_specs.emplace_back("coolpixp7100", 7.5300f); make_specs.emplace_back("coolpixp7700", 7.5300f); make_specs.emplace_back("coolpixp7800", 7.5300f); make_specs.emplace_back("coolpixp80", 6.0800f); make_specs.emplace_back("coolpixp900", 6.1600f); make_specs.emplace_back("coolpixp90", 6.0800f); make_specs.emplace_back("coolpixs01", 4.9600f); make_specs.emplace_back("coolpixs02", 4.8000f); make_specs.emplace_back("coolpixs1000pj", 6.1600f); make_specs.emplace_back("coolpixs100", 6.1600f); make_specs.emplace_back("coolpixs10", 5.7500f); make_specs.emplace_back("coolpixs1100pj", 6.1600f); make_specs.emplace_back("coolpixs1200pj", 6.1600f); make_specs.emplace_back("coolpixs1", 5.7500f); make_specs.emplace_back("coolpixs200", 5.7500f); make_specs.emplace_back("coolpixs210", 5.7500f); make_specs.emplace_back("coolpixs220", 6.0800f); make_specs.emplace_back("coolpixs225", 6.0800f); make_specs.emplace_back("coolpixs230", 6.0800f); make_specs.emplace_back("coolpixs2500", 6.1600f); make_specs.emplace_back("coolpixs2600", 6.1600f); make_specs.emplace_back("coolpixs2700", 6.1600f); make_specs.emplace_back("coolpixs2750", 6.1600f); make_specs.emplace_back("coolpixs2800", 6.1600f); make_specs.emplace_back("coolpixs2900", 6.1600f); make_specs.emplace_back("coolpixs2", 5.7500f); make_specs.emplace_back("coolpixs3000", 6.1600f); make_specs.emplace_back("coolpixs30", 4.8000f); make_specs.emplace_back("coolpixs3100", 6.1600f); make_specs.emplace_back("coolpixs31", 4.9600f); make_specs.emplace_back("coolpixs3200", 6.1600f); make_specs.emplace_back("coolpixs32", 4.8000f); make_specs.emplace_back("coolpixs3300", 6.1600f); make_specs.emplace_back("coolpixs33", 4.8000f); make_specs.emplace_back("coolpixs3400", 6.1600f); make_specs.emplace_back("coolpixs3500", 6.1600f); make_specs.emplace_back("coolpixs3600", 6.1600f); make_specs.emplace_back("coolpixs3700", 6.1600f); make_specs.emplace_back("coolpixs3", 5.7500f); make_specs.emplace_back("coolpixs4000", 6.1600f); make_specs.emplace_back("coolpixs4100", 6.1600f); make_specs.emplace_back("coolpixs4150", 6.1600f); make_specs.emplace_back("coolpixs4200", 6.1600f); make_specs.emplace_back("coolpixs4300", 6.1600f); make_specs.emplace_back("coolpixs4400", 6.1600f); make_specs.emplace_back("coolpixs4", 5.7500f); make_specs.emplace_back("coolpixs500", 5.7500f); make_specs.emplace_back("coolpixs50c", 5.7500f); make_specs.emplace_back("coolpixs50", 5.7500f); make_specs.emplace_back("coolpixs5100", 6.1600f); make_specs.emplace_back("coolpixs510", 5.7500f); make_specs.emplace_back("coolpixs51c", 5.7500f); make_specs.emplace_back("coolpixs51", 5.7500f); make_specs.emplace_back("coolpixs5200", 6.1600f); make_specs.emplace_back("coolpixs520", 5.7500f); make_specs.emplace_back("coolpixs52c", 5.7500f); make_specs.emplace_back("coolpixs52", 5.7500f); make_specs.emplace_back("coolpixs5300", 6.1600f); make_specs.emplace_back("coolpixs550", 6.1600f); make_specs.emplace_back("coolpixs560", 6.0800f); make_specs.emplace_back("coolpixs570", 6.1600f); make_specs.emplace_back("coolpixs5", 5.7500f); make_specs.emplace_back("coolpixs6000", 6.1600f); make_specs.emplace_back("coolpixs600", 6.0800f); make_specs.emplace_back("coolpixs60", 6.1600f); make_specs.emplace_back("coolpixs6100", 6.1600f); make_specs.emplace_back("coolpixs610c", 6.0800f); make_specs.emplace_back("coolpixs610", 6.0800f); make_specs.emplace_back("coolpixs6150", 6.1600f); make_specs.emplace_back("coolpixs6200", 6.1600f); make_specs.emplace_back("coolpixs620", 6.0800f); make_specs.emplace_back("coolpixs6300", 6.1600f); make_specs.emplace_back("coolpixs630", 6.0800f); make_specs.emplace_back("coolpixs6400", 6.1600f); make_specs.emplace_back("coolpixs640", 6.0800f); make_specs.emplace_back("coolpixs6500", 6.1600f); make_specs.emplace_back("coolpixs6600", 6.1600f); make_specs.emplace_back("coolpixs6700", 6.1600f); make_specs.emplace_back("coolpixs6800", 6.1600f); make_specs.emplace_back("coolpixs6900", 6.1600f); make_specs.emplace_back("coolpixs6", 5.7500f); make_specs.emplace_back("coolpixs7000", 6.1600f); make_specs.emplace_back("coolpixs700", 7.4400f); make_specs.emplace_back("coolpixs70", 6.1600f); make_specs.emplace_back("coolpixs710", 7.4400f); make_specs.emplace_back("coolpixs7c", 5.7500f); make_specs.emplace_back("coolpixs8000", 6.1600f); make_specs.emplace_back("coolpixs800c", 6.1600f); make_specs.emplace_back("coolpixs80", 6.1600f); make_specs.emplace_back("coolpixs8100", 6.1600f); make_specs.emplace_back("coolpixs810c", 6.1600f); make_specs.emplace_back("coolpixs8200", 6.1600f); make_specs.emplace_back("coolpixs9050", 6.1600f); make_specs.emplace_back("coolpixs9100", 6.1600f); make_specs.emplace_back("coolpixs9200", 6.1600f); make_specs.emplace_back("coolpixs9300", 6.1600f); make_specs.emplace_back("coolpixs9400", 6.1600f); make_specs.emplace_back("coolpixs9500", 6.1600f); make_specs.emplace_back("coolpixs9600", 6.1600f); make_specs.emplace_back("coolpixs9700", 6.1600f); make_specs.emplace_back("coolpixs9900", 6.1600f); make_specs.emplace_back("coolpixs9", 5.7500f); make_specs.emplace_back("coolpixsq", 5.3300f); make_specs.emplace_back("d100", 23.7000f); make_specs.emplace_back("d1h", 23.7000f); make_specs.emplace_back("d1x", 23.7000f); make_specs.emplace_back("d1", 23.7000f); make_specs.emplace_back("d200", 23.6000f); make_specs.emplace_back("d2hs", 23.2000f); make_specs.emplace_back("d2h", 23.7000f); make_specs.emplace_back("d2xs", 23.7000f); make_specs.emplace_back("d2x", 23.7000f); make_specs.emplace_back("d3000", 23.6000f); make_specs.emplace_back("d300s", 23.6000f); make_specs.emplace_back("d300", 23.6000f); make_specs.emplace_back("d3100", 23.1000f); make_specs.emplace_back("d3200", 23.2000f); make_specs.emplace_back("d3300", 23.5000f); make_specs.emplace_back("d3s", 36.0000f); make_specs.emplace_back("d3x", 35.9000f); make_specs.emplace_back("d3", 36.0000f); make_specs.emplace_back("d40x", 23.6000f); make_specs.emplace_back("d40", 23.7000f); make_specs.emplace_back("d4s", 36.0000f); make_specs.emplace_back("d4", 36.0000f); make_specs.emplace_back("d5000", 23.6000f); make_specs.emplace_back("d50", 23.7000f); make_specs.emplace_back("d5100", 23.6000f); make_specs.emplace_back("d5200", 23.5000f); make_specs.emplace_back("d5300", 23.5000f); make_specs.emplace_back("d5500", 23.5000f); make_specs.emplace_back("d600", 35.9000f); make_specs.emplace_back("d60", 23.6000f); make_specs.emplace_back("d610", 35.9000f); make_specs.emplace_back("d7000", 23.6000f); make_specs.emplace_back("d700", 36.0000f); make_specs.emplace_back("d70s", 23.7000f); make_specs.emplace_back("d70", 23.7000f); make_specs.emplace_back("d7100", 23.5000f); make_specs.emplace_back("d7200", 23.5000f); make_specs.emplace_back("d750", 35.9000f); make_specs.emplace_back("d800e", 35.9000f); make_specs.emplace_back("d800", 35.9000f); make_specs.emplace_back("d80", 23.6000f); make_specs.emplace_back("d810", 35.9000f); make_specs.emplace_back("d90", 23.6000f); make_specs.emplace_back("df", 36.0000f); make_specs.emplace_back("e2ns", 8.8000f); make_specs.emplace_back("e2n", 8.8000f); make_specs.emplace_back("e2s", 8.8000f); make_specs.emplace_back("e3s", 8.8000f); make_specs.emplace_back("e3", 8.8000f); } { auto& make_specs = specs["nokia"]; make_specs.reserve(2); make_specs.emplace_back("808pureview", 10.8200f); make_specs.emplace_back("lumia1020", 8.6400f); } { auto& make_specs = specs["olympus"]; make_specs.reserve(314); make_specs.emplace_back("aira01", 17.3000f); make_specs.emplace_back("az1ferrari2004", 5.3300f); make_specs.emplace_back("az1", 5.3300f); make_specs.emplace_back("az2zoom", 5.3300f); make_specs.emplace_back("c1zoom", 4.5000f); make_specs.emplace_back("c1000l", 6.4000f); make_specs.emplace_back("c100", 4.5000f); make_specs.emplace_back("c120", 4.5000f); make_specs.emplace_back("c1400l", 8.8000f); make_specs.emplace_back("c1400xl", 8.8000f); make_specs.emplace_back("c150", 4.5000f); make_specs.emplace_back("c160", 5.3300f); make_specs.emplace_back("c170", 5.7500f); make_specs.emplace_back("c180", 5.7500f); make_specs.emplace_back("c1", 4.5000f); make_specs.emplace_back("c200zoom", 5.3300f); make_specs.emplace_back("c2000zoom", 6.4000f); make_specs.emplace_back("c2020zoom", 6.4000f); make_specs.emplace_back("c2040zoom", 6.4000f); make_specs.emplace_back("c2100uz", 6.4000f); make_specs.emplace_back("c21", 6.4000f); make_specs.emplace_back("c220zoom", 4.5000f); make_specs.emplace_back("c2500l", 8.8000f); make_specs.emplace_back("c2", 5.3300f); make_specs.emplace_back("c300zoom", 5.7500f); make_specs.emplace_back("c3000zoom", 7.1100f); make_specs.emplace_back("c3020zoom", 7.1100f); make_specs.emplace_back("c3030zoom", 7.1100f); make_specs.emplace_back("c3040zoom", 7.1100f); make_specs.emplace_back("c310zoom", 5.3300f); make_specs.emplace_back("c315zoom", 5.7500f); make_specs.emplace_back("c350zoom", 5.7500f); make_specs.emplace_back("c360zoom", 5.7500f); make_specs.emplace_back("c370zoom", 5.3300f); make_specs.emplace_back("c40zoom", 7.1100f); make_specs.emplace_back("c4000zoom", 7.1100f); make_specs.emplace_back("c4040zoom", 7.1100f); make_specs.emplace_back("c450zoom", 5.7500f); make_specs.emplace_back("c460zoomdelsol", 5.7500f); make_specs.emplace_back("c470zoom", 5.7500f); make_specs.emplace_back("c480zoom", 5.7500f); make_specs.emplace_back("c50zoom", 7.1100f); make_specs.emplace_back("c5000zoom", 7.2700f); make_specs.emplace_back("c5050zoom", 7.1100f); make_specs.emplace_back("c5060widezoom", 7.1100f); make_specs.emplace_back("c55zoom", 7.1100f); make_specs.emplace_back("c5500sportzoom", 7.1100f); make_specs.emplace_back("c60zoom", 7.2700f); make_specs.emplace_back("c70zoom", 7.1100f); make_specs.emplace_back("c700uz", 5.3300f); make_specs.emplace_back("c7000zoom", 7.1100f); make_specs.emplace_back("c7070widezoom", 7.1100f); make_specs.emplace_back("c720uz", 5.7500f); make_specs.emplace_back("c730uz", 5.3300f); make_specs.emplace_back("c740uz", 5.7500f); make_specs.emplace_back("c750uz", 5.3300f); make_specs.emplace_back("c760uz", 5.3300f); make_specs.emplace_back("c765uz", 5.7500f); make_specs.emplace_back("c770uz", 5.7500f); make_specs.emplace_back("c8080widezoom", 8.8000f); make_specs.emplace_back("c820l", 4.8000f); make_specs.emplace_back("c840l", 5.3300f); make_specs.emplace_back("c860l", 5.3300f); make_specs.emplace_back("c900zoom", 5.3300f); make_specs.emplace_back("c920zoom", 5.3300f); make_specs.emplace_back("c960zoom", 5.3300f); make_specs.emplace_back("c990zoom", 5.3300f); make_specs.emplace_back("d150z", 4.5000f); make_specs.emplace_back("d200l", 8.8000f); make_specs.emplace_back("d300l", 8.8000f); make_specs.emplace_back("d340l", 8.8000f); make_specs.emplace_back("d340r", 6.4000f); make_specs.emplace_back("d360l", 7.1100f); make_specs.emplace_back("d370", 4.5000f); make_specs.emplace_back("d380", 4.5000f); make_specs.emplace_back("d390", 4.5000f); make_specs.emplace_back("d395", 5.3300f); make_specs.emplace_back("d40zoom", 7.1100f); make_specs.emplace_back("d400zoom", 6.4000f); make_specs.emplace_back("d425", 5.7500f); make_specs.emplace_back("d435", 5.7500f); make_specs.emplace_back("d450zoom", 5.3300f); make_specs.emplace_back("d460zoom", 7.1100f); make_specs.emplace_back("d490zoom", 5.3300f); make_specs.emplace_back("d500l", 8.8000f); make_specs.emplace_back("d510zoom", 5.3300f); make_specs.emplace_back("d520zoom", 4.5000f); make_specs.emplace_back("d535zoom", 5.3300f); make_specs.emplace_back("d540zoom", 5.3300f); make_specs.emplace_back("d545zoom", 5.7500f); make_specs.emplace_back("d560zoom", 5.7500f); make_specs.emplace_back("d580zoom", 5.3300f); make_specs.emplace_back("d595zoom", 5.7500f); make_specs.emplace_back("d600l", 8.8000f); make_specs.emplace_back("d620l", 8.8000f); make_specs.emplace_back("d630zoom", 5.7500f); make_specs.emplace_back("e100rs", 6.4000f); make_specs.emplace_back("e10", 8.8000f); make_specs.emplace_back("e1", 17.3000f); make_specs.emplace_back("e20", 8.8000f); make_specs.emplace_back("e30", 17.3000f); make_specs.emplace_back("e3", 17.3000f); make_specs.emplace_back("e400", 17.3000f); make_specs.emplace_back("e410/evolte410", 17.3000f); make_specs.emplace_back("e420", 17.3000f); make_specs.emplace_back("e450", 17.3000f); make_specs.emplace_back("e510/evolte510", 17.3000f); make_specs.emplace_back("e520", 17.3000f); make_specs.emplace_back("e5", 17.3000f); make_specs.emplace_back("e600", 17.3000f); make_specs.emplace_back("e620", 17.3000f); make_specs.emplace_back("e300/evolte300", 17.3000f); make_specs.emplace_back("e500/evolte500", 17.3000f); make_specs.emplace_back("fe100", 5.7500f); make_specs.emplace_back("fe110", 5.7500f); make_specs.emplace_back("fe115", 5.7500f); make_specs.emplace_back("fe120", 5.7500f); make_specs.emplace_back("fe130", 5.7500f); make_specs.emplace_back("fe140", 5.7500f); make_specs.emplace_back("fe150", 5.7500f); make_specs.emplace_back("fe160", 5.7500f); make_specs.emplace_back("fe170", 5.7500f); make_specs.emplace_back("fe180", 5.7500f); make_specs.emplace_back("fe190", 5.7500f); make_specs.emplace_back("fe200", 5.7500f); make_specs.emplace_back("fe20", 6.0300f); make_specs.emplace_back("fe210", 5.7500f); make_specs.emplace_back("fe220", 5.7500f); make_specs.emplace_back("fe230", 5.7500f); make_specs.emplace_back("fe240", 5.7500f); make_specs.emplace_back("fe250", 7.1100f); make_specs.emplace_back("fe25", 6.0800f); make_specs.emplace_back("fe26", 6.0800f); make_specs.emplace_back("fe270", 5.7500f); make_specs.emplace_back("fe280", 6.1600f); make_specs.emplace_back("fe290", 5.7500f); make_specs.emplace_back("fe3000", 6.0800f); make_specs.emplace_back("fe300", 7.4400f); make_specs.emplace_back("fe3010", 6.0800f); make_specs.emplace_back("fe310", 5.7500f); make_specs.emplace_back("fe340", 6.0300f); make_specs.emplace_back("fe350", 5.7500f); make_specs.emplace_back("fe360", 6.0300f); make_specs.emplace_back("fe370", 6.0300f); make_specs.emplace_back("fe4000", 6.1600f); make_specs.emplace_back("fe4010", 6.1600f); make_specs.emplace_back("fe4020", 6.1600f); make_specs.emplace_back("fe4030", 6.0800f); make_specs.emplace_back("fe4040", 6.0800f); make_specs.emplace_back("fe4050", 6.1600f); make_specs.emplace_back("fe45", 6.0800f); make_specs.emplace_back("fe47", 6.0800f); make_specs.emplace_back("fe48", 6.1600f); make_specs.emplace_back("fe5000", 6.0800f); make_specs.emplace_back("fe5010", 6.0800f); make_specs.emplace_back("fe5020", 6.1600f); make_specs.emplace_back("fe5030", 6.0800f); make_specs.emplace_back("fe5035", 6.0800f); make_specs.emplace_back("fe5040", 6.1600f); make_specs.emplace_back("fe5050", 6.1600f); make_specs.emplace_back("ir300", 5.7500f); make_specs.emplace_back("ir500", 5.3300f); make_specs.emplace_back("m:robemr500i", 3.2000f); make_specs.emplace_back("mju1060", 6.0800f); make_specs.emplace_back("mju400digitalferrari", 5.7500f); make_specs.emplace_back("mju5000", 6.0800f); make_specs.emplace_back("mju7050", 6.1600f); make_specs.emplace_back("mju800black", 7.1100f); make_specs.emplace_back("mjuminidigitals", 5.7500f); make_specs.emplace_back("mjuminidigital", 5.7500f); make_specs.emplace_back("omdem10ii", 17.3000f); make_specs.emplace_back("omdem10", 17.3000f); make_specs.emplace_back("omdem1", 17.3000f); make_specs.emplace_back("omdem5markii", 17.3000f); make_specs.emplace_back("omdem5", 17.3000f); make_specs.emplace_back("penep1", 17.3000f); make_specs.emplace_back("penep2", 17.3000f); make_specs.emplace_back("penep3", 17.3000f); make_specs.emplace_back("penep5", 17.3000f); make_specs.emplace_back("penepl1s", 17.3000f); make_specs.emplace_back("penepl1", 17.3000f); make_specs.emplace_back("penepl2", 17.3000f); make_specs.emplace_back("penepl3", 17.3000f); make_specs.emplace_back("penepl5", 17.3000f); make_specs.emplace_back("penepl6", 17.3000f); make_specs.emplace_back("penepl7", 17.3000f); make_specs.emplace_back("penepm1", 17.3000f); make_specs.emplace_back("penepm2", 17.3000f); make_specs.emplace_back("sh21", 6.1600f); make_specs.emplace_back("sh25mr", 6.1600f); make_specs.emplace_back("sh50ihs", 6.1600f); make_specs.emplace_back("sp100", 6.1600f); make_specs.emplace_back("sp310", 7.1100f); make_specs.emplace_back("sp320", 7.1100f); make_specs.emplace_back("sp350", 7.1100f); make_specs.emplace_back("sp500uz", 5.7500f); make_specs.emplace_back("sp510uz", 5.7500f); make_specs.emplace_back("sp550uz", 5.7500f); make_specs.emplace_back("sp560uz", 6.1600f); make_specs.emplace_back("sp565uz", 6.0800f); make_specs.emplace_back("sp570uz", 6.0800f); make_specs.emplace_back("sp590uz", 6.0800f); make_specs.emplace_back("sp600uz", 6.0800f); make_specs.emplace_back("sp610uz", 6.1600f); make_specs.emplace_back("sp620uz", 6.1600f); make_specs.emplace_back("sp700", 5.7500f); make_specs.emplace_back("sp720uz", 6.1600f); make_specs.emplace_back("sp800uz", 6.0800f); make_specs.emplace_back("sp810uz", 6.1600f); make_specs.emplace_back("stylus1000", 7.1100f); make_specs.emplace_back("stylus1010", 6.0800f); make_specs.emplace_back("stylus1020", 6.0800f); make_specs.emplace_back("stylus1030sw", 6.0800f); make_specs.emplace_back("stylus1040", 6.0800f); make_specs.emplace_back("stylus1050sw", 6.0800f); make_specs.emplace_back("stylus1200", 7.4400f); make_specs.emplace_back("stylus1s", 7.5300f); make_specs.emplace_back("stylus1", 7.5300f); make_specs.emplace_back("stylus300", 5.7500f); make_specs.emplace_back("stylus400", 5.7500f); make_specs.emplace_back("stylus410", 5.7500f); make_specs.emplace_back("stylus500", 5.7500f); make_specs.emplace_back("stylus5010", 6.0800f); make_specs.emplace_back("stylus550wp", 6.0800f); make_specs.emplace_back("stylus600", 5.7500f); make_specs.emplace_back("stylus7000", 6.0800f); make_specs.emplace_back("stylus700", 6.1600f); make_specs.emplace_back("stylus7010", 6.0800f); make_specs.emplace_back("stylus7030", 6.0800f); make_specs.emplace_back("stylus7040", 6.0800f); make_specs.emplace_back("stylus720sw", 6.1600f); make_specs.emplace_back("stylus725sw", 6.1600f); make_specs.emplace_back("stylus730", 6.1600f); make_specs.emplace_back("stylus740", 6.1600f); make_specs.emplace_back("stylus750", 6.1600f); make_specs.emplace_back("stylus760", 6.1600f); make_specs.emplace_back("stylus770sw", 6.1600f); make_specs.emplace_back("stylus780", 6.1600f); make_specs.emplace_back("stylus790sw", 6.1600f); make_specs.emplace_back("stylus800", 7.1100f); make_specs.emplace_back("stylus810", 7.1100f); make_specs.emplace_back("stylus820", 6.1600f); make_specs.emplace_back("stylus830", 6.1600f); make_specs.emplace_back("stylus840", 6.0300f); make_specs.emplace_back("stylus850sw", 6.0300f); make_specs.emplace_back("stylus9000", 6.0800f); make_specs.emplace_back("stylus9010", 6.0800f); make_specs.emplace_back("stylussh1", 6.1600f); make_specs.emplace_back("stylussh2", 6.1600f); make_specs.emplace_back("stylussp820uz", 6.1600f); make_specs.emplace_back("stylustough3000", 6.0800f); make_specs.emplace_back("stylustough6000", 6.1600f); make_specs.emplace_back("stylustough6010", 6.1600f); make_specs.emplace_back("stylustough6020", 6.0800f); make_specs.emplace_back("stylustough8000", 6.0800f); make_specs.emplace_back("stylustough8010", 6.0800f); make_specs.emplace_back("stylusverves", 5.7500f); make_specs.emplace_back("stylusverve", 5.7500f); make_specs.emplace_back("stylusxz10", 6.1600f); make_specs.emplace_back("sz10", 6.1600f); make_specs.emplace_back("sz11", 6.1600f); make_specs.emplace_back("sz12", 6.1600f); make_specs.emplace_back("sz14", 6.1600f); make_specs.emplace_back("sz15", 6.1600f); make_specs.emplace_back("sz16", 6.1600f); make_specs.emplace_back("sz20", 6.1600f); make_specs.emplace_back("sz30mr", 6.1600f); make_specs.emplace_back("sz31mrihs", 6.1600f); make_specs.emplace_back("t100", 6.1600f); make_specs.emplace_back("t10", 6.1600f); make_specs.emplace_back("t110", 6.1600f); make_specs.emplace_back("tg310", 6.1600f); make_specs.emplace_back("tg320", 6.1600f); make_specs.emplace_back("tg610", 6.1600f); make_specs.emplace_back("tg630ihs", 6.1600f); make_specs.emplace_back("tg810", 6.1600f); make_specs.emplace_back("tg820ihs", 6.1600f); make_specs.emplace_back("tg830ihs", 6.1600f); make_specs.emplace_back("tg850ihs", 6.1600f); make_specs.emplace_back("toughtg1ihs", 6.1600f); make_specs.emplace_back("toughtg2ihs", 6.1600f); make_specs.emplace_back("toughtg3", 6.1600f); make_specs.emplace_back("toughtg4", 6.1600f); make_specs.emplace_back("toughtg620", 6.1600f); make_specs.emplace_back("toughtg860", 6.1600f); make_specs.emplace_back("vg110", 6.1600f); make_specs.emplace_back("vg120", 6.1600f); make_specs.emplace_back("vg130", 6.1600f); make_specs.emplace_back("vg145", 6.1600f); make_specs.emplace_back("vg150", 6.1600f); make_specs.emplace_back("vg160", 6.1600f); make_specs.emplace_back("vg165", 6.1600f); make_specs.emplace_back("vg170", 6.1600f); make_specs.emplace_back("vg180", 6.1600f); make_specs.emplace_back("vg190", 6.1600f); make_specs.emplace_back("vh210", 6.1600f); make_specs.emplace_back("vh410", 6.1600f); make_specs.emplace_back("vh510", 6.1600f); make_specs.emplace_back("vh515", 6.1600f); make_specs.emplace_back("vh520", 6.1600f); make_specs.emplace_back("vr310", 6.1600f); make_specs.emplace_back("vr320", 6.1600f); make_specs.emplace_back("vr330", 6.1600f); make_specs.emplace_back("vr340", 6.1600f); make_specs.emplace_back("vr350", 6.1600f); make_specs.emplace_back("vr360", 6.1600f); make_specs.emplace_back("vr370", 6.1600f); make_specs.emplace_back("x15", 6.0300f); make_specs.emplace_back("x775", 5.7500f); make_specs.emplace_back("x785", 5.7500f); make_specs.emplace_back("x905", 6.1600f); make_specs.emplace_back("x920", 6.1600f); make_specs.emplace_back("xz1", 7.8500f); make_specs.emplace_back("xz2ihs", 7.5300f); } { auto& make_specs = specs["panasonic"]; make_specs.reserve(234); make_specs.emplace_back("dsnapsvas10", 4.5000f); make_specs.emplace_back("dsnapsvas30", 4.5000f); make_specs.emplace_back("dsnapsvas3", 4.5000f); make_specs.emplace_back("lumixdmc3d1", 6.1600f); make_specs.emplace_back("lumixdmcf1", 5.7500f); make_specs.emplace_back("lumixdmcf3", 6.0800f); make_specs.emplace_back("lumixdmcf5", 6.0800f); make_specs.emplace_back("lumixdmcf7", 5.3300f); make_specs.emplace_back("lumixdmcfh10", 6.0800f); make_specs.emplace_back("lumixdmcfh1", 6.0800f); make_specs.emplace_back("lumixdmcfh20", 6.0800f); make_specs.emplace_back("lumixdmcfh22", 6.0800f); make_specs.emplace_back("lumixdmcfh25", 6.1600f); make_specs.emplace_back("lumixdmcfh27", 6.1600f); make_specs.emplace_back("lumixdmcfh2", 6.0800f); make_specs.emplace_back("lumixdmcfh3", 6.0800f); make_specs.emplace_back("lumixdmcfh4", 6.0800f); make_specs.emplace_back("lumixdmcfh5", 6.0800f); make_specs.emplace_back("lumixdmcfh6", 6.0800f); make_specs.emplace_back("lumixdmcfh7", 6.0800f); make_specs.emplace_back("lumixdmcfh8", 6.0800f); make_specs.emplace_back("lumixdmcfp1", 6.0800f); make_specs.emplace_back("lumixdmcfp2", 6.0800f); make_specs.emplace_back("lumixdmcfp3", 6.0800f); make_specs.emplace_back("lumixdmcfp5", 6.0800f); make_specs.emplace_back("lumixdmcfp7", 6.0800f); make_specs.emplace_back("lumixdmcfp8", 6.0800f); make_specs.emplace_back("lumixdmcfs10", 6.0800f); make_specs.emplace_back("lumixdmcfs11", 6.0800f); make_specs.emplace_back("lumixdmcfs12", 6.0800f); make_specs.emplace_back("lumixdmcfs15", 6.0800f); make_specs.emplace_back("lumixdmcfs16", 6.0800f); make_specs.emplace_back("lumixdmcfs18", 6.0800f); make_specs.emplace_back("lumixdmcfs20", 6.0800f); make_specs.emplace_back("lumixdmcfs22", 6.0800f); make_specs.emplace_back("lumixdmcfs25", 6.0800f); make_specs.emplace_back("lumixdmcfs28", 6.0800f); make_specs.emplace_back("lumixdmcfs2", 5.7500f); make_specs.emplace_back("lumixdmcfs30", 6.0800f); make_specs.emplace_back("lumixdmcfs33", 6.0800f); make_specs.emplace_back("lumixdmcfs35", 6.1600f); make_specs.emplace_back("lumixdmcfs37", 6.1600f); make_specs.emplace_back("lumixdmcfs3", 5.7500f); make_specs.emplace_back("lumixdmcfs40", 6.0800f); make_specs.emplace_back("lumixdmcfs42", 5.7500f); make_specs.emplace_back("lumixdmcfs45", 6.0800f); make_specs.emplace_back("lumixdmcfs5", 6.0800f); make_specs.emplace_back("lumixdmcfs62", 5.7500f); make_specs.emplace_back("lumixdmcfs6", 5.7500f); make_specs.emplace_back("lumixdmcfs7", 5.7500f); make_specs.emplace_back("lumixdmcft10", 6.0800f); make_specs.emplace_back("lumixdmcft1", 6.0800f); make_specs.emplace_back("lumixdmcft20", 6.0800f); make_specs.emplace_back("lumixdmcft2", 6.0800f); make_specs.emplace_back("lumixdmcft30", 6.0800f); make_specs.emplace_back("lumixdmcft3", 6.0800f); make_specs.emplace_back("lumixdmcft4", 6.0800f); make_specs.emplace_back("lumixdmcfx01", 5.7500f); make_specs.emplace_back("lumixdmcfx07", 5.7500f); make_specs.emplace_back("lumixdmcfx100", 7.4400f); make_specs.emplace_back("lumixdmcfx10", 5.7500f); make_specs.emplace_back("lumixdmcfx12", 5.7500f); make_specs.emplace_back("lumixdmcfx150", 7.4400f); make_specs.emplace_back("lumixdmcfx1", 5.7500f); make_specs.emplace_back("lumixdmcfx2", 5.7500f); make_specs.emplace_back("lumixdmcfx30", 5.7500f); make_specs.emplace_back("lumixdmcfx33", 5.7500f); make_specs.emplace_back("lumixdmcfx35", 6.0800f); make_specs.emplace_back("lumixdmcfx37", 6.0800f); make_specs.emplace_back("lumixdmcfx3", 5.7500f); make_specs.emplace_back("lumixdmcfx40", 6.0800f); make_specs.emplace_back("lumixdmcfx48", 6.0800f); make_specs.emplace_back("lumixdmcfx500", 6.0800f); make_specs.emplace_back("lumixdmcfx50", 5.7500f); make_specs.emplace_back("lumixdmcfx550", 6.0800f); make_specs.emplace_back("lumixdmcfx55", 5.7500f); make_specs.emplace_back("lumixdmcfx580", 6.0800f); make_specs.emplace_back("lumixdmcfx5", 5.7500f); make_specs.emplace_back("lumixdmcfx60", 6.0800f); make_specs.emplace_back("lumixdmcfx65", 6.0800f); make_specs.emplace_back("lumixdmcfx66", 6.0800f); make_specs.emplace_back("lumixdmcfx68", 6.0800f); make_specs.emplace_back("lumixdmcfx700", 6.0800f); make_specs.emplace_back("lumixdmcfx70", 6.0800f); make_specs.emplace_back("lumixdmcfx75", 6.0800f); make_specs.emplace_back("lumixdmcfx77", 6.0800f); make_specs.emplace_back("lumixdmcfx78", 6.0800f); make_specs.emplace_back("lumixdmcfx7", 5.7500f); make_specs.emplace_back("lumixdmcfx80", 6.0800f); make_specs.emplace_back("lumixdmcfx8", 5.7500f); make_specs.emplace_back("lumixdmcfx90", 6.0800f); make_specs.emplace_back("lumixdmcfx9", 5.7500f); make_specs.emplace_back("lumixdmcfz1000", 13.2000f); make_specs.emplace_back("lumixdmcfz100", 6.0800f); make_specs.emplace_back("lumixdmcfz10", 5.7500f); make_specs.emplace_back("lumixdmcfz150", 6.0800f); make_specs.emplace_back("lumixdmcfz15", 5.7500f); make_specs.emplace_back("lumixdmcfz18", 5.7500f); make_specs.emplace_back("lumixdmcfz1", 4.5000f); make_specs.emplace_back("lumixdmcfz200", 6.1600f); make_specs.emplace_back("lumixdmcfz20", 5.7500f); make_specs.emplace_back("lumixdmcfz28", 6.0800f); make_specs.emplace_back("lumixdmcfz2", 4.5000f); make_specs.emplace_back("lumixdmcfz300", 6.1600f); make_specs.emplace_back("lumixdmcfz30", 7.1100f); make_specs.emplace_back("lumixdmcfz35", 6.0800f); make_specs.emplace_back("lumixdmcfz38", 6.0800f); make_specs.emplace_back("lumixdmcfz3", 4.5000f); make_specs.emplace_back("lumixdmcfz40", 6.0800f); make_specs.emplace_back("lumixdmcfz45", 6.0800f); make_specs.emplace_back("lumixdmcfz47", 6.0800f); make_specs.emplace_back("lumixdmcfz48", 6.0800f); make_specs.emplace_back("lumixdmcfz4", 5.7500f); make_specs.emplace_back("lumixdmcfz50", 7.1100f); make_specs.emplace_back("lumixdmcfz5", 5.7500f); make_specs.emplace_back("lumixdmcfz60", 6.0800f); make_specs.emplace_back("lumixdmcfz70", 6.1600f); make_specs.emplace_back("lumixdmcfz7", 5.7500f); make_specs.emplace_back("lumixdmcfz8", 5.7500f); make_specs.emplace_back("lumixdmcg10", 17.3000f); make_specs.emplace_back("lumixdmcg1", 17.3000f); make_specs.emplace_back("lumixdmcg2", 17.3000f); make_specs.emplace_back("lumixdmcg3", 17.3000f); make_specs.emplace_back("lumixdmcg5", 17.3000f); make_specs.emplace_back("lumixdmcg6", 17.3000f); make_specs.emplace_back("lumixdmcg7", 17.3000f); make_specs.emplace_back("lumixdmcgf1", 17.3000f); make_specs.emplace_back("lumixdmcgf2", 17.3000f); make_specs.emplace_back("lumixdmcgf3", 17.3000f); make_specs.emplace_back("lumixdmcgf5", 17.3000f); make_specs.emplace_back("lumixdmcgf6", 17.3000f); make_specs.emplace_back("lumixdmcgf7", 17.3000f); make_specs.emplace_back("lumixdmcgh1", 17.3000f); make_specs.emplace_back("lumixdmcgh2", 17.3000f); make_specs.emplace_back("lumixdmcgh3", 17.3000f); make_specs.emplace_back("lumixdmcgh4", 17.3000f); make_specs.emplace_back("lumixdmcgm1", 17.3000f); make_specs.emplace_back("lumixdmcgm5", 17.3000f); make_specs.emplace_back("lumixdmcgx1", 17.3000f); make_specs.emplace_back("lumixdmcgx7", 17.3000f); make_specs.emplace_back("lumixdmcgx8", 17.3000f); make_specs.emplace_back("lumixdmcl10", 17.3000f); make_specs.emplace_back("lumixdmcl1", 17.3000f); make_specs.emplace_back("lumixdmclc1", 8.8000f); make_specs.emplace_back("lumixdmclc20", 5.3300f); make_specs.emplace_back("lumixdmclc33", 5.7500f); make_specs.emplace_back("lumixdmclc40", 7.5300f); make_specs.emplace_back("lumixdmclc43", 5.7500f); make_specs.emplace_back("lumixdmclc50", 5.7500f); make_specs.emplace_back("lumixdmclc5", 7.5300f); make_specs.emplace_back("lumixdmclc70", 5.7500f); make_specs.emplace_back("lumixdmclc80", 5.7500f); make_specs.emplace_back("lumixdmclf1", 7.5300f); make_specs.emplace_back("lumixdmcls1", 5.7500f); make_specs.emplace_back("lumixdmcls2", 5.7500f); make_specs.emplace_back("lumixdmcls3", 5.7500f); make_specs.emplace_back("lumixdmcls5", 6.0800f); make_specs.emplace_back("lumixdmcls60", 5.7500f); make_specs.emplace_back("lumixdmcls6", 6.0800f); make_specs.emplace_back("lumixdmcls75", 5.7500f); make_specs.emplace_back("lumixdmcls80", 5.7500f); make_specs.emplace_back("lumixdmcls85", 5.7500f); make_specs.emplace_back("lumixdmclx100", 17.3000f); make_specs.emplace_back("lumixdmclx1", 7.7600f); make_specs.emplace_back("lumixdmclx2", 7.7600f); make_specs.emplace_back("lumixdmclx3", 7.8500f); make_specs.emplace_back("lumixdmclx5", 7.8500f); make_specs.emplace_back("lumixdmclx7", 7.5300f); make_specs.emplace_back("lumixdmclz10", 6.0800f); make_specs.emplace_back("lumixdmclz1", 5.7500f); make_specs.emplace_back("lumixdmclz20", 6.0800f); make_specs.emplace_back("lumixdmclz2", 5.7500f); make_specs.emplace_back("lumixdmclz30", 6.1600f); make_specs.emplace_back("lumixdmclz3", 5.7500f); make_specs.emplace_back("lumixdmclz40", 6.1600f); make_specs.emplace_back("lumixdmclz5", 5.7500f); make_specs.emplace_back("lumixdmclz6", 5.7500f); make_specs.emplace_back("lumixdmclz7", 5.7500f); make_specs.emplace_back("lumixdmclz8", 5.7500f); make_specs.emplace_back("lumixdmcs1", 6.0800f); make_specs.emplace_back("lumixdmcs2", 6.0800f); make_specs.emplace_back("lumixdmcs3", 6.0800f); make_specs.emplace_back("lumixdmcs5", 6.0800f); make_specs.emplace_back("lumixdmcsz10", 6.0800f); make_specs.emplace_back("lumixdmcsz1", 6.0800f); make_specs.emplace_back("lumixdmcsz3", 6.0800f); make_specs.emplace_back("lumixdmcsz5", 6.0800f); make_specs.emplace_back("lumixdmcsz7", 6.0800f); make_specs.emplace_back("lumixdmcsz8", 6.0800f); make_specs.emplace_back("lumixdmcsz9", 6.0800f); make_specs.emplace_back("lumixdmcts10", 6.0800f); make_specs.emplace_back("lumixdmcts1", 6.0800f); make_specs.emplace_back("lumixdmcts20", 6.0800f); make_specs.emplace_back("lumixdmcts25", 6.0800f); make_specs.emplace_back("lumixdmcts2", 6.0800f); make_specs.emplace_back("lumixdmcts3", 6.0800f); make_specs.emplace_back("lumixdmcts4", 6.0800f); make_specs.emplace_back("lumixdmcts5", 6.0800f); make_specs.emplace_back("lumixdmctz10", 6.0800f); make_specs.emplace_back("lumixdmctz18", 6.0800f); make_specs.emplace_back("lumixdmctz1", 5.7500f); make_specs.emplace_back("lumixdmctz20", 6.0800f); make_specs.emplace_back("lumixdmctz22", 6.0800f); make_specs.emplace_back("lumixdmctz25", 6.1600f); make_specs.emplace_back("lumixdmctz2", 6.0800f); make_specs.emplace_back("lumixdmctz30", 6.0800f); make_specs.emplace_back("lumixdmctz31", 6.0800f); make_specs.emplace_back("lumixdmctz3", 6.0800f); make_specs.emplace_back("lumixdmctz4", 5.7500f); make_specs.emplace_back("lumixdmctz50", 6.0800f); make_specs.emplace_back("lumixdmctz57", 6.0800f); make_specs.emplace_back("lumixdmctz5", 6.0800f); make_specs.emplace_back("lumixdmctz6", 5.7500f); make_specs.emplace_back("lumixdmctz70", 6.1600f); make_specs.emplace_back("lumixdmctz7", 6.0800f); make_specs.emplace_back("lumixdmcxs1", 6.0800f); make_specs.emplace_back("lumixdmcxs3", 6.0800f); make_specs.emplace_back("lumixdmczr1", 6.0800f); make_specs.emplace_back("lumixdmczr3", 6.0800f); make_specs.emplace_back("lumixdmczs10", 6.0800f); make_specs.emplace_back("lumixdmczs15", 6.1600f); make_specs.emplace_back("lumixdmczs1", 5.7500f); make_specs.emplace_back("lumixdmczs20", 6.0800f); make_specs.emplace_back("lumixdmczs25", 6.0800f); make_specs.emplace_back("lumixdmczs30", 6.1600f); make_specs.emplace_back("lumixdmczs35/tz55", 6.0800f); make_specs.emplace_back("lumixdmczs3", 6.0800f); make_specs.emplace_back("lumixdmczs40/tz60", 6.1600f); make_specs.emplace_back("lumixdmczs5", 6.0800f); make_specs.emplace_back("lumixdmczs7", 6.0800f); make_specs.emplace_back("lumixdmczs8", 6.0800f); make_specs.emplace_back("lumixdmczx1", 6.0800f); make_specs.emplace_back("lumixdmczx3", 6.0800f); make_specs.emplace_back("pvdc3000", 7.1100f); } { auto& make_specs = specs["pentax"]; make_specs.reserve(140); make_specs.emplace_back("645d", 44.0000f); make_specs.emplace_back("645z", 44.0000f); make_specs.emplace_back("efina", 6.1600f); make_specs.emplace_back("ei100", 4.5000f); make_specs.emplace_back("ei2000", 8.8000f); make_specs.emplace_back("ei200", 5.3300f); make_specs.emplace_back("*istdl2", 23.5000f); make_specs.emplace_back("*istdl", 23.5000f); make_specs.emplace_back("*istds2", 23.5000f); make_specs.emplace_back("*istds", 23.5000f); make_specs.emplace_back("*istd", 23.5000f); make_specs.emplace_back("k01", 23.7000f); make_specs.emplace_back("k3ii", 23.5000f); make_specs.emplace_back("k30", 23.7000f); make_specs.emplace_back("k3", 23.5000f); make_specs.emplace_back("k5ii", 23.7000f); make_specs.emplace_back("k500", 23.7000f); make_specs.emplace_back("k50", 23.7000f); make_specs.emplace_back("k5", 23.6000f); make_specs.emplace_back("k7", 23.4000f); make_specs.emplace_back("km", 23.5000f); make_specs.emplace_back("kr", 23.6000f); make_specs.emplace_back("ks1", 23.5000f); make_specs.emplace_back("ks2", 23.5000f); make_specs.emplace_back("kx", 23.6000f); make_specs.emplace_back("k100dsuper", 23.5000f); make_specs.emplace_back("k100d", 23.5000f); make_specs.emplace_back("k10d", 23.5000f); make_specs.emplace_back("k110d", 23.7000f); make_specs.emplace_back("k200d", 23.5000f); make_specs.emplace_back("k20d", 23.4000f); make_specs.emplace_back("mx1", 7.5300f); make_specs.emplace_back("optio230", 5.3300f); make_specs.emplace_back("optio30", 5.3300f); make_specs.emplace_back("optio330gs", 5.3300f); make_specs.emplace_back("optio330rs", 7.1100f); make_specs.emplace_back("optio330", 7.1100f); make_specs.emplace_back("optio33lf", 5.3300f); make_specs.emplace_back("optio33l", 5.3300f); make_specs.emplace_back("optio33wr", 5.3300f); make_specs.emplace_back("optio430rs", 7.1100f); make_specs.emplace_back("optio430", 7.1100f); make_specs.emplace_back("optio43wr", 5.3300f); make_specs.emplace_back("optio450", 7.1100f); make_specs.emplace_back("optio50l", 5.7500f); make_specs.emplace_back("optio50", 5.7500f); make_specs.emplace_back("optio550", 7.1100f); make_specs.emplace_back("optio555", 7.1100f); make_specs.emplace_back("optio60", 7.1100f); make_specs.emplace_back("optio750z", 7.1100f); make_specs.emplace_back("optioa10", 7.1100f); make_specs.emplace_back("optioa20", 5.7500f); make_specs.emplace_back("optioa30", 7.1100f); make_specs.emplace_back("optioa40", 7.5300f); make_specs.emplace_back("optioe10", 5.7500f); make_specs.emplace_back("optioe20", 5.7500f); make_specs.emplace_back("optioe25", 5.7500f); make_specs.emplace_back("optioe30", 5.7500f); make_specs.emplace_back("optioe40", 5.7500f); make_specs.emplace_back("optioe50", 5.7500f); make_specs.emplace_back("optioe60", 6.0800f); make_specs.emplace_back("optioe70l", 6.1600f); make_specs.emplace_back("optioe70", 6.1600f); make_specs.emplace_back("optioe75", 6.0800f); make_specs.emplace_back("optioe80", 6.0800f); make_specs.emplace_back("optioe85", 6.1600f); make_specs.emplace_back("optioe90", 6.1600f); make_specs.emplace_back("optioh90", 6.1600f); make_specs.emplace_back("optioi10", 6.1600f); make_specs.emplace_back("optiol20", 5.7500f); make_specs.emplace_back("optiol50", 6.0300f); make_specs.emplace_back("optiols1000", 6.1600f); make_specs.emplace_back("optiols1100", 6.0800f); make_specs.emplace_back("optiols465", 23.7000f); make_specs.emplace_back("optiom10", 5.7500f); make_specs.emplace_back("optiom20", 5.7500f); make_specs.emplace_back("optiom30", 5.7500f); make_specs.emplace_back("optiom40", 6.0300f); make_specs.emplace_back("optiom50", 6.0300f); make_specs.emplace_back("optiom60", 6.1600f); make_specs.emplace_back("optiom85", 6.1600f); make_specs.emplace_back("optiom90", 6.0800f); make_specs.emplace_back("optiomx4", 5.3300f); make_specs.emplace_back("optiomx", 5.3300f); make_specs.emplace_back("optiop70", 6.1600f); make_specs.emplace_back("optiop80", 6.1600f); make_specs.emplace_back("optiors1000", 6.1600f); make_specs.emplace_back("optiors1500", 6.0800f); make_specs.emplace_back("optiorz10", 6.0800f); make_specs.emplace_back("optiorz18", 6.0800f); make_specs.emplace_back("optios10", 7.1100f); make_specs.emplace_back("optios12", 7.5300f); make_specs.emplace_back("optios1", 6.1600f); make_specs.emplace_back("optios30", 5.3300f); make_specs.emplace_back("optios40", 5.7500f); make_specs.emplace_back("optios45", 5.7500f); make_specs.emplace_back("optios4i", 5.7500f); make_specs.emplace_back("optios4", 5.7500f); make_specs.emplace_back("optios50", 5.7500f); make_specs.emplace_back("optios55", 5.7500f); make_specs.emplace_back("optios5i", 5.7500f); make_specs.emplace_back("optios5n", 5.7500f); make_specs.emplace_back("optios5z", 5.7500f); make_specs.emplace_back("optios60", 5.7500f); make_specs.emplace_back("optios6", 5.7500f); make_specs.emplace_back("optios7", 5.7500f); make_specs.emplace_back("optiosvi", 5.7500f); make_specs.emplace_back("optiosv", 5.7500f); make_specs.emplace_back("optios", 5.7500f); make_specs.emplace_back("optiot10", 5.7500f); make_specs.emplace_back("optiot20", 5.7500f); make_specs.emplace_back("optiot30", 5.7500f); make_specs.emplace_back("optiov10", 6.0300f); make_specs.emplace_back("optiov20", 6.0300f); make_specs.emplace_back("optiovs20", 6.0800f); make_specs.emplace_back("optiow10", 5.7500f); make_specs.emplace_back("optiow20", 5.7500f); make_specs.emplace_back("optiow30", 5.7500f); make_specs.emplace_back("optiow60", 6.1600f); make_specs.emplace_back("optiow80", 6.1600f); make_specs.emplace_back("optiow90", 6.1600f); make_specs.emplace_back("optiowg1gps", 6.1600f); make_specs.emplace_back("optiowg1", 6.1600f); make_specs.emplace_back("optiowg2gps", 6.1600f); make_specs.emplace_back("optiowg2", 6.1600f); make_specs.emplace_back("optiowpi", 5.7500f); make_specs.emplace_back("optiowp", 5.7500f); make_specs.emplace_back("optiows80", 6.0800f); make_specs.emplace_back("optiox", 5.7500f); make_specs.emplace_back("optioz10", 5.7500f); make_specs.emplace_back("qs1", 7.5300f); make_specs.emplace_back("q10", 6.1600f); make_specs.emplace_back("q7", 7.5300f); make_specs.emplace_back("q", 6.1600f); make_specs.emplace_back("wg10", 6.1600f); make_specs.emplace_back("wg3", 6.1600f); make_specs.emplace_back("x5", 6.0800f); make_specs.emplace_back("x70", 6.0800f); make_specs.emplace_back("x90", 6.0800f); make_specs.emplace_back("xg1", 6.0800f); } { auto& make_specs = specs["praktica"]; make_specs.reserve(131); make_specs.emplace_back("dc20", 6.4000f); make_specs.emplace_back("dc21", 5.3300f); make_specs.emplace_back("dc22", 5.3300f); make_specs.emplace_back("dc32", 5.3300f); make_specs.emplace_back("dc34", 5.3300f); make_specs.emplace_back("dc42", 5.7500f); make_specs.emplace_back("dc44", 5.7500f); make_specs.emplace_back("dc50", 5.7500f); make_specs.emplace_back("dc52", 5.7500f); make_specs.emplace_back("dc60", 5.7500f); make_specs.emplace_back("dcslim2", 6.4000f); make_specs.emplace_back("dcslim5", 7.1100f); make_specs.emplace_back("dc440", 5.7500f); make_specs.emplace_back("dcz1.3", 6.4000f); make_specs.emplace_back("dcz10.1", 7.1100f); make_specs.emplace_back("dcz10.2", 6.1600f); make_specs.emplace_back("dcz10.3", 6.0800f); make_specs.emplace_back("dcz10.4", 6.1600f); make_specs.emplace_back("dcz12.1", 6.1600f); make_specs.emplace_back("dcz12.z4", 6.1600f); make_specs.emplace_back("dcz14.1", 6.1600f); make_specs.emplace_back("dcz14.2", 6.1600f); make_specs.emplace_back("dcz2.0", 4.8000f); make_specs.emplace_back("dcz2.1s", 4.5000f); make_specs.emplace_back("dcz2.1", 4.8000f); make_specs.emplace_back("dcz2.2s", 6.4000f); make_specs.emplace_back("dcz2.2", 5.3300f); make_specs.emplace_back("dcz3.0", 6.4000f); make_specs.emplace_back("dcz3.2d", 6.4000f); make_specs.emplace_back("dcz3.2s", 6.4000f); make_specs.emplace_back("dcz3.2", 7.1100f); make_specs.emplace_back("dcz3.3", 7.1100f); make_specs.emplace_back("dcz3.4", 5.7500f); make_specs.emplace_back("dcz3.5", 5.7500f); make_specs.emplace_back("dcz4.1", 7.1100f); make_specs.emplace_back("dcz4.2", 7.1100f); make_specs.emplace_back("dcz4.3", 7.1100f); make_specs.emplace_back("dcz4.4", 5.7500f); make_specs.emplace_back("dcz5.1", 7.1100f); make_specs.emplace_back("dcz5.2", 7.1100f); make_specs.emplace_back("dcz5.3", 5.7500f); make_specs.emplace_back("dcz5.4", 5.7500f); make_specs.emplace_back("dcz5.5", 5.7500f); make_specs.emplace_back("dcz5.8", 5.7500f); make_specs.emplace_back("dcz6.1", 5.7500f); make_specs.emplace_back("dcz6.2", 5.7500f); make_specs.emplace_back("dcz6.3", 5.7500f); make_specs.emplace_back("dcz6.8", 5.7500f); make_specs.emplace_back("dcz7.1", 5.7500f); make_specs.emplace_back("dcz7.2", 5.7500f); make_specs.emplace_back("dcz7.3", 5.7500f); make_specs.emplace_back("dcz7.4", 5.7500f); make_specs.emplace_back("dcz8.1", 5.7500f); make_specs.emplace_back("dcz8.2", 5.7500f); make_specs.emplace_back("dcz8.3", 5.7500f); make_specs.emplace_back("digi3lm", 6.4000f); make_specs.emplace_back("digi30", 6.4000f); make_specs.emplace_back("digi3", 6.4000f); make_specs.emplace_back("digicam3", 6.4000f); make_specs.emplace_back("dmmc4", 4.8000f); make_specs.emplace_back("dmmc", 4.8000f); make_specs.emplace_back("dpix1000z", 6.1600f); make_specs.emplace_back("dpix1100z", 5.7500f); make_specs.emplace_back("dpix1220z", 6.0800f); make_specs.emplace_back("dpix3000", 6.4000f); make_specs.emplace_back("dpix3200", 4.8000f); make_specs.emplace_back("dpix3300", 4.8000f); make_specs.emplace_back("dpix5000wp", 4.5000f); make_specs.emplace_back("dpix5100", 5.7500f); make_specs.emplace_back("dpix510z", 5.7500f); make_specs.emplace_back("dpix5200", 5.7500f); make_specs.emplace_back("dpix530z", 5.7500f); make_specs.emplace_back("dpix740z", 5.7500f); make_specs.emplace_back("dpix750z", 5.7500f); make_specs.emplace_back("dpix810z", 5.7500f); make_specs.emplace_back("dpix820z", 5.7500f); make_specs.emplace_back("dpix9000", 6.1600f); make_specs.emplace_back("dpix910z", 6.4000f); make_specs.emplace_back("dvc6.1", 5.7500f); make_specs.emplace_back("exaktadc4200", 7.1100f); make_specs.emplace_back("g2.0", 6.4000f); make_specs.emplace_back("g3.2", 6.4000f); make_specs.emplace_back("luxmedia1003", 6.1600f); make_specs.emplace_back("luxmedia1023", 6.1600f); make_specs.emplace_back("luxmedia10ts", 6.1600f); make_specs.emplace_back("luxmedia10x3", 7.1100f); make_specs.emplace_back("luxmedia10xs", 5.7500f); make_specs.emplace_back("luxmedia1203", 5.7500f); make_specs.emplace_back("luxmedia1204", 6.1600f); make_specs.emplace_back("luxmedia1223", 6.1600f); make_specs.emplace_back("luxmedia12hd", 7.4400f); make_specs.emplace_back("luxmedia12ts", 6.1600f); make_specs.emplace_back("luxmedia12xs", 5.7500f); make_specs.emplace_back("luxmedia12z4ts", 6.1600f); make_specs.emplace_back("luxmedia12z4", 6.1600f); make_specs.emplace_back("luxmedia12z5", 6.1600f); make_specs.emplace_back("luxmedia14z50s", 6.1600f); make_specs.emplace_back("luxmedia14z51", 6.1600f); make_specs.emplace_back("luxmedia14z80s", 6.1600f); make_specs.emplace_back("luxmedia16z12s", 6.1600f); make_specs.emplace_back("luxmedia16z21c", 6.1600f); make_specs.emplace_back("luxmedia16z21s", 6.1600f); make_specs.emplace_back("luxmedia16z24s", 6.1600f); make_specs.emplace_back("luxmedia16z51", 6.1600f); make_specs.emplace_back("luxmedia16z52", 6.1600f); make_specs.emplace_back("luxmedia18z36c", 6.1600f); make_specs.emplace_back("luxmedia4008", 5.7500f); make_specs.emplace_back("luxmedia5003", 7.1100f); make_specs.emplace_back("luxmedia5008", 5.7500f); make_specs.emplace_back("luxmedia5103", 7.1100f); make_specs.emplace_back("luxmedia5203", 5.7500f); make_specs.emplace_back("luxmedia5303", 5.7500f); make_specs.emplace_back("luxmedia6103", 7.1100f); make_specs.emplace_back("luxmedia6105", 5.7500f); make_specs.emplace_back("luxmedia6203", 5.7500f); make_specs.emplace_back("luxmedia6403", 5.7500f); make_specs.emplace_back("luxmedia6503", 5.7500f); make_specs.emplace_back("luxmedia7103", 5.7500f); make_specs.emplace_back("luxmedia7105", 5.7500f); make_specs.emplace_back("luxmedia7203", 5.7500f); make_specs.emplace_back("luxmedia7303", 5.7500f); make_specs.emplace_back("luxmedia7403", 5.7500f); make_specs.emplace_back("luxmedia8003", 7.1100f); make_specs.emplace_back("luxmedia8203", 5.7500f); make_specs.emplace_back("luxmedia8213", 5.7500f); make_specs.emplace_back("luxmedia8303", 5.7500f); make_specs.emplace_back("luxmedia8403", 5.7500f); make_specs.emplace_back("luxmedia8503", 5.7500f); make_specs.emplace_back("mini", 6.4000f); make_specs.emplace_back("v2.1", 6.4000f); make_specs.emplace_back("v3.2", 6.4000f); } { auto& make_specs = specs["ricoh"]; make_specs.reserve(76); make_specs.emplace_back("caplio400gwide", 5.3300f); make_specs.emplace_back("caplio500gwide", 7.1100f); make_specs.emplace_back("caplio500g", 7.1100f); make_specs.emplace_back("caplio500se", 7.1100f); make_specs.emplace_back("capliog3s", 5.3300f); make_specs.emplace_back("capliog3", 5.3300f); make_specs.emplace_back("capliogx100", 7.3100f); make_specs.emplace_back("capliogx200", 7.5300f); make_specs.emplace_back("capliogx8", 7.1100f); make_specs.emplace_back("capliogx", 7.1100f); make_specs.emplace_back("caplior1v", 5.7500f); make_specs.emplace_back("caplior1", 7.1100f); make_specs.emplace_back("caplior2s", 5.7500f); make_specs.emplace_back("caplior2", 5.7500f); make_specs.emplace_back("caplior30", 5.7500f); make_specs.emplace_back("caplior3", 5.7500f); make_specs.emplace_back("caplior40", 5.7500f); make_specs.emplace_back("caplior4", 5.7500f); make_specs.emplace_back("caplior5", 5.7500f); make_specs.emplace_back("caplior6", 5.7500f); make_specs.emplace_back("caplior7", 5.7500f); make_specs.emplace_back("caplior8", 6.1600f); make_specs.emplace_back("capliorr10", 5.3300f); make_specs.emplace_back("capliorr120", 4.5000f); make_specs.emplace_back("capliorr1", 7.1100f); make_specs.emplace_back("capliorr230", 4.5000f); make_specs.emplace_back("capliorr30", 5.3300f); make_specs.emplace_back("capliorr330", 5.7500f); make_specs.emplace_back("capliorr530", 5.7500f); make_specs.emplace_back("capliorr630", 7.1100f); make_specs.emplace_back("capliorr660", 5.7500f); make_specs.emplace_back("capliorr750", 5.7500f); make_specs.emplace_back("capliorr770", 5.7500f); make_specs.emplace_back("capliorx", 5.3300f); make_specs.emplace_back("capliorz1", 5.7500f); make_specs.emplace_back("cx1", 6.1600f); make_specs.emplace_back("cx2", 6.1600f); make_specs.emplace_back("cx3", 6.1600f); make_specs.emplace_back("cx4", 6.1600f); make_specs.emplace_back("cx5", 6.1600f); make_specs.emplace_back("cx6", 6.1600f); make_specs.emplace_back("g600", 6.1600f); make_specs.emplace_back("g700se", 6.1600f); make_specs.emplace_back("g700", 6.1600f); make_specs.emplace_back("g800", 6.1600f); make_specs.emplace_back("grdigital3", 7.5300f); make_specs.emplace_back("grdigital4", 7.5300f); make_specs.emplace_back("grdigitalii", 7.3100f); make_specs.emplace_back("grdigital", 7.1100f); make_specs.emplace_back("grii", 23.6000f); make_specs.emplace_back("gr", 23.6000f); make_specs.emplace_back("gx200", 7.5300f); make_specs.emplace_back("gxra1250mmf2.5macro", 23.6000f); make_specs.emplace_back("gxra162485mmf3.55.5", 23.6000f); make_specs.emplace_back("gxrgrlensa1228mmf2.5", 23.6000f); make_specs.emplace_back("gxrmounta12", 23.6000f); make_specs.emplace_back("gxrp1028300mmf3.55.6vc", 6.1600f); make_specs.emplace_back("gxrs102472mmf2.54.4vc", 7.5300f); make_specs.emplace_back("hz15", 6.1600f); make_specs.emplace_back("px", 6.1600f); make_specs.emplace_back("r10", 6.1600f); make_specs.emplace_back("r50", 6.0800f); make_specs.emplace_back("r8", 6.1600f); make_specs.emplace_back("rdc200g", 6.4000f); make_specs.emplace_back("rdc4300", 4.8000f); make_specs.emplace_back("rdc5000", 5.3300f); make_specs.emplace_back("rdc5300", 5.3300f); make_specs.emplace_back("rdc6000", 6.4000f); make_specs.emplace_back("rdc7", 7.1100f); make_specs.emplace_back("rdci500", 7.1100f); make_specs.emplace_back("rdci700", 7.1100f); make_specs.emplace_back("wg20", 6.1600f); make_specs.emplace_back("wg30", 6.1600f); make_specs.emplace_back("wg4", 6.1600f); make_specs.emplace_back("wg5gps", 6.1600f); make_specs.emplace_back("wgm1", 6.1600f); } { auto& make_specs = specs["rollei"]; make_specs.reserve(121); make_specs.emplace_back("compactline100", 6.1600f); make_specs.emplace_back("compactline101", 5.7500f); make_specs.emplace_back("compactline102", 5.7500f); make_specs.emplace_back("compactline103", 6.0800f); make_specs.emplace_back("compactline110", 5.7500f); make_specs.emplace_back("compactline130", 5.7500f); make_specs.emplace_back("compactline150", 6.0800f); make_specs.emplace_back("compactline200", 6.1600f); make_specs.emplace_back("compactline202", 6.0800f); make_specs.emplace_back("compactline203", 6.0800f); make_specs.emplace_back("compactline230", 6.0800f); make_specs.emplace_back("compactline302", 6.0800f); make_specs.emplace_back("compactline304", 6.0800f); make_specs.emplace_back("compactline312", 6.1600f); make_specs.emplace_back("compactline320", 6.0800f); make_specs.emplace_back("compactline350", 6.1600f); make_specs.emplace_back("compactline360ts", 6.1600f); make_specs.emplace_back("compactline370ts", 6.1600f); make_specs.emplace_back("compactline390se", 6.1600f); make_specs.emplace_back("compactline412", 6.0800f); make_specs.emplace_back("compactline415", 6.0800f); make_specs.emplace_back("compactline425", 6.1600f); make_specs.emplace_back("compactline50", 6.1600f); make_specs.emplace_back("compactline52", 6.1600f); make_specs.emplace_back("compactline55", 6.1600f); make_specs.emplace_back("compactline80", 5.7500f); make_specs.emplace_back("compactline81", 5.7500f); make_specs.emplace_back("compactline90", 6.1600f); make_specs.emplace_back("d20motion", 7.1100f); make_specs.emplace_back("d210motion", 4.5000f); make_specs.emplace_back("d23com", 7.5300f); make_specs.emplace_back("d33com", 7.1100f); make_specs.emplace_back("d330motion", 5.3300f); make_specs.emplace_back("d41com", 7.1100f); make_specs.emplace_back("d530flex", 8.8000f); make_specs.emplace_back("da10", 5.7500f); make_specs.emplace_back("da1325prego", 5.7500f); make_specs.emplace_back("da5324", 5.7500f); make_specs.emplace_back("da5325prego", 5.7500f); make_specs.emplace_back("da6324", 5.7500f); make_specs.emplace_back("da7325prego", 5.7500f); make_specs.emplace_back("da8324", 5.7500f); make_specs.emplace_back("dc3100", 5.3300f); make_specs.emplace_back("dcx310", 7.1100f); make_specs.emplace_back("dcx400", 7.1100f); make_specs.emplace_back("dk3000", 5.3300f); make_specs.emplace_back("dk4010", 5.3300f); make_specs.emplace_back("dp300", 5.3300f); make_specs.emplace_back("dp3210", 5.3300f); make_specs.emplace_back("dp6500", 7.1100f); make_specs.emplace_back("dpx310", 5.3300f); make_specs.emplace_back("dr5100", 7.1100f); make_specs.emplace_back("dr5", 5.7500f); make_specs.emplace_back("ds6", 5.7500f); make_specs.emplace_back("dsx410", 5.7500f); make_specs.emplace_back("dt3200", 5.7500f); make_specs.emplace_back("dt4000", 5.7500f); make_specs.emplace_back("dt4200", 5.7500f); make_specs.emplace_back("dt6tribute", 5.7500f); make_specs.emplace_back("dx63", 5.7500f); make_specs.emplace_back("flexline100it", 5.7500f); make_specs.emplace_back("flexline100", 5.7500f); make_specs.emplace_back("flexline140", 6.0800f); make_specs.emplace_back("flexline200", 6.0800f); make_specs.emplace_back("flexline202", 6.1600f); make_specs.emplace_back("flexline250", 6.0800f); make_specs.emplace_back("kids100", 8.8000f); make_specs.emplace_back("powerflex240hd", 6.1600f); make_specs.emplace_back("powerflex360fullhd", 6.1600f); make_specs.emplace_back("powerflex3d", 6.0800f); make_specs.emplace_back("powerflex400", 6.0800f); make_specs.emplace_back("powerflex440", 6.0800f); make_specs.emplace_back("powerflex450", 6.0800f); make_specs.emplace_back("powerflex455", 6.0800f); make_specs.emplace_back("powerflex460", 6.0800f); make_specs.emplace_back("powerflex470", 6.0800f); make_specs.emplace_back("powerflex500", 6.0800f); make_specs.emplace_back("powerflex600", 6.1600f); make_specs.emplace_back("powerflex610hd", 6.1600f); make_specs.emplace_back("powerflex700fullhd", 6.0800f); make_specs.emplace_back("powerflex800", 6.0800f); make_specs.emplace_back("powerflex820", 6.0800f); make_specs.emplace_back("pregoda3", 5.3300f); make_specs.emplace_back("pregoda4", 5.7500f); make_specs.emplace_back("pregoda5", 5.7500f); make_specs.emplace_back("pregoda6", 7.1100f); make_specs.emplace_back("pregodp4200", 5.7500f); make_specs.emplace_back("pregodp5200", 5.7500f); make_specs.emplace_back("pregodp5300", 7.1100f); make_specs.emplace_back("pregodp5500", 5.7500f); make_specs.emplace_back("pregodp6000", 7.1100f); make_specs.emplace_back("pregodp6200", 7.1100f); make_specs.emplace_back("pregodp6300", 7.1100f); make_specs.emplace_back("pregodp8300", 7.1100f); make_specs.emplace_back("rcp10325x", 7.1100f); make_specs.emplace_back("rcp5324", 5.7500f); make_specs.emplace_back("rcp6324", 5.7500f); make_specs.emplace_back("rcp7324", 5.7500f); make_specs.emplace_back("rcp7325xs", 5.7500f); make_specs.emplace_back("rcp7330x", 5.7500f); make_specs.emplace_back("rcp7430xw", 5.7500f); make_specs.emplace_back("rcp8325xs", 5.7500f); make_specs.emplace_back("rcp8325x", 7.1100f); make_specs.emplace_back("rcp8325", 5.7500f); make_specs.emplace_back("rcp8330x", 5.7500f); make_specs.emplace_back("rcp8427xw", 5.7500f); make_specs.emplace_back("rcp8527x", 5.7500f); make_specs.emplace_back("rcps10", 6.1600f); make_specs.emplace_back("rcps8", 5.7500f); make_specs.emplace_back("sportsline50", 6.1600f); make_specs.emplace_back("sportsline60camouflage", 6.1600f); make_specs.emplace_back("sportsline62", 6.1600f); make_specs.emplace_back("sportsline90", 6.1600f); make_specs.emplace_back("sportsline99", 6.1600f); make_specs.emplace_back("x8compact", 5.7500f); make_specs.emplace_back("x8sports", 5.7500f); make_specs.emplace_back("x8", 5.7500f); make_specs.emplace_back("xs10intouch", 6.1600f); make_specs.emplace_back("xs10", 5.7500f); make_specs.emplace_back("xs8crystal", 5.7500f); make_specs.emplace_back("xs8", 5.7500f); } { auto& make_specs = specs["samsung"]; make_specs.reserve(279); make_specs.emplace_back("aq100", 6.0800f); make_specs.emplace_back("cl5", 5.7500f); make_specs.emplace_back("cl65", 6.0800f); make_specs.emplace_back("cl80", 6.1600f); make_specs.emplace_back("d75", 5.7500f); make_specs.emplace_back("d830", 7.1100f); make_specs.emplace_back("d85", 5.7500f); make_specs.emplace_back("d860", 5.7500f); make_specs.emplace_back("digimax101", 6.4000f); make_specs.emplace_back("digimax130", 4.5000f); make_specs.emplace_back("digimax200", 5.3300f); make_specs.emplace_back("digimax201", 4.5000f); make_specs.emplace_back("digimax202", 6.4000f); make_specs.emplace_back("digimax210se", 5.3300f); make_specs.emplace_back("digimax220se", 5.3300f); make_specs.emplace_back("digimax230", 5.3300f); make_specs.emplace_back("digimax240", 4.5000f); make_specs.emplace_back("digimax250", 4.5000f); make_specs.emplace_back("digimax301", 5.3300f); make_specs.emplace_back("digimax330", 7.5300f); make_specs.emplace_back("digimax340", 7.1100f); make_specs.emplace_back("digimax35mp3", 4.8000f); make_specs.emplace_back("digimax350se", 7.1100f); make_specs.emplace_back("digimax360", 7.1100f); make_specs.emplace_back("digimax370", 5.7500f); make_specs.emplace_back("digimax401", 5.7500f); make_specs.emplace_back("digimax410", 7.1100f); make_specs.emplace_back("digimax420", 7.1100f); make_specs.emplace_back("digimax430", 5.7500f); make_specs.emplace_back("digimax50duo", 4.8000f); make_specs.emplace_back("digimax530", 7.1100f); make_specs.emplace_back("digimaxa400", 5.7500f); make_specs.emplace_back("digimaxa402", 5.7500f); make_specs.emplace_back("digimaxa502", 5.7500f); make_specs.emplace_back("digimaxa503", 5.7500f); make_specs.emplace_back("digimaxa50", 5.7500f); make_specs.emplace_back("digimaxa55w", 5.7500f); make_specs.emplace_back("digimaxa5", 7.1100f); make_specs.emplace_back("digimaxa6", 7.1100f); make_specs.emplace_back("digimaxa7", 7.1100f); make_specs.emplace_back("digimaxd103", 7.1100f); make_specs.emplace_back("digimaxi50mp3", 5.7500f); make_specs.emplace_back("digimaxi5", 5.7500f); make_specs.emplace_back("digimaxi6", 5.7500f); make_specs.emplace_back("digimaxl50", 5.7500f); make_specs.emplace_back("digimaxl55w", 5.7500f); make_specs.emplace_back("digimaxl60", 5.7500f); make_specs.emplace_back("digimaxl70", 5.7500f); make_specs.emplace_back("digimaxl85", 7.1100f); make_specs.emplace_back("digimaxs1000", 7.1100f); make_specs.emplace_back("digimaxs500", 5.7500f); make_specs.emplace_back("digimaxs600", 5.7500f); make_specs.emplace_back("digimaxs700", 5.7500f); make_specs.emplace_back("digimaxs800", 7.1100f); make_specs.emplace_back("digimaxuca3", 5.3300f); make_specs.emplace_back("digimaxuca401", 5.7500f); make_specs.emplace_back("digimaxuca4", 5.7500f); make_specs.emplace_back("digimaxuca501", 5.7500f); make_specs.emplace_back("digimaxuca505", 5.7500f); make_specs.emplace_back("digimaxuca5", 5.7500f); make_specs.emplace_back("digimaxv3", 7.1100f); make_specs.emplace_back("digimaxv4000", 7.1100f); make_specs.emplace_back("digimaxv40", 7.1100f); make_specs.emplace_back("digimaxv4", 7.1100f); make_specs.emplace_back("digimaxv50", 7.1100f); make_specs.emplace_back("digimaxv5", 7.1100f); make_specs.emplace_back("digimaxv600", 7.1100f); make_specs.emplace_back("digimaxv6", 6.7400f); make_specs.emplace_back("digimaxv700", 7.1100f); make_specs.emplace_back("digimaxv70", 7.1100f); make_specs.emplace_back("digimaxv800", 7.1100f); make_specs.emplace_back("dv100", 6.1600f); make_specs.emplace_back("dv150f", 6.1600f); make_specs.emplace_back("dv300f", 6.1600f); make_specs.emplace_back("es10", 5.7500f); make_specs.emplace_back("es15", 6.0800f); make_specs.emplace_back("es17", 6.0800f); make_specs.emplace_back("es20", 6.0800f); make_specs.emplace_back("es25", 6.0800f); make_specs.emplace_back("es28", 6.0800f); make_specs.emplace_back("es30", 6.1600f); make_specs.emplace_back("es50", 6.0800f); make_specs.emplace_back("es55", 6.0800f); make_specs.emplace_back("es60", 6.0800f); make_specs.emplace_back("es65", 6.0800f); make_specs.emplace_back("es70", 6.0800f); make_specs.emplace_back("es73", 6.0800f); make_specs.emplace_back("es75", 6.0800f); make_specs.emplace_back("es80", 6.1600f); make_specs.emplace_back("es90", 6.1600f); make_specs.emplace_back("es95", 6.1600f); make_specs.emplace_back("ex1", 7.5300f); make_specs.emplace_back("ex2f", 7.5300f); make_specs.emplace_back("galaxycamera2", 6.1600f); make_specs.emplace_back("galaxycamera", 6.1600f); make_specs.emplace_back("galaxynx", 23.5000f); make_specs.emplace_back("gx10", 23.5000f); make_specs.emplace_back("gx1l", 23.5000f); make_specs.emplace_back("gx1s", 23.5000f); make_specs.emplace_back("gx20", 23.4000f); make_specs.emplace_back("hz10w", 6.0800f); make_specs.emplace_back("hz15w", 6.0800f); make_specs.emplace_back("hz25w", 6.0800f); make_specs.emplace_back("hz30w", 6.1600f); make_specs.emplace_back("hz35w", 6.1600f); make_specs.emplace_back("hz50w", 6.1600f); make_specs.emplace_back("i100", 6.0800f); make_specs.emplace_back("i70", 5.7500f); make_specs.emplace_back("i7", 5.7500f); make_specs.emplace_back("i80", 5.7500f); make_specs.emplace_back("i85", 5.7500f); make_specs.emplace_back("i8", 5.7500f); make_specs.emplace_back("it100", 6.0800f); make_specs.emplace_back("l100", 5.7500f); make_specs.emplace_back("l110", 5.7500f); make_specs.emplace_back("l200", 5.7500f); make_specs.emplace_back("l201", 6.0800f); make_specs.emplace_back("l210", 6.0800f); make_specs.emplace_back("l301", 6.0800f); make_specs.emplace_back("l310w", 7.4400f); make_specs.emplace_back("l700", 5.7500f); make_specs.emplace_back("l730", 5.7500f); make_specs.emplace_back("l73", 5.7500f); make_specs.emplace_back("l74wide", 5.7500f); make_specs.emplace_back("l74", 5.7500f); make_specs.emplace_back("l77", 5.7500f); make_specs.emplace_back("l80", 7.1100f); make_specs.emplace_back("l830", 5.7500f); make_specs.emplace_back("l83t", 5.7500f); make_specs.emplace_back("m100", 5.7500f); make_specs.emplace_back("miniketvpms10", 5.7500f); make_specs.emplace_back("miniketvpms11", 5.7500f); make_specs.emplace_back("miniketvpms15", 5.7500f); make_specs.emplace_back("mv800", 6.1600f); make_specs.emplace_back("nv100hd", 7.4400f); make_specs.emplace_back("nv10", 7.1100f); make_specs.emplace_back("nv11", 7.1100f); make_specs.emplace_back("nv15", 7.2700f); make_specs.emplace_back("nv20", 7.4400f); make_specs.emplace_back("nv24hd", 6.1600f); make_specs.emplace_back("nv30", 5.7500f); make_specs.emplace_back("nv3", 5.7500f); make_specs.emplace_back("nv40", 6.0800f); make_specs.emplace_back("nv4", 5.7500f); make_specs.emplace_back("nv7ops", 5.7500f); make_specs.emplace_back("nv8", 7.2700f); make_specs.emplace_back("nv9", 6.1600f); make_specs.emplace_back("nxmini", 13.2000f); make_specs.emplace_back("nx1000", 23.5000f); make_specs.emplace_back("nx100", 23.4000f); make_specs.emplace_back("nx10", 23.4000f); make_specs.emplace_back("nx1100", 23.5000f); make_specs.emplace_back("nx11", 23.4000f); make_specs.emplace_back("nx1", 23.5000f); make_specs.emplace_back("nx2000", 23.5000f); make_specs.emplace_back("nx200", 23.5000f); make_specs.emplace_back("nx20", 23.5000f); make_specs.emplace_back("nx210", 23.5000f); make_specs.emplace_back("nx3000", 23.5000f); make_specs.emplace_back("nx300", 23.5000f); make_specs.emplace_back("nx30", 23.5000f); make_specs.emplace_back("nx500", 23.5000f); make_specs.emplace_back("nx5", 23.4000f); make_specs.emplace_back("pl100", 6.0800f); make_specs.emplace_back("pl10", 5.7500f); make_specs.emplace_back("pl120", 6.1600f); make_specs.emplace_back("pl150", 6.0800f); make_specs.emplace_back("pl160", 6.0800f); make_specs.emplace_back("pl170", 6.0800f); make_specs.emplace_back("pl200", 6.1600f); make_specs.emplace_back("pl20", 6.1600f); make_specs.emplace_back("pl210", 6.1600f); make_specs.emplace_back("pl50", 6.0800f); make_specs.emplace_back("pl51", 6.1600f); make_specs.emplace_back("pl55", 6.0800f); make_specs.emplace_back("pl60", 6.0800f); make_specs.emplace_back("pl65", 6.0800f); make_specs.emplace_back("pl70", 6.0800f); make_specs.emplace_back("pl80", 6.0800f); make_specs.emplace_back("pl90", 6.0800f); make_specs.emplace_back("pro815", 8.8000f); make_specs.emplace_back("s1030", 7.1100f); make_specs.emplace_back("s1050", 7.1100f); make_specs.emplace_back("s1060", 6.0800f); make_specs.emplace_back("s1070", 6.0800f); make_specs.emplace_back("s630", 5.7500f); make_specs.emplace_back("s730", 5.7500f); make_specs.emplace_back("s750", 5.7500f); make_specs.emplace_back("s760", 5.7500f); make_specs.emplace_back("s830", 7.1100f); make_specs.emplace_back("s850", 7.1100f); make_specs.emplace_back("s85", 5.7500f); make_specs.emplace_back("s860", 5.7500f); make_specs.emplace_back("sdcms61", 5.7500f); make_specs.emplace_back("sh100", 6.0800f); make_specs.emplace_back("sl102", 6.0800f); make_specs.emplace_back("sl201", 6.0800f); make_specs.emplace_back("sl202", 6.0800f); make_specs.emplace_back("sl30", 6.0800f); make_specs.emplace_back("sl310w", 7.4400f); make_specs.emplace_back("sl502", 6.0800f); make_specs.emplace_back("sl50", 6.0800f); make_specs.emplace_back("sl600", 6.0800f); make_specs.emplace_back("sl605", 6.0800f); make_specs.emplace_back("sl620", 6.0800f); make_specs.emplace_back("sl630", 6.0800f); make_specs.emplace_back("sl720", 6.0800f); make_specs.emplace_back("sl820", 6.0800f); make_specs.emplace_back("st1000", 6.1600f); make_specs.emplace_back("st100", 6.1600f); make_specs.emplace_back("st10", 5.7500f); make_specs.emplace_back("st150f", 6.1600f); make_specs.emplace_back("st200f", 6.1600f); make_specs.emplace_back("st30", 4.8000f); make_specs.emplace_back("st45", 6.0800f); make_specs.emplace_back("st5000", 6.1600f); make_specs.emplace_back("st500", 6.1600f); make_specs.emplace_back("st50", 6.0800f); make_specs.emplace_back("st5500", 6.1600f); make_specs.emplace_back("st550", 6.0800f); make_specs.emplace_back("st600", 6.0800f); make_specs.emplace_back("st60", 6.1600f); make_specs.emplace_back("st6500", 6.0800f); make_specs.emplace_back("st65", 6.1600f); make_specs.emplace_back("st66", 6.1600f); make_specs.emplace_back("st700", 6.1600f); make_specs.emplace_back("st70", 6.1600f); make_specs.emplace_back("st72", 6.1600f); make_specs.emplace_back("st76", 6.1600f); make_specs.emplace_back("st77", 6.1600f); make_specs.emplace_back("st80", 6.0800f); make_specs.emplace_back("st88", 6.1600f); make_specs.emplace_back("st90", 6.1600f); make_specs.emplace_back("st93", 6.1600f); make_specs.emplace_back("st95", 6.1600f); make_specs.emplace_back("st96", 6.1600f); make_specs.emplace_back("tl100", 6.0800f); make_specs.emplace_back("tl105", 6.1600f); make_specs.emplace_back("tl110", 6.1600f); make_specs.emplace_back("tl205", 6.0800f); make_specs.emplace_back("tl210", 6.0800f); make_specs.emplace_back("tl220", 6.0800f); make_specs.emplace_back("tl225", 6.0800f); make_specs.emplace_back("tl240", 6.1600f); make_specs.emplace_back("tl320", 6.0800f); make_specs.emplace_back("tl34hd", 7.4400f); make_specs.emplace_back("tl350", 6.0800f); make_specs.emplace_back("tl500", 7.5300f); make_specs.emplace_back("tl9", 6.1600f); make_specs.emplace_back("wb1000", 6.0800f); make_specs.emplace_back("wb100", 6.1600f); make_specs.emplace_back("wb1100f", 6.1600f); make_specs.emplace_back("wb110", 6.1600f); make_specs.emplace_back("wb150f", 6.1600f); make_specs.emplace_back("wb2000", 6.0800f); make_specs.emplace_back("wb200f", 6.1600f); make_specs.emplace_back("wb2100", 6.1600f); make_specs.emplace_back("wb210", 6.1600f); make_specs.emplace_back("wb2200f", 6.1600f); make_specs.emplace_back("wb250f", 6.1600f); make_specs.emplace_back("wb30f", 6.1600f); make_specs.emplace_back("wb350f", 6.1600f); make_specs.emplace_back("wb35f", 6.1600f); make_specs.emplace_back("wb5000", 6.0800f); make_specs.emplace_back("wb500", 6.0800f); make_specs.emplace_back("wb50f", 6.1600f); make_specs.emplace_back("wb510", 6.0800f); make_specs.emplace_back("wb5500", 6.1600f); make_specs.emplace_back("wb550", 6.0800f); make_specs.emplace_back("wb560", 6.0800f); make_specs.emplace_back("wb600", 6.1600f); make_specs.emplace_back("wb650", 6.1600f); make_specs.emplace_back("wb660", 6.1600f); make_specs.emplace_back("wb690", 6.0800f); make_specs.emplace_back("wb700", 6.0800f); make_specs.emplace_back("wb750", 6.0800f); make_specs.emplace_back("wb800f", 6.1600f); make_specs.emplace_back("wb850f", 6.1600f); make_specs.emplace_back("wp10", 6.0800f); } { auto& make_specs = specs["sanyo"]; make_specs.reserve(78); make_specs.emplace_back("dscs1", 5.3300f); make_specs.emplace_back("dscs3", 5.3300f); make_specs.emplace_back("dscs4", 5.7500f); make_specs.emplace_back("dscs5", 5.7500f); make_specs.emplace_back("vpca5", 5.7500f); make_specs.emplace_back("vpcaz1", 7.1100f); make_specs.emplace_back("vpcaz3ex", 7.1100f); make_specs.emplace_back("vpce1090", 6.1600f); make_specs.emplace_back("vpce1403", 6.1600f); make_specs.emplace_back("vpce1500tp", 6.0800f); make_specs.emplace_back("vpce890", 5.7500f); make_specs.emplace_back("vpchd1ex", 5.7500f); make_specs.emplace_back("vpcj1ex", 5.3300f); make_specs.emplace_back("vpcj2ex", 5.3300f); make_specs.emplace_back("vpcj4ex", 5.3300f); make_specs.emplace_back("vpcmz1", 7.1100f); make_specs.emplace_back("vpcmz2", 7.1100f); make_specs.emplace_back("vpcs1085", 6.0800f); make_specs.emplace_back("vpcs122", 6.1600f); make_specs.emplace_back("vpcs1275", 6.1600f); make_specs.emplace_back("vpcs1414", 6.0800f); make_specs.emplace_back("vpcs885", 5.7500f); make_specs.emplace_back("vpct1495", 6.0800f); make_specs.emplace_back("vpcx1200", 6.1600f); make_specs.emplace_back("vpcx1220", 6.1600f); make_specs.emplace_back("vpcx1420", 6.1600f); make_specs.emplace_back("xactic1", 5.3300f); make_specs.emplace_back("xactic40", 5.3300f); make_specs.emplace_back("xactic4", 5.3300f); make_specs.emplace_back("xactic5", 5.7500f); make_specs.emplace_back("xactic6", 5.7500f); make_specs.emplace_back("xactidmxca65", 5.7500f); make_specs.emplace_back("xactidmxca8", 5.7500f); make_specs.emplace_back("xactidmxcg65", 5.7500f); make_specs.emplace_back("xactidmxcg9", 6.0800f); make_specs.emplace_back("xactidmxhd700", 5.7500f); make_specs.emplace_back("xactidmxhd800", 5.7500f); make_specs.emplace_back("xactie60", 5.7500f); make_specs.emplace_back("xactie6", 5.7500f); make_specs.emplace_back("xactis50", 5.7500f); make_specs.emplace_back("xactis60", 5.7500f); make_specs.emplace_back("xactis6", 5.7500f); make_specs.emplace_back("xactis70", 5.7500f); make_specs.emplace_back("xactivpc503", 5.7500f); make_specs.emplace_back("xactivpc603", 5.7500f); make_specs.emplace_back("xactivpcca6", 5.7500f); make_specs.emplace_back("xactivpcca9", 5.7500f); make_specs.emplace_back("xactivpccg10", 6.0800f); make_specs.emplace_back("xactivpccg6", 5.7500f); make_specs.emplace_back("xactivpce1075", 6.0800f); make_specs.emplace_back("xactivpce10", 6.1600f); make_specs.emplace_back("xactivpce760", 5.7500f); make_specs.emplace_back("xactivpce7", 5.7500f); make_specs.emplace_back("xactivpce860", 5.7500f); make_specs.emplace_back("xactivpce870", 5.7500f); make_specs.emplace_back("xactivpce875", 5.7500f); make_specs.emplace_back("xactivpchd1a", 5.7500f); make_specs.emplace_back("xactivpchd2000", 5.7500f); make_specs.emplace_back("xactivpchd2", 5.7500f); make_specs.emplace_back("xactivpcs1ex", 5.3300f); make_specs.emplace_back("xactivpcs1070", 6.0800f); make_specs.emplace_back("xactivpcs3ex", 5.3300f); make_specs.emplace_back("xactivpcs4ex", 5.7500f); make_specs.emplace_back("xactivpcs500", 5.7500f); make_specs.emplace_back("xactivpcs600", 5.7500f); make_specs.emplace_back("xactivpcs650", 5.7500f); make_specs.emplace_back("xactivpcs670", 5.7500f); make_specs.emplace_back("xactivpcs750", 5.7500f); make_specs.emplace_back("xactivpcs760", 5.7500f); make_specs.emplace_back("xactivpcs770", 5.7500f); make_specs.emplace_back("xactivpcs7", 5.7500f); make_specs.emplace_back("xactivpcs870", 5.7500f); make_specs.emplace_back("xactivpcs880", 5.7500f); make_specs.emplace_back("xactivpct1060", 6.0800f); make_specs.emplace_back("xactivpct700", 5.7500f); make_specs.emplace_back("xactivpct850", 5.7500f); make_specs.emplace_back("xactivpcw800", 5.7500f); make_specs.emplace_back("xactivpcx1200", 6.1600f); } { auto& make_specs = specs["sigma"]; make_specs.reserve(15); make_specs.emplace_back("dp1merrill", 24.0000f); make_specs.emplace_back("dp1s", 20.7000f); make_specs.emplace_back("dp1x", 20.7000f); make_specs.emplace_back("dp1", 20.7000f); make_specs.emplace_back("dp2merrill", 24.0000f); make_specs.emplace_back("dp2s", 20.7000f); make_specs.emplace_back("dp2x", 20.7000f); make_specs.emplace_back("dp2", 20.7000f); make_specs.emplace_back("dp3merrill", 24.0000f); make_specs.emplace_back("sd1merrill", 24.0000f); make_specs.emplace_back("sd10", 20.7000f); make_specs.emplace_back("sd14", 20.7000f); make_specs.emplace_back("sd15", 20.7000f); make_specs.emplace_back("sd1", 24.0000f); make_specs.emplace_back("sd9", 20.7000f); } { auto& make_specs = specs["sony"]; make_specs.reserve(302); make_specs.emplace_back("a77ii", 23.5000f); make_specs.emplace_back("alpha7ii", 35.8000f); make_specs.emplace_back("alpha7r", 35.9000f); make_specs.emplace_back("alpha7rii", 35.9000f); make_specs.emplace_back("alpha7sii", 35.6000f); make_specs.emplace_back("alpha7s", 35.6000f); make_specs.emplace_back("alpha7", 35.8000f); make_specs.emplace_back("alphaa3000", 23.5000f); make_specs.emplace_back("alphaa5000", 23.5000f); make_specs.emplace_back("alphaa5100", 23.5000f); make_specs.emplace_back("alphaa6000", 23.5000f); make_specs.emplace_back("alphadslra100", 23.6000f); make_specs.emplace_back("alphadslra200", 23.6000f); make_specs.emplace_back("alphadslra230", 23.5000f); make_specs.emplace_back("alphadslra290", 23.5000f); make_specs.emplace_back("alphadslra300", 23.6000f); make_specs.emplace_back("alphadslra330", 23.5000f); make_specs.emplace_back("alphadslra350", 23.6000f); make_specs.emplace_back("alphadslra380", 23.6000f); make_specs.emplace_back("alphadslra390", 23.5000f); make_specs.emplace_back("alphadslra450", 23.4000f); make_specs.emplace_back("alphadslra500", 23.5000f); make_specs.emplace_back("alphadslra550", 23.4000f); make_specs.emplace_back("alphadslra560", 23.5000f); make_specs.emplace_back("alphadslra580", 23.5000f); make_specs.emplace_back("alphadslra700", 23.5000f); make_specs.emplace_back("alphadslra850", 35.9000f); make_specs.emplace_back("alphadslra900", 35.9000f); make_specs.emplace_back("alphanex3n", 23.5000f); make_specs.emplace_back("alphanex3", 23.4000f); make_specs.emplace_back("alphanex5n", 23.4000f); make_specs.emplace_back("alphanex5r", 23.4000f); make_specs.emplace_back("alphanex5t", 23.4000f); make_specs.emplace_back("alphanex5", 23.4000f); make_specs.emplace_back("alphanex7", 23.5000f); make_specs.emplace_back("alphanexc3", 23.4000f); make_specs.emplace_back("alphanexf3", 23.4000f); make_specs.emplace_back("alphanex6", 23.5000f); make_specs.emplace_back("cybershotdscrx1rii", 35.9000f); make_specs.emplace_back("cybershotdscrx1", 35.8000f); make_specs.emplace_back("cybershotdscd700", 6.4000f); make_specs.emplace_back("cybershotdscd770", 6.4000f); make_specs.emplace_back("cybershotdscf505v", 7.1100f); make_specs.emplace_back("cybershotdscf505", 6.4000f); make_specs.emplace_back("cybershotdscf55v", 7.1100f); make_specs.emplace_back("cybershotdscf55", 6.4000f); make_specs.emplace_back("cybershotdscf707", 8.8000f); make_specs.emplace_back("cybershotdscf717", 8.8000f); make_specs.emplace_back("cybershotdscf77", 7.1100f); make_specs.emplace_back("cybershotdscf828", 8.8000f); make_specs.emplace_back("cybershotdscf88", 5.9000f); make_specs.emplace_back("cybershotdscfx77", 7.1100f); make_specs.emplace_back("cybershotdscg1", 5.7500f); make_specs.emplace_back("cybershotdscg3", 6.1600f); make_specs.emplace_back("cybershotdsch100", 6.1600f); make_specs.emplace_back("cybershotdsch10", 5.7500f); make_specs.emplace_back("cybershotdsch1", 5.7500f); make_specs.emplace_back("cybershotdsch200", 6.1600f); make_specs.emplace_back("cybershotdsch20", 6.1600f); make_specs.emplace_back("cybershotdsch2", 5.7500f); make_specs.emplace_back("cybershotdsch300", 6.1600f); make_specs.emplace_back("cybershotdsch3", 5.7500f); make_specs.emplace_back("cybershotdsch400", 6.1600f); make_specs.emplace_back("cybershotdsch50", 6.1600f); make_specs.emplace_back("cybershotdsch55", 6.1600f); make_specs.emplace_back("cybershotdsch5", 5.7500f); make_specs.emplace_back("cybershotdsch70", 6.1600f); make_specs.emplace_back("cybershotdsch7", 5.7500f); make_specs.emplace_back("cybershotdsch90", 6.1600f); make_specs.emplace_back("cybershotdsch9", 5.7500f); make_specs.emplace_back("cybershotdschx100v", 6.1600f); make_specs.emplace_back("cybershotdschx10v", 6.1600f); make_specs.emplace_back("cybershotdschx1", 5.9000f); make_specs.emplace_back("cybershotdschx200v", 6.1600f); make_specs.emplace_back("cybershotdschx20v", 6.1600f); make_specs.emplace_back("cybershotdschx300", 6.1600f); make_specs.emplace_back("cybershotdschx30v", 6.1600f); make_specs.emplace_back("cybershotdschx400", 6.1600f); make_specs.emplace_back("cybershotdschx50", 6.1600f); make_specs.emplace_back("cybershotdschx5", 5.9000f); make_specs.emplace_back("cybershotdschx60", 6.1600f); make_specs.emplace_back("cybershotdschx7v", 6.1600f); make_specs.emplace_back("cybershotdschx90v", 6.1600f); make_specs.emplace_back("cybershotdschx9v", 6.1600f); make_specs.emplace_back("cybershotdscj10", 6.1600f); make_specs.emplace_back("cybershotdscl1", 5.3300f); make_specs.emplace_back("cybershotdscm1", 5.9000f); make_specs.emplace_back("cybershotdscm2", 5.7500f); make_specs.emplace_back("cybershotdscn1", 7.1100f); make_specs.emplace_back("cybershotdscn2", 7.5300f); make_specs.emplace_back("cybershotdscp100", 7.1100f); make_specs.emplace_back("cybershotdscp10", 7.1100f); make_specs.emplace_back("cybershotdscp120", 7.1100f); make_specs.emplace_back("cybershotdscp12", 7.1100f); make_specs.emplace_back("cybershotdscp150", 7.1100f); make_specs.emplace_back("cybershotdscp1", 7.1100f); make_specs.emplace_back("cybershotdscp200", 7.1100f); make_specs.emplace_back("cybershotdscp20", 5.3300f); make_specs.emplace_back("cybershotdscp2", 5.3300f); make_specs.emplace_back("cybershotdscp30", 5.3300f); make_specs.emplace_back("cybershotdscp31", 5.3300f); make_specs.emplace_back("cybershotdscp32", 5.3300f); make_specs.emplace_back("cybershotdscp3", 7.1100f); make_specs.emplace_back("cybershotdscp41", 5.3300f); make_specs.emplace_back("cybershotdscp43", 5.3300f); make_specs.emplace_back("cybershotdscp50", 5.3300f); make_specs.emplace_back("cybershotdscp51", 5.3300f); make_specs.emplace_back("cybershotdscp52", 5.3300f); make_specs.emplace_back("cybershotdscp5", 7.1100f); make_specs.emplace_back("cybershotdscp71", 7.1100f); make_specs.emplace_back("cybershotdscp72", 7.1100f); make_specs.emplace_back("cybershotdscp73", 5.3300f); make_specs.emplace_back("cybershotdscp7", 7.1100f); make_specs.emplace_back("cybershotdscp8", 5.3300f); make_specs.emplace_back("cybershotdscp92", 7.1100f); make_specs.emplace_back("cybershotdscp93", 7.1100f); make_specs.emplace_back("cybershotdscp9", 7.1100f); make_specs.emplace_back("cybershotdscqx100", 13.2000f); make_specs.emplace_back("cybershotdscqx10", 6.1600f); make_specs.emplace_back("cybershotdscr1", 21.5000f); make_specs.emplace_back("cybershotdscrx10ii", 13.2000f); make_specs.emplace_back("cybershotdscrx100iii", 13.2000f); make_specs.emplace_back("cybershotdscrx100ii", 13.2000f); make_specs.emplace_back("cybershotdscrx100iv", 13.2000f); make_specs.emplace_back("cybershotdscrx100", 13.2000f); make_specs.emplace_back("cybershotdscrx10", 13.2000f); make_specs.emplace_back("cybershotdscrx1r", 35.8000f); make_specs.emplace_back("cybershotdscs2000", 6.1600f); make_specs.emplace_back("cybershotdscs2100", 6.1600f); make_specs.emplace_back("cybershotdscs3000", 4.9600f); make_specs.emplace_back("cybershotdscs30", 5.3300f); make_specs.emplace_back("cybershotdscs40", 5.3300f); make_specs.emplace_back("cybershotdscs45", 5.7500f); make_specs.emplace_back("cybershotdscs5000", 6.1600f); make_specs.emplace_back("cybershotdscs50", 5.3300f); make_specs.emplace_back("cybershotdscs600", 5.7500f); make_specs.emplace_back("cybershotdscs60", 5.3300f); make_specs.emplace_back("cybershotdscs650", 5.7500f); make_specs.emplace_back("cybershotdscs700", 5.7500f); make_specs.emplace_back("cybershotdscs70", 7.1100f); make_specs.emplace_back("cybershotdscs730", 5.7500f); make_specs.emplace_back("cybershotdscs750", 5.7500f); make_specs.emplace_back("cybershotdscs75", 7.1100f); make_specs.emplace_back("cybershotdscs780", 5.7500f); make_specs.emplace_back("cybershotdscs800", 7.1100f); make_specs.emplace_back("cybershotdscs80", 5.3300f); make_specs.emplace_back("cybershotdscs85", 7.1100f); make_specs.emplace_back("cybershotdscs90", 5.3300f); make_specs.emplace_back("cybershotdscs930", 6.0800f); make_specs.emplace_back("cybershotdscs950", 6.1600f); make_specs.emplace_back("cybershotdscs980", 6.0800f); make_specs.emplace_back("cybershotdsct100", 5.7500f); make_specs.emplace_back("cybershotdsct10", 5.7500f); make_specs.emplace_back("cybershotdsct110", 6.1600f); make_specs.emplace_back("cybershotdsct11", 5.9000f); make_specs.emplace_back("cybershotdsct1", 5.9000f); make_specs.emplace_back("cybershotdsct200", 5.7500f); make_specs.emplace_back("cybershotdsct20", 5.7500f); make_specs.emplace_back("cybershotdsct2", 5.7500f); make_specs.emplace_back("cybershotdsct300", 6.1600f); make_specs.emplace_back("cybershotdsct30", 5.7500f); make_specs.emplace_back("cybershotdsct33", 5.9000f); make_specs.emplace_back("cybershotdsct3", 5.9000f); make_specs.emplace_back("cybershotdsct500", 6.1600f); make_specs.emplace_back("cybershotdsct50", 5.7500f); make_specs.emplace_back("cybershotdsct5", 5.7500f); make_specs.emplace_back("cybershotdsct700", 6.1600f); make_specs.emplace_back("cybershotdsct70", 5.7500f); make_specs.emplace_back("cybershotdsct77", 6.1600f); make_specs.emplace_back("cybershotdsct7", 5.9000f); make_specs.emplace_back("cybershotdsct900", 6.1600f); make_specs.emplace_back("cybershotdsct90", 6.1600f); make_specs.emplace_back("cybershotdsct99", 6.1600f); make_specs.emplace_back("cybershotdsct9", 5.7500f); make_specs.emplace_back("cybershotdsctf1", 6.1600f); make_specs.emplace_back("cybershotdsctx100v", 6.1600f); make_specs.emplace_back("cybershotdsctx10", 6.1600f); make_specs.emplace_back("cybershotdsctx1", 5.9000f); make_specs.emplace_back("cybershotdsctx200v", 6.1600f); make_specs.emplace_back("cybershotdsctx20", 6.1600f); make_specs.emplace_back("cybershotdsctx30", 6.1600f); make_specs.emplace_back("cybershotdsctx55", 6.1600f); make_specs.emplace_back("cybershotdsctx5", 5.9000f); make_specs.emplace_back("cybershotdsctx66", 6.1600f); make_specs.emplace_back("cybershotdsctx7", 5.9000f); make_specs.emplace_back("cybershotdsctx9", 6.1600f); make_specs.emplace_back("cybershotdscu10", 5.3300f); make_specs.emplace_back("cybershotdscu20", 5.3300f); make_specs.emplace_back("cybershotdscu30", 5.3300f); make_specs.emplace_back("cybershotdscu40", 5.3300f); make_specs.emplace_back("cybershotdscu50", 5.3300f); make_specs.emplace_back("cybershotdscu60", 5.3300f); make_specs.emplace_back("cybershotdscv1", 7.1100f); make_specs.emplace_back("cybershotdscv3", 7.1100f); make_specs.emplace_back("cybershotdscw100", 7.1100f); make_specs.emplace_back("cybershotdscw110", 5.7500f); make_specs.emplace_back("cybershotdscw115", 5.7500f); make_specs.emplace_back("cybershotdscw120", 5.7500f); make_specs.emplace_back("cybershotdscw125", 5.7500f); make_specs.emplace_back("cybershotdscw12", 7.1100f); make_specs.emplace_back("cybershotdscw130", 5.7500f); make_specs.emplace_back("cybershotdscw150", 5.7500f); make_specs.emplace_back("cybershotdscw170", 6.1600f); make_specs.emplace_back("cybershotdscw17", 7.1100f); make_specs.emplace_back("cybershotdscw180", 6.0800f); make_specs.emplace_back("cybershotdscw190", 6.0800f); make_specs.emplace_back("cybershotdscw1", 7.1100f); make_specs.emplace_back("cybershotdscw200", 7.5300f); make_specs.emplace_back("cybershotdscw210", 6.1600f); make_specs.emplace_back("cybershotdscw215", 6.1600f); make_specs.emplace_back("cybershotdscw220", 6.1600f); make_specs.emplace_back("cybershotdscw230", 6.1600f); make_specs.emplace_back("cybershotdscw270", 7.5300f); make_specs.emplace_back("cybershotdscw275", 6.1600f); make_specs.emplace_back("cybershotdscw290", 6.1600f); make_specs.emplace_back("cybershotdscw300", 7.5300f); make_specs.emplace_back("cybershotdscw30", 5.7500f); make_specs.emplace_back("cybershotdscw310", 6.1600f); make_specs.emplace_back("cybershotdscw320", 6.1600f); make_specs.emplace_back("cybershotdscw330", 6.1600f); make_specs.emplace_back("cybershotdscw350", 6.1600f); make_specs.emplace_back("cybershotdscw35", 5.7500f); make_specs.emplace_back("cybershotdscw360", 6.1600f); make_specs.emplace_back("cybershotdscw370", 6.1600f); make_specs.emplace_back("cybershotdscw380", 6.1600f); make_specs.emplace_back("cybershotdscw40", 5.7500f); make_specs.emplace_back("cybershotdscw50", 5.7500f); make_specs.emplace_back("cybershotdscw510", 6.1600f); make_specs.emplace_back("cybershotdscw520", 6.1600f); make_specs.emplace_back("cybershotdscw530", 6.1600f); make_specs.emplace_back("cybershotdscw550", 6.1600f); make_specs.emplace_back("cybershotdscw55", 5.7500f); make_specs.emplace_back("cybershotdscw560", 6.1600f); make_specs.emplace_back("cybershotdscw570", 6.1600f); make_specs.emplace_back("cybershotdscw580", 6.1600f); make_specs.emplace_back("cybershotdscw5", 7.1100f); make_specs.emplace_back("cybershotdscw610", 6.1600f); make_specs.emplace_back("cybershotdscw620", 6.1600f); make_specs.emplace_back("cybershotdscw630", 6.1600f); make_specs.emplace_back("cybershotdscw650", 6.1600f); make_specs.emplace_back("cybershotdscw670", 6.1600f); make_specs.emplace_back("cybershotdscw690", 6.1600f); make_specs.emplace_back("cybershotdscw70", 5.7500f); make_specs.emplace_back("cybershotdscw710", 6.1600f); make_specs.emplace_back("cybershotdscw730", 6.1600f); make_specs.emplace_back("cybershotdscw7", 7.1100f); make_specs.emplace_back("cybershotdscw800", 6.1600f); make_specs.emplace_back("cybershotdscw80", 5.7500f); make_specs.emplace_back("cybershotdscw810", 6.1600f); make_specs.emplace_back("cybershotdscw830", 6.1600f); make_specs.emplace_back("cybershotdscw85", 5.7500f); make_specs.emplace_back("cybershotdscw90", 5.7500f); make_specs.emplace_back("cybershotdscwx100", 6.1600f); make_specs.emplace_back("cybershotdscwx10", 6.1600f); make_specs.emplace_back("cybershotdscwx150", 6.1600f); make_specs.emplace_back("cybershotdscwx1", 5.9000f); make_specs.emplace_back("cybershotdscwx200", 6.1600f); make_specs.emplace_back("cybershotdscwx220", 6.1600f); make_specs.emplace_back("cybershotdscwx300", 6.1600f); make_specs.emplace_back("cybershotdscwx30", 6.1600f); make_specs.emplace_back("cybershotdscwx350", 6.1600f); make_specs.emplace_back("cybershotdscwx500", 6.1600f); make_specs.emplace_back("cybershotdscwx50", 6.1600f); make_specs.emplace_back("cybershotdscwx5", 6.1600f); make_specs.emplace_back("cybershotdscwx60", 6.1600f); make_specs.emplace_back("cybershotdscwx70", 6.1600f); make_specs.emplace_back("cybershotdscwx7", 6.1600f); make_specs.emplace_back("cybershotdscwx80", 6.1600f); make_specs.emplace_back("cybershotdscwx9", 6.1600f); make_specs.emplace_back("mavicacd1000", 5.3300f); make_specs.emplace_back("mavicacd200", 5.3300f); make_specs.emplace_back("mavicacd250", 5.3300f); make_specs.emplace_back("mavicacd300", 7.1100f); make_specs.emplace_back("mavicacd350", 5.3300f); make_specs.emplace_back("mavicacd400", 7.1100f); make_specs.emplace_back("mavicacd500", 7.1100f); make_specs.emplace_back("mavicafd100", 5.3300f); make_specs.emplace_back("mavicafd200", 5.3300f); make_specs.emplace_back("mavicafd71", 6.4000f); make_specs.emplace_back("mavicafd73", 6.4000f); make_specs.emplace_back("mavicafd75", 7.1100f); make_specs.emplace_back("mavicafd81", 4.8000f); make_specs.emplace_back("mavicafd83", 4.8000f); make_specs.emplace_back("mavicafd85", 5.3300f); make_specs.emplace_back("mavicafd87", 5.3300f); make_specs.emplace_back("mavicafd88", 4.8000f); make_specs.emplace_back("mavicafd90", 5.3300f); make_specs.emplace_back("mavicafd91", 4.8000f); make_specs.emplace_back("mavicafd92", 5.3300f); make_specs.emplace_back("mavicafd95", 5.3300f); make_specs.emplace_back("mavicafd97", 5.3300f); make_specs.emplace_back("qx1", 23.5000f); make_specs.emplace_back("qx30", 6.1600f); make_specs.emplace_back("slta33", 23.5000f); make_specs.emplace_back("slta35", 23.5000f); make_specs.emplace_back("slta37", 23.5000f); make_specs.emplace_back("slta55", 23.5000f); make_specs.emplace_back("slta57", 23.5000f); make_specs.emplace_back("slta58", 23.2000f); make_specs.emplace_back("slta65", 23.5000f); make_specs.emplace_back("slta77", 23.5000f); make_specs.emplace_back("slta99", 35.8000f); } { auto& make_specs = specs["toshiba"]; make_specs.reserve(19); make_specs.emplace_back("pdr2300", 5.3300f); make_specs.emplace_back("pdr3300", 7.1100f); make_specs.emplace_back("pdr3310", 7.1100f); make_specs.emplace_back("pdr3320", 7.1100f); make_specs.emplace_back("pdr4300", 7.1100f); make_specs.emplace_back("pdr5300", 7.1100f); make_specs.emplace_back("pdrm25", 5.3300f); make_specs.emplace_back("pdrm500", 5.3300f); make_specs.emplace_back("pdrm5", 6.4000f); make_specs.emplace_back("pdrm60", 6.4000f); make_specs.emplace_back("pdrm61", 6.4000f); make_specs.emplace_back("pdrm65", 6.4000f); make_specs.emplace_back("pdrm700", 5.3300f); make_specs.emplace_back("pdrm70", 7.1100f); make_specs.emplace_back("pdrm71", 7.1100f); make_specs.emplace_back("pdrm81", 7.1100f); make_specs.emplace_back("pdrt10", 5.3300f); make_specs.emplace_back("pdrt20", 5.3300f); make_specs.emplace_back("pdrt30", 5.3300f); } { auto& make_specs = specs["vivitar"]; make_specs.reserve(30); make_specs.emplace_back("v8025", 7.1100f); make_specs.emplace_back("vivicam5105s", 5.7500f); make_specs.emplace_back("vivicam5150s", 5.7500f); make_specs.emplace_back("vivicam5160s", 5.7500f); make_specs.emplace_back("vivicam5195", 5.7500f); make_specs.emplace_back("vivicam5350s", 5.7500f); make_specs.emplace_back("vivicam5355", 5.7500f); make_specs.emplace_back("vivicam5385", 5.7500f); make_specs.emplace_back("vivicam5386", 5.7500f); make_specs.emplace_back("vivicam5388", 5.7500f); make_specs.emplace_back("vivicam6150s", 5.7500f); make_specs.emplace_back("vivicam6200w", 5.7500f); make_specs.emplace_back("vivicam6300", 5.7500f); make_specs.emplace_back("vivicam6320", 5.7500f); make_specs.emplace_back("vivicam6326", 5.7500f); make_specs.emplace_back("vivicam6330", 5.7500f); make_specs.emplace_back("vivicam6380u", 5.7500f); make_specs.emplace_back("vivicam6385u", 5.7500f); make_specs.emplace_back("vivicam6388s", 5.7500f); make_specs.emplace_back("vivicam7100s", 5.7500f); make_specs.emplace_back("vivicam7310", 5.7500f); make_specs.emplace_back("vivicam7388s", 5.7500f); make_specs.emplace_back("vivicam7500i", 5.7500f); make_specs.emplace_back("vivicam8300s", 7.1100f); make_specs.emplace_back("vivicam8400", 7.1100f); make_specs.emplace_back("vivicam8600s", 7.1100f); make_specs.emplace_back("vivicam8600", 7.1100f); make_specs.emplace_back("vivicam8625", 7.1100f); make_specs.emplace_back("vivicamx30", 7.1100f); make_specs.emplace_back("vivicamx60", 7.1100f); } { auto& make_specs = specs["yakumo"]; make_specs.reserve(25); make_specs.emplace_back("cammastersd432", 5.3300f); make_specs.emplace_back("cammastersd482", 5.7500f); make_specs.emplace_back("megaimage34", 5.3300f); make_specs.emplace_back("megaimage35", 7.1100f); make_specs.emplace_back("megaimage37", 5.7500f); make_specs.emplace_back("megaimage410", 5.7500f); make_specs.emplace_back("megaimage45", 7.1100f); make_specs.emplace_back("megaimage47sl", 5.7500f); make_specs.emplace_back("megaimage47sx", 7.1100f); make_specs.emplace_back("megaimage47", 5.7500f); make_specs.emplace_back("megaimage55cx", 7.1100f); make_specs.emplace_back("megaimage57x", 7.1100f); make_specs.emplace_back("megaimage57", 7.1100f); make_specs.emplace_back("megaimage610x", 7.1100f); make_specs.emplace_back("megaimage67x", 7.1100f); make_specs.emplace_back("megaimage811x", 7.1100f); make_specs.emplace_back("megaimage84d", 5.7500f); make_specs.emplace_back("megaimage85d", 5.7500f); make_specs.emplace_back("megaimageii", 7.1100f); make_specs.emplace_back("megaimageiv", 7.1100f); make_specs.emplace_back("megaimagevii", 6.4000f); make_specs.emplace_back("megaimagevi", 7.1100f); make_specs.emplace_back("megaimagexl", 4.2300f); make_specs.emplace_back("megaimagexs", 6.4000f); make_specs.emplace_back("megaimagex", 7.1100f); } return specs; } } // namespace colmap colmap-3.9.1/src/colmap/sensor/specs.h000066400000000000000000000037211454702036400176500ustar00rootroot00000000000000// 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. #pragma once #include #include #include namespace colmap { // { make1 : ({ model1 : sensor-width in mm }, ...), ... } typedef std::vector> camera_make_specs_t; typedef std::unordered_map camera_specs_t; camera_specs_t InitializeCameraSpecs(); } // namespace colmap colmap-3.9.1/src/colmap/sfm/000077500000000000000000000000001454702036400156335ustar00rootroot00000000000000colmap-3.9.1/src/colmap/sfm/CMakeLists.txt000066400000000000000000000036111454702036400203740ustar00rootroot00000000000000# 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. set(FOLDER_NAME "sfm") COLMAP_ADD_LIBRARY( NAME colmap_sfm SRCS incremental_mapper.h incremental_mapper.cc incremental_triangulator.h incremental_triangulator.cc PUBLIC_LINK_LIBS colmap_scene colmap_estimators PRIVATE_LINK_LIBS colmap_geometry colmap_image ) colmap-3.9.1/src/colmap/sfm/incremental_mapper.cc000066400000000000000000001260541454702036400220170ustar00rootroot00000000000000// 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. #include "colmap/sfm/incremental_mapper.h" #include "colmap/estimators/pose.h" #include "colmap/estimators/two_view_geometry.h" #include "colmap/geometry/triangulation.h" #include "colmap/scene/projection.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/misc.h" #include #include namespace colmap { namespace { void SortAndAppendNextImages(std::vector> image_ranks, std::vector* sorted_images_ids) { std::sort(image_ranks.begin(), image_ranks.end(), [](const std::pair& image1, const std::pair& image2) { return image1.second > image2.second; }); sorted_images_ids->reserve(sorted_images_ids->size() + image_ranks.size()); for (const auto& image : image_ranks) { sorted_images_ids->push_back(image.first); } image_ranks.clear(); } float RankNextImageMaxVisiblePointsNum(const Image& image) { return static_cast(image.NumVisiblePoints3D()); } float RankNextImageMaxVisiblePointsRatio(const Image& image) { return static_cast(image.NumVisiblePoints3D()) / static_cast(image.NumObservations()); } float RankNextImageMinUncertainty(const Image& image) { return static_cast(image.Point3DVisibilityScore()); } } // namespace bool IncrementalMapper::Options::Check() const { CHECK_OPTION_GT(init_min_num_inliers, 0); CHECK_OPTION_GT(init_max_error, 0.0); CHECK_OPTION_GE(init_max_forward_motion, 0.0); CHECK_OPTION_LE(init_max_forward_motion, 1.0); CHECK_OPTION_GE(init_min_tri_angle, 0.0); CHECK_OPTION_GE(init_max_reg_trials, 1); CHECK_OPTION_GT(abs_pose_max_error, 0.0); CHECK_OPTION_GT(abs_pose_min_num_inliers, 0); CHECK_OPTION_GE(abs_pose_min_inlier_ratio, 0.0); CHECK_OPTION_LE(abs_pose_min_inlier_ratio, 1.0); CHECK_OPTION_GE(local_ba_num_images, 2); CHECK_OPTION_GE(local_ba_min_tri_angle, 0.0); CHECK_OPTION_GE(min_focal_length_ratio, 0.0); CHECK_OPTION_GE(max_focal_length_ratio, min_focal_length_ratio); CHECK_OPTION_GE(max_extra_param, 0.0); CHECK_OPTION_GE(filter_max_reproj_error, 0.0); CHECK_OPTION_GE(filter_min_tri_angle, 0.0); CHECK_OPTION_GE(max_reg_trials, 1); return true; } IncrementalMapper::IncrementalMapper( std::shared_ptr database_cache) : database_cache_(std::move(database_cache)), reconstruction_(nullptr), triangulator_(nullptr), num_total_reg_images_(0), num_shared_reg_images_(0), prev_init_image_pair_id_(kInvalidImagePairId) {} void IncrementalMapper::BeginReconstruction( const std::shared_ptr& reconstruction) { CHECK(reconstruction_ == nullptr); reconstruction_ = reconstruction; reconstruction_->Load(*database_cache_); reconstruction_->SetUp(database_cache_->CorrespondenceGraph()); triangulator_ = std::make_unique( database_cache_->CorrespondenceGraph(), reconstruction); num_shared_reg_images_ = 0; num_reg_images_per_camera_.clear(); for (const image_t image_id : reconstruction_->RegImageIds()) { RegisterImageEvent(image_id); } existing_image_ids_ = std::unordered_set(reconstruction->RegImageIds().begin(), reconstruction->RegImageIds().end()); prev_init_image_pair_id_ = kInvalidImagePairId; prev_init_two_view_geometry_ = TwoViewGeometry(); filtered_images_.clear(); num_reg_trials_.clear(); } void IncrementalMapper::EndReconstruction(const bool discard) { CHECK_NOTNULL(reconstruction_); if (discard) { for (const image_t image_id : reconstruction_->RegImageIds()) { DeRegisterImageEvent(image_id); } } reconstruction_->TearDown(); reconstruction_ = nullptr; triangulator_.reset(); } bool IncrementalMapper::FindInitialImagePair(const Options& options, image_t* image_id1, image_t* image_id2) { CHECK(options.Check()); std::vector image_ids1; if (*image_id1 != kInvalidImageId && *image_id2 == kInvalidImageId) { // Only *image_id1 provided. if (!database_cache_->ExistsImage(*image_id1)) { return false; } image_ids1.push_back(*image_id1); } else if (*image_id1 == kInvalidImageId && *image_id2 != kInvalidImageId) { // Only *image_id2 provided. if (!database_cache_->ExistsImage(*image_id2)) { return false; } image_ids1.push_back(*image_id2); } else { // No initial seed image provided. image_ids1 = FindFirstInitialImage(options); } // Try to find good initial pair. for (size_t i1 = 0; i1 < image_ids1.size(); ++i1) { *image_id1 = image_ids1[i1]; const std::vector image_ids2 = FindSecondInitialImage(options, *image_id1); for (size_t i2 = 0; i2 < image_ids2.size(); ++i2) { *image_id2 = image_ids2[i2]; const image_pair_t pair_id = Database::ImagePairToPairId(*image_id1, *image_id2); // Try every pair only once. if (init_image_pairs_.count(pair_id) > 0) { continue; } init_image_pairs_.insert(pair_id); if (EstimateInitialTwoViewGeometry(options, *image_id1, *image_id2)) { return true; } } } // No suitable pair found in entire dataset. *image_id1 = kInvalidImageId; *image_id2 = kInvalidImageId; return false; } std::vector IncrementalMapper::FindNextImages(const Options& options) { CHECK_NOTNULL(reconstruction_); CHECK(options.Check()); std::function rank_image_func; switch (options.image_selection_method) { case Options::ImageSelectionMethod::MAX_VISIBLE_POINTS_NUM: rank_image_func = RankNextImageMaxVisiblePointsNum; break; case Options::ImageSelectionMethod::MAX_VISIBLE_POINTS_RATIO: rank_image_func = RankNextImageMaxVisiblePointsRatio; break; case Options::ImageSelectionMethod::MIN_UNCERTAINTY: rank_image_func = RankNextImageMinUncertainty; break; } std::vector> image_ranks; std::vector> other_image_ranks; // Append images that have not failed to register before. for (const auto& image : reconstruction_->Images()) { // Skip images that are already registered. if (image.second.IsRegistered()) { continue; } // Only consider images with a sufficient number of visible points. if (image.second.NumVisiblePoints3D() < static_cast(options.abs_pose_min_num_inliers)) { continue; } // Only try registration for a certain maximum number of times. const size_t num_reg_trials = num_reg_trials_[image.first]; if (num_reg_trials >= static_cast(options.max_reg_trials)) { continue; } // If image has been filtered or failed to register, place it in the // second bucket and prefer images that have not been tried before. const float rank = rank_image_func(image.second); if (filtered_images_.count(image.first) == 0 && num_reg_trials == 0) { image_ranks.emplace_back(image.first, rank); } else { other_image_ranks.emplace_back(image.first, rank); } } std::vector ranked_images_ids; SortAndAppendNextImages(image_ranks, &ranked_images_ids); SortAndAppendNextImages(other_image_ranks, &ranked_images_ids); return ranked_images_ids; } bool IncrementalMapper::RegisterInitialImagePair(const Options& options, const image_t image_id1, const image_t image_id2) { CHECK_NOTNULL(reconstruction_); CHECK_EQ(reconstruction_->NumRegImages(), 0); CHECK(options.Check()); init_num_reg_trials_[image_id1] += 1; init_num_reg_trials_[image_id2] += 1; num_reg_trials_[image_id1] += 1; num_reg_trials_[image_id2] += 1; const image_pair_t pair_id = Database::ImagePairToPairId(image_id1, image_id2); init_image_pairs_.insert(pair_id); Image& image1 = reconstruction_->Image(image_id1); const Camera& camera1 = reconstruction_->Camera(image1.CameraId()); Image& image2 = reconstruction_->Image(image_id2); const Camera& camera2 = reconstruction_->Camera(image2.CameraId()); ////////////////////////////////////////////////////////////////////////////// // Estimate two-view geometry ////////////////////////////////////////////////////////////////////////////// if (!EstimateInitialTwoViewGeometry(options, image_id1, image_id2)) { return false; } image1.CamFromWorld() = Rigid3d(); image2.CamFromWorld() = prev_init_two_view_geometry_.cam2_from_cam1; const Eigen::Matrix3x4d cam_from_world1 = image1.CamFromWorld().ToMatrix(); const Eigen::Matrix3x4d cam_from_world2 = image2.CamFromWorld().ToMatrix(); const Eigen::Vector3d proj_center1 = image1.ProjectionCenter(); const Eigen::Vector3d proj_center2 = image2.ProjectionCenter(); ////////////////////////////////////////////////////////////////////////////// // Update Reconstruction ////////////////////////////////////////////////////////////////////////////// reconstruction_->RegisterImage(image_id1); reconstruction_->RegisterImage(image_id2); RegisterImageEvent(image_id1); RegisterImageEvent(image_id2); const FeatureMatches& corrs = database_cache_->CorrespondenceGraph()->FindCorrespondencesBetweenImages( image_id1, image_id2); const double min_tri_angle_rad = DegToRad(options.init_min_tri_angle); // Add 3D point tracks. Track track; track.Reserve(2); track.AddElement(TrackElement()); track.AddElement(TrackElement()); track.Element(0).image_id = image_id1; track.Element(1).image_id = image_id2; for (const auto& corr : corrs) { const Eigen::Vector2d point2D1 = camera1.CamFromImg(image1.Point2D(corr.point2D_idx1).xy); const Eigen::Vector2d point2D2 = camera2.CamFromImg(image2.Point2D(corr.point2D_idx2).xy); const Eigen::Vector3d& xyz = TriangulatePoint(cam_from_world1, cam_from_world2, point2D1, point2D2); const double tri_angle = CalculateTriangulationAngle(proj_center1, proj_center2, xyz); if (tri_angle >= min_tri_angle_rad && HasPointPositiveDepth(cam_from_world1, xyz) && HasPointPositiveDepth(cam_from_world2, xyz)) { track.Element(0).point2D_idx = corr.point2D_idx1; track.Element(1).point2D_idx = corr.point2D_idx2; reconstruction_->AddPoint3D(xyz, track); } } return true; } bool IncrementalMapper::RegisterNextImage(const Options& options, const image_t image_id) { CHECK_NOTNULL(reconstruction_); CHECK_GE(reconstruction_->NumRegImages(), 2); CHECK(options.Check()); Image& image = reconstruction_->Image(image_id); Camera& camera = reconstruction_->Camera(image.CameraId()); CHECK(!image.IsRegistered()) << "Image cannot be registered multiple times"; num_reg_trials_[image_id] += 1; // Check if enough 2D-3D correspondences. if (image.NumVisiblePoints3D() < static_cast(options.abs_pose_min_num_inliers)) { return false; } ////////////////////////////////////////////////////////////////////////////// // Search for 2D-3D correspondences ////////////////////////////////////////////////////////////////////////////// std::vector> tri_corrs; std::vector tri_points2D; std::vector tri_points3D; const std::shared_ptr correspondence_graph = database_cache_->CorrespondenceGraph(); std::unordered_set corr_point3D_ids; for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { const Point2D& point2D = image.Point2D(point2D_idx); corr_point3D_ids.clear(); const auto corr_range = correspondence_graph->FindCorrespondences(image_id, point2D_idx); for (const auto* corr = corr_range.beg; corr < corr_range.end; ++corr) { const Image& corr_image = reconstruction_->Image(corr->image_id); if (!corr_image.IsRegistered()) { continue; } const Point2D& corr_point2D = corr_image.Point2D(corr->point2D_idx); if (!corr_point2D.HasPoint3D()) { continue; } // Avoid duplicate correspondences. if (corr_point3D_ids.count(corr_point2D.point3D_id) > 0) { continue; } const Camera& corr_camera = reconstruction_->Camera(corr_image.CameraId()); // Avoid correspondences to images with bogus camera parameters. if (corr_camera.HasBogusParams(options.min_focal_length_ratio, options.max_focal_length_ratio, options.max_extra_param)) { continue; } const Point3D& point3D = reconstruction_->Point3D(corr_point2D.point3D_id); tri_corrs.emplace_back(point2D_idx, corr_point2D.point3D_id); corr_point3D_ids.insert(corr_point2D.point3D_id); tri_points2D.push_back(point2D.xy); tri_points3D.push_back(point3D.xyz); } } // The size of `next_image.num_tri_obs` and `tri_corrs_point2D_idxs.size()` // can only differ, when there are images with bogus camera parameters, and // hence we skip some of the 2D-3D correspondences. if (tri_points2D.size() < static_cast(options.abs_pose_min_num_inliers)) { return false; } ////////////////////////////////////////////////////////////////////////////// // 2D-3D estimation ////////////////////////////////////////////////////////////////////////////// // Only refine / estimate focal length, if no focal length was specified // (manually or through EXIF) and if it was not already estimated previously // from another image (when multiple images share the same camera // parameters) AbsolutePoseEstimationOptions abs_pose_options; abs_pose_options.num_threads = options.num_threads; abs_pose_options.num_focal_length_samples = 30; abs_pose_options.min_focal_length_ratio = options.min_focal_length_ratio; abs_pose_options.max_focal_length_ratio = options.max_focal_length_ratio; abs_pose_options.ransac_options.max_error = options.abs_pose_max_error; abs_pose_options.ransac_options.min_inlier_ratio = options.abs_pose_min_inlier_ratio; // Use high confidence to avoid preemptive termination of P3P RANSAC // - too early termination may lead to bad registration. abs_pose_options.ransac_options.min_num_trials = 100; abs_pose_options.ransac_options.max_num_trials = 10000; abs_pose_options.ransac_options.confidence = 0.99999; AbsolutePoseRefinementOptions abs_pose_refinement_options; if (num_reg_images_per_camera_[image.CameraId()] > 0) { // Camera already refined from another image with the same camera. if (camera.HasBogusParams(options.min_focal_length_ratio, options.max_focal_length_ratio, options.max_extra_param)) { // Previously refined camera has bogus parameters, // so reset parameters and try to re-estimage. camera.params = database_cache_->Camera(image.CameraId()).params; abs_pose_options.estimate_focal_length = !camera.has_prior_focal_length; abs_pose_refinement_options.refine_focal_length = true; abs_pose_refinement_options.refine_extra_params = true; } else { abs_pose_options.estimate_focal_length = false; abs_pose_refinement_options.refine_focal_length = false; abs_pose_refinement_options.refine_extra_params = false; } } else { // Camera not refined before. Note that the camera parameters might have // been changed before but the image was filtered, so we explicitly reset // the camera parameters and try to re-estimate them. camera.params = database_cache_->Camera(image.CameraId()).params; abs_pose_options.estimate_focal_length = !camera.has_prior_focal_length; abs_pose_refinement_options.refine_focal_length = true; abs_pose_refinement_options.refine_extra_params = true; } if (!options.abs_pose_refine_focal_length) { abs_pose_options.estimate_focal_length = false; abs_pose_refinement_options.refine_focal_length = false; } if (!options.abs_pose_refine_extra_params) { abs_pose_refinement_options.refine_extra_params = false; } size_t num_inliers; std::vector inlier_mask; if (!EstimateAbsolutePose(abs_pose_options, tri_points2D, tri_points3D, &image.CamFromWorld(), &camera, &num_inliers, &inlier_mask)) { return false; } if (num_inliers < static_cast(options.abs_pose_min_num_inliers)) { return false; } ////////////////////////////////////////////////////////////////////////////// // Pose refinement ////////////////////////////////////////////////////////////////////////////// if (!RefineAbsolutePose(abs_pose_refinement_options, inlier_mask, tri_points2D, tri_points3D, &image.CamFromWorld(), &camera)) { return false; } ////////////////////////////////////////////////////////////////////////////// // Continue tracks ////////////////////////////////////////////////////////////////////////////// reconstruction_->RegisterImage(image_id); RegisterImageEvent(image_id); for (size_t i = 0; i < inlier_mask.size(); ++i) { if (inlier_mask[i]) { const point2D_t point2D_idx = tri_corrs[i].first; const Point2D& point2D = image.Point2D(point2D_idx); if (!point2D.HasPoint3D()) { const point3D_t point3D_id = tri_corrs[i].second; const TrackElement track_el(image_id, point2D_idx); reconstruction_->AddObservation(point3D_id, track_el); triangulator_->AddModifiedPoint3D(point3D_id); } } } return true; } size_t IncrementalMapper::TriangulateImage( const IncrementalTriangulator::Options& tri_options, const image_t image_id) { CHECK_NOTNULL(reconstruction_); return triangulator_->TriangulateImage(tri_options, image_id); } size_t IncrementalMapper::Retriangulate( const IncrementalTriangulator::Options& tri_options) { CHECK_NOTNULL(reconstruction_); return triangulator_->Retriangulate(tri_options); } size_t IncrementalMapper::CompleteTracks( const IncrementalTriangulator::Options& tri_options) { CHECK_NOTNULL(reconstruction_); return triangulator_->CompleteAllTracks(tri_options); } size_t IncrementalMapper::MergeTracks( const IncrementalTriangulator::Options& tri_options) { CHECK_NOTNULL(reconstruction_); return triangulator_->MergeAllTracks(tri_options); } IncrementalMapper::LocalBundleAdjustmentReport IncrementalMapper::AdjustLocalBundle( const Options& options, const BundleAdjustmentOptions& ba_options, const IncrementalTriangulator::Options& tri_options, const image_t image_id, const std::unordered_set& point3D_ids) { CHECK_NOTNULL(reconstruction_); CHECK(options.Check()); LocalBundleAdjustmentReport report; // Find images that have most 3D points with given image in common. const std::vector local_bundle = FindLocalBundle(options, image_id); // Do the bundle adjustment only if there is any connected images. if (local_bundle.size() > 0) { BundleAdjustmentConfig ba_config; ba_config.AddImage(image_id); for (const image_t local_image_id : local_bundle) { ba_config.AddImage(local_image_id); } // Fix the existing images, if option specified. if (options.fix_existing_images) { for (const image_t local_image_id : local_bundle) { if (existing_image_ids_.count(local_image_id)) { ba_config.SetConstantCamPose(local_image_id); } } } // Determine which cameras to fix, when not all the registered images // are within the current local bundle. std::unordered_map num_images_per_camera; for (const image_t image_id : ba_config.Images()) { const Image& image = reconstruction_->Image(image_id); num_images_per_camera[image.CameraId()] += 1; } for (const auto& camera_id_and_num_images_pair : num_images_per_camera) { const size_t num_reg_images_for_camera = num_reg_images_per_camera_.at(camera_id_and_num_images_pair.first); if (camera_id_and_num_images_pair.second < num_reg_images_for_camera) { ba_config.SetConstantCamIntrinsics(camera_id_and_num_images_pair.first); } } // Fix 7 DOF to avoid scale/rotation/translation drift in bundle adjustment. if (local_bundle.size() == 1) { ba_config.SetConstantCamPose(local_bundle[0]); ba_config.SetConstantCamPositions(image_id, {0}); } else if (local_bundle.size() > 1) { const image_t image_id1 = local_bundle[local_bundle.size() - 1]; const image_t image_id2 = local_bundle[local_bundle.size() - 2]; ba_config.SetConstantCamPose(image_id1); if (!options.fix_existing_images || !existing_image_ids_.count(image_id2)) { ba_config.SetConstantCamPositions(image_id2, {0}); } } // Make sure, we refine all new and short-track 3D points, no matter if // they are fully contained in the local image set or not. Do not include // long track 3D points as they are usually already very stable and adding // to them to bundle adjustment and track merging/completion would slow // down the local bundle adjustment significantly. std::unordered_set variable_point3D_ids; for (const point3D_t point3D_id : point3D_ids) { const Point3D& point3D = reconstruction_->Point3D(point3D_id); const size_t kMaxTrackLength = 15; if (!point3D.HasError() || point3D.track.Length() <= kMaxTrackLength) { ba_config.AddVariablePoint(point3D_id); variable_point3D_ids.insert(point3D_id); } } // Adjust the local bundle. BundleAdjuster bundle_adjuster(ba_options, ba_config); bundle_adjuster.Solve(reconstruction_.get()); report.num_adjusted_observations = bundle_adjuster.Summary().num_residuals / 2; // Merge refined tracks with other existing points. report.num_merged_observations = triangulator_->MergeTracks(tri_options, variable_point3D_ids); // Complete tracks that may have failed to triangulate before refinement // of camera pose and calibration in bundle-adjustment. This may avoid // that some points are filtered and it helps for subsequent image // registrations. report.num_completed_observations = triangulator_->CompleteTracks(tri_options, variable_point3D_ids); report.num_completed_observations += triangulator_->CompleteImage(tri_options, image_id); } // Filter both the modified images and all changed 3D points to make sure // there are no outlier points in the model. This results in duplicate work as // many of the provided 3D points may also be contained in the adjusted // images, but the filtering is not a bottleneck at this point. std::unordered_set filter_image_ids; filter_image_ids.insert(image_id); filter_image_ids.insert(local_bundle.begin(), local_bundle.end()); report.num_filtered_observations = reconstruction_->FilterPoints3DInImages(options.filter_max_reproj_error, options.filter_min_tri_angle, filter_image_ids); report.num_filtered_observations += reconstruction_->FilterPoints3D(options.filter_max_reproj_error, options.filter_min_tri_angle, point3D_ids); return report; } bool IncrementalMapper::AdjustGlobalBundle( const Options& options, const BundleAdjustmentOptions& ba_options) { CHECK_NOTNULL(reconstruction_); const std::vector& reg_image_ids = reconstruction_->RegImageIds(); CHECK_GE(reg_image_ids.size(), 2) << "At least two images must be " "registered for global " "bundle-adjustment"; // Avoid degeneracies in bundle adjustment. reconstruction_->FilterObservationsWithNegativeDepth(); // Configure bundle adjustment. BundleAdjustmentConfig ba_config; for (const image_t image_id : reg_image_ids) { ba_config.AddImage(image_id); } // Fix the existing images, if option specified. if (options.fix_existing_images) { for (const image_t image_id : reg_image_ids) { if (existing_image_ids_.count(image_id)) { ba_config.SetConstantCamPose(image_id); } } } // Fix 7-DOFs of the bundle adjustment problem. ba_config.SetConstantCamPose(reg_image_ids[0]); if (!options.fix_existing_images || !existing_image_ids_.count(reg_image_ids[1])) { ba_config.SetConstantCamPositions(reg_image_ids[1], {0}); } // Run bundle adjustment. BundleAdjuster bundle_adjuster(ba_options, ba_config); if (!bundle_adjuster.Solve(reconstruction_.get())) { return false; } // Normalize scene for numerical stability and // to avoid large scale changes in viewer. reconstruction_->Normalize(); return true; } size_t IncrementalMapper::FilterImages(const Options& options) { CHECK_NOTNULL(reconstruction_); CHECK(options.Check()); // Do not filter images in the early stage of the reconstruction, since the // calibration is often still refining a lot. Hence, the camera parameters // are not stable in the beginning. const size_t kMinNumImages = 20; if (reconstruction_->NumRegImages() < kMinNumImages) { return {}; } const std::vector image_ids = reconstruction_->FilterImages(options.min_focal_length_ratio, options.max_focal_length_ratio, options.max_extra_param); for (const image_t image_id : image_ids) { DeRegisterImageEvent(image_id); filtered_images_.insert(image_id); } return image_ids.size(); } size_t IncrementalMapper::FilterPoints(const Options& options) { CHECK_NOTNULL(reconstruction_); CHECK(options.Check()); return reconstruction_->FilterAllPoints3D(options.filter_max_reproj_error, options.filter_min_tri_angle); } const Reconstruction& IncrementalMapper::GetReconstruction() const { CHECK_NOTNULL(reconstruction_); return *reconstruction_; } size_t IncrementalMapper::NumTotalRegImages() const { return num_total_reg_images_; } size_t IncrementalMapper::NumSharedRegImages() const { return num_shared_reg_images_; } const std::unordered_set& IncrementalMapper::GetModifiedPoints3D() { return triangulator_->GetModifiedPoints3D(); } void IncrementalMapper::ClearModifiedPoints3D() { triangulator_->ClearModifiedPoints3D(); } std::vector IncrementalMapper::FindFirstInitialImage( const Options& options) const { // Struct to hold meta-data for ranking images. struct ImageInfo { image_t image_id; bool prior_focal_length; image_t num_correspondences; }; const size_t init_max_reg_trials = static_cast(options.init_max_reg_trials); // Collect information of all not yet registered images with // correspondences. std::vector image_infos; image_infos.reserve(reconstruction_->NumImages()); for (const auto& image : reconstruction_->Images()) { // Only images with correspondences can be registered. if (image.second.NumCorrespondences() == 0) { continue; } // Only use images for initialization a maximum number of times. if (init_num_reg_trials_.count(image.first) && init_num_reg_trials_.at(image.first) >= init_max_reg_trials) { continue; } // Only use images for initialization that are not registered in any // of the other reconstructions. if (num_registrations_.count(image.first) > 0 && num_registrations_.at(image.first) > 0) { continue; } const struct Camera& camera = reconstruction_->Camera(image.second.CameraId()); ImageInfo image_info; image_info.image_id = image.first; image_info.prior_focal_length = camera.has_prior_focal_length; image_info.num_correspondences = image.second.NumCorrespondences(); image_infos.push_back(image_info); } // Sort images such that images with a prior focal length and more // correspondences are preferred, i.e. they appear in the front of the list. std::sort( image_infos.begin(), image_infos.end(), [](const ImageInfo& image_info1, const ImageInfo& image_info2) { if (image_info1.prior_focal_length && !image_info2.prior_focal_length) { return true; } else if (!image_info1.prior_focal_length && image_info2.prior_focal_length) { return false; } else { return image_info1.num_correspondences > image_info2.num_correspondences; } }); // Extract image identifiers in sorted order. std::vector image_ids; image_ids.reserve(image_infos.size()); for (const ImageInfo& image_info : image_infos) { image_ids.push_back(image_info.image_id); } return image_ids; } std::vector IncrementalMapper::FindSecondInitialImage( const Options& options, const image_t image_id1) const { const std::shared_ptr correspondence_graph = database_cache_->CorrespondenceGraph(); // Collect images that are connected to the first seed image and have // not been registered before in other reconstructions. const class Image& image1 = reconstruction_->Image(image_id1); std::unordered_map num_correspondences; for (point2D_t point2D_idx = 0; point2D_idx < image1.NumPoints2D(); ++point2D_idx) { const auto corr_range = correspondence_graph->FindCorrespondences(image_id1, point2D_idx); for (const auto* corr = corr_range.beg; corr < corr_range.end; ++corr) { if (num_registrations_.count(corr->image_id) == 0 || num_registrations_.at(corr->image_id) == 0) { num_correspondences[corr->image_id] += 1; } } } // Struct to hold meta-data for ranking images. struct ImageInfo { image_t image_id; bool prior_focal_length; point2D_t num_correspondences; }; const size_t init_min_num_inliers = static_cast(options.init_min_num_inliers); // Compose image information in a compact form for sorting. std::vector image_infos; image_infos.reserve(reconstruction_->NumImages()); for (const auto elem : num_correspondences) { if (elem.second >= init_min_num_inliers) { const class Image& image = reconstruction_->Image(elem.first); const struct Camera& camera = reconstruction_->Camera(image.CameraId()); ImageInfo image_info; image_info.image_id = elem.first; image_info.prior_focal_length = camera.has_prior_focal_length; image_info.num_correspondences = elem.second; image_infos.push_back(image_info); } } // Sort images such that images with a prior focal length and more // correspondences are preferred, i.e. they appear in the front of the list. std::sort( image_infos.begin(), image_infos.end(), [](const ImageInfo& image_info1, const ImageInfo& image_info2) { if (image_info1.prior_focal_length && !image_info2.prior_focal_length) { return true; } else if (!image_info1.prior_focal_length && image_info2.prior_focal_length) { return false; } else { return image_info1.num_correspondences > image_info2.num_correspondences; } }); // Extract image identifiers in sorted order. std::vector image_ids; image_ids.reserve(image_infos.size()); for (const ImageInfo& image_info : image_infos) { image_ids.push_back(image_info.image_id); } return image_ids; } std::vector IncrementalMapper::FindLocalBundle( const Options& options, const image_t image_id) const { CHECK(options.Check()); const Image& image = reconstruction_->Image(image_id); CHECK(image.IsRegistered()); // Extract all images that have at least one 3D point with the query image // in common, and simultaneously count the number of common 3D points. std::unordered_map shared_observations; std::unordered_set point3D_ids; point3D_ids.reserve(image.NumPoints3D()); for (const Point2D& point2D : image.Points2D()) { if (point2D.HasPoint3D()) { point3D_ids.insert(point2D.point3D_id); const Point3D& point3D = reconstruction_->Point3D(point2D.point3D_id); for (const TrackElement& track_el : point3D.track.Elements()) { if (track_el.image_id != image_id) { shared_observations[track_el.image_id] += 1; } } } } // Sort overlapping images according to number of shared observations. std::vector> overlapping_images( shared_observations.begin(), shared_observations.end()); std::sort(overlapping_images.begin(), overlapping_images.end(), [](const std::pair& image1, const std::pair& image2) { return image1.second > image2.second; }); // The local bundle is composed of the given image and its most connected // neighbor images, hence the subtraction of 1. const size_t num_images = static_cast(options.local_ba_num_images - 1); const size_t num_eff_images = std::min(num_images, overlapping_images.size()); // Extract most connected images and ensure sufficient triangulation angle. std::vector local_bundle_image_ids; local_bundle_image_ids.reserve(num_eff_images); // If the number of overlapping images equals the number of desired images in // the local bundle, then simply copy over the image identifiers. if (overlapping_images.size() == num_eff_images) { for (const auto& overlapping_image : overlapping_images) { local_bundle_image_ids.push_back(overlapping_image.first); } return local_bundle_image_ids; } // In the following iteration, we start with the most overlapping images and // check whether it has sufficient triangulation angle. If none of the // overlapping images has sufficient triangulation angle, we relax the // triangulation angle threshold and start from the most overlapping image // again. In the end, if we still haven't found enough images, we simply use // the most overlapping images. const double min_tri_angle_rad = DegToRad(options.local_ba_min_tri_angle); // The selection thresholds (minimum triangulation angle, minimum number of // shared observations), which are successively relaxed. const std::array, 8> selection_thresholds = {{ std::make_pair(min_tri_angle_rad / 1.0, 0.6 * image.NumPoints3D()), std::make_pair(min_tri_angle_rad / 1.5, 0.6 * image.NumPoints3D()), std::make_pair(min_tri_angle_rad / 2.0, 0.5 * image.NumPoints3D()), std::make_pair(min_tri_angle_rad / 2.5, 0.4 * image.NumPoints3D()), std::make_pair(min_tri_angle_rad / 3.0, 0.3 * image.NumPoints3D()), std::make_pair(min_tri_angle_rad / 4.0, 0.2 * image.NumPoints3D()), std::make_pair(min_tri_angle_rad / 5.0, 0.1 * image.NumPoints3D()), std::make_pair(min_tri_angle_rad / 6.0, 0.1 * image.NumPoints3D()), }}; const Eigen::Vector3d proj_center = image.ProjectionCenter(); std::vector shared_points3D; shared_points3D.reserve(image.NumPoints3D()); std::vector tri_angles(overlapping_images.size(), -1.0); std::vector used_overlapping_images(overlapping_images.size(), false); for (const auto& selection_threshold : selection_thresholds) { for (size_t overlapping_image_idx = 0; overlapping_image_idx < overlapping_images.size(); ++overlapping_image_idx) { // Check if the image has sufficient overlap. Since the images are ordered // based on the overlap, we can just skip the remaining ones. if (overlapping_images[overlapping_image_idx].second < selection_threshold.second) { break; } // Check if the image is already in the local bundle. if (used_overlapping_images[overlapping_image_idx]) { continue; } const auto& overlapping_image = reconstruction_->Image( overlapping_images[overlapping_image_idx].first); const Eigen::Vector3d overlapping_proj_center = overlapping_image.ProjectionCenter(); // In the first iteration, compute the triangulation angle. In later // iterations, reuse the previously computed value. double& tri_angle = tri_angles[overlapping_image_idx]; if (tri_angle < 0.0) { // Collect the commonly observed 3D points. shared_points3D.clear(); for (const Point2D& point2D : overlapping_image.Points2D()) { if (point2D.HasPoint3D() && point3D_ids.count(point2D.point3D_id)) { shared_points3D.push_back( reconstruction_->Point3D(point2D.point3D_id).xyz); } } // Calculate the triangulation angle at a certain percentile. const double kTriangulationAnglePercentile = 75; tri_angle = Percentile( CalculateTriangulationAngles( proj_center, overlapping_proj_center, shared_points3D), kTriangulationAnglePercentile); } // Check that the image has sufficient triangulation angle. if (tri_angle >= selection_threshold.first) { local_bundle_image_ids.push_back(overlapping_image.ImageId()); used_overlapping_images[overlapping_image_idx] = true; // Check if we already collected enough images. if (local_bundle_image_ids.size() >= num_eff_images) { break; } } } // Check if we already collected enough images. if (local_bundle_image_ids.size() >= num_eff_images) { break; } } // In case there are not enough images with sufficient triangulation angle, // simply fill up the rest with the most overlapping images. if (local_bundle_image_ids.size() < num_eff_images) { for (size_t overlapping_image_idx = 0; overlapping_image_idx < overlapping_images.size(); ++overlapping_image_idx) { // Collect image if it is not yet in the local bundle. if (!used_overlapping_images[overlapping_image_idx]) { local_bundle_image_ids.push_back( overlapping_images[overlapping_image_idx].first); used_overlapping_images[overlapping_image_idx] = true; // Check if we already collected enough images. if (local_bundle_image_ids.size() >= num_eff_images) { break; } } } } return local_bundle_image_ids; } void IncrementalMapper::RegisterImageEvent(const image_t image_id) { const Image& image = reconstruction_->Image(image_id); size_t& num_reg_images_for_camera = num_reg_images_per_camera_[image.CameraId()]; num_reg_images_for_camera += 1; size_t& num_regs_for_image = num_registrations_[image_id]; num_regs_for_image += 1; if (num_regs_for_image == 1) { num_total_reg_images_ += 1; } else if (num_regs_for_image > 1) { num_shared_reg_images_ += 1; } } void IncrementalMapper::DeRegisterImageEvent(const image_t image_id) { const Image& image = reconstruction_->Image(image_id); size_t& num_reg_images_for_camera = num_reg_images_per_camera_.at(image.CameraId()); CHECK_GT(num_reg_images_for_camera, 0); num_reg_images_for_camera -= 1; size_t& num_regs_for_image = num_registrations_[image_id]; num_regs_for_image -= 1; if (num_regs_for_image == 0) { num_total_reg_images_ -= 1; } else if (num_regs_for_image > 0) { num_shared_reg_images_ -= 1; } } bool IncrementalMapper::EstimateInitialTwoViewGeometry( const Options& options, const image_t image_id1, const image_t image_id2) { const image_pair_t image_pair_id = Database::ImagePairToPairId(image_id1, image_id2); if (prev_init_image_pair_id_ == image_pair_id) { return true; } const Image& image1 = database_cache_->Image(image_id1); const Camera& camera1 = database_cache_->Camera(image1.CameraId()); const Image& image2 = database_cache_->Image(image_id2); const Camera& camera2 = database_cache_->Camera(image2.CameraId()); const FeatureMatches matches = database_cache_->CorrespondenceGraph()->FindCorrespondencesBetweenImages( image_id1, image_id2); std::vector points1; points1.reserve(image1.NumPoints2D()); for (const auto& point : image1.Points2D()) { points1.push_back(point.xy); } std::vector points2; points2.reserve(image2.NumPoints2D()); for (const auto& point : image2.Points2D()) { points2.push_back(point.xy); } TwoViewGeometryOptions two_view_geometry_options; two_view_geometry_options.ransac_options.min_num_trials = 30; two_view_geometry_options.ransac_options.max_error = options.init_max_error; TwoViewGeometry two_view_geometry = EstimateCalibratedTwoViewGeometry( camera1, points1, camera2, points2, matches, two_view_geometry_options); if (!EstimateTwoViewGeometryPose( camera1, points1, camera2, points2, &two_view_geometry)) { return false; } if (static_cast(two_view_geometry.inlier_matches.size()) >= options.init_min_num_inliers && std::abs(two_view_geometry.cam2_from_cam1.translation.z()) < options.init_max_forward_motion && two_view_geometry.tri_angle > DegToRad(options.init_min_tri_angle)) { prev_init_image_pair_id_ = image_pair_id; prev_init_two_view_geometry_ = two_view_geometry; return true; } return false; } } // namespace colmap colmap-3.9.1/src/colmap/sfm/incremental_mapper.h000066400000000000000000000321521454702036400216540ustar00rootroot00000000000000// 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. #pragma once #include "colmap/estimators/bundle_adjustment.h" #include "colmap/scene/database.h" #include "colmap/scene/database_cache.h" #include "colmap/scene/reconstruction.h" #include "colmap/sfm/incremental_triangulator.h" namespace colmap { // Class that provides all functionality for the incremental reconstruction // procedure. Example usage: // // IncrementalMapper mapper(&database_cache); // mapper.BeginReconstruction(&reconstruction); // CHECK(mapper.FindInitialImagePair(options, image_id1, image_id2)); // CHECK(mapper.RegisterInitialImagePair(options, image_id1, image_id2)); // while (...) { // const auto next_image_ids = mapper.FindNextImages(options); // for (const auto image_id : next_image_ids) { // CHECK(mapper.RegisterNextImage(options, image_id)); // if (...) { // mapper.AdjustLocalBundle(...); // } else { // mapper.AdjustGlobalBundle(...); // } // } // } // mapper.EndReconstruction(false); // class IncrementalMapper { public: struct Options { // Minimum number of inliers for initial image pair. int init_min_num_inliers = 100; // Maximum error in pixels for two-view geometry estimation for initial // image pair. double init_max_error = 4.0; // Maximum forward motion for initial image pair. double init_max_forward_motion = 0.95; // Minimum triangulation angle for initial image pair. double init_min_tri_angle = 16.0; // Maximum number of trials to use an image for initialization. int init_max_reg_trials = 2; // Maximum reprojection error in absolute pose estimation. double abs_pose_max_error = 12.0; // Minimum number of inliers in absolute pose estimation. int abs_pose_min_num_inliers = 30; // Minimum inlier ratio in absolute pose estimation. double abs_pose_min_inlier_ratio = 0.25; // Whether to estimate the focal length in absolute pose estimation. bool abs_pose_refine_focal_length = true; // Whether to estimate the extra parameters in absolute pose estimation. bool abs_pose_refine_extra_params = true; // Number of images to optimize in local bundle adjustment. int local_ba_num_images = 6; // Minimum triangulation for images to be chosen in local bundle adjustment. double local_ba_min_tri_angle = 6; // Thresholds for bogus camera parameters. Images with bogus camera // parameters are filtered and ignored in triangulation. double min_focal_length_ratio = 0.1; // Opening angle of ~130deg double max_focal_length_ratio = 10; // Opening angle of ~5deg double max_extra_param = 1; // Maximum reprojection error in pixels for observations. double filter_max_reproj_error = 4.0; // Minimum triangulation angle in degrees for stable 3D points. double filter_min_tri_angle = 1.5; // Maximum number of trials to register an image. int max_reg_trials = 3; // If reconstruction is provided as input, fix the existing image poses. bool fix_existing_images = false; // Number of threads. int num_threads = -1; // Method to find and select next best image to register. enum class ImageSelectionMethod { MAX_VISIBLE_POINTS_NUM, MAX_VISIBLE_POINTS_RATIO, MIN_UNCERTAINTY, }; ImageSelectionMethod image_selection_method = ImageSelectionMethod::MIN_UNCERTAINTY; bool Check() const; }; struct LocalBundleAdjustmentReport { size_t num_merged_observations = 0; size_t num_completed_observations = 0; size_t num_filtered_observations = 0; size_t num_adjusted_observations = 0; }; // Create incremental mapper. The database cache must live for the entire // life-time of the incremental mapper. explicit IncrementalMapper( std::shared_ptr database_cache); // Prepare the mapper for a new reconstruction, which might have existing // registered images (in which case `RegisterNextImage` must be called) or // which is empty (in which case `RegisterInitialImagePair` must be called). void BeginReconstruction( const std::shared_ptr& reconstruction); // Cleanup the mapper after the current reconstruction is done. If the // model is discarded, the number of total and shared registered images will // be updated accordingly. void EndReconstruction(bool discard); // Find initial image pair to seed the incremental reconstruction. The image // pairs should be passed to `RegisterInitialImagePair`. This function // automatically ignores image pairs that failed to register previously. bool FindInitialImagePair(const Options& options, image_t* image_id1, image_t* image_id2); // Find best next image to register in the incremental reconstruction. The // images should be passed to `RegisterNextImage`. This function automatically // ignores images that failed to registered for `max_reg_trials`. std::vector FindNextImages(const Options& options); // Attempt to seed the reconstruction from an image pair. bool RegisterInitialImagePair(const Options& options, image_t image_id1, image_t image_id2); // Attempt to register image to the existing model. This requires that // a previous call to `RegisterInitialImagePair` was successful. bool RegisterNextImage(const Options& options, image_t image_id); // Triangulate observations of image. size_t TriangulateImage(const IncrementalTriangulator::Options& tri_options, image_t image_id); // Retriangulate image pairs that should have common observations according to // the scene graph but don't due to drift, etc. To handle drift, the employed // reprojection error thresholds should be relatively large. If the thresholds // are too large, non-robust bundle adjustment will break down; if the // thresholds are too small, we cannot fix drift effectively. size_t Retriangulate(const IncrementalTriangulator::Options& tri_options); // Complete tracks by transitively following the scene graph correspondences. // This is especially effective after bundle adjustment, since many cameras // and point locations might have improved. Completion of tracks enables // better subsequent registration of new images. size_t CompleteTracks(const IncrementalTriangulator::Options& tri_options); // Merge tracks by using scene graph correspondences. Similar to // `CompleteTracks`, this is effective after bundle adjustment and improves // the redundancy in subsequent bundle adjustments. size_t MergeTracks(const IncrementalTriangulator::Options& tri_options); // Adjust locally connected images and points of a reference image. In // addition, refine the provided 3D points. Only images connected to the // reference image are optimized. If the provided 3D points are not locally // connected to the reference image, their observing images are set as // constant in the adjustment. LocalBundleAdjustmentReport AdjustLocalBundle( const Options& options, const BundleAdjustmentOptions& ba_options, const IncrementalTriangulator::Options& tri_options, image_t image_id, const std::unordered_set& point3D_ids); // Global bundle adjustment using Ceres Solver. bool AdjustGlobalBundle(const Options& options, const BundleAdjustmentOptions& ba_options); // Filter images and point observations. size_t FilterImages(const Options& options); size_t FilterPoints(const Options& options); const Reconstruction& GetReconstruction() const; // Number of images that are registered in at least on reconstruction. size_t NumTotalRegImages() const; // Number of shared images between current reconstruction and all other // previous reconstructions. size_t NumSharedRegImages() const; // Get changed 3D points, since the last call to `ClearModifiedPoints3D`. const std::unordered_set& GetModifiedPoints3D(); // Clear the collection of changed 3D points. void ClearModifiedPoints3D(); private: // Find seed images for incremental reconstruction. Suitable seed images have // a large number of correspondences and have camera calibration priors. The // returned list is ordered such that most suitable images are in the front. std::vector FindFirstInitialImage(const Options& options) const; // For a given first seed image, find other images that are connected to the // first image. Suitable second images have a large number of correspondences // to the first image and have camera calibration priors. The returned list is // ordered such that most suitable images are in the front. std::vector FindSecondInitialImage(const Options& options, image_t image_id1) const; // Find local bundle for given image in the reconstruction. The local bundle // is defined as the images that are most connected, i.e. maximum number of // shared 3D points, to the given image. std::vector FindLocalBundle(const Options& options, image_t image_id) const; // Register / De-register image in current reconstruction and update // the number of shared images between all reconstructions. void RegisterImageEvent(image_t image_id); void DeRegisterImageEvent(image_t image_id); bool EstimateInitialTwoViewGeometry(const Options& options, image_t image_id1, image_t image_id2); // Class that holds all necessary data from database in memory. const std::shared_ptr database_cache_; // Class that holds data of the reconstruction. std::shared_ptr reconstruction_; // Class that is responsible for incremental triangulation. std::unique_ptr triangulator_; // Number of images that are registered in at least on reconstruction. size_t num_total_reg_images_; // Number of shared images between current reconstruction and all other // previous reconstructions. size_t num_shared_reg_images_; // Estimated two-view geometry of last call to `FindFirstInitialImage`, // used as a cache for a subsequent call to `RegisterInitialImagePair`. image_pair_t prev_init_image_pair_id_; TwoViewGeometry prev_init_two_view_geometry_; // Images and image pairs that have been used for initialization. Each image // and image pair is only tried once for initialization. std::unordered_map init_num_reg_trials_; std::unordered_set init_image_pairs_; // The number of registered images per camera. This information is used // to avoid duplicate refinement of camera parameters and degradation of // already refined camera parameters in local bundle adjustment when multiple // images share intrinsics. std::unordered_map num_reg_images_per_camera_; // The number of reconstructions in which images are registered. std::unordered_map num_registrations_; // Images that have been filtered in current reconstruction. std::unordered_set filtered_images_; // Number of trials to register image in current reconstruction. Used to set // an upper bound to the number of trials to register an image. std::unordered_map num_reg_trials_; // Images that were registered before beginning the reconstruction. // This image list will be non-empty, if the reconstruction is continued from // an existing reconstruction. std::unordered_set existing_image_ids_; }; } // namespace colmap colmap-3.9.1/src/colmap/sfm/incremental_triangulator.cc000066400000000000000000000633321454702036400232450ustar00rootroot00000000000000// 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. #include "colmap/sfm/incremental_triangulator.h" #include "colmap/estimators/triangulation.h" #include "colmap/scene/projection.h" #include "colmap/util/misc.h" namespace colmap { bool IncrementalTriangulator::Options::Check() const { CHECK_OPTION_GE(max_transitivity, 0); CHECK_OPTION_GT(create_max_angle_error, 0); CHECK_OPTION_GT(continue_max_angle_error, 0); CHECK_OPTION_GT(merge_max_reproj_error, 0); CHECK_OPTION_GT(complete_max_reproj_error, 0); CHECK_OPTION_GE(complete_max_transitivity, 0); CHECK_OPTION_GT(re_max_angle_error, 0); CHECK_OPTION_GE(re_min_ratio, 0); CHECK_OPTION_LE(re_min_ratio, 1); CHECK_OPTION_GE(re_max_trials, 0); CHECK_OPTION_GT(min_angle, 0); return true; } IncrementalTriangulator::IncrementalTriangulator( std::shared_ptr correspondence_graph, std::shared_ptr reconstruction) : correspondence_graph_(std::move(correspondence_graph)), reconstruction_(std::move(reconstruction)) {} size_t IncrementalTriangulator::TriangulateImage(const Options& options, const image_t image_id) { CHECK(options.Check()); size_t num_tris = 0; ClearCaches(); const Image& image = reconstruction_->Image(image_id); if (!image.IsRegistered()) { return num_tris; } const Camera& camera = reconstruction_->Camera(image.CameraId()); if (HasCameraBogusParams(options, camera)) { return num_tris; } // Correspondence data for reference observation in given image. We iterate // over all observations of the image and each observation once becomes // the reference correspondence. CorrData ref_corr_data; ref_corr_data.image_id = image_id; ref_corr_data.image = ℑ ref_corr_data.camera = &camera; // Container for correspondences from reference observation to other images. std::vector corrs_data; // Try to triangulate all image observations. for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { const size_t num_triangulated = Find(options, image_id, point2D_idx, static_cast(options.max_transitivity), &corrs_data); if (corrs_data.empty()) { continue; } const Point2D& point2D = image.Point2D(point2D_idx); ref_corr_data.point2D_idx = point2D_idx; ref_corr_data.point2D = &point2D; if (num_triangulated == 0) { corrs_data.push_back(ref_corr_data); num_tris += Create(options, corrs_data); } else { // Continue correspondences to existing 3D points. num_tris += Continue(options, ref_corr_data, corrs_data); // Create points from correspondences that are not continued. corrs_data.push_back(ref_corr_data); num_tris += Create(options, corrs_data); } } return num_tris; } size_t IncrementalTriangulator::CompleteImage(const Options& options, const image_t image_id) { CHECK(options.Check()); size_t num_tris = 0; ClearCaches(); const Image& image = reconstruction_->Image(image_id); if (!image.IsRegistered()) { return num_tris; } const Camera& camera = reconstruction_->Camera(image.CameraId()); if (HasCameraBogusParams(options, camera)) { return num_tris; } // Setup estimation options. EstimateTriangulationOptions tri_options; tri_options.min_tri_angle = DegToRad(options.min_angle); tri_options.residual_type = TriangulationEstimator::ResidualType::REPROJECTION_ERROR; tri_options.ransac_options.max_error = options.complete_max_reproj_error; tri_options.ransac_options.confidence = 0.9999; tri_options.ransac_options.min_inlier_ratio = 0.02; tri_options.ransac_options.max_num_trials = 10000; // Correspondence data for reference observation in given image. We iterate // over all observations of the image and each observation once becomes // the reference correspondence. CorrData ref_corr_data; ref_corr_data.image_id = image_id; ref_corr_data.image = ℑ ref_corr_data.camera = &camera; // Container for correspondences from reference observation to other images. std::vector corrs_data; for (point2D_t point2D_idx = 0; point2D_idx < image.NumPoints2D(); ++point2D_idx) { const Point2D& point2D = image.Point2D(point2D_idx); if (point2D.HasPoint3D()) { // Complete existing track. num_tris += Complete(options, point2D.point3D_id); continue; } if (options.ignore_two_view_tracks && correspondence_graph_->IsTwoViewObservation(image_id, point2D_idx)) { continue; } const size_t num_triangulated = Find(options, image_id, point2D_idx, static_cast(options.max_transitivity), &corrs_data); if (num_triangulated || corrs_data.empty()) { continue; } ref_corr_data.point2D = &point2D; ref_corr_data.point2D_idx = point2D_idx; corrs_data.push_back(ref_corr_data); // Setup data for triangulation estimation. std::vector point_data; point_data.resize(corrs_data.size()); std::vector pose_data; pose_data.resize(corrs_data.size()); for (size_t i = 0; i < corrs_data.size(); ++i) { const CorrData& corr_data = corrs_data[i]; point_data[i].point = corr_data.point2D->xy; point_data[i].point_normalized = corr_data.camera->CamFromImg(point_data[i].point); pose_data[i].proj_matrix = corr_data.image->CamFromWorld().ToMatrix(); pose_data[i].proj_center = corr_data.image->ProjectionCenter(); pose_data[i].camera = corr_data.camera; } // Enforce exhaustive sampling for small track lengths. const size_t kExhaustiveSamplingThreshold = 15; if (point_data.size() <= kExhaustiveSamplingThreshold) { tri_options.ransac_options.min_num_trials = NChooseK(point_data.size(), 2); } // Estimate triangulation. Eigen::Vector3d xyz; std::vector inlier_mask; if (!EstimateTriangulation( tri_options, point_data, pose_data, &inlier_mask, &xyz)) { continue; } // Add inliers to estimated track. Track track; track.Reserve(corrs_data.size()); for (size_t i = 0; i < inlier_mask.size(); ++i) { if (inlier_mask[i]) { const CorrData& corr_data = corrs_data[i]; track.AddElement(corr_data.image_id, corr_data.point2D_idx); num_tris += 1; } } const point3D_t point3D_id = reconstruction_->AddPoint3D(xyz, std::move(track)); modified_point3D_ids_.insert(point3D_id); } return num_tris; } size_t IncrementalTriangulator::CompleteTracks( const Options& options, const std::unordered_set& point3D_ids) { CHECK(options.Check()); size_t num_completed = 0; ClearCaches(); for (const point3D_t point3D_id : point3D_ids) { num_completed += Complete(options, point3D_id); } return num_completed; } size_t IncrementalTriangulator::CompleteAllTracks(const Options& options) { CHECK(options.Check()); size_t num_completed = 0; ClearCaches(); for (const point3D_t point3D_id : reconstruction_->Point3DIds()) { num_completed += Complete(options, point3D_id); } return num_completed; } size_t IncrementalTriangulator::MergeTracks( const Options& options, const std::unordered_set& point3D_ids) { CHECK(options.Check()); size_t num_merged = 0; ClearCaches(); for (const point3D_t point3D_id : point3D_ids) { num_merged += Merge(options, point3D_id); } return num_merged; } size_t IncrementalTriangulator::MergeAllTracks(const Options& options) { CHECK(options.Check()); size_t num_merged = 0; ClearCaches(); for (const point3D_t point3D_id : reconstruction_->Point3DIds()) { num_merged += Merge(options, point3D_id); } return num_merged; } size_t IncrementalTriangulator::Retriangulate(const Options& options) { CHECK(options.Check()); size_t num_tris = 0; ClearCaches(); Options re_options = options; re_options.continue_max_angle_error = options.re_max_angle_error; for (const auto& image_pair : reconstruction_->ImagePairs()) { // Only perform retriangulation for under-reconstructed image pairs. const double tri_ratio = static_cast(image_pair.second.num_tri_corrs) / static_cast(image_pair.second.num_total_corrs); if (tri_ratio >= options.re_min_ratio) { continue; } // Check if images are registered yet. image_t image_id1; image_t image_id2; Database::PairIdToImagePair(image_pair.first, &image_id1, &image_id2); const Image& image1 = reconstruction_->Image(image_id1); if (!image1.IsRegistered()) { continue; } const Image& image2 = reconstruction_->Image(image_id2); if (!image2.IsRegistered()) { continue; } // Only perform retriangulation for a maximum number of trials. int& num_re_trials = re_num_trials_[image_pair.first]; if (num_re_trials >= options.re_max_trials) { continue; } num_re_trials += 1; const Camera& camera1 = reconstruction_->Camera(image1.CameraId()); const Camera& camera2 = reconstruction_->Camera(image2.CameraId()); if (HasCameraBogusParams(options, camera1) || HasCameraBogusParams(options, camera2)) { continue; } // Find correspondences and perform retriangulation. const FeatureMatches& corrs = correspondence_graph_->FindCorrespondencesBetweenImages(image_id1, image_id2); for (const auto& corr : corrs) { const Point2D& point2D1 = image1.Point2D(corr.point2D_idx1); const Point2D& point2D2 = image2.Point2D(corr.point2D_idx2); // Two cases are possible here: both points belong to the same 3D point // or to different 3D points. In the former case, there is nothing // to do. In the latter case, we do not attempt retriangulation, // as retriangulated correspondences are very likely bogus and // would therefore destroy both 3D points if merged. if (point2D1.HasPoint3D() && point2D2.HasPoint3D()) { continue; } CorrData corr_data1; corr_data1.image_id = image_id1; corr_data1.point2D_idx = corr.point2D_idx1; corr_data1.image = &image1; corr_data1.camera = &camera1; corr_data1.point2D = &point2D1; CorrData corr_data2; corr_data2.image_id = image_id2; corr_data2.point2D_idx = corr.point2D_idx2; corr_data2.image = &image2; corr_data2.camera = &camera2; corr_data2.point2D = &point2D2; if (point2D1.HasPoint3D() && !point2D2.HasPoint3D()) { const std::vector corrs_data1 = {corr_data1}; num_tris += Continue(re_options, corr_data2, corrs_data1); } else if (!point2D1.HasPoint3D() && point2D2.HasPoint3D()) { const std::vector corrs_data2 = {corr_data2}; num_tris += Continue(re_options, corr_data1, corrs_data2); } else if (!point2D1.HasPoint3D() && !point2D2.HasPoint3D()) { const std::vector corrs_data = {corr_data1, corr_data2}; // Do not use larger triangulation threshold as this causes // significant drift when creating points (options vs. re_options). num_tris += Create(options, corrs_data); } // Else both points have a 3D point, but we do not want to // merge points in retriangulation. } } return num_tris; } void IncrementalTriangulator::AddModifiedPoint3D(const point3D_t point3D_id) { modified_point3D_ids_.insert(point3D_id); } const std::unordered_set& IncrementalTriangulator::GetModifiedPoints3D() { // First remove any missing 3D points from the set. for (auto it = modified_point3D_ids_.begin(); it != modified_point3D_ids_.end();) { if (reconstruction_->ExistsPoint3D(*it)) { ++it; } else { modified_point3D_ids_.erase(it++); } } return modified_point3D_ids_; } void IncrementalTriangulator::ClearModifiedPoints3D() { modified_point3D_ids_.clear(); } void IncrementalTriangulator::ClearCaches() { camera_has_bogus_params_.clear(); merge_trials_.clear(); found_corrs_.clear(); } size_t IncrementalTriangulator::Find(const Options& options, const image_t image_id, const point2D_t point2D_idx, const size_t transitivity, std::vector* corrs_data) { correspondence_graph_->ExtractTransitiveCorrespondences( image_id, point2D_idx, transitivity, &found_corrs_); corrs_data->clear(); corrs_data->reserve(found_corrs_.size()); size_t num_triangulated = 0; for (const auto& corr : found_corrs_) { const Image& corr_image = reconstruction_->Image(corr.image_id); if (!corr_image.IsRegistered()) { continue; } const Camera& corr_camera = reconstruction_->Camera(corr_image.CameraId()); if (HasCameraBogusParams(options, corr_camera)) { continue; } CorrData corr_data; corr_data.image_id = corr.image_id; corr_data.point2D_idx = corr.point2D_idx; corr_data.image = &corr_image; corr_data.camera = &corr_camera; corr_data.point2D = &corr_image.Point2D(corr.point2D_idx); corrs_data->push_back(corr_data); if (corr_data.point2D->HasPoint3D()) { num_triangulated += 1; } } return num_triangulated; } size_t IncrementalTriangulator::Create( const Options& options, const std::vector& corrs_data) { // Extract correspondences without an existing triangulated observation. std::vector create_corrs_data; create_corrs_data.reserve(corrs_data.size()); for (const CorrData& corr_data : corrs_data) { if (!corr_data.point2D->HasPoint3D()) { create_corrs_data.push_back(corr_data); } } if (create_corrs_data.size() < 2) { // Need at least two observations for triangulation. return 0; } else if (options.ignore_two_view_tracks && create_corrs_data.size() == 2) { const CorrData& corr_data1 = create_corrs_data[0]; if (correspondence_graph_->IsTwoViewObservation(corr_data1.image_id, corr_data1.point2D_idx)) { return 0; } } // Setup data for triangulation estimation. std::vector point_data; point_data.resize(create_corrs_data.size()); std::vector pose_data; pose_data.resize(create_corrs_data.size()); for (size_t i = 0; i < create_corrs_data.size(); ++i) { const CorrData& corr_data = create_corrs_data[i]; point_data[i].point = corr_data.point2D->xy; point_data[i].point_normalized = corr_data.camera->CamFromImg(point_data[i].point); pose_data[i].proj_matrix = corr_data.image->CamFromWorld().ToMatrix(); pose_data[i].proj_center = corr_data.image->ProjectionCenter(); pose_data[i].camera = corr_data.camera; } // Setup estimation options. EstimateTriangulationOptions tri_options; tri_options.min_tri_angle = DegToRad(options.min_angle); tri_options.residual_type = TriangulationEstimator::ResidualType::ANGULAR_ERROR; tri_options.ransac_options.max_error = DegToRad(options.create_max_angle_error); tri_options.ransac_options.confidence = 0.9999; tri_options.ransac_options.min_inlier_ratio = 0.02; tri_options.ransac_options.max_num_trials = 10000; // Enforce exhaustive sampling for small track lengths. const size_t kExhaustiveSamplingThreshold = 15; if (point_data.size() <= kExhaustiveSamplingThreshold) { tri_options.ransac_options.min_num_trials = NChooseK(point_data.size(), 2); } // Estimate triangulation. Eigen::Vector3d xyz; std::vector inlier_mask; if (!EstimateTriangulation( tri_options, point_data, pose_data, &inlier_mask, &xyz)) { return 0; } // Add inliers to estimated track. Track track; track.Reserve(create_corrs_data.size()); for (size_t i = 0; i < inlier_mask.size(); ++i) { if (inlier_mask[i]) { const CorrData& corr_data = create_corrs_data[i]; track.AddElement(corr_data.image_id, corr_data.point2D_idx); } } // Add estimated point to reconstruction. const size_t track_length = track.Length(); const point3D_t point3D_id = reconstruction_->AddPoint3D(xyz, std::move(track)); modified_point3D_ids_.insert(point3D_id); const size_t kMinRecursiveTrackLength = 3; if (create_corrs_data.size() - track_length >= kMinRecursiveTrackLength) { return track_length + Create(options, create_corrs_data); } return track_length; } size_t IncrementalTriangulator::Continue( const Options& options, const CorrData& ref_corr_data, const std::vector& corrs_data) { // No need to continue, if the reference observation is triangulated. if (ref_corr_data.point2D->HasPoint3D()) { return 0; } double best_angle_error = std::numeric_limits::max(); size_t best_idx = std::numeric_limits::max(); for (size_t idx = 0; idx < corrs_data.size(); ++idx) { const CorrData& corr_data = corrs_data[idx]; if (!corr_data.point2D->HasPoint3D()) { continue; } const Point3D& point3D = reconstruction_->Point3D(corr_data.point2D->point3D_id); const double angle_error = CalculateAngularError(ref_corr_data.point2D->xy, point3D.xyz, ref_corr_data.image->CamFromWorld(), *ref_corr_data.camera); if (angle_error < best_angle_error) { best_angle_error = angle_error; best_idx = idx; } } const double max_angle_error = DegToRad(options.continue_max_angle_error); if (best_angle_error <= max_angle_error && best_idx != std::numeric_limits::max()) { const CorrData& corr_data = corrs_data[best_idx]; const TrackElement track_el(ref_corr_data.image_id, ref_corr_data.point2D_idx); reconstruction_->AddObservation(corr_data.point2D->point3D_id, track_el); modified_point3D_ids_.insert(corr_data.point2D->point3D_id); return 1; } return 0; } size_t IncrementalTriangulator::Merge(const Options& options, const point3D_t point3D_id) { if (!reconstruction_->ExistsPoint3D(point3D_id)) { return 0; } const double max_squared_reproj_error = options.merge_max_reproj_error * options.merge_max_reproj_error; const auto& point3D = reconstruction_->Point3D(point3D_id); for (const auto& track_el : point3D.track.Elements()) { const auto corr_range = correspondence_graph_->FindCorrespondences( track_el.image_id, track_el.point2D_idx); for (const auto* corr = corr_range.beg; corr < corr_range.end; ++corr) { const auto& image = reconstruction_->Image(corr->image_id); if (!image.IsRegistered()) { continue; } const Point2D& corr_point2D = image.Point2D(corr->point2D_idx); if (!corr_point2D.HasPoint3D() || corr_point2D.point3D_id == point3D_id || merge_trials_[point3D_id].count(corr_point2D.point3D_id) > 0) { continue; } // Try to merge the two 3D points. const Point3D& corr_point3D = reconstruction_->Point3D(corr_point2D.point3D_id); merge_trials_[point3D_id].insert(corr_point2D.point3D_id); merge_trials_[corr_point2D.point3D_id].insert(point3D_id); // Weighted average of point locations, depending on track length. const Eigen::Vector3d merged_xyz = (point3D.track.Length() * point3D.xyz + corr_point3D.track.Length() * corr_point3D.xyz) / (point3D.track.Length() + corr_point3D.track.Length()); // Count number of inlier track elements of the merged track. bool merge_success = true; for (const Track* track : {&point3D.track, &corr_point3D.track}) { for (const auto test_track_el : track->Elements()) { const Image& test_image = reconstruction_->Image(test_track_el.image_id); const Camera& test_camera = reconstruction_->Camera(test_image.CameraId()); const Point2D& test_point2D = test_image.Point2D(test_track_el.point2D_idx); if (CalculateSquaredReprojectionError(test_point2D.xy, merged_xyz, test_image.CamFromWorld(), test_camera) > max_squared_reproj_error) { merge_success = false; break; } } if (!merge_success) { break; } } // Only accept merge if all track elements are inliers. if (merge_success) { const size_t num_merged = point3D.track.Length() + corr_point3D.track.Length(); const point3D_t merged_point3D_id = reconstruction_->MergePoints3D(point3D_id, corr_point2D.point3D_id); modified_point3D_ids_.erase(point3D_id); modified_point3D_ids_.erase(corr_point2D.point3D_id); modified_point3D_ids_.insert(merged_point3D_id); // Merge merged 3D point and return, as the original points are // deleted. const size_t num_merged_recursive = Merge(options, merged_point3D_id); if (num_merged_recursive > 0) { return num_merged_recursive; } else { return num_merged; } } } } return 0; } size_t IncrementalTriangulator::Complete(const Options& options, const point3D_t point3D_id) { size_t num_completed = 0; if (!reconstruction_->ExistsPoint3D(point3D_id)) { return num_completed; } const double max_squared_reproj_error = options.complete_max_reproj_error * options.complete_max_reproj_error; const Point3D& point3D = reconstruction_->Point3D(point3D_id); std::vector queue = point3D.track.Elements(); const int max_transitivity = options.complete_max_transitivity; for (int transitivity = 0; transitivity < max_transitivity; ++transitivity) { if (queue.empty()) { break; } const std::vector prev_queue = queue; queue.clear(); for (const TrackElement& queue_elem : prev_queue) { const auto corr_range = correspondence_graph_->FindCorrespondences( queue_elem.image_id, queue_elem.point2D_idx); for (const auto* corr = corr_range.beg; corr < corr_range.end; ++corr) { const Image& image = reconstruction_->Image(corr->image_id); if (!image.IsRegistered()) { continue; } const Point2D& point2D = image.Point2D(corr->point2D_idx); if (point2D.HasPoint3D()) { continue; } const Camera& camera = reconstruction_->Camera(image.CameraId()); if (HasCameraBogusParams(options, camera)) { continue; } if (CalculateSquaredReprojectionError( point2D.xy, point3D.xyz, image.CamFromWorld(), camera) > max_squared_reproj_error) { continue; } // Success, add observation to point track. const TrackElement track_el(corr->image_id, corr->point2D_idx); reconstruction_->AddObservation(point3D_id, track_el); modified_point3D_ids_.insert(point3D_id); // Recursively complete track for this new correspondence. if (transitivity < max_transitivity - 1) { queue.emplace_back(corr->image_id, corr->point2D_idx); } num_completed += 1; } } } return num_completed; } bool IncrementalTriangulator::HasCameraBogusParams(const Options& options, const Camera& camera) { const auto it = camera_has_bogus_params_.find(camera.camera_id); if (it == camera_has_bogus_params_.end()) { const bool has_bogus_params = camera.HasBogusParams(options.min_focal_length_ratio, options.max_focal_length_ratio, options.max_extra_param); camera_has_bogus_params_.emplace(camera.camera_id, has_bogus_params); return has_bogus_params; } else { return it->second; } } } // namespace colmap colmap-3.9.1/src/colmap/sfm/incremental_triangulator.h000066400000000000000000000205151454702036400231030ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/database_cache.h" #include "colmap/scene/reconstruction.h" #include namespace colmap { // Class that triangulates points during the incremental reconstruction. // It holds the state and provides all functionality for triangulation. class IncrementalTriangulator { public: struct Options { // Maximum transitivity to search for correspondences. int max_transitivity = 1; // Maximum angular error to create new triangulations. double create_max_angle_error = 2.0; // Maximum angular error to continue existing triangulations. double continue_max_angle_error = 2.0; // Maximum reprojection error in pixels to merge triangulations. double merge_max_reproj_error = 4.0; // Maximum reprojection error to complete an existing triangulation. double complete_max_reproj_error = 4.0; // Maximum transitivity for track completion. int complete_max_transitivity = 5; // Maximum angular error to re-triangulate under-reconstructed image pairs. double re_max_angle_error = 5.0; // Minimum ratio of common triangulations between an image pair over the // number of correspondences between that image pair to be considered // as under-reconstructed. double re_min_ratio = 0.2; // Maximum number of trials to re-triangulate an image pair. int re_max_trials = 1; // Minimum pairwise triangulation angle for a stable triangulation. double min_angle = 1.5; // Whether to ignore two-view tracks. bool ignore_two_view_tracks = true; // Thresholds for bogus camera parameters. Images with bogus camera // parameters are ignored in triangulation. double min_focal_length_ratio = 0.1; double max_focal_length_ratio = 10.0; double max_extra_param = 1.0; bool Check() const; }; // Create new incremental triangulator. Note that both the correspondence // graph and the reconstruction objects must live as long as the triangulator. IncrementalTriangulator( std::shared_ptr correspondence_graph, std::shared_ptr reconstruction); // Triangulate observations of image. // // Triangulation includes creation of new points, continuation of existing // points, and merging of separate points if given image bridges tracks. // // Note that the given image must be registered and its pose must be set // in the associated reconstruction. size_t TriangulateImage(const Options& options, image_t image_id); // Complete triangulations for image. Tries to create new tracks for not // yet triangulated observations and tries to complete existing tracks. // Returns the number of completed observations. size_t CompleteImage(const Options& options, image_t image_id); // Complete tracks for specific 3D points. // // Completion tries to recursively add observations to a track that might // have failed to triangulate before due to inaccurate poses, etc. // Returns the number of completed observations. size_t CompleteTracks(const Options& options, const std::unordered_set& point3D_ids); // Complete tracks of all 3D points. // Returns the number of completed observations. size_t CompleteAllTracks(const Options& options); // Merge tracks of for specific 3D points. // Returns the number of merged observations. size_t MergeTracks(const Options& options, const std::unordered_set& point3D_ids); // Merge tracks of all 3D points. // Returns the number of merged observations. size_t MergeAllTracks(const Options& options); // Perform retriangulation for under-reconstructed image pairs. Under- // reconstruction usually occurs in the case of a drifting reconstruction. // // Image pairs are under-reconstructed if less than `Options::tri_re_min_ratio // > tri_ratio`, where `tri_ratio` is the number of triangulated matches over // inlier matches between the image pair. size_t Retriangulate(const Options& options); // Indicate that a 3D point has been modified. void AddModifiedPoint3D(point3D_t point3D_id); // Get changed 3D points, since the last call to `ClearModifiedPoints3D`. const std::unordered_set& GetModifiedPoints3D(); // Clear the collection of changed 3D points. void ClearModifiedPoints3D(); // Data for a correspondence / element of a track, used to store all // relevant data for triangulation, in order to avoid duplicate lookup // in the underlying unordered_map's in the Reconstruction struct CorrData { image_t image_id; point2D_t point2D_idx; const Image* image; const Camera* camera; const Point2D* point2D; }; private: // Clear cache of bogus camera parameters and merge trials. void ClearCaches(); // Find (transitive) correspondences to other images. size_t Find(const Options& options, image_t image_id, point2D_t point2D_idx, size_t transitivity, std::vector* corrs_data); // Try to create a new 3D point from the given correspondences. size_t Create(const Options& options, const std::vector& corrs_data); // Try to continue the 3D point with the given correspondences. size_t Continue(const Options& options, const CorrData& ref_corr_data, const std::vector& corrs_data); // Try to merge 3D point with any of its corresponding 3D points. size_t Merge(const Options& options, point3D_t point3D_id); // Try to transitively complete the track of a 3D point. size_t Complete(const Options& options, point3D_t point3D_id); // Check if camera has bogus parameters and cache the result. bool HasCameraBogusParams(const Options& options, const Camera& camera); // Database cache for the reconstruction. Used to retrieve correspondence // information for triangulation. const std::shared_ptr correspondence_graph_; // Reconstruction of the model. Modified when triangulating new points. std::shared_ptr reconstruction_; // Cache for cameras with bogus parameters. std::unordered_map camera_has_bogus_params_; // Cache for tried track merges to avoid duplicate merge trials. std::unordered_map> merge_trials_; // Cache for found correspondences in the graph. std::vector found_corrs_; // Number of trials to retriangulate image pair. std::unordered_map re_num_trials_; // Changed 3D points, i.e. if a 3D point is modified (created, continued, // deleted, merged, etc.). Cleared once `ModifiedPoints3D` is called. std::unordered_set modified_point3D_ids_; }; } // namespace colmap colmap-3.9.1/src/colmap/tools/000077500000000000000000000000001454702036400162065ustar00rootroot00000000000000colmap-3.9.1/src/colmap/tools/CMakeLists.txt000066400000000000000000000032131454702036400207450ustar00rootroot00000000000000# 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. set(FOLDER_NAME "tools") # COLMAP_ADD_EXECUTABLE(example example.cc) colmap-3.9.1/src/colmap/tools/example.cc000066400000000000000000000043111454702036400201470ustar00rootroot00000000000000// 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. #include "colmap/controllers/option_manager.h" #include "colmap/scene/reconstruction.h" #include "colmap/util/logging.h" // Simple example that reads and writes a reconstruction. int main(int argc, char** argv) { colmap::InitializeGlog(argv); std::string input_path; std::string output_path; colmap::OptionManager options; options.AddRequiredOption("input_path", &input_path); options.AddRequiredOption("output_path", &output_path); options.Parse(argc, argv); colmap::Reconstruction reconstruction; reconstruction.Read(input_path); reconstruction.Write(output_path); return EXIT_SUCCESS; } colmap-3.9.1/src/colmap/ui/000077500000000000000000000000001454702036400154635ustar00rootroot00000000000000colmap-3.9.1/src/colmap/ui/CMakeLists.txt000066400000000000000000000064111454702036400202250ustar00rootroot00000000000000# 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. set(FOLDER_NAME "ui") COLMAP_ADD_LIBRARY( NAME colmap_ui SRCS automatic_reconstruction_widget.h automatic_reconstruction_widget.cc bundle_adjustment_widget.h bundle_adjustment_widget.cc colormaps.h colormaps.cc database_management_widget.h database_management_widget.cc dense_reconstruction_widget.h dense_reconstruction_widget.cc feature_extraction_widget.h feature_extraction_widget.cc feature_matching_widget.h feature_matching_widget.cc image_viewer_widget.h image_viewer_widget.cc license_widget.h license_widget.cc line_painter.h line_painter.cc log_widget.h log_widget.cc main_window.h main_window.cc match_matrix_widget.h match_matrix_widget.cc model_viewer_widget.h model_viewer_widget.cc movie_grabber_widget.h movie_grabber_widget.cc options_widget.h options_widget.cc point_painter.h point_painter.cc point_viewer_widget.h point_viewer_widget.cc project_widget.h project_widget.cc qt_utils.h qt_utils.cc reconstruction_manager_widget.h reconstruction_manager_widget.cc reconstruction_options_widget.h reconstruction_options_widget.cc reconstruction_stats_widget.h reconstruction_stats_widget.cc render_options.h render_options_widget.h render_options_widget.cc thread_control_widget.h thread_control_widget.cc triangle_painter.h triangle_painter.cc undistortion_widget.h undistortion_widget.cc resources.qrc PUBLIC_LINK_LIBS colmap_util colmap_image colmap_scene colmap_controllers Qt5::Core Qt5::OpenGL Qt5::Widgets ) colmap-3.9.1/src/colmap/ui/automatic_reconstruction_widget.cc000066400000000000000000000172471454702036400244770ustar00rootroot00000000000000// 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. #include "colmap/ui/automatic_reconstruction_widget.h" #include "colmap/ui/main_window.h" namespace colmap { AutomaticReconstructionWidget::AutomaticReconstructionWidget( MainWindow* main_window) : OptionsWidget(main_window), main_window_(main_window), thread_control_widget_(new ThreadControlWidget(this)) { setWindowTitle("Automatic reconstruction"); AddOptionDirPath(&options_.workspace_path, "Workspace folder"); AddSpacer(); AddOptionDirPath(&options_.image_path, "Image folder"); AddSpacer(); AddOptionDirPath(&options_.mask_path, "Mask folder"); AddSpacer(); AddOptionFilePath(&options_.vocab_tree_path, "Vocabulary tree
(optional)"); AddSpacer(); QLabel* data_type_label = new QLabel(tr("Data type"), this); data_type_label->setFont(font()); data_type_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid_layout_->addWidget(data_type_label, grid_layout_->rowCount(), 0); data_type_cb_ = new QComboBox(this); data_type_cb_->addItem("Individual images"); data_type_cb_->addItem("Video frames"); data_type_cb_->addItem("Internet images"); grid_layout_->addWidget(data_type_cb_, grid_layout_->rowCount() - 1, 1); QLabel* quality_label = new QLabel(tr("Quality"), this); quality_label->setFont(font()); quality_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid_layout_->addWidget(quality_label, grid_layout_->rowCount(), 0); quality_cb_ = new QComboBox(this); quality_cb_->addItem("Low"); quality_cb_->addItem("Medium"); quality_cb_->addItem("High"); quality_cb_->addItem("Extreme"); quality_cb_->setCurrentIndex(2); grid_layout_->addWidget(quality_cb_, grid_layout_->rowCount() - 1, 1); AddSpacer(); AddOptionBool(&options_.single_camera, "Shared intrinsics"); AddOptionBool(&options_.single_camera_per_folder, "Shared intrinsics per sub-folder"); AddOptionBool(&options_.sparse, "Sparse model"); AddOptionBool(&options_.dense, "Dense model"); QLabel* mesher_label = new QLabel(tr("Mesher"), this); mesher_label->setFont(font()); mesher_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid_layout_->addWidget(mesher_label, grid_layout_->rowCount(), 0); mesher_cb_ = new QComboBox(this); mesher_cb_->addItem("Poisson"); mesher_cb_->addItem("Delaunay"); mesher_cb_->setCurrentIndex(0); grid_layout_->addWidget(mesher_cb_, grid_layout_->rowCount() - 1, 1); AddSpacer(); AddOptionInt(&options_.num_threads, "num_threads", -1); AddOptionBool(&options_.use_gpu, "GPU"); AddOptionText(&options_.gpu_index, "gpu_index"); AddSpacer(); QPushButton* run_button = new QPushButton(tr("Run"), this); grid_layout_->addWidget(run_button, grid_layout_->rowCount(), 1); connect(run_button, &QPushButton::released, this, &AutomaticReconstructionWidget::Run); render_result_ = new QAction(this); connect(render_result_, &QAction::triggered, this, &AutomaticReconstructionWidget::RenderResult, Qt::QueuedConnection); } void AutomaticReconstructionWidget::Run() { WriteOptions(); if (!ExistsDir(options_.workspace_path)) { QMessageBox::critical(this, "", tr("Invalid workspace folder")); return; } if (!ExistsDir(options_.image_path)) { QMessageBox::critical(this, "", tr("Invalid image folder")); return; } switch (data_type_cb_->currentIndex()) { case 0: options_.data_type = AutomaticReconstructionController::DataType::INDIVIDUAL; break; case 1: options_.data_type = AutomaticReconstructionController::DataType::VIDEO; break; case 2: options_.data_type = AutomaticReconstructionController::DataType::INTERNET; break; default: options_.data_type = AutomaticReconstructionController::DataType::INDIVIDUAL; break; } switch (quality_cb_->currentIndex()) { case 0: options_.quality = AutomaticReconstructionController::Quality::LOW; break; case 1: options_.quality = AutomaticReconstructionController::Quality::MEDIUM; break; case 2: options_.quality = AutomaticReconstructionController::Quality::HIGH; break; case 3: options_.quality = AutomaticReconstructionController::Quality::EXTREME; break; default: options_.quality = AutomaticReconstructionController::Quality::HIGH; break; } switch (mesher_cb_->currentIndex()) { case 0: options_.mesher = AutomaticReconstructionController::Mesher::POISSON; break; case 1: options_.mesher = AutomaticReconstructionController::Mesher::DELAUNAY; break; default: options_.mesher = AutomaticReconstructionController::Mesher::POISSON; break; } main_window_->reconstruction_manager_->Clear(); main_window_->reconstruction_manager_widget_->Update(); main_window_->RenderClear(); main_window_->RenderNow(); auto controller = std::make_unique( options_, main_window_->reconstruction_manager_); controller->AddCallback(Thread::FINISHED_CALLBACK, [this]() { render_result_->trigger(); }); thread_control_widget_->StartThread( "Reconstructing...", true, std::move(controller)); } void AutomaticReconstructionWidget::RenderResult() { if (main_window_->reconstruction_manager_->Size() > 0) { main_window_->reconstruction_manager_widget_->Update(); main_window_->RenderClear(); main_window_->RenderNow(); } if (options_.sparse) { QMessageBox::information( this, "", tr("Imported the reconstructed sparse models for visualization. The " "models were also exported to the sparse sub-folder in the " "workspace.")); } if (options_.dense) { QMessageBox::information( this, "", tr("To visualize the reconstructed dense point cloud, navigate to the " "dense sub-folder in your workspace with File > Import " "model from.... To visualize the meshed model, you must use an " "external viewer such as Meshlab.")); } } } // namespace colmap colmap-3.9.1/src/colmap/ui/automatic_reconstruction_widget.h000066400000000000000000000043441454702036400243330ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/automatic_reconstruction.h" #include "colmap/ui/options_widget.h" #include "colmap/ui/thread_control_widget.h" namespace colmap { class MainWindow; class AutomaticReconstructionWidget : public OptionsWidget { public: explicit AutomaticReconstructionWidget(MainWindow* main_window); void Run(); private: void RenderResult(); MainWindow* main_window_; AutomaticReconstructionController::Options options_; ThreadControlWidget* thread_control_widget_; QComboBox* data_type_cb_; QComboBox* quality_cb_; QComboBox* mesher_cb_; QAction* render_result_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/bundle_adjustment_widget.cc000066400000000000000000000106071454702036400230500ustar00rootroot00000000000000// 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. #include "colmap/ui/bundle_adjustment_widget.h" #include "colmap/controllers/bundle_adjustment.h" #include "colmap/ui/main_window.h" namespace colmap { BundleAdjustmentWidget::BundleAdjustmentWidget(MainWindow* main_window, OptionManager* options) : OptionsWidget(main_window), main_window_(main_window), options_(options), reconstruction_(nullptr), thread_control_widget_(new ThreadControlWidget(this)) { setWindowTitle("Bundle adjustment"); AddOptionInt(&options->bundle_adjustment->solver_options.max_num_iterations, "max_num_iterations"); AddOptionInt( &options->bundle_adjustment->solver_options.max_linear_solver_iterations, "max_linear_solver_iterations"); AddOptionDoubleLog( &options->bundle_adjustment->solver_options.function_tolerance, "function_tolerance [10eX]", -1000, 1000); AddOptionDoubleLog( &options->bundle_adjustment->solver_options.gradient_tolerance, "gradient_tolerance [10eX]", -1000, 1000); AddOptionDoubleLog( &options->bundle_adjustment->solver_options.parameter_tolerance, "parameter_tolerance [10eX]", -1000, 1000); AddOptionBool(&options->bundle_adjustment->refine_focal_length, "refine_focal_length"); AddOptionBool(&options->bundle_adjustment->refine_principal_point, "refine_principal_point"); AddOptionBool(&options->bundle_adjustment->refine_extra_params, "refine_extra_params"); AddOptionBool(&options->bundle_adjustment->refine_extrinsics, "refine_extrinsics"); QPushButton* run_button = new QPushButton(tr("Run"), this); grid_layout_->addWidget(run_button, grid_layout_->rowCount(), 1); connect( run_button, &QPushButton::released, this, &BundleAdjustmentWidget::Run); render_action_ = new QAction(this); connect(render_action_, &QAction::triggered, this, &BundleAdjustmentWidget::Render, Qt::QueuedConnection); } void BundleAdjustmentWidget::Show( std::shared_ptr reconstruction) { reconstruction_ = std::move(reconstruction); show(); raise(); } void BundleAdjustmentWidget::Run() { CHECK_NOTNULL(reconstruction_); WriteOptions(); auto thread = std::make_unique(*options_, reconstruction_); thread->AddCallback(Thread::FINISHED_CALLBACK, [this]() { render_action_->trigger(); }); // Normalize scene for numerical stability and // to avoid large scale changes in viewer. reconstruction_->Normalize(); thread_control_widget_->StartThread( "Bundle adjusting...", true, std::move(thread)); } void BundleAdjustmentWidget::Render() { main_window_->RenderNow(); } } // namespace colmap colmap-3.9.1/src/colmap/ui/bundle_adjustment_widget.h000066400000000000000000000044531454702036400227140ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/reconstruction.h" #include "colmap/ui/options_widget.h" #include "colmap/ui/thread_control_widget.h" #include #include namespace colmap { class MainWindow; class BundleAdjustmentWidget : public OptionsWidget { public: BundleAdjustmentWidget(MainWindow* main_window, OptionManager* options); void Show(std::shared_ptr reconstruction); private: void Run(); void Render(); MainWindow* main_window_; OptionManager* options_; std::shared_ptr reconstruction_; ThreadControlWidget* thread_control_widget_; QAction* render_action_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/colormaps.cc000066400000000000000000000230701454702036400177730ustar00rootroot00000000000000// 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. #include "colmap/ui/colormaps.h" #include "colmap/sensor/bitmap.h" namespace colmap { PointColormapBase::PointColormapBase() : scale(1.0f), min(0.0f), max(0.0f), range(0.0f), min_q(0.0f), max_q(1.0f) {} void PointColormapBase::UpdateScale(std::vector* values) { if (values->empty()) { min = 0.0f; max = 0.0f; range = 0.0f; } else { std::sort(values->begin(), values->end()); min = (*values)[static_cast(min_q * (values->size() - 1))]; max = (*values)[static_cast(max_q * (values->size() - 1))]; range = max - min; } } float PointColormapBase::AdjustScale(const float gray) { if (range == 0.0f) { return 0.0f; } else { const float gray_clipped = std::min(std::max(gray, min), max); const float gray_scaled = (gray_clipped - min) / range; return std::pow(gray_scaled, scale); } } void PointColormapPhotometric::Prepare( std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) {} Eigen::Vector4f PointColormapPhotometric::ComputeColor( const point3D_t point3D_id, const Point3D& point3D) { return Eigen::Vector4f(point3D.color(0) / 255.0f, point3D.color(1) / 255.0f, point3D.color(2) / 255.0f, 1.0f); } void PointColormapError::Prepare( std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) { std::vector errors; errors.reserve(points3D.size()); for (const auto& point3D : points3D) { errors.push_back(static_cast(point3D.second.error)); } UpdateScale(&errors); } Eigen::Vector4f PointColormapError::ComputeColor(const point3D_t point3D_id, const Point3D& point3D) { const float gray = AdjustScale(static_cast(point3D.error)); return Eigen::Vector4f(JetColormap::Red(gray), JetColormap::Green(gray), JetColormap::Blue(gray), 1.0f); } void PointColormapTrackLen::Prepare( std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) { std::vector track_lengths; track_lengths.reserve(points3D.size()); for (const auto& point3D : points3D) { track_lengths.push_back(point3D.second.track.Length()); } UpdateScale(&track_lengths); } Eigen::Vector4f PointColormapTrackLen::ComputeColor(const point3D_t point3D_id, const Point3D& point3D) { const float gray = AdjustScale(point3D.track.Length()); return Eigen::Vector4f(JetColormap::Red(gray), JetColormap::Green(gray), JetColormap::Blue(gray), 1.0f); } void PointColormapGroundResolution::Prepare( std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) { std::vector resolutions; resolutions.reserve(points3D.size()); std::unordered_map focal_lengths; std::unordered_map principal_points; for (const auto& camera : cameras) { focal_lengths[camera.first] = static_cast(camera.second.MeanFocalLength()); principal_points[camera.first] = Eigen::Vector2f(static_cast(camera.second.PrincipalPointX()), static_cast(camera.second.PrincipalPointY())); } std::unordered_map proj_centers; for (const auto& image : images) { proj_centers[image.first] = image.second.ProjectionCenter().cast(); } for (const auto& point3D : points3D) { float min_resolution = std::numeric_limits::max(); const Eigen::Vector3f xyz = point3D.second.xyz.cast(); for (const auto& track_el : point3D.second.track.Elements()) { const auto& image = images[track_el.image_id]; const float focal_length = focal_lengths[image.CameraId()]; const float focal_length2 = focal_length * focal_length; const Eigen::Vector2f& pp = principal_points[image.CameraId()]; const Eigen::Vector2f xy = image.Point2D(track_el.point2D_idx).xy.cast() - pp; // Distance from principal point to observation on image plane. const float pixel_radius1 = xy.norm(); const float x1 = xy(0) + (xy(0) < 0 ? -1.0f : 1.0f); const float y1 = xy(1) + (xy(1) < 0 ? -1.0f : 1.0f); const float pixel_radius2 = std::sqrt(x1 * x1 + y1 * y1); // Distance from camera center to observation on image plane. const float pixel_dist1 = std::sqrt(pixel_radius1 * pixel_radius1 + focal_length2); const float pixel_dist2 = std::sqrt(pixel_radius2 * pixel_radius2 + focal_length2); // Distance from 3D point to camera center. const float dist = (xyz - proj_centers[track_el.image_id]).norm(); // Perpendicular distance from 3D point to principal axis const float r1 = pixel_radius1 * dist / pixel_dist1; const float r2 = pixel_radius2 * dist / pixel_dist2; const float dr = r2 - r1; // Ground resolution of observation, use "minus" to highlight // high resolution. const float resolution = -dr * dr; if (std::isfinite(resolution)) { min_resolution = std::min(resolution, min_resolution); } } resolutions.push_back(min_resolution); resolutions_[point3D.first] = min_resolution; } UpdateScale(&resolutions); } Eigen::Vector4f PointColormapGroundResolution::ComputeColor( const point3D_t point3D_id, const Point3D& point3D) { const float gray = AdjustScale(resolutions_[point3D_id]); return Eigen::Vector4f(JetColormap::Red(gray), JetColormap::Green(gray), JetColormap::Blue(gray), 1.0f); } const Eigen::Vector4f ImageColormapBase::kDefaultPlaneColor = { 1.0f, 0.1f, 0.0f, 0.6f}; const Eigen::Vector4f ImageColormapBase::kDefaultFrameColor = { 0.8f, 0.1f, 0.0f, 1.0f}; ImageColormapBase::ImageColormapBase() {} void ImageColormapUniform::Prepare( std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) {} void ImageColormapUniform::ComputeColor(const Image& image, Eigen::Vector4f* plane_color, Eigen::Vector4f* frame_color) { *plane_color = uniform_plane_color; *frame_color = uniform_frame_color; } void ImageColormapNameFilter::Prepare( std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) {} void ImageColormapNameFilter::AddColorForWord( const std::string& word, const Eigen::Vector4f& plane_color, const Eigen::Vector4f& frame_color) { image_name_colors_.emplace_back(word, std::make_pair(plane_color, frame_color)); } void ImageColormapNameFilter::ComputeColor(const Image& image, Eigen::Vector4f* plane_color, Eigen::Vector4f* frame_color) { for (const auto& image_name_color : image_name_colors_) { if (StringContains(image.Name(), image_name_color.first)) { *plane_color = image_name_color.second.first; *frame_color = image_name_color.second.second; return; } } *plane_color = kDefaultPlaneColor; *frame_color = kDefaultFrameColor; } } // namespace colmap colmap-3.9.1/src/colmap/ui/colormaps.h000066400000000000000000000151111454702036400176320ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/reconstruction.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include namespace colmap { // Base class for 3D point color mapping. class PointColormapBase { public: PointColormapBase(); virtual ~PointColormapBase() = default; virtual void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) = 0; virtual Eigen::Vector4f ComputeColor(point3D_t point3D_id, const Point3D& point3D) = 0; void UpdateScale(std::vector* values); float AdjustScale(float gray); float scale; float min; float max; float range; float min_q; float max_q; }; // Map color according to RGB value from image. class PointColormapPhotometric : public PointColormapBase { public: void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) override; Eigen::Vector4f ComputeColor(point3D_t point3D_id, const Point3D& point3D) override; }; // Map color according to error. class PointColormapError : public PointColormapBase { public: void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) override; Eigen::Vector4f ComputeColor(point3D_t point3D_id, const Point3D& point3D) override; }; // Map color according to track length. class PointColormapTrackLen : public PointColormapBase { public: void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) override; Eigen::Vector4f ComputeColor(point3D_t point3D_id, const Point3D& point3D) override; }; // Map color according to ground-resolution. class PointColormapGroundResolution : public PointColormapBase { public: void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) override; Eigen::Vector4f ComputeColor(point3D_t point3D_id, const Point3D& point3D) override; private: std::unordered_map resolutions_; }; // Base class for image color mapping. class ImageColormapBase { public: ImageColormapBase(); virtual ~ImageColormapBase() = default; virtual void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) = 0; virtual void ComputeColor(const Image& image, Eigen::Vector4f* plane_color, Eigen::Vector4f* frame_color) = 0; const static Eigen::Vector4f kDefaultPlaneColor; const static Eigen::Vector4f kDefaultFrameColor; }; // Use uniform color for all images. class ImageColormapUniform : public ImageColormapBase { public: void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) override; void ComputeColor(const Image& image, Eigen::Vector4f* plane_color, Eigen::Vector4f* frame_color) override; Eigen::Vector4f uniform_plane_color = kDefaultPlaneColor; Eigen::Vector4f uniform_frame_color = kDefaultFrameColor; }; // Use color for images with specific words in their name. class ImageColormapNameFilter : public ImageColormapBase { public: void Prepare(std::unordered_map& cameras, std::unordered_map& images, std::unordered_map& points3D, std::vector& reg_image_ids) override; void AddColorForWord(const std::string& word, const Eigen::Vector4f& plane_color, const Eigen::Vector4f& frame_color); void ComputeColor(const Image& image, Eigen::Vector4f* plane_color, Eigen::Vector4f* frame_color) override; private: // The plane and frame colors for different words. std::vector< std::pair>> image_name_colors_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/database_management_widget.cc000066400000000000000000000670231454702036400233050ustar00rootroot00000000000000// 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. #include "colmap/ui/database_management_widget.h" #include "colmap/sensor/models.h" namespace colmap { TwoViewInfoTab::TwoViewInfoTab(QWidget* parent, OptionManager* options, Database* database) : QWidget(parent), options_(options), database_(database), matches_viewer_widget_(new FeatureImageViewerWidget(parent, "matches")) {} void TwoViewInfoTab::Clear() { table_widget_->clearContents(); matches_.clear(); configs_.clear(); sorted_matches_idxs_.clear(); } void TwoViewInfoTab::InitializeTable(const QStringList& table_header) { QGridLayout* grid = new QGridLayout(this); info_label_ = new QLabel(this); grid->addWidget(info_label_, 0, 0); QPushButton* show_button = new QPushButton(tr("Show matches"), this); connect( show_button, &QPushButton::released, this, &TwoViewInfoTab::ShowMatches); grid->addWidget(show_button, 0, 1, Qt::AlignRight); table_widget_ = new QTableWidget(this); table_widget_->setColumnCount(table_header.size()); table_widget_->setHorizontalHeaderLabels(table_header); table_widget_->setShowGrid(true); table_widget_->setSelectionBehavior(QAbstractItemView::SelectRows); table_widget_->setSelectionMode(QAbstractItemView::SingleSelection); table_widget_->setEditTriggers(QAbstractItemView::NoEditTriggers); table_widget_->horizontalHeader()->setStretchLastSection(true); table_widget_->verticalHeader()->setVisible(false); table_widget_->verticalHeader()->setDefaultSectionSize(20); grid->addWidget(table_widget_, 1, 0, 1, 2); } void TwoViewInfoTab::ShowMatches() { QItemSelectionModel* select = table_widget_->selectionModel(); if (!select->hasSelection()) { QMessageBox::critical(this, "", tr("No image pair selected.")); return; } if (select->selectedRows().size() > 1) { QMessageBox::critical(this, "", tr("Only one image pair may be selected.")); return; } const size_t idx = sorted_matches_idxs_[select->selectedRows().begin()->row()]; const auto& selection = matches_[idx]; const std::string path1 = JoinPaths(*options_->image_path, image_->Name()); const std::string path2 = JoinPaths(*options_->image_path, selection.first->Name()); const auto keypoints1 = database_->ReadKeypoints(image_->ImageId()); const auto keypoints2 = database_->ReadKeypoints(selection.first->ImageId()); matches_viewer_widget_->setWindowTitle(QString::fromStdString( "Matches for image pair " + std::to_string(image_->ImageId()) + " - " + std::to_string(selection.first->ImageId()))); matches_viewer_widget_->ReadAndShowWithMatches( path1, path2, keypoints1, keypoints2, selection.second); } void TwoViewInfoTab::FillTable() { // Sort the matched pairs according to number of matches in descending order. sorted_matches_idxs_.resize(matches_.size()); std::iota(sorted_matches_idxs_.begin(), sorted_matches_idxs_.end(), 0); std::sort(sorted_matches_idxs_.begin(), sorted_matches_idxs_.end(), [&](const size_t idx1, const size_t idx2) { return matches_[idx1].second.size() > matches_[idx2].second.size(); }); QString info; info += QString("Matched images: ") + QString::number(matches_.size()); info_label_->setText(info); table_widget_->clearContents(); table_widget_->setRowCount(matches_.size()); for (size_t i = 0; i < sorted_matches_idxs_.size(); ++i) { const size_t idx = sorted_matches_idxs_[i]; QTableWidgetItem* image_id_item = new QTableWidgetItem(QString::number(matches_[idx].first->ImageId())); table_widget_->setItem(i, 0, image_id_item); QTableWidgetItem* num_matches_item = new QTableWidgetItem(QString::number(matches_[idx].second.size())); table_widget_->setItem(i, 1, num_matches_item); // config for inlier matches tab if (table_widget_->columnCount() == 3) { QTableWidgetItem* config_item = new QTableWidgetItem(QString::number(configs_[idx])); table_widget_->setItem(i, 2, config_item); } } table_widget_->resizeColumnsToContents(); } MatchesTab::MatchesTab(QWidget* parent, OptionManager* options, Database* database) : TwoViewInfoTab(parent, options, database) { QStringList table_header; table_header << "image_id" << "num_matches"; InitializeTable(table_header); } void MatchesTab::Reload(const std::vector& images, const image_t image_id) { matches_.clear(); // Find all matched images for (const auto& image : images) { if (image.ImageId() == image_id) { image_ = ℑ continue; } if (database_->ExistsMatches(image_id, image.ImageId())) { const auto matches = database_->ReadMatches(image_id, image.ImageId()); if (matches.size() > 0) { matches_.emplace_back(&image, matches); } } } FillTable(); } TwoViewGeometriesTab::TwoViewGeometriesTab(QWidget* parent, OptionManager* options, Database* database) : TwoViewInfoTab(parent, options, database) { QStringList table_header; table_header << "image_id" << "num_matches" << "config"; InitializeTable(table_header); } void TwoViewGeometriesTab::Reload(const std::vector& images, const image_t image_id) { matches_.clear(); configs_.clear(); // Find all matched images. for (const auto& image : images) { if (image.ImageId() == image_id) { image_ = ℑ continue; } if (database_->ExistsInlierMatches(image_id, image.ImageId())) { const auto two_view_geometry = database_->ReadTwoViewGeometry(image_id, image.ImageId()); if (two_view_geometry.inlier_matches.size() > 0) { matches_.emplace_back(&image, two_view_geometry.inlier_matches); configs_.push_back(two_view_geometry.config); } } } FillTable(); } OverlappingImagesWidget::OverlappingImagesWidget(QWidget* parent, OptionManager* options, Database* database) : parent_(parent), options_(options) { // Do not change flag, to make sure feature database is not accessed from // multiple threads. setWindowFlags(Qt::Window); resize(parent->size().width() - 20, parent->size().height() - 20); QGridLayout* grid = new QGridLayout(this); tab_widget_ = new QTabWidget(this); matches_tab_ = new MatchesTab(this, options_, database); tab_widget_->addTab(matches_tab_, tr("Matches")); two_view_geometries_tab_ = new TwoViewGeometriesTab(this, options_, database); tab_widget_->addTab(two_view_geometries_tab_, tr("Two-view geometries")); grid->addWidget(tab_widget_, 0, 0); QPushButton* close_button = new QPushButton(tr("Close"), this); connect(close_button, &QPushButton::released, this, &OverlappingImagesWidget::close); grid->addWidget(close_button, 1, 0, Qt::AlignRight); } void OverlappingImagesWidget::ShowMatches(const std::vector& images, const image_t image_id) { parent_->setDisabled(true); setWindowTitle( QString::fromStdString("Matches for image " + std::to_string(image_id))); matches_tab_->Reload(images, image_id); two_view_geometries_tab_->Reload(images, image_id); } void OverlappingImagesWidget::closeEvent(QCloseEvent*) { matches_tab_->Clear(); two_view_geometries_tab_->Clear(); parent_->setEnabled(true); } CameraTab::CameraTab(QWidget* parent, Database* database) : QWidget(parent), database_(database) { QGridLayout* grid = new QGridLayout(this); info_label_ = new QLabel(this); grid->addWidget(info_label_, 0, 0); QPushButton* add_camera_button = new QPushButton(tr("Add camera"), this); connect(add_camera_button, &QPushButton::released, this, &CameraTab::Add); grid->addWidget(add_camera_button, 0, 1, Qt::AlignRight); QPushButton* set_model_button = new QPushButton(tr("Set model"), this); connect(set_model_button, &QPushButton::released, this, &CameraTab::SetModel); grid->addWidget(set_model_button, 0, 2, Qt::AlignRight); table_widget_ = new QTableWidget(this); table_widget_->setColumnCount(6); QStringList table_header; table_header << "camera_id" << "model" << "width" << "height" << "params" << "prior_focal_length"; table_widget_->setHorizontalHeaderLabels(table_header); table_widget_->setShowGrid(true); table_widget_->setSelectionBehavior(QAbstractItemView::SelectRows); table_widget_->horizontalHeader()->setStretchLastSection(true); table_widget_->verticalHeader()->setVisible(false); table_widget_->verticalHeader()->setDefaultSectionSize(20); connect( table_widget_, &QTableWidget::itemChanged, this, &CameraTab::itemChanged); grid->addWidget(table_widget_, 1, 0, 1, 3); grid->setColumnStretch(0, 1); } void CameraTab::Reload() { QString info; info += QString("Cameras: ") + QString::number(database_->NumCameras()); info_label_->setText(info); cameras_ = database_->ReadAllCameras(); // Make sure, itemChanged is not invoked, while setting up the table. table_widget_->blockSignals(true); table_widget_->clearContents(); table_widget_->setRowCount(cameras_.size()); std::sort(cameras_.begin(), cameras_.end(), [](const Camera& camera1, const Camera& camera2) { return camera1.camera_id < camera2.camera_id; }); for (size_t i = 0; i < cameras_.size(); ++i) { const Camera& camera = cameras_[i]; QTableWidgetItem* id_item = new QTableWidgetItem(QString::number(camera.camera_id)); id_item->setFlags(Qt::ItemIsSelectable); table_widget_->setItem(i, 0, id_item); QTableWidgetItem* model_item = new QTableWidgetItem(QString::fromStdString(camera.ModelName())); model_item->setFlags(Qt::ItemIsSelectable); table_widget_->setItem(i, 1, model_item); table_widget_->setItem( i, 2, new QTableWidgetItem(QString::number(camera.width))); table_widget_->setItem( i, 3, new QTableWidgetItem(QString::number(camera.height))); table_widget_->setItem(i, 4, new QTableWidgetItem(QString::fromStdString( VectorToCSV(camera.params)))); table_widget_->setItem( i, 5, new QTableWidgetItem(QString::number(camera.has_prior_focal_length))); } table_widget_->resizeColumnsToContents(); table_widget_->blockSignals(false); } void CameraTab::Clear() { cameras_.clear(); table_widget_->clearContents(); } void CameraTab::itemChanged(QTableWidgetItem* item) { Camera& camera = cameras_.at(item->row()); const std::vector prev_params = camera.params; switch (item->column()) { // case 0: never change the camera ID // case 1: never change the camera model case 2: camera.width = static_cast(item->data(Qt::DisplayRole).toInt()); break; case 3: camera.height = static_cast(item->data(Qt::DisplayRole).toInt()); break; case 4: if (!camera.SetParamsFromString(item->text().toUtf8().constData())) { QMessageBox::critical(this, "", tr("Invalid camera parameters.")); table_widget_->blockSignals(true); item->setText(QString::fromStdString(VectorToCSV(prev_params))); table_widget_->blockSignals(false); } break; case 5: camera.has_prior_focal_length = static_cast(item->data(Qt::DisplayRole).toInt()); break; default: break; } database_->UpdateCamera(camera); } void CameraTab::Add() { QStringList camera_models; #define CAMERA_MODEL_CASE(CameraModel) \ << QString::fromStdString(CameraModelIdToName(CameraModel::model_id)) camera_models CAMERA_MODEL_CASES; #undef CAMERA_MODEL_CASE bool ok; const QString camera_model = QInputDialog::getItem( this, "", tr("Model:"), camera_models, 0, false, &ok); if (!ok) { return; } // Add new camera to feature database. Camera camera; const double kDefaultFocalLength = 1.0; const size_t kDefaultWidth = 1; const size_t kDefaultHeight = 1; camera = Camera::CreateFromModelName(kInvalidCameraId, camera_model.toUtf8().constData(), kDefaultFocalLength, kDefaultWidth, kDefaultHeight); database_->WriteCamera(camera); // Reload all cameras Reload(); // Highlight new camera table_widget_->selectRow(cameras_.size() - 1); } void CameraTab::SetModel() { QItemSelectionModel* select = table_widget_->selectionModel(); if (!select->hasSelection()) { QMessageBox::critical(this, "", tr("No camera selected.")); return; } QStringList camera_models; #define CAMERA_MODEL_CASE(CameraModel) \ << QString::fromStdString(CameraModelIdToName(CameraModel::model_id)) camera_models CAMERA_MODEL_CASES; #undef CAMERA_MODEL_CASE bool ok; const QString camera_model = QInputDialog::getItem( this, "", tr("Model:"), camera_models, 0, false, &ok); if (!ok) { return; } // Make sure, itemChanged is not invoked, while updating up the table table_widget_->blockSignals(true); for (QModelIndex& index : select->selectedRows()) { LOG(INFO) << index.row(); auto& camera = cameras_.at(index.row()); camera = Camera::CreateFromModelName(camera.camera_id, camera_model.toUtf8().constData(), camera.MeanFocalLength(), camera.width, camera.height); database_->UpdateCamera(camera); } table_widget_->blockSignals(false); Reload(); } ImageTab::ImageTab(QWidget* parent, CameraTab* camera_tab, OptionManager* options, Database* database) : QWidget(parent), camera_tab_(camera_tab), options_(options), database_(database) { QGridLayout* grid = new QGridLayout(this); info_label_ = new QLabel(this); grid->addWidget(info_label_, 0, 0); QPushButton* set_camera_button = new QPushButton(tr("Set camera"), this); connect( set_camera_button, &QPushButton::released, this, &ImageTab::SetCamera); grid->addWidget(set_camera_button, 0, 1, Qt::AlignRight); QPushButton* split_camera_button = new QPushButton(tr("Split camera"), this); connect(split_camera_button, &QPushButton::released, this, &ImageTab::SplitCamera); grid->addWidget(split_camera_button, 0, 2, Qt::AlignRight); QPushButton* show_image_button = new QPushButton(tr("Show image"), this); connect( show_image_button, &QPushButton::released, this, &ImageTab::ShowImage); grid->addWidget(show_image_button, 0, 3, Qt::AlignRight); QPushButton* overlapping_images_button = new QPushButton(tr("Overlapping images"), this); connect(overlapping_images_button, &QPushButton::released, this, &ImageTab::ShowMatches); grid->addWidget(overlapping_images_button, 0, 4, Qt::AlignRight); table_widget_ = new QTableWidget(this); table_widget_->setColumnCount(10); QStringList table_header; table_header << "image_id" << "name" << "camera_id" << "qw" << "qx" << "qy" << "qz" << "tx" << "ty" << "tz"; table_widget_->setHorizontalHeaderLabels(table_header); table_widget_->setShowGrid(true); table_widget_->setSelectionBehavior(QAbstractItemView::SelectRows); table_widget_->horizontalHeader()->setStretchLastSection(true); table_widget_->verticalHeader()->setVisible(false); table_widget_->verticalHeader()->setDefaultSectionSize(20); connect( table_widget_, &QTableWidget::itemChanged, this, &ImageTab::itemChanged); grid->addWidget(table_widget_, 1, 0, 1, 5); grid->setColumnStretch(0, 3); image_viewer_widget_ = new FeatureImageViewerWidget(parent, "keypoints"); overlapping_images_widget_ = new OverlappingImagesWidget(parent, options, database_); } void ImageTab::Reload() { QString info; info += QString("Images: ") + QString::number(database_->NumImages()); info += QString("\n"); info += QString("Features: ") + QString::number(database_->NumKeypoints()); info_label_->setText(info); images_ = database_->ReadAllImages(); // Make sure, itemChanged is not invoked, while setting up the table table_widget_->blockSignals(true); table_widget_->clearContents(); table_widget_->setRowCount(images_.size()); for (size_t i = 0; i < images_.size(); ++i) { const auto& image = images_[i]; QTableWidgetItem* id_item = new QTableWidgetItem(QString::number(image.ImageId())); id_item->setFlags(Qt::ItemIsSelectable); table_widget_->setItem(i, 0, id_item); table_widget_->setItem( i, 1, new QTableWidgetItem(QString::fromStdString(image.Name()))); table_widget_->setItem( i, 2, new QTableWidgetItem(QString::number(image.CameraId()))); table_widget_->setItem(i, 3, new QTableWidgetItem(QString::number( image.CamFromWorldPrior().rotation.w()))); table_widget_->setItem(i, 4, new QTableWidgetItem(QString::number( image.CamFromWorldPrior().rotation.x()))); table_widget_->setItem(i, 5, new QTableWidgetItem(QString::number( image.CamFromWorldPrior().rotation.y()))); table_widget_->setItem(i, 6, new QTableWidgetItem(QString::number( image.CamFromWorldPrior().rotation.z()))); table_widget_->setItem(i, 7, new QTableWidgetItem(QString::number( image.CamFromWorldPrior().translation.x()))); table_widget_->setItem(i, 8, new QTableWidgetItem(QString::number( image.CamFromWorldPrior().translation.y()))); table_widget_->setItem(i, 9, new QTableWidgetItem(QString::number( image.CamFromWorldPrior().translation.z()))); } table_widget_->resizeColumnsToContents(); table_widget_->blockSignals(false); } void ImageTab::Clear() { images_.clear(); table_widget_->clearContents(); } void ImageTab::itemChanged(QTableWidgetItem* item) { Image& image = images_.at(item->row()); camera_t camera_id = kInvalidCameraId; switch (item->column()) { // case 0: never change the image ID case 1: image.SetName(item->text().toUtf8().constData()); break; case 2: camera_id = static_cast(item->data(Qt::DisplayRole).toInt()); if (!database_->ExistsCamera(camera_id)) { QMessageBox::critical(this, "", tr("camera_id does not exist.")); table_widget_->blockSignals(true); item->setText(QString::number(image.CameraId())); table_widget_->blockSignals(false); } else { image.SetCameraId(camera_id); } break; case 3: image.CamFromWorldPrior().rotation.w() = item->data(Qt::DisplayRole).toReal(); break; case 4: image.CamFromWorldPrior().rotation.x() = item->data(Qt::DisplayRole).toReal(); break; case 5: image.CamFromWorldPrior().rotation.y() = item->data(Qt::DisplayRole).toReal(); break; case 6: image.CamFromWorldPrior().rotation.z() = item->data(Qt::DisplayRole).toReal(); break; case 7: image.CamFromWorldPrior().translation.x() = item->data(Qt::DisplayRole).toReal(); break; case 8: image.CamFromWorldPrior().translation.y() = item->data(Qt::DisplayRole).toReal(); break; case 9: image.CamFromWorldPrior().translation.z() = item->data(Qt::DisplayRole).toReal(); break; default: break; } database_->UpdateImage(image); } void ImageTab::ShowImage() { QItemSelectionModel* select = table_widget_->selectionModel(); if (!select->hasSelection()) { QMessageBox::critical(this, "", tr("No image selected.")); return; } if (select->selectedRows().size() > 1) { QMessageBox::critical(this, "", tr("Only one image may be selected.")); return; } const auto& image = images_[select->selectedRows().begin()->row()]; const auto keypoints = database_->ReadKeypoints(image.ImageId()); const std::vector tri_mask(keypoints.size(), false); image_viewer_widget_->ReadAndShowWithKeypoints( JoinPaths(*options_->image_path, image.Name()), keypoints, tri_mask); image_viewer_widget_->setWindowTitle( QString::fromStdString("Image " + std::to_string(image.ImageId()))); } void ImageTab::ShowMatches() { QItemSelectionModel* select = table_widget_->selectionModel(); if (!select->hasSelection()) { QMessageBox::critical(this, "", tr("No image selected.")); return; } if (select->selectedRows().size() > 1) { QMessageBox::critical(this, "", tr("Only one image may be selected.")); return; } const auto& image = images_[select->selectedRows().begin()->row()]; overlapping_images_widget_->ShowMatches(images_, image.ImageId()); overlapping_images_widget_->show(); overlapping_images_widget_->raise(); } void ImageTab::SetCamera() { QItemSelectionModel* select = table_widget_->selectionModel(); if (!select->hasSelection()) { QMessageBox::critical(this, "", tr("No image selected.")); return; } bool ok; const camera_t camera_id = static_cast( QInputDialog::getInt(this, "", tr("camera_id"), 0, 0, INT_MAX, 1, &ok)); if (!ok) { return; } if (!database_->ExistsCamera(camera_id)) { QMessageBox::critical(this, "", tr("camera_id does not exist.")); return; } // Make sure, itemChanged is not invoked, while updating up the table table_widget_->blockSignals(true); for (QModelIndex& index : select->selectedRows()) { table_widget_->setItem( index.row(), 2, new QTableWidgetItem(QString::number(camera_id))); auto& image = images_[index.row()]; image.SetCameraId(camera_id); database_->UpdateImage(image); } table_widget_->blockSignals(false); } void ImageTab::SplitCamera() { QItemSelectionModel* select = table_widget_->selectionModel(); if (!select->hasSelection()) { QMessageBox::critical(this, "", tr("No image selected.")); return; } bool ok; const camera_t camera_id = static_cast( QInputDialog::getInt(this, "", tr("camera_id"), 0, 0, INT_MAX, 1, &ok)); if (!ok) { return; } if (!database_->ExistsCamera(camera_id)) { QMessageBox::critical(this, "", tr("camera_id does not exist.")); return; } const auto camera = database_->ReadCamera(camera_id); // Make sure, itemChanged is not invoked, while updating up the table table_widget_->blockSignals(true); for (QModelIndex& index : select->selectedRows()) { auto& image = images_[index.row()]; image.SetCameraId(database_->WriteCamera(camera)); database_->UpdateImage(image); table_widget_->setItem( index.row(), 2, new QTableWidgetItem(QString::number(image.CameraId()))); } table_widget_->blockSignals(false); camera_tab_->Reload(); } DatabaseManagementWidget::DatabaseManagementWidget(QWidget* parent, OptionManager* options) : parent_(parent), options_(options) { setWindowFlags(Qt::Window); setWindowTitle("Database management"); resize(parent->size().width() - 20, parent->size().height() - 20); QGridLayout* grid = new QGridLayout(this); tab_widget_ = new QTabWidget(this); camera_tab_ = new CameraTab(this, &database_); image_tab_ = new ImageTab(this, camera_tab_, options_, &database_); tab_widget_->addTab(image_tab_, tr("Images")); tab_widget_->addTab(camera_tab_, tr("Cameras")); grid->addWidget(tab_widget_, 0, 0, 1, 4); QPushButton* clear_matches_button = new QPushButton(tr("Clear Matches"), this); connect(clear_matches_button, &QPushButton::released, this, &DatabaseManagementWidget::ClearMatches); grid->addWidget(clear_matches_button, 1, 0, Qt::AlignLeft); QPushButton* clear_two_view_geometries_button = new QPushButton(tr("Clear two-view geometries"), this); connect(clear_two_view_geometries_button, &QPushButton::released, this, &DatabaseManagementWidget::ClearTwoViewGeometries); grid->addWidget(clear_two_view_geometries_button, 1, 1, Qt::AlignLeft); grid->setColumnStretch(1, 1); } void DatabaseManagementWidget::showEvent(QShowEvent*) { parent_->setDisabled(true); database_.Open(*options_->database_path); image_tab_->Reload(); camera_tab_->Reload(); } void DatabaseManagementWidget::hideEvent(QHideEvent*) { parent_->setEnabled(true); image_tab_->Clear(); camera_tab_->Clear(); database_.Close(); } void DatabaseManagementWidget::ClearMatches() { QMessageBox::StandardButton reply = QMessageBox::question(this, "", tr("Do you really want to clear all matches?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::No) { return; } database_.ClearMatches(); } void DatabaseManagementWidget::ClearTwoViewGeometries() { QMessageBox::StandardButton reply = QMessageBox::question( this, "", tr("Do you really want to clear all two-view geometries?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::No) { return; } database_.ClearTwoViewGeometries(); } } // namespace colmap colmap-3.9.1/src/colmap/ui/database_management_widget.h000066400000000000000000000121051454702036400231360ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/database.h" #include "colmap/ui/image_viewer_widget.h" #include "colmap/util/misc.h" #include #include #include namespace colmap { //////////////////////////////////////////////////////////////////////////////// // Matches //////////////////////////////////////////////////////////////////////////////// class TwoViewInfoTab : public QWidget { public: TwoViewInfoTab() {} TwoViewInfoTab(QWidget* parent, OptionManager* options, Database* database); void Clear(); protected: void InitializeTable(const QStringList& table_header); void ShowMatches(); void FillTable(); OptionManager* options_; Database* database_; const Image* image_; std::vector> matches_; std::vector configs_; std::vector sorted_matches_idxs_; QTableWidget* table_widget_; QLabel* info_label_; FeatureImageViewerWidget* matches_viewer_widget_; }; class MatchesTab : public TwoViewInfoTab { public: MatchesTab(QWidget* parent, OptionManager* options, Database* database); void Reload(const std::vector& images, image_t image_id); }; class TwoViewGeometriesTab : public TwoViewInfoTab { public: TwoViewGeometriesTab(QWidget* parent, OptionManager* options, Database* database); void Reload(const std::vector& images, image_t image_id); }; class OverlappingImagesWidget : public QWidget { public: OverlappingImagesWidget(QWidget* parent, OptionManager* options, Database* database); void ShowMatches(const std::vector& images, image_t image_id); private: void closeEvent(QCloseEvent* event); QWidget* parent_; OptionManager* options_; QTabWidget* tab_widget_; MatchesTab* matches_tab_; TwoViewGeometriesTab* two_view_geometries_tab_; }; //////////////////////////////////////////////////////////////////////////////// // Images, Cameras //////////////////////////////////////////////////////////////////////////////// class CameraTab : public QWidget { public: CameraTab(QWidget* parent, Database* database); void Reload(); void Clear(); private: void itemChanged(QTableWidgetItem* item); void Add(); void SetModel(); Database* database_; std::vector cameras_; QTableWidget* table_widget_; QLabel* info_label_; }; class ImageTab : public QWidget { public: ImageTab(QWidget* parent, CameraTab* camera_tab, OptionManager* options, Database* database); void Reload(); void Clear(); private: void itemChanged(QTableWidgetItem* item); void ShowImage(); void ShowMatches(); void SetCamera(); void SplitCamera(); CameraTab* camera_tab_; OptionManager* options_; Database* database_; std::vector images_; QTableWidget* table_widget_; QLabel* info_label_; OverlappingImagesWidget* overlapping_images_widget_; FeatureImageViewerWidget* image_viewer_widget_; }; class DatabaseManagementWidget : public QWidget { public: DatabaseManagementWidget(QWidget* parent, OptionManager* options); private: void showEvent(QShowEvent* event); void hideEvent(QHideEvent* event); void ClearMatches(); void ClearTwoViewGeometries(); QWidget* parent_; OptionManager* options_; Database database_; QTabWidget* tab_widget_; ImageTab* image_tab_; CameraTab* camera_tab_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/dense_reconstruction_widget.cc000066400000000000000000000630021454702036400235750ustar00rootroot00000000000000// 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. #include "colmap/ui/dense_reconstruction_widget.h" #include "colmap/image/undistortion.h" #include "colmap/mvs/fusion.h" #include "colmap/mvs/meshing.h" #include "colmap/mvs/patch_match.h" #include "colmap/ui/main_window.h" namespace colmap { namespace { const static std::string kFusedFileName = "fused.ply"; const static std::string kPoissonMeshedFileName = "meshed-poisson.ply"; const static std::string kDelaunayMeshedFileName = "meshed-delaunay.ply"; class StereoOptionsTab : public OptionsWidget { public: StereoOptionsTab(QWidget* parent, OptionManager* options) : OptionsWidget(parent) { // Set a relatively small default image size to avoid too long computation. if (options->patch_match_stereo->max_image_size == -1) { options->patch_match_stereo->max_image_size = 2000; } AddOptionInt( &options->patch_match_stereo->max_image_size, "max_image_size", -1); AddOptionText(&options->patch_match_stereo->gpu_index, "gpu_index"); AddOptionDouble(&options->patch_match_stereo->depth_min, "depth_min", -1); AddOptionDouble(&options->patch_match_stereo->depth_max, "depth_max", -1); AddOptionInt(&options->patch_match_stereo->window_radius, "window_radius"); AddOptionInt(&options->patch_match_stereo->window_step, "window_step"); AddOptionDouble( &options->patch_match_stereo->sigma_spatial, "sigma_spatial", -1); AddOptionDouble(&options->patch_match_stereo->sigma_color, "sigma_color"); AddOptionInt(&options->patch_match_stereo->num_samples, "num_samples"); AddOptionDouble(&options->patch_match_stereo->ncc_sigma, "ncc_sigma"); AddOptionDouble(&options->patch_match_stereo->min_triangulation_angle, "min_triangulation_angle"); AddOptionDouble(&options->patch_match_stereo->incident_angle_sigma, "incident_angle_sigma"); AddOptionInt(&options->patch_match_stereo->num_iterations, "num_iterations"); AddOptionBool(&options->patch_match_stereo->geom_consistency, "geom_consistency"); AddOptionDouble(&options->patch_match_stereo->geom_consistency_regularizer, "geom_consistency_regularizer"); AddOptionDouble(&options->patch_match_stereo->geom_consistency_max_cost, "geom_consistency_max_cost"); AddOptionBool(&options->patch_match_stereo->filter, "filter"); AddOptionDouble(&options->patch_match_stereo->filter_min_ncc, "filter_min_ncc"); AddOptionDouble( &options->patch_match_stereo->filter_min_triangulation_angle, "filter_min_triangulation_angle"); AddOptionInt(&options->patch_match_stereo->filter_min_num_consistent, "filter_min_num_consistent"); AddOptionDouble( &options->patch_match_stereo->filter_geom_consistency_max_cost, "filter_geom_consistency_max_cost"); AddOptionDouble(&options->patch_match_stereo->cache_size, "cache_size [gigabytes]", 0, std::numeric_limits::max(), 0.1, 1); AddOptionBool(&options->patch_match_stereo->write_consistency_graph, "write_consistency_graph"); } }; class FusionOptionsTab : public OptionsWidget { public: FusionOptionsTab(QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddOptionInt(&options->stereo_fusion->max_image_size, "max_image_size", -1); AddOptionInt(&options->stereo_fusion->min_num_pixels, "min_num_pixels", 0); AddOptionInt(&options->stereo_fusion->max_num_pixels, "max_num_pixels", 0); AddOptionInt( &options->stereo_fusion->max_traversal_depth, "max_traversal_depth", 1); AddOptionDouble( &options->stereo_fusion->max_reproj_error, "max_reproj_error", 0); AddOptionDouble(&options->stereo_fusion->max_depth_error, "max_depth_error", 0, 1, 0.0001, 4); AddOptionDouble( &options->stereo_fusion->max_normal_error, "max_normal_error", 0, 180); AddOptionInt( &options->stereo_fusion->check_num_images, "check_num_images", 1); AddOptionDouble(&options->stereo_fusion->cache_size, "cache_size [gigabytes]", 0, std::numeric_limits::max(), 0.1, 1); AddOptionBool(&options->stereo_fusion->use_cache, "use_cache"); } }; class MeshingOptionsTab : public OptionsWidget { public: MeshingOptionsTab(QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddSection("Poisson Meshing"); AddOptionDouble(&options->poisson_meshing->point_weight, "point_weight", 0); AddOptionInt(&options->poisson_meshing->depth, "depth", 1); AddOptionDouble(&options->poisson_meshing->color, "color", 0); AddOptionDouble(&options->poisson_meshing->trim, "trim", 0); AddOptionInt(&options->poisson_meshing->num_threads, "num_threads", -1); AddSection("Delaunay Meshing"); AddOptionDouble( &options->delaunay_meshing->max_proj_dist, "max_proj_dist", 0); AddOptionDouble( &options->delaunay_meshing->max_depth_dist, "max_depth_dist", 0); AddOptionDouble(&options->delaunay_meshing->distance_sigma_factor, "distance_sigma_factor", 0); AddOptionDouble(&options->delaunay_meshing->quality_regularization, "quality_regularization", 0); AddOptionDouble(&options->delaunay_meshing->max_side_length_factor, "max_side_length_factor", 0); AddOptionDouble(&options->delaunay_meshing->max_side_length_percentile, "max_side_length_percentile", 0); AddOptionInt(&options->delaunay_meshing->num_threads, "num_threads", -1); } }; // Read the specified reference image names from a patch match configuration. std::vector> ReadPatchMatchConfig( const std::string& config_path) { std::ifstream file(config_path); CHECK(file.is_open()) << config_path; std::string line; std::string ref_image_name; std::vector> images; while (std::getline(file, line)) { StringTrim(&line); if (line.empty() || line[0] == '#') { continue; } if (ref_image_name.empty()) { ref_image_name = line; } else { images.emplace_back(ref_image_name, line); ref_image_name.clear(); } } return images; } } // namespace DenseReconstructionOptionsWidget::DenseReconstructionOptionsWidget( QWidget* parent, OptionManager* options) : QWidget(parent) { setWindowFlags(Qt::Dialog); setWindowModality(Qt::ApplicationModal); setWindowTitle("Dense reconstruction options"); QGridLayout* grid = new QGridLayout(this); QTabWidget* tab_widget = new QTabWidget(this); tab_widget->setElideMode(Qt::TextElideMode::ElideRight); tab_widget->addTab(new StereoOptionsTab(this, options), "Stereo"); tab_widget->addTab(new FusionOptionsTab(this, options), "Fusion"); tab_widget->addTab(new MeshingOptionsTab(this, options), "Meshing"); grid->addWidget(tab_widget, 0, 0); } DenseReconstructionWidget::DenseReconstructionWidget(MainWindow* main_window, OptionManager* options) : QWidget(main_window), main_window_(main_window), options_(options), reconstruction_(nullptr), thread_control_widget_(new ThreadControlWidget(this)), options_widget_(new DenseReconstructionOptionsWidget(this, options)), photometric_done_(false), geometric_done_(false) { setWindowFlags(Qt::Dialog); setWindowModality(Qt::ApplicationModal); setWindowTitle("Dense reconstruction"); resize(main_window_->size().width() - 20, main_window_->size().height() - 20); QGridLayout* grid = new QGridLayout(this); undistortion_button_ = new QPushButton(tr("Undistortion"), this); connect(undistortion_button_, &QPushButton::released, this, &DenseReconstructionWidget::Undistort); grid->addWidget(undistortion_button_, 0, 0, Qt::AlignLeft); stereo_button_ = new QPushButton(tr("Stereo"), this); connect(stereo_button_, &QPushButton::released, this, &DenseReconstructionWidget::Stereo); grid->addWidget(stereo_button_, 0, 1, Qt::AlignLeft); fusion_button_ = new QPushButton(tr("Fusion"), this); connect(fusion_button_, &QPushButton::released, this, &DenseReconstructionWidget::Fusion); grid->addWidget(fusion_button_, 0, 2, Qt::AlignLeft); poisson_meshing_button_ = new QPushButton(tr("Poisson"), this); connect(poisson_meshing_button_, &QPushButton::released, this, &DenseReconstructionWidget::PoissonMeshing); grid->addWidget(poisson_meshing_button_, 0, 3, Qt::AlignLeft); delaunay_meshing_button_ = new QPushButton(tr("Delaunay"), this); connect(delaunay_meshing_button_, &QPushButton::released, this, &DenseReconstructionWidget::DelaunayMeshing); grid->addWidget(delaunay_meshing_button_, 0, 4, Qt::AlignLeft); QPushButton* options_button = new QPushButton(tr("Options"), this); connect(options_button, &QPushButton::released, options_widget_, &OptionsWidget::show); grid->addWidget(options_button, 0, 5, Qt::AlignLeft); QLabel* workspace_path_label = new QLabel("Workspace", this); grid->addWidget(workspace_path_label, 0, 6, Qt::AlignRight); workspace_path_text_ = new QLineEdit(this); grid->addWidget(workspace_path_text_, 0, 7, Qt::AlignRight); connect(workspace_path_text_, &QLineEdit::textChanged, this, &DenseReconstructionWidget::RefreshWorkspace, Qt::QueuedConnection); QPushButton* refresh_path_button = new QPushButton(tr("Refresh"), this); connect(refresh_path_button, &QPushButton::released, this, &DenseReconstructionWidget::RefreshWorkspace, Qt::QueuedConnection); grid->addWidget(refresh_path_button, 0, 8, Qt::AlignRight); QPushButton* workspace_path_button = new QPushButton(tr("Select"), this); connect(workspace_path_button, &QPushButton::released, this, &DenseReconstructionWidget::SelectWorkspacePath, Qt::QueuedConnection); grid->addWidget(workspace_path_button, 0, 9, Qt::AlignRight); QStringList table_header; table_header << "image_name" << "" << "photometric" << "geometric" << "src_images"; table_widget_ = new QTableWidget(this); table_widget_->setColumnCount(table_header.size()); table_widget_->setHorizontalHeaderLabels(table_header); table_widget_->setShowGrid(true); table_widget_->setSelectionBehavior(QAbstractItemView::SelectRows); table_widget_->setSelectionMode(QAbstractItemView::SingleSelection); table_widget_->setEditTriggers(QAbstractItemView::NoEditTriggers); table_widget_->verticalHeader()->setDefaultSectionSize(25); grid->addWidget(table_widget_, 1, 0, 1, 10); grid->setColumnStretch(4, 1); image_viewer_widget_ = new ImageViewerWidget(this); image_viewer_widget_->setWindowModality(Qt::ApplicationModal); refresh_workspace_action_ = new QAction(this); connect(refresh_workspace_action_, &QAction::triggered, this, &DenseReconstructionWidget::RefreshWorkspace); write_fused_points_action_ = new QAction(this); connect(write_fused_points_action_, &QAction::triggered, this, &DenseReconstructionWidget::WriteFusedPoints); show_meshing_info_action_ = new QAction(this); connect(show_meshing_info_action_, &QAction::triggered, this, &DenseReconstructionWidget::ShowMeshingInfo); RefreshWorkspace(); } void DenseReconstructionWidget::showEvent(QShowEvent* event) { RefreshWorkspace(); } void DenseReconstructionWidget::Show( std::shared_ptr reconstruction) { reconstruction_ = std::move(reconstruction); show(); raise(); } void DenseReconstructionWidget::Undistort() { const std::string workspace_path = GetWorkspacePath(); if (workspace_path.empty()) { return; } if (reconstruction_ == nullptr || reconstruction_->NumRegImages() < 2) { QMessageBox::critical( this, "", tr("No reconstruction selected in main window")); return; } auto undistorter = std::make_unique(UndistortCameraOptions(), *reconstruction_, *options_->image_path, workspace_path); undistorter->AddCallback(Thread::FINISHED_CALLBACK, [this]() { refresh_workspace_action_->trigger(); }); thread_control_widget_->StartThread( "Undistorting...", true, std::move(undistorter)); } void DenseReconstructionWidget::Stereo() { const std::string workspace_path = GetWorkspacePath(); if (workspace_path.empty()) { return; } #if defined(COLMAP_CUDA_ENABLED) auto processor = std::make_unique( *options_->patch_match_stereo, workspace_path, "COLMAP", ""); processor->AddCallback(Thread::FINISHED_CALLBACK, [this]() { refresh_workspace_action_->trigger(); }); thread_control_widget_->StartThread("Stereo...", true, std::move(processor)); #else QMessageBox::critical(this, "", tr("Dense stereo reconstruction requires CUDA, which " "is not available on your system.")); #endif } void DenseReconstructionWidget::Fusion() { const std::string workspace_path = GetWorkspacePath(); if (workspace_path.empty()) { return; } std::string input_type; if (geometric_done_) { input_type = "geometric"; } else if (photometric_done_) { input_type = "photometric"; } else { QMessageBox::critical( this, "", tr("All images must be processed prior to fusion")); } auto fuser = std::make_unique( *options_->stereo_fusion, workspace_path, "COLMAP", "", input_type); fuser->AddCallback(Thread::FINISHED_CALLBACK, [this, fuser = fuser.get()]() { fused_points_ = fuser->GetFusedPoints(); fused_points_visibility_ = fuser->GetFusedPointsVisibility(); write_fused_points_action_->trigger(); }); thread_control_widget_->StartThread("Fusion...", true, std::move(fuser)); } void DenseReconstructionWidget::PoissonMeshing() { const std::string workspace_path = GetWorkspacePath(); if (workspace_path.empty()) { return; } if (ExistsFile(JoinPaths(workspace_path, kFusedFileName))) { thread_control_widget_->StartFunction( "Poisson Meshing...", [this, workspace_path]() { mvs::PoissonMeshing( *options_->poisson_meshing, JoinPaths(workspace_path, kFusedFileName), JoinPaths(workspace_path, kPoissonMeshedFileName)); show_meshing_info_action_->trigger(); }); } } void DenseReconstructionWidget::DelaunayMeshing() { #if defined(COLMAP_CGAL_ENABLED) const std::string workspace_path = GetWorkspacePath(); if (workspace_path.empty()) { return; } if (ExistsFile(JoinPaths(workspace_path, kFusedFileName))) { thread_control_widget_->StartFunction( "Delaunay Meshing...", [this, workspace_path]() { mvs::DenseDelaunayMeshing( *options_->delaunay_meshing, workspace_path, JoinPaths(workspace_path, kDelaunayMeshedFileName)); show_meshing_info_action_->trigger(); }); } #else QMessageBox::critical(this, "", tr("Delaunay meshing requires CGAL, which " "is not available on your system.")); #endif } void DenseReconstructionWidget::SelectWorkspacePath() { std::string workspace_path; if (workspace_path_text_->text().isEmpty()) { workspace_path = GetParentDir(*options_->project_path); } else { workspace_path = workspace_path_text_->text().toUtf8().constData(); } workspace_path_text_->setText( QFileDialog::getExistingDirectory(this, tr("Select workspace path..."), QString::fromStdString(workspace_path), QFileDialog::ShowDirsOnly)); RefreshWorkspace(); } std::string DenseReconstructionWidget::GetWorkspacePath() { std::string workspace_path = workspace_path_text_->text().toUtf8().constData(); if (ExistsDir(workspace_path)) { return workspace_path; } else { QMessageBox::critical(this, "", tr("Invalid workspace path")); return ""; } } void DenseReconstructionWidget::RefreshWorkspace() { table_widget_->clearContents(); table_widget_->setRowCount(0); const std::string workspace_path = workspace_path_text_->text().toUtf8().constData(); if (ExistsDir(workspace_path)) { undistortion_button_->setEnabled(true); } else { undistortion_button_->setEnabled(false); stereo_button_->setEnabled(false); fusion_button_->setEnabled(false); poisson_meshing_button_->setEnabled(false); delaunay_meshing_button_->setEnabled(false); return; } images_path_ = JoinPaths(workspace_path, "images"); depth_maps_path_ = JoinPaths(workspace_path, "stereo/depth_maps"); normal_maps_path_ = JoinPaths(workspace_path, "stereo/normal_maps"); const std::string config_path = JoinPaths(workspace_path, "stereo/patch-match.cfg"); if (ExistsDir(images_path_) && ExistsDir(depth_maps_path_) && ExistsDir(normal_maps_path_) && ExistsDir(JoinPaths(workspace_path, "sparse")) && ExistsDir(JoinPaths(workspace_path, "stereo/consistency_graphs")) && ExistsFile(config_path)) { stereo_button_->setEnabled(true); } else { stereo_button_->setEnabled(false); fusion_button_->setEnabled(false); poisson_meshing_button_->setEnabled(false); delaunay_meshing_button_->setEnabled(false); return; } const auto images = ReadPatchMatchConfig(config_path); table_widget_->setRowCount(images.size()); for (size_t i = 0; i < images.size(); ++i) { const std::string image_name = images[i].first; const std::string src_images = images[i].second; const std::string image_path = JoinPaths(images_path_, image_name); QTableWidgetItem* image_name_item = new QTableWidgetItem(QString::fromStdString(image_name)); table_widget_->setItem(i, 0, image_name_item); QPushButton* image_button = new QPushButton("Image"); connect( image_button, &QPushButton::released, [this, image_name, image_path]() { image_viewer_widget_->setWindowTitle( QString("Image for %1").arg(image_name.c_str())); image_viewer_widget_->ReadAndShow(image_path); }); table_widget_->setCellWidget(i, 1, image_button); table_widget_->setCellWidget( i, 2, GenerateTableButtonWidget(image_name, "photometric")); table_widget_->setCellWidget( i, 3, GenerateTableButtonWidget(image_name, "geometric")); QTableWidgetItem* src_images_item = new QTableWidgetItem(QString::fromStdString(src_images)); table_widget_->setItem(i, 4, src_images_item); } table_widget_->resizeColumnsToContents(); fusion_button_->setEnabled(photometric_done_ || geometric_done_); poisson_meshing_button_->setEnabled( ExistsFile(JoinPaths(workspace_path, kFusedFileName))); delaunay_meshing_button_->setEnabled( ExistsFile(JoinPaths(workspace_path, kFusedFileName))); } void DenseReconstructionWidget::WriteFusedPoints() { const int reply = QMessageBox::question( this, "", tr("Do you want to visualize the point cloud? Otherwise, to visualize " "the reconstructed dense point cloud later, navigate to the " "dense sub-folder in your workspace with File > Import " "model from...."), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { const size_t reconstruction_idx = main_window_->reconstruction_manager_->Add(); std::shared_ptr reconstruction = main_window_->reconstruction_manager_->Get(reconstruction_idx); for (const PlyPoint& point : fused_points_) { reconstruction->AddPoint3D(Eigen::Vector3d(point.x, point.y, point.z), Track(), Eigen::Vector3ub(point.r, point.g, point.b)); } options_->render->min_track_len = 0; main_window_->reconstruction_manager_widget_->Update(); main_window_->reconstruction_manager_widget_->SelectReconstruction( reconstruction_idx); main_window_->RenderNow(); } const std::string workspace_path = workspace_path_text_->text().toUtf8().constData(); if (workspace_path.empty()) { fused_points_ = {}; fused_points_visibility_ = {}; return; } thread_control_widget_->StartFunction( "Exporting...", [this, workspace_path]() { const std::string output_path = JoinPaths(workspace_path, kFusedFileName); WriteBinaryPlyPoints(output_path, fused_points_); mvs::WritePointsVisibility(output_path + ".vis", fused_points_visibility_); fused_points_ = {}; fused_points_visibility_ = {}; poisson_meshing_button_->setEnabled(true); delaunay_meshing_button_->setEnabled(true); }); } void DenseReconstructionWidget::ShowMeshingInfo() { QMessageBox::information( this, "", tr("To visualize the meshed model, you must use an external viewer such " "as Meshlab. The model is located in the workspace folder.")); } QWidget* DenseReconstructionWidget::GenerateTableButtonWidget( const std::string& image_name, const std::string& type) { CHECK(type == "photometric" || type == "geometric"); const bool photometric = type == "photometric"; if (photometric) { photometric_done_ = true; } else { geometric_done_ = true; } const std::string depth_map_path = JoinPaths(depth_maps_path_, StringPrintf("%s.%s.bin", image_name.c_str(), type.c_str())); const std::string normal_map_path = JoinPaths(normal_maps_path_, StringPrintf("%s.%s.bin", image_name.c_str(), type.c_str())); QWidget* button_widget = new QWidget(); QGridLayout* button_layout = new QGridLayout(button_widget); button_layout->setContentsMargins(0, 0, 0, 0); QPushButton* depth_map_button = new QPushButton("Depth map", button_widget); if (ExistsFile(depth_map_path)) { connect(depth_map_button, &QPushButton::released, [this, image_name, depth_map_path]() { mvs::DepthMap depth_map; depth_map.Read(depth_map_path); image_viewer_widget_->setWindowTitle( QString("Depth map for %1").arg(image_name.c_str())); image_viewer_widget_->ShowBitmap(depth_map.ToBitmap(2, 98)); }); } else { depth_map_button->setEnabled(false); if (photometric) { photometric_done_ = false; } else { geometric_done_ = false; } } button_layout->addWidget(depth_map_button, 0, 1, Qt::AlignLeft); QPushButton* normal_map_button = new QPushButton("Normal map", button_widget); if (ExistsFile(normal_map_path)) { connect(normal_map_button, &QPushButton::released, [this, image_name, normal_map_path]() { mvs::NormalMap normal_map; normal_map.Read(normal_map_path); image_viewer_widget_->setWindowTitle( QString("Normal map for %1").arg(image_name.c_str())); image_viewer_widget_->ShowBitmap(normal_map.ToBitmap()); }); } else { normal_map_button->setEnabled(false); if (photometric) { photometric_done_ = false; } else { geometric_done_ = false; } } button_layout->addWidget(normal_map_button, 0, 2, Qt::AlignLeft); return button_widget; } } // namespace colmap colmap-3.9.1/src/colmap/ui/dense_reconstruction_widget.h000066400000000000000000000070231454702036400234400ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/mvs/fusion.h" #include "colmap/ui/image_viewer_widget.h" #include "colmap/ui/options_widget.h" #include "colmap/ui/thread_control_widget.h" #include #include namespace colmap { class MainWindow; class DenseReconstructionOptionsWidget : public QWidget { public: DenseReconstructionOptionsWidget(QWidget* parent, OptionManager* options); }; class DenseReconstructionWidget : public QWidget { public: DenseReconstructionWidget(MainWindow* main_window, OptionManager* options); void Show(std::shared_ptr reconstruction); private: void showEvent(QShowEvent* event); void Undistort(); void Stereo(); void Fusion(); void PoissonMeshing(); void DelaunayMeshing(); void SelectWorkspacePath(); std::string GetWorkspacePath(); void RefreshWorkspace(); void WriteFusedPoints(); void ShowMeshingInfo(); QWidget* GenerateTableButtonWidget(const std::string& image_name, const std::string& type); MainWindow* main_window_; OptionManager* options_; std::shared_ptr reconstruction_; ThreadControlWidget* thread_control_widget_; DenseReconstructionOptionsWidget* options_widget_; ImageViewerWidget* image_viewer_widget_; QLineEdit* workspace_path_text_; QTableWidget* table_widget_; QPushButton* undistortion_button_; QPushButton* stereo_button_; QPushButton* fusion_button_; QPushButton* poisson_meshing_button_; QPushButton* delaunay_meshing_button_; QAction* refresh_workspace_action_; QAction* write_fused_points_action_; QAction* show_meshing_info_action_; bool photometric_done_; bool geometric_done_; std::string images_path_; std::string depth_maps_path_; std::string normal_maps_path_; std::vector fused_points_; std::vector> fused_points_visibility_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/feature_extraction_widget.cc000066400000000000000000000276321454702036400232420ustar00rootroot00000000000000// 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. #include "colmap/ui/feature_extraction_widget.h" #include "colmap/controllers/feature_extraction.h" #include "colmap/sensor/models.h" #include "colmap/ui/options_widget.h" #include "colmap/ui/qt_utils.h" #include "colmap/ui/thread_control_widget.h" namespace colmap { class ExtractionWidget : public OptionsWidget { public: ExtractionWidget(QWidget* parent, OptionManager* options); virtual void Run() = 0; protected: OptionManager* options_; ThreadControlWidget* thread_control_widget_; }; class SIFTExtractionWidget : public ExtractionWidget { public: SIFTExtractionWidget(QWidget* parent, OptionManager* options); void Run() override; }; class ImportFeaturesWidget : public ExtractionWidget { public: ImportFeaturesWidget(QWidget* parent, OptionManager* options); void Run() override; private: std::string import_path_; }; ExtractionWidget::ExtractionWidget(QWidget* parent, OptionManager* options) : OptionsWidget(parent), options_(options), thread_control_widget_(new ThreadControlWidget(this)) {} SIFTExtractionWidget::SIFTExtractionWidget(QWidget* parent, OptionManager* options) : ExtractionWidget(parent, options) { AddOptionDirPath(&options->image_reader->mask_path, "mask_path"); AddOptionFilePath(&options->image_reader->camera_mask_path, "camera_mask_path"); AddOptionInt(&options->sift_extraction->max_image_size, "max_image_size"); AddOptionInt(&options->sift_extraction->max_num_features, "max_num_features"); AddOptionInt(&options->sift_extraction->first_octave, "first_octave", -5); AddOptionInt(&options->sift_extraction->num_octaves, "num_octaves"); AddOptionInt(&options->sift_extraction->octave_resolution, "octave_resolution"); AddOptionDouble(&options->sift_extraction->peak_threshold, "peak_threshold", 0.0, 1e7, 0.00001, 5); AddOptionDouble(&options->sift_extraction->edge_threshold, "edge_threshold"); AddOptionBool(&options->sift_extraction->estimate_affine_shape, "estimate_affine_shape"); AddOptionInt(&options->sift_extraction->max_num_orientations, "max_num_orientations"); AddOptionBool(&options->sift_extraction->upright, "upright"); AddOptionBool(&options->sift_extraction->domain_size_pooling, "domain_size_pooling"); AddOptionDouble(&options->sift_extraction->dsp_min_scale, "dsp_min_scale", 0.0, 1e7, 0.00001, 5); AddOptionDouble(&options->sift_extraction->dsp_max_scale, "dsp_max_scale", 0.0, 1e7, 0.00001, 5); AddOptionInt(&options->sift_extraction->dsp_num_scales, "dsp_num_scales", 1); AddOptionInt(&options->sift_extraction->num_threads, "num_threads", -1); AddOptionBool(&options->sift_extraction->use_gpu, "use_gpu"); AddOptionText(&options->sift_extraction->gpu_index, "gpu_index"); } void SIFTExtractionWidget::Run() { WriteOptions(); ImageReaderOptions reader_options = *options_->image_reader; reader_options.database_path = *options_->database_path; reader_options.image_path = *options_->image_path; auto extractor = CreateFeatureExtractorController(reader_options, *options_->sift_extraction); thread_control_widget_->StartThread( "Extracting...", true, std::move(extractor)); } ImportFeaturesWidget::ImportFeaturesWidget(QWidget* parent, OptionManager* options) : ExtractionWidget(parent, options) { AddOptionDirPath(&import_path_, "import_path"); } void ImportFeaturesWidget::Run() { WriteOptions(); if (!ExistsDir(import_path_)) { QMessageBox::critical(this, "", tr("Path is not a directory")); return; } ImageReaderOptions reader_options = *options_->image_reader; reader_options.database_path = *options_->database_path; reader_options.image_path = *options_->image_path; auto importer = CreateFeatureImporterController(reader_options, import_path_); thread_control_widget_->StartThread( "Importing...", true, std::move(importer)); } FeatureExtractionWidget::FeatureExtractionWidget(QWidget* parent, OptionManager* options) : parent_(parent), options_(options) { // Do not change flag, to make sure feature database is not accessed from // multiple threads setWindowFlags(Qt::Window); setWindowTitle("Feature extraction"); QGridLayout* grid = new QGridLayout(this); grid->addWidget(CreateCameraModelBox(), 0, 0); tab_widget_ = new QTabWidget(this); QScrollArea* extraction_widget = new QScrollArea(this); extraction_widget->setAlignment(Qt::AlignHCenter); extraction_widget->setWidget(new SIFTExtractionWidget(this, options)); tab_widget_->addTab(extraction_widget, tr("Extract")); QScrollArea* import_widget = new QScrollArea(this); import_widget->setAlignment(Qt::AlignHCenter); import_widget->setWidget(new ImportFeaturesWidget(this, options)); tab_widget_->addTab(import_widget, tr("Import")); grid->addWidget(tab_widget_); QPushButton* extract_button = new QPushButton(tr("Extract"), this); connect(extract_button, &QPushButton::released, this, &FeatureExtractionWidget::Extract); grid->addWidget(extract_button, grid->rowCount(), 0); } QGroupBox* FeatureExtractionWidget::CreateCameraModelBox() { camera_model_ids_.clear(); camera_model_cb_ = new QComboBox(this); #define CAMERA_MODEL_CASE(CameraModel) \ camera_model_cb_->addItem( \ QString::fromStdString(CameraModelIdToName(CameraModel::model_id))); \ camera_model_ids_.push_back(static_cast(CameraModel::model_id)); CAMERA_MODEL_CASES #undef CAMERA_MODEL_CASE camera_params_exif_rb_ = new QRadioButton(tr("Parameters from EXIF"), this); camera_params_exif_rb_->setChecked(true); camera_params_custom_rb_ = new QRadioButton(tr("Custom parameters"), this); camera_params_info_ = new QLabel(tr(""), this); QPalette pal = QPalette(camera_params_info_->palette()); pal.setColor(QPalette::WindowText, QColor(130, 130, 130)); camera_params_info_->setPalette(pal); camera_params_text_ = new QLineEdit(this); camera_params_text_->setEnabled(false); single_camera_cb_ = new QCheckBox("Shared for all images", this); single_camera_cb_->setChecked(false); single_camera_per_folder_cb_ = new QCheckBox("Shared per sub-folder", this); single_camera_per_folder_cb_->setChecked(false); QGroupBox* box = new QGroupBox(tr("Camera model"), this); QVBoxLayout* vbox = new QVBoxLayout(box); vbox->addWidget(camera_model_cb_); vbox->addWidget(camera_params_info_); vbox->addWidget(single_camera_cb_); vbox->addWidget(single_camera_per_folder_cb_); vbox->addWidget(camera_params_exif_rb_); vbox->addWidget(camera_params_custom_rb_); vbox->addWidget(camera_params_text_); vbox->addStretch(1); box->setLayout(vbox); SelectCameraModel(camera_model_cb_->currentIndex()); connect(camera_model_cb_, (void(QComboBox::*)(int)) & QComboBox::currentIndexChanged, this, &FeatureExtractionWidget::SelectCameraModel); connect(camera_params_exif_rb_, &QRadioButton::clicked, camera_params_text_, &QLineEdit::setDisabled); connect(camera_params_custom_rb_, &QRadioButton::clicked, camera_params_text_, &QLineEdit::setEnabled); return box; } void FeatureExtractionWidget::showEvent(QShowEvent* event) { parent_->setDisabled(true); ReadOptions(); } void FeatureExtractionWidget::hideEvent(QHideEvent* event) { parent_->setEnabled(true); WriteOptions(); } void FeatureExtractionWidget::ReadOptions() { const CameraModelId model_id = CameraModelNameToId(options_->image_reader->camera_model); for (size_t i = 0; i < camera_model_ids_.size(); ++i) { if (camera_model_ids_[i] == static_cast(model_id)) { SelectCameraModel(i); camera_model_cb_->setCurrentIndex(i); break; } } single_camera_cb_->setChecked(options_->image_reader->single_camera); single_camera_per_folder_cb_->setChecked( options_->image_reader->single_camera_per_folder); camera_params_text_->setText( QString::fromStdString(options_->image_reader->camera_params)); } void FeatureExtractionWidget::WriteOptions() { options_->image_reader->camera_model = CameraModelIdToName(static_cast( camera_model_ids_[camera_model_cb_->currentIndex()])); options_->image_reader->single_camera = single_camera_cb_->isChecked(); options_->image_reader->single_camera_per_folder = single_camera_per_folder_cb_->isChecked(); options_->image_reader->camera_params = camera_params_text_->text().toUtf8().constData(); } void FeatureExtractionWidget::SelectCameraModel(const int idx) { const CameraModelId model_id = static_cast(camera_model_ids_[idx]); camera_params_info_->setText(QString::fromStdString( StringPrintf("Parameters: %s", CameraModelParamsInfo(model_id).c_str()))); } void FeatureExtractionWidget::Extract() { // If the custom parameter radiobuttion is not checked, but the // parameters textbox contains parameters. const auto old_camera_params_text = camera_params_text_->text(); if (!camera_params_custom_rb_->isChecked()) { camera_params_text_->setText(""); } WriteOptions(); if (!ExistsCameraModelWithName(options_->image_reader->camera_model)) { QMessageBox::critical(this, "", tr("Camera model does not exist")); return; } const std::vector camera_params = CSVToVector(options_->image_reader->camera_params); const auto camera_code = CameraModelNameToId(options_->image_reader->camera_model); if (camera_params_custom_rb_->isChecked() && !CameraModelVerifyParams(camera_code, camera_params)) { QMessageBox::critical(this, "", tr("Invalid camera parameters")); return; } QWidget* widget = static_cast(tab_widget_->currentWidget())->widget(); static_cast(widget)->Run(); camera_params_text_->setText(old_camera_params_text); } } // namespace colmap colmap-3.9.1/src/colmap/ui/feature_extraction_widget.h000066400000000000000000000047431454702036400231020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/util/misc.h" #include #include namespace colmap { class FeatureExtractionWidget : public QWidget { public: FeatureExtractionWidget(QWidget* parent, OptionManager* options); private: void showEvent(QShowEvent* event); void hideEvent(QHideEvent* event); void ReadOptions(); void WriteOptions(); QGroupBox* CreateCameraModelBox(); void SelectCameraModel(int code); void Extract(); QWidget* parent_; OptionManager* options_; QComboBox* camera_model_cb_; QCheckBox* single_camera_cb_; QCheckBox* single_camera_per_folder_cb_; QRadioButton* camera_params_exif_rb_; QRadioButton* camera_params_custom_rb_; QLabel* camera_params_info_; QLineEdit* camera_params_text_; std::vector camera_model_ids_; QTabWidget* tab_widget_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/feature_matching_widget.cc000066400000000000000000000364001454702036400226450ustar00rootroot00000000000000// 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. #include "colmap/ui/feature_matching_widget.h" #include "colmap/controllers/feature_matching.h" #include "colmap/ui/options_widget.h" #include "colmap/ui/thread_control_widget.h" namespace colmap { class FeatureMatchingTab : public QWidget { public: FeatureMatchingTab(QWidget* parent, OptionManager* options); virtual void Run() = 0; protected: void CreateGeneralOptions(); OptionManager* options_; OptionsWidget* options_widget_; QGridLayout* grid_layout_; ThreadControlWidget* thread_control_widget_; }; class ExhaustiveMatchingTab : public FeatureMatchingTab { public: ExhaustiveMatchingTab(QWidget* parent, OptionManager* options); void Run() override; }; class SequentialMatchingTab : public FeatureMatchingTab { public: SequentialMatchingTab(QWidget* parent, OptionManager* options); void Run() override; }; class VocabTreeMatchingTab : public FeatureMatchingTab { public: VocabTreeMatchingTab(QWidget* parent, OptionManager* options); void Run() override; }; class SpatialMatchingTab : public FeatureMatchingTab { public: SpatialMatchingTab(QWidget* parent, OptionManager* options); void Run() override; }; class TransitiveMatchingTab : public FeatureMatchingTab { public: TransitiveMatchingTab(QWidget* parent, OptionManager* options); void Run() override; }; class CustomMatchingTab : public FeatureMatchingTab { public: CustomMatchingTab(QWidget* parent, OptionManager* options); void Run() override; private: std::string match_list_path_; QComboBox* match_type_cb_; }; FeatureMatchingTab::FeatureMatchingTab(QWidget* parent, OptionManager* options) : QWidget(parent), options_(options), options_widget_(new OptionsWidget(this)), grid_layout_(new QGridLayout(this)), thread_control_widget_(new ThreadControlWidget(this)) {} void FeatureMatchingTab::CreateGeneralOptions() { options_widget_->AddSpacer(); options_widget_->AddSpacer(); options_widget_->AddSection("General Options"); options_widget_->AddSpacer(); options_widget_->AddOptionInt( &options_->sift_matching->num_threads, "num_threads", -1); options_widget_->AddOptionBool(&options_->sift_matching->use_gpu, "use_gpu"); options_widget_->AddOptionText(&options_->sift_matching->gpu_index, "gpu_index"); options_widget_->AddOptionDouble(&options_->sift_matching->max_ratio, "max_ratio"); options_widget_->AddOptionDouble(&options_->sift_matching->max_distance, "max_distance"); options_widget_->AddOptionBool(&options_->sift_matching->cross_check, "cross_check"); options_widget_->AddOptionInt(&options_->sift_matching->max_num_matches, "max_num_matches"); options_widget_->AddOptionBool(&options_->sift_matching->guided_matching, "guided_matching"); options_widget_->AddOptionDouble( &options_->two_view_geometry->ransac_options.max_error, "max_error"); options_widget_->AddOptionDouble( &options_->two_view_geometry->ransac_options.confidence, "confidence", 0, 1, 0.00001, 5); options_widget_->AddOptionInt( &options_->two_view_geometry->ransac_options.max_num_trials, "max_num_trials"); options_widget_->AddOptionDouble( &options_->two_view_geometry->ransac_options.min_inlier_ratio, "min_inlier_ratio", 0, 1, 0.001, 3); options_widget_->AddOptionInt(&options_->two_view_geometry->min_num_inliers, "min_num_inliers"); options_widget_->AddOptionBool(&options_->two_view_geometry->multiple_models, "multiple_models"); options_widget_->AddSpacer(); QScrollArea* options_scroll_area = new QScrollArea(this); options_scroll_area->setAlignment(Qt::AlignHCenter); options_scroll_area->setWidget(options_widget_); grid_layout_->addWidget(options_scroll_area, grid_layout_->rowCount(), 0); QPushButton* run_button = new QPushButton(tr("Run"), this); grid_layout_->addWidget(run_button, grid_layout_->rowCount(), 0); connect(run_button, &QPushButton::released, this, &FeatureMatchingTab::Run); } ExhaustiveMatchingTab::ExhaustiveMatchingTab(QWidget* parent, OptionManager* options) : FeatureMatchingTab(parent, options) { options_widget_->AddOptionInt( &options_->exhaustive_matching->block_size, "block_size", 2); CreateGeneralOptions(); } void ExhaustiveMatchingTab::Run() { options_widget_->WriteOptions(); auto matcher = CreateExhaustiveFeatureMatcher(*options_->exhaustive_matching, *options_->sift_matching, *options_->two_view_geometry, *options_->database_path); thread_control_widget_->StartThread("Matching...", true, std::move(matcher)); } SequentialMatchingTab::SequentialMatchingTab(QWidget* parent, OptionManager* options) : FeatureMatchingTab(parent, options) { options_widget_->AddOptionInt(&options_->sequential_matching->overlap, "overlap"); options_widget_->AddOptionBool( &options_->sequential_matching->quadratic_overlap, "quadratic_overlap"); options_widget_->AddOptionBool(&options_->sequential_matching->loop_detection, "loop_detection"); options_widget_->AddOptionInt( &options_->sequential_matching->loop_detection_period, "loop_detection_period"); options_widget_->AddOptionInt( &options_->sequential_matching->loop_detection_num_images, "loop_detection_num_images"); options_widget_->AddOptionInt( &options_->sequential_matching->loop_detection_num_nearest_neighbors, "loop_detection_num_nearest_neighbors"); options_widget_->AddOptionInt( &options_->sequential_matching->loop_detection_num_checks, "loop_detection_num_checks", 1); options_widget_->AddOptionInt( &options_->sequential_matching ->loop_detection_num_images_after_verification, "loop_detection_num_images_after_verification", 0); options_widget_->AddOptionInt( &options_->sequential_matching->loop_detection_max_num_features, "loop_detection_max_num_features", -1); options_widget_->AddOptionFilePath( &options_->sequential_matching->vocab_tree_path, "vocab_tree_path"); CreateGeneralOptions(); } void SequentialMatchingTab::Run() { options_widget_->WriteOptions(); if (options_->sequential_matching->loop_detection && !ExistsFile(options_->sequential_matching->vocab_tree_path)) { QMessageBox::critical(this, "", tr("Invalid vocabulary tree path.")); return; } auto matcher = CreateSequentialFeatureMatcher(*options_->sequential_matching, *options_->sift_matching, *options_->two_view_geometry, *options_->database_path); thread_control_widget_->StartThread("Matching...", true, std::move(matcher)); } VocabTreeMatchingTab::VocabTreeMatchingTab(QWidget* parent, OptionManager* options) : FeatureMatchingTab(parent, options) { options_widget_->AddOptionInt(&options_->vocab_tree_matching->num_images, "num_images"); options_widget_->AddOptionInt( &options_->vocab_tree_matching->num_nearest_neighbors, "num_nearest_neighbors"); options_widget_->AddOptionInt( &options_->vocab_tree_matching->num_checks, "num_checks", 1); options_widget_->AddOptionInt( &options_->vocab_tree_matching->num_images_after_verification, "num_images_after_verification", 0); options_widget_->AddOptionInt( &options_->vocab_tree_matching->max_num_features, "max_num_features", -1); options_widget_->AddOptionFilePath( &options_->vocab_tree_matching->vocab_tree_path, "vocab_tree_path"); CreateGeneralOptions(); } void VocabTreeMatchingTab::Run() { options_widget_->WriteOptions(); if (!ExistsFile(options_->vocab_tree_matching->vocab_tree_path)) { QMessageBox::critical(this, "", tr("Invalid vocabulary tree path.")); return; } auto matcher = CreateVocabTreeFeatureMatcher(*options_->vocab_tree_matching, *options_->sift_matching, *options_->two_view_geometry, *options_->database_path); thread_control_widget_->StartThread("Matching...", true, std::move(matcher)); } SpatialMatchingTab::SpatialMatchingTab(QWidget* parent, OptionManager* options) : FeatureMatchingTab(parent, options) { options_widget_->AddOptionBool(&options_->spatial_matching->is_gps, "is_gps"); options_widget_->AddOptionBool(&options_->spatial_matching->ignore_z, "ignore_z"); options_widget_->AddOptionInt(&options_->spatial_matching->max_num_neighbors, "max_num_neighbors"); options_widget_->AddOptionDouble(&options_->spatial_matching->max_distance, "max_distance"); CreateGeneralOptions(); } void SpatialMatchingTab::Run() { options_widget_->WriteOptions(); auto matcher = CreateSpatialFeatureMatcher(*options_->spatial_matching, *options_->sift_matching, *options_->two_view_geometry, *options_->database_path); thread_control_widget_->StartThread("Matching...", true, std::move(matcher)); } TransitiveMatchingTab::TransitiveMatchingTab(QWidget* parent, OptionManager* options) : FeatureMatchingTab(parent, options) { options_widget_->AddOptionInt(&options->transitive_matching->batch_size, "batch_size"); options_widget_->AddOptionInt(&options->transitive_matching->num_iterations, "num_iterations"); CreateGeneralOptions(); } void TransitiveMatchingTab::Run() { options_widget_->WriteOptions(); auto matcher = CreateTransitiveFeatureMatcher(*options_->transitive_matching, *options_->sift_matching, *options_->two_view_geometry, *options_->database_path); thread_control_widget_->StartThread("Matching...", true, std::move(matcher)); } CustomMatchingTab::CustomMatchingTab(QWidget* parent, OptionManager* options) : FeatureMatchingTab(parent, options) { match_type_cb_ = new QComboBox(this); match_type_cb_->addItem(QString("Image pairs")); match_type_cb_->addItem(QString("Raw feature matches")); match_type_cb_->addItem(QString("Inlier feature matches")); options_widget_->AddOptionRow("type", match_type_cb_, nullptr); options_widget_->AddOptionFilePath(&match_list_path_, "match_list_path"); options_widget_->AddOptionInt( &options_->image_pairs_matching->block_size, "block_size", 2); CreateGeneralOptions(); } void CustomMatchingTab::Run() { options_widget_->WriteOptions(); if (!ExistsFile(match_list_path_)) { QMessageBox::critical(this, "", tr("Path does not exist!")); return; } std::unique_ptr matcher; if (match_type_cb_->currentIndex() == 0) { ImagePairsMatchingOptions matcher_options; matcher_options.match_list_path = match_list_path_; matcher = CreateImagePairsFeatureMatcher(matcher_options, *options_->sift_matching, *options_->two_view_geometry, *options_->database_path); } else { FeaturePairsMatchingOptions matcher_options; matcher_options.match_list_path = match_list_path_; if (match_type_cb_->currentIndex() == 1) { matcher_options.verify_matches = true; } else if (match_type_cb_->currentIndex() == 2) { matcher_options.verify_matches = false; } matcher = CreateFeaturePairsFeatureMatcher(matcher_options, *options_->sift_matching, *options_->two_view_geometry, *options_->database_path); } thread_control_widget_->StartThread("Matching...", true, std::move(matcher)); } FeatureMatchingWidget::FeatureMatchingWidget(QWidget* parent, OptionManager* options) : parent_(parent) { // Do not change flag, to make sure feature database is not accessed from // multiple threads setWindowFlags(Qt::Window); setWindowTitle("Feature matching"); QGridLayout* grid = new QGridLayout(this); tab_widget_ = new QTabWidget(this); tab_widget_->addTab(new ExhaustiveMatchingTab(this, options), tr("Exhaustive")); tab_widget_->addTab(new SequentialMatchingTab(this, options), tr("Sequential")); tab_widget_->addTab(new VocabTreeMatchingTab(this, options), tr("VocabTree")); tab_widget_->addTab(new SpatialMatchingTab(this, options), tr("Spatial")); tab_widget_->addTab(new TransitiveMatchingTab(this, options), tr("Transitive")); tab_widget_->addTab(new CustomMatchingTab(this, options), tr("Custom")); grid->addWidget(tab_widget_, 0, 0); } void FeatureMatchingWidget::showEvent(QShowEvent* event) { parent_->setDisabled(true); } void FeatureMatchingWidget::hideEvent(QHideEvent* event) { parent_->setEnabled(true); } } // namespace colmap colmap-3.9.1/src/colmap/ui/feature_matching_widget.h000066400000000000000000000040221454702036400225020ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/util/misc.h" #include #include namespace colmap { class FeatureMatchingWidget : public QWidget { public: FeatureMatchingWidget(QWidget* parent, OptionManager* options); private: void showEvent(QShowEvent* event); void hideEvent(QHideEvent* event); QWidget* parent_; QTabWidget* tab_widget_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/image_viewer_widget.cc000066400000000000000000000346001454702036400220030ustar00rootroot00000000000000// 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. #include "colmap/ui/image_viewer_widget.h" #include "colmap/ui/model_viewer_widget.h" #include "colmap/util/misc.h" namespace colmap { const double ImageViewerWidget::kZoomFactor = 1.20; ImageViewerGraphicsScene::ImageViewerGraphicsScene() { setSceneRect(0, 0, 0, 0); image_pixmap_item_ = addPixmap(QPixmap::fromImage(QImage())); image_pixmap_item_->setZValue(-1); } QGraphicsPixmapItem* ImageViewerGraphicsScene::ImagePixmapItem() const { return image_pixmap_item_; } ImageViewerWidget::ImageViewerWidget(QWidget* parent) : QWidget(parent) { setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint); resize(parent->width() - 20, parent->height() - 20); QFont font; font.setPointSize(10); setFont(font); grid_layout_ = new QGridLayout(this); grid_layout_->setContentsMargins(5, 5, 5, 5); graphics_view_ = new QGraphicsView(); graphics_view_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); graphics_view_->setScene(&graphics_scene_); graphics_view_->setAlignment(Qt::AlignLeft | Qt::AlignTop); grid_layout_->addWidget(graphics_view_, 1, 0); button_layout_ = new QHBoxLayout(); QPushButton* zoom_in_button = new QPushButton("+", this); zoom_in_button->setFont(font); zoom_in_button->setFixedWidth(50); button_layout_->addWidget(zoom_in_button); connect( zoom_in_button, &QPushButton::released, this, &ImageViewerWidget::ZoomIn); QPushButton* zoom_out_button = new QPushButton("-", this); zoom_out_button->setFont(font); zoom_out_button->setFixedWidth(50); button_layout_->addWidget(zoom_out_button); connect(zoom_out_button, &QPushButton::released, this, &ImageViewerWidget::ZoomOut); QPushButton* save_button = new QPushButton("Save image", this); save_button->setFont(font); button_layout_->addWidget(save_button); connect(save_button, &QPushButton::released, this, &ImageViewerWidget::Save); grid_layout_->addLayout(button_layout_, 2, 0, Qt::AlignRight); } void ImageViewerWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); graphics_view_->fitInView(graphics_scene_.sceneRect(), Qt::KeepAspectRatio); } void ImageViewerWidget::closeEvent(QCloseEvent* event) { graphics_scene_.ImagePixmapItem()->setPixmap(QPixmap()); } void ImageViewerWidget::ShowBitmap(const Bitmap& bitmap) { ShowPixmap(QPixmap::fromImage(BitmapToQImageRGB(bitmap))); } void ImageViewerWidget::ShowPixmap(const QPixmap& pixmap) { graphics_scene_.ImagePixmapItem()->setPixmap(pixmap); graphics_scene_.setSceneRect(pixmap.rect()); show(); graphics_view_->fitInView(graphics_scene_.sceneRect(), Qt::KeepAspectRatio); raise(); } void ImageViewerWidget::ReadAndShow(const std::string& path) { Bitmap bitmap; if (!bitmap.Read(path, true)) { LOG(ERROR) << "Cannot read image at path " << path; } ShowBitmap(bitmap); } void ImageViewerWidget::ZoomIn() { graphics_view_->scale(kZoomFactor, kZoomFactor); } void ImageViewerWidget::ZoomOut() { graphics_view_->scale(1.0 / kZoomFactor, 1.0 / kZoomFactor); } void ImageViewerWidget::Save() { QString filter("PNG (*.png)"); const QString save_path = QFileDialog::getSaveFileName(this, tr("Select destination..."), "", "PNG (*.png);;JPEG (*.jpg);;BMP (*.bmp)", &filter) .toUtf8() .constData(); // Selection canceled? if (save_path == "") { return; } graphics_scene_.ImagePixmapItem()->pixmap().save(save_path); } FeatureImageViewerWidget::FeatureImageViewerWidget( QWidget* parent, const std::string& switch_text) : ImageViewerWidget(parent), switch_state_(true), switch_text_(switch_text) { switch_button_ = new QPushButton(tr(("Hide " + switch_text_).c_str()), this); switch_button_->setFont(font()); button_layout_->addWidget(switch_button_); connect(switch_button_, &QPushButton::released, this, &FeatureImageViewerWidget::ShowOrHide); } void FeatureImageViewerWidget::ReadAndShowWithKeypoints( const std::string& path, const FeatureKeypoints& keypoints, const std::vector& tri_mask) { Bitmap bitmap; if (!bitmap.Read(path, true)) { LOG(ERROR) << "Cannot read image at path " << path; } image1_ = QPixmap::fromImage(BitmapToQImageRGB(bitmap)); image2_ = image1_; const size_t num_tri_keypoints = std::count_if( tri_mask.begin(), tri_mask.end(), [](const bool tri) { return tri; }); FeatureKeypoints keypoints_tri(num_tri_keypoints); FeatureKeypoints keypoints_not_tri(keypoints.size() - num_tri_keypoints); size_t i_tri = 0; size_t i_not_tri = 0; for (size_t i = 0; i < tri_mask.size(); ++i) { if (tri_mask[i]) { keypoints_tri[i_tri] = keypoints[i]; i_tri += 1; } else { keypoints_not_tri[i_not_tri] = keypoints[i]; i_not_tri += 1; } } DrawKeypoints(&image2_, keypoints_tri, Qt::magenta); DrawKeypoints(&image2_, keypoints_not_tri, Qt::red); if (switch_state_) { ShowPixmap(image2_); } else { ShowPixmap(image1_); } } void FeatureImageViewerWidget::ReadAndShowWithMatches( const std::string& path1, const std::string& path2, const FeatureKeypoints& keypoints1, const FeatureKeypoints& keypoints2, const FeatureMatches& matches) { Bitmap bitmap1; Bitmap bitmap2; if (!bitmap1.Read(path1, true) || !bitmap2.Read(path2, true)) { LOG(ERROR) << "Cannot read images at paths " << path1 << " and " << path2; return; } const auto image1 = QPixmap::fromImage(BitmapToQImageRGB(bitmap1)); const auto image2 = QPixmap::fromImage(BitmapToQImageRGB(bitmap2)); image1_ = ShowImagesSideBySide(image1, image2); image2_ = DrawMatches(image1, image2, keypoints1, keypoints2, matches); if (switch_state_) { ShowPixmap(image2_); } else { ShowPixmap(image1_); } } void FeatureImageViewerWidget::ShowOrHide() { if (switch_state_) { switch_button_->setText(std::string("Show " + switch_text_).c_str()); ShowPixmap(image1_); switch_state_ = false; } else { switch_button_->setText(std::string("Hide " + switch_text_).c_str()); ShowPixmap(image2_); switch_state_ = true; } } DatabaseImageViewerWidget::DatabaseImageViewerWidget( QWidget* parent, ModelViewerWidget* model_viewer_widget, OptionManager* options) : FeatureImageViewerWidget(parent, "keypoints"), model_viewer_widget_(model_viewer_widget), options_(options) { setWindowTitle("Image information"); table_widget_ = new QTableWidget(this); table_widget_->setColumnCount(2); table_widget_->setRowCount(11); QFont font; font.setPointSize(10); table_widget_->setFont(font); table_widget_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); table_widget_->setEditTriggers(QAbstractItemView::NoEditTriggers); table_widget_->setSelectionMode(QAbstractItemView::SingleSelection); table_widget_->setShowGrid(true); table_widget_->horizontalHeader()->setStretchLastSection(true); table_widget_->horizontalHeader()->setVisible(false); table_widget_->verticalHeader()->setVisible(false); table_widget_->verticalHeader()->setDefaultSectionSize(18); int table_row = 0; table_widget_->setItem(table_row, 0, new QTableWidgetItem("image_id")); image_id_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, image_id_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("camera_id")); camera_id_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, camera_id_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("camera_model")); camera_model_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, camera_model_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("camera_params")); camera_params_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, camera_params_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("qw, qx, qy, qz")); rotation_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, rotation_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("tx, ty, tz")); translation_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, translation_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("dims")); dimensions_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, dimensions_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("num_points2D")); num_points2D_item_ = new QTableWidgetItem(); num_points2D_item_->setForeground(Qt::red); table_widget_->setItem(table_row, 1, num_points2D_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("num_points3D")); num_points3D_item_ = new QTableWidgetItem(); num_points3D_item_->setForeground(Qt::magenta); table_widget_->setItem(table_row, 1, num_points3D_item_); table_row += 1; table_widget_->setItem( table_row, 0, new QTableWidgetItem("num_observations")); num_obs_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, num_obs_item_); table_row += 1; table_widget_->setItem(table_row, 0, new QTableWidgetItem("name")); name_item_ = new QTableWidgetItem(); table_widget_->setItem(table_row, 1, name_item_); // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores) table_row += 1; grid_layout_->addWidget(table_widget_, 0, 0); delete_button_ = new QPushButton(tr("Delete"), this); delete_button_->setFont(font); button_layout_->addWidget(delete_button_); connect(delete_button_, &QPushButton::released, this, &DatabaseImageViewerWidget::DeleteImage); } void DatabaseImageViewerWidget::ShowImageWithId(const image_t image_id) { if (model_viewer_widget_->images.count(image_id) == 0) { return; } image_id_ = image_id; const Image& image = model_viewer_widget_->images.at(image_id); const Camera& camera = model_viewer_widget_->cameras.at(image.CameraId()); image_id_item_->setText(QString::number(image_id)); camera_id_item_->setText(QString::number(image.CameraId())); camera_model_item_->setText(QString::fromStdString(camera.ModelName())); camera_params_item_->setText(QString::fromStdString(camera.ParamsToString())); rotation_item_->setText( QString::number(image.CamFromWorld().rotation.w()) + ", " + QString::number(image.CamFromWorld().rotation.x()) + ", " + QString::number(image.CamFromWorld().rotation.y()) + ", " + QString::number(image.CamFromWorld().rotation.z())); translation_item_->setText( QString::number(image.CamFromWorld().translation.x()) + ", " + QString::number(image.CamFromWorld().translation.y()) + ", " + QString::number(image.CamFromWorld().translation.z())); dimensions_item_->setText(QString::number(camera.width) + "x" + QString::number(camera.height)); num_points2D_item_->setText(QString::number(image.NumPoints2D())); std::vector tri_mask(image.NumPoints2D()); for (size_t i = 0; i < image.NumPoints2D(); ++i) { tri_mask[i] = image.Point2D(i).HasPoint3D(); } num_points3D_item_->setText(QString::number(image.NumPoints3D())); num_obs_item_->setText(QString::number(image.NumObservations())); name_item_->setText(QString::fromStdString(image.Name())); ResizeTable(); FeatureKeypoints keypoints(image.NumPoints2D()); for (point2D_t i = 0; i < image.NumPoints2D(); ++i) { keypoints[i].x = static_cast(image.Point2D(i).xy(0)); keypoints[i].y = static_cast(image.Point2D(i).xy(1)); } const std::string path = JoinPaths(*options_->image_path, image.Name()); ReadAndShowWithKeypoints(path, keypoints, tri_mask); } void DatabaseImageViewerWidget::ResizeTable() { // Set fixed table dimensions. table_widget_->resizeColumnsToContents(); int height = table_widget_->horizontalHeader()->height() + 2 * table_widget_->frameWidth(); for (int i = 0; i < table_widget_->rowCount(); i++) { height += table_widget_->rowHeight(i); } table_widget_->setFixedHeight(height); } void DatabaseImageViewerWidget::DeleteImage() { QMessageBox::StandardButton reply = QMessageBox::question(this, "", tr("Do you really want to delete this image?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { if (model_viewer_widget_->reconstruction->ExistsImage(image_id_)) { model_viewer_widget_->reconstruction->DeRegisterImage(image_id_); } model_viewer_widget_->ReloadReconstruction(); } hide(); } } // namespace colmap colmap-3.9.1/src/colmap/ui/image_viewer_widget.h000066400000000000000000000105401454702036400216420ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/database.h" #include "colmap/scene/projection.h" #include "colmap/scene/reconstruction.h" #include "colmap/ui/qt_utils.h" #include #include namespace colmap { class ModelViewerWidget; class ImageViewerGraphicsScene : public QGraphicsScene { public: ImageViewerGraphicsScene(); QGraphicsPixmapItem* ImagePixmapItem() const; private: QGraphicsPixmapItem* image_pixmap_item_ = nullptr; }; class ImageViewerWidget : public QWidget { public: explicit ImageViewerWidget(QWidget* parent); void ShowBitmap(const Bitmap& bitmap); void ShowPixmap(const QPixmap& pixmap); void ReadAndShow(const std::string& path); private: static const double kZoomFactor; ImageViewerGraphicsScene graphics_scene_; QGraphicsView* graphics_view_; protected: void resizeEvent(QResizeEvent* event); void closeEvent(QCloseEvent* event); void ZoomIn(); void ZoomOut(); void Save(); QGridLayout* grid_layout_; QHBoxLayout* button_layout_; }; class FeatureImageViewerWidget : public ImageViewerWidget { public: FeatureImageViewerWidget(QWidget* parent, const std::string& switch_text); void ReadAndShowWithKeypoints(const std::string& path, const FeatureKeypoints& keypoints, const std::vector& tri_mask); void ReadAndShowWithMatches(const std::string& path1, const std::string& path2, const FeatureKeypoints& keypoints1, const FeatureKeypoints& keypoints2, const FeatureMatches& matches); protected: void ShowOrHide(); QPixmap image1_; QPixmap image2_; bool switch_state_; QPushButton* switch_button_; const std::string switch_text_; }; class DatabaseImageViewerWidget : public FeatureImageViewerWidget { public: DatabaseImageViewerWidget(QWidget* parent, ModelViewerWidget* model_viewer_widget, OptionManager* options); void ShowImageWithId(image_t image_id); private: void ResizeTable(); void DeleteImage(); ModelViewerWidget* model_viewer_widget_; OptionManager* options_; QPushButton* delete_button_; image_t image_id_; QTableWidget* table_widget_; QTableWidgetItem* image_id_item_; QTableWidgetItem* camera_id_item_; QTableWidgetItem* camera_model_item_; QTableWidgetItem* camera_params_item_; QTableWidgetItem* rotation_item_; QTableWidgetItem* translation_item_; QTableWidgetItem* dimensions_item_; QTableWidgetItem* num_points2D_item_; QTableWidgetItem* num_points3D_item_; QTableWidgetItem* num_obs_item_; QTableWidgetItem* name_item_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/license_widget.cc000066400000000000000000000241641454702036400207660ustar00rootroot00000000000000// 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. #include "colmap/ui/license_widget.h" namespace colmap { LicenseWidget::LicenseWidget(QWidget* parent) : QTextEdit(parent) { setReadOnly(true); setWindowFlags(Qt::Dialog); resize(parent->width() - 20, parent->height() - 20); setWindowTitle("License"); QString licenses; licenses += "

COLMAP

"; licenses += GetCOLMAPLicense(); licenses += "

External

"; licenses += "

LSD

"; licenses += GetLSDLicense(); licenses += "

PoissonRecon

"; licenses += GetPoissonReconLicense(); licenses += "

SiftGPU

"; licenses += GetSiftGPULicense(); licenses += "

SQLite

"; licenses += GetSQLiteLicense(); licenses += "

VLFeat

"; licenses += GetVLFeatLicense(); setHtml(licenses); } QString LicenseWidget::GetCOLMAPLicense() const { QString license = "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."; return license; } QString LicenseWidget::GetLSDLicense() const { QString license = "LSD - Line Segment Detector on digital images
" "
" "This code is part of the following publication and was subject
" "to peer review:" "
" " \"LSD: a Line Segment Detector\" by Rafael Grompone von Gioi,
" " Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall,
" " Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd
" " http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd
" "
" "Copyright(c) 2007-2011 rafael grompone von gioi
" "
" "This program is free software: you can redistribute it and/or
" "modify it under the terms of the GNU Affero General Public License
" "as published by the Free Software Foundation, either version 3 of
" "the License, or (at your option) any later version.
" "
" "This program is distributed in the hope that it will be useful,
" "but WITHOUT ANY WARRANTY; without even the implied warranty of
" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
" "GNU Affero General Public License for more details.
" "
" "You should have received a copy of the GNU Affero General Public
" "License along with this program.
" "If not, see http://www.gnu.org/licenses/."; return license; } QString LicenseWidget::GetPoissonReconLicense() const { QString license = "The MIT License (MIT)
" "
" "Copyright (c) 2015 mkazhdan
" "
" "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 PARTICULAR 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."; return license; } QString LicenseWidget::GetSiftGPULicense() const { QString license = "Copyright (c) 2007 University of North Carolina at Chapel Hill
" "All Rights Reserved
" "
" "Permission to use, copy, modify and distribute this software and its
" "documentation for educational, research and non-profit purposes,
" "without fee, and without a written agreement is hereby granted,
" "provided that the above copyright notice and the following paragraph
" "appear in all copies.
" "
" "The University of North Carolina at Chapel Hill make no
" "representations about the suitability of this software for any
" "purpose. It is provided 'as is' without express or implied warranty."; return license; } QString LicenseWidget::GetSQLiteLicense() const { QString license = "The author disclaims copyright to this source code. In place of
" "a legal notice, here is a blessing:
" "May you do good and not evil.
" "May you find forgiveness for yourself and forgive others.
" "May you share freely, never taking more than you give."; return license; } QString LicenseWidget::GetVLFeatLicense() const { QString license = "Copyright (C) 2007-11, Andrea Vedaldi and Brian Fulkerson
" "Copyright (C) 2012-13, The VLFeat Team
" "All rights reserved.
" "
" "Redistribution and use in source and binary forms, with or without
" "modification, are permitted provided that the following conditions
" "are met:
" "1. Redistributions of source code must retain the above copyright
" " notice, this list of conditions and the following disclaimer.
" "2. Redistributions in binary form must reproduce the above copyright
" " notice, this list of conditions and the following disclaimer in
" " the documentation and/or other materials provided with the
" " distribution.
" "
" "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
" "AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
" "LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
" "FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
" "COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
" "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
" "BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
" "LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
" "CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
" "LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
" "ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
" "POSSIBILITY OF SUCH DAMAGE."; return license; } } // namespace colmap colmap-3.9.1/src/colmap/ui/license_widget.h000066400000000000000000000037671454702036400206360ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { class LicenseWidget : public QTextEdit { public: explicit LicenseWidget(QWidget* parent); private: QString GetCOLMAPLicense() const; QString GetLSDLicense() const; QString GetPoissonReconLicense() const; QString GetSiftGPULicense() const; QString GetSQLiteLicense() const; QString GetVLFeatLicense() const; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/line_painter.cc000066400000000000000000000101401454702036400204370ustar00rootroot00000000000000// 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. #include "colmap/ui/line_painter.h" #include "colmap/util/opengl_utils.h" namespace colmap { LinePainter::LinePainter() : num_geoms_(0) {} LinePainter::~LinePainter() { vao_.destroy(); vbo_.destroy(); } void LinePainter::Setup() { vao_.destroy(); vbo_.destroy(); if (shader_program_.isLinked()) { shader_program_.release(); shader_program_.removeAllShaders(); } shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/lines.v.glsl"); shader_program_.addShaderFromSourceFile(QOpenGLShader::Geometry, ":/shaders/lines.g.glsl"); shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/lines.f.glsl"); shader_program_.link(); shader_program_.bind(); vao_.create(); vbo_.create(); #if DEBUG glDebugLog(); #endif } void LinePainter::Upload(const std::vector& data) { num_geoms_ = data.size(); if (num_geoms_ == 0) { return; } vao_.bind(); vbo_.bind(); // Upload data array to GPU vbo_.setUsagePattern(QOpenGLBuffer::DynamicDraw); vbo_.allocate(data.data(), static_cast(data.size() * sizeof(LinePainter::Data))); // in_position shader_program_.enableAttributeArray("a_pos"); shader_program_.setAttributeBuffer( "a_pos", GL_FLOAT, 0, 3, sizeof(PointPainter::Data)); // in_color shader_program_.enableAttributeArray("a_color"); shader_program_.setAttributeBuffer( "a_color", GL_FLOAT, 3 * sizeof(GLfloat), 4, sizeof(PointPainter::Data)); // Make sure they are not changed from the outside vbo_.release(); vao_.release(); #if DEBUG glDebugLog(); #endif } void LinePainter::Render(const QMatrix4x4& pmv_matrix, const int width, const int height, const float line_width) { if (num_geoms_ == 0) { return; } shader_program_.bind(); vao_.bind(); shader_program_.setUniformValue("u_pmv_matrix", pmv_matrix); shader_program_.setUniformValue("u_inv_viewport", QVector2D(1.0f / width, 1.0f / height)); shader_program_.setUniformValue("u_line_width", line_width); QOpenGLFunctions* gl_funcs = QOpenGLContext::currentContext()->functions(); gl_funcs->glDrawArrays(GL_LINES, 0, (GLsizei)(2 * num_geoms_)); // Make sure the VAO is not changed from the outside vao_.release(); #if DEBUG glDebugLog(); #endif } } // namespace colmap colmap-3.9.1/src/colmap/ui/line_painter.h000066400000000000000000000045021454702036400203060ustar00rootroot00000000000000// 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. #pragma once #include "colmap/ui/point_painter.h" #include #include namespace colmap { class LinePainter { public: LinePainter(); ~LinePainter(); struct Data { Data() {} Data(const PointPainter::Data& p1, const PointPainter::Data& p2) : point1(p1), point2(p2) {} PointPainter::Data point1; PointPainter::Data point2; }; void Setup(); void Upload(const std::vector& data); void Render(const QMatrix4x4& pmv_matrix, int width, int height, float line_width); private: QOpenGLShaderProgram shader_program_; QOpenGLVertexArrayObject vao_; QOpenGLBuffer vbo_; size_t num_geoms_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/log_widget.cc000066400000000000000000000112501454702036400201150ustar00rootroot00000000000000// 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. #include "colmap/ui/log_widget.h" namespace colmap { namespace { class GlogSink : public google::LogSink { public: explicit GlogSink(LogWidget* log_widget) : log_widget_(CHECK_NOTNULL(log_widget)) { google::AddLogSink(this); } ~GlogSink() { google::RemoveLogSink(this); } void send(google::LogSeverity severity, const char* /*full_filename*/, const char* /*base_filename*/, int /*line*/, const struct ::tm* /*tm_time*/, const char* message, size_t message_len) override { if (severity != google::GLOG_INFO && severity != google::GLOG_WARNING && severity != google::GLOG_ERROR) { return; } const int severity_len = (severity == google::GLOG_WARNING || severity == google::GLOG_ERROR) ? 3 : 0; std::string text(message_len + 1 + severity_len, '\0'); std::copy(message + severity_len, message + message_len + severity_len, text.begin()); text.back() = '\n'; switch (severity) { case google::GLOG_WARNING: text[0] = 'W'; text[1] = ':'; text[2] = ' '; break; case google::GLOG_ERROR: text[0] = 'E'; text[1] = ':'; text[2] = ' '; break; } log_widget_->Append(text); } private: LogWidget* log_widget_; }; } // namespace LogWidget::LogWidget(QWidget* parent, const int max_num_blocks) { setWindowFlags(Qt::Window); setWindowTitle("Log"); resize(320, parent->height()); QGridLayout* grid = new QGridLayout(this); grid->setContentsMargins(5, 10, 5, 5); qRegisterMetaType("QTextCursor"); qRegisterMetaType("QTextBlock"); QTimer* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &LogWidget::Flush); timer->start(100); log_sink_ = std::make_unique(this); QHBoxLayout* left_button_layout = new QHBoxLayout(); QPushButton* clear_button = new QPushButton(tr("Clear"), this); connect(clear_button, &QPushButton::released, this, &LogWidget::Clear); left_button_layout->addWidget(clear_button); grid->addLayout(left_button_layout, 0, 0, Qt::AlignLeft); QHBoxLayout* right_button_layout = new QHBoxLayout(); grid->addLayout(right_button_layout, 0, 1, Qt::AlignRight); text_box_ = new QPlainTextEdit(this); text_box_->setReadOnly(true); text_box_->setMaximumBlockCount(max_num_blocks); text_box_->setWordWrapMode(QTextOption::NoWrap); text_box_->setFont(QFont("Courier", 10)); grid->addWidget(text_box_, 1, 0, 1, 2); } void LogWidget::Append(const std::string& text) { QMutexLocker locker(&mutex_); text_queue_ += text; } void LogWidget::Flush() { QMutexLocker locker(&mutex_); if (text_queue_.size() > 0) { // Write to log widget text_box_->moveCursor(QTextCursor::End); text_box_->insertPlainText(QString::fromStdString(text_queue_)); text_box_->moveCursor(QTextCursor::End); text_queue_.clear(); } } void LogWidget::Clear() { QMutexLocker locker(&mutex_); text_queue_.clear(); text_box_->clear(); } } // namespace colmap colmap-3.9.1/src/colmap/ui/log_widget.h000066400000000000000000000041261454702036400177630ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include #include #include #include namespace colmap { class LogWidget : public QWidget { public: explicit LogWidget(QWidget* parent, int max_num_blocks = 100000); void Append(const std::string& text); void Flush(); void Clear(); private: QMutex mutex_; std::string text_queue_; QPlainTextEdit* text_box_; std::unique_ptr log_sink_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/main_window.cc000066400000000000000000001344531454702036400203170ustar00rootroot00000000000000// 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. #include "colmap/ui/main_window.h" #include "colmap/util/version.h" #include namespace colmap { MainWindow::MainWindow(const OptionManager& options) : options_(options), reconstruction_manager_(std::make_shared()), thread_control_widget_(new ThreadControlWidget(this)), window_closed_(false) { // NOLINTNEXTLINE(concurrency-mt-unsafe) std::setlocale(LC_NUMERIC, "C"); resize(1024, 600); UpdateWindowTitle(); CreateWidgets(); CreateActions(); CreateMenus(); CreateToolbar(); CreateStatusbar(); CreateControllers(); ShowLog(); options_.AddAllOptions(); } void MainWindow::ImportReconstruction(const std::string& path) { const size_t idx = reconstruction_manager_->Read(path); reconstruction_manager_widget_->Update(); reconstruction_manager_widget_->SelectReconstruction(idx); RenderNow(); } void MainWindow::closeEvent(QCloseEvent* event) { if (window_closed_) { event->accept(); return; } if (project_widget_->IsValid() && *options_.project_path == "") { // Project was created, but not yet saved QMessageBox::StandardButton reply; reply = QMessageBox::question( this, "", tr("You have not saved your project. Do you want to save it?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { ProjectSave(); } } QMessageBox::StandardButton reply; reply = QMessageBox::question(this, "", tr("Do you really want to quit?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::No) { event->ignore(); } else { if (mapper_controller_) { mapper_controller_->Stop(); mapper_controller_->Wait(); } log_widget_->close(); event->accept(); window_closed_ = true; } } void MainWindow::CreateWidgets() { model_viewer_widget_ = new ModelViewerWidget(this, &options_); setCentralWidget(model_viewer_widget_); project_widget_ = new ProjectWidget(this, &options_); project_widget_->SetDatabasePath(*options_.database_path); project_widget_->SetImagePath(*options_.image_path); feature_extraction_widget_ = new FeatureExtractionWidget(this, &options_); feature_matching_widget_ = new FeatureMatchingWidget(this, &options_); database_management_widget_ = new DatabaseManagementWidget(this, &options_); automatic_reconstruction_widget_ = new AutomaticReconstructionWidget(this); reconstruction_options_widget_ = new ReconstructionOptionsWidget(this, &options_); bundle_adjustment_widget_ = new BundleAdjustmentWidget(this, &options_); dense_reconstruction_widget_ = new DenseReconstructionWidget(this, &options_); render_options_widget_ = new RenderOptionsWidget(this, &options_, model_viewer_widget_); log_widget_ = new LogWidget(this); undistortion_widget_ = new UndistortionWidget(this, &options_); reconstruction_manager_widget_ = new ReconstructionManagerWidget(this, reconstruction_manager_); reconstruction_stats_widget_ = new ReconstructionStatsWidget(this); match_matrix_widget_ = new MatchMatrixWidget(this, &options_); license_widget_ = new LicenseWidget(this); dock_log_widget_ = new QDockWidget("Log", this); dock_log_widget_->setWidget(log_widget_); addDockWidget(Qt::RightDockWidgetArea, dock_log_widget_); } void MainWindow::CreateActions() { ////////////////////////////////////////////////////////////////////////////// // File actions ////////////////////////////////////////////////////////////////////////////// action_project_new_ = new QAction(QIcon(":/media/project-new.png"), tr("New project"), this); action_project_new_->setShortcuts(QKeySequence::New); connect( action_project_new_, &QAction::triggered, this, &MainWindow::ProjectNew); action_project_open_ = new QAction(QIcon(":/media/project-open.png"), tr("Open project"), this); action_project_open_->setShortcuts(QKeySequence::Open); connect(action_project_open_, &QAction::triggered, this, &MainWindow::ProjectOpen); action_project_edit_ = new QAction(QIcon(":/media/project-edit.png"), tr("Edit project"), this); connect(action_project_edit_, &QAction::triggered, this, &MainWindow::ProjectEdit); action_project_save_ = new QAction(QIcon(":/media/project-save.png"), tr("Save project"), this); action_project_save_->setShortcuts(QKeySequence::Save); connect(action_project_save_, &QAction::triggered, this, &MainWindow::ProjectSave); action_project_save_as_ = new QAction( QIcon(":/media/project-save-as.png"), tr("Save project as..."), this); action_project_save_as_->setShortcuts(QKeySequence::SaveAs); connect(action_project_save_as_, &QAction::triggered, this, &MainWindow::ProjectSaveAs); action_import_ = new QAction(QIcon(":/media/import.png"), tr("Import model"), this); connect(action_import_, &QAction::triggered, this, &MainWindow::Import); blocking_actions_.push_back(action_import_); action_import_from_ = new QAction( QIcon(":/media/import-from.png"), tr("Import model from..."), this); connect( action_import_from_, &QAction::triggered, this, &MainWindow::ImportFrom); blocking_actions_.push_back(action_import_from_); action_export_ = new QAction(QIcon(":/media/export.png"), tr("Export model"), this); connect(action_export_, &QAction::triggered, this, &MainWindow::Export); blocking_actions_.push_back(action_export_); action_export_all_ = new QAction( QIcon(":/media/export-all.png"), tr("Export all models"), this); connect( action_export_all_, &QAction::triggered, this, &MainWindow::ExportAll); blocking_actions_.push_back(action_export_all_); action_export_as_ = new QAction( QIcon(":/media/export-as.png"), tr("Export model as..."), this); connect(action_export_as_, &QAction::triggered, this, &MainWindow::ExportAs); blocking_actions_.push_back(action_export_as_); action_export_as_text_ = new QAction( QIcon(":/media/export-as-text.png"), tr("Export model as text"), this); connect(action_export_as_text_, &QAction::triggered, this, &MainWindow::ExportAsText); blocking_actions_.push_back(action_export_as_text_); action_quit_ = new QAction(tr("Quit"), this); connect(action_quit_, &QAction::triggered, this, &MainWindow::close); ////////////////////////////////////////////////////////////////////////////// // Processing action ////////////////////////////////////////////////////////////////////////////// action_feature_extraction_ = new QAction( QIcon(":/media/feature-extraction.png"), tr("Feature extraction"), this); connect(action_feature_extraction_, &QAction::triggered, this, &MainWindow::FeatureExtraction); blocking_actions_.push_back(action_feature_extraction_); action_feature_matching_ = new QAction( QIcon(":/media/feature-matching.png"), tr("Feature matching"), this); connect(action_feature_matching_, &QAction::triggered, this, &MainWindow::FeatureMatching); blocking_actions_.push_back(action_feature_matching_); action_database_management_ = new QAction(QIcon(":/media/database-management.png"), tr("Database management"), this); connect(action_database_management_, &QAction::triggered, this, &MainWindow::DatabaseManagement); blocking_actions_.push_back(action_database_management_); ////////////////////////////////////////////////////////////////////////////// // Reconstruction actions ////////////////////////////////////////////////////////////////////////////// action_automatic_reconstruction_ = new QAction(QIcon(":/media/automatic-reconstruction.png"), tr("Automatic reconstruction"), this); connect(action_automatic_reconstruction_, &QAction::triggered, this, &MainWindow::AutomaticReconstruction); action_reconstruction_start_ = new QAction(QIcon(":/media/reconstruction-start.png"), tr("Start reconstruction"), this); connect(action_reconstruction_start_, &QAction::triggered, this, &MainWindow::ReconstructionStart); blocking_actions_.push_back(action_reconstruction_start_); action_reconstruction_step_ = new QAction(QIcon(":/media/reconstruction-step.png"), tr("Reconstruct next image"), this); connect(action_reconstruction_step_, &QAction::triggered, this, &MainWindow::ReconstructionStep); blocking_actions_.push_back(action_reconstruction_step_); action_reconstruction_pause_ = new QAction(QIcon(":/media/reconstruction-pause.png"), tr("Pause reconstruction"), this); connect(action_reconstruction_pause_, &QAction::triggered, this, &MainWindow::ReconstructionPause); action_reconstruction_pause_->setEnabled(false); blocking_actions_.push_back(action_reconstruction_pause_); action_reconstruction_reset_ = new QAction(QIcon(":/media/reconstruction-reset.png"), tr("Reset reconstruction"), this); connect(action_reconstruction_reset_, &QAction::triggered, this, &MainWindow::ReconstructionOverwrite); action_reconstruction_normalize_ = new QAction(QIcon(":/media/reconstruction-normalize.png"), tr("Normalize reconstruction"), this); connect(action_reconstruction_normalize_, &QAction::triggered, this, &MainWindow::ReconstructionNormalize); blocking_actions_.push_back(action_reconstruction_normalize_); action_reconstruction_options_ = new QAction(QIcon(":/media/reconstruction-options.png"), tr("Reconstruction options"), this); connect(action_reconstruction_options_, &QAction::triggered, this, &MainWindow::ReconstructionOptions); blocking_actions_.push_back(action_reconstruction_options_); action_bundle_adjustment_ = new QAction( QIcon(":/media/bundle-adjustment.png"), tr("Bundle adjustment"), this); connect(action_bundle_adjustment_, &QAction::triggered, this, &MainWindow::BundleAdjustment); action_bundle_adjustment_->setEnabled(false); blocking_actions_.push_back(action_bundle_adjustment_); action_dense_reconstruction_ = new QAction(QIcon(":/media/dense-reconstruction.png"), tr("Dense reconstruction"), this); connect(action_dense_reconstruction_, &QAction::triggered, this, &MainWindow::DenseReconstruction); ////////////////////////////////////////////////////////////////////////////// // Render actions ////////////////////////////////////////////////////////////////////////////// action_render_toggle_ = new QAction( QIcon(":/media/render-enabled.png"), tr("Disable rendering"), this); connect(action_render_toggle_, &QAction::triggered, this, &MainWindow::RenderToggle); action_render_reset_view_ = new QAction( QIcon(":/media/render-reset-view.png"), tr("Reset view"), this); connect(action_render_reset_view_, &QAction::triggered, model_viewer_widget_, &ModelViewerWidget::ResetView); action_render_options_ = new QAction( QIcon(":/media/render-options.png"), tr("Render options"), this); connect(action_render_options_, &QAction::triggered, this, &MainWindow::RenderOptions); connect( reconstruction_manager_widget_, static_cast(&QComboBox::currentIndexChanged), this, &MainWindow::SelectReconstructionIdx); ////////////////////////////////////////////////////////////////////////////// // Extras actions ////////////////////////////////////////////////////////////////////////////// action_reconstruction_stats_ = new QAction(QIcon(":/media/reconstruction-stats.png"), tr("Show model statistics"), this); connect(action_reconstruction_stats_, &QAction::triggered, this, &MainWindow::ReconstructionStats); action_match_matrix_ = new QAction( QIcon(":/media/match-matrix.png"), tr("Show match matrix"), this); connect(action_match_matrix_, &QAction::triggered, this, &MainWindow::MatchMatrix); action_log_show_ = new QAction(QIcon(":/media/log.png"), tr("Show log"), this); connect(action_log_show_, &QAction::triggered, this, &MainWindow::ShowLog); action_grab_image_ = new QAction(QIcon(":/media/grab-image.png"), tr("Grab image"), this); connect( action_grab_image_, &QAction::triggered, this, &MainWindow::GrabImage); action_grab_movie_ = new QAction(QIcon(":/media/grab-movie.png"), tr("Grab movie"), this); connect(action_grab_movie_, &QAction::triggered, model_viewer_widget_, &ModelViewerWidget::GrabMovie); action_undistort_ = new QAction(QIcon(":/media/undistort.png"), tr("Undistortion"), this); connect(action_undistort_, &QAction::triggered, this, &MainWindow::UndistortImages); blocking_actions_.push_back(action_undistort_); action_extract_colors_ = new QAction(tr("Extract colors"), this); connect(action_extract_colors_, &QAction::triggered, this, &MainWindow::ExtractColors); action_set_options_ = new QAction(tr("Set options for ..."), this); connect( action_set_options_, &QAction::triggered, this, &MainWindow::SetOptions); action_reset_options_ = new QAction(tr("Set default options"), this); connect(action_reset_options_, &QAction::triggered, this, &MainWindow::ResetOptions); ////////////////////////////////////////////////////////////////////////////// // Misc actions ////////////////////////////////////////////////////////////////////////////// action_render_ = new QAction(tr("Render"), this); connect(action_render_, &QAction::triggered, this, &MainWindow::Render, Qt::BlockingQueuedConnection); action_render_now_ = new QAction(tr("Render now"), this); render_options_widget_->action_render_now = action_render_now_; connect(action_render_now_, &QAction::triggered, this, &MainWindow::RenderNow, Qt::BlockingQueuedConnection); action_reconstruction_finish_ = new QAction(tr("Finish reconstruction"), this); connect(action_reconstruction_finish_, &QAction::triggered, this, &MainWindow::ReconstructionFinish, Qt::BlockingQueuedConnection); action_about_ = new QAction(tr("About"), this); connect(action_about_, &QAction::triggered, this, &MainWindow::About); action_documentation_ = new QAction(tr("Documentation"), this); connect(action_documentation_, &QAction::triggered, this, &MainWindow::Documentation); action_support_ = new QAction(tr("Support"), this); connect(action_support_, &QAction::triggered, this, &MainWindow::Support); action_license_ = new QAction(tr("License"), this); connect( action_license_, &QAction::triggered, license_widget_, &QTextEdit::show); } void MainWindow::CreateMenus() { QMenu* file_menu = new QMenu(tr("File"), this); file_menu->addAction(action_project_new_); file_menu->addAction(action_project_open_); file_menu->addAction(action_project_edit_); file_menu->addAction(action_project_save_); file_menu->addAction(action_project_save_as_); file_menu->addSeparator(); file_menu->addAction(action_import_); file_menu->addAction(action_import_from_); file_menu->addSeparator(); file_menu->addAction(action_export_); file_menu->addAction(action_export_all_); file_menu->addAction(action_export_as_); file_menu->addAction(action_export_as_text_); file_menu->addSeparator(); file_menu->addAction(action_quit_); menuBar()->addAction(file_menu->menuAction()); QMenu* preprocessing_menu = new QMenu(tr("Processing"), this); preprocessing_menu->addAction(action_feature_extraction_); preprocessing_menu->addAction(action_feature_matching_); preprocessing_menu->addAction(action_database_management_); menuBar()->addAction(preprocessing_menu->menuAction()); QMenu* reconstruction_menu = new QMenu(tr("Reconstruction"), this); reconstruction_menu->addAction(action_automatic_reconstruction_); reconstruction_menu->addSeparator(); reconstruction_menu->addAction(action_reconstruction_start_); reconstruction_menu->addAction(action_reconstruction_pause_); reconstruction_menu->addAction(action_reconstruction_step_); reconstruction_menu->addSeparator(); reconstruction_menu->addAction(action_reconstruction_reset_); reconstruction_menu->addAction(action_reconstruction_normalize_); reconstruction_menu->addAction(action_reconstruction_options_); reconstruction_menu->addSeparator(); reconstruction_menu->addAction(action_bundle_adjustment_); reconstruction_menu->addAction(action_dense_reconstruction_); menuBar()->addAction(reconstruction_menu->menuAction()); QMenu* render_menu = new QMenu(tr("Render"), this); render_menu->addAction(action_render_toggle_); render_menu->addAction(action_render_reset_view_); render_menu->addAction(action_render_options_); menuBar()->addAction(render_menu->menuAction()); QMenu* extras_menu = new QMenu(tr("Extras"), this); extras_menu->addAction(action_log_show_); extras_menu->addAction(action_match_matrix_); extras_menu->addAction(action_reconstruction_stats_); extras_menu->addSeparator(); extras_menu->addAction(action_grab_image_); extras_menu->addAction(action_grab_movie_); extras_menu->addSeparator(); extras_menu->addAction(action_undistort_); extras_menu->addAction(action_extract_colors_); extras_menu->addSeparator(); extras_menu->addAction(action_set_options_); extras_menu->addAction(action_reset_options_); menuBar()->addAction(extras_menu->menuAction()); QMenu* help_menu = new QMenu(tr("Help"), this); help_menu->addAction(action_about_); help_menu->addAction(action_documentation_); help_menu->addAction(action_support_); help_menu->addAction(action_license_); menuBar()->addAction(help_menu->menuAction()); // TODO: Make the native menu bar work on OSX. Simply setting this to true // will result in a menubar which is not clickable until the main window is // defocused and refocused. menuBar()->setNativeMenuBar(false); } void MainWindow::CreateToolbar() { file_toolbar_ = addToolBar(tr("File")); file_toolbar_->addAction(action_project_new_); file_toolbar_->addAction(action_project_open_); file_toolbar_->addAction(action_project_edit_); file_toolbar_->addAction(action_project_save_); file_toolbar_->addAction(action_import_); file_toolbar_->addAction(action_export_); file_toolbar_->setIconSize(QSize(16, 16)); preprocessing_toolbar_ = addToolBar(tr("Processing")); preprocessing_toolbar_->addAction(action_feature_extraction_); preprocessing_toolbar_->addAction(action_feature_matching_); preprocessing_toolbar_->addAction(action_database_management_); preprocessing_toolbar_->setIconSize(QSize(16, 16)); reconstruction_toolbar_ = addToolBar(tr("Reconstruction")); reconstruction_toolbar_->addAction(action_automatic_reconstruction_); reconstruction_toolbar_->addAction(action_reconstruction_start_); reconstruction_toolbar_->addAction(action_reconstruction_step_); reconstruction_toolbar_->addAction(action_reconstruction_pause_); reconstruction_toolbar_->addAction(action_reconstruction_options_); reconstruction_toolbar_->addAction(action_bundle_adjustment_); reconstruction_toolbar_->addAction(action_dense_reconstruction_); reconstruction_toolbar_->setIconSize(QSize(16, 16)); render_toolbar_ = addToolBar(tr("Render")); render_toolbar_->addAction(action_render_toggle_); render_toolbar_->addAction(action_render_reset_view_); render_toolbar_->addAction(action_render_options_); render_toolbar_->addWidget(reconstruction_manager_widget_); render_toolbar_->setIconSize(QSize(16, 16)); extras_toolbar_ = addToolBar(tr("Extras")); extras_toolbar_->addAction(action_log_show_); extras_toolbar_->addAction(action_match_matrix_); extras_toolbar_->addAction(action_reconstruction_stats_); extras_toolbar_->addAction(action_grab_image_); extras_toolbar_->addAction(action_grab_movie_); extras_toolbar_->setIconSize(QSize(16, 16)); } void MainWindow::CreateStatusbar() { QFont font; font.setPointSize(11); statusbar_timer_label_ = new QLabel("Time 00:00:00:00", this); statusbar_timer_label_->setFont(font); statusbar_timer_label_->setAlignment(Qt::AlignCenter); statusBar()->addWidget(statusbar_timer_label_, 1); statusbar_timer_ = new QTimer(this); connect(statusbar_timer_, &QTimer::timeout, this, &MainWindow::UpdateTimer); statusbar_timer_->start(1000); model_viewer_widget_->statusbar_status_label = new QLabel("0 Images - 0 Points", this); model_viewer_widget_->statusbar_status_label->setFont(font); model_viewer_widget_->statusbar_status_label->setAlignment(Qt::AlignCenter); statusBar()->addWidget(model_viewer_widget_->statusbar_status_label, 1); } void MainWindow::CreateControllers() { if (mapper_controller_) { mapper_controller_->Stop(); mapper_controller_->Wait(); } mapper_controller_ = std::make_unique(options_.mapper, *options_.image_path, *options_.database_path, reconstruction_manager_); mapper_controller_->AddCallback( IncrementalMapperController::INITIAL_IMAGE_PAIR_REG_CALLBACK, [this]() { if (!mapper_controller_->IsStopped()) { action_render_now_->trigger(); } }); mapper_controller_->AddCallback( IncrementalMapperController::NEXT_IMAGE_REG_CALLBACK, [this]() { if (!mapper_controller_->IsStopped()) { action_render_->trigger(); } }); mapper_controller_->AddCallback( IncrementalMapperController::LAST_IMAGE_REG_CALLBACK, [this]() { if (!mapper_controller_->IsStopped()) { action_render_now_->trigger(); } }); mapper_controller_->AddCallback( IncrementalMapperController::FINISHED_CALLBACK, [this]() { if (!mapper_controller_->IsStopped()) { action_render_now_->trigger(); action_reconstruction_finish_->trigger(); } if (reconstruction_manager_->Size() == 0) { action_reconstruction_reset_->trigger(); } }); } void MainWindow::ProjectNew() { if (ReconstructionOverwrite()) { project_widget_->Reset(); project_widget_->show(); project_widget_->raise(); } } bool MainWindow::ProjectOpen() { if (!ReconstructionOverwrite()) { return false; } const std::string project_path = QFileDialog::getOpenFileName( this, tr("Select project file"), "", tr("Project file (*.ini)")) .toUtf8() .constData(); // If selection not canceled if (project_path != "") { if (options_.ReRead(project_path)) { *options_.project_path = project_path; project_widget_->SetDatabasePath(*options_.database_path); project_widget_->SetImagePath(*options_.image_path); UpdateWindowTitle(); return true; } else { ShowInvalidProjectError(); } } return false; } void MainWindow::ProjectEdit() { project_widget_->show(); project_widget_->raise(); } void MainWindow::ProjectSave() { if (!ExistsFile(*options_.project_path)) { std::string project_path = QFileDialog::getSaveFileName( this, tr("Select project file"), "", tr("Project file (*.ini)")) .toUtf8() .constData(); // If selection not canceled if (project_path != "") { if (!HasFileExtension(project_path, ".ini")) { project_path += ".ini"; } *options_.project_path = project_path; options_.Write(*options_.project_path); } } else { // Project path was chosen previously, either here or via command-line. options_.Write(*options_.project_path); } UpdateWindowTitle(); } void MainWindow::ProjectSaveAs() { const std::string new_project_path = QFileDialog::getSaveFileName( this, tr("Select project file"), "", tr("Project file (*.ini)")) .toUtf8() .constData(); if (new_project_path != "") { *options_.project_path = new_project_path; options_.Write(*options_.project_path); } UpdateWindowTitle(); } void MainWindow::Import() { const std::string import_path = QFileDialog::getExistingDirectory( this, tr("Select source..."), "", QFileDialog::ShowDirsOnly) .toUtf8() .constData(); // Selection canceled? if (import_path == "") { return; } const std::string project_path = JoinPaths(import_path, "project.ini"); const std::string cameras_bin_path = JoinPaths(import_path, "cameras.bin"); const std::string images_bin_path = JoinPaths(import_path, "images.bin"); const std::string points3D_bin_path = JoinPaths(import_path, "points3D.bin"); const std::string cameras_txt_path = JoinPaths(import_path, "cameras.txt"); const std::string images_txt_path = JoinPaths(import_path, "images.txt"); const std::string points3D_txt_path = JoinPaths(import_path, "points3D.txt"); if ((!ExistsFile(cameras_bin_path) || !ExistsFile(images_bin_path) || !ExistsFile(points3D_bin_path)) && (!ExistsFile(cameras_txt_path) || !ExistsFile(images_txt_path) || !ExistsFile(points3D_txt_path))) { QMessageBox::critical(this, "", tr("cameras, images, and points3D files do not exist " "in chosen directory.")); return; } if (!ReconstructionOverwrite()) { return; } bool edit_project = false; if (ExistsFile(project_path)) { options_.ReRead(project_path); } else { QMessageBox::StandardButton reply = QMessageBox::question( this, "", tr("Directory does not contain a project.ini. To " "resume the reconstruction, you need to specify a valid " "database and image path. Do you want to select the paths " "now (or press No to only visualize the reconstruction)?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { edit_project = true; } } thread_control_widget_->StartFunction( "Importing...", [this, import_path, edit_project]() { const size_t idx = reconstruction_manager_->Read(import_path); reconstruction_manager_widget_->Update(); reconstruction_manager_widget_->SelectReconstruction(idx); action_bundle_adjustment_->setEnabled(true); action_render_now_->trigger(); if (edit_project) { action_project_edit_->trigger(); } }); } void MainWindow::ImportFrom() { const std::string import_path = QFileDialog::getOpenFileName(this, tr("Select source..."), "") .toUtf8() .constData(); // Selection canceled? if (import_path == "") { return; } if (!ExistsFile(import_path)) { QMessageBox::critical(this, "", tr("Invalid file")); return; } if (!HasFileExtension(import_path, ".ply")) { QMessageBox::critical( this, "", tr("Invalid file format (supported formats: PLY)")); return; } thread_control_widget_->StartFunction("Importing...", [this, import_path]() { const size_t reconstruction_idx = reconstruction_manager_->Add(); reconstruction_manager_->Get(reconstruction_idx)->ImportPLY(import_path); options_.render->min_track_len = 0; reconstruction_manager_widget_->Update(); reconstruction_manager_widget_->SelectReconstruction(reconstruction_idx); action_render_now_->trigger(); }); } void MainWindow::Export() { if (!IsSelectedReconstructionValid()) { return; } const std::string export_path = QFileDialog::getExistingDirectory( this, tr("Select destination..."), "", QFileDialog::ShowDirsOnly) .toUtf8() .constData(); // Selection canceled? if (export_path == "") { return; } const std::string cameras_name = "cameras.bin"; const std::string images_name = "images.bin"; const std::string points3D_name = "points3D.bin"; const std::string project_path = JoinPaths(export_path, "project.ini"); const std::string cameras_path = JoinPaths(export_path, cameras_name); const std::string images_path = JoinPaths(export_path, images_name); const std::string points3D_path = JoinPaths(export_path, points3D_name); if (ExistsFile(cameras_path) || ExistsFile(images_path) || ExistsFile(points3D_path)) { QMessageBox::StandardButton reply = QMessageBox::question( this, "", StringPrintf( "The files %s, %s, or %s already " "exist in the selected destination. Do you want to overwrite them?", cameras_name.c_str(), images_name.c_str(), points3D_name.c_str()) .c_str(), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::No) { return; } } thread_control_widget_->StartFunction( "Exporting...", [this, export_path, project_path]() { reconstruction_manager_->Get(SelectedReconstructionIdx()) ->WriteBinary(export_path); options_.Write(project_path); }); } void MainWindow::ExportAll() { if (!IsSelectedReconstructionValid()) { return; } const std::string export_path = QFileDialog::getExistingDirectory( this, tr("Select destination..."), "", QFileDialog::ShowDirsOnly) .toUtf8() .constData(); // Selection canceled? if (export_path == "") { return; } thread_control_widget_->StartFunction("Exporting...", [this, export_path]() { reconstruction_manager_->Write(export_path); options_.Write(JoinPaths(export_path, "project.ini")); }); } void MainWindow::ExportAs() { if (!IsSelectedReconstructionValid()) { return; } QString filter("NVM (*.nvm)"); const std::string export_path = QFileDialog::getSaveFileName( this, tr("Select destination..."), "", "NVM (*.nvm);;Bundler (*.out);;PLY (*.ply);;VRML (*.wrl)", &filter) .toUtf8() .constData(); // Selection canceled? if (export_path == "") { return; } thread_control_widget_->StartFunction( "Exporting...", [this, export_path, filter]() { const std::shared_ptr reconstruction = reconstruction_manager_->Get(SelectedReconstructionIdx()); if (filter == "NVM (*.nvm)") { reconstruction->ExportNVM(export_path); } else if (filter == "Bundler (*.out)") { reconstruction->ExportBundler(export_path, export_path + ".list.txt"); } else if (filter == "PLY (*.ply)") { reconstruction->ExportPLY(export_path); } else if (filter == "VRML (*.wrl)") { const auto base_path = export_path.substr(0, export_path.find_last_of('.')); reconstruction->ExportVRML(base_path + ".images.wrl", base_path + ".points3D.wrl", 1, Eigen::Vector3d(1, 0, 0)); } }); } void MainWindow::ExportAsText() { if (!IsSelectedReconstructionValid()) { return; } const std::string export_path = QFileDialog::getExistingDirectory( this, tr("Select destination..."), "", QFileDialog::ShowDirsOnly) .toUtf8() .constData(); // Selection canceled? if (export_path == "") { return; } const std::string cameras_name = "cameras.txt"; const std::string images_name = "images.txt"; const std::string points3D_name = "points3D.txt"; const std::string project_path = JoinPaths(export_path, "project.ini"); const std::string cameras_path = JoinPaths(export_path, cameras_name); const std::string images_path = JoinPaths(export_path, images_name); const std::string points3D_path = JoinPaths(export_path, points3D_name); if (ExistsFile(cameras_path) || ExistsFile(images_path) || ExistsFile(points3D_path)) { QMessageBox::StandardButton reply = QMessageBox::question( this, "", StringPrintf( "The files %s, %s, or %s already " "exist in the selected destination. Do you want to overwrite them?", cameras_name.c_str(), images_name.c_str(), points3D_name.c_str()) .c_str(), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::No) { return; } } thread_control_widget_->StartFunction( "Exporting...", [this, export_path, project_path]() { reconstruction_manager_->Get(SelectedReconstructionIdx()) ->WriteText(export_path); options_.Write(project_path); }); } void MainWindow::FeatureExtraction() { if (options_.Check()) { feature_extraction_widget_->show(); feature_extraction_widget_->raise(); } else { ShowInvalidProjectError(); } } void MainWindow::FeatureMatching() { if (options_.Check()) { feature_matching_widget_->show(); feature_matching_widget_->raise(); } else { ShowInvalidProjectError(); } } void MainWindow::DatabaseManagement() { if (options_.Check()) { database_management_widget_->show(); database_management_widget_->raise(); } else { ShowInvalidProjectError(); } } void MainWindow::AutomaticReconstruction() { automatic_reconstruction_widget_->show(); automatic_reconstruction_widget_->raise(); } void MainWindow::ReconstructionStart() { if (!mapper_controller_->IsStarted() && !options_.Check()) { ShowInvalidProjectError(); return; } if (mapper_controller_->IsFinished() && HasSelectedReconstruction()) { QMessageBox::critical( this, "", tr("Reset reconstruction before starting.")); return; } if (mapper_controller_->IsStarted()) { // Resume existing reconstruction. timer_.Resume(); mapper_controller_->Resume(); } else { // Start new reconstruction. CreateControllers(); timer_.Restart(); mapper_controller_->Start(); action_reconstruction_start_->setText(tr("Resume reconstruction")); } DisableBlockingActions(); action_reconstruction_pause_->setEnabled(true); } void MainWindow::ReconstructionStep() { if (mapper_controller_->IsFinished() && HasSelectedReconstruction()) { QMessageBox::critical( this, "", tr("Reset reconstruction before starting.")); return; } action_reconstruction_step_->setEnabled(false); ReconstructionStart(); ReconstructionPause(); action_reconstruction_step_->setEnabled(true); } void MainWindow::ReconstructionPause() { timer_.Pause(); mapper_controller_->Pause(); EnableBlockingActions(); action_reconstruction_pause_->setEnabled(false); } void MainWindow::ReconstructionOptions() { reconstruction_options_widget_->show(); reconstruction_options_widget_->raise(); } void MainWindow::ReconstructionFinish() { timer_.Pause(); mapper_controller_->Stop(); EnableBlockingActions(); action_reconstruction_start_->setEnabled(false); action_reconstruction_step_->setEnabled(false); action_reconstruction_pause_->setEnabled(false); } void MainWindow::ReconstructionReset() { CreateControllers(); reconstruction_manager_->Clear(); reconstruction_manager_widget_->Update(); timer_.Reset(); UpdateTimer(); EnableBlockingActions(); action_reconstruction_start_->setText(tr("Start reconstruction")); action_reconstruction_pause_->setEnabled(false); RenderClear(); } void MainWindow::ReconstructionNormalize() { if (!IsSelectedReconstructionValid()) { return; } action_reconstruction_step_->setEnabled(false); reconstruction_manager_->Get(SelectedReconstructionIdx())->Normalize(); action_reconstruction_step_->setEnabled(true); } bool MainWindow::ReconstructionOverwrite() { if (reconstruction_manager_->Size() == 0) { ReconstructionReset(); return true; } QMessageBox::StandardButton reply = QMessageBox::question( this, "", tr("Do you really want to overwrite the existing reconstruction?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::No) { return false; } else { ReconstructionReset(); return true; } } void MainWindow::BundleAdjustment() { if (!IsSelectedReconstructionValid()) { return; } bundle_adjustment_widget_->Show( reconstruction_manager_->Get(SelectedReconstructionIdx())); } void MainWindow::DenseReconstruction() { if (HasSelectedReconstruction()) { dense_reconstruction_widget_->Show( reconstruction_manager_->Get(SelectedReconstructionIdx())); } else { dense_reconstruction_widget_->Show(nullptr); } } void MainWindow::Render() { if (reconstruction_manager_->Size() == 0) { return; } int refresh_rate; if (options_.render->adapt_refresh_rate) { const auto num_reg_images = reconstruction_manager_->Get(SelectedReconstructionIdx()) ->NumRegImages(); refresh_rate = static_cast(num_reg_images / 50 + 1); } else { refresh_rate = options_.render->refresh_rate; } if (!render_options_widget_->automatic_update || render_options_widget_->counter % refresh_rate != 0) { render_options_widget_->counter += 1; return; } render_options_widget_->counter += 1; RenderNow(); } void MainWindow::RenderNow() { reconstruction_manager_widget_->Update(); RenderSelectedReconstruction(); } void MainWindow::RenderSelectedReconstruction() { if (reconstruction_manager_->Size() == 0) { RenderClear(); return; } const size_t reconstruction_idx = SelectedReconstructionIdx(); model_viewer_widget_->reconstruction = reconstruction_manager_->Get(reconstruction_idx); model_viewer_widget_->ReloadReconstruction(); } void MainWindow::RenderClear() { reconstruction_manager_widget_->SelectReconstruction( ReconstructionManagerWidget::kNewestReconstructionIdx); model_viewer_widget_->ClearReconstruction(); } void MainWindow::RenderOptions() { render_options_widget_->show(); render_options_widget_->raise(); } void MainWindow::SelectReconstructionIdx(const size_t) { RenderSelectedReconstruction(); } size_t MainWindow::SelectedReconstructionIdx() { size_t reconstruction_idx = reconstruction_manager_widget_->SelectedReconstructionIdx(); if (reconstruction_idx == ReconstructionManagerWidget::kNewestReconstructionIdx) { if (reconstruction_manager_->Size() > 0) { reconstruction_idx = reconstruction_manager_->Size() - 1; } } return reconstruction_idx; } bool MainWindow::HasSelectedReconstruction() { const size_t reconstruction_idx = reconstruction_manager_widget_->SelectedReconstructionIdx(); if (reconstruction_idx == ReconstructionManagerWidget::kNewestReconstructionIdx) { if (reconstruction_manager_->Size() == 0) { return false; } } return true; } bool MainWindow::IsSelectedReconstructionValid() { if (!HasSelectedReconstruction()) { QMessageBox::critical(this, "", tr("No reconstruction selected")); return false; } return true; } void MainWindow::GrabImage() { QString file_name = QFileDialog::getSaveFileName( this, tr("Save image"), "", tr("Images (*.png *.jpg)")); if (file_name != "") { if (!HasFileExtension(file_name.toUtf8().constData(), ".png") && !HasFileExtension(file_name.toUtf8().constData(), ".jpg")) { file_name += ".png"; } QImage image = model_viewer_widget_->GrabImage(); image.save(file_name); } } void MainWindow::UndistortImages() { if (!IsSelectedReconstructionValid()) { return; } undistortion_widget_->Show( reconstruction_manager_->Get(SelectedReconstructionIdx())); } void MainWindow::ReconstructionStats() { if (!IsSelectedReconstructionValid()) { return; } reconstruction_stats_widget_->show(); reconstruction_stats_widget_->raise(); reconstruction_stats_widget_->Show( *reconstruction_manager_->Get(SelectedReconstructionIdx())); } void MainWindow::MatchMatrix() { match_matrix_widget_->Show(); } void MainWindow::ShowLog() { log_widget_->show(); log_widget_->raise(); dock_log_widget_->show(); dock_log_widget_->raise(); } void MainWindow::ExtractColors() { if (!IsSelectedReconstructionValid()) { return; } thread_control_widget_->StartFunction("Extracting colors...", [this]() { reconstruction_manager_->Get(SelectedReconstructionIdx()) ->ExtractColorsForAllImages(*options_.image_path); }); } void MainWindow::SetOptions() { QStringList data_items; data_items << "Individual images" << "Video frames" << "Internet images"; bool data_ok; const QString data_item = QInputDialog::getItem(this, "", "Data:", data_items, 0, false, &data_ok); if (!data_ok) { return; } QStringList quality_items; quality_items << "Low" << "Medium" << "High" << "Extreme"; bool quality_ok; const QString quality_item = QInputDialog::getItem( this, "", "Quality:", quality_items, 2, false, &quality_ok); if (!quality_ok) { return; } const bool kResetPaths = false; options_.ResetOptions(kResetPaths); if (data_item == "Individual images") { options_.ModifyForIndividualData(); } else if (data_item == "Video frames") { options_.ModifyForVideoData(); } else if (data_item == "Internet images") { options_.ModifyForInternetData(); } else { LOG(FATAL) << "Data type does not exist"; } if (quality_item == "Low") { options_.ModifyForLowQuality(); } else if (quality_item == "Medium") { options_.ModifyForMediumQuality(); } else if (quality_item == "High") { options_.ModifyForHighQuality(); } else if (quality_item == "Extreme") { options_.ModifyForExtremeQuality(); } else { LOG(FATAL) << "Quality level does not exist"; } } void MainWindow::ResetOptions() { const bool kResetPaths = false; options_.ResetOptions(kResetPaths); } void MainWindow::About() { QMessageBox::about( this, tr("About"), QString().asprintf("%s
" "(%s)

" "Author: Johannes L. Schönberger

" "Email: jsch-at-demuc-dot-de
", GetVersionInfo().c_str(), GetBuildInfo().c_str())); } void MainWindow::Documentation() { QDesktopServices::openUrl(QUrl("https://colmap.github.io/")); } void MainWindow::Support() { QDesktopServices::openUrl( QUrl("https://github.com/colmap/colmap/discussions")); } void MainWindow::RenderToggle() { if (render_options_widget_->automatic_update) { render_options_widget_->automatic_update = false; render_options_widget_->counter = 0; action_render_toggle_->setIcon(QIcon(":/media/render-disabled.png")); action_render_toggle_->setText(tr("Enable rendering")); } else { render_options_widget_->automatic_update = true; render_options_widget_->counter = 0; Render(); action_render_toggle_->setIcon(QIcon(":/media/render-enabled.png")); action_render_toggle_->setText(tr("Disable rendering")); } } void MainWindow::UpdateTimer() { const int elapsed_time = static_cast(timer_.ElapsedSeconds()); const int seconds = elapsed_time % 60; const int minutes = (elapsed_time / 60) % 60; const int hours = (elapsed_time / 3600) % 24; const int days = elapsed_time / 86400; statusbar_timer_label_->setText(QString().asprintf( "Time %02d:%02d:%02d:%02d", days, hours, minutes, seconds)); } void MainWindow::ShowInvalidProjectError() { QMessageBox::critical(this, "", tr("You must create a valid project using: File > " "New project or File > Edit project")); } void MainWindow::EnableBlockingActions() { for (auto& action : blocking_actions_) { action->setEnabled(true); } } void MainWindow::DisableBlockingActions() { for (auto& action : blocking_actions_) { action->setDisabled(true); } } void MainWindow::UpdateWindowTitle() { if (*options_.project_path == "") { setWindowTitle(QString::fromStdString("COLMAP")); } else { std::string project_title = *options_.project_path; if (project_title.size() > 80) { project_title = "..." + project_title.substr(project_title.size() - 77, 77); } setWindowTitle(QString::fromStdString("COLMAP - " + project_title)); } } } // namespace colmap colmap-3.9.1/src/colmap/ui/main_window.h000066400000000000000000000163301454702036400201520ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/incremental_mapper.h" #include "colmap/scene/reconstruction.h" #include "colmap/sensor/bitmap.h" #include "colmap/ui/automatic_reconstruction_widget.h" #include "colmap/ui/bundle_adjustment_widget.h" #include "colmap/ui/database_management_widget.h" #include "colmap/ui/dense_reconstruction_widget.h" #include "colmap/ui/feature_extraction_widget.h" #include "colmap/ui/feature_matching_widget.h" #include "colmap/ui/license_widget.h" #include "colmap/ui/log_widget.h" #include "colmap/ui/match_matrix_widget.h" #include "colmap/ui/model_viewer_widget.h" #include "colmap/ui/project_widget.h" #include "colmap/ui/reconstruction_manager_widget.h" #include "colmap/ui/reconstruction_options_widget.h" #include "colmap/ui/reconstruction_stats_widget.h" #include "colmap/ui/render_options_widget.h" #include "colmap/ui/undistortion_widget.h" #include #include #include #include namespace colmap { class MainWindow : public QMainWindow { public: explicit MainWindow(const OptionManager& options); void ImportReconstruction(const std::string& path); protected: void closeEvent(QCloseEvent* event); private: friend class AutomaticReconstructionWidget; friend class BundleAdjustmentWidget; friend class DenseReconstructionWidget; void CreateWidgets(); void CreateActions(); void CreateMenus(); void CreateToolbar(); void CreateStatusbar(); void CreateControllers(); void ProjectNew(); bool ProjectOpen(); void ProjectEdit(); void ProjectSave(); void ProjectSaveAs(); void Import(); void ImportFrom(); void Export(); void ExportAll(); void ExportAs(); void ExportAsText(); void FeatureExtraction(); void FeatureMatching(); void DatabaseManagement(); void AutomaticReconstruction(); void ReconstructionStart(); void ReconstructionStep(); void ReconstructionPause(); void ReconstructionReset(); void ReconstructionOptions(); void ReconstructionFinish(); void ReconstructionNormalize(); bool ReconstructionOverwrite(); void BundleAdjustment(); void DenseReconstruction(); void Render(); void RenderNow(); void RenderToggle(); void RenderOptions(); void RenderSelectedReconstruction(); void RenderClear(); void SelectReconstructionIdx(size_t); size_t SelectedReconstructionIdx(); bool HasSelectedReconstruction(); bool IsSelectedReconstructionValid(); void GrabImage(); void UndistortImages(); void ReconstructionStats(); void MatchMatrix(); void ShowLog(); void ExtractColors(); void SetOptions(); void ResetOptions(); void About(); void Documentation(); void Support(); void ShowInvalidProjectError(); void UpdateTimer(); void EnableBlockingActions(); void DisableBlockingActions(); void UpdateWindowTitle(); OptionManager options_; std::shared_ptr reconstruction_manager_; std::unique_ptr mapper_controller_; Timer timer_; ModelViewerWidget* model_viewer_widget_; ProjectWidget* project_widget_; FeatureExtractionWidget* feature_extraction_widget_; FeatureMatchingWidget* feature_matching_widget_; DatabaseManagementWidget* database_management_widget_; AutomaticReconstructionWidget* automatic_reconstruction_widget_; ReconstructionOptionsWidget* reconstruction_options_widget_; BundleAdjustmentWidget* bundle_adjustment_widget_; DenseReconstructionWidget* dense_reconstruction_widget_; RenderOptionsWidget* render_options_widget_; LogWidget* log_widget_; UndistortionWidget* undistortion_widget_; ReconstructionManagerWidget* reconstruction_manager_widget_; ReconstructionStatsWidget* reconstruction_stats_widget_; MatchMatrixWidget* match_matrix_widget_; LicenseWidget* license_widget_; ThreadControlWidget* thread_control_widget_; QToolBar* file_toolbar_; QToolBar* preprocessing_toolbar_; QToolBar* reconstruction_toolbar_; QToolBar* render_toolbar_; QToolBar* extras_toolbar_; QDockWidget* dock_log_widget_; QTimer* statusbar_timer_; QLabel* statusbar_timer_label_; QAction* action_project_new_; QAction* action_project_open_; QAction* action_project_edit_; QAction* action_project_save_; QAction* action_project_save_as_; QAction* action_import_; QAction* action_import_from_; QAction* action_export_; QAction* action_export_all_; QAction* action_export_as_; QAction* action_export_as_text_; QAction* action_quit_; QAction* action_feature_extraction_; QAction* action_feature_matching_; QAction* action_database_management_; QAction* action_automatic_reconstruction_; QAction* action_reconstruction_start_; QAction* action_reconstruction_step_; QAction* action_reconstruction_pause_; QAction* action_reconstruction_reset_; QAction* action_reconstruction_finish_; QAction* action_reconstruction_normalize_; QAction* action_reconstruction_options_; QAction* action_bundle_adjustment_; QAction* action_dense_reconstruction_; QAction* action_render_; QAction* action_render_now_; QAction* action_render_toggle_; QAction* action_render_reset_view_; QAction* action_render_options_; QAction* action_reconstruction_stats_; QAction* action_match_matrix_; QAction* action_log_show_; QAction* action_grab_image_; QAction* action_grab_movie_; QAction* action_undistort_; QAction* action_extract_colors_; QAction* action_set_options_; QAction* action_reset_options_; QAction* action_about_; QAction* action_documentation_; QAction* action_support_; QAction* action_license_; std::vector blocking_actions_; // Necessary for OS X to avoid duplicate closeEvents. bool window_closed_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/match_matrix_widget.cc000066400000000000000000000071141454702036400220200ustar00rootroot00000000000000// 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. #include "colmap/ui/match_matrix_widget.h" namespace colmap { MatchMatrixWidget::MatchMatrixWidget(QWidget* parent, OptionManager* options) : ImageViewerWidget(parent), options_(options) { setWindowTitle("Match matrix"); } void MatchMatrixWidget::Show() { Database database(*options_->database_path); if (database.NumImages() == 0) { return; } // Sort the images according to their name. std::vector images = database.ReadAllImages(); std::sort(images.begin(), images.end(), [](const Image& image1, const Image& image2) { return image1.Name() < image2.Name(); }); // Allocate the match matrix image. Bitmap match_matrix; match_matrix.Allocate(images.size(), images.size(), true); match_matrix.Fill(BitmapColor(255)); // Map image identifiers to match matrix locations. std::unordered_map image_id_to_idx; for (size_t idx = 0; idx < images.size(); ++idx) { image_id_to_idx.emplace(images[idx].ImageId(), idx); } std::vector> image_pairs; std::vector num_inliers; database.ReadTwoViewGeometryNumInliers(&image_pairs, &num_inliers); // Fill the match matrix. if (!num_inliers.empty()) { const double max_value = std::log1p(*std::max_element(num_inliers.begin(), num_inliers.end())); for (size_t i = 0; i < image_pairs.size(); ++i) { const double value = std::log1p(num_inliers[i]) / max_value; const size_t idx1 = image_id_to_idx.at(image_pairs[i].first); const size_t idx2 = image_id_to_idx.at(image_pairs[i].second); const BitmapColor color(255 * JetColormap::Red(value), 255 * JetColormap::Green(value), 255 * JetColormap::Blue(value)); match_matrix.SetPixel(idx1, idx2, color.Cast()); match_matrix.SetPixel(idx2, idx1, color.Cast()); } } ShowBitmap(match_matrix); } } // namespace colmap colmap-3.9.1/src/colmap/ui/match_matrix_widget.h000066400000000000000000000037171454702036400216670ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/ui/image_viewer_widget.h" namespace colmap { // Widget to visualize match matrix. class MatchMatrixWidget : public ImageViewerWidget { public: MatchMatrixWidget(QWidget* parent, OptionManager* options); void Show(); private: OptionManager* options_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/media/000077500000000000000000000000001454702036400165425ustar00rootroot00000000000000colmap-3.9.1/src/colmap/ui/media/LICENSE000077500000000000000000000043021454702036400175510ustar00rootroot00000000000000Fugue Icons (C) 2013 Yusuke Kamiyamane. All rights reserved. These icons are licensed under a Creative Commons Attribution 3.0 License. If you can't or don't want to provide attribution, please purchase a royalty-free license. I'm unavailable for custom icon design work. But your suggestions are always welcome! ------------------------------------------------------------ All logos and trademarks in some icons are property of their respective owners. ------------------------------------------------------------ - geotag (C) Geotag Icon Project. All rights reserved. Geotag icon is licensed under a Creative Commons Attribution-Share Alike 3.0 License or LGPL. - language (C) Language Icon Project. All rights reserved. Language icon is licensed under a Creative Commons Attribution-Share Alike 3.0 License. - open-share (C) Open Share Icon Project. All rights reserved. Open Share icon is licensed under a Creative Commons Attribution-Share Alike 3.0 License. - opml (C) OPML Icon Project. All rights reserved. OPML icon is licensed under a Creative Commons Attribution-Share Alike 2.5 License. - share (C) Share Icon Project. All rights reserved. Share icon is licensed under a GPL or LGPL or BSD or Creative Commons Attribution 2.5 License. - xfn (C) Wolfgang Bartelme. All rights reserved. XFN icon is licensed under a Creative Commons Attribution-Share Alike 2.5 License. colmap-3.9.1/src/colmap/ui/media/automatic-reconstruction.png000077500000000000000000000006201454702036400243160ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<2IDATxb?%B0 `dbbr{|W+ļ@ĭ@5TTT2>|۷o a~3?ӍU>| !_~e _Ҁ6=e a .. 3-[m޼/<E!H]*,Pgh6w+F-JH!}!&ne:4bA+Agge~R$"c-oP"I@ޞCJB`Ai٫S]љĈd8eh`J,iF^'( GS5>k538վ{ث/.S5kzU?WqHIENDB`colmap-3.9.1/src/colmap/ui/media/dense-reconstruction.png000077500000000000000000000006161454702036400234330ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<0IDATxSN@fYv8|a"~Fbq0BdKe'`%U]zyq%š( V#iZaeYmqJhe+cn>87qu} p(()&ru_<'Yi^:S]4 u]G}0 $FF gPE$@m) ]oq ~H<,Q:3=Q&J1Mb~$._#–%bGwaE_C_@,u~`y"hpIENDB`colmap-3.9.1/src/colmap/ui/media/export-all.png000077500000000000000000000011531454702036400213420ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe< IDATxڔkAǿ;f?K$BAo^jEh<$z 9-9會(Uz$Ń=9xP&Iw7dv׷6&l 3Ha"|>gD>^mH$4 ˲JY(ocJ 2!:ZH8D &/8CRצ{Q  v>mã`6v\f Q*$`B^C{?X,JIfNEU`:Rqyeb!!5;phn "%@{ '2]qH|L&w8=z!&AOtsW.Lӳ퍱 7^԰cK&Rus%c=ye-mm|9ۅ%|" ,^X|XfHGeːeåx[}-1VHF?fQeW5]0Unh<ŏJW$ cC:~ҵ>*^"'>`Ec_,pəkuj? aMIENDB`colmap-3.9.1/src/colmap/ui/media/export-as-text.png000077500000000000000000000014321454702036400221570ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxڌKqǿsqӸn)]Lq 0HT,"|@*|70/e)BW/ت;ۮs̆*;ߜ93455aՑ -X,V$#q-+eq,{Hd@QxYe=: ƔFs-9a%a||0 <y {e/=gYvq8QUu AVqxuʕEQ1n2hWWnK 46 ɳ fש~ukTe^\˺'21RtqN~V2 O8jIENDB`colmap-3.9.1/src/colmap/ui/media/export-as.png000077500000000000000000000007631454702036400212030ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxڤS=kA;#m,UlB*CER*$U*`c"4viS{~E8]fV⊨sΛ7{,8Z^z82C~bo%nokC"I)A l6;JC0`M0 ӟf;/E<PP$F1bu:[.LI}z=3R?f~f`fZGm`0RDz\*\&v_ F4T*tx|8N#ߋzm XYw]9T*%dQ!{HD&Z&ٶ0buXݲ,L{8(P.,ZNfϧ<$ꁲsFgOgM2pIENDB`colmap-3.9.1/src/colmap/ui/media/export.png000077500000000000000000000012001454702036400205650ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<"IDATxڌkQƿ;GҊ-D0 Hҽ EZn"n]׮dM! p%TEi55$d;G!{{9yD[4q^<{3ryq\49t]G4E,EpA qnaƸ@`ȡ:ׁQe01>&:}J,ݱc§R2lsVbi?jr?fXܞX?4 *b;ه>6"+ADBE'9sak,vIB5Zm0 5a:1m&m*_CsGn|_h/b`ϯccm{sl,oV~6`9}9"y).~JK^Kh(?+#J:[-PEq@(e`B 1ZWn\BRa"W4M >q]j X&D!$IӟhQ_ A'IENDB`colmap-3.9.1/src/colmap/ui/media/feature-extraction.png000077500000000000000000000027021454702036400230650ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<iiTXtXML:com.adobe.xmp :iIDATxēAǿ3;&dӘlTp.EN8KAPB;H `l=BPk fy殹0I8!qPu[,> 6nL/QAEu<Hږnt:0Mau]eqzĪ(L 4N J۶Y`|}:LT*0d/ s(f=\<lXX8OQ,NyL= h"mxcep\_3Q*D۶ٺS"Bj{p .^^ |řgi1 eY?D ,0ʙ5|*&^>¶ 6r|>PPY\Ne28N=h4 :"J@ow$\ᙶ^pTxHZ%u7癭6qsmm$XQX^~6\~퍍C??[[IENDB`colmap-3.9.1/src/colmap/ui/media/feature-matching.png000077500000000000000000000004051454702036400224750ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxb?%D0115)m]_=r##: fb } 5 t،`S6#I^b]l| yDѽ,H{KDH#A, ǡD1pyDa^`‹d&IlD I!1H `b[@XIENDB`colmap-3.9.1/src/colmap/ui/media/grab-image.png000077500000000000000000000011731454702036400212500ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxڤkQƿɼ&}$}FdFpх-Dp_Et!T Q>6 TQ4{rg3QsHn  v炴F-(̯ӥ<s% ,ZD{Vr|<Φ XӦ( f.i(z,mo3Y`vhx :bc Y-2HSjOEJ<* !yT击x7pOυ"! Bm$UQ~2rUZ2"Xz :=(5/O?A4LX˲`[6\Aɟ@a, ! 3*4Mam k#thp e!gɡS1ޜμ'y3s;|s Zs~@oGt'{q4qP4c H}l%J)|'Fiǭ$aTq!ߏ_ZZ&/RIRROX[n *\ eHOqXc[*԰*œ]j۱m %ަ Z[E>a6VA3ge(a ~?HRm###&)3L1 ?*s=SbL`FTUE8>@QUׇ\P('qh?G>`4j5i}躎h4Z$%|ele!>X"2 !l$4H$sEdUVO4L&w[9DWťą-]YIENDB`colmap-3.9.1/src/colmap/ui/media/import.png000077500000000000000000000012641454702036400205700ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<VIDATxڌS;Q:Y !X6 )%U ,l^p_l!3"EZh-ѝ5>3;ꠛ-r9pw|\>ƞgVSY"n+t:|>a ~Sn瓠( B ŧ}bё0 fp҂߯0 / [4 t~I+T9kXபd2Nq8!>~jFm7M8t^-xg1"_A @!!"yV,o\.+ XV5Tk=1 >xd-D"TUI-μkTʹ rdoak?*D"qHf ÈH5EּaAT:Ub@O!zyQ.3&S3"fܣWSxenA YqID[7% m-8#^ sFe 38bعi s2,,'i[ p2U IENDB`colmap-3.9.1/src/colmap/ui/media/log.png000077500000000000000000000005171454702036400200370ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxb?%B7HiAPH4__!X! @Hh ` Lh 6HvL=Fv&eAb17K(]H6!T QUR+0ȹ ?`?##3F ?b`Tz۷o?bb;ß? ;{>#X H )1\2uW8]ƃ셃`2!m'Fw0ȧ^=:=e}#XI&߲q~B曎xxd`eJz"F:$fe#F>+%MdM 4$Nb"GBRK@`T^J! bz.Pt7?%@\B8\.A 8.$g?IKt:Q Л% !> HK ϐ:x5Ґ<)@uvz79ֆZKg )erIENDB`colmap-3.9.1/src/colmap/ui/media/project-save.png000077500000000000000000000006621454702036400216610ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<TIDATxڤNPO`t$1!}|W.Mꂱa+qs5>/i\Y\(!4PI>.9#Fl8XW~J|~'}+C,rPMJIlz. "-,Ky/`!v gҴJEAC0%]ǽ9 OڽeY+㘎]Tn(& {6'q(Ifz t]*tR9`[2LlfA$۶+'ޫ)Y^E_ifR*RWo9gd/}>Xn oKBUIENDB`colmap-3.9.1/src/colmap/ui/media/reconstruction-normalize.png000077500000000000000000000007061454702036400243350ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<hIDATxSN@SiK?{ޗ0D .BhC]w0V$o:,SJAV` {,J)vM(viSH @×ae ;Çǂ"{:jua@^'lZ@$y~"v mShb˲VQ xL&X,}R$.s&x4=~NQ~O[:*%s8naBR<ZA0I8az ?njIK;0L8]P^?] < !!>䫔߅_ {IENDB`colmap-3.9.1/src/colmap/ui/media/reconstruction-options.png000077500000000000000000000013471454702036400240320ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxlSMLQvZn鶶Rx#`HH @CHj@x!M$ؓp TL $(OTJ n]8yoo3*TPevcunO1\nmA B<ꝌB UVa}AIp'$9 .[ _ÃRN޹-T/cЩQ+-KN&?BǏӷN4M%h7y}I.AvX".K$WG"5܏b~3^%S-'R[6qQ泑AiscǻI9]05/ؕ < 7xIENDB`colmap-3.9.1/src/colmap/ui/media/reconstruction-pause.png000077500000000000000000000005721454702036400234530ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxēKN08- +6=F. KV=$p-P_y6Ic2#\"QY0?E)0Fg)yۼn}j7gzPK=+v! yU8jCIENDB`colmap-3.9.1/src/colmap/ui/media/reconstruction-start.png000077500000000000000000000006141454702036400234700ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<.IDATxb?%]q@?ϟ` j4RPB.`0hk1.`?d@}C_Հ0\!Ol<` ߿ p# WK~iV޼S ; `C_߿3;7$Ȱv3肗 /t%9ξ8A\؂llKHv3W`-!-bx8yY`B"sYX)͍L AJ 1IENDB`colmap-3.9.1/src/colmap/ui/media/reconstruction-stats.png000077500000000000000000000012121454702036400234640ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<,IDATxڤ;oAϮwDBBgm==#‚&EqwfN4ګ{YՃ{VˑJ-o~^HWʌbcGJeb3|wsZK.V/p.浪:?usZGHewZ{0 V7}պp@k@A$'Pb+@))jfkK"!`%27W 7)nNAJ RLP &~cw_lJO.>),R3&pP HkZ[Ckm׫g".1wxq8288%3fDftKj 堵>jFI# lNԆ`@1t~( bΰgfE ]=UM+|NQh!Ș|ii&8pJxmC8,GtnAkUYGW鋫_Fk'u[@9dׇ=$K##*؎GIENDB`colmap-3.9.1/src/colmap/ui/media/reconstruction-step.png000077500000000000000000000010001454702036400232740ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxڤSK@~[u"X\vpr͟IT" I tr.EH4i4{ZM[[<^}}~XǰRBթ ð(1$fٷٓGI @3$!"27qn7!Iw]KLsv9#Df!|?$?WZw(l6=[ / & Q&39 r8bv Bި7L03 Uue|ߒX~n /w6,,F9TШXZ*UMALL4Q DVLcW-35q<زοȚV1)?b Dah˷D0`=7a'9lרS>bBF<IENDB`colmap-3.9.1/src/colmap/ui/media/render-disabled.png000077500000000000000000000012371454702036400223020ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<AIDATxtS;av1ˋ*|,W,,xY XYX(B m#!x؟wV,mrBpy$Ù?83|eY/IrRliE6[w:Վ}x ])epmi\ ׋J"n.-0 8ɚ9mKƆ(.Q>==3 ]/,Z (k~tND\5{RѦ fd9 Z~p8hX,NUU 86ւad2)X8%Hc2  fk _Q&z•#á vq5f@fm0 4 pefg G# ZRԟU,<:"-3vN6gXsjՄxp˫[Ow 1h4w/^ WRW 4MeS.śGQd2,n?ͼպMf0 Y tZ<%}>?{MZ~'i1 &i $ 'IENDB`colmap-3.9.1/src/colmap/ui/media/render-enabled.png000077500000000000000000000012641454702036400221250ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<VIDATxtSKkQf2i˘n$Dq@W Tؕ.\ qF ]?@D(P] ."D1O5I-PJfYCQnc0ޅ[fL+BnV)seX3%޼< ?-|C2gJТ(k'P_]]EHlJƤv[A֝E(͆j![Xt )x 8n-\o%͇c6$飕$ sq'\˳ 2+g[UZ.Wb1m[x|qdA1qp,_9M,FiXlIjk<}tFh.u޻-/ ơJIENDB`colmap-3.9.1/src/colmap/ui/media/render-options.png000077500000000000000000000012221454702036400222200ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<4IDATxtSMkQ=yĤ f.YЍ?0v!EںA7m7q%%ಋl\[% D\@4̼L uy~ +^@b9OzaLmaaXϧb_TTQܼ-jTUcKJG BiJ`_pm ^wpd.e}Z{0W O_r?Ƈ{@L_E ?'/" OXm0e0LƣvzVUUGo0a<~ ,\Jf!}p8 Q$S0c4d &k6~t[-A$/7S$ʧ,BPq1KKFx 3WI˰h _N@lX,,v錽z$I5 i&p:rј`n ZTlRdxO}WJQܡt^qmvRɓ˷cV}((.z{ hIENDB`colmap-3.9.1/src/colmap/ui/media/undistort.png000077500000000000000000000010761454702036400213120ustar00rootroot00000000000000PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxڤSn@=cgbgiCaQ$6m!<-P?@GcSUv**qرJi4{sg,L/ߜXNYts'$Ѡxa+J =NTH8mZD'ň5B˲E &I( =<8xFɋ/FZц p~>4.;%i^V]`l_-1 vvr:{ Nn(14S?w4wzk[u25_TpA|oDӡqMhFR*hÓ4MO+e&rXY4X>ݿ R ~j*7d2_XRHrUhr6x|]J(ж#fU 8K.ih˰RaF K_ Z'Fv|IENDB`colmap-3.9.1/src/colmap/ui/model_viewer_widget.cc000066400000000000000000001357641454702036400220360ustar00rootroot00000000000000// 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. #include "colmap/ui/model_viewer_widget.h" #include "colmap/ui/main_window.h" #define SELECTION_BUFFER_IMAGE_IDX 0 #define SELECTION_BUFFER_POINT_IDX 1 const Eigen::Vector4f kSelectedPointColor(0.0f, 1.0f, 0.0f, 1.0f); const Eigen::Vector4f kSelectedImagePlaneColor(1.0f, 0.0f, 1.0f, 0.6f); const Eigen::Vector4f kSelectedImageFrameColor(0.8f, 0.0f, 0.8f, 1.0f); const Eigen::Vector4f kMovieGrabberImagePlaneColor(0.0f, 1.0f, 1.0f, 0.6f); const Eigen::Vector4f kMovieGrabberImageFrameColor(0.0f, 0.8f, 0.8f, 1.0f); const Eigen::Vector4f kGridColor(0.2f, 0.2f, 0.2f, 0.6f); const Eigen::Vector4f kXAxisColor(0.9f, 0.0f, 0.0f, 0.5f); const Eigen::Vector4f kYAxisColor(0.0f, 0.9f, 0.0f, 0.5f); const Eigen::Vector4f kZAxisColor(0.0f, 0.0f, 0.9f, 0.5f); namespace colmap { namespace { // Generate unique index from RGB color in the range [0, 256^3]. inline size_t RGBToIndex(const uint8_t r, const uint8_t g, const uint8_t b) { return static_cast(r) + static_cast(g) * 256 + static_cast(b) * 65536; } // Derive color from unique index, generated by `RGBToIndex`. inline Eigen::Vector4f IndexToRGB(const size_t index) { Eigen::Vector4f color; color(0) = ((index & 0x000000FF) >> 0) / 255.0f; color(1) = ((index & 0x0000FF00) >> 8) / 255.0f; color(2) = ((index & 0x00FF0000) >> 16) / 255.0f; color(3) = 1.0f; return color; } void BuildImageModel(const Image& image, const Camera& camera, const float image_size, const Eigen::Vector4f& plane_color, const Eigen::Vector4f& frame_color, std::vector* triangle_data, std::vector* line_data) { // Generate camera dimensions in OpenGL (world) coordinate space. const float kBaseCameraWidth = 1024.0f; const float image_width = image_size * camera.width / kBaseCameraWidth; const float image_height = image_width * static_cast(camera.height) / static_cast(camera.width); const float image_extent = std::max(image_width, image_height); const float camera_extent = std::max(camera.width, camera.height); const float camera_extent_normalized = static_cast(camera.CamFromImgThreshold(camera_extent)); const float focal_length = 2.0f * image_extent / camera_extent_normalized; const Eigen::Matrix inv_proj_matrix = Inverse(image.CamFromWorld()).ToMatrix().cast(); // Projection center, top-left, top-right, bottom-right, bottom-left corners. const Eigen::Vector3f pc = inv_proj_matrix.rightCols<1>(); const Eigen::Vector3f tl = inv_proj_matrix * Eigen::Vector4f(-image_width, image_height, focal_length, 1); const Eigen::Vector3f tr = inv_proj_matrix * Eigen::Vector4f(image_width, image_height, focal_length, 1); const Eigen::Vector3f br = inv_proj_matrix * Eigen::Vector4f(image_width, -image_height, focal_length, 1); const Eigen::Vector3f bl = inv_proj_matrix * Eigen::Vector4f(-image_width, -image_height, focal_length, 1); // Image plane as two triangles. if (triangle_data != nullptr) { triangle_data->emplace_back(PointPainter::Data(tl(0), tl(1), tl(2), plane_color(0), plane_color(1), plane_color(2), plane_color(3)), PointPainter::Data(tr(0), tr(1), tr(2), plane_color(0), plane_color(1), plane_color(2), plane_color(3)), PointPainter::Data(bl(0), bl(1), bl(2), plane_color(0), plane_color(1), plane_color(2), plane_color(3))); triangle_data->emplace_back(PointPainter::Data(bl(0), bl(1), bl(2), plane_color(0), plane_color(1), plane_color(2), plane_color(3)), PointPainter::Data(tr(0), tr(1), tr(2), plane_color(0), plane_color(1), plane_color(2), plane_color(3)), PointPainter::Data(br(0), br(1), br(2), plane_color(0), plane_color(1), plane_color(2), plane_color(3))); } if (line_data != nullptr) { // Frame around image plane and connecting lines to projection center. line_data->emplace_back(PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(tl(0), tl(1), tl(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); line_data->emplace_back(PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(tr(0), tr(1), tr(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); line_data->emplace_back(PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(br(0), br(1), br(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); line_data->emplace_back(PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(bl(0), bl(1), bl(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); line_data->emplace_back(PointPainter::Data(tl(0), tl(1), tl(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(tr(0), tr(1), tr(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); line_data->emplace_back(PointPainter::Data(tr(0), tr(1), tr(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(br(0), br(1), br(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); line_data->emplace_back(PointPainter::Data(br(0), br(1), br(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(bl(0), bl(1), bl(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); line_data->emplace_back(PointPainter::Data(bl(0), bl(1), bl(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3)), PointPainter::Data(tl(0), tl(1), tl(2), frame_color(0), frame_color(1), frame_color(2), frame_color(3))); } } } // namespace ModelViewerWidget::ModelViewerWidget(QWidget* parent, OptionManager* options) : QOpenGLWidget(parent), options_(options), point_viewer_widget_(new PointViewerWidget(parent, this, options)), image_viewer_widget_( new DatabaseImageViewerWidget(parent, this, options)), movie_grabber_widget_(new MovieGrabberWidget(parent, this)), mouse_is_pressed_(false), focus_distance_(kInitFocusDistance), selected_image_id_(kInvalidImageId), selected_point3D_id_(kInvalidPoint3DId), coordinate_grid_enabled_(true), near_plane_(kInitNearPlane) { background_color_[0] = 1.0f; background_color_[1] = 1.0f; background_color_[2] = 1.0f; QSurfaceFormat format; format.setDepthBufferSize(24); format.setMajorVersion(3); format.setMinorVersion(2); format.setSamples(4); format.setProfile(QSurfaceFormat::CoreProfile); #ifdef DEBUG format.setOption(QSurfaceFormat::DebugContext); #endif setFormat(format); QSurfaceFormat::setDefaultFormat(format); SetPointColormap(new PointColormapPhotometric()); SetImageColormap(new ImageColormapUniform()); image_size_ = static_cast(devicePixelRatio() * image_size_); point_size_ = static_cast(devicePixelRatio() * point_size_); } void ModelViewerWidget::initializeGL() { initializeOpenGLFunctions(); glEnable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); SetupPainters(); SetupView(); } void ModelViewerWidget::paintGL() { glClearColor( background_color_[0], background_color_[1], background_color_[2], 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); const QMatrix4x4 pmv_matrix = projection_matrix_ * model_view_matrix_; // Model view matrix for center of view QMatrix4x4 model_view_center_matrix = model_view_matrix_; const Eigen::Vector4f rot_center = QMatrixToEigen(model_view_matrix_).inverse() * Eigen::Vector4f(0, 0, -focus_distance_, 1); model_view_center_matrix.translate( rot_center(0), rot_center(1), rot_center(2)); // Coordinate system if (coordinate_grid_enabled_) { const QMatrix4x4 pmvc_matrix = projection_matrix_ * model_view_center_matrix; coordinate_axes_painter_.Render(pmv_matrix, width(), height(), 2); coordinate_grid_painter_.Render(pmvc_matrix, width(), height(), 1); } // Points point_painter_.Render(pmv_matrix, point_size_); point_connection_painter_.Render(pmv_matrix, width(), height(), 1); // Images image_line_painter_.Render(pmv_matrix, width(), height(), 1); image_triangle_painter_.Render(pmv_matrix); image_connection_painter_.Render(pmv_matrix, width(), height(), 1); // Movie grabber cameras movie_grabber_path_painter_.Render(pmv_matrix, width(), height(), 1.5); movie_grabber_line_painter_.Render(pmv_matrix, width(), height(), 1); movie_grabber_triangle_painter_.Render(pmv_matrix); } void ModelViewerWidget::resizeGL(int width, int height) { glViewport(0, 0, width, height); ComposeProjectionMatrix(); UploadCoordinateGridData(); } void ModelViewerWidget::ReloadReconstruction() { if (reconstruction == nullptr) { return; } cameras = reconstruction->Cameras(); points3D = reconstruction->Points3D(); reg_image_ids = reconstruction->RegImageIds(); images.clear(); for (const image_t image_id : reg_image_ids) { images[image_id] = reconstruction->Image(image_id); } statusbar_status_label->setText( QString().asprintf("%d Images - %d Points", static_cast(reg_image_ids.size()), static_cast(points3D.size()))); Upload(); } void ModelViewerWidget::ClearReconstruction() { cameras.clear(); images.clear(); points3D.clear(); reg_image_ids.clear(); reconstruction = nullptr; Upload(); } int ModelViewerWidget::GetProjectionType() const { return options_->render->projection_type; } void ModelViewerWidget::SetPointColormap(PointColormapBase* colormap) { point_colormap_.reset(colormap); } void ModelViewerWidget::SetImageColormap(ImageColormapBase* colormap) { image_colormap_.reset(colormap); } void ModelViewerWidget::UpdateMovieGrabber() { UploadMovieGrabberData(); update(); } void ModelViewerWidget::EnableCoordinateGrid() { coordinate_grid_enabled_ = true; update(); } void ModelViewerWidget::DisableCoordinateGrid() { coordinate_grid_enabled_ = false; update(); } void ModelViewerWidget::ChangeFocusDistance(const float delta) { if (delta == 0.0f) { return; } const float prev_focus_distance = focus_distance_; float diff = delta * ZoomScale() * kFocusSpeed; focus_distance_ -= diff; if (focus_distance_ < kMinFocusDistance) { focus_distance_ = kMinFocusDistance; diff = prev_focus_distance - focus_distance_; } else if (focus_distance_ > kMaxFocusDistance) { focus_distance_ = kMaxFocusDistance; diff = prev_focus_distance - focus_distance_; } const Eigen::Matrix4f vm_mat = QMatrixToEigen(model_view_matrix_).inverse(); const Eigen::Vector3f tvec(0, 0, diff); const Eigen::Vector3f tvec_rot = vm_mat.block<3, 3>(0, 0) * tvec; model_view_matrix_.translate(tvec_rot(0), tvec_rot(1), tvec_rot(2)); ComposeProjectionMatrix(); UploadCoordinateGridData(); update(); } void ModelViewerWidget::ChangeNearPlane(const float delta) { if (delta == 0.0f) { return; } near_plane_ *= (1.0f + delta / 100.0f * kNearPlaneScaleSpeed); near_plane_ = std::max(kMinNearPlane, std::min(kMaxNearPlane, near_plane_)); ComposeProjectionMatrix(); UploadCoordinateGridData(); update(); } void ModelViewerWidget::ChangePointSize(const float delta) { if (delta == 0.0f) { return; } point_size_ *= (1.0f + delta / 100.0f * kPointScaleSpeed); point_size_ = std::max(kMinPointSize, std::min(kMaxPointSize, point_size_)); update(); } void ModelViewerWidget::RotateView(const float x, const float y, const float prev_x, const float prev_y) { if (x - prev_x == 0 && y - prev_y == 0) { return; } // Rotation according to the Arcball method "ARCBALL: A User Interface for // Specifying Three-Dimensional Orientation Using a Mouse", Ken Shoemake, // University of Pennsylvania, 1992. // Determine Arcball vector on unit sphere. const Eigen::Vector3f u = PositionToArcballVector(x, y); const Eigen::Vector3f v = PositionToArcballVector(prev_x, prev_y); // Angle between vectors. const float angle = 2.0f * std::acos(std::min(1.0f, u.dot(v))); const float kMinAngle = 1e-3f; if (angle > kMinAngle) { const Eigen::Matrix4f vm_mat = QMatrixToEigen(model_view_matrix_).inverse(); // Rotation axis. Eigen::Vector3f axis = vm_mat.block<3, 3>(0, 0) * v.cross(u); axis = axis.normalized(); // Center of rotation is current focus. const Eigen::Vector4f rot_center = vm_mat * Eigen::Vector4f(0, 0, -focus_distance_, 1); // First shift to rotation center, then rotate and shift back. model_view_matrix_.translate(rot_center(0), rot_center(1), rot_center(2)); model_view_matrix_.rotate(RadToDeg(angle), axis(0), axis(1), axis(2)); model_view_matrix_.translate( -rot_center(0), -rot_center(1), -rot_center(2)); update(); } } void ModelViewerWidget::TranslateView(const float x, const float y, const float prev_x, const float prev_y) { if (x - prev_x == 0 && y - prev_y == 0) { return; } Eigen::Vector3f tvec(x - prev_x, prev_y - y, 0.0f); if (options_->render->projection_type == RenderOptions::ProjectionType::PERSPECTIVE) { tvec *= ZoomScale(); } else if (options_->render->projection_type == RenderOptions::ProjectionType::ORTHOGRAPHIC) { tvec *= 2.0f * OrthographicWindowExtent() / height(); } const Eigen::Matrix4f vm_mat = QMatrixToEigen(model_view_matrix_).inverse(); const Eigen::Vector3f tvec_rot = vm_mat.block<3, 3>(0, 0) * tvec; model_view_matrix_.translate(tvec_rot(0), tvec_rot(1), tvec_rot(2)); update(); } void ModelViewerWidget::ChangeCameraSize(const float delta) { if (delta == 0.0f) { return; } image_size_ *= (1.0f + delta / 100.0f * kImageScaleSpeed); image_size_ = std::max(kMinImageSize, std::min(kMaxImageSize, image_size_)); UploadImageData(); UploadMovieGrabberData(); update(); } void ModelViewerWidget::ResetView() { SetupView(); Upload(); } QMatrix4x4 ModelViewerWidget::ModelViewMatrix() const { return model_view_matrix_; } void ModelViewerWidget::SetModelViewMatrix(const QMatrix4x4& matrix) { model_view_matrix_ = matrix; update(); } void ModelViewerWidget::SelectObject(const int x, const int y) { makeCurrent(); // Ensure that anti-aliasing does not change the colors of objects. glDisable(GL_MULTISAMPLE); glClearColor(1.0f, 1.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Upload data in selection mode (one color per object). UploadImageData(true); UploadPointData(true); // Render in selection mode, with larger points to improve selection accuracy. const QMatrix4x4 pmv_matrix = projection_matrix_ * model_view_matrix_; image_triangle_painter_.Render(pmv_matrix); point_painter_.Render(pmv_matrix, 2 * point_size_); const int scaled_x = devicePixelRatio() * x; const int scaled_y = devicePixelRatio() * (height() - y - 1); QOpenGLFramebufferObjectFormat fbo_format; fbo_format.setSamples(0); QOpenGLFramebufferObject fbo(1, 1, fbo_format); glBindFramebuffer(GL_READ_FRAMEBUFFER, defaultFramebufferObject()); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.handle()); glBlitFramebuffer(scaled_x, scaled_y, scaled_x + 1, scaled_y + 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST); fbo.bind(); std::array color; glReadPixels(0, 0, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, color.data()); fbo.release(); const size_t index = RGBToIndex(color[0], color[1], color[2]); if (index < selection_buffer_.size()) { const char buffer_type = selection_buffer_[index].second; if (buffer_type == SELECTION_BUFFER_IMAGE_IDX) { selected_image_id_ = static_cast(selection_buffer_[index].first); selected_point3D_id_ = kInvalidPoint3DId; ShowImageInfo(selected_image_id_); } else if (buffer_type == SELECTION_BUFFER_POINT_IDX) { selected_image_id_ = kInvalidImageId; selected_point3D_id_ = selection_buffer_[index].first; ShowPointInfo(selection_buffer_[index].first); } else { selected_image_id_ = kInvalidImageId; selected_point3D_id_ = kInvalidPoint3DId; image_viewer_widget_->hide(); } } else { selected_image_id_ = kInvalidImageId; selected_point3D_id_ = kInvalidPoint3DId; image_viewer_widget_->hide(); } // Re-enable, since temporarily disabled above. glEnable(GL_MULTISAMPLE); selection_buffer_.clear(); UploadPointData(); UploadImageData(); UploadPointConnectionData(); UploadImageConnectionData(); update(); } void ModelViewerWidget::SelectMoviewGrabberView(const size_t view_idx) { selected_movie_grabber_view_ = view_idx; UploadMovieGrabberData(); update(); } QImage ModelViewerWidget::GrabImage() { makeCurrent(); DisableCoordinateGrid(); paintGL(); const int scaled_width = static_cast(devicePixelRatio() * width()); const int scaled_height = static_cast(devicePixelRatio() * height()); QOpenGLFramebufferObjectFormat fbo_format; fbo_format.setSamples(0); QOpenGLFramebufferObject fbo(scaled_width, scaled_height, fbo_format); glBindFramebuffer(GL_READ_FRAMEBUFFER, defaultFramebufferObject()); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.handle()); glBlitFramebuffer(0, 0, scaled_width, scaled_height, 0, 0, scaled_width, scaled_height, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST); fbo.bind(); QImage image(scaled_width, scaled_height, QImage::Format_RGB888); glReadPixels(0, 0, scaled_width, scaled_height, GL_RGB, GL_UNSIGNED_BYTE, image.bits()); fbo.release(); EnableCoordinateGrid(); return image.mirrored(); } void ModelViewerWidget::GrabMovie() { movie_grabber_widget_->show(); } void ModelViewerWidget::ShowPointInfo(const point3D_t point3D_id) { point_viewer_widget_->Show(point3D_id); } void ModelViewerWidget::ShowImageInfo(const image_t image_id) { image_viewer_widget_->ShowImageWithId(image_id); } float ModelViewerWidget::PointSize() const { return point_size_; } float ModelViewerWidget::ImageSize() const { return image_size_; } void ModelViewerWidget::SetPointSize(const float point_size) { point_size_ = point_size; } void ModelViewerWidget::SetImageSize(const float image_size) { image_size_ = image_size; UploadImageData(); } void ModelViewerWidget::SetBackgroundColor(const float r, const float g, const float b) { background_color_[0] = r; background_color_[1] = g; background_color_[2] = b; update(); } void ModelViewerWidget::mousePressEvent(QMouseEvent* event) { if (mouse_press_timer_.isActive()) { // Select objects (2. click) mouse_is_pressed_ = false; mouse_press_timer_.stop(); selection_buffer_.clear(); SelectObject(event->pos().x(), event->pos().y()); } else { // Set timer to remember 1. click mouse_press_timer_.setSingleShot(true); mouse_press_timer_.start(kDoubleClickInterval); mouse_is_pressed_ = true; prev_mouse_pos_ = event->pos(); } event->accept(); } void ModelViewerWidget::mouseReleaseEvent(QMouseEvent* event) { mouse_is_pressed_ = false; event->accept(); } void ModelViewerWidget::mouseMoveEvent(QMouseEvent* event) { if (mouse_is_pressed_) { if (event->buttons() & Qt::RightButton || (event->buttons() & Qt::LeftButton && event->modifiers() & Qt::ControlModifier)) { TranslateView(event->pos().x(), event->pos().y(), prev_mouse_pos_.x(), prev_mouse_pos_.y()); } else if (event->buttons() & Qt::LeftButton) { RotateView(event->pos().x(), event->pos().y(), prev_mouse_pos_.x(), prev_mouse_pos_.y()); } } prev_mouse_pos_ = event->pos(); event->accept(); } void ModelViewerWidget::wheelEvent(QWheelEvent* event) { // We don't mind whether horizontal or vertical scroll. const float delta = event->angleDelta().x() + event->angleDelta().y(); if (event->modifiers().testFlag(Qt::ControlModifier)) { ChangePointSize(delta); } else if (event->modifiers().testFlag(Qt::AltModifier)) { ChangeCameraSize(delta); } else if (event->modifiers().testFlag(Qt::ShiftModifier)) { ChangeNearPlane(delta); } else { ChangeFocusDistance(delta); } event->accept(); } void ModelViewerWidget::SetupPainters() { makeCurrent(); coordinate_axes_painter_.Setup(); coordinate_grid_painter_.Setup(); point_painter_.Setup(); point_connection_painter_.Setup(); image_line_painter_.Setup(); image_triangle_painter_.Setup(); image_connection_painter_.Setup(); movie_grabber_path_painter_.Setup(); movie_grabber_line_painter_.Setup(); movie_grabber_triangle_painter_.Setup(); } void ModelViewerWidget::SetupView() { point_size_ = kInitPointSize; image_size_ = kInitImageSize; focus_distance_ = kInitFocusDistance; model_view_matrix_.setToIdentity(); model_view_matrix_.translate(0, 0, -focus_distance_); model_view_matrix_.rotate(225, 1, 0, 0); model_view_matrix_.rotate(-45, 0, 1, 0); } void ModelViewerWidget::Upload() { point_colormap_->Prepare(cameras, images, points3D, reg_image_ids); image_colormap_->Prepare(cameras, images, points3D, reg_image_ids); ComposeProjectionMatrix(); UploadPointData(); UploadImageData(); UploadMovieGrabberData(); UploadPointConnectionData(); UploadImageConnectionData(); update(); } void ModelViewerWidget::UploadCoordinateGridData() { makeCurrent(); const float scale = ZoomScale(); // View center grid. std::vector grid_data(3); grid_data[0].point1 = PointPainter::Data(-20 * scale, 0, 0, kGridColor(0), kGridColor(1), kGridColor(2), kGridColor(3)); grid_data[0].point2 = PointPainter::Data(20 * scale, 0, 0, kGridColor(0), kGridColor(1), kGridColor(2), kGridColor(3)); grid_data[1].point1 = PointPainter::Data(0, -20 * scale, 0, kGridColor(0), kGridColor(1), kGridColor(2), kGridColor(3)); grid_data[1].point2 = PointPainter::Data(0, 20 * scale, 0, kGridColor(0), kGridColor(1), kGridColor(2), kGridColor(3)); grid_data[2].point1 = PointPainter::Data(0, 0, -20 * scale, kGridColor(0), kGridColor(1), kGridColor(2), kGridColor(3)); grid_data[2].point2 = PointPainter::Data(0, 0, 20 * scale, kGridColor(0), kGridColor(1), kGridColor(2), kGridColor(3)); coordinate_grid_painter_.Upload(grid_data); // Coordinate axes. std::vector axes_data(3); axes_data[0].point1 = PointPainter::Data( 0, 0, 0, kXAxisColor(0), kXAxisColor(1), kXAxisColor(2), kXAxisColor(3)); axes_data[0].point2 = PointPainter::Data(50 * scale, 0, 0, kXAxisColor(0), kXAxisColor(1), kXAxisColor(2), kXAxisColor(3)); axes_data[1].point1 = PointPainter::Data( 0, 0, 0, kYAxisColor(0), kYAxisColor(1), kYAxisColor(2), kYAxisColor(3)); axes_data[1].point2 = PointPainter::Data(0, 50 * scale, 0, kYAxisColor(0), kYAxisColor(1), kYAxisColor(2), kYAxisColor(3)); axes_data[2].point1 = PointPainter::Data( 0, 0, 0, kZAxisColor(0), kZAxisColor(1), kZAxisColor(2), kZAxisColor(3)); axes_data[2].point2 = PointPainter::Data(0, 0, 50 * scale, kZAxisColor(0), kZAxisColor(1), kZAxisColor(2), kZAxisColor(3)); coordinate_axes_painter_.Upload(axes_data); } void ModelViewerWidget::UploadPointData(const bool selection_mode) { makeCurrent(); std::vector data; // Assume we want to display the majority of points data.reserve(points3D.size()); const size_t min_track_len = static_cast(options_->render->min_track_len); if (selected_image_id_ == kInvalidImageId && images.count(selected_image_id_) == 0) { for (const auto& point3D : points3D) { if (point3D.second.error <= options_->render->max_error && point3D.second.track.Length() >= min_track_len) { PointPainter::Data painter_point; painter_point.x = static_cast(point3D.second.xyz(0)); painter_point.y = static_cast(point3D.second.xyz(1)); painter_point.z = static_cast(point3D.second.xyz(2)); Eigen::Vector4f color; if (selection_mode) { const size_t index = selection_buffer_.size(); selection_buffer_.push_back( std::make_pair(point3D.first, SELECTION_BUFFER_POINT_IDX)); color = IndexToRGB(index); } else if (point3D.first == selected_point3D_id_) { color = kSelectedPointColor; } else { color = point_colormap_->ComputeColor(point3D.first, point3D.second); } painter_point.r = color(0); painter_point.g = color(1); painter_point.b = color(2); painter_point.a = color(3); data.push_back(painter_point); } } } else { // Image selected const auto& selected_image = images[selected_image_id_]; for (const auto& point3D : points3D) { if (point3D.second.error <= options_->render->max_error && point3D.second.track.Length() >= min_track_len) { PointPainter::Data painter_point; painter_point.x = static_cast(point3D.second.xyz(0)); painter_point.y = static_cast(point3D.second.xyz(1)); painter_point.z = static_cast(point3D.second.xyz(2)); Eigen::Vector4f color; if (selection_mode) { const size_t index = selection_buffer_.size(); selection_buffer_.push_back( std::make_pair(point3D.first, SELECTION_BUFFER_POINT_IDX)); color = IndexToRGB(index); } else if (selected_image.HasPoint3D(point3D.first)) { color = kSelectedImagePlaneColor; } else if (point3D.first == selected_point3D_id_) { color = kSelectedPointColor; } else { color = point_colormap_->ComputeColor(point3D.first, point3D.second); } painter_point.r = color(0); painter_point.g = color(1); painter_point.b = color(2); painter_point.a = color(3); data.push_back(painter_point); } } } point_painter_.Upload(data); } void ModelViewerWidget::UploadPointConnectionData() { makeCurrent(); std::vector line_data; if (selected_point3D_id_ == kInvalidPoint3DId) { // No point selected, so upload empty data point_connection_painter_.Upload(line_data); return; } const auto& point3D = points3D[selected_point3D_id_]; // 3D point position. LinePainter::Data line; line.point1 = PointPainter::Data(static_cast(point3D.xyz(0)), static_cast(point3D.xyz(1)), static_cast(point3D.xyz(2)), kSelectedPointColor(0), kSelectedPointColor(1), kSelectedPointColor(2), 0.8f); // All images in which 3D point is observed. for (const auto& track_el : point3D.track.Elements()) { const Image& conn_image = images[track_el.image_id]; const Eigen::Vector3f conn_proj_center = conn_image.ProjectionCenter().cast(); line.point2 = PointPainter::Data(conn_proj_center(0), conn_proj_center(1), conn_proj_center(2), kSelectedPointColor(0), kSelectedPointColor(1), kSelectedPointColor(2), 0.8f); line_data.push_back(line); } point_connection_painter_.Upload(line_data); } void ModelViewerWidget::UploadImageData(const bool selection_mode) { makeCurrent(); std::vector line_data; line_data.reserve(8 * reg_image_ids.size()); std::vector triangle_data; triangle_data.reserve(2 * reg_image_ids.size()); for (const image_t image_id : reg_image_ids) { const Image& image = images[image_id]; const Camera& camera = cameras[image.CameraId()]; Eigen::Vector4f plane_color; Eigen::Vector4f frame_color; if (selection_mode) { const size_t index = selection_buffer_.size(); selection_buffer_.push_back( std::make_pair(image_id, SELECTION_BUFFER_IMAGE_IDX)); plane_color = frame_color = IndexToRGB(index); } else { if (image_id == selected_image_id_) { plane_color = kSelectedImagePlaneColor; frame_color = kSelectedImageFrameColor; } else { image_colormap_->ComputeColor(image, &plane_color, &frame_color); } } // Lines are not colored with the indexed color in selection mode, so do not // show them, so they do not block the selection process BuildImageModel(image, camera, image_size_, plane_color, frame_color, &triangle_data, selection_mode ? nullptr : &line_data); } image_line_painter_.Upload(line_data); image_triangle_painter_.Upload(triangle_data); } void ModelViewerWidget::UploadImageConnectionData() { makeCurrent(); std::vector line_data; std::vector image_ids; if (selected_image_id_ != kInvalidImageId) { // Show connections to selected images image_ids.push_back(selected_image_id_); } else if (options_->render->image_connections) { // Show all connections image_ids = reg_image_ids; } else { // Disabled, so upload empty data image_connection_painter_.Upload(line_data); return; } for (const image_t image_id : image_ids) { const Image& image = images.at(image_id); const Eigen::Vector3f proj_center = image.ProjectionCenter().cast(); // Collect all connected images std::unordered_set conn_image_ids; for (const Point2D& point2D : image.Points2D()) { if (point2D.HasPoint3D()) { const Point3D& point3D = points3D[point2D.point3D_id]; for (const auto& track_elem : point3D.track.Elements()) { conn_image_ids.insert(track_elem.image_id); } } } // Selected image in the center. LinePainter::Data line; line.point1 = PointPainter::Data(proj_center(0), proj_center(1), proj_center(2), kSelectedImageFrameColor(0), kSelectedImageFrameColor(1), kSelectedImageFrameColor(2), 0.8f); // All connected images to the selected image. for (const image_t conn_image_id : conn_image_ids) { const Image& conn_image = images[conn_image_id]; const Eigen::Vector3f conn_proj_center = conn_image.ProjectionCenter().cast(); line.point2 = PointPainter::Data(conn_proj_center(0), conn_proj_center(1), conn_proj_center(2), kSelectedImageFrameColor(0), kSelectedImageFrameColor(1), kSelectedImageFrameColor(2), 0.8f); line_data.push_back(line); } } image_connection_painter_.Upload(line_data); } void ModelViewerWidget::UploadMovieGrabberData() { makeCurrent(); std::vector path_data; path_data.reserve(movie_grabber_widget_->views.size()); std::vector line_data; line_data.reserve(4 * movie_grabber_widget_->views.size()); std::vector triangle_data; triangle_data.reserve(2 * movie_grabber_widget_->views.size()); if (movie_grabber_widget_->views.size() > 0) { const Image& image0 = movie_grabber_widget_->views[0]; Eigen::Vector3f prev_proj_center = image0.ProjectionCenter().cast(); for (size_t i = 1; i < movie_grabber_widget_->views.size(); ++i) { const Image& image = movie_grabber_widget_->views[i]; const Eigen::Vector3f curr_proj_center = image.ProjectionCenter().cast(); LinePainter::Data path; path.point1 = PointPainter::Data(prev_proj_center(0), prev_proj_center(1), prev_proj_center(2), kSelectedImagePlaneColor(0), kSelectedImagePlaneColor(1), kSelectedImagePlaneColor(2), kSelectedImagePlaneColor(3)); path.point2 = PointPainter::Data(curr_proj_center(0), curr_proj_center(1), curr_proj_center(2), kSelectedImagePlaneColor(0), kSelectedImagePlaneColor(1), kSelectedImagePlaneColor(2), kSelectedImagePlaneColor(3)); path_data.push_back(path); prev_proj_center = curr_proj_center; } // Setup dummy camera with same settings as current OpenGL viewpoint. const float kDefaultImageWdith = 2048.0f; const float kDefaultImageHeight = 1536.0f; const float focal_length = -2.0f * std::tan(DegToRad(kFieldOfView) / 2.0f) * kDefaultImageWdith; Camera camera = Camera::CreateFromModelId(kInvalidCameraId, SimplePinholeCameraModel::model_id, focal_length, kDefaultImageWdith, kDefaultImageHeight); // Build all camera models for (size_t i = 0; i < movie_grabber_widget_->views.size(); ++i) { const Image& image = movie_grabber_widget_->views[i]; Eigen::Vector4f plane_color; Eigen::Vector4f frame_color; if (i == selected_movie_grabber_view_) { plane_color = kSelectedImagePlaneColor; frame_color = kSelectedImageFrameColor; } else { plane_color = kMovieGrabberImagePlaneColor; frame_color = kMovieGrabberImageFrameColor; } BuildImageModel(image, camera, image_size_, plane_color, frame_color, &triangle_data, &line_data); } } movie_grabber_path_painter_.Upload(path_data); movie_grabber_line_painter_.Upload(line_data); movie_grabber_triangle_painter_.Upload(triangle_data); } void ModelViewerWidget::ComposeProjectionMatrix() { projection_matrix_.setToIdentity(); if (options_->render->projection_type == RenderOptions::ProjectionType::PERSPECTIVE) { projection_matrix_.perspective( kFieldOfView, AspectRatio(), near_plane_, kFarPlane); } else if (options_->render->projection_type == RenderOptions::ProjectionType::ORTHOGRAPHIC) { const float extent = OrthographicWindowExtent(); projection_matrix_.ortho(-AspectRatio() * extent, AspectRatio() * extent, -extent, extent, near_plane_, kFarPlane); } } float ModelViewerWidget::ZoomScale() const { // "Constant" scale factor w.r.t. zoom-level. return 2.0f * std::tan(static_cast(DegToRad(kFieldOfView)) / 2.0f) * std::abs(focus_distance_) / height(); } float ModelViewerWidget::AspectRatio() const { return static_cast(width()) / static_cast(height()); } float ModelViewerWidget::OrthographicWindowExtent() const { return std::tan(DegToRad(kFieldOfView) / 2.0f) * focus_distance_; } Eigen::Vector3f ModelViewerWidget::PositionToArcballVector( const float x, const float y) const { Eigen::Vector3f vec(2.0f * x / width() - 1, 1 - 2.0f * y / height(), 0.0f); const float norm2 = vec.squaredNorm(); if (norm2 <= 1.0f) { vec.z() = std::sqrt(1.0f - norm2); } else { vec = vec.normalized(); } return vec; } } // namespace colmap colmap-3.9.1/src/colmap/ui/model_viewer_widget.h000066400000000000000000000151501454702036400216620ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/database.h" #include "colmap/scene/reconstruction.h" #include "colmap/ui/colormaps.h" #include "colmap/ui/image_viewer_widget.h" #include "colmap/ui/line_painter.h" #include "colmap/ui/movie_grabber_widget.h" #include "colmap/ui/point_painter.h" #include "colmap/ui/point_viewer_widget.h" #include "colmap/ui/render_options.h" #include "colmap/ui/triangle_painter.h" #include #include #include namespace colmap { class ModelViewerWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_2_Core { public: const float kInitNearPlane = 1.0f; const float kMinNearPlane = 1e-3f; const float kMaxNearPlane = 1e5f; const float kNearPlaneScaleSpeed = 0.02f; const float kFarPlane = 1e5f; const float kInitFocusDistance = 100.0f; const float kMinFocusDistance = 1e-5f; const float kMaxFocusDistance = 1e8f; const float kFieldOfView = 25.0f; const float kFocusSpeed = 2.0f; const float kInitPointSize = 1.0f; const float kMinPointSize = 0.5f; const float kMaxPointSize = 100.0f; const float kPointScaleSpeed = 0.1f; const float kInitImageSize = 0.2f; const float kMinImageSize = 1e-6f; const float kMaxImageSize = 1e3f; const float kImageScaleSpeed = 0.1f; const int kDoubleClickInterval = 250; ModelViewerWidget(QWidget* parent, OptionManager* options); void ReloadReconstruction(); void ClearReconstruction(); int GetProjectionType() const; // Takes ownwership of the colormap objects. void SetPointColormap(PointColormapBase* colormap); void SetImageColormap(ImageColormapBase* colormap); void UpdateMovieGrabber(); void EnableCoordinateGrid(); void DisableCoordinateGrid(); void ChangeFocusDistance(float delta); void ChangeNearPlane(float delta); void ChangePointSize(float delta); void ChangeCameraSize(float delta); void RotateView(float x, float y, float prev_x, float prev_y); void TranslateView(float x, float y, float prev_x, float prev_y); void ResetView(); QMatrix4x4 ModelViewMatrix() const; void SetModelViewMatrix(const QMatrix4x4& matrix); void SelectObject(int x, int y); void SelectMoviewGrabberView(size_t view_idx); QImage GrabImage(); void GrabMovie(); void ShowPointInfo(point3D_t point3D_id); void ShowImageInfo(image_t image_id); float PointSize() const; float ImageSize() const; void SetPointSize(float point_size); void SetImageSize(float image_size); void SetBackgroundColor(float r, float g, float b); // Copy of current scene data that is displayed std::shared_ptr reconstruction; std::unordered_map cameras; std::unordered_map images; std::unordered_map points3D; std::vector reg_image_ids; QLabel* statusbar_status_label; protected: void initializeGL() override; void resizeGL(int width, int height) override; void paintGL() override; private: void mousePressEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; void SetupPainters(); void SetupView(); void Upload(); void UploadCoordinateGridData(); void UploadPointData(bool selection_mode = false); void UploadPointConnectionData(); void UploadImageData(bool selection_mode = false); void UploadImageConnectionData(); void UploadMovieGrabberData(); void ComposeProjectionMatrix(); float ZoomScale() const; float AspectRatio() const; float OrthographicWindowExtent() const; Eigen::Vector3f PositionToArcballVector(float x, float y) const; OptionManager* options_; QMatrix4x4 model_view_matrix_; QMatrix4x4 projection_matrix_; LinePainter coordinate_axes_painter_; LinePainter coordinate_grid_painter_; PointPainter point_painter_; LinePainter point_connection_painter_; LinePainter image_line_painter_; TrianglePainter image_triangle_painter_; LinePainter image_connection_painter_; LinePainter movie_grabber_path_painter_; LinePainter movie_grabber_line_painter_; TrianglePainter movie_grabber_triangle_painter_; PointViewerWidget* point_viewer_widget_; DatabaseImageViewerWidget* image_viewer_widget_; MovieGrabberWidget* movie_grabber_widget_; std::unique_ptr point_colormap_; std::unique_ptr image_colormap_; bool mouse_is_pressed_; QTimer mouse_press_timer_; QPoint prev_mouse_pos_; float focus_distance_; std::vector> selection_buffer_; image_t selected_image_id_; point3D_t selected_point3D_id_; size_t selected_movie_grabber_view_; bool coordinate_grid_enabled_; // Size of points (dynamic): does not require re-uploading of points. float point_size_; // Size of image models (not dynamic): requires re-uploading of image models. float image_size_; // Near clipping plane. float near_plane_; float background_color_[3]; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/movie_grabber_widget.cc000066400000000000000000000261401454702036400221430ustar00rootroot00000000000000// 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. #include "colmap/ui/movie_grabber_widget.h" #include "colmap/geometry/pose.h" #include "colmap/scene/projection.h" #include "colmap/ui/model_viewer_widget.h" namespace colmap { MovieGrabberWidget::MovieGrabberWidget(QWidget* parent, ModelViewerWidget* model_viewer_widget) : QWidget(parent), model_viewer_widget_(model_viewer_widget) { setWindowFlags(Qt::Widget | Qt::WindowStaysOnTopHint | Qt::Tool); setWindowTitle("Grab movie"); QGridLayout* grid = new QGridLayout(this); grid->setContentsMargins(0, 5, 0, 5); add_button_ = new QPushButton(tr("Add"), this); connect(add_button_, &QPushButton::released, this, &MovieGrabberWidget::Add); grid->addWidget(add_button_, 0, 0); delete_button_ = new QPushButton(tr("Delete"), this); connect(delete_button_, &QPushButton::released, this, &MovieGrabberWidget::Delete); grid->addWidget(delete_button_, 0, 1); clear_button_ = new QPushButton(tr("Clear"), this); connect( clear_button_, &QPushButton::released, this, &MovieGrabberWidget::Clear); grid->addWidget(clear_button_, 0, 2); table_ = new QTableWidget(this); table_->setColumnCount(1); QStringList table_header; table_header << "Time [seconds]"; table_->setHorizontalHeaderLabels(table_header); table_->resizeColumnsToContents(); table_->setShowGrid(true); table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table_->verticalHeader()->setVisible(true); table_->verticalHeader()->setDefaultSectionSize(18); table_->setSelectionMode(QAbstractItemView::SingleSelection); table_->setSelectionBehavior(QAbstractItemView::SelectRows); connect(table_, &QTableWidget::itemChanged, this, &MovieGrabberWidget::TimeChanged); connect(table_->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MovieGrabberWidget::SelectionChanged); grid->addWidget(table_, 1, 0, 1, 3); grid->addWidget(new QLabel(tr("Frame rate"), this), 2, 1); frame_rate_sb_ = new QSpinBox(this); frame_rate_sb_->setMinimum(1); frame_rate_sb_->setMaximum(1000); frame_rate_sb_->setSingleStep(1); frame_rate_sb_->setValue(100); grid->addWidget(frame_rate_sb_, 2, 2); grid->addWidget(new QLabel(tr("Smooth transition"), this), 3, 1); smooth_cb_ = new QCheckBox(this); smooth_cb_->setChecked(true); grid->addWidget(smooth_cb_, 3, 2); grid->addWidget(new QLabel(tr("Smoothness"), this), 4, 1); smoothness_sb_ = new QDoubleSpinBox(this); smoothness_sb_->setMinimum(0); smoothness_sb_->setMaximum(1); smoothness_sb_->setSingleStep(0.01); smoothness_sb_->setValue(0.5); grid->addWidget(smoothness_sb_, 4, 2); assemble_button_ = new QPushButton(tr("Assemble movie"), this); connect(assemble_button_, &QPushButton::released, this, &MovieGrabberWidget::Assemble); grid->addWidget(assemble_button_, 5, 1, 1, 2); } void MovieGrabberWidget::Add() { const QMatrix4x4 matrix = model_viewer_widget_->ModelViewMatrix(); double time = 0; if (table_->rowCount() > 0) { time = table_->item(table_->rowCount() - 1, 0)->text().toDouble() + 1; } QTableWidgetItem* item = new QTableWidgetItem(); item->setData(Qt::DisplayRole, time); item->setFlags(Qt::NoItemFlags | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); item->setTextAlignment(Qt::AlignRight); // Save size state of current viewpoint. ViewData view_data; view_data.model_view_matrix = matrix; view_data.point_size = model_viewer_widget_->PointSize(); view_data.image_size = model_viewer_widget_->ImageSize(); view_data_.emplace(item, view_data); table_->insertRow(table_->rowCount()); table_->setItem(table_->rowCount() - 1, 0, item); table_->selectRow(table_->rowCount() - 1); // Zoom out a little, so that we can see the newly added camera model_viewer_widget_->ChangeFocusDistance(-5); } void MovieGrabberWidget::Delete() { QModelIndexList selection = table_->selectionModel()->selectedIndexes(); for (const auto& index : selection) { table_->removeRow(index.row()); } UpdateViews(); model_viewer_widget_->UpdateMovieGrabber(); } void MovieGrabberWidget::Clear() { view_data_.clear(); while (table_->rowCount() > 0) { table_->removeRow(0); } views.clear(); model_viewer_widget_->UpdateMovieGrabber(); } void MovieGrabberWidget::Assemble() { if (table_->rowCount() < 2) { QMessageBox::critical( this, tr("Error"), tr("You must add at least two control views.")); return; } if (model_viewer_widget_->GetProjectionType() != RenderOptions::ProjectionType::PERSPECTIVE) { QMessageBox::critical( this, tr("Error"), tr("You must use perspective projection.")); return; } const QString path = QFileDialog::getExistingDirectory( this, tr("Choose destination..."), "", QFileDialog::ShowDirsOnly); // File dialog cancelled? if (path == "") { return; } const QDir dir = QDir(path); const QMatrix4x4 model_view_matrix_cached = model_viewer_widget_->ModelViewMatrix(); const float point_size_cached = model_viewer_widget_->PointSize(); const float image_size_cached = model_viewer_widget_->ImageSize(); const std::vector views_cached = views; // Make sure we do not render movie grabber path. views.clear(); model_viewer_widget_->UpdateMovieGrabber(); model_viewer_widget_->DisableCoordinateGrid(); const float frame_rate = frame_rate_sb_->value(); const float frame_time = 1.0f / frame_rate; size_t frame_number = 0; // Data of first view. const Eigen::Matrix4d prev_model_view_matrix = QMatrixToEigen(view_data_[table_->item(0, 0)].model_view_matrix) .cast(); Rigid3d prev_view_model = Inverse( Rigid3d(Eigen::Quaterniond(prev_model_view_matrix.topLeftCorner<3, 3>()), prev_model_view_matrix.topRightCorner<3, 1>())); for (int row = 1; row < table_->rowCount(); ++row) { const auto logical_idx = table_->verticalHeader()->logicalIndex(row); QTableWidgetItem* prev_table_item = table_->item(logical_idx - 1, 0); QTableWidgetItem* table_item = table_->item(logical_idx, 0); const ViewData& prev_view_data = view_data_.at(prev_table_item); const ViewData& view_data = view_data_.at(table_item); // Data of next view. const Eigen::Matrix4d curr_model_view_matrix = QMatrixToEigen(view_data.model_view_matrix).cast(); const Rigid3d curr_view_model = Inverse(Rigid3d( Eigen::Quaterniond(curr_model_view_matrix.topLeftCorner<3, 3>()), curr_model_view_matrix.topRightCorner<3, 1>())); // Time difference between previous and current view. const float dt = std::abs(table_item->text().toFloat() - prev_table_item->text().toFloat()); // Point size differences between previous and current view. const float dpoint_size = view_data.point_size - prev_view_data.point_size; const float dimage_size = view_data.image_size - prev_view_data.image_size; const auto num_frames = dt * frame_rate; for (size_t i = 0; i < num_frames; ++i) { const float t = i * frame_time; float tt = t / dt; if (smooth_cb_->isChecked()) { tt = ScaleSigmoid(tt, static_cast(smoothness_sb_->value())); } const Rigid3d interp_view_model = InterpolateCameraPoses(prev_view_model, curr_view_model, tt); Eigen::Matrix4d frame_model_view_matrix = Eigen::Matrix4d::Identity(); frame_model_view_matrix.topLeftCorner<3, 4>() = Inverse(interp_view_model).ToMatrix(); model_viewer_widget_->SetModelViewMatrix( EigenToQMatrix(frame_model_view_matrix.cast())); // Set point and image sizes. model_viewer_widget_->SetPointSize(prev_view_data.point_size + dpoint_size * tt); model_viewer_widget_->SetImageSize(prev_view_data.image_size + dimage_size * tt); QImage image = model_viewer_widget_->GrabImage(); image.save(dir.filePath( "frame" + QString().asprintf("%06zu", frame_number) + ".png")); frame_number += 1; } prev_view_model = curr_view_model; } views = views_cached; model_viewer_widget_->SetPointSize(point_size_cached); model_viewer_widget_->SetImageSize(image_size_cached); model_viewer_widget_->UpdateMovieGrabber(); model_viewer_widget_->EnableCoordinateGrid(); model_viewer_widget_->SetModelViewMatrix(model_view_matrix_cached); } void MovieGrabberWidget::TimeChanged(QTableWidgetItem* item) { table_->sortItems(0, Qt::AscendingOrder); UpdateViews(); model_viewer_widget_->UpdateMovieGrabber(); } void MovieGrabberWidget::SelectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { for (const auto& index : table_->selectionModel()->selectedIndexes()) { model_viewer_widget_->SelectMoviewGrabberView(index.row()); } } void MovieGrabberWidget::UpdateViews() { views.clear(); for (int row = 0; row < table_->rowCount(); ++row) { const auto logical_idx = table_->verticalHeader()->logicalIndex(row); QTableWidgetItem* item = table_->item(logical_idx, 0); const Eigen::Matrix4d model_view_matrix = QMatrixToEigen(view_data_.at(item).model_view_matrix).cast(); Image image; image.CamFromWorld() = Rigid3d(Eigen::Quaterniond(model_view_matrix.topLeftCorner<3, 3>()), model_view_matrix.topRightCorner<3, 1>()); views.push_back(image); } } } // namespace colmap colmap-3.9.1/src/colmap/ui/movie_grabber_widget.h000066400000000000000000000056771454702036400220210ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/reconstruction.h" #include #include #include #include namespace colmap { class ModelViewerWidget; class MovieGrabberWidget : public QWidget { public: MovieGrabberWidget(QWidget* parent, ModelViewerWidget* model_viewer_widget); // List of views, used to visualize the movie grabber camera path. std::vector views; struct ViewData { QMatrix4x4 model_view_matrix; float point_size = -1.0f; float image_size = -1.0f; }; private: // Add, delete, clear viewpoints. void Add(); void Delete(); void Clear(); // Assemble movie from current viewpoints. void Assemble(); // Event slot for time modification. void TimeChanged(QTableWidgetItem* item); // Event slot for changed selection. void SelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); // Update state when viewpoints reordered. void UpdateViews(); ModelViewerWidget* model_viewer_widget_; QPushButton* assemble_button_; QPushButton* add_button_; QPushButton* delete_button_; QPushButton* clear_button_; QTableWidget* table_; QSpinBox* frame_rate_sb_; QCheckBox* smooth_cb_; QDoubleSpinBox* smoothness_sb_; std::unordered_map view_data_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/options_widget.cc000066400000000000000000000243301454702036400210320ustar00rootroot00000000000000// 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. #include "colmap/ui/options_widget.h" namespace colmap { OptionsWidget::OptionsWidget(QWidget* parent) : QWidget(parent) { setWindowFlags(Qt::Dialog); setWindowModality(Qt::ApplicationModal); QFont font; font.setPointSize(10); setFont(font); grid_layout_ = new QGridLayout(this); grid_layout_->setVerticalSpacing(3); grid_layout_->setAlignment(Qt::AlignTop); setLayout(grid_layout_); } void OptionsWidget::AddOptionRow(const std::string& label_text, QWidget* widget, void* option) { QLabel* label = new QLabel(tr(label_text.c_str()), this); label->setFont(font()); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid_layout_->addWidget(label, grid_layout_->rowCount(), 0); widget->setFont(font()); grid_layout_->addWidget(widget, grid_layout_->rowCount() - 1, 1); option_rows_.emplace(option, std::make_pair(label, widget)); widget_rows_.emplace(widget, std::make_pair(label, widget)); } void OptionsWidget::AddWidgetRow(const std::string& label_text, QWidget* widget) { QLabel* label = new QLabel(tr(label_text.c_str()), this); label->setFont(font()); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid_layout_->addWidget(label, grid_layout_->rowCount(), 0); widget->setFont(font()); grid_layout_->addWidget(widget, grid_layout_->rowCount() - 1, 1); widget_rows_.emplace(widget, std::make_pair(label, widget)); } void OptionsWidget::AddLayoutRow(const std::string& label_text, QLayout* layout) { QLabel* label = new QLabel(tr(label_text.c_str()), this); label->setFont(font()); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid_layout_->addWidget(label, grid_layout_->rowCount(), 0); QWidget* layout_widget = new QWidget(this); layout_widget->setLayout(layout); layout->setContentsMargins(0, 0, 0, 0); grid_layout_->addWidget(layout_widget, grid_layout_->rowCount() - 1, 1); layout_rows_.emplace(layout, std::make_pair(label, layout_widget)); } QSpinBox* OptionsWidget::AddOptionInt(int* option, const std::string& label_text, const int min, const int max) { QSpinBox* spinbox = new QSpinBox(this); spinbox->setMinimum(min); spinbox->setMaximum(max); AddOptionRow(label_text, spinbox, option); options_int_.emplace_back(spinbox, option); return spinbox; } QDoubleSpinBox* OptionsWidget::AddOptionDouble(double* option, const std::string& label_text, const double min, const double max, const double step, const int decimals) { QDoubleSpinBox* spinbox = new QDoubleSpinBox(this); spinbox->setMinimum(min); spinbox->setMaximum(max); spinbox->setSingleStep(step); spinbox->setDecimals(decimals); AddOptionRow(label_text, spinbox, option); options_double_.emplace_back(spinbox, option); return spinbox; } QDoubleSpinBox* OptionsWidget::AddOptionDoubleLog(double* option, const std::string& label_text, const double min, const double max, const double step, const int decimals) { QDoubleSpinBox* spinbox = new QDoubleSpinBox(this); spinbox->setMinimum(min); spinbox->setMaximum(max); spinbox->setSingleStep(step); spinbox->setDecimals(decimals); AddOptionRow(label_text, spinbox, option); options_double_log_.emplace_back(spinbox, option); return spinbox; } QCheckBox* OptionsWidget::AddOptionBool(bool* option, const std::string& label_text) { QCheckBox* checkbox = new QCheckBox(this); AddOptionRow(label_text, checkbox, option); options_bool_.emplace_back(checkbox, option); return checkbox; } QLineEdit* OptionsWidget::AddOptionText(std::string* option, const std::string& label_text) { QLineEdit* line_edit = new QLineEdit(this); AddOptionRow(label_text, line_edit, option); options_text_.emplace_back(line_edit, option); return line_edit; } QLineEdit* OptionsWidget::AddOptionFilePath(std::string* option, const std::string& label_text) { QLineEdit* line_edit = new QLineEdit(this); AddOptionRow(label_text, line_edit, option); auto SelectPathFunc = [this, line_edit]() { line_edit->setText(QFileDialog::getOpenFileName(this, tr("Select file"))); }; QPushButton* select_button = new QPushButton(tr("Select file"), this); select_button->setFont(font()); connect(select_button, &QPushButton::released, this, SelectPathFunc); grid_layout_->addWidget(select_button, grid_layout_->rowCount(), 1); options_path_.emplace_back(line_edit, option); return line_edit; } QLineEdit* OptionsWidget::AddOptionDirPath(std::string* option, const std::string& label_text) { QLineEdit* line_edit = new QLineEdit(this); AddOptionRow(label_text, line_edit, option); auto SelectPathFunc = [this, line_edit]() { line_edit->setText( QFileDialog::getExistingDirectory(this, tr("Select folder"))); }; QPushButton* select_button = new QPushButton(tr("Select folder"), this); select_button->setFont(font()); connect(select_button, &QPushButton::released, this, SelectPathFunc); grid_layout_->addWidget(select_button, grid_layout_->rowCount(), 1); options_path_.emplace_back(line_edit, option); return line_edit; } void OptionsWidget::AddSpacer() { QLabel* label = new QLabel("", this); label->setFont(font()); grid_layout_->addWidget(label, grid_layout_->rowCount(), 0, 2, 1); } void OptionsWidget::AddSection(const std::string& title) { QLabel* label = new QLabel(tr(title.c_str()), this); label->setFont(font()); label->setContentsMargins(0, 0, 0, 5); grid_layout_->addWidget( label, grid_layout_->rowCount(), 0, 1, 2, Qt::AlignHCenter); } void OptionsWidget::ReadOptions() { for (auto& option : options_int_) { option.first->setValue(*option.second); } for (auto& option : options_double_) { option.first->setValue(*option.second); } for (auto& option : options_double_log_) { option.first->setValue(std::log10(*option.second)); } for (auto& option : options_bool_) { option.first->setChecked(*option.second); } for (auto& option : options_text_) { option.first->setText(QString::fromStdString(*option.second)); } for (auto& option : options_path_) { option.first->setText(QString::fromStdString(*option.second)); } } void OptionsWidget::WriteOptions() { for (auto& option : options_int_) { *option.second = option.first->value(); } for (auto& option : options_double_) { *option.second = option.first->value(); } for (auto& option : options_double_log_) { *option.second = std::pow(10, option.first->value()); } for (auto& option : options_bool_) { *option.second = option.first->isChecked(); } for (auto& option : options_text_) { *option.second = option.first->text().toUtf8().constData(); } for (auto& option : options_path_) { *option.second = option.first->text().toUtf8().constData(); } } void OptionsWidget::showEvent(QShowEvent* event) { ReadOptions(); } void OptionsWidget::closeEvent(QCloseEvent* event) { WriteOptions(); } void OptionsWidget::hideEvent(QHideEvent* event) { WriteOptions(); } void OptionsWidget::ShowOption(void* option) { auto& option_row = option_rows_.at(option); option_row.first->show(); option_row.second->show(); } void OptionsWidget::HideOption(void* option) { auto& option_row = option_rows_.at(option); option_row.first->hide(); option_row.second->hide(); } void OptionsWidget::ShowWidget(QWidget* widget) { auto& widget_row = widget_rows_.at(widget); widget_row.first->show(); widget_row.second->show(); } void OptionsWidget::HideWidget(QWidget* widget) { auto& widget_row = widget_rows_.at(widget); widget_row.first->hide(); widget_row.second->hide(); } void OptionsWidget::ShowLayout(QLayout* layout) { auto& layout_row = layout_rows_.at(layout); layout_row.first->show(); layout_row.second->show(); } void OptionsWidget::HideLayout(QLayout* layout) { auto& layout_row = layout_rows_.at(layout); layout_row.first->hide(); layout_row.second->hide(); } } // namespace colmap colmap-3.9.1/src/colmap/ui/options_widget.h000066400000000000000000000105321454702036400206730ustar00rootroot00000000000000// 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. #pragma once #include #include #include namespace colmap { class OptionsWidget : public QWidget { public: explicit OptionsWidget(QWidget* parent); void AddOptionRow(const std::string& label_text, QWidget* widget, void* option); void AddWidgetRow(const std::string& label_text, QWidget* widget); void AddLayoutRow(const std::string& label_text, QLayout* layout); QSpinBox* AddOptionInt(int* option, const std::string& label_text, int min = 0, int max = static_cast(1e7)); QDoubleSpinBox* AddOptionDouble(double* option, const std::string& label_text, double min = 0, double max = 1e7, double step = 0.01, int decimals = 2); QDoubleSpinBox* AddOptionDoubleLog(double* option, const std::string& label_text, double min = 0, double max = 1e7, double step = 0.01, int decimals = 2); QCheckBox* AddOptionBool(bool* option, const std::string& label_text); QLineEdit* AddOptionText(std::string* option, const std::string& label_text); QLineEdit* AddOptionFilePath(std::string* option, const std::string& label_text); QLineEdit* AddOptionDirPath(std::string* option, const std::string& label_text); void AddSpacer(); void AddSection(const std::string& title); void ReadOptions(); void WriteOptions(); protected: void showEvent(QShowEvent* event); void closeEvent(QCloseEvent* event); void hideEvent(QHideEvent* event); void ShowOption(void* option); void HideOption(void* option); void ShowWidget(QWidget* option); void HideWidget(QWidget* option); void ShowLayout(QLayout* option); void HideLayout(QLayout* option); QGridLayout* grid_layout_; std::unordered_map> option_rows_; std::unordered_map> widget_rows_; std::unordered_map> layout_rows_; std::vector> options_int_; std::vector> options_double_; std::vector> options_double_log_; std::vector> options_bool_; std::vector> options_text_; std::vector> options_path_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/point_painter.cc000066400000000000000000000074261454702036400206560ustar00rootroot00000000000000// 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. #include "colmap/ui/point_painter.h" #include "colmap/util/opengl_utils.h" namespace colmap { PointPainter::PointPainter() : num_geoms_(0) {} PointPainter::~PointPainter() { vao_.destroy(); vbo_.destroy(); } void PointPainter::Setup() { vao_.destroy(); vbo_.destroy(); if (shader_program_.isLinked()) { shader_program_.release(); shader_program_.removeAllShaders(); } shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/points.v.glsl"); shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/points.f.glsl"); shader_program_.link(); shader_program_.bind(); vao_.create(); vbo_.create(); #if DEBUG glDebugLog(); #endif } void PointPainter::Upload(const std::vector& data) { num_geoms_ = data.size(); if (num_geoms_ == 0) { return; } vao_.bind(); vbo_.bind(); // Upload data array to GPU vbo_.setUsagePattern(QOpenGLBuffer::DynamicDraw); vbo_.allocate(data.data(), static_cast(data.size() * sizeof(PointPainter::Data))); // in_position shader_program_.enableAttributeArray("a_position"); shader_program_.setAttributeBuffer( "a_position", GL_FLOAT, 0, 3, sizeof(PointPainter::Data)); // in_color shader_program_.enableAttributeArray("a_color"); shader_program_.setAttributeBuffer( "a_color", GL_FLOAT, 3 * sizeof(GLfloat), 4, sizeof(PointPainter::Data)); // Make sure they are not changed from the outside vbo_.release(); vao_.release(); #if DEBUG glDebugLog(); #endif } void PointPainter::Render(const QMatrix4x4& pmv_matrix, const float point_size) { if (num_geoms_ == 0) { return; } shader_program_.bind(); vao_.bind(); shader_program_.setUniformValue("u_pmv_matrix", pmv_matrix); shader_program_.setUniformValue("u_point_size", point_size); QOpenGLFunctions* gl_funcs = QOpenGLContext::currentContext()->functions(); gl_funcs->glDrawArrays(GL_POINTS, 0, (GLsizei)num_geoms_); // Make sure the VAO is not changed from the outside vao_.release(); #if DEBUG glDebugLog(); #endif } } // namespace colmap colmap-3.9.1/src/colmap/ui/point_painter.h000066400000000000000000000045701454702036400205150ustar00rootroot00000000000000// 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. #pragma once #include #include namespace colmap { class PointPainter { public: PointPainter(); ~PointPainter(); struct Data { Data() : x(0), y(0), z(0), r(0), g(0), b(0), a(0) {} Data(const float x_, const float y_, const float z_, const float r_, const float g_, const float b_, const float a_) : x(x_), y(y_), z(z_), r(r_), g(g_), b(b_), a(a_) {} float x, y, z; float r, g, b, a; }; void Setup(); void Upload(const std::vector& data); void Render(const QMatrix4x4& pmv_matrix, float point_size); private: QOpenGLShaderProgram shader_program_; QOpenGLVertexArrayObject vao_; QOpenGLBuffer vbo_; size_t num_geoms_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/point_viewer_widget.cc000066400000000000000000000265641454702036400220640ustar00rootroot00000000000000// 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. #include "colmap/ui/point_viewer_widget.h" #include "colmap/ui/model_viewer_widget.h" #include "colmap/util/misc.h" namespace colmap { PointViewerWidget::PointViewerWidget(QWidget* parent, ModelViewerWidget* model_viewer_widget, OptionManager* options) : QWidget(parent), model_viewer_widget_(model_viewer_widget), options_(options), point3D_id_(kInvalidPoint3DId), zoom_(250.0 / 1024.0) { setWindowFlags(Qt::Window); resize(parent->size().width() - 20, parent->size().height() - 20); QFont font; font.setPointSize(10); setFont(font); QGridLayout* grid = new QGridLayout(this); grid->setContentsMargins(5, 5, 5, 5); info_table_ = new QTableWidget(this); info_table_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); info_table_->setEditTriggers(QAbstractItemView::NoEditTriggers); info_table_->setSelectionMode(QAbstractItemView::SingleSelection); info_table_->setShowGrid(true); info_table_->horizontalHeader()->setStretchLastSection(true); info_table_->horizontalHeader()->setVisible(false); info_table_->verticalHeader()->setVisible(false); info_table_->verticalHeader()->setDefaultSectionSize(18); info_table_->setColumnCount(2); info_table_->setRowCount(3); info_table_->setItem(0, 0, new QTableWidgetItem("position")); xyz_item_ = new QTableWidgetItem(); info_table_->setItem(0, 1, xyz_item_); info_table_->setItem(1, 0, new QTableWidgetItem("color")); rgb_item_ = new QTableWidgetItem(); info_table_->setItem(1, 1, rgb_item_); info_table_->setItem(2, 0, new QTableWidgetItem("error")); error_item_ = new QTableWidgetItem(); info_table_->setItem(2, 1, error_item_); grid->addWidget(info_table_, 0, 0); location_table_ = new QTableWidget(this); location_table_->setColumnCount(4); QStringList table_header; table_header << "image_id" << "reproj_error" << "track_location" << "image_name"; location_table_->setHorizontalHeaderLabels(table_header); location_table_->resizeColumnsToContents(); location_table_->setShowGrid(true); location_table_->horizontalHeader()->setStretchLastSection(true); location_table_->verticalHeader()->setVisible(true); location_table_->setSelectionMode(QAbstractItemView::NoSelection); location_table_->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); location_table_->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); location_table_->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); grid->addWidget(location_table_, 1, 0); QHBoxLayout* button_layout = new QHBoxLayout(); zoom_in_button_ = new QPushButton(tr("+"), this); zoom_in_button_->setFont(font); zoom_in_button_->setFixedWidth(50); button_layout->addWidget(zoom_in_button_); connect(zoom_in_button_, &QPushButton::released, this, &PointViewerWidget::ZoomIn); zoom_out_button_ = new QPushButton(tr("-"), this); zoom_out_button_->setFont(font); zoom_out_button_->setFixedWidth(50); button_layout->addWidget(zoom_out_button_); connect(zoom_out_button_, &QPushButton::released, this, &PointViewerWidget::ZoomOut); delete_button_ = new QPushButton(tr("Delete"), this); button_layout->addWidget(delete_button_); connect( delete_button_, &QPushButton::released, this, &PointViewerWidget::Delete); grid->addLayout(button_layout, 2, 0, Qt::AlignRight); } void PointViewerWidget::Show(const point3D_t point3D_id) { location_pixmaps_.clear(); image_ids_.clear(); reproj_errors_.clear(); image_names_.clear(); if (model_viewer_widget_->points3D.count(point3D_id) == 0) { point3D_id_ = kInvalidPoint3DId; ClearLocations(); return; } show(); raise(); point3D_id_ = point3D_id; // Show some general information about the point. setWindowTitle(QString::fromStdString("Point " + std::to_string(point3D_id))); const auto& point3D = model_viewer_widget_->points3D[point3D_id]; xyz_item_->setText(QString::number(point3D.xyz(0)) + ", " + QString::number(point3D.xyz(1)) + ", " + QString::number(point3D.xyz(2))); rgb_item_->setText(QString::number(point3D.color(0)) + ", " + QString::number(point3D.color(1)) + ", " + QString::number(point3D.color(2))); error_item_->setText(QString::number(point3D.error)); ResizeInfoTable(); // Sort the track elements by the image names. std::vector> track_idx_image_name_pairs; track_idx_image_name_pairs.reserve(point3D.track.Length()); for (const auto& track_el : point3D.track.Elements()) { const Image& image = model_viewer_widget_->images[track_el.image_id]; track_idx_image_name_pairs.emplace_back(track_el, image.Name()); } std::sort(track_idx_image_name_pairs.begin(), track_idx_image_name_pairs.end(), [](const std::pair& track_el1, const std::pair& track_el2) { return track_el1.second < track_el2.second; }); // Paint features for each track element. for (const auto& track_el : track_idx_image_name_pairs) { const Image& image = model_viewer_widget_->images[track_el.first.image_id]; const Camera& camera = model_viewer_widget_->cameras[image.CameraId()]; const Point2D& point2D = image.Point2D(track_el.first.point2D_idx); const Eigen::Vector2d proj_point2D = camera.ImgFromCam((image.CamFromWorld() * point3D.xyz).hnormalized()); const double reproj_error = (point2D.xy - proj_point2D).norm(); Bitmap bitmap; const std::string path = JoinPaths(*options_->image_path, image.Name()); if (!bitmap.Read(path, true)) { LOG(ERROR) << "Cannot read image at path " << path; continue; } QPixmap pixmap = QPixmap::fromImage(BitmapToQImageRGB(bitmap)); // Paint feature in current image. QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); QPen pen; pen.setWidth(3); pen.setColor(Qt::green); painter.setPen(pen); const int kCrossSize = 15; const int x = static_cast(std::round(point2D.xy(0))); const int y = static_cast(std::round(point2D.xy(1))); painter.drawLine( x - kCrossSize, y - kCrossSize, x + kCrossSize, y + kCrossSize); painter.drawLine( x - kCrossSize, y + kCrossSize, x + kCrossSize, y - kCrossSize); pen.setColor(Qt::red); painter.setPen(pen); const int proj_x = static_cast(std::round(proj_point2D.x())); const int proj_y = static_cast(std::round(proj_point2D.y())); painter.drawEllipse(proj_x - 5, proj_y - 5, 10, 10); painter.drawEllipse(proj_x - 15, proj_y - 15, 30, 30); painter.drawEllipse(proj_x - 45, proj_y - 45, 90, 90); location_pixmaps_.push_back(pixmap); image_ids_.push_back(track_el.first.image_id); reproj_errors_.push_back(reproj_error); image_names_.push_back(image.Name()); } UpdateImages(); } void PointViewerWidget::closeEvent(QCloseEvent* event) { // Release the images, since zoomed in images can use a lot of memory. location_pixmaps_.clear(); image_ids_.clear(); reproj_errors_.clear(); image_names_.clear(); ClearLocations(); } void PointViewerWidget::ResizeInfoTable() { // Set fixed table dimensions. info_table_->resizeColumnsToContents(); int height = info_table_->horizontalHeader()->height() + 2 * info_table_->frameWidth(); for (int i = 0; i < info_table_->rowCount(); i++) { height += info_table_->rowHeight(i); } info_table_->setFixedHeight(height); } void PointViewerWidget::ClearLocations() { while (location_table_->rowCount() > 0) { location_table_->removeRow(0); } for (auto location_label : location_labels_) { delete location_label; } location_labels_.clear(); } void PointViewerWidget::UpdateImages() { ClearLocations(); location_table_->setRowCount(static_cast(location_pixmaps_.size())); for (size_t i = 0; i < location_pixmaps_.size(); ++i) { QLabel* image_id_label = new QLabel(QString::number(image_ids_[i]), this); image_id_label->setAlignment(Qt::AlignCenter); location_table_->setCellWidget(i, 0, image_id_label); location_labels_.push_back(image_id_label); QLabel* error_label = new QLabel(QString::number(reproj_errors_[i]), this); error_label->setAlignment(Qt::AlignCenter); location_table_->setCellWidget(i, 1, error_label); location_labels_.push_back(error_label); const QPixmap& pixmap = location_pixmaps_[i]; QLabel* image_label = new QLabel(this); image_label->setPixmap( pixmap.scaledToWidth(zoom_ * pixmap.width(), Qt::FastTransformation)); location_table_->setCellWidget(i, 2, image_label); location_table_->resizeRowToContents(i); location_labels_.push_back(image_label); QLabel* image_name_label = new QLabel(image_names_[i].c_str(), this); image_name_label->setAlignment(Qt::AlignCenter); location_table_->setCellWidget(i, 3, image_name_label); location_labels_.push_back(image_name_label); } location_table_->resizeColumnToContents(2); } void PointViewerWidget::ZoomIn() { zoom_ *= 1.33; UpdateImages(); } void PointViewerWidget::ZoomOut() { zoom_ /= 1.3; UpdateImages(); } void PointViewerWidget::Delete() { QMessageBox::StandardButton reply = QMessageBox::question(this, "", tr("Do you really want to delete this point?"), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { if (model_viewer_widget_->reconstruction->ExistsPoint3D(point3D_id_)) { model_viewer_widget_->reconstruction->DeletePoint3D(point3D_id_); } model_viewer_widget_->ReloadReconstruction(); } } } // namespace colmap colmap-3.9.1/src/colmap/ui/point_viewer_widget.h000066400000000000000000000054071454702036400217170ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/scene/reconstruction.h" #include #include namespace colmap { class ModelViewerWidget; class PointViewerWidget : public QWidget { public: PointViewerWidget(QWidget* parent, ModelViewerWidget* model_viewer_widget, OptionManager* option); void Show(point3D_t point3D_id); private: void closeEvent(QCloseEvent* event); void ResizeInfoTable(); void ClearLocations(); void UpdateImages(); void ZoomIn(); void ZoomOut(); void Delete(); ModelViewerWidget* model_viewer_widget_; OptionManager* options_; QPushButton* delete_button_; point3D_t point3D_id_; QTableWidget* info_table_; QTableWidgetItem* xyz_item_; QTableWidgetItem* rgb_item_; QTableWidgetItem* error_item_; QTableWidget* location_table_; std::vector location_pixmaps_; std::vector location_labels_; std::vector image_ids_; std::vector reproj_errors_; std::vector image_names_; QPushButton* zoom_in_button_; QPushButton* zoom_out_button_; double zoom_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/project_widget.cc000066400000000000000000000144621454702036400210120ustar00rootroot00000000000000// 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. #include "colmap/ui/project_widget.h" #include "colmap/scene/database.h" namespace colmap { ProjectWidget::ProjectWidget(QWidget* parent, OptionManager* options) : QWidget(parent), options_(options), prev_selected_(false) { setWindowFlags(Qt::Dialog); setWindowModality(Qt::ApplicationModal); setWindowTitle("Project"); // Database path. QPushButton* database_path_new = new QPushButton(tr("New"), this); connect(database_path_new, &QPushButton::released, this, &ProjectWidget::SelectNewDatabasePath); QPushButton* database_path_open = new QPushButton(tr("Open"), this); connect(database_path_open, &QPushButton::released, this, &ProjectWidget::SelectExistingDatabasePath); database_path_text_ = new QLineEdit(this); database_path_text_->setText( QString::fromStdString(*options_->database_path)); // Image path. QPushButton* image_path_select = new QPushButton(tr("Select"), this); connect(image_path_select, &QPushButton::released, this, &ProjectWidget::SelectImagePath); image_path_text_ = new QLineEdit(this); image_path_text_->setText(QString::fromStdString(*options_->image_path)); // Save button. QPushButton* create_button = new QPushButton(tr("Save"), this); connect(create_button, &QPushButton::released, this, &ProjectWidget::Save); QGridLayout* grid = new QGridLayout(this); grid->addWidget(new QLabel(tr("Database"), this), 0, 0); grid->addWidget(database_path_text_, 0, 1); grid->addWidget(database_path_new, 0, 2); grid->addWidget(database_path_open, 0, 3); grid->addWidget(new QLabel(tr("Images"), this), 1, 0); grid->addWidget(image_path_text_, 1, 1); grid->addWidget(image_path_select, 1, 2); grid->addWidget(create_button, 2, 2); } bool ProjectWidget::IsValid() const { return ExistsDir(GetImagePath()) && !ExistsDir(GetDatabasePath()) && ExistsDir(GetParentDir(GetDatabasePath())); } void ProjectWidget::Reset() { database_path_text_->clear(); image_path_text_->clear(); } std::string ProjectWidget::GetDatabasePath() const { return database_path_text_->text().toUtf8().constData(); } std::string ProjectWidget::GetImagePath() const { return image_path_text_->text().toUtf8().constData(); } void ProjectWidget::SetDatabasePath(const std::string& path) { database_path_text_->setText(QString::fromStdString(path)); } void ProjectWidget::SetImagePath(const std::string& path) { image_path_text_->setText(QString::fromStdString(path)); } void ProjectWidget::Save() { if (IsValid()) { *options_->database_path = GetDatabasePath(); *options_->image_path = GetImagePath(); // Save empty database file. Database database(*options_->database_path); hide(); } else { QMessageBox::critical(this, "", tr("Invalid paths")); } } void ProjectWidget::SelectNewDatabasePath() { QString database_path = QFileDialog::getSaveFileName(this, tr("Select database file"), DefaultDirectory(), tr("SQLite3 database (*.db)")); if (database_path != "") { if (!HasFileExtension(database_path.toUtf8().constData(), ".db")) { database_path += ".db"; } database_path_text_->setText(database_path); } } void ProjectWidget::SelectExistingDatabasePath() { const auto database_path = QFileDialog::getOpenFileName(this, tr("Select database file"), DefaultDirectory(), tr("SQLite3 database (*.db)")); if (database_path != "") { database_path_text_->setText(database_path); } } void ProjectWidget::SelectImagePath() { const auto image_path = QFileDialog::getExistingDirectory(this, tr("Select image path..."), DefaultDirectory(), QFileDialog::ShowDirsOnly); if (image_path != "") { image_path_text_->setText(image_path); } } QString ProjectWidget::DefaultDirectory() { if (prev_selected_) { return ""; } prev_selected_ = true; if (!options_->project_path->empty()) { const auto parent_path = GetParentDir(*options_->project_path); if (ExistsDir(parent_path)) { return QString::fromStdString(parent_path); } } if (!database_path_text_->text().isEmpty()) { const auto parent_path = GetParentDir(database_path_text_->text().toUtf8().constData()); if (ExistsDir(parent_path)) { return QString::fromStdString(parent_path); } } if (!image_path_text_->text().isEmpty()) { return image_path_text_->text(); } return ""; } } // namespace colmap colmap-3.9.1/src/colmap/ui/project_widget.h000066400000000000000000000047061454702036400206540ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/util/misc.h" #include #include namespace colmap { class ProjectWidget : public QWidget { public: ProjectWidget(QWidget* parent, OptionManager* options); bool IsValid() const; void Reset(); std::string GetDatabasePath() const; std::string GetImagePath() const; void SetDatabasePath(const std::string& path); void SetImagePath(const std::string& path); private: void Save(); void SelectNewDatabasePath(); void SelectExistingDatabasePath(); void SelectImagePath(); QString DefaultDirectory(); OptionManager* options_; // Whether file dialog was opened previously. bool prev_selected_; // Text boxes that hold the currently selected paths. QLineEdit* database_path_text_; QLineEdit* image_path_text_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/qt_utils.cc000066400000000000000000000117361454702036400176460ustar00rootroot00000000000000// 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. #include "colmap/ui/qt_utils.h" #include "colmap/sensor/models.h" #include "colmap/util/misc.h" namespace colmap { Eigen::Matrix4f QMatrixToEigen(const QMatrix4x4& matrix) { Eigen::Matrix4f eigen; for (size_t r = 0; r < 4; ++r) { for (size_t c = 0; c < 4; ++c) { eigen(r, c) = matrix(r, c); } } return eigen; } QMatrix4x4 EigenToQMatrix(const Eigen::Matrix4f& matrix) { QMatrix4x4 qt; for (size_t r = 0; r < 4; ++r) { for (size_t c = 0; c < 4; ++c) { qt(r, c) = matrix(r, c); } } return qt; } QImage BitmapToQImageRGB(const Bitmap& bitmap) { QImage image(bitmap.Width(), bitmap.Height(), QImage::Format_RGB32); for (int y = 0; y < image.height(); ++y) { QRgb* image_line = (QRgb*)image.scanLine(y); for (int x = 0; x < image.width(); ++x) { BitmapColor color; if (bitmap.GetPixel(x, y, &color)) { image_line[x] = qRgba(color.r, color.g, color.b, 255); } } } return image; } QPixmap ShowImagesSideBySide(const QPixmap& image1, const QPixmap& image2) { QPixmap image = QPixmap(QSize(image1.width() + image2.width(), std::max(image1.height(), image2.height()))); image.fill(Qt::black); QPainter painter(&image); painter.drawImage(0, 0, image1.toImage()); painter.drawImage(image1.width(), 0, image2.toImage()); return image; } void DrawKeypoints(QPixmap* pixmap, const FeatureKeypoints& points, const QColor& color) { if (pixmap->isNull()) { return; } const int pen_width = std::max(pixmap->width(), pixmap->height()) / 2048 + 1; const int radius = 3 * pen_width + (3 * pen_width) % 2; const float radius2 = radius / 2.0f; QPainter painter(pixmap); painter.setRenderHint(QPainter::Antialiasing); QPen pen; pen.setWidth(pen_width); pen.setColor(color); painter.setPen(pen); for (const auto& point : points) { painter.drawEllipse(point.x - radius2, point.y - radius2, radius, radius); } } QPixmap DrawMatches(const QPixmap& image1, const QPixmap& image2, const FeatureKeypoints& points1, const FeatureKeypoints& points2, const FeatureMatches& matches, const QColor& keypoints_color) { QPixmap image = ShowImagesSideBySide(image1, image2); QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); // Draw keypoints const int pen_width = std::max(image.width(), image.height()) / 2048 + 1; const int radius = 3 * pen_width + (3 * pen_width) % 2; const float radius2 = radius / 2.0f; QPen pen; pen.setWidth(pen_width); pen.setColor(keypoints_color); painter.setPen(pen); for (const auto& point : points1) { painter.drawEllipse(point.x - radius2, point.y - radius2, radius, radius); } for (const auto& point : points2) { painter.drawEllipse( image1.width() + point.x - radius2, point.y - radius2, radius, radius); } // Draw matches pen.setWidth(std::max(pen_width / 2, 1)); for (const auto& match : matches) { const point2D_t idx1 = match.point2D_idx1; const point2D_t idx2 = match.point2D_idx2; pen.setColor(QColor(0, 255, 0)); painter.setPen(pen); painter.drawLine(QPoint(points1[idx1].x, points1[idx1].y), QPoint(image1.width() + points2[idx2].x, points2[idx2].y)); } return image; } } // namespace colmap colmap-3.9.1/src/colmap/ui/qt_utils.h000066400000000000000000000050241454702036400175010ustar00rootroot00000000000000// 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. #pragma once #include "colmap/feature/types.h" #include "colmap/sensor/bitmap.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/types.h" #include #include #include namespace colmap { Eigen::Matrix4f QMatrixToEigen(const QMatrix4x4& matrix); QMatrix4x4 EigenToQMatrix(const Eigen::Matrix4f& matrix); QImage BitmapToQImageRGB(const Bitmap& bitmap); void DrawKeypoints(QPixmap* image, const FeatureKeypoints& points, const QColor& color = Qt::red); QPixmap ShowImagesSideBySide(const QPixmap& image1, const QPixmap& image2); QPixmap DrawMatches(const QPixmap& image1, const QPixmap& image2, const FeatureKeypoints& points1, const FeatureKeypoints& points2, const FeatureMatches& matches, const QColor& keypoints_color = Qt::red); } // namespace colmap colmap-3.9.1/src/colmap/ui/reconstruction_manager_widget.cc000066400000000000000000000075321454702036400241170ustar00rootroot00000000000000// 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. #include "colmap/ui/reconstruction_manager_widget.h" namespace colmap { const size_t ReconstructionManagerWidget::kNewestReconstructionIdx = std::numeric_limits::max(); ReconstructionManagerWidget::ReconstructionManagerWidget( QWidget* parent, std::shared_ptr reconstruction_manager) : QComboBox(parent), reconstruction_manager_(std::move(reconstruction_manager)) { QFont font; font.setPointSize(10); setFont(font); } void ReconstructionManagerWidget::Update() { if (view()->isVisible()) { return; } blockSignals(true); const int prev_idx = currentIndex() == -1 ? 0 : currentIndex(); clear(); addItem("Newest model"); int max_width = 0; for (size_t i = 0; i < reconstruction_manager_->Size(); ++i) { const QString item = QString().asprintf( "Model %d (%d images, %d points)", static_cast(i + 1), static_cast(reconstruction_manager_->Get(i)->NumRegImages()), static_cast(reconstruction_manager_->Get(i)->NumPoints3D())); QFontMetrics font_metrics(view()->font()); #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) const auto width = font_metrics.horizontalAdvance(item); #else const auto width = font_metrics.width(item); #endif max_width = std::max(max_width, width); addItem(item); } view()->setMinimumWidth(max_width); if (reconstruction_manager_->Size() == 0) { setCurrentIndex(0); } else { const int max_idx = static_cast(reconstruction_manager_->Size()); if (prev_idx <= max_idx) { setCurrentIndex(prev_idx); } else { setCurrentIndex(max_idx); } } blockSignals(false); } size_t ReconstructionManagerWidget::SelectedReconstructionIdx() const { if (reconstruction_manager_->Size() == 0) { return kNewestReconstructionIdx; } else { if (currentIndex() == 0) { return kNewestReconstructionIdx; } else { return currentIndex() - 1; } } } void ReconstructionManagerWidget::SelectReconstruction(const size_t idx) { if (reconstruction_manager_->Size() == 0) { blockSignals(true); setCurrentIndex(0); blockSignals(false); } else { blockSignals(true); setCurrentIndex(idx + 1); blockSignals(false); } } } // namespace colmap colmap-3.9.1/src/colmap/ui/reconstruction_manager_widget.h000066400000000000000000000042311454702036400237520ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/reconstruction_manager.h" #include namespace colmap { class ReconstructionManagerWidget : public QComboBox { public: const static size_t kNewestReconstructionIdx; ReconstructionManagerWidget( QWidget* parent, std::shared_ptr reconstruction_manager); void Update(); size_t SelectedReconstructionIdx() const; void SelectReconstruction(size_t idx); private: const std::shared_ptr reconstruction_manager_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/reconstruction_options_widget.cc000066400000000000000000000215071454702036400241760ustar00rootroot00000000000000// 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. #include "colmap/ui/reconstruction_options_widget.h" #include "colmap/controllers/incremental_mapper.h" namespace colmap { MapperGeneralOptionsWidget::MapperGeneralOptionsWidget(QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddOptionBool(&options->mapper->multiple_models, "multiple_models"); AddOptionInt(&options->mapper->max_num_models, "max_num_models"); AddOptionInt(&options->mapper->max_model_overlap, "max_model_overlap"); AddOptionInt(&options->mapper->min_model_size, "min_model_size"); AddOptionBool(&options->mapper->extract_colors, "extract_colors"); AddOptionInt(&options->mapper->num_threads, "num_threads", -1); AddOptionInt(&options->mapper->min_num_matches, "min_num_matches"); AddOptionBool(&options->mapper->ignore_watermarks, "ignore_watermarks"); AddOptionDirPath(&options->mapper->snapshot_path, "snapshot_path"); AddOptionInt( &options->mapper->snapshot_images_freq, "snapshot_images_freq", 0); } MapperTriangulationOptionsWidget::MapperTriangulationOptionsWidget( QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddOptionInt(&options->mapper->triangulation.max_transitivity, "max_transitivity"); AddOptionDouble(&options->mapper->triangulation.create_max_angle_error, "create_max_angle_error [deg]"); AddOptionDouble(&options->mapper->triangulation.continue_max_angle_error, "continue_max_angle_error [deg]"); AddOptionDouble(&options->mapper->triangulation.merge_max_reproj_error, "merge_max_reproj_error [px]"); AddOptionDouble(&options->mapper->triangulation.re_max_angle_error, "re_max_angle_error [deg]"); AddOptionDouble(&options->mapper->triangulation.re_min_ratio, "re_min_ratio"); AddOptionInt(&options->mapper->triangulation.re_max_trials, "re_max_trials"); AddOptionDouble(&options->mapper->triangulation.complete_max_reproj_error, "complete_max_reproj_error [px]"); AddOptionInt(&options->mapper->triangulation.complete_max_transitivity, "complete_max_transitivity"); AddOptionDouble( &options->mapper->triangulation.min_angle, "min_angle [deg]", 0, 180); AddOptionBool(&options->mapper->triangulation.ignore_two_view_tracks, "ignore_two_view_tracks"); } MapperRegistrationOptionsWidget::MapperRegistrationOptionsWidget( QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddOptionDouble(&options->mapper->mapper.abs_pose_max_error, "abs_pose_max_error [px]"); AddOptionInt(&options->mapper->mapper.abs_pose_min_num_inliers, "abs_pose_min_num_inliers"); AddOptionDouble(&options->mapper->mapper.abs_pose_min_inlier_ratio, "abs_pose_min_inlier_ratio"); AddOptionInt(&options->mapper->mapper.max_reg_trials, "max_reg_trials", 1); } MapperInitializationOptionsWidget::MapperInitializationOptionsWidget( QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddOptionInt(&options->mapper->init_image_id1, "init_image_id1", -1); AddOptionInt(&options->mapper->init_image_id2, "init_image_id2", -1); AddOptionInt(&options->mapper->init_num_trials, "init_num_trials"); AddOptionInt(&options->mapper->mapper.init_min_num_inliers, "init_min_num_inliers"); AddOptionDouble(&options->mapper->mapper.init_max_error, "init_max_error"); AddOptionDouble(&options->mapper->mapper.init_max_forward_motion, "init_max_forward_motion"); AddOptionDouble(&options->mapper->mapper.init_min_tri_angle, "init_min_tri_angle [deg]"); AddOptionInt( &options->mapper->mapper.init_max_reg_trials, "init_max_reg_trials", 1); } MapperBundleAdjustmentOptionsWidget::MapperBundleAdjustmentOptionsWidget( QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddSection("Camera parameters"); AddOptionBool(&options->mapper->ba_refine_focal_length, "refine_focal_length"); AddOptionBool(&options->mapper->ba_refine_principal_point, "refine_principal_point"); AddOptionBool(&options->mapper->ba_refine_extra_params, "refine_extra_params"); AddSpacer(); AddSection("Local Bundle Adjustment"); AddOptionInt(&options->mapper->ba_local_num_images, "num_images"); AddOptionInt(&options->mapper->ba_local_max_num_iterations, "max_num_iterations"); AddOptionInt( &options->mapper->ba_local_max_refinements, "max_refinements", 1); AddOptionDouble(&options->mapper->ba_local_max_refinement_change, "max_refinement_change", 0, 1, 1e-6, 6); AddSpacer(); AddSection("Global Bundle Adjustment"); AddOptionDouble(&options->mapper->ba_global_images_ratio, "images_ratio"); AddOptionInt(&options->mapper->ba_global_images_freq, "images_freq"); AddOptionDouble(&options->mapper->ba_global_points_ratio, "points_ratio"); AddOptionInt(&options->mapper->ba_global_points_freq, "points_freq"); AddOptionInt(&options->mapper->ba_global_max_num_iterations, "max_num_iterations"); AddOptionInt( &options->mapper->ba_global_max_refinements, "max_refinements", 1); AddOptionDouble(&options->mapper->ba_global_max_refinement_change, "max_refinement_change", 0, 1, 1e-6, 6); } MapperFilteringOptionsWidget::MapperFilteringOptionsWidget( QWidget* parent, OptionManager* options) : OptionsWidget(parent) { AddOptionDouble(&options->mapper->min_focal_length_ratio, "min_focal_length_ratio"); AddOptionDouble(&options->mapper->max_focal_length_ratio, "max_focal_length_ratio"); AddOptionDouble(&options->mapper->max_extra_param, "max_extra_param"); AddOptionDouble(&options->mapper->mapper.filter_max_reproj_error, "filter_max_reproj_error [px]"); AddOptionDouble(&options->mapper->mapper.filter_min_tri_angle, "filter_min_tri_angle [deg]"); } ReconstructionOptionsWidget::ReconstructionOptionsWidget(QWidget* parent, OptionManager* options) : QWidget(parent) { setWindowFlags(Qt::Dialog); setWindowModality(Qt::ApplicationModal); setWindowTitle("Reconstruction options"); QGridLayout* grid = new QGridLayout(this); QTabWidget* tab_widget = new QTabWidget(this); tab_widget->setElideMode(Qt::TextElideMode::ElideRight); tab_widget->addTab(new MapperGeneralOptionsWidget(this, options), tr("General")); tab_widget->addTab(new MapperInitializationOptionsWidget(this, options), tr("Init")); tab_widget->addTab(new MapperRegistrationOptionsWidget(this, options), tr("Registration")); tab_widget->addTab(new MapperTriangulationOptionsWidget(this, options), tr("Triangulation")); tab_widget->addTab(new MapperBundleAdjustmentOptionsWidget(this, options), tr("Bundle")); tab_widget->addTab(new MapperFilteringOptionsWidget(this, options), tr("Filter")); grid->addWidget(tab_widget, 0, 0); } } // namespace colmap colmap-3.9.1/src/colmap/ui/reconstruction_options_widget.h000066400000000000000000000054571454702036400240460ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/ui/options_widget.h" #include #include namespace colmap { class MapperGeneralOptionsWidget : public OptionsWidget { public: MapperGeneralOptionsWidget(QWidget* parent, OptionManager* options); }; class MapperTriangulationOptionsWidget : public OptionsWidget { public: MapperTriangulationOptionsWidget(QWidget* parent, OptionManager* options); }; class MapperRegistrationOptionsWidget : public OptionsWidget { public: MapperRegistrationOptionsWidget(QWidget* parent, OptionManager* options); }; class MapperInitializationOptionsWidget : public OptionsWidget { public: MapperInitializationOptionsWidget(QWidget* parent, OptionManager* options); }; class MapperBundleAdjustmentOptionsWidget : public OptionsWidget { public: MapperBundleAdjustmentOptionsWidget(QWidget* parent, OptionManager* options); }; class MapperFilteringOptionsWidget : public OptionsWidget { public: MapperFilteringOptionsWidget(QWidget* parent, OptionManager* options); }; class ReconstructionOptionsWidget : public QWidget { public: ReconstructionOptionsWidget(QWidget* parent, OptionManager* options); }; } // namespace colmap colmap-3.9.1/src/colmap/ui/reconstruction_stats_widget.cc000066400000000000000000000070111454702036400236330ustar00rootroot00000000000000// 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. #include "colmap/ui/reconstruction_stats_widget.h" namespace colmap { ReconstructionStatsWidget::ReconstructionStatsWidget(QWidget* parent) : QWidget(parent) { setWindowFlags(Qt::Window); resize(parent->width() - 20, parent->height() - 20); setWindowTitle("Reconstruction statistics"); stats_table_ = new QTableWidget(this); stats_table_->setColumnCount(2); stats_table_->horizontalHeader()->setVisible(false); stats_table_->verticalHeader()->setVisible(false); stats_table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); QGridLayout* grid = new QGridLayout(this); grid->addWidget(stats_table_); } void ReconstructionStatsWidget::Show(const Reconstruction& reconstruction) { QString stats; stats_table_->clearContents(); stats_table_->setRowCount(0); AddStatistic("Cameras", QString::number(reconstruction.NumCameras())); AddStatistic("Images", QString::number(reconstruction.NumImages())); AddStatistic("Registered images", QString::number(reconstruction.NumRegImages())); AddStatistic("Points", QString::number(reconstruction.NumPoints3D())); AddStatistic("Observations", QString::number(reconstruction.ComputeNumObservations())); AddStatistic("Mean track length", QString::number(reconstruction.ComputeMeanTrackLength())); AddStatistic( "Mean observations per image", QString::number(reconstruction.ComputeMeanObservationsPerRegImage())); AddStatistic("Mean reprojection error", QString::number(reconstruction.ComputeMeanReprojectionError())); } void ReconstructionStatsWidget::AddStatistic(const QString& header, const QString& content) { const int row = stats_table_->rowCount(); stats_table_->insertRow(row); stats_table_->setItem(row, 0, new QTableWidgetItem(header)); stats_table_->setItem(row, 1, new QTableWidgetItem(content)); } } // namespace colmap colmap-3.9.1/src/colmap/ui/reconstruction_stats_widget.h000066400000000000000000000037621454702036400235060ustar00rootroot00000000000000// 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. #pragma once #include "colmap/scene/reconstruction.h" #include namespace colmap { class ReconstructionStatsWidget : public QWidget { public: explicit ReconstructionStatsWidget(QWidget* parent); void Show(const Reconstruction& reconstruction); private: void AddStatistic(const QString& header, const QString& content); QTableWidget* stats_table_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/render_options.h000066400000000000000000000052341454702036400206720ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include namespace colmap { struct RenderOptions { enum ProjectionType { PERSPECTIVE, ORTHOGRAPHIC, }; // Minimum track length for a point to be rendered. int min_track_len = 3; // Maximum error for a point to be rendered. double max_error = 2; // The rate of registered images at which to refresh. int refresh_rate = 1; // Whether to automatically adjust the refresh rate. The bigger the // reconstruction gets, the less frequently the scene is rendered. bool adapt_refresh_rate = true; // Whether to visualize image connections. bool image_connections = false; // The projection type of the renderer. int projection_type = ProjectionType::PERSPECTIVE; inline bool Check() const { CHECK_OPTION_GE(min_track_len, 0); CHECK_OPTION_GE(max_error, 0); CHECK_OPTION_GT(refresh_rate, 0); CHECK_OPTION(projection_type == ProjectionType::PERSPECTIVE || projection_type == ProjectionType::ORTHOGRAPHIC); return true; } }; } // namespace colmap colmap-3.9.1/src/colmap/ui/render_options_widget.cc000066400000000000000000000310411454702036400223660ustar00rootroot00000000000000// 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. #include "colmap/ui/render_options_widget.h" #include "colmap/ui/colormaps.h" namespace colmap { RenderOptionsWidget::RenderOptionsWidget(QWidget* parent, OptionManager* options, ModelViewerWidget* model_viewer_widget) : OptionsWidget(parent), counter(0), automatic_update(true), options_(options), model_viewer_widget_(model_viewer_widget), background_color_(1.0f, 1.0f, 1.0f, 1.0f), point3D_colormap_scale_(1), point3D_colormap_min_q_(0.02), point3D_colormap_max_q_(0.98), image_plane_color_(ImageColormapUniform::kDefaultPlaneColor), image_frame_color_(ImageColormapUniform::kDefaultFrameColor) { setWindowFlags(Qt::Widget | Qt::WindowStaysOnTopHint | Qt::Tool); setWindowModality(Qt::NonModal); setWindowTitle("Render options"); QHBoxLayout* point_size_layout = new QHBoxLayout(); QPushButton* decrease_point_size = new QPushButton("-", this); connect(decrease_point_size, &QPushButton::released, this, &RenderOptionsWidget::DecreasePointSize); QPushButton* increase_point_size = new QPushButton("+", this); connect(increase_point_size, &QPushButton::released, this, &RenderOptionsWidget::IncreasePointSize); point_size_layout->addWidget(decrease_point_size); point_size_layout->addWidget(increase_point_size); AddLayoutRow("Point size", point_size_layout); QHBoxLayout* camera_size_layout = new QHBoxLayout(); QPushButton* decrease_camera_size = new QPushButton("-", this); connect(decrease_camera_size, &QPushButton::released, this, &RenderOptionsWidget::DecreaseCameraSize); QPushButton* increase_camera_size = new QPushButton("+", this); connect(increase_camera_size, &QPushButton::released, this, &RenderOptionsWidget::IncreaseCameraSize); camera_size_layout->addWidget(decrease_camera_size); camera_size_layout->addWidget(increase_camera_size); AddLayoutRow("Camera size", camera_size_layout); AddSpacer(); projection_cb_ = new QComboBox(this); projection_cb_->addItem("Perspective"); projection_cb_->addItem("Orthographic"); AddWidgetRow("Projection", projection_cb_); AddSpacer(); QPushButton* select_background_color = new QPushButton(tr("Select color"), this); grid_layout_->addWidget( select_background_color, grid_layout_->rowCount() - 1, 1); connect(select_background_color, &QPushButton::released, this, [&]() { SelectColor("Background color", &background_color_); }); AddWidgetRow("Background", select_background_color); AddSpacer(); AddOptionDouble(&options->render->max_error, "Point max. error [px]"); AddOptionInt(&options->render->min_track_len, "Point min. track length", 0); AddSpacer(); point3D_colormap_cb_ = new QComboBox(this); point3D_colormap_cb_->addItem("Photometric"); point3D_colormap_cb_->addItem("Error"); point3D_colormap_cb_->addItem("Track-Length"); point3D_colormap_cb_->addItem("Ground-Resolution"); AddWidgetRow("Point colormap", point3D_colormap_cb_); AddOptionDouble( &point3D_colormap_min_q_, "Point colormap minq", 0, 1, 0.001, 3); AddOptionDouble( &point3D_colormap_max_q_, "Point colormap maxq", 0, 1, 0.001, 3); AddOptionDouble(&point3D_colormap_scale_, "Point colormap scale", -1e7, 1e7); // Show the above items only for other colormaps than the photometric one. HideOption(&point3D_colormap_min_q_); HideOption(&point3D_colormap_max_q_); HideOption(&point3D_colormap_scale_); connect(point3D_colormap_cb_, (void(QComboBox::*)(int)) & QComboBox::currentIndexChanged, this, &RenderOptionsWidget::SelectPointColormap); AddSpacer(); image_colormap_cb_ = new QComboBox(this); image_colormap_cb_->addItem("Uniform color"); image_colormap_cb_->addItem("Images with words in name"); AddWidgetRow("Image colormap", image_colormap_cb_); select_image_plane_color_ = new QPushButton(tr("Select color"), this); connect(select_image_plane_color_, &QPushButton::released, this, [&]() { SelectColor("Image plane color", &image_plane_color_); }); AddWidgetRow("Image plane", select_image_plane_color_); select_image_frame_color_ = new QPushButton(tr("Select color"), this); connect(select_image_frame_color_, &QPushButton::released, this, [&]() { SelectColor("Image frame color", &image_frame_color_); }); AddWidgetRow("Image frame", select_image_frame_color_); image_colormap_name_filter_layout_ = new QHBoxLayout(); QPushButton* image_colormap_add_word = new QPushButton("Add", this); connect(image_colormap_add_word, &QPushButton::released, this, &RenderOptionsWidget::ImageColormapNameFilterAddWord); QPushButton* image_colormap_clear_words = new QPushButton("Clear", this); connect(image_colormap_clear_words, &QPushButton::released, this, &RenderOptionsWidget::ImageColormapNameFilterClearWords); image_colormap_name_filter_layout_->addWidget(image_colormap_add_word); image_colormap_name_filter_layout_->addWidget(image_colormap_clear_words); AddLayoutRow("Words", image_colormap_name_filter_layout_); HideLayout(image_colormap_name_filter_layout_); connect(image_colormap_cb_, (void(QComboBox::*)(int)) & QComboBox::currentIndexChanged, this, &RenderOptionsWidget::SelectImageColormap); AddSpacer(); AddOptionBool(&options->render->adapt_refresh_rate, "Adaptive refresh rate"); AddOptionInt(&options->render->refresh_rate, "Refresh rate [frames]", 1); AddSpacer(); AddOptionBool(&options->render->image_connections, "Image connections"); AddSpacer(); QPushButton* apply = new QPushButton(tr("Apply"), this); grid_layout_->addWidget(apply, grid_layout_->rowCount(), 1); connect(apply, &QPushButton::released, this, &RenderOptionsWidget::Apply); } void RenderOptionsWidget::closeEvent(QCloseEvent* event) { // Just overwrite parent closeEvent to prevent automatic write of options } void RenderOptionsWidget::Apply() { WriteOptions(); counter = 0; ApplyProjection(); ApplyPointColormap(); ApplyImageColormap(); ApplyBackgroundColor(); model_viewer_widget_->ReloadReconstruction(); } void RenderOptionsWidget::ApplyProjection() { switch (projection_cb_->currentIndex()) { case 0: options_->render->projection_type = RenderOptions::ProjectionType::PERSPECTIVE; break; case 1: options_->render->projection_type = RenderOptions::ProjectionType::ORTHOGRAPHIC; break; default: options_->render->projection_type = RenderOptions::ProjectionType::PERSPECTIVE; break; } } void RenderOptionsWidget::ApplyPointColormap() { PointColormapBase* point3D_color_map; switch (point3D_colormap_cb_->currentIndex()) { case 0: point3D_color_map = new PointColormapPhotometric(); break; case 1: point3D_color_map = new PointColormapError(); break; case 2: point3D_color_map = new PointColormapTrackLen(); break; case 3: point3D_color_map = new PointColormapGroundResolution(); break; default: point3D_color_map = new PointColormapPhotometric(); break; } point3D_color_map->scale = static_cast(point3D_colormap_scale_); point3D_color_map->min_q = static_cast(point3D_colormap_min_q_); point3D_color_map->max_q = static_cast(point3D_colormap_max_q_); model_viewer_widget_->SetPointColormap(point3D_color_map); } void RenderOptionsWidget::ApplyImageColormap() { ImageColormapBase* image_color_map; switch (image_colormap_cb_->currentIndex()) { case 0: image_color_map = new ImageColormapUniform(); reinterpret_cast(image_color_map) ->uniform_plane_color = image_plane_color_; reinterpret_cast(image_color_map) ->uniform_frame_color = image_frame_color_; break; case 1: image_color_map = new ImageColormapNameFilter(image_colormap_name_filter_); break; default: image_color_map = new ImageColormapUniform(); break; } model_viewer_widget_->SetImageColormap(image_color_map); } void RenderOptionsWidget::ApplyBackgroundColor() { model_viewer_widget_->SetBackgroundColor( background_color_(0), background_color_(1), background_color_(2)); } void RenderOptionsWidget::SelectColor(const std::string& title, Eigen::Vector4f* color) { const QColor initial_color(static_cast(255 * (*color)(0)), static_cast(255 * (*color)(1)), static_cast(255 * (*color)(2)), static_cast(255 * (*color)(3))); const QColor selected_color = QColorDialog::getColor(initial_color, this, title.c_str()); (*color)(0) = selected_color.red() / 255.0; (*color)(1) = selected_color.green() / 255.0; (*color)(2) = selected_color.blue() / 255.0; (*color)(3) = selected_color.alpha() / 255.0; } void RenderOptionsWidget::SelectPointColormap(const int idx) { if (idx == 0) { HideOption(&point3D_colormap_scale_); HideOption(&point3D_colormap_min_q_); HideOption(&point3D_colormap_max_q_); } else { ShowOption(&point3D_colormap_scale_); ShowOption(&point3D_colormap_min_q_); ShowOption(&point3D_colormap_max_q_); } } void RenderOptionsWidget::SelectImageColormap(const int idx) { if (idx == 0) { ShowWidget(select_image_plane_color_); ShowWidget(select_image_frame_color_); HideLayout(image_colormap_name_filter_layout_); } else { HideWidget(select_image_plane_color_); HideWidget(select_image_frame_color_); ShowLayout(image_colormap_name_filter_layout_); } } void RenderOptionsWidget::IncreasePointSize() { const float kDelta = 100; model_viewer_widget_->ChangePointSize(kDelta); } void RenderOptionsWidget::DecreasePointSize() { const float kDelta = -100; model_viewer_widget_->ChangePointSize(kDelta); } void RenderOptionsWidget::IncreaseCameraSize() { const float kDelta = 100; model_viewer_widget_->ChangeCameraSize(kDelta); } void RenderOptionsWidget::DecreaseCameraSize() { const float kDelta = -100; model_viewer_widget_->ChangeCameraSize(kDelta); } void RenderOptionsWidget::ImageColormapNameFilterAddWord() { bool word_ok; const QString word = QInputDialog::getText(this, "", "Word:", QLineEdit::Normal, "", &word_ok); if (!word_ok || word == "") { return; } Eigen::Vector4f plane_color(ImageColormapBase::kDefaultPlaneColor); SelectColor("Image plane color", &plane_color); Eigen::Vector4f frame_color(ImageColormapBase::kDefaultFrameColor); SelectColor("Image frame color", &frame_color); image_colormap_name_filter_.AddColorForWord( word.toUtf8().constData(), plane_color, frame_color); } void RenderOptionsWidget::ImageColormapNameFilterClearWords() { image_colormap_name_filter_ = ImageColormapNameFilter(); } } // namespace colmap colmap-3.9.1/src/colmap/ui/render_options_widget.h000066400000000000000000000062571454702036400222430ustar00rootroot00000000000000// 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. #pragma once #include "colmap/sfm/incremental_mapper.h" #include "colmap/ui/model_viewer_widget.h" #include "colmap/ui/options_widget.h" #include #include namespace colmap { class RenderOptionsWidget : public OptionsWidget { public: RenderOptionsWidget(QWidget* parent, OptionManager* options, ModelViewerWidget* model_viewer_widget); size_t counter; bool automatic_update; QAction* action_render_now; private: void closeEvent(QCloseEvent* event); void Apply(); void ApplyProjection(); void ApplyPointColormap(); void ApplyImageColormap(); void ApplyBackgroundColor(); void SelectColor(const std::string& title, Eigen::Vector4f* color); void SelectPointColormap(int idx); void SelectImageColormap(int idx); void IncreasePointSize(); void DecreasePointSize(); void IncreaseCameraSize(); void DecreaseCameraSize(); void ImageColormapNameFilterAddWord(); void ImageColormapNameFilterClearWords(); OptionManager* options_; ModelViewerWidget* model_viewer_widget_; Eigen::Vector4f background_color_; QComboBox* projection_cb_; QComboBox* point3D_colormap_cb_; double point3D_colormap_scale_; double point3D_colormap_min_q_; double point3D_colormap_max_q_; QComboBox* image_colormap_cb_; QPushButton* select_image_plane_color_; QPushButton* select_image_frame_color_; QHBoxLayout* image_colormap_name_filter_layout_; Eigen::Vector4f image_plane_color_; Eigen::Vector4f image_frame_color_; ImageColormapNameFilter image_colormap_name_filter_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/resources.qrc000077500000000000000000000033241454702036400202110ustar00rootroot00000000000000 shaders/points.v.glsl shaders/points.f.glsl shaders/lines.v.glsl shaders/lines.g.glsl shaders/lines.f.glsl shaders/triangles.v.glsl shaders/triangles.f.glsl media/project-new.png media/project-open.png media/project-edit.png media/project-save.png media/project-save-as.png media/import.png media/import-from.png media/export.png media/export-all.png media/export-as.png media/export-as-text.png media/feature-extraction.png media/feature-matching.png media/database-management.png media/automatic-reconstruction.png media/reconstruction-start.png media/reconstruction-pause.png media/reconstruction-step.png media/reconstruction-reset.png media/reconstruction-normalize.png media/reconstruction-options.png media/reconstruction-stats.png media/bundle-adjustment.png media/dense-reconstruction.png media/render-enabled.png media/render-disabled.png media/render-reset-view.png media/render-options.png media/match-matrix.png media/log.png media/grab-image.png media/grab-movie.png media/undistort.png colmap-3.9.1/src/colmap/ui/shaders/000077500000000000000000000000001454702036400171145ustar00rootroot00000000000000colmap-3.9.1/src/colmap/ui/shaders/lines.f.glsl000077500000000000000000000001331454702036400213350ustar00rootroot00000000000000#version 150 in vec4 g_color; out vec4 f_color; void main(void) { f_color = g_color; } colmap-3.9.1/src/colmap/ui/shaders/lines.g.glsl000077500000000000000000000016071454702036400213450ustar00rootroot00000000000000#version 150 layout(lines) in; layout(triangle_strip, max_vertices = 4) out; uniform vec2 u_inv_viewport; uniform float u_line_width; in vec4 v_pos[2]; in vec4 v_color[2]; out vec4 g_color; void main() { vec2 dir = normalize(v_pos[1].xy / v_pos[1].w - v_pos[0].xy / v_pos[0].w); vec2 normal_dir = vec2(-dir.y, dir.x); vec2 offset = (vec2(u_line_width) * u_inv_viewport) * normal_dir; gl_Position = vec4(v_pos[0].xy + offset * v_pos[0].w, v_pos[0].z, v_pos[0].w); g_color = v_color[0]; EmitVertex(); gl_Position = vec4(v_pos[1].xy + offset * v_pos[1].w, v_pos[1].z, v_pos[1].w); g_color = v_color[1]; EmitVertex(); gl_Position = vec4(v_pos[0].xy - offset * v_pos[0].w, v_pos[0].z, v_pos[0].w); g_color = v_color[0]; EmitVertex(); gl_Position = vec4(v_pos[1].xy - offset * v_pos[1].w, v_pos[1].z, v_pos[1].w); g_color = v_color[1]; EmitVertex(); EndPrimitive(); } colmap-3.9.1/src/colmap/ui/shaders/lines.v.glsl000077500000000000000000000002771454702036400213660ustar00rootroot00000000000000#version 150 uniform mat4 u_pmv_matrix; in vec3 a_pos; in vec4 a_color; out vec4 v_pos; out vec4 v_color; void main(void) { v_pos = u_pmv_matrix * vec4(a_pos, 1); v_color = a_color; } colmap-3.9.1/src/colmap/ui/shaders/points.f.glsl000077500000000000000000000001331454702036400215370ustar00rootroot00000000000000#version 150 in vec4 v_color; out vec4 f_color; void main(void) { f_color = v_color; } colmap-3.9.1/src/colmap/ui/shaders/points.v.glsl000077500000000000000000000003721454702036400215640ustar00rootroot00000000000000#version 150 uniform float u_point_size; uniform mat4 u_pmv_matrix; in vec3 a_position; in vec4 a_color; out vec4 v_color; void main(void) { gl_Position = u_pmv_matrix * vec4(a_position, 1); gl_PointSize = u_point_size; v_color = a_color; } colmap-3.9.1/src/colmap/ui/shaders/triangles.f.glsl000077500000000000000000000001331454702036400222130ustar00rootroot00000000000000#version 150 in vec4 v_color; out vec4 f_color; void main(void) { f_color = v_color; } colmap-3.9.1/src/colmap/ui/shaders/triangles.v.glsl000077500000000000000000000002771454702036400222440ustar00rootroot00000000000000#version 150 uniform mat4 u_pmv_matrix; in vec3 a_position; in vec4 a_color; out vec4 v_color; void main(void) { gl_Position = u_pmv_matrix * vec4(a_position, 1); v_color = a_color; } colmap-3.9.1/src/colmap/ui/thread_control_widget.cc000066400000000000000000000102131454702036400223410ustar00rootroot00000000000000// 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. #include "colmap/ui/bundle_adjustment_widget.h" namespace colmap { ThreadControlWidget::ThreadControlWidget(QWidget* parent) : QWidget(parent), progress_bar_(nullptr), destructor_(new QAction(this)), thread_(nullptr) { connect(destructor_, &QAction::triggered, this, [this]() { if (thread_) { thread_->Stop(); thread_->Wait(); thread_.reset(); } if (progress_bar_ != nullptr) { progress_bar_->hide(); } }); } void ThreadControlWidget::StartThread(const QString& progress_text, const bool stoppable, std::unique_ptr thread) { CHECK(!thread_); CHECK_NOTNULL(thread); thread_ = std::move(thread); if (progress_bar_ == nullptr) { progress_bar_ = new QProgressDialog(this); progress_bar_->setWindowModality(Qt::ApplicationModal); progress_bar_->setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); // Use a single space to clear the window title on Windows, otherwise it // will contain the name of the executable. progress_bar_->setWindowTitle(" "); progress_bar_->setLabel(new QLabel(this)); progress_bar_->setMaximum(0); progress_bar_->setMinimum(0); progress_bar_->setValue(0); connect(progress_bar_, &QProgressDialog::canceled, [this]() { destructor_->trigger(); }); } // Enable the cancel button if the thread is stoppable. QPushButton* cancel_button = progress_bar_->findChildren().at(0); cancel_button->setEnabled(stoppable); progress_bar_->setLabelText(progress_text); // Center the progress bar wrt. the parent widget. const QPoint global = parentWidget()->mapToGlobal(parentWidget()->rect().center()); progress_bar_->move(global.x() - progress_bar_->width() / 2, global.y() - progress_bar_->height() / 2); progress_bar_->show(); progress_bar_->raise(); thread_->AddCallback(Thread::FINISHED_CALLBACK, [this]() { destructor_->trigger(); }); thread_->Start(); } void ThreadControlWidget::StartFunction(const QString& progress_text, const std::function& func) { class FunctionThread : public Thread { public: explicit FunctionThread(const std::function& f) : func_(f) {} private: void Run() { func_(); } const std::function func_; }; StartThread(progress_text, false, std::make_unique(func)); } } // namespace colmap colmap-3.9.1/src/colmap/ui/thread_control_widget.h000066400000000000000000000042771454702036400222200ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/threading.h" #include #include #include namespace colmap { class ThreadControlWidget : public QWidget { public: explicit ThreadControlWidget(QWidget* parent); void StartThread(const QString& progress_text, bool stoppable, std::unique_ptr thread); void StartFunction(const QString& progress_text, const std::function& func); private: QProgressDialog* progress_bar_; QAction* destructor_; std::unique_ptr thread_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/triangle_painter.cc000066400000000000000000000073221454702036400213250ustar00rootroot00000000000000// 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. #include "colmap/ui/triangle_painter.h" #include "colmap/util/opengl_utils.h" namespace colmap { TrianglePainter::TrianglePainter() : num_geoms_(0) {} TrianglePainter::~TrianglePainter() { vao_.destroy(); vbo_.destroy(); } void TrianglePainter::Setup() { vao_.destroy(); vbo_.destroy(); if (shader_program_.isLinked()) { shader_program_.release(); shader_program_.removeAllShaders(); } shader_program_.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/triangles.v.glsl"); shader_program_.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/triangles.f.glsl"); shader_program_.link(); shader_program_.bind(); vao_.create(); vbo_.create(); #if DEBUG glDebugLog(); #endif } void TrianglePainter::Upload(const std::vector& data) { num_geoms_ = data.size(); if (num_geoms_ == 0) { return; } vao_.bind(); vbo_.bind(); // Upload data array to GPU vbo_.setUsagePattern(QOpenGLBuffer::DynamicDraw); vbo_.allocate(data.data(), static_cast(data.size() * sizeof(TrianglePainter::Data))); // in_position shader_program_.enableAttributeArray("a_position"); shader_program_.setAttributeBuffer( "a_position", GL_FLOAT, 0, 3, sizeof(PointPainter::Data)); // in_color shader_program_.enableAttributeArray("a_color"); shader_program_.setAttributeBuffer( "a_color", GL_FLOAT, 3 * sizeof(GLfloat), 4, sizeof(PointPainter::Data)); // Make sure they are not changed from the outside vbo_.release(); vao_.release(); #if DEBUG glDebugLog(); #endif } void TrianglePainter::Render(const QMatrix4x4& pmv_matrix) { if (num_geoms_ == 0) { return; } shader_program_.bind(); vao_.bind(); shader_program_.setUniformValue("u_pmv_matrix", pmv_matrix); QOpenGLFunctions* gl_funcs = QOpenGLContext::currentContext()->functions(); gl_funcs->glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(3 * num_geoms_)); // Make sure the VAO is not changed from the outside vao_.release(); #if DEBUG glDebugLog(); #endif } } // namespace colmap colmap-3.9.1/src/colmap/ui/triangle_painter.h000066400000000000000000000045321454702036400211670ustar00rootroot00000000000000// 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. #pragma once #include "colmap/ui/point_painter.h" #include #include namespace colmap { class TrianglePainter { public: TrianglePainter(); ~TrianglePainter(); struct Data { Data() {} Data(const PointPainter::Data& p1, const PointPainter::Data& p2, const PointPainter::Data& p3) : point1(p1), point2(p2), point3(p3) {} PointPainter::Data point1; PointPainter::Data point2; PointPainter::Data point3; }; void Setup(); void Upload(const std::vector& data); void Render(const QMatrix4x4& pmv_matrix); private: QOpenGLShaderProgram shader_program_; QOpenGLVertexArrayObject vao_; QOpenGLBuffer vbo_; size_t num_geoms_; }; } // namespace colmap colmap-3.9.1/src/colmap/ui/undistortion_widget.cc000066400000000000000000000116021454702036400220760ustar00rootroot00000000000000// 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. #include "colmap/ui/undistortion_widget.h" namespace colmap { UndistortionWidget::UndistortionWidget(QWidget* parent, const OptionManager* options) : OptionsWidget(parent), options_(options), reconstruction_(nullptr), thread_control_widget_(new ThreadControlWidget(this)) { setWindowFlags(Qt::Dialog); setWindowModality(Qt::ApplicationModal); setWindowTitle("Undistortion"); output_format_ = new QComboBox(this); output_format_->addItem("COLMAP"); output_format_->addItem("PMVS"); output_format_->addItem("CMP-MVS"); output_format_->setFont(font()); AddWidgetRow("format", output_format_); AddOptionDouble(&undistortion_options_.min_scale, "min_scale", 0); AddOptionDouble(&undistortion_options_.max_scale, "max_scale", 0); AddOptionInt(&undistortion_options_.max_image_size, "max_image_size", -1); AddOptionDouble(&undistortion_options_.blank_pixels, "blank_pixels", 0); AddOptionDouble(&undistortion_options_.roi_min_x, "roi_min_x", 0.0, 1.0); AddOptionDouble(&undistortion_options_.roi_min_y, "roi_min_y", 0.0, 1.0); AddOptionDouble(&undistortion_options_.roi_max_x, "roi_max_x", 0.0, 1.0); AddOptionDouble(&undistortion_options_.roi_max_y, "roi_max_y", 0.0, 1.0); AddOptionDirPath(&output_path_, "output_path"); AddSpacer(); QPushButton* undistort_button = new QPushButton(tr("Undistort"), this); connect(undistort_button, &QPushButton::released, this, &UndistortionWidget::Undistort); grid_layout_->addWidget(undistort_button, grid_layout_->rowCount(), 1); } void UndistortionWidget::Show( std::shared_ptr reconstruction) { reconstruction_ = std::move(reconstruction); show(); raise(); } bool UndistortionWidget::IsValid() const { return ExistsDir(output_path_); } void UndistortionWidget::Undistort() { CHECK_NOTNULL(reconstruction_); WriteOptions(); if (IsValid()) { std::unique_ptr undistorter; if (output_format_->currentIndex() == 0) { undistorter = std::make_unique(undistortion_options_, *reconstruction_, *options_->image_path, output_path_); } else if (output_format_->currentIndex() == 1) { undistorter = std::make_unique(undistortion_options_, *reconstruction_, *options_->image_path, output_path_); } else if (output_format_->currentIndex() == 2) { undistorter = std::make_unique(undistortion_options_, *reconstruction_, *options_->image_path, output_path_); } else { QMessageBox::critical(this, "", tr("Invalid output format")); return; } thread_control_widget_->StartThread( "Undistorting...", true, std::move(undistorter)); } else { QMessageBox::critical(this, "", tr("Invalid output path")); } } } // namespace colmap colmap-3.9.1/src/colmap/ui/undistortion_widget.h000066400000000000000000000046461454702036400217520ustar00rootroot00000000000000// 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. #pragma once #include "colmap/controllers/option_manager.h" #include "colmap/image/undistortion.h" #include "colmap/scene/reconstruction.h" #include "colmap/ui/options_widget.h" #include "colmap/ui/thread_control_widget.h" #include "colmap/util/misc.h" #include #include namespace colmap { class UndistortionWidget : public OptionsWidget { public: UndistortionWidget(QWidget* parent, const OptionManager* options); void Show(std::shared_ptr reconstruction); bool IsValid() const; private: void Undistort(); const OptionManager* options_; std::shared_ptr reconstruction_; ThreadControlWidget* thread_control_widget_; QComboBox* output_format_; UndistortCameraOptions undistortion_options_; std::string output_path_; }; } // namespace colmap colmap-3.9.1/src/colmap/util/000077500000000000000000000000001454702036400160235ustar00rootroot00000000000000colmap-3.9.1/src/colmap/util/CMakeLists.txt000066400000000000000000000064561454702036400205760ustar00rootroot00000000000000# 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. set(FOLDER_NAME "util") COLMAP_ADD_LIBRARY( NAME colmap_util SRCS cache.h eigen_alignment.h logging.h logging.cc misc.h misc.cc opengl_utils.h opengl_utils.cc ply.h ply.cc sqlite3_utils.h string.h string.cc threading.h threading.cc timer.h timer.cc types.h version.h version.cc PUBLIC_LINK_LIBS Boost::filesystem Eigen3::Eigen glog::glog SQLite::SQLite3 PRIVATE_LINK_LIBS Boost::boost ) if(TESTS_ENABLED) target_sources( colmap_util PRIVATE testing.h testing.cc ) endif() if(GUI_ENABLED) target_link_libraries(colmap_util PUBLIC Qt5::Core Qt5::OpenGL OpenGL::GL) endif() if(CUDA_ENABLED) COLMAP_ADD_LIBRARY( NAME colmap_util_cuda SRCS cuda.h cuda.cc cudacc.h cudacc.cc PUBLIC_LINK_LIBS colmap_util CUDA::cudart ) endif() COLMAP_ADD_TEST( NAME cache_test SRCS cache_test.cc LINK_LIBS colmap_util ) COLMAP_ADD_TEST( NAME endian_test SRCS endian_test.cc LINK_LIBS colmap_util ) COLMAP_ADD_TEST( NAME misc_test SRCS misc_test.cc LINK_LIBS colmap_util ) COLMAP_ADD_TEST( NAME string_test SRCS string_test.cc LINK_LIBS colmap_util ) COLMAP_ADD_TEST( NAME threading_test SRCS threading_test.cc LINK_LIBS colmap_util ) COLMAP_ADD_TEST( NAME timer_test SRCS timer_test.cc LINK_LIBS colmap_util ) COLMAP_ADD_TEST( NAME types_test SRCS types_test.cc LINK_LIBS colmap_util ) if(GUI_ENABLED) COLMAP_ADD_TEST( NAME opengl_utils_test SRCS opengl_utils_test.cc LINK_LIBS colmap_util ) endif() colmap-3.9.1/src/colmap/util/cache.h000066400000000000000000000213561454702036400172460ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/logging.h" #include #include #include #include #include namespace colmap { // Least Recently Used cache implementation. Whenever the cache size is // exceeded, the least recently used (by Get and GetMutable) is deleted. template class LRUCache { public: LRUCache(size_t max_num_elems, const std::function& getter_func); virtual ~LRUCache() = default; // The number of elements in the cache. size_t NumElems() const; size_t MaxNumElems() const; // Check whether the element with the given key exists. bool Exists(const key_t& key) const; // Get the value of an element either from the cache or compute the new value. const value_t& Get(const key_t& key); value_t& GetMutable(const key_t& key); // Manually set the value of an element. virtual void Set(const key_t& key, value_t value); // Pop least recently used element from cache. virtual void Pop(); // Clear all elements from cache. virtual void Clear(); protected: typedef typename std::pair key_value_pair_t; typedef typename std::list::iterator list_iterator_t; // Maximum number of least-recently-used elements the cache remembers. const size_t max_num_elems_; // List to keep track of the least-recently-used elements. std::list elems_list_; // Mapping from key to location in the list. std::unordered_map elems_map_; // Function to compute new values if not in the cache. const std::function getter_func_; }; // Least Recently Used cache implementation that is constrained by a maximum // memory limitation of its elements. Whenever the memory limit is exceeded, the // least recently used (by Get and GetMutable) is deleted. Each element must // implement a `size_t NumBytes()` method that returns its size in memory. template class MemoryConstrainedLRUCache : public LRUCache { public: MemoryConstrainedLRUCache( size_t max_num_bytes, const std::function& getter_func); size_t NumBytes() const; size_t MaxNumBytes() const; void UpdateNumBytes(const key_t& key); void Set(const key_t& key, value_t value) override; void Pop() override; void Clear() override; private: using typename LRUCache::key_value_pair_t; using typename LRUCache::list_iterator_t; using LRUCache::max_num_elems_; using LRUCache::elems_list_; using LRUCache::elems_map_; using LRUCache::getter_func_; const size_t max_num_bytes_; size_t num_bytes_; std::unordered_map elems_num_bytes_; }; //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template LRUCache::LRUCache( const size_t max_num_elems, const std::function& getter_func) : max_num_elems_(max_num_elems), getter_func_(getter_func) { CHECK(getter_func); CHECK_GT(max_num_elems, 0); } template size_t LRUCache::NumElems() const { return elems_map_.size(); } template size_t LRUCache::MaxNumElems() const { return max_num_elems_; } template bool LRUCache::Exists(const key_t& key) const { return elems_map_.find(key) != elems_map_.end(); } template const value_t& LRUCache::Get(const key_t& key) { return GetMutable(key); } template value_t& LRUCache::GetMutable(const key_t& key) { const auto it = elems_map_.find(key); if (it == elems_map_.end()) { Set(key, std::move(getter_func_(key))); return elems_map_[key]->second; } else { elems_list_.splice(elems_list_.begin(), elems_list_, it->second); return it->second->second; } } template void LRUCache::Set(const key_t& key, value_t value) { auto it = elems_map_.find(key); elems_list_.emplace_front(key, std::move(value)); if (it != elems_map_.end()) { elems_list_.erase(it->second); elems_map_.erase(it); } elems_map_[key] = elems_list_.begin(); if (elems_map_.size() > max_num_elems_) { Pop(); } } template void LRUCache::Pop() { if (!elems_list_.empty()) { auto last = elems_list_.end(); --last; elems_map_.erase(last->first); elems_list_.pop_back(); } } template void LRUCache::Clear() { elems_list_.clear(); elems_map_.clear(); } template MemoryConstrainedLRUCache::MemoryConstrainedLRUCache( const size_t max_num_bytes, const std::function& getter_func) : LRUCache(std::numeric_limits::max(), getter_func), max_num_bytes_(max_num_bytes), num_bytes_(0) { CHECK_GT(max_num_bytes, 0); } template size_t MemoryConstrainedLRUCache::NumBytes() const { return num_bytes_; } template size_t MemoryConstrainedLRUCache::MaxNumBytes() const { return max_num_bytes_; } template void MemoryConstrainedLRUCache::Set(const key_t& key, value_t value) { const size_t num_bytes = value.NumBytes(); auto it = elems_map_.find(key); elems_list_.emplace_front(key, std::move(value)); if (it != elems_map_.end()) { elems_list_.erase(it->second); elems_map_.erase(it); } elems_map_[key] = elems_list_.begin(); num_bytes_ += num_bytes; elems_num_bytes_.emplace(key, num_bytes); while (num_bytes_ > max_num_bytes_ && elems_map_.size() > 1) { Pop(); } } template void MemoryConstrainedLRUCache::Pop() { if (!elems_list_.empty()) { auto last = elems_list_.end(); --last; num_bytes_ -= elems_num_bytes_.at(last->first); CHECK_GE(num_bytes_, 0); elems_num_bytes_.erase(last->first); elems_map_.erase(last->first); elems_list_.pop_back(); } } template void MemoryConstrainedLRUCache::UpdateNumBytes( const key_t& key) { auto& num_bytes = elems_num_bytes_.at(key); num_bytes_ -= num_bytes; CHECK_GE(num_bytes_, 0); num_bytes = LRUCache::Get(key).NumBytes(); num_bytes_ += num_bytes; while (num_bytes_ > max_num_bytes_ && elems_map_.size() > 1) { Pop(); } } template void MemoryConstrainedLRUCache::Clear() { LRUCache::Clear(); num_bytes_ = 0; elems_num_bytes_.clear(); } } // namespace colmap colmap-3.9.1/src/colmap/util/cache_test.cc000066400000000000000000000201211454702036400204300ustar00rootroot00000000000000// 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. #include "colmap/util/cache.h" #include namespace colmap { namespace { TEST(LRUCache, Empty) { LRUCache cache(5, [](const int key) { return key; }); EXPECT_EQ(cache.NumElems(), 0); EXPECT_EQ(cache.MaxNumElems(), 5); } TEST(LRUCache, Get) { LRUCache cache(5, [](const int key) { return key; }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { EXPECT_EQ(cache.Get(i), i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } EXPECT_EQ(cache.Get(5), 5); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); EXPECT_EQ(cache.Get(5), 5); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); EXPECT_EQ(cache.Get(6), 6); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_FALSE(cache.Exists(1)); EXPECT_TRUE(cache.Exists(6)); } TEST(LRUCache, GetMutable) { LRUCache cache(5, [](const int key) { return key; }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { EXPECT_EQ(cache.GetMutable(i), i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } EXPECT_EQ(cache.GetMutable(5), 5); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); EXPECT_EQ(cache.GetMutable(5), 5); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); EXPECT_EQ(cache.GetMutable(6), 6); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_FALSE(cache.Exists(1)); EXPECT_TRUE(cache.Exists(6)); cache.GetMutable(6) = 66; EXPECT_EQ(cache.GetMutable(6), 66); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_FALSE(cache.Exists(1)); EXPECT_TRUE(cache.Exists(6)); } TEST(LRUCache, Set) { LRUCache cache(5, [](const int key) { return -1; }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { cache.Set(i, i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } EXPECT_EQ(cache.Get(5), -1); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); EXPECT_EQ(cache.Get(6), -1); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_FALSE(cache.Exists(1)); EXPECT_TRUE(cache.Exists(6)); } TEST(LRUCache, Pop) { LRUCache cache(5, [](const int key) { return key; }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { EXPECT_EQ(cache.Get(i), i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } EXPECT_EQ(cache.Get(5), 5); EXPECT_EQ(cache.NumElems(), 5); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); cache.Pop(); EXPECT_EQ(cache.NumElems(), 4); cache.Pop(); EXPECT_EQ(cache.NumElems(), 3); cache.Pop(); EXPECT_EQ(cache.NumElems(), 2); cache.Pop(); EXPECT_EQ(cache.NumElems(), 1); cache.Pop(); EXPECT_EQ(cache.NumElems(), 0); cache.Pop(); EXPECT_EQ(cache.NumElems(), 0); } TEST(LRUCache, Clear) { LRUCache cache(5, [](const int key) { return key; }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { EXPECT_EQ(cache.Get(i), i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } cache.Clear(); EXPECT_EQ(cache.NumElems(), 0); EXPECT_EQ(cache.Get(0), 0); EXPECT_EQ(cache.NumElems(), 1); EXPECT_TRUE(cache.Exists(0)); } struct SizedElem { explicit SizedElem(const size_t num_bytes_) : num_bytes(num_bytes_) {} size_t NumBytes() const { return num_bytes; } size_t num_bytes; }; TEST(MemoryConstrainedLRUCache, Empty) { MemoryConstrainedLRUCache cache( 5, [](const int key) { return SizedElem(key); }); EXPECT_EQ(cache.NumElems(), 0); EXPECT_EQ(cache.MaxNumElems(), std::numeric_limits::max()); EXPECT_EQ(cache.NumBytes(), 0); EXPECT_EQ(cache.MaxNumBytes(), 5); } TEST(MemoryConstrainedLRUCache, Get) { MemoryConstrainedLRUCache cache( 10, [](const int key) { return SizedElem(key); }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { EXPECT_EQ(cache.Get(i).NumBytes(), i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } EXPECT_EQ(cache.Get(5).NumBytes(), 5); EXPECT_EQ(cache.NumElems(), 2); EXPECT_EQ(cache.NumBytes(), 9); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); EXPECT_EQ(cache.Get(5).NumBytes(), 5); EXPECT_EQ(cache.NumElems(), 2); EXPECT_EQ(cache.NumBytes(), 9); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(5)); EXPECT_EQ(cache.Get(6).NumBytes(), 6); EXPECT_EQ(cache.NumElems(), 1); EXPECT_EQ(cache.NumBytes(), 6); EXPECT_FALSE(cache.Exists(0)); EXPECT_FALSE(cache.Exists(1)); EXPECT_TRUE(cache.Exists(6)); EXPECT_EQ(cache.Get(1).NumBytes(), 1); EXPECT_EQ(cache.NumElems(), 2); EXPECT_EQ(cache.NumBytes(), 7); EXPECT_FALSE(cache.Exists(0)); EXPECT_TRUE(cache.Exists(1)); EXPECT_TRUE(cache.Exists(6)); } TEST(MemoryConstrainedLRUCache, Clear) { MemoryConstrainedLRUCache cache( 10, [](const int key) { return SizedElem(key); }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { EXPECT_EQ(cache.Get(i).NumBytes(), i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } cache.Clear(); EXPECT_EQ(cache.NumElems(), 0); EXPECT_EQ(cache.NumBytes(), 0); EXPECT_EQ(cache.Get(1).NumBytes(), 1); EXPECT_EQ(cache.NumBytes(), 1); EXPECT_EQ(cache.NumElems(), 1); EXPECT_TRUE(cache.Exists(1)); } TEST(MemoryConstrainedLRUCache, UpdateNumBytes) { MemoryConstrainedLRUCache cache( 50, [](const int key) { return SizedElem(key); }); EXPECT_EQ(cache.NumElems(), 0); for (int i = 0; i < 5; ++i) { EXPECT_EQ(cache.Get(i).NumBytes(), i); EXPECT_EQ(cache.NumElems(), i + 1); EXPECT_TRUE(cache.Exists(i)); } EXPECT_EQ(cache.NumBytes(), 10); cache.GetMutable(4).num_bytes = 3; EXPECT_EQ(cache.NumBytes(), 10); cache.UpdateNumBytes(4); EXPECT_EQ(cache.NumBytes(), 9); cache.GetMutable(2).num_bytes = 3; EXPECT_EQ(cache.NumBytes(), 9); cache.UpdateNumBytes(2); EXPECT_EQ(cache.NumBytes(), 10); cache.GetMutable(0).num_bytes = 40; EXPECT_EQ(cache.NumBytes(), 10); cache.UpdateNumBytes(0); EXPECT_EQ(cache.NumBytes(), 50); cache.Clear(); EXPECT_EQ(cache.NumBytes(), 0); EXPECT_EQ(cache.Get(2).NumBytes(), 2); EXPECT_EQ(cache.NumBytes(), 2); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/cuda.cc000066400000000000000000000063721454702036400172560ustar00rootroot00000000000000// 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. #include "colmap/util/cuda.h" #include "colmap/util/cudacc.h" #include "colmap/util/logging.h" #include #include #include #include namespace colmap { namespace { // Check whether the first Cuda device is better than the second. bool CompareCudaDevice(const cudaDeviceProp& d1, const cudaDeviceProp& d2) { bool result = (d1.major > d2.major) || ((d1.major == d2.major) && (d1.minor > d2.minor)) || ((d1.major == d2.major) && (d1.minor == d2.minor) && (d1.multiProcessorCount > d2.multiProcessorCount)); return result; } } // namespace int GetNumCudaDevices() { int num_cuda_devices; CUDA_SAFE_CALL(cudaGetDeviceCount(&num_cuda_devices)); return num_cuda_devices; } void SetBestCudaDevice(const int gpu_index) { const int num_cuda_devices = GetNumCudaDevices(); CHECK_GT(num_cuda_devices, 0) << "No CUDA devices available"; int selected_gpu_index = -1; if (gpu_index >= 0) { selected_gpu_index = gpu_index; } else { std::vector all_devices(num_cuda_devices); for (int device_id = 0; device_id < num_cuda_devices; ++device_id) { cudaGetDeviceProperties(&all_devices[device_id], device_id); } std::sort(all_devices.begin(), all_devices.end(), CompareCudaDevice); CUDA_SAFE_CALL(cudaChooseDevice(&selected_gpu_index, all_devices.data())); } CHECK_GE(selected_gpu_index, 0); CHECK_LT(selected_gpu_index, num_cuda_devices) << "Invalid CUDA GPU selected"; cudaDeviceProp device; cudaGetDeviceProperties(&device, selected_gpu_index); CUDA_SAFE_CALL(cudaSetDevice(selected_gpu_index)); } } // namespace colmap colmap-3.9.1/src/colmap/util/cuda.h000066400000000000000000000033331454702036400171120ustar00rootroot00000000000000// 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. #pragma once namespace colmap { int GetNumCudaDevices(); void SetBestCudaDevice(int gpu_index); } // namespace colmap colmap-3.9.1/src/colmap/util/cudacc.cc000066400000000000000000000067451454702036400175700ustar00rootroot00000000000000// 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. #include "colmap/util/cudacc.h" #include "colmap/util/logging.h" namespace colmap { CudaTimer::CudaTimer() { CUDA_SAFE_CALL(cudaEventCreate(&start_)); CUDA_SAFE_CALL(cudaEventCreate(&stop_)); CUDA_SAFE_CALL(cudaEventRecord(start_, 0)); } CudaTimer::~CudaTimer() { CUDA_SAFE_CALL(cudaEventDestroy(start_)); CUDA_SAFE_CALL(cudaEventDestroy(stop_)); } void CudaTimer::Print(const std::string& message) { CUDA_SAFE_CALL(cudaEventRecord(stop_, 0)); CUDA_SAFE_CALL(cudaEventSynchronize(stop_)); CUDA_SAFE_CALL(cudaEventElapsedTime(&elapsed_time_, start_, stop_)); LOG(INFO) << StringPrintf( "%s: %.4fs", message.c_str(), elapsed_time_ / 1000.0f); } void CudaSafeCall(const cudaError_t error, const std::string& file, const int line) { if (error != cudaSuccess) { LOG(ERROR) << StringPrintf("CUDA error at %s:%i - %s", file.c_str(), line, cudaGetErrorString(error)); exit(EXIT_FAILURE); } } void CudaCheck(const char* file, const int line) { const cudaError error = cudaGetLastError(); while (error != cudaSuccess) { LOG(ERROR) << StringPrintf( "CUDA error at %s:%i - %s", file, line, cudaGetErrorString(error)); exit(EXIT_FAILURE); } } void CudaSyncAndCheck(const char* file, const int line) { // Synchronizes the default stream which is a nullptr. const cudaError error = cudaStreamSynchronize(nullptr); if (cudaSuccess != error) { LOG(ERROR) << StringPrintf( "CUDA error at %s:%i - %s", file, line, cudaGetErrorString(error)); LOG(ERROR) << "This error is likely caused by the graphics card timeout " "detection mechanism of your operating system. Please refer to " "the FAQ in the documentation on how to solve this problem."; exit(EXIT_FAILURE); } } } // namespace colmap colmap-3.9.1/src/colmap/util/cudacc.h000066400000000000000000000044401454702036400174200ustar00rootroot00000000000000// 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. #pragma once #include #include #define CUDA_SAFE_CALL(error) CudaSafeCall(error, __FILE__, __LINE__) #define CUDA_CHECK() CudaCheck(__FILE__, __LINE__) #define CUDA_SYNC_AND_CHECK() CudaSyncAndCheck(__FILE__, __LINE__) namespace colmap { class CudaTimer { public: CudaTimer(); ~CudaTimer(); void Print(const std::string& message); private: cudaEvent_t start_; cudaEvent_t stop_; float elapsed_time_; }; void CudaSafeCall(const cudaError_t error, const std::string& file, const int line); void CudaCheck(const char* file, const int line); void CudaSyncAndCheck(const char* file, const int line); } // namespace colmap colmap-3.9.1/src/colmap/util/eigen_alignment.h000066400000000000000000000126741454702036400213330ustar00rootroot00000000000000// 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. #pragma once #include #ifdef _MSVC_LANG #define CPP_VERSION _MSVC_LANG #else #define CPP_VERSION __cplusplus #endif #if !EIGEN_VERSION_AT_LEAST(3, 4, 0) || CPP_VERSION < 201703L #include #include #include #include #include #ifndef EIGEN_ALIGNED_ALLOCATOR #define EIGEN_ALIGNED_ALLOCATOR Eigen::aligned_allocator #endif // Equivalent to EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION but with support for // initializer lists, which is a C++11 feature and not supported by the Eigen. // The initializer list extension is inspired by Theia and StackOverflow code. #define EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(...) \ namespace std { \ template <> \ class vector<__VA_ARGS__, std::allocator<__VA_ARGS__>> \ : public vector<__VA_ARGS__, EIGEN_ALIGNED_ALLOCATOR<__VA_ARGS__>> { \ typedef vector<__VA_ARGS__, EIGEN_ALIGNED_ALLOCATOR<__VA_ARGS__>> \ vector_base; \ \ public: \ typedef __VA_ARGS__ value_type; \ typedef vector_base::allocator_type allocator_type; \ typedef vector_base::size_type size_type; \ typedef vector_base::iterator iterator; \ explicit vector(const allocator_type& a = allocator_type()) \ : vector_base(a) {} \ template \ vector(InputIterator first, \ InputIterator last, \ const allocator_type& a = allocator_type()) \ : vector_base(first, last, a) {} \ vector(const vector& c) : vector_base(c) {} \ explicit vector(size_type num, const value_type& val = value_type()) \ : vector_base(num, val) {} \ vector(iterator start, iterator end) : vector_base(start, end) {} \ vector& operator=(const vector& x) { \ vector_base::operator=(x); \ return *this; \ } \ vector(initializer_list<__VA_ARGS__> list) \ : vector_base(list.begin(), list.end()) {} \ }; \ } // namespace std EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Vector2d) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Vector4d) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Vector4f) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Matrix2d) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Matrix2f) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Matrix4d) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Matrix4f) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Affine3d) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Affine3f) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Quaterniond) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Quaternionf) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Matrix) EIGEN_DEFINE_STL_VECTOR_SPECIALIZATION_CUSTOM(Eigen::Matrix) #endif #undef CPP_VERSION colmap-3.9.1/src/colmap/util/endian.h000066400000000000000000000114051454702036400174330ustar00rootroot00000000000000// 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. #pragma once #include #include #include namespace colmap { // Reverse the order of each byte. template T ReverseBytes(const T& data); // Check the order in which bytes are stored in computer memory. bool IsLittleEndian(); bool IsBigEndian(); // Convert data between endianness and the native format. Note that, for float // and double types, these functions are only valid if the format is IEEE-754. // This is the case for pretty much most processors. template T LittleEndianToNative(T x); template T BigEndianToNative(T x); template T NativeToLittleEndian(T x); template T NativeToBigEndian(T x); // Read data in little endian format for cross-platform support. template T ReadBinaryLittleEndian(std::istream* stream); template void ReadBinaryLittleEndian(std::istream* stream, std::vector* data); // Write data in little endian format for cross-platform support. template void WriteBinaryLittleEndian(std::ostream* stream, const T& data); template void WriteBinaryLittleEndian(std::ostream* stream, const std::vector& data); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template T ReverseBytes(const T& data) { T data_reversed = data; std::reverse(reinterpret_cast(&data_reversed), reinterpret_cast(&data_reversed) + sizeof(T)); return data_reversed; } inline bool IsLittleEndian() { #ifdef BOOST_BIG_ENDIAN return false; #else return true; #endif } inline bool IsBigEndian() { #ifdef BOOST_BIG_ENDIAN return true; #else return false; #endif } template T LittleEndianToNative(const T x) { if (IsLittleEndian()) { return x; } else { return ReverseBytes(x); } } template T BigEndianToNative(const T x) { if (IsBigEndian()) { return x; } else { return ReverseBytes(x); } } template T NativeToLittleEndian(const T x) { if (IsLittleEndian()) { return x; } else { return ReverseBytes(x); } } template T NativeToBigEndian(const T x) { if (IsBigEndian()) { return x; } else { return ReverseBytes(x); } } template T ReadBinaryLittleEndian(std::istream* stream) { T data_little_endian; stream->read(reinterpret_cast(&data_little_endian), sizeof(T)); return LittleEndianToNative(data_little_endian); } template void ReadBinaryLittleEndian(std::istream* stream, std::vector* data) { for (size_t i = 0; i < data->size(); ++i) { (*data)[i] = ReadBinaryLittleEndian(stream); } } template void WriteBinaryLittleEndian(std::ostream* stream, const T& data) { const T data_little_endian = NativeToLittleEndian(data); stream->write(reinterpret_cast(&data_little_endian), sizeof(T)); } template void WriteBinaryLittleEndian(std::ostream* stream, const std::vector& data) { for (const auto& elem : data) { WriteBinaryLittleEndian(stream, elem); } } } // namespace colmap colmap-3.9.1/src/colmap/util/endian_test.cc000066400000000000000000000206041454702036400206310ustar00rootroot00000000000000// 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. #include "colmap/util/endian.h" #include #include namespace colmap { namespace { TEST(ReverseBytes, Nominal) { for (size_t i = 0; i < 256; ++i) { EXPECT_EQ(ReverseBytes(i), static_cast(i)); EXPECT_EQ(ReverseBytes(i), static_cast(i)); } EXPECT_EQ(ReverseBytes(0), 0); EXPECT_EQ(ReverseBytes(1), 256); EXPECT_EQ(ReverseBytes(2), 512); EXPECT_EQ(ReverseBytes(3), 768); EXPECT_EQ(ReverseBytes(256), 1); EXPECT_EQ(ReverseBytes(512), 2); EXPECT_EQ(ReverseBytes(768), 3); EXPECT_EQ(ReverseBytes(0), 0); EXPECT_EQ(ReverseBytes(1), 256); EXPECT_EQ(ReverseBytes(2), 512); EXPECT_EQ(ReverseBytes(3), 768); EXPECT_EQ(ReverseBytes(256), 1); EXPECT_EQ(ReverseBytes(512), 2); EXPECT_EQ(ReverseBytes(768), 3); EXPECT_EQ(ReverseBytes(0), 0); EXPECT_EQ(ReverseBytes(1), 16777216); EXPECT_EQ(ReverseBytes(2), 33554432); EXPECT_EQ(ReverseBytes(3), 50331648); EXPECT_EQ(ReverseBytes(16777216), 1); EXPECT_EQ(ReverseBytes(33554432), 2); EXPECT_EQ(ReverseBytes(50331648), 3); EXPECT_EQ(ReverseBytes(0), 0); EXPECT_EQ(ReverseBytes(1), 16777216); EXPECT_EQ(ReverseBytes(2), 33554432); EXPECT_EQ(ReverseBytes(3), 50331648); EXPECT_EQ(ReverseBytes(16777216), 1); EXPECT_EQ(ReverseBytes(33554432), 2); EXPECT_EQ(ReverseBytes(50331648), 3); EXPECT_EQ(ReverseBytes(0), 0); EXPECT_EQ(ReverseBytes(1), 72057594037927936); EXPECT_EQ(ReverseBytes(2), 144115188075855872); EXPECT_EQ(ReverseBytes(3), 216172782113783808); EXPECT_EQ(ReverseBytes(72057594037927936), 1); EXPECT_EQ(ReverseBytes(144115188075855872), 2); EXPECT_EQ(ReverseBytes(216172782113783808), 3); EXPECT_EQ(ReverseBytes(0), 0); EXPECT_EQ(ReverseBytes(1), 72057594037927936); EXPECT_EQ(ReverseBytes(2), 144115188075855872); EXPECT_EQ(ReverseBytes(3), 216172782113783808); EXPECT_EQ(ReverseBytes(72057594037927936), 1); EXPECT_EQ(ReverseBytes(144115188075855872), 2); EXPECT_EQ(ReverseBytes(216172782113783808), 3); } TEST(IsLittleBigEndian, Nominal) { EXPECT_NE(IsLittleEndian(), IsBigEndian()); } template void TestIntNativeToLitteBigEndian() { std::default_random_engine prng; std::uniform_int_distribution distribution( std::numeric_limits::lowest(), std::numeric_limits::max()); constexpr int kNumTrials = 100; for (int i = 0; i < kNumTrials; ++i) { const T x = distribution(prng); EXPECT_EQ(LittleEndianToNative(NativeToLittleEndian(x)), x); EXPECT_EQ(BigEndianToNative(NativeToBigEndian(x)), x); } } template void TestRealNativeToLitteBigEndian() { std::default_random_engine prng; std::uniform_real_distribution distribution( std::numeric_limits::lowest(), std::numeric_limits::max()); constexpr int kNumTrials = 100; for (int i = 0; i < kNumTrials; ++i) { const T x = distribution(prng); EXPECT_EQ(LittleEndianToNative(NativeToLittleEndian(x)), x); EXPECT_EQ(BigEndianToNative(NativeToBigEndian(x)), x); EXPECT_EQ(NativeToLittleEndian(LittleEndianToNative(x)), x); EXPECT_EQ(NativeToBigEndian(BigEndianToNative(x)), x); } } TEST(NativeToLitteBigEndian, Nominal) { #ifndef _MSC_VER // There is no random number generator in MSVC for char's. TestIntNativeToLitteBigEndian(); #endif TestIntNativeToLitteBigEndian(); TestIntNativeToLitteBigEndian(); TestIntNativeToLitteBigEndian(); #ifndef _MSC_VER // There is no random number generator in MSVC for char's. TestIntNativeToLitteBigEndian(); #endif TestIntNativeToLitteBigEndian(); TestIntNativeToLitteBigEndian(); TestIntNativeToLitteBigEndian(); TestRealNativeToLitteBigEndian(); TestRealNativeToLitteBigEndian(); } template void TestIntReadWriteBinaryLittleEndian() { std::default_random_engine prng; std::uniform_int_distribution distribution( std::numeric_limits::lowest(), std::numeric_limits::max()); constexpr int kNumTrials = 100; for (int i = 0; i < kNumTrials; ++i) { std::stringstream file; const T orig_value = distribution(prng); WriteBinaryLittleEndian(&file, orig_value); const T read_value = ReadBinaryLittleEndian(&file); EXPECT_EQ(orig_value, read_value); std::stringstream file_vector; std::vector orig_vector(100); std::generate(orig_vector.begin(), orig_vector.end(), [&]() { return distribution(prng); }); WriteBinaryLittleEndian(&file_vector, orig_vector); std::vector read_vector(orig_vector.size()); ReadBinaryLittleEndian(&file_vector, &read_vector); for (size_t i = 0; i < orig_vector.size(); ++i) { EXPECT_EQ(orig_vector[i], read_vector[i]); } } } template void TestFloatReadWriteBinaryLittleEndian() { std::default_random_engine prng; std::uniform_real_distribution distribution( std::numeric_limits::lowest(), std::numeric_limits::max()); constexpr int kNumTrials = 100; for (int i = 0; i < kNumTrials; ++i) { std::stringstream file; const T orig_value = distribution(prng); WriteBinaryLittleEndian(&file, orig_value); const T read_value = ReadBinaryLittleEndian(&file); EXPECT_EQ(orig_value, read_value); std::stringstream file_vector; std::vector orig_vector(100); std::generate(orig_vector.begin(), orig_vector.end(), [&]() { return distribution(prng); }); WriteBinaryLittleEndian(&file_vector, orig_vector); std::vector read_vector(orig_vector.size()); ReadBinaryLittleEndian(&file_vector, &read_vector); for (size_t i = 0; i < orig_vector.size(); ++i) { EXPECT_EQ(orig_vector[i], read_vector[i]); } } } TEST(ReadWriteBinaryLittleEndian, Nominal) { #ifndef _MSC_VER // There is no random number generator in MSVC for char's. TestIntReadWriteBinaryLittleEndian(); #endif TestIntReadWriteBinaryLittleEndian(); TestIntReadWriteBinaryLittleEndian(); TestIntReadWriteBinaryLittleEndian(); #ifndef _MSC_VER // There is no random number generator in MSVC for char's. TestIntReadWriteBinaryLittleEndian(); #endif TestIntReadWriteBinaryLittleEndian(); TestIntReadWriteBinaryLittleEndian(); TestIntReadWriteBinaryLittleEndian(); TestFloatReadWriteBinaryLittleEndian(); TestFloatReadWriteBinaryLittleEndian(); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/logging.cc000066400000000000000000000046731454702036400177720ustar00rootroot00000000000000// 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. #include "colmap/util/logging.h" namespace colmap { void InitializeGlog(char** argv) { #ifndef _MSC_VER // Broken in MSVC google::InstallFailureSignalHandler(); #endif google::InitGoogleLogging(argv[0]); } const char* __GetConstFileBaseName(const char* file) { const char* base = strrchr(file, '/'); if (!base) { base = strrchr(file, '\\'); } return base ? (base + 1) : file; } bool __CheckOptionImpl(const char* file, const int line, const bool result, const char* expr_str) { if (result) { return true; } else { LOG(ERROR) << StringPrintf("[%s:%d] Check failed: %s", __GetConstFileBaseName(file), line, expr_str); return false; } } } // namespace colmap colmap-3.9.1/src/colmap/util/logging.h000066400000000000000000000105571454702036400176320ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/string.h" #include #include // Option checker macros. In contrast to glog, this function does not abort the // program, but simply returns false on failure. #define CHECK_OPTION_IMPL(expr) \ __CheckOptionImpl(__FILE__, __LINE__, (expr), #expr) #define CHECK_OPTION(expr) \ if (!__CheckOptionImpl(__FILE__, __LINE__, (expr), #expr)) { \ return false; \ } #define CHECK_OPTION_OP(name, op, val1, val2) \ if (!__CheckOptionOpImpl(__FILE__, \ __LINE__, \ (val1 op val2), \ val1, \ val2, \ #val1, \ #val2, \ #op)) { \ return false; \ } #define CHECK_OPTION_EQ(val1, val2) CHECK_OPTION_OP(_EQ, ==, val1, val2) #define CHECK_OPTION_NE(val1, val2) CHECK_OPTION_OP(_NE, !=, val1, val2) #define CHECK_OPTION_LE(val1, val2) CHECK_OPTION_OP(_LE, <=, val1, val2) #define CHECK_OPTION_LT(val1, val2) CHECK_OPTION_OP(_LT, <, val1, val2) #define CHECK_OPTION_GE(val1, val2) CHECK_OPTION_OP(_GE, >=, val1, val2) #define CHECK_OPTION_GT(val1, val2) CHECK_OPTION_OP(_GT, >, val1, val2) namespace colmap { // Initialize glog at the beginning of the program. void InitializeGlog(char** argv); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// const char* __GetConstFileBaseName(const char* file); bool __CheckOptionImpl(const char* file, int line, bool result, const char* expr_str); template bool __CheckOptionOpImpl(const char* file, const int line, const bool result, const T1& val1, const T2& val2, const char* val1_str, const char* val2_str, const char* op_str) { if (result) { return true; } else { LOG(ERROR) << StringPrintf("[%s:%d] Check failed: %s %s %s (%s vs. %s)", __GetConstFileBaseName(file), line, val1_str, op_str, val2_str, std::to_string(val1).c_str(), std::to_string(val2).c_str()); return false; } } } // namespace colmap colmap-3.9.1/src/colmap/util/misc.cc000066400000000000000000000210511454702036400172640ustar00rootroot00000000000000// 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. #include "colmap/util/misc.h" #include namespace colmap { std::string EnsureTrailingSlash(const std::string& str) { if (str.length() > 0) { if (str.back() != '/') { return str + "/"; } } else { return str + "/"; } return str; } bool HasFileExtension(const std::string& file_name, const std::string& ext) { CHECK(!ext.empty()); CHECK_EQ(ext.at(0), '.'); std::string ext_lower = ext; StringToLower(&ext_lower); if (file_name.size() >= ext_lower.size() && file_name.substr(file_name.size() - ext_lower.size(), ext_lower.size()) == ext_lower) { return true; } return false; } void SplitFileExtension(const std::string& path, std::string* root, std::string* ext) { const auto parts = StringSplit(path, "."); CHECK_GT(parts.size(), 0); if (parts.size() == 1) { *root = parts[0]; *ext = ""; } else { *root = ""; for (size_t i = 0; i < parts.size() - 1; ++i) { *root += parts[i] + "."; } *root = root->substr(0, root->length() - 1); if (parts.back() == "") { *ext = ""; } else { *ext = "." + parts.back(); } } } void FileCopy(const std::string& src_path, const std::string& dst_path, CopyType type) { switch (type) { case CopyType::COPY: boost::filesystem::copy_file(src_path, dst_path); break; case CopyType::HARD_LINK: boost::filesystem::create_hard_link(src_path, dst_path); break; case CopyType::SOFT_LINK: boost::filesystem::create_symlink(src_path, dst_path); break; } } bool ExistsFile(const std::string& path) { return boost::filesystem::is_regular_file(path); } bool ExistsDir(const std::string& path) { return boost::filesystem::is_directory(path); } bool ExistsPath(const std::string& path) { return boost::filesystem::exists(path); } void CreateDirIfNotExists(const std::string& path, bool recursive) { if (ExistsDir(path)) { return; } if (recursive) { CHECK(boost::filesystem::create_directories(path)); } else { CHECK(boost::filesystem::create_directory(path)); } } std::string GetPathBaseName(const std::string& path) { const std::vector names = StringSplit(StringReplace(path, "\\", "/"), "/"); if (names.size() > 1 && names.back() == "") { return names[names.size() - 2]; } else { return names.back(); } } std::string GetParentDir(const std::string& path) { return boost::filesystem::path(path).parent_path().string(); } std::vector GetFileList(const std::string& path) { std::vector file_list; for (auto it = boost::filesystem::directory_iterator(path); it != boost::filesystem::directory_iterator(); ++it) { if (boost::filesystem::is_regular_file(*it)) { const boost::filesystem::path file_path = *it; file_list.push_back(file_path.string()); } } return file_list; } std::vector GetRecursiveFileList(const std::string& path) { std::vector file_list; for (auto it = boost::filesystem::recursive_directory_iterator(path); it != boost::filesystem::recursive_directory_iterator(); ++it) { if (boost::filesystem::is_regular_file(*it)) { const boost::filesystem::path file_path = *it; file_list.push_back(file_path.string()); } } return file_list; } std::vector GetDirList(const std::string& path) { std::vector dir_list; for (auto it = boost::filesystem::directory_iterator(path); it != boost::filesystem::directory_iterator(); ++it) { if (boost::filesystem::is_directory(*it)) { const boost::filesystem::path dir_path = *it; dir_list.push_back(dir_path.string()); } } return dir_list; } std::vector GetRecursiveDirList(const std::string& path) { std::vector dir_list; for (auto it = boost::filesystem::recursive_directory_iterator(path); it != boost::filesystem::recursive_directory_iterator(); ++it) { if (boost::filesystem::is_directory(*it)) { const boost::filesystem::path dir_path = *it; dir_list.push_back(dir_path.string()); } } return dir_list; } size_t GetFileSize(const std::string& path) { std::ifstream file(path, std::ifstream::ate | std::ifstream::binary); CHECK(file.is_open()) << path; return file.tellg(); } void PrintHeading1(const std::string& heading) { std::ostringstream log; log << "\n" << std::string(78, '=') << "\n"; log << heading << "\n"; log << std::string(78, '='); LOG(INFO) << log.str(); } void PrintHeading2(const std::string& heading) { std::ostringstream log; log << "\n" << heading << "\n"; log << std::string(std::min(heading.size(), 78), '-'); LOG(INFO) << log.str(); } template <> std::vector CSVToVector(const std::string& csv) { auto elems = StringSplit(csv, ",;"); std::vector values; values.reserve(elems.size()); for (auto& elem : elems) { StringTrim(&elem); if (elem.empty()) { continue; } values.push_back(elem); } return values; } template <> std::vector CSVToVector(const std::string& csv) { auto elems = StringSplit(csv, ",;"); std::vector values; values.reserve(elems.size()); for (auto& elem : elems) { StringTrim(&elem); if (elem.empty()) { continue; } try { values.push_back(std::stoi(elem)); } catch (const std::invalid_argument&) { return std::vector(0); } } return values; } template <> std::vector CSVToVector(const std::string& csv) { auto elems = StringSplit(csv, ",;"); std::vector values; values.reserve(elems.size()); for (auto& elem : elems) { StringTrim(&elem); if (elem.empty()) { continue; } try { values.push_back(std::stod(elem)); } catch (const std::invalid_argument&) { return std::vector(0); } } return values; } template <> std::vector CSVToVector(const std::string& csv) { auto elems = StringSplit(csv, ",;"); std::vector values; values.reserve(elems.size()); for (auto& elem : elems) { StringTrim(&elem); if (elem.empty()) { continue; } try { values.push_back(std::stold(elem)); } catch (const std::invalid_argument&) { return std::vector(0); } } return values; } std::vector ReadTextFileLines(const std::string& path) { std::ifstream file(path); CHECK(file.is_open()) << path; std::string line; std::vector lines; while (std::getline(file, line)) { StringTrim(&line); if (line.empty()) { continue; } lines.push_back(line); } return lines; } void RemoveCommandLineArgument(const std::string& arg, int* argc, char** argv) { for (int i = 0; i < *argc; ++i) { if (argv[i] == arg) { for (int j = i + 1; j < *argc; ++j) { argv[i] = argv[j]; } *argc -= 1; break; } } } } // namespace colmap colmap-3.9.1/src/colmap/util/misc.h000066400000000000000000000157531454702036400171420ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/endian.h" #include "colmap/util/logging.h" #include "colmap/util/string.h" #include #include #include #include #include namespace colmap { #ifndef STRINGIFY #define STRINGIFY(s) STRINGIFY_(s) #define STRINGIFY_(s) #s #endif // STRINGIFY enum class CopyType { COPY, HARD_LINK, SOFT_LINK }; // Append trailing slash to string if it does not yet end with a slash. std::string EnsureTrailingSlash(const std::string& str); // Check whether file name has the file extension (case insensitive). bool HasFileExtension(const std::string& file_name, const std::string& ext); // Split the path into its root and extension, for example, // "dir/file.jpg" into "dir/file" and ".jpg". void SplitFileExtension(const std::string& path, std::string* root, std::string* ext); // Copy or link file from source to destination path void FileCopy(const std::string& src_path, const std::string& dst_path, CopyType type = CopyType::COPY); // Check if the path points to an existing directory. bool ExistsFile(const std::string& path); // Check if the path points to an existing directory. bool ExistsDir(const std::string& path); // Check if the path points to an existing file or directory. bool ExistsPath(const std::string& path); // Create the directory if it does not exist. void CreateDirIfNotExists(const std::string& path, bool recursive = false); // Extract the base name of a path, e.g., "image.jpg" for "/dir/image.jpg". std::string GetPathBaseName(const std::string& path); // Get the path of the parent directory for the given path. std::string GetParentDir(const std::string& path); // Join multiple paths into one path. template std::string JoinPaths(T const&... paths); // Return list of files in directory. std::vector GetFileList(const std::string& path); // Return list of files, recursively in all sub-directories. std::vector GetRecursiveFileList(const std::string& path); // Return list of directories, recursively in all sub-directories. std::vector GetDirList(const std::string& path); // Return list of directories, recursively in all sub-directories. std::vector GetRecursiveDirList(const std::string& path); // Get the size in bytes of a file. size_t GetFileSize(const std::string& path); // Log first-order heading with over- and underscores. void PrintHeading1(const std::string& heading); // Log second-order heading with underscores. void PrintHeading2(const std::string& heading); // Check if vector contains elements. template bool VectorContainsValue(const std::vector& vector, T value); template bool VectorContainsDuplicateValues(const std::vector& vector); // Parse CSV line to a list of values. template std::vector CSVToVector(const std::string& csv); // Concatenate values in list to comma-separated list. template std::string VectorToCSV(const std::vector& values); // Read contiguous binary blob from file. template void ReadBinaryBlob(const std::string& path, std::vector* data); // Write contiguous binary blob to file. template void WriteBinaryBlob(const std::string& path, const std::vector& data); // Read each line of a text file into a separate element. Empty lines are // ignored and leading/trailing whitespace is removed. std::vector ReadTextFileLines(const std::string& path); // Remove an argument from the list of command-line arguments. void RemoveCommandLineArgument(const std::string& arg, int* argc, char** argv); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// template std::string JoinPaths(T const&... paths) { boost::filesystem::path result; int unpack[]{0, (result = result / boost::filesystem::path(paths), 0)...}; static_cast(unpack); return result.string(); } template bool VectorContainsValue(const std::vector& vector, const T value) { return std::find_if(vector.begin(), vector.end(), [value](const T element) { return element == value; }) != vector.end(); } template bool VectorContainsDuplicateValues(const std::vector& vector) { std::vector unique_vector = vector; return std::unique(unique_vector.begin(), unique_vector.end()) != unique_vector.end(); } template std::string VectorToCSV(const std::vector& values) { std::string string; for (const T value : values) { string += std::to_string(value) + ", "; } return string.substr(0, string.length() - 2); } template void ReadBinaryBlob(const std::string& path, std::vector* data) { std::ifstream file(path, std::ios::binary | std::ios::ate); CHECK(file.is_open()) << path; file.seekg(0, std::ios::end); const size_t num_bytes = file.tellg(); CHECK_EQ(num_bytes % sizeof(T), 0); data->resize(num_bytes / sizeof(T)); file.seekg(0, std::ios::beg); ReadBinaryLittleEndian(&file, data); } template void WriteBinaryBlob(const std::string& path, const std::vector& data) { std::ofstream file(path, std::ios::binary); CHECK(file.is_open()) << path; WriteBinaryLittleEndian(&file, data); } } // namespace colmap colmap-3.9.1/src/colmap/util/misc_test.cc000066400000000000000000000204211454702036400203230ustar00rootroot00000000000000// 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. #include "colmap/util/misc.h" #include namespace colmap { namespace { TEST(EnsureTrailingSlash, Nominal) { EXPECT_EQ(EnsureTrailingSlash(""), "/"); EXPECT_EQ(EnsureTrailingSlash("/"), "/"); EXPECT_EQ(EnsureTrailingSlash("////"), "////"); EXPECT_EQ(EnsureTrailingSlash("test"), "test/"); EXPECT_EQ(EnsureTrailingSlash("/test"), "/test/"); } TEST(HasFileExtension, Nominal) { EXPECT_FALSE(HasFileExtension("", ".jpg")); EXPECT_FALSE(HasFileExtension("testjpg", ".jpg")); EXPECT_TRUE(HasFileExtension("test.jpg", ".jpg")); EXPECT_TRUE(HasFileExtension("test.jpg", ".Jpg")); EXPECT_TRUE(HasFileExtension("test.jpg", ".JPG")); EXPECT_TRUE(HasFileExtension("test.", ".")); } TEST(SplitFileExtension, Nominal) { std::string root; std::string ext; SplitFileExtension("", &root, &ext); EXPECT_EQ(root, ""); EXPECT_EQ(ext, ""); SplitFileExtension(".", &root, &ext); EXPECT_EQ(root, ""); EXPECT_EQ(ext, ""); SplitFileExtension("file", &root, &ext); EXPECT_EQ(root, "file"); EXPECT_EQ(ext, ""); SplitFileExtension("file.", &root, &ext); EXPECT_EQ(root, "file"); EXPECT_EQ(ext, ""); SplitFileExtension("file.jpg", &root, &ext); EXPECT_EQ(root, "file"); EXPECT_EQ(ext, ".jpg"); SplitFileExtension("dir/file.jpg", &root, &ext); EXPECT_EQ(root, "dir/file"); EXPECT_EQ(ext, ".jpg"); SplitFileExtension("/dir/file.jpg", &root, &ext); EXPECT_EQ(root, "/dir/file"); EXPECT_EQ(ext, ".jpg"); SplitFileExtension("dir/file.suffix.jpg", &root, &ext); EXPECT_EQ(root, "dir/file.suffix"); EXPECT_EQ(ext, ".jpg"); SplitFileExtension("dir.suffix/file.suffix.jpg", &root, &ext); EXPECT_EQ(root, "dir.suffix/file.suffix"); EXPECT_EQ(ext, ".jpg"); SplitFileExtension("dir.suffix/file.", &root, &ext); EXPECT_EQ(root, "dir.suffix/file"); EXPECT_EQ(ext, ""); SplitFileExtension("./dir.suffix/file.", &root, &ext); EXPECT_EQ(root, "./dir.suffix/file"); EXPECT_EQ(ext, ""); } TEST(GetPathBaseName, Nominal) { EXPECT_EQ(GetPathBaseName(""), ""); EXPECT_EQ(GetPathBaseName("test"), "test"); EXPECT_EQ(GetPathBaseName("/test"), "test"); EXPECT_EQ(GetPathBaseName("test/"), "test"); EXPECT_EQ(GetPathBaseName("/test/"), "test"); EXPECT_EQ(GetPathBaseName("test1/test2"), "test2"); EXPECT_EQ(GetPathBaseName("/test1/test2"), "test2"); EXPECT_EQ(GetPathBaseName("/test1/test2/"), "test2"); EXPECT_EQ(GetPathBaseName("/test1/test2/"), "test2"); EXPECT_EQ(GetPathBaseName("\\test1/test2/"), "test2"); EXPECT_EQ(GetPathBaseName("\\test1\\test2\\"), "test2"); EXPECT_EQ(GetPathBaseName("/test1/test2/test3.ext"), "test3.ext"); } TEST(GetParentDir, Nominal) { EXPECT_EQ(GetParentDir(""), ""); EXPECT_EQ(GetParentDir("test"), ""); EXPECT_EQ(GetParentDir("/test"), "/"); EXPECT_EQ(GetParentDir("/"), ""); EXPECT_EQ(GetParentDir("test/test"), "test"); } TEST(JoinPaths, Nominal) { EXPECT_EQ(JoinPaths(""), ""); EXPECT_EQ(JoinPaths("test"), "test"); EXPECT_EQ(JoinPaths("/test"), "/test"); EXPECT_EQ(JoinPaths("test/"), "test/"); EXPECT_EQ(JoinPaths("/test/"), "/test/"); EXPECT_EQ(JoinPaths("test1/test2"), "test1/test2"); EXPECT_EQ(JoinPaths("/test1/test2"), "/test1/test2"); EXPECT_EQ(JoinPaths("/test1/test2/"), "/test1/test2/"); EXPECT_EQ(JoinPaths("/test1/test2/"), "/test1/test2/"); EXPECT_EQ(JoinPaths("\\test1/test2/"), "\\test1/test2/"); EXPECT_EQ(JoinPaths("\\test1\\test2\\"), "\\test1\\test2\\"); #ifdef _MSC_VER EXPECT_EQ(JoinPaths("test1", "test2"), "test1\\test2"); EXPECT_EQ(JoinPaths("/test1", "test2"), "/test1\\test2"); #else EXPECT_EQ(JoinPaths("test1", "test2"), "test1/test2"); EXPECT_EQ(JoinPaths("/test1", "test2"), "/test1/test2"); #endif EXPECT_EQ(JoinPaths("/test1", "/test2"), "/test1/test2"); EXPECT_EQ(JoinPaths("/test1", "/test2/"), "/test1/test2/"); EXPECT_EQ(JoinPaths("/test1", "/test2/", "test3.ext"), "/test1/test2/test3.ext"); } TEST(VectorContainsValue, Nominal) { EXPECT_TRUE(VectorContainsValue({1, 2, 3}, 1)); EXPECT_FALSE(VectorContainsValue({2, 3}, 1)); } TEST(VectorContainsDuplicateValues, Nominal) { EXPECT_FALSE(VectorContainsDuplicateValues({})); EXPECT_FALSE(VectorContainsDuplicateValues({1})); EXPECT_FALSE(VectorContainsDuplicateValues({1, 2})); EXPECT_FALSE(VectorContainsDuplicateValues({1, 2, 3})); EXPECT_TRUE(VectorContainsDuplicateValues({1, 1, 2, 3})); EXPECT_TRUE(VectorContainsDuplicateValues({1, 1, 2, 2, 3})); EXPECT_TRUE(VectorContainsDuplicateValues({1, 2, 3, 3})); EXPECT_FALSE(VectorContainsDuplicateValues({"a"})); EXPECT_FALSE(VectorContainsDuplicateValues({"a", "b"})); EXPECT_TRUE(VectorContainsDuplicateValues({"a", "a"})); } TEST(CSVToVector, Nominal) { const std::vector list1 = CSVToVector("1, 2, 3 , 4,5,6 "); EXPECT_EQ(list1.size(), 6); EXPECT_EQ(list1[0], 1); EXPECT_EQ(list1[1], 2); EXPECT_EQ(list1[2], 3); EXPECT_EQ(list1[3], 4); EXPECT_EQ(list1[4], 5); EXPECT_EQ(list1[5], 6); const std::vector list2 = CSVToVector("1; 2; 3 ; 4;5;6 "); EXPECT_EQ(list2.size(), 6); EXPECT_EQ(list2[0], 1); EXPECT_EQ(list2[1], 2); EXPECT_EQ(list2[2], 3); EXPECT_EQ(list2[3], 4); EXPECT_EQ(list2[4], 5); EXPECT_EQ(list2[5], 6); const std::vector list3 = CSVToVector("1;, 2;; 3 ; 4;5;6 "); EXPECT_EQ(list3.size(), 6); EXPECT_EQ(list3[0], 1); EXPECT_EQ(list3[1], 2); EXPECT_EQ(list3[2], 3); EXPECT_EQ(list3[3], 4); EXPECT_EQ(list3[4], 5); EXPECT_EQ(list3[5], 6); } TEST(VectorToCSV, Nominal) { EXPECT_EQ(VectorToCSV({}), ""); EXPECT_EQ(VectorToCSV({1}), "1"); EXPECT_EQ(VectorToCSV({1, 2, 3}), "1, 2, 3"); } TEST(RemoveCommandLineArgument, Nominal) { int argc = 3; std::unique_ptr arg1(new char[4]); std::memcpy(arg1.get(), "abc", 4 * sizeof(char)); std::unique_ptr arg2(new char[4]); std::memcpy(arg2.get(), "def", 4 * sizeof(char)); std::unique_ptr arg3(new char[4]); std::memcpy(arg3.get(), "ghi", 4 * sizeof(char)); std::vector argv = {arg1.get(), arg2.get(), arg3.get()}; RemoveCommandLineArgument("abcd", &argc, argv.data()); EXPECT_EQ(argc, 3); EXPECT_EQ(std::string(argv[0]), "abc"); EXPECT_EQ(std::string(argv[1]), "def"); EXPECT_EQ(std::string(argv[2]), "ghi"); RemoveCommandLineArgument("def", &argc, argv.data()); EXPECT_EQ(argc, 2); EXPECT_EQ(std::string(argv[0]), "abc"); EXPECT_EQ(std::string(argv[1]), "ghi"); RemoveCommandLineArgument("ghi", &argc, argv.data()); EXPECT_EQ(argc, 1); EXPECT_EQ(std::string(argv[0]), "abc"); RemoveCommandLineArgument("abc", &argc, argv.data()); EXPECT_EQ(argc, 0); RemoveCommandLineArgument("abc", &argc, argv.data()); EXPECT_EQ(argc, 0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/opengl_utils.cc000066400000000000000000000104161454702036400210400ustar00rootroot00000000000000// 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. #include "colmap/util/opengl_utils.h" #include "colmap/util/logging.h" namespace colmap { #if defined(COLMAP_GUI_ENABLED) OpenGLContextManager::OpenGLContextManager(int opengl_major_version, int opengl_minor_version) : parent_thread_(QThread::currentThread()), current_thread_(nullptr), make_current_action_(new QAction(this)) { CHECK_NOTNULL(QCoreApplication::instance()); CHECK_EQ(QCoreApplication::instance()->thread(), QThread::currentThread()); QSurfaceFormat format; format.setDepthBufferSize(24); format.setMajorVersion(opengl_major_version); format.setMinorVersion(opengl_minor_version); format.setSamples(4); format.setProfile(QSurfaceFormat::CompatibilityProfile); context_.setFormat(format); surface_.create(); CHECK(context_.create()); context_.makeCurrent(&surface_); CHECK(context_.isValid()) << "Could not create valid OpenGL context"; connect( make_current_action_, &QAction::triggered, this, [this]() { CHECK_NOTNULL(current_thread_); context_.doneCurrent(); context_.moveToThread(current_thread_); }, Qt::BlockingQueuedConnection); } bool OpenGLContextManager::MakeCurrent() { current_thread_ = QThread::currentThread(); make_current_action_->trigger(); context_.makeCurrent(&surface_); return context_.isValid(); } void RunThreadWithOpenGLContext(Thread* thread) { std::thread opengl_thread([thread]() { thread->Start(); thread->Wait(); CHECK_NOTNULL(QCoreApplication::instance())->exit(); }); CHECK_NOTNULL(QCoreApplication::instance())->exec(); opengl_thread.join(); // Make sure that all triggered OpenGLContextManager events are processed in // case the application exits before the contexts were made current. QCoreApplication::processEvents(); } void GLError(const char* file, const int line) { GLenum error_code(glGetError()); while (error_code != GL_NO_ERROR) { std::string error_name; switch (error_code) { case GL_INVALID_OPERATION: error_name = "INVALID_OPERATION"; break; case GL_INVALID_ENUM: error_name = "INVALID_ENUM"; break; case GL_INVALID_VALUE: error_name = "INVALID_VALUE"; break; case GL_OUT_OF_MEMORY: error_name = "OUT_OF_MEMORY"; break; case GL_INVALID_FRAMEBUFFER_OPERATION: error_name = "INVALID_FRAMEBUFFER_OPERATION"; break; default: error_name = "UNKNOWN_ERROR"; break; } LOG(ERROR) << "OpenGL error [" << file << ", line " << line << "]: GL_" << error_name; error_code = glGetError(); } } #endif } // namespace colmap colmap-3.9.1/src/colmap/util/opengl_utils.h000066400000000000000000000071161454702036400207050ustar00rootroot00000000000000// 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. #pragma once #if defined(COLMAP_GUI_ENABLED) #include #include #include #include #include #include #endif #include "colmap/util/threading.h" namespace colmap { #ifdef DEBUG #define glDebugLog() glError(__FILE__, __LINE__) #else #define glDebugLog() #endif #if defined(COLMAP_GUI_ENABLED) // This class manages a thread-safe OpenGL context. Note that this class must be // instantiated in the main Qt thread, since an OpenGL context must be created // in it. The context can then be made current in any other thread. class OpenGLContextManager : public QObject { public: explicit OpenGLContextManager(int opengl_major_version = 2, int opengl_minor_version = 1); // Make the OpenGL context available by moving it from the thread where it was // created to the current thread and making it current. bool MakeCurrent(); private: QOffscreenSurface surface_; QOpenGLContext context_; QThread* parent_thread_; QThread* current_thread_; QAction* make_current_action_; }; // Run and wait for the thread, that uses the OpenGLContextManager, e.g.: // // class TestThread : public Thread { // private: // void Run() { opengl_context_.MakeCurrent(); } // OpenGLContextManager opengl_context_; // }; // QApplication app(argc, argv); // TestThread thread; // RunThreadWithOpenGLContext(&thread); // void RunThreadWithOpenGLContext(Thread* thread); // Get the OpenGL errors and print them to stderr. void GLError(const char* file, int line); #else // Dummy implementation when GUI support is disabled class OpenGLContextManager { public: explicit OpenGLContextManager(int opengl_major_version = 2, int opengl_minor_version = 1) {} inline bool MakeCurrent() { return false; } }; inline void RunThreadWithOpenGLContext(Thread* thread) {} inline void GLError(const char* file, const int line) {} #endif } // namespace colmap colmap-3.9.1/src/colmap/util/opengl_utils_test.cc000066400000000000000000000047501454702036400221030ustar00rootroot00000000000000// 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. #include "colmap/util/opengl_utils.h" #include #include #include namespace colmap { namespace { TEST(OpenGLContextManager, Nominal) { char app_name[] = "Test"; int argc = 1; char* argv[] = {app_name}; QApplication app(argc, argv); OpenGLContextManager manager; std::thread thread([&manager]() { EXPECT_TRUE(manager.MakeCurrent()); EXPECT_TRUE(manager.MakeCurrent()); qApp->exit(); }); app.exec(); thread.join(); } TEST(RunThreadWithOpenGLContext, Nominal) { char app_name[] = "Test"; int argc = 1; char* argv[] = {app_name}; QApplication app(argc, argv); class TestThread : public Thread { private: void Run() { EXPECT_TRUE(opengl_context_.MakeCurrent()); } OpenGLContextManager opengl_context_; }; TestThread thread; RunThreadWithOpenGLContext(&thread); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/ply.cc000066400000000000000000000430131454702036400171370ustar00rootroot00000000000000// 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. #include "colmap/util/ply.h" #include "colmap/util/eigen_alignment.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" #include #include namespace colmap { std::vector ReadPly(const std::string& path) { std::ifstream file(path, std::ios::binary); CHECK(file.is_open()) << path; std::vector points; std::string line; // The index of the property for ASCII PLY files. int X_index = -1; int Y_index = -1; int Z_index = -1; int NX_index = -1; int NY_index = -1; int NZ_index = -1; int R_index = -1; int G_index = -1; int B_index = -1; // The position in number of bytes of the property for binary PLY files. int X_byte_pos = -1; int Y_byte_pos = -1; int Z_byte_pos = -1; int NX_byte_pos = -1; int NY_byte_pos = -1; int NZ_byte_pos = -1; int R_byte_pos = -1; int G_byte_pos = -1; int B_byte_pos = -1; // Flag to use double precision in binary PLY files bool X_double = false; bool Y_double = false; bool Z_double = false; bool NX_double = false; bool NY_double = false; bool NZ_double = false; bool in_vertex_section = false; bool is_binary = false; bool is_little_endian = false; size_t num_bytes_per_line = 0; size_t num_vertices = 0; int index = 0; while (std::getline(file, line)) { StringTrim(&line); if (line.empty()) { continue; } if (line == "end_header") { break; } if (line.size() >= 6 && line.substr(0, 6) == "format") { if (line == "format ascii 1.0") { is_binary = false; } else if (line == "format binary_little_endian 1.0") { is_binary = true; is_little_endian = true; } else if (line == "format binary_big_endian 1.0") { is_binary = true; is_little_endian = false; } } const std::vector line_elems = StringSplit(line, " "); if (line_elems.size() >= 3 && line_elems[0] == "element") { in_vertex_section = false; if (line_elems[1] == "vertex") { num_vertices = std::stoll(line_elems[2]); in_vertex_section = true; } else if (std::stoll(line_elems[2]) > 0) { LOG(WARNING) << "Only vertex elements supported; ignoring " << line_elems[1]; } } if (!in_vertex_section) { continue; } // Show diffuse, ambient, specular colors as regular colors. if (line_elems.size() >= 3 && line_elems[0] == "property") { CHECK(line_elems[1] == "float" || line_elems[1] == "float32" || line_elems[1] == "double" || line_elems[1] == "float64" || line_elems[1] == "uchar") << "PLY import only supports float, double, and uchar data types"; if (line == "property float x" || line == "property float32 x" || line == "property double x" || line == "property float64 x") { X_index = index; X_byte_pos = num_bytes_per_line; X_double = (line_elems[1] == "double" || line_elems[1] == "float64"); } else if (line == "property float y" || line == "property float32 y" || line == "property double y" || line == "property float64 y") { Y_index = index; Y_byte_pos = num_bytes_per_line; Y_double = (line_elems[1] == "double" || line_elems[1] == "float64"); } else if (line == "property float z" || line == "property float32 z" || line == "property double z" || line == "property float64 z") { Z_index = index; Z_byte_pos = num_bytes_per_line; Z_double = (line_elems[1] == "double" || line_elems[1] == "float64"); } else if (line == "property float nx" || line == "property float32 nx" || line == "property double nx" || line == "property float64 nx") { NX_index = index; NX_byte_pos = num_bytes_per_line; NX_double = (line_elems[1] == "double" || line_elems[1] == "float64"); } else if (line == "property float ny" || line == "property float32 ny" || line == "property double ny" || line == "property float64 ny") { NY_index = index; NY_byte_pos = num_bytes_per_line; NY_double = (line_elems[1] == "double" || line_elems[1] == "float64"); } else if (line == "property float nz" || line == "property float32 nz" || line == "property double nz" || line == "property float64 nz") { NZ_index = index; NZ_byte_pos = num_bytes_per_line; NZ_double = (line_elems[1] == "double" || line_elems[1] == "float64"); } else if (line == "property uchar r" || line == "property uchar red" || line == "property uchar diffuse_red" || line == "property uchar ambient_red" || line == "property uchar specular_red") { R_index = index; R_byte_pos = num_bytes_per_line; } else if (line == "property uchar g" || line == "property uchar green" || line == "property uchar diffuse_green" || line == "property uchar ambient_green" || line == "property uchar specular_green") { G_index = index; G_byte_pos = num_bytes_per_line; } else if (line == "property uchar b" || line == "property uchar blue" || line == "property uchar diffuse_blue" || line == "property uchar ambient_blue" || line == "property uchar specular_blue") { B_index = index; B_byte_pos = num_bytes_per_line; } index += 1; if (line_elems[1] == "float" || line_elems[1] == "float32") { num_bytes_per_line += 4; } else if (line_elems[1] == "double" || line_elems[1] == "float64") { num_bytes_per_line += 8; } else if (line_elems[1] == "uchar") { num_bytes_per_line += 1; } else { LOG(FATAL) << "Invalid data type: " << line_elems[1]; } } } const bool is_normal_missing = (NX_index == -1) || (NY_index == -1) || (NZ_index == -1); const bool is_rgb_missing = (R_index == -1) || (G_index == -1) || (B_index == -1); CHECK(X_index != -1 && Y_index != -1 && Z_index != -1) << "Invalid PLY file format: x, y, z properties missing"; points.reserve(num_vertices); if (is_binary) { std::vector buffer(num_bytes_per_line); for (size_t i = 0; i < num_vertices; ++i) { file.read(buffer.data(), num_bytes_per_line); PlyPoint point; if (is_little_endian) { point.x = LittleEndianToNative( X_double ? *reinterpret_cast(&buffer[X_byte_pos]) : *reinterpret_cast(&buffer[X_byte_pos])); point.y = LittleEndianToNative( Y_double ? *reinterpret_cast(&buffer[Y_byte_pos]) : *reinterpret_cast(&buffer[Y_byte_pos])); point.z = LittleEndianToNative( Z_double ? *reinterpret_cast(&buffer[Z_byte_pos]) : *reinterpret_cast(&buffer[Z_byte_pos])); if (!is_normal_missing) { point.nx = LittleEndianToNative( NX_double ? *reinterpret_cast(&buffer[NX_byte_pos]) : *reinterpret_cast(&buffer[NX_byte_pos])); point.ny = LittleEndianToNative( NY_double ? *reinterpret_cast(&buffer[NY_byte_pos]) : *reinterpret_cast(&buffer[NY_byte_pos])); point.nz = LittleEndianToNative( NZ_double ? *reinterpret_cast(&buffer[NZ_byte_pos]) : *reinterpret_cast(&buffer[NZ_byte_pos])); } if (!is_rgb_missing) { point.r = LittleEndianToNative( *reinterpret_cast(&buffer[R_byte_pos])); point.g = LittleEndianToNative( *reinterpret_cast(&buffer[G_byte_pos])); point.b = LittleEndianToNative( *reinterpret_cast(&buffer[B_byte_pos])); } } else { point.x = BigEndianToNative( X_double ? *reinterpret_cast(&buffer[X_byte_pos]) : *reinterpret_cast(&buffer[X_byte_pos])); point.y = BigEndianToNative( Y_double ? *reinterpret_cast(&buffer[Y_byte_pos]) : *reinterpret_cast(&buffer[Y_byte_pos])); point.z = BigEndianToNative( Z_double ? *reinterpret_cast(&buffer[Z_byte_pos]) : *reinterpret_cast(&buffer[Z_byte_pos])); if (!is_normal_missing) { point.nx = BigEndianToNative( NX_double ? *reinterpret_cast(&buffer[NX_byte_pos]) : *reinterpret_cast(&buffer[NX_byte_pos])); point.ny = BigEndianToNative( NY_double ? *reinterpret_cast(&buffer[NY_byte_pos]) : *reinterpret_cast(&buffer[NY_byte_pos])); point.nz = BigEndianToNative( NZ_double ? *reinterpret_cast(&buffer[NZ_byte_pos]) : *reinterpret_cast(&buffer[NZ_byte_pos])); } if (!is_rgb_missing) { point.r = BigEndianToNative( *reinterpret_cast(&buffer[R_byte_pos])); point.g = BigEndianToNative( *reinterpret_cast(&buffer[G_byte_pos])); point.b = BigEndianToNative( *reinterpret_cast(&buffer[B_byte_pos])); } } points.push_back(point); } } else { while (std::getline(file, line)) { StringTrim(&line); std::stringstream line_stream(line); std::string item; std::vector items; while (!line_stream.eof()) { std::getline(line_stream, item, ' '); StringTrim(&item); items.push_back(item); } PlyPoint point; point.x = std::stold(items.at(X_index)); point.y = std::stold(items.at(Y_index)); point.z = std::stold(items.at(Z_index)); if (!is_normal_missing) { point.nx = std::stold(items.at(NX_index)); point.ny = std::stold(items.at(NY_index)); point.nz = std::stold(items.at(NZ_index)); } if (!is_rgb_missing) { point.r = std::stoi(items.at(R_index)); point.g = std::stoi(items.at(G_index)); point.b = std::stoi(items.at(B_index)); } points.push_back(point); } } return points; } void WriteTextPlyPoints(const std::string& path, const std::vector& points, const bool write_normal, const bool write_rgb) { std::ofstream file(path); CHECK(file.is_open()) << path; file << "ply" << std::endl; file << "format ascii 1.0" << std::endl; file << "element vertex " << points.size() << std::endl; file << "property float x" << std::endl; file << "property float y" << std::endl; file << "property float z" << std::endl; if (write_normal) { file << "property float nx" << std::endl; file << "property float ny" << std::endl; file << "property float nz" << std::endl; } if (write_rgb) { file << "property uchar red" << std::endl; file << "property uchar green" << std::endl; file << "property uchar blue" << std::endl; } file << "end_header" << std::endl; for (const auto& point : points) { file << point.x << " " << point.y << " " << point.z; if (write_normal) { file << " " << point.nx << " " << point.ny << " " << point.nz; } if (write_rgb) { file << " " << static_cast(point.r) << " " << static_cast(point.g) << " " << static_cast(point.b); } file << std::endl; } file.close(); } void WriteBinaryPlyPoints(const std::string& path, const std::vector& points, const bool write_normal, const bool write_rgb) { std::fstream text_file(path, std::ios::out); CHECK(text_file.is_open()) << path; text_file << "ply" << std::endl; text_file << "format binary_little_endian 1.0" << std::endl; text_file << "element vertex " << points.size() << std::endl; text_file << "property float x" << std::endl; text_file << "property float y" << std::endl; text_file << "property float z" << std::endl; if (write_normal) { text_file << "property float nx" << std::endl; text_file << "property float ny" << std::endl; text_file << "property float nz" << std::endl; } if (write_rgb) { text_file << "property uchar red" << std::endl; text_file << "property uchar green" << std::endl; text_file << "property uchar blue" << std::endl; } text_file << "end_header" << std::endl; text_file.close(); std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); CHECK(binary_file.is_open()) << path; for (const auto& point : points) { WriteBinaryLittleEndian(&binary_file, point.x); WriteBinaryLittleEndian(&binary_file, point.y); WriteBinaryLittleEndian(&binary_file, point.z); if (write_normal) { WriteBinaryLittleEndian(&binary_file, point.nx); WriteBinaryLittleEndian(&binary_file, point.ny); WriteBinaryLittleEndian(&binary_file, point.nz); } if (write_rgb) { WriteBinaryLittleEndian(&binary_file, point.r); WriteBinaryLittleEndian(&binary_file, point.g); WriteBinaryLittleEndian(&binary_file, point.b); } } binary_file.close(); } void WriteTextPlyMesh(const std::string& path, const PlyMesh& mesh) { std::fstream file(path, std::ios::out); CHECK(file.is_open()); file << "ply" << std::endl; file << "format ascii 1.0" << std::endl; file << "element vertex " << mesh.vertices.size() << std::endl; file << "property float x" << std::endl; file << "property float y" << std::endl; file << "property float z" << std::endl; file << "element face " << mesh.faces.size() << std::endl; file << "property list uchar int vertex_index" << std::endl; file << "end_header" << std::endl; for (const auto& vertex : mesh.vertices) { file << vertex.x << " " << vertex.y << " " << vertex.z << std::endl; } for (const auto& face : mesh.faces) { file << StringPrintf("3 %d %d %d", face.vertex_idx1, face.vertex_idx2, face.vertex_idx3) << std::endl; } } void WriteBinaryPlyMesh(const std::string& path, const PlyMesh& mesh) { std::fstream text_file(path, std::ios::out); CHECK(text_file.is_open()); text_file << "ply" << std::endl; text_file << "format binary_little_endian 1.0" << std::endl; text_file << "element vertex " << mesh.vertices.size() << std::endl; text_file << "property float x" << std::endl; text_file << "property float y" << std::endl; text_file << "property float z" << std::endl; text_file << "element face " << mesh.faces.size() << std::endl; text_file << "property list uchar int vertex_index" << std::endl; text_file << "end_header" << std::endl; text_file.close(); std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); CHECK(binary_file.is_open()) << path; for (const auto& vertex : mesh.vertices) { WriteBinaryLittleEndian(&binary_file, vertex.x); WriteBinaryLittleEndian(&binary_file, vertex.y); WriteBinaryLittleEndian(&binary_file, vertex.z); } for (const auto& face : mesh.faces) { CHECK_LT(face.vertex_idx1, mesh.vertices.size()); CHECK_LT(face.vertex_idx2, mesh.vertices.size()); CHECK_LT(face.vertex_idx3, mesh.vertices.size()); const uint8_t kNumVertices = 3; WriteBinaryLittleEndian(&binary_file, kNumVertices); WriteBinaryLittleEndian(&binary_file, face.vertex_idx1); WriteBinaryLittleEndian(&binary_file, face.vertex_idx2); WriteBinaryLittleEndian(&binary_file, face.vertex_idx3); } binary_file.close(); } } // namespace colmap colmap-3.9.1/src/colmap/util/ply.h000066400000000000000000000065171454702036400170110ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/types.h" #include #include namespace colmap { struct PlyPoint { float x = 0.0f; float y = 0.0f; float z = 0.0f; float nx = 0.0f; float ny = 0.0f; float nz = 0.0f; uint8_t r = 0; uint8_t g = 0; uint8_t b = 0; }; struct PlyMeshVertex { PlyMeshVertex() : x(0), y(0), z(0) {} PlyMeshVertex(const float x, const float y, const float z) : x(x), y(y), z(z) {} float x = 0.0f; float y = 0.0f; float z = 0.0f; }; struct PlyMeshFace { PlyMeshFace() : vertex_idx1(0), vertex_idx2(0), vertex_idx3(0) {} PlyMeshFace(const size_t vertex_idx1, const size_t vertex_idx2, const size_t vertex_idx3) : vertex_idx1(vertex_idx1), vertex_idx2(vertex_idx2), vertex_idx3(vertex_idx3) {} size_t vertex_idx1 = 0; size_t vertex_idx2 = 0; size_t vertex_idx3 = 0; }; struct PlyMesh { std::vector vertices; std::vector faces; }; // Read PLY point cloud from text or binary file. std::vector ReadPly(const std::string& path); // Write PLY point cloud to text or binary file. void WriteTextPlyPoints(const std::string& path, const std::vector& points, bool write_normal = true, bool write_rgb = true); void WriteBinaryPlyPoints(const std::string& path, const std::vector& points, bool write_normal = true, bool write_rgb = true); // Write PLY mesh to text or binary file. void WriteTextPlyMesh(const std::string& path, const PlyMesh& mesh); void WriteBinaryPlyMesh(const std::string& path, const PlyMesh& mesh); } // namespace colmap colmap-3.9.1/src/colmap/util/sqlite3_utils.h000066400000000000000000000057631454702036400210130ustar00rootroot00000000000000// 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. #pragma once #include #include #include #include namespace colmap { inline int SQLite3CallHelper(int result_code, const std::string& filename, int line) { switch (result_code) { case SQLITE_OK: case SQLITE_ROW: case SQLITE_DONE: return result_code; default: LOG(ERROR) << "SQLite error [" << filename << ", line " << line << "]: " << sqlite3_errstr(result_code); throw std::runtime_error("SQLite error"); } } #define SQLITE3_CALL(func) SQLite3CallHelper(func, __FILE__, __LINE__) #define SQLITE3_EXEC(database, sql, callback) \ { \ char* err_msg = nullptr; \ const int result_code = \ sqlite3_exec(database, sql, callback, nullptr, &err_msg); \ if (result_code != SQLITE_OK) { \ LOG(ERROR) << "SQLite error [" << __FILE__ << ", line " << __LINE__ \ << "]: " << err_msg; \ sqlite3_free(err_msg); \ } \ } } // namespace colmap colmap-3.9.1/src/colmap/util/string.cc000066400000000000000000000164301454702036400176440ustar00rootroot00000000000000// 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. #include "colmap/util/string.h" #include #include #include #include #include namespace colmap { namespace { // The StringAppendV function is borrowed from Google under the BSD license: // // Copyright 2012 Google Inc. All rights reserved. // https://developers.google.com/protocol-buffers/ // // 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 Google Inc. 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 // OWNER 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. void StringAppendV(std::string* dst, const char* format, va_list ap) { // First try with a small fixed size buffer. static const int kFixedBufferSize = 1024; char fixed_buffer[kFixedBufferSize]; // It is possible for methods that use a va_list to invalidate // the data in it upon use. The fix is to make a copy // of the structure before using it and use that copy instead. va_list backup_ap; va_copy(backup_ap, ap); int result = vsnprintf(fixed_buffer, kFixedBufferSize, format, backup_ap); va_end(backup_ap); if (result < kFixedBufferSize) { if (result >= 0) { // Normal case - everything fits. dst->append(fixed_buffer, result); return; } #ifdef _MSC_VER // Error or MSVC running out of space. MSVC 8.0 and higher // can be asked about space needed with the special idiom below: va_copy(backup_ap, ap); result = vsnprintf(nullptr, 0, format, backup_ap); va_end(backup_ap); #endif if (result < 0) { // Just an error. return; } } // Increase the buffer size to the size requested by vsnprintf, // plus one for the closing \0. const int variable_buffer_size = result + 1; std::unique_ptr variable_buffer(new char[variable_buffer_size]); // Restore the va_list before we use it again. va_copy(backup_ap, ap); result = vsnprintf(variable_buffer.get(), variable_buffer_size, format, backup_ap); va_end(backup_ap); if (result >= 0 && result < variable_buffer_size) { dst->append(variable_buffer.get(), result); } } bool IsNotWhiteSpace(const int character) { return character != ' ' && character != '\n' && character != '\r' && character != '\t'; } } // namespace std::string StringPrintf(const char* format, ...) { va_list ap; va_start(ap, format); std::string result; StringAppendV(&result, format, ap); va_end(ap); return result; } std::string StringReplace(const std::string& str, const std::string& old_str, const std::string& new_str) { if (old_str.empty()) { return str; } size_t position = 0; std::string mod_str = str; while ((position = mod_str.find(old_str, position)) != std::string::npos) { mod_str.replace(position, old_str.size(), new_str); position += new_str.size(); } return mod_str; } std::string StringGetAfter(const std::string& str, const std::string& key) { if (key.empty()) { return str; } std::size_t found = str.rfind(key); if (found != std::string::npos) { return str.substr(found + key.length(), str.length() - (found + key.length())); } return ""; } std::vector StringSplit(const std::string& str, const std::string& delim) { std::vector elems; // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) boost::split(elems, str, boost::is_any_of(delim), boost::token_compress_on); return elems; } bool StringStartsWith(const std::string& str, const std::string& prefix) { return !prefix.empty() && prefix.size() <= str.size() && str.substr(0, prefix.size()) == prefix; } void StringLeftTrim(std::string* str) { str->erase(str->begin(), std::find_if(str->begin(), str->end(), IsNotWhiteSpace)); } void StringRightTrim(std::string* str) { str->erase(std::find_if(str->rbegin(), str->rend(), IsNotWhiteSpace).base(), str->end()); } void StringTrim(std::string* str) { StringLeftTrim(str); StringRightTrim(str); } void StringToLower(std::string* str) { std::transform(str->begin(), str->end(), str->begin(), ::tolower); } void StringToUpper(std::string* str) { std::transform(str->begin(), str->end(), str->begin(), ::toupper); } bool StringContains(const std::string& str, const std::string& sub_str) { return str.find(sub_str) != std::string::npos; } } // namespace colmap colmap-3.9.1/src/colmap/util/string.h000066400000000000000000000060301454702036400175010ustar00rootroot00000000000000// 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. #pragma once #include #include namespace colmap { // Format string by replacing embedded format specifiers with their respective // values, see `printf` for more details. This is a modified implementation // of Google's BSD-licensed StringPrintf function. std::string StringPrintf(const char* format, ...); // Replace all occurrences of `old_str` with `new_str` in the given string. std::string StringReplace(const std::string& str, const std::string& old_str, const std::string& new_str); // Get substring of string after search key std::string StringGetAfter(const std::string& str, const std::string& key); // Split string into list of words using the given delimiters. std::vector StringSplit(const std::string& str, const std::string& delim); // Check whether a string starts with a certain prefix. bool StringStartsWith(const std::string& str, const std::string& prefix); // Remove whitespace from string on both, left, or right sides. void StringTrim(std::string* str); void StringLeftTrim(std::string* str); void StringRightTrim(std::string* str); // Convert string to lower/upper case. void StringToLower(std::string* str); void StringToUpper(std::string* str); // Check whether the sub-string is contained in the given string. bool StringContains(const std::string& str, const std::string& sub_str); } // namespace colmap colmap-3.9.1/src/colmap/util/string_test.cc000066400000000000000000000221531454702036400207020ustar00rootroot00000000000000// 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. #include "colmap/util/string.h" #include namespace colmap { namespace { #define TEST_STRING_INPLACE(Func, str, ref_str) \ { \ std::string str_inplace = str; \ Func(&str_inplace); \ EXPECT_EQ(str_inplace, ref_str); \ } TEST(StringPrintf, Nominal) { EXPECT_EQ(StringPrintf("%s", "test"), "test"); EXPECT_EQ(StringPrintf("%d", 1), "1"); EXPECT_EQ(StringPrintf("%.3f", 1.234), "1.234"); EXPECT_EQ(StringPrintf("test%s", "test"), "testtest"); EXPECT_EQ(StringPrintf("test%d", 1), "test1"); EXPECT_EQ(StringPrintf("test%.3f", 1.234), "test1.234"); EXPECT_EQ(StringPrintf("%s%s", "test", "test"), "testtest"); EXPECT_EQ(StringPrintf("%d%s", 1, "test"), "1test"); EXPECT_EQ(StringPrintf("%.3f%s", 1.234, "test"), "1.234test"); } TEST(StringReplace, Nominal) { EXPECT_EQ(StringReplace("test", "-", ""), "test"); EXPECT_EQ(StringReplace("test", "t", "a"), "aesa"); EXPECT_EQ(StringReplace("test", "t", "---"), "---es---"); EXPECT_EQ(StringReplace("test", "", "a"), "test"); EXPECT_EQ(StringReplace("test", "", ""), "test"); EXPECT_EQ(StringReplace("ttt", "ttt", "+++"), "+++"); } TEST(StringGetAfter, Nominal) { EXPECT_EQ(StringGetAfter("test", ""), "test"); EXPECT_EQ(StringGetAfter("test", "notinit"), ""); EXPECT_EQ(StringGetAfter("test", "e"), "st"); EXPECT_EQ(StringGetAfter("test, multiple tests", "test"), "s"); EXPECT_EQ(StringGetAfter("", ""), ""); EXPECT_EQ(StringGetAfter("path/to/dataset/sub1/image.png", "sub1/"), "image.png"); } TEST(StringSplit, Nominal) { const std::vector list1 = StringSplit("1,2,3,4,5 , 6", ","); EXPECT_EQ(list1.size(), 6); EXPECT_EQ(list1[0], "1"); EXPECT_EQ(list1[1], "2"); EXPECT_EQ(list1[2], "3"); EXPECT_EQ(list1[3], "4"); EXPECT_EQ(list1[4], "5 "); EXPECT_EQ(list1[5], " 6"); const std::vector list2 = StringSplit("1,2,3,4,5 , 6", ""); EXPECT_EQ(list2.size(), 1); EXPECT_EQ(list2[0], "1,2,3,4,5 , 6"); const std::vector list3 = StringSplit("1,,2,,3,4,5 , 6", ","); EXPECT_EQ(list3.size(), 6); EXPECT_EQ(list3[0], "1"); EXPECT_EQ(list3[1], "2"); EXPECT_EQ(list3[2], "3"); EXPECT_EQ(list3[3], "4"); EXPECT_EQ(list3[4], "5 "); EXPECT_EQ(list3[5], " 6"); const std::vector list4 = StringSplit("1,,2,,3,4,5 , 6", ",,"); EXPECT_EQ(list4.size(), 6); EXPECT_EQ(list4[0], "1"); EXPECT_EQ(list4[1], "2"); EXPECT_EQ(list4[2], "3"); EXPECT_EQ(list4[3], "4"); EXPECT_EQ(list4[4], "5 "); EXPECT_EQ(list4[5], " 6"); const std::vector list5 = StringSplit("1,,2,,3,4,5 , 6", ", "); EXPECT_EQ(list5.size(), 6); EXPECT_EQ(list5[0], "1"); EXPECT_EQ(list5[1], "2"); EXPECT_EQ(list5[2], "3"); EXPECT_EQ(list5[3], "4"); EXPECT_EQ(list5[4], "5"); EXPECT_EQ(list5[5], "6"); const std::vector list6 = StringSplit(",1,,2,,3,4,5 , 6 ", ", "); EXPECT_EQ(list6.size(), 8); EXPECT_EQ(list6[0], ""); EXPECT_EQ(list6[1], "1"); EXPECT_EQ(list6[2], "2"); EXPECT_EQ(list6[3], "3"); EXPECT_EQ(list6[4], "4"); EXPECT_EQ(list6[5], "5"); EXPECT_EQ(list6[6], "6"); EXPECT_EQ(list6[7], ""); } TEST(StringStartsWith, Nominal) { EXPECT_FALSE(StringStartsWith("", "")); EXPECT_FALSE(StringStartsWith("a", "")); EXPECT_FALSE(StringStartsWith("", "a")); EXPECT_TRUE(StringStartsWith("a", "a")); EXPECT_TRUE(StringStartsWith("aa", "a")); EXPECT_TRUE(StringStartsWith("aa", "aa")); EXPECT_TRUE(StringStartsWith("aaaaaaaaa", "aa")); } TEST(StringStrim, Nominal) { TEST_STRING_INPLACE(StringTrim, "", ""); TEST_STRING_INPLACE(StringTrim, " ", ""); TEST_STRING_INPLACE(StringTrim, "a", "a"); TEST_STRING_INPLACE(StringTrim, " a", "a"); TEST_STRING_INPLACE(StringTrim, "a ", "a"); TEST_STRING_INPLACE(StringTrim, " a ", "a"); TEST_STRING_INPLACE(StringTrim, " a ", "a"); TEST_STRING_INPLACE(StringTrim, "aa ", "aa"); TEST_STRING_INPLACE(StringTrim, "a a ", "a a"); TEST_STRING_INPLACE(StringTrim, "a a a ", "a a a"); TEST_STRING_INPLACE(StringTrim, "\n\r\t", ""); } TEST(StringLeftString, Nominal) { TEST_STRING_INPLACE(StringLeftTrim, "", ""); TEST_STRING_INPLACE(StringLeftTrim, " ", ""); TEST_STRING_INPLACE(StringLeftTrim, "a", "a"); TEST_STRING_INPLACE(StringLeftTrim, " a", "a"); TEST_STRING_INPLACE(StringLeftTrim, "a ", "a "); TEST_STRING_INPLACE(StringLeftTrim, " a ", "a "); TEST_STRING_INPLACE(StringLeftTrim, " a ", "a "); TEST_STRING_INPLACE(StringLeftTrim, "aa ", "aa "); TEST_STRING_INPLACE(StringLeftTrim, "a a ", "a a "); TEST_STRING_INPLACE(StringLeftTrim, " a a", "a a"); TEST_STRING_INPLACE(StringLeftTrim, "a a a ", "a a a "); TEST_STRING_INPLACE(StringTrim, "\n\r\ta", "a"); } TEST(StringStrimRight, Nominal) { TEST_STRING_INPLACE(StringRightTrim, "", ""); TEST_STRING_INPLACE(StringRightTrim, " ", ""); TEST_STRING_INPLACE(StringRightTrim, "a", "a"); TEST_STRING_INPLACE(StringRightTrim, " a", " a"); TEST_STRING_INPLACE(StringRightTrim, "a ", "a"); TEST_STRING_INPLACE(StringRightTrim, " a ", " a"); TEST_STRING_INPLACE(StringRightTrim, " a ", " a"); TEST_STRING_INPLACE(StringRightTrim, "aa ", "aa"); TEST_STRING_INPLACE(StringRightTrim, "a a ", "a a"); TEST_STRING_INPLACE(StringRightTrim, "a a a ", "a a a"); TEST_STRING_INPLACE(StringTrim, "a\n\r\t", "a"); } TEST(StringToLower, Nominal) { TEST_STRING_INPLACE(StringToLower, "", ""); TEST_STRING_INPLACE(StringToLower, " ", " "); TEST_STRING_INPLACE(StringToLower, "a", "a"); TEST_STRING_INPLACE(StringToLower, " a", " a"); TEST_STRING_INPLACE(StringToLower, "a ", "a "); TEST_STRING_INPLACE(StringToLower, " a ", " a "); TEST_STRING_INPLACE(StringToLower, "aa ", "aa "); TEST_STRING_INPLACE(StringToLower, "A", "a"); TEST_STRING_INPLACE(StringToLower, " A", " a"); TEST_STRING_INPLACE(StringToLower, "A ", "a "); TEST_STRING_INPLACE(StringToLower, " A ", " a "); TEST_STRING_INPLACE(StringToLower, "AA ", "aa "); TEST_STRING_INPLACE(StringToLower, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"); TEST_STRING_INPLACE(StringToLower, "0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789 abcdefghijklmnopqrstuvwxyz"); } TEST(StringToUpper, Nominal) { TEST_STRING_INPLACE(StringToUpper, "", ""); TEST_STRING_INPLACE(StringToUpper, " ", " "); TEST_STRING_INPLACE(StringToUpper, "A", "A"); TEST_STRING_INPLACE(StringToUpper, " A", " A"); TEST_STRING_INPLACE(StringToUpper, "A ", "A "); TEST_STRING_INPLACE(StringToUpper, " A ", " A "); TEST_STRING_INPLACE(StringToUpper, "AA ", "AA "); TEST_STRING_INPLACE(StringToUpper, "a", "A"); TEST_STRING_INPLACE(StringToUpper, " a", " A"); TEST_STRING_INPLACE(StringToUpper, "a ", "A "); TEST_STRING_INPLACE(StringToUpper, " a ", " A "); TEST_STRING_INPLACE(StringToUpper, "aa ", "AA "); TEST_STRING_INPLACE(StringToUpper, "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); TEST_STRING_INPLACE(StringToUpper, "0123456789 abcdefghijklmnopqrstuvwxyz", "0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ"); } TEST(StringContains, Nominal) { EXPECT_TRUE(StringContains("", "")); EXPECT_TRUE(StringContains("a", "")); EXPECT_TRUE(StringContains("a", "a")); EXPECT_TRUE(StringContains("ab", "a")); EXPECT_TRUE(StringContains("ab", "ab")); EXPECT_FALSE(StringContains("", "a")); EXPECT_FALSE(StringContains("ab", "c")); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/testing.cc000066400000000000000000000052671454702036400200210ustar00rootroot00000000000000// 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. #include "colmap/util/testing.h" #include #include #include #include #include namespace colmap { std::string CreateTestDir() { const testing::TestInfo* test_info = CHECK_NOTNULL(testing::UnitTest::GetInstance()->current_test_info()); std::ostringstream test_name_stream; test_name_stream << test_info->test_suite_name() << "." << test_info->name(); const std::string test_name = test_name_stream.str(); const boost::filesystem::path test_dir = boost::filesystem::path("colmap_test_tmp_test_data") / test_name; // Create directory once. Cleanup artifacts from previous test runs. static std::mutex mutex; std::lock_guard lock(mutex); static std::set existing_test_names; if (existing_test_names.count(test_name) == 0) { if (boost::filesystem::is_directory(test_dir)) { boost::filesystem::remove_all(test_dir); } boost::filesystem::create_directories(test_dir); } existing_test_names.insert(test_name); return test_dir.string(); } } // namespace colmap colmap-3.9.1/src/colmap/util/testing.h000066400000000000000000000033121454702036400176500ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { std::string CreateTestDir(); } // namespace colmap colmap-3.9.1/src/colmap/util/threading.cc000066400000000000000000000155671454702036400203150ustar00rootroot00000000000000// 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. #include "colmap/util/threading.h" #include "colmap/util/logging.h" namespace colmap { Thread::Thread() : started_(false), stopped_(false), paused_(false), pausing_(false), finished_(false), setup_(false), setup_valid_(false) { RegisterCallback(STARTED_CALLBACK); RegisterCallback(FINISHED_CALLBACK); } void Thread::Start() { std::unique_lock lock(mutex_); CHECK(!started_ || finished_); Wait(); timer_.Restart(); thread_ = std::thread(&Thread::RunFunc, this); started_ = true; stopped_ = false; paused_ = false; pausing_ = false; finished_ = false; setup_ = false; setup_valid_ = false; } void Thread::Stop() { { std::unique_lock lock(mutex_); stopped_ = true; } Resume(); } void Thread::Pause() { std::unique_lock lock(mutex_); paused_ = true; } void Thread::Resume() { std::unique_lock lock(mutex_); if (paused_) { paused_ = false; pause_condition_.notify_all(); } } void Thread::Wait() { if (thread_.joinable()) { thread_.join(); } } bool Thread::IsStarted() { std::unique_lock lock(mutex_); return started_; } bool Thread::IsStopped() { std::unique_lock lock(mutex_); return stopped_; } bool Thread::IsPaused() { std::unique_lock lock(mutex_); return paused_; } bool Thread::IsRunning() { std::unique_lock lock(mutex_); return started_ && !pausing_ && !finished_; } bool Thread::IsFinished() { std::unique_lock lock(mutex_); return finished_; } void Thread::AddCallback(const int id, const std::function& func) { CHECK(func); CHECK_GT(callbacks_.count(id), 0) << "Callback not registered"; callbacks_.at(id).push_back(func); } void Thread::RegisterCallback(const int id) { callbacks_.emplace(id, std::list>()); } void Thread::Callback(const int id) const { CHECK_GT(callbacks_.count(id), 0) << "Callback not registered"; for (const auto& callback : callbacks_.at(id)) { callback(); } } std::thread::id Thread::GetThreadId() const { return std::this_thread::get_id(); } void Thread::SignalValidSetup() { std::unique_lock lock(mutex_); CHECK(!setup_); setup_ = true; setup_valid_ = true; setup_condition_.notify_all(); } void Thread::SignalInvalidSetup() { std::unique_lock lock(mutex_); CHECK(!setup_); setup_ = true; setup_valid_ = false; setup_condition_.notify_all(); } const class Timer& Thread::GetTimer() const { return timer_; } void Thread::BlockIfPaused() { std::unique_lock lock(mutex_); if (paused_) { pausing_ = true; timer_.Pause(); pause_condition_.wait(lock); pausing_ = false; timer_.Resume(); } } bool Thread::CheckValidSetup() { std::unique_lock lock(mutex_); if (!setup_) { setup_condition_.wait(lock); } return setup_valid_; } void Thread::RunFunc() { Callback(STARTED_CALLBACK); Run(); { std::unique_lock lock(mutex_); finished_ = true; timer_.Pause(); } Callback(FINISHED_CALLBACK); } ThreadPool::ThreadPool(const int num_threads) : stopped_(false), num_active_workers_(0) { const int num_effective_threads = GetEffectiveNumThreads(num_threads); for (int index = 0; index < num_effective_threads; ++index) { std::function worker = std::bind(&ThreadPool::WorkerFunc, this, index); workers_.emplace_back(worker); } } ThreadPool::~ThreadPool() { Stop(); } void ThreadPool::Stop() { { std::unique_lock lock(mutex_); if (stopped_) { return; } stopped_ = true; std::queue> empty_tasks; std::swap(tasks_, empty_tasks); } task_condition_.notify_all(); for (auto& worker : workers_) { worker.join(); } finished_condition_.notify_all(); } void ThreadPool::Wait() { std::unique_lock lock(mutex_); if (!tasks_.empty() || num_active_workers_ > 0) { finished_condition_.wait( lock, [this]() { return tasks_.empty() && num_active_workers_ == 0; }); } } void ThreadPool::WorkerFunc(const int index) { { std::lock_guard lock(mutex_); thread_id_to_index_.emplace(GetThreadId(), index); } while (true) { std::function task; { std::unique_lock lock(mutex_); task_condition_.wait(lock, [this] { return stopped_ || !tasks_.empty(); }); if (stopped_ && tasks_.empty()) { return; } task = std::move(tasks_.front()); tasks_.pop(); num_active_workers_ += 1; } task(); { std::unique_lock lock(mutex_); num_active_workers_ -= 1; } finished_condition_.notify_all(); } } std::thread::id ThreadPool::GetThreadId() const { return std::this_thread::get_id(); } int ThreadPool::GetThreadIndex() { std::unique_lock lock(mutex_); return thread_id_to_index_.at(GetThreadId()); } int GetEffectiveNumThreads(const int num_threads) { int num_effective_threads = num_threads; if (num_threads <= 0) { num_effective_threads = std::thread::hardware_concurrency(); } if (num_effective_threads <= 0) { num_effective_threads = 1; } return num_effective_threads; } } // namespace colmap colmap-3.9.1/src/colmap/util/threading.h000066400000000000000000000275651454702036400201600ustar00rootroot00000000000000// 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. #pragma once #include "colmap/util/timer.h" #include #include #include #include #include #include #include #include #include namespace colmap { #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wkeyword-macro" #endif #ifdef __clang__ #pragma clang diagnostic pop // -Wkeyword-macro #endif // Helper class to create single threads with simple controls and timing, e.g.: // // class MyThread : public Thread { // enum { // PROCESSED_CALLBACK, // }; // // MyThread() { RegisterCallback(PROCESSED_CALLBACK); } // void Run() { // // Some setup routine... note that this optional. // if (setup_valid) { // SignalValidSetup(); // } else { // SignalInvalidSetup(); // } // // // Some pre-processing... // for (const auto& item : items) { // BlockIfPaused(); // if (IsStopped()) { // // Tear down... // break; // } // // Process item... // Callback(PROCESSED_CALLBACK); // } // } // }; // // MyThread thread; // thread.AddCallback(MyThread::PROCESSED_CALLBACK, []() { // LOG(INFO) << "Processed item"; }) // thread.AddCallback(MyThread::STARTED_CALLBACK, []() { // LOG(INFO) << "Start"; }) // thread.AddCallback(MyThread::FINISHED_CALLBACK, []() { // LOG(INFO) << "Finished"; }) // thread.Start(); // // thread.CheckValidSetup(); // // Pause, resume, stop, ... // thread.Wait(); // thread.Timer().PrintElapsedSeconds(); // class Thread { public: enum { STARTED_CALLBACK = INT_MIN, FINISHED_CALLBACK, }; Thread(); virtual ~Thread() = default; // Control the state of the thread. virtual void Start(); virtual void Stop(); virtual void Pause(); virtual void Resume(); virtual void Wait(); // Check the state of the thread. bool IsStarted(); bool IsStopped(); bool IsPaused(); bool IsRunning(); bool IsFinished(); // To be called from inside the main run function. This blocks the main // caller, if the thread is paused, until the thread is resumed. void BlockIfPaused(); // To be called from outside. This blocks the caller until the thread is // setup, i.e. it signaled that its setup was valid or not. If it never gives // this signal, this call will block the caller infinitely. Check whether // setup is valid. Note that the result is only meaningful if the thread gives // a setup signal. bool CheckValidSetup(); // Set callbacks that can be triggered within the main run function. void AddCallback(int id, const std::function& func); // Get timing information of the thread, properly accounting for pause times. const Timer& GetTimer() const; protected: // This is the main run function to be implemented by the child class. If you // are looping over data and want to support the pause operation, call // `BlockIfPaused` at appropriate places in the loop. To support the stop // operation, check the `IsStopped` state and early return from this method. virtual void Run() = 0; // Register a new callback. Note that only registered callbacks can be // set/reset and called from within the thread. Hence, this method should be // called from the derived thread constructor. void RegisterCallback(int id); // Call back to the function with the specified name, if it exists. void Callback(int id) const; // Get the unique identifier of the current thread. std::thread::id GetThreadId() const; // Signal that the thread is setup. Only call this function once. void SignalValidSetup(); void SignalInvalidSetup(); private: // Wrapper around the main run function to set the finished flag. void RunFunc(); std::thread thread_; std::mutex mutex_; std::condition_variable pause_condition_; std::condition_variable setup_condition_; Timer timer_; bool started_; bool stopped_; bool paused_; bool pausing_; bool finished_; bool setup_; bool setup_valid_; std::unordered_map>> callbacks_; }; // A thread pool class to submit generic tasks (functors) to a pool of workers: // // ThreadPool thread_pool; // thread_pool.AddTask([]() { /* Do some work */ }); // auto future = thread_pool.AddTask([]() { /* Do some work */ return 1; }); // const auto result = future.get(); // for (int i = 0; i < 10; ++i) { // thread_pool.AddTask([](const int i) { /* Do some work */ }); // } // thread_pool.Wait(); // class ThreadPool { public: static const int kMaxNumThreads = -1; template #ifdef __cpp_lib_is_invocable using result_of_t = std::invoke_result_t; #else using result_of_t = typename std::result_of::type; #endif explicit ThreadPool(int num_threads = kMaxNumThreads); ~ThreadPool(); inline size_t NumThreads() const; // Add new task to the thread pool. template auto AddTask(func_t&& f, args_t&&... args) -> std::future>; // Stop the execution of all workers. void Stop(); // Wait until tasks are finished. void Wait(); // Get the unique identifier of the current thread. std::thread::id GetThreadId() const; // Get the index of the current thread. In a thread pool of size N, // the thread index defines the 0-based index of the thread in the pool. // In other words, there are the thread indices 0, ..., N-1. int GetThreadIndex(); private: void WorkerFunc(int index); std::vector workers_; std::queue> tasks_; std::mutex mutex_; std::condition_variable task_condition_; std::condition_variable finished_condition_; bool stopped_; int num_active_workers_; std::unordered_map thread_id_to_index_; }; // A job queue class for the producer-consumer paradigm. // // JobQueue job_queue; // // std::thread producer_thread([&job_queue]() { // for (int i = 0; i < 10; ++i) { // job_queue.Push(i); // } // }); // // std::thread consumer_thread([&job_queue]() { // for (int i = 0; i < 10; ++i) { // const auto job = job_queue.Pop(); // if (job.IsValid()) { /* Do some work */ } // else { break; } // } // }); // // producer_thread.join(); // consumer_thread.join(); // template class JobQueue { public: class Job { public: Job() : data_{}, valid_(false) {} explicit Job(T data) : data_(std::move(data)), valid_(true) {} // Check whether the data is valid. bool IsValid() const { return valid_; } // Get reference to the data. T& Data() { return data_; } const T& Data() const { return data_; } private: T data_; bool valid_; }; JobQueue(); explicit JobQueue(size_t max_num_jobs); ~JobQueue(); // The number of pushed and not popped jobs in the queue. size_t Size(); // Push a new job to the queue. Waits if the number of jobs is exceeded. bool Push(T data); // Pop a job from the queue. Waits if there is no job in the queue. Job Pop(); // Wait for all jobs to be popped and then stop the queue. void Wait(); // Stop the queue and return from all push/pop calls with false. void Stop(); // Clear all pushed and not popped jobs from the queue. void Clear(); private: size_t max_num_jobs_; std::atomic stop_; std::queue jobs_; std::mutex mutex_; std::condition_variable push_condition_; std::condition_variable pop_condition_; std::condition_variable empty_condition_; }; // Return the number of logical CPU cores if num_threads <= 0, // otherwise return the input value of num_threads. int GetEffectiveNumThreads(int num_threads); //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// size_t ThreadPool::NumThreads() const { return workers_.size(); } template auto ThreadPool::AddTask(func_t&& f, args_t&&... args) -> std::future> { typedef result_of_t return_t; auto task = std::make_shared>( std::bind(std::forward(f), std::forward(args)...)); std::future result = task->get_future(); { std::unique_lock lock(mutex_); if (stopped_) { throw std::runtime_error("Cannot add task to stopped thread pool."); } tasks_.emplace([task]() { (*task)(); }); } task_condition_.notify_one(); return result; } template JobQueue::JobQueue() : JobQueue(std::numeric_limits::max()) {} template JobQueue::JobQueue(const size_t max_num_jobs) : max_num_jobs_(max_num_jobs), stop_(false) {} template JobQueue::~JobQueue() { Stop(); } template size_t JobQueue::Size() { std::unique_lock lock(mutex_); return jobs_.size(); } template bool JobQueue::Push(T data) { std::unique_lock lock(mutex_); while (jobs_.size() >= max_num_jobs_ && !stop_) { pop_condition_.wait(lock); } if (stop_) { return false; } else { jobs_.push(std::move(data)); push_condition_.notify_one(); return true; } } template typename JobQueue::Job JobQueue::Pop() { std::unique_lock lock(mutex_); while (jobs_.empty() && !stop_) { push_condition_.wait(lock); } if (stop_) { return Job(); } else { Job job(std::move(jobs_.front())); jobs_.pop(); pop_condition_.notify_one(); if (jobs_.empty()) { empty_condition_.notify_all(); } return job; } } template void JobQueue::Wait() { std::unique_lock lock(mutex_); while (!jobs_.empty()) { empty_condition_.wait(lock); } } template void JobQueue::Stop() { stop_ = true; push_condition_.notify_all(); pop_condition_.notify_all(); } template void JobQueue::Clear() { std::unique_lock lock(mutex_); std::queue empty_jobs; std::swap(jobs_, empty_jobs); } } // namespace colmap colmap-3.9.1/src/colmap/util/threading_test.cc000066400000000000000000000525521454702036400213470ustar00rootroot00000000000000// 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. #include "colmap/util/threading.h" #include "colmap/util/logging.h" #include namespace colmap { namespace { namespace { // Custom implementation of std::barrier that allows us to execute the below // tests deterministically. class Barrier { public: Barrier() : Barrier(2) {} explicit Barrier(const size_t count) : threshold_(count), count_(count), generation_(0) {} void Wait() { std::unique_lock lock(mutex_); auto current_generation = generation_; if (!--count_) { ++generation_; count_ = threshold_; condition_.notify_all(); } else { condition_.wait(lock, [this, current_generation] { return current_generation != generation_; }); } } private: std::mutex mutex_; std::condition_variable condition_; const size_t threshold_; size_t count_; size_t generation_; }; } // namespace // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. TEST(Thread, Wait) { class TestThread : public Thread { public: Barrier startBarrier; Barrier endBarrier; void Run() { startBarrier.Wait(); endBarrier.Wait(); } }; TestThread thread; EXPECT_FALSE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.Start(); thread.startBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.endBarrier.Wait(); thread.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_TRUE(thread.IsFinished()); } TEST(Thread, Pause) { class TestThread : public Thread { public: Barrier startBarrier; Barrier pauseBarrier; Barrier pausedBarrier; Barrier resumedBarrier; Barrier endBarrier; void Run() { startBarrier.Wait(); pauseBarrier.Wait(); pausedBarrier.Wait(); BlockIfPaused(); resumedBarrier.Wait(); endBarrier.Wait(); } }; TestThread thread; EXPECT_FALSE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.Start(); thread.startBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.pauseBarrier.Wait(); thread.Pause(); thread.pausedBarrier.Wait(); while (!thread.IsPaused() || thread.IsRunning()) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_TRUE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.Resume(); thread.resumedBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.endBarrier.Wait(); thread.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_TRUE(thread.IsFinished()); } TEST(Thread, PauseStop) { class TestThread : public Thread { public: Barrier startBarrier; Barrier pauseBarrier; Barrier pausedBarrier; Barrier resumedBarrier; Barrier stopBarrier; Barrier stoppingBarrier; Barrier stoppedBarrier; Barrier endBarrier; void Run() { startBarrier.Wait(); pauseBarrier.Wait(); pausedBarrier.Wait(); BlockIfPaused(); resumedBarrier.Wait(); stopBarrier.Wait(); stoppingBarrier.Wait(); if (IsStopped()) { stoppedBarrier.Wait(); endBarrier.Wait(); return; } } }; TestThread thread; EXPECT_FALSE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.Start(); thread.startBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.pauseBarrier.Wait(); thread.Pause(); thread.pausedBarrier.Wait(); while (!thread.IsPaused() || thread.IsRunning()) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_TRUE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.Resume(); thread.resumedBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.stopBarrier.Wait(); thread.Stop(); thread.stoppingBarrier.Wait(); thread.stoppedBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_TRUE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.endBarrier.Wait(); thread.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_TRUE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_TRUE(thread.IsFinished()); } TEST(Thread, Restart) { class TestThread : public Thread { public: Barrier startBarrier; Barrier endBarrier; void Run() { startBarrier.Wait(); endBarrier.Wait(); } }; TestThread thread; EXPECT_FALSE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); for (size_t i = 0; i < 2; ++i) { thread.Start(); thread.startBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.endBarrier.Wait(); thread.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_TRUE(thread.IsFinished()); } } TEST(Thread, ValidSetup) { class TestThread : public Thread { public: Barrier startBarrier; Barrier signalBarrier; Barrier endBarrier; void Run() { startBarrier.Wait(); SignalValidSetup(); signalBarrier.Wait(); endBarrier.Wait(); } }; TestThread thread; EXPECT_FALSE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.Start(); thread.startBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.signalBarrier.Wait(); EXPECT_TRUE(thread.CheckValidSetup()); thread.endBarrier.Wait(); thread.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_TRUE(thread.IsFinished()); } TEST(Thread, InvalidSetup) { class TestThread : public Thread { public: Barrier startBarrier; Barrier signalBarrier; Barrier endBarrier; void Run() { startBarrier.Wait(); SignalInvalidSetup(); signalBarrier.Wait(); endBarrier.Wait(); } }; TestThread thread; EXPECT_FALSE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.Start(); thread.startBarrier.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_TRUE(thread.IsRunning()); EXPECT_FALSE(thread.IsFinished()); thread.signalBarrier.Wait(); EXPECT_FALSE(thread.CheckValidSetup()); thread.endBarrier.Wait(); thread.Wait(); EXPECT_TRUE(thread.IsStarted()); EXPECT_FALSE(thread.IsStopped()); EXPECT_FALSE(thread.IsPaused()); EXPECT_FALSE(thread.IsRunning()); EXPECT_TRUE(thread.IsFinished()); } TEST(Thread, Callback) { class TestThread : public Thread { public: enum Callbacks { CALLBACK1, CALLBACK2, }; TestThread() { RegisterCallback(CALLBACK1); RegisterCallback(CALLBACK2); } private: void Run() { Callback(CALLBACK1); Callback(CALLBACK2); } }; bool called_back1 = false; std::function CallbackFunc1 = [&called_back1]() { called_back1 = true; }; bool called_back2 = false; std::function CallbackFunc2 = [&called_back2]() { called_back2 = true; }; bool called_back3 = false; std::function CallbackFunc3 = [&called_back3]() { called_back3 = true; }; TestThread thread; thread.AddCallback(TestThread::CALLBACK1, CallbackFunc1); thread.Start(); thread.Wait(); EXPECT_TRUE(called_back1); EXPECT_FALSE(called_back2); EXPECT_FALSE(called_back3); called_back1 = false; called_back2 = false; thread.AddCallback(TestThread::CALLBACK2, CallbackFunc2); thread.Start(); thread.Wait(); EXPECT_TRUE(called_back1); EXPECT_TRUE(called_back2); EXPECT_FALSE(called_back3); called_back1 = false; called_back2 = false; called_back3 = false; thread.AddCallback(TestThread::CALLBACK1, CallbackFunc3); thread.Start(); thread.Wait(); EXPECT_TRUE(called_back1); EXPECT_TRUE(called_back2); EXPECT_TRUE(called_back3); } TEST(Thread, DefaultCallback) { class TestThread : public Thread { public: Barrier startBarrier; Barrier signalBarrier; Barrier endBarrier; void Run() { startBarrier.Wait(); endBarrier.Wait(); } }; bool called_back1 = false; std::function CallbackFunc1 = [&called_back1]() { called_back1 = true; }; bool called_back2 = false; std::function CallbackFunc2 = [&called_back2]() { called_back2 = true; }; TestThread thread; thread.AddCallback(TestThread::STARTED_CALLBACK, CallbackFunc1); thread.AddCallback(TestThread::FINISHED_CALLBACK, CallbackFunc2); thread.Start(); thread.startBarrier.Wait(); EXPECT_TRUE(called_back1); EXPECT_FALSE(called_back2); thread.endBarrier.Wait(); thread.Wait(); EXPECT_TRUE(called_back1); EXPECT_TRUE(called_back2); } TEST(ThreadPool, NoArgNoReturn) { std::function Func = []() { std::this_thread::sleep_for(std::chrono::microseconds(1)); }; ThreadPool pool(4); std::vector> futures; futures.reserve(100); for (int i = 0; i < 100; ++i) { futures.push_back(pool.AddTask(Func)); } for (auto& future : futures) { future.get(); } } TEST(ThreadPool, ArgNoReturn) { std::function Func = [](int num) { for (int i = 0; i < 100; ++i) { num += i; } }; ThreadPool pool(4); std::vector> futures; futures.reserve(100); for (int i = 0; i < 100; ++i) { futures.push_back(pool.AddTask(Func, i)); } for (auto& future : futures) { future.get(); } } TEST(ThreadPool, NoArgReturn) { std::function Func = []() { return 0; }; ThreadPool pool(4); std::vector> futures; futures.reserve(100); for (int i = 0; i < 100; ++i) { futures.push_back(pool.AddTask(Func)); } for (auto& future : futures) { future.get(); } } TEST(ThreadPool, ArgReturn) { std::function Func = [](int num) { for (int i = 0; i < 100; ++i) { num += i; } return num; }; ThreadPool pool(4); std::vector> futures; futures.reserve(100); for (int i = 0; i < 100; ++i) { futures.push_back(pool.AddTask(Func, i)); } for (auto& future : futures) { future.get(); } } TEST(ThreadPool, Stop) { std::function Func = [](int num) { for (int i = 0; i < 100; ++i) { num += i; } return num; }; ThreadPool pool(4); std::vector> futures; futures.reserve(100); for (int i = 0; i < 100; ++i) { futures.push_back(pool.AddTask(Func, i)); } pool.Stop(); EXPECT_THROW(pool.AddTask(Func, 100), std::runtime_error); pool.Stop(); } TEST(ThreadPool, Wait) { std::vector results(100, 0); std::function Func = [&results](const int num) { results[num] = 1; }; ThreadPool pool(4); pool.Wait(); for (size_t i = 0; i < results.size(); ++i) { pool.AddTask(Func, i); } pool.Wait(); for (const auto result : results) { EXPECT_EQ(result, 1); } } TEST(ThreadPool, WaitEverytime) { std::vector results(4, 0); std::function Func = [&results](const int num) { results[num] = 1; }; ThreadPool pool(4); for (size_t i = 0; i < results.size(); ++i) { pool.AddTask(Func, i); pool.Wait(); for (size_t j = 0; j < results.size(); ++j) { if (j <= i) { EXPECT_EQ(results[j], 1); } else { EXPECT_EQ(results[j], 0); } } } pool.Wait(); } TEST(ThreadPool, GetThreadIndex) { ThreadPool pool(4); std::vector results(100, -1); std::function Func = [&](const int num) { results[num] = pool.GetThreadIndex(); }; for (size_t i = 0; i < results.size(); ++i) { pool.AddTask(Func, i); } pool.Wait(); for (const auto result : results) { EXPECT_GE(result, 0); EXPECT_LE(result, 3); } } TEST(JobQueue, SingleProducerSingleConsumer) { JobQueue job_queue; // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. std::thread producer_thread([&job_queue]() { for (int i = 0; i < 10; ++i) { CHECK(job_queue.Push(i)); } }); std::thread consumer_thread([&job_queue]() { CHECK_LE(job_queue.Size(), 10); for (int i = 0; i < 10; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_LT(job.Data(), 10); } }); producer_thread.join(); consumer_thread.join(); } TEST(JobQueue, SingleProducerSingleConsumerMaxNumJobs) { JobQueue job_queue(2); // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. std::thread producer_thread([&job_queue]() { for (int i = 0; i < 10; ++i) { CHECK(job_queue.Push(i)); } }); std::thread consumer_thread([&job_queue]() { CHECK_LE(job_queue.Size(), 2); for (int i = 0; i < 10; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_LT(job.Data(), 10); } }); producer_thread.join(); consumer_thread.join(); } TEST(JobQueue, MultipleProducerSingleConsumer) { JobQueue job_queue(1); // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. std::thread producer_thread1([&job_queue]() { for (int i = 0; i < 10; ++i) { CHECK(job_queue.Push(i)); } }); std::thread producer_thread2([&job_queue]() { for (int i = 0; i < 10; ++i) { CHECK(job_queue.Push(i)); } }); std::thread consumer_thread([&job_queue]() { CHECK_LE(job_queue.Size(), 1); for (int i = 0; i < 20; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_LT(job.Data(), 10); } }); producer_thread1.join(); producer_thread2.join(); consumer_thread.join(); } TEST(JobQueue, SingleProducerMultipleConsumer) { JobQueue job_queue(1); // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. std::thread producer_thread([&job_queue]() { for (int i = 0; i < 20; ++i) { CHECK(job_queue.Push(i)); } }); std::thread consumer_thread1([&job_queue]() { CHECK_LE(job_queue.Size(), 1); for (int i = 0; i < 10; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_LT(job.Data(), 20); } }); std::thread consumer_thread2([&job_queue]() { CHECK_LE(job_queue.Size(), 1); for (int i = 0; i < 10; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_LT(job.Data(), 20); } }); producer_thread.join(); consumer_thread1.join(); consumer_thread2.join(); } TEST(JobQueue, MultipleProducerMultipleConsumer) { JobQueue job_queue(1); // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. std::thread producer_thread1([&job_queue]() { for (int i = 0; i < 10; ++i) { CHECK(job_queue.Push(i)); } }); std::thread producer_thread2([&job_queue]() { for (int i = 0; i < 10; ++i) { CHECK(job_queue.Push(i)); } }); std::thread consumer_thread1([&job_queue]() { CHECK_LE(job_queue.Size(), 1); for (int i = 0; i < 10; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_LT(job.Data(), 10); } }); std::thread consumer_thread2([&job_queue]() { CHECK_LE(job_queue.Size(), 1); for (int i = 0; i < 10; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_LT(job.Data(), 10); } }); producer_thread1.join(); producer_thread2.join(); consumer_thread1.join(); consumer_thread2.join(); } TEST(JobQueue, Wait) { JobQueue job_queue; // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. for (int i = 0; i < 10; ++i) { CHECK(job_queue.Push(i)); } std::thread consumer_thread([&job_queue]() { CHECK_EQ(job_queue.Size(), 10); for (int i = 0; i < 10; ++i) { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_EQ(job.Data(), i); } }); job_queue.Wait(); EXPECT_EQ(job_queue.Size(), 0); EXPECT_TRUE(job_queue.Push(0)); EXPECT_TRUE(job_queue.Pop().IsValid()); consumer_thread.join(); } TEST(JobQueue, StopProducer) { JobQueue job_queue(1); // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. Barrier stopBarrier; std::thread producer_thread([&job_queue, &stopBarrier]() { CHECK(job_queue.Push(0)); stopBarrier.Wait(); CHECK(!job_queue.Push(0)); }); stopBarrier.Wait(); EXPECT_EQ(job_queue.Size(), 1); job_queue.Stop(); producer_thread.join(); EXPECT_FALSE(job_queue.Push(0)); EXPECT_FALSE(job_queue.Pop().IsValid()); } TEST(JobQueue, StopConsumer) { JobQueue job_queue(1); // IMPORTANT: EXPECT_TRUE_* macros are not thread-safe, // so we use glog's CHECK macros inside threads. EXPECT_TRUE(job_queue.Push(0)); Barrier popBarrier; std::thread consumer_thread([&job_queue, &popBarrier]() { const auto job = job_queue.Pop(); CHECK(job.IsValid()); CHECK_EQ(job.Data(), 0); popBarrier.Wait(); CHECK(!job_queue.Pop().IsValid()); }); popBarrier.Wait(); EXPECT_EQ(job_queue.Size(), 0); job_queue.Stop(); consumer_thread.join(); EXPECT_FALSE(job_queue.Push(0)); EXPECT_FALSE(job_queue.Pop().IsValid()); } TEST(JobQueue, Clear) { JobQueue job_queue(1); EXPECT_TRUE(job_queue.Push(0)); EXPECT_EQ(job_queue.Size(), 1); job_queue.Clear(); EXPECT_EQ(job_queue.Size(), 0); } TEST(GetEffectiveNumThreads, Nominal) { EXPECT_GT(GetEffectiveNumThreads(-2), 0); EXPECT_GT(GetEffectiveNumThreads(-1), 0); EXPECT_GT(GetEffectiveNumThreads(0), 0); EXPECT_EQ(GetEffectiveNumThreads(1), 1); EXPECT_EQ(GetEffectiveNumThreads(2), 2); EXPECT_EQ(GetEffectiveNumThreads(3), 3); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/timer.cc000066400000000000000000000063471454702036400174640ustar00rootroot00000000000000// 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. #include "colmap/util/timer.h" #include "colmap/util/logging.h" #include "colmap/util/misc.h" namespace colmap { Timer::Timer() : started_(false), paused_(false) {} void Timer::Start() { started_ = true; paused_ = false; start_time_ = std::chrono::high_resolution_clock::now(); } void Timer::Restart() { started_ = false; Start(); } void Timer::Pause() { paused_ = true; pause_time_ = std::chrono::high_resolution_clock::now(); } void Timer::Resume() { paused_ = false; start_time_ += std::chrono::high_resolution_clock::now() - pause_time_; } void Timer::Reset() { started_ = false; paused_ = false; } double Timer::ElapsedMicroSeconds() const { if (!started_) { return 0.0; } if (paused_) { return std::chrono::duration_cast(pause_time_ - start_time_) .count(); } else { return std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - start_time_) .count(); } } double Timer::ElapsedSeconds() const { return ElapsedMicroSeconds() / 1e6; } double Timer::ElapsedMinutes() const { return ElapsedSeconds() / 60; } double Timer::ElapsedHours() const { return ElapsedMinutes() / 60; } void Timer::PrintSeconds() const { LOG(INFO) << StringPrintf("Elapsed time: %.5f [seconds]", ElapsedSeconds()); } void Timer::PrintMinutes() const { LOG(INFO) << StringPrintf("Elapsed time: %.3f [minutes]", ElapsedMinutes()); } void Timer::PrintHours() const { LOG(INFO) << StringPrintf("Elapsed time: %.3f [hours]", ElapsedHours()); } } // namespace colmap colmap-3.9.1/src/colmap/util/timer.h000066400000000000000000000042541454702036400173210ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { class Timer { public: Timer(); void Start(); void Restart(); void Pause(); void Resume(); void Reset(); double ElapsedMicroSeconds() const; double ElapsedSeconds() const; double ElapsedMinutes() const; double ElapsedHours() const; void PrintSeconds() const; void PrintMinutes() const; void PrintHours() const; private: bool started_; bool paused_; std::chrono::high_resolution_clock::time_point start_time_; std::chrono::high_resolution_clock::time_point pause_time_; }; } // namespace colmap colmap-3.9.1/src/colmap/util/timer_test.cc000066400000000000000000000051021454702036400205070ustar00rootroot00000000000000// 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. #include "colmap/util/timer.h" #include namespace colmap { namespace { TEST(Timer, Default) { Timer timer; EXPECT_EQ(timer.ElapsedMicroSeconds(), 0); EXPECT_EQ(timer.ElapsedSeconds(), 0); EXPECT_EQ(timer.ElapsedMinutes(), 0); EXPECT_EQ(timer.ElapsedHours(), 0); } TEST(Timer, Start) { Timer timer; timer.Start(); EXPECT_GE(timer.ElapsedMicroSeconds(), 0); EXPECT_GE(timer.ElapsedSeconds(), 0); EXPECT_GE(timer.ElapsedMinutes(), 0); EXPECT_GE(timer.ElapsedHours(), 0); } TEST(Timer, Pause) { Timer timer; timer.Start(); timer.Pause(); double prev_time = timer.ElapsedMicroSeconds(); for (size_t i = 0; i < 1000; ++i) { EXPECT_EQ(timer.ElapsedMicroSeconds(), prev_time); prev_time = timer.ElapsedMicroSeconds(); } timer.Resume(); for (size_t i = 0; i < 1000; ++i) { EXPECT_GE(timer.ElapsedMicroSeconds(), prev_time); } timer.Reset(); EXPECT_EQ(timer.ElapsedMicroSeconds(), 0); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/types.h000066400000000000000000000121001454702036400173320ustar00rootroot00000000000000// 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. #pragma once #ifdef _MSC_VER #if _MSC_VER >= 1600 #include #else typedef __int8 int8_t; typedef __int16 int16_t; typedef __int32 int32_t; typedef __int64 int64_t; typedef unsigned __int8 uint8_t; typedef unsigned __int16 uint16_t; typedef unsigned __int32 uint32_t; typedef unsigned __int64 uint64_t; #endif #elif __GNUC__ >= 3 #include #endif // Define non-copyable or non-movable classes. // NOLINTNEXTLINE(bugprone-macro-parentheses) #define NON_COPYABLE(class_name) \ class_name(class_name const&) = delete; \ void operator=(class_name const& obj) = delete; // NOLINTNEXTLINE(bugprone-macro-parentheses) #define NON_MOVABLE(class_name) class_name(class_name&&) = delete; #include "colmap/util/eigen_alignment.h" #include namespace Eigen { typedef Eigen::Matrix Matrix3x4f; typedef Eigen::Matrix Matrix3x4d; typedef Eigen::Matrix Matrix6d; typedef Eigen::Matrix Vector3ub; typedef Eigen::Matrix Vector4ub; typedef Eigen::Matrix Vector6d; } // namespace Eigen namespace colmap { //////////////////////////////////////////////////////////////////////////////// // Index types, determines the maximum number of objects. //////////////////////////////////////////////////////////////////////////////// // Unique identifier for cameras. typedef uint32_t camera_t; // Unique identifier for images. typedef uint32_t image_t; // Each image pair gets a unique ID, see `Database::ImagePairToPairId`. typedef uint64_t image_pair_t; // Index per image, i.e. determines maximum number of 2D points per image. typedef uint32_t point2D_t; // Unique identifier per added 3D point. Since we add many 3D points, // delete them, and possibly re-add them again, the maximum number of allowed // unique indices should be large. typedef uint64_t point3D_t; // Values for invalid identifiers or indices. const camera_t kInvalidCameraId = std::numeric_limits::max(); const image_t kInvalidImageId = std::numeric_limits::max(); const image_pair_t kInvalidImagePairId = std::numeric_limits::max(); const point2D_t kInvalidPoint2DIdx = std::numeric_limits::max(); const point3D_t kInvalidPoint3DId = std::numeric_limits::max(); // Simple implementation of C++20's std::span, as Ubuntu 20.04's default GCC // version does not come with full C++20 and we still want to support it. template class span { T* ptr_; const size_t size_; public: span(T* ptr, size_t len) noexcept : ptr_{ptr}, size_{len} {} T& operator[](size_t i) noexcept { return ptr_[i]; } T const& operator[](size_t i) const noexcept { return ptr_[i]; } size_t size() const noexcept { return size_; } T* begin() noexcept { return ptr_; } T* end() noexcept { return ptr_ + size_; } const T* begin() const noexcept { return ptr_; } const T* end() const noexcept { return ptr_ + size_; } }; } // namespace colmap // This file provides specializations of the templated hash function for // custom types. These are used for comparison in unordered sets/maps. namespace std { // Hash function specialization for uint32_t pairs, e.g., image_t or camera_t. template <> struct hash> { std::size_t operator()(const std::pair& p) const { const uint64_t s = (static_cast(p.first) << 32) + static_cast(p.second); return std::hash()(s); } }; } // namespace std colmap-3.9.1/src/colmap/util/types_test.cc000066400000000000000000000044271454702036400205440ustar00rootroot00000000000000// 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. #include "colmap/util/types.h" #include #include namespace colmap { namespace { TEST(FeatureMatchHashing, Nominal) { std::unordered_set> set; set.emplace(1, 2); EXPECT_EQ(set.size(), 1); set.emplace(1, 2); EXPECT_EQ(set.size(), 1); EXPECT_EQ(set.count(std::make_pair(0, 0)), 0); EXPECT_EQ(set.count(std::make_pair(1, 2)), 1); EXPECT_EQ(set.count(std::make_pair(2, 1)), 0); set.emplace(2, 1); EXPECT_EQ(set.size(), 2); EXPECT_EQ(set.count(std::make_pair(0, 0)), 0); EXPECT_EQ(set.count(std::make_pair(1, 2)), 1); EXPECT_EQ(set.count(std::make_pair(2, 1)), 1); } } // namespace } // namespace colmap colmap-3.9.1/src/colmap/util/version.cc.in000066400000000000000000000045321454702036400204300ustar00rootroot00000000000000// 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. #include "colmap/util/version.h" #include "colmap/util/misc.h" namespace colmap { namespace { const std::string COLMAP_VERSION = "${COLMAP_VERSION}"; const std::string COLMAP_COMMIT_ID = "${GIT_COMMIT_ID}"; const std::string COLMAP_COMMIT_DATE = "${GIT_COMMIT_DATE}"; } // namespace std::string GetVersionInfo() { return StringPrintf("COLMAP %s", COLMAP_VERSION.c_str()); } std::string GetBuildInfo() { #if defined(COLMAP_CUDA_ENABLED) const std::string cuda_info = "with CUDA"; #else const std::string cuda_info = "without CUDA"; #endif return StringPrintf("Commit %s on %s %s", COLMAP_COMMIT_ID.c_str(), COLMAP_COMMIT_DATE.c_str(), cuda_info.c_str()); } } // namespace colmap colmap-3.9.1/src/colmap/util/version.h000066400000000000000000000033501454702036400176620ustar00rootroot00000000000000// 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. #pragma once #include namespace colmap { std::string GetVersionInfo(); std::string GetBuildInfo(); } // namespace colmap colmap-3.9.1/src/thirdparty/000077500000000000000000000000001454702036400157655ustar00rootroot00000000000000colmap-3.9.1/src/thirdparty/CMakeLists.txt000066400000000000000000000031021454702036400205210ustar00rootroot00000000000000set(FOLDER_NAME "lib") # Only show moderate warnings for external library code. if(IS_MSVC) if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]") string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(REMOVED_WARNING_LEVEL TRUE) elseif(CMAKE_CXX_FLAGS MATCHES "/Wall") string(REGEX REPLACE "/Wall" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(REMOVED_WARNING_LEVEL TRUE) endif() if(CMAKE_C_FLAGS MATCHES "/W[0-4]") string(REGEX REPLACE "/W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") set(REMOVED_WARNING_LEVEL TRUE) elseif(CMAKE_C_FLAGS MATCHES "/Wall") string(REGEX REPLACE "/Wall" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") set(REMOVED_WARNING_LEVEL TRUE) endif() elseif(IS_GNU OR IS_CLANG) if(CMAKE_CXX_FLAGS MATCHES "-Wall") string(REGEX REPLACE "-Wall" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(REMOVED_WARNING_LEVEL TRUE) endif() if(CMAKE_C_FLAGS MATCHES "-Wall") string(REGEX REPLACE "-Wall" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") set(REMOVED_WARNING_LEVEL TRUE) endif() endif() if(IS_MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W0") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W0") elseif(IS_GNU OR IS_CLANG) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") endif() set(CLANG_TIDY_EXE_TEMP ${CLANG_TIDY_EXE}) set(CLANG_TIDY_EXE "") add_subdirectory(LSD) add_subdirectory(PoissonRecon) if(GPU_ENABLED) add_subdirectory(SiftGPU) endif() add_subdirectory(VLFeat) set(CLANG_TIDY_EXE ${CLANG_TIDY_EXE_TEMP}) colmap-3.9.1/src/thirdparty/LSD/000077500000000000000000000000001454702036400164075ustar00rootroot00000000000000colmap-3.9.1/src/thirdparty/LSD/CMakeLists.txt000066400000000000000000000001071454702036400211450ustar00rootroot00000000000000COLMAP_ADD_LIBRARY( NAME colmap_lsd SRCS lsd.h lsd.c ) colmap-3.9.1/src/thirdparty/LSD/LICENSE000066400000000000000000001033301454702036400174140ustar00rootroot00000000000000 GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . colmap-3.9.1/src/thirdparty/LSD/lsd.c000066400000000000000000002304401454702036400173400ustar00rootroot00000000000000/*---------------------------------------------------------------------------- LSD - Line Segment Detector on digital images This code is part of the following publication and was subject to peer review: "LSD: a Line Segment Detector" by Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall, Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd Copyright (c) 2007-2011 rafael grompone von gioi This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . ----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** @file lsd.c LSD module code @author rafael grompone von gioi */ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** @mainpage LSD code documentation This is an implementation of the Line Segment Detector described in the paper: "LSD: A Fast Line Segment Detector with a False Detection Control" by Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall, IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 32, no. 4, pp. 722-732, April, 2010. and in more details in the CMLA Technical Report: "LSD: A Line Segment Detector, Technical Report", by Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel, Gregory Randall, CMLA, ENS Cachan, 2010. The version implemented here includes some further improvements described in the following publication, of which this code is part: "LSD: a Line Segment Detector" by Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall, Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd The module's main function is lsd(). The source code is contained in two files: lsd.h and lsd.c. HISTORY: - version 1.6 - nov 2011: - changes in the interface, - max_grad parameter removed, - the factor 11 was added to the number of test to consider the different precision values tested, - a minor bug corrected in the gradient sorting code, - the algorithm now also returns p and log_nfa for each detection, - a minor bug was corrected in the image scaling, - the angle comparison in "isaligned" changed from < to <=, - "eps" variable renamed "log_eps", - "lsd_scale_region" interface was added, - minor changes to comments. - version 1.5 - dec 2010: Changes in 'refine', -W option added, and more comments added. - version 1.4 - jul 2010: lsd_scale interface added and doxygen doc. - version 1.3 - feb 2010: Multiple bug correction and improved code. - version 1.2 - dec 2009: First full Ansi C Language version. - version 1.1 - sep 2009: Systematic subsampling to scale 0.8 and correction to partially handle "angle problem". - version 1.0 - jan 2009: First complete Megawave2 and Ansi C Language version. @author rafael grompone von gioi */ /*----------------------------------------------------------------------------*/ #include #include #include #include #include #include "lsd.h" /** ln(10) */ #ifndef M_LN10 #define M_LN10 2.30258509299404568402 #endif /* !M_LN10 */ /** PI */ #ifndef M_PI #define M_PI 3.14159265358979323846 #endif /* !M_PI */ #ifndef FALSE #define FALSE 0 #endif /* !FALSE */ #ifndef TRUE #define TRUE 1 #endif /* !TRUE */ /** Label for pixels with undefined gradient. */ #define NOTDEF -1024.0 /** 3/2 pi */ #define M_3_2_PI 4.71238898038 /** 2 pi */ #define M_2__PI 6.28318530718 /** Label for pixels not used in yet. */ #define NOTUSED 0 /** Label for pixels already used in detection. */ #define USED 1 /*----------------------------------------------------------------------------*/ /** Chained list of coordinates. */ struct coorlist { int x,y; struct coorlist * next; }; /*----------------------------------------------------------------------------*/ /** A point (or pixel). */ struct point {int x,y;}; /*----------------------------------------------------------------------------*/ /*------------------------- Miscellaneous functions --------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** Fatal error, print a message to standard-error output and exit. */ static void error(char * msg) { fprintf(stderr,"LSD Error: %s\n",msg); exit(EXIT_FAILURE); } /*----------------------------------------------------------------------------*/ /** Doubles relative error factor */ #define RELATIVE_ERROR_FACTOR 100.0 /*----------------------------------------------------------------------------*/ /** Compare doubles by relative error. The resulting rounding error after floating point computations depend on the specific operations done. The same number computed by different algorithms could present different rounding errors. For a useful comparison, an estimation of the relative rounding error should be considered and compared to a factor times EPS. The factor should be related to the cumulated rounding error in the chain of computation. Here, as a simplification, a fixed factor is used. */ static int double_equal(double a, double b) { double abs_diff,aa,bb,abs_max; /* trivial case */ if( a == b ) return TRUE; abs_diff = fabs(a-b); aa = fabs(a); bb = fabs(b); abs_max = aa > bb ? aa : bb; /* DBL_MIN is the smallest normalized number, thus, the smallest number whose relative error is bounded by DBL_EPSILON. For smaller numbers, the same quantization steps as for DBL_MIN are used. Then, for smaller numbers, a meaningful "relative" error should be computed by dividing the difference by DBL_MIN. */ if( abs_max < DBL_MIN ) abs_max = DBL_MIN; /* equal if relative error <= factor x eps */ return (abs_diff / abs_max) <= (RELATIVE_ERROR_FACTOR * DBL_EPSILON); } /*----------------------------------------------------------------------------*/ /** Computes Euclidean distance between point (x1,y1) and point (x2,y2). */ static double dist(double x1, double y1, double x2, double y2) { return sqrt( (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1) ); } /*----------------------------------------------------------------------------*/ /*----------------------- 'list of n-tuple' data type ------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** 'list of n-tuple' data type The i-th component of the j-th n-tuple of an n-tuple list 'ntl' is accessed with: ntl->values[ i + j * ntl->dim ] The dimension of the n-tuple (n) is: ntl->dim The number of n-tuples in the list is: ntl->size The maximum number of n-tuples that can be stored in the list with the allocated memory at a given time is given by: ntl->max_size */ typedef struct ntuple_list_s { unsigned int size; unsigned int max_size; unsigned int dim; double * values; } * ntuple_list; /*----------------------------------------------------------------------------*/ /** Free memory used in n-tuple 'in'. */ static void free_ntuple_list(ntuple_list in) { if( in == NULL || in->values == NULL ) error("free_ntuple_list: invalid n-tuple input."); free( (void *) in->values ); free( (void *) in ); } /*----------------------------------------------------------------------------*/ /** Create an n-tuple list and allocate memory for one element. @param dim the dimension (n) of the n-tuple. */ static ntuple_list new_ntuple_list(unsigned int dim) { ntuple_list n_tuple; /* check parameters */ if( dim == 0 ) error("new_ntuple_list: 'dim' must be positive."); /* get memory for list structure */ n_tuple = (ntuple_list) malloc( sizeof(struct ntuple_list_s) ); if( n_tuple == NULL ) error("not enough memory."); /* initialize list */ n_tuple->size = 0; n_tuple->max_size = 1; n_tuple->dim = dim; /* get memory for tuples */ n_tuple->values = (double *) malloc( dim*n_tuple->max_size * sizeof(double) ); if( n_tuple->values == NULL ) error("not enough memory."); return n_tuple; } /*----------------------------------------------------------------------------*/ /** Enlarge the allocated memory of an n-tuple list. */ static void enlarge_ntuple_list(ntuple_list n_tuple) { /* check parameters */ if( n_tuple == NULL || n_tuple->values == NULL || n_tuple->max_size == 0 ) error("enlarge_ntuple_list: invalid n-tuple."); /* duplicate number of tuples */ n_tuple->max_size *= 2; /* realloc memory */ n_tuple->values = (double *) realloc( (void *) n_tuple->values, n_tuple->dim * n_tuple->max_size * sizeof(double) ); if( n_tuple->values == NULL ) error("not enough memory."); } /*----------------------------------------------------------------------------*/ /** Add a 7-tuple to an n-tuple list. */ static void add_7tuple( ntuple_list out, double v1, double v2, double v3, double v4, double v5, double v6, double v7 ) { /* check parameters */ if( out == NULL ) error("add_7tuple: invalid n-tuple input."); if( out->dim != 7 ) error("add_7tuple: the n-tuple must be a 7-tuple."); /* if needed, alloc more tuples to 'out' */ if( out->size == out->max_size ) enlarge_ntuple_list(out); if( out->values == NULL ) error("add_7tuple: invalid n-tuple input."); /* add new 7-tuple */ out->values[ out->size * out->dim + 0 ] = v1; out->values[ out->size * out->dim + 1 ] = v2; out->values[ out->size * out->dim + 2 ] = v3; out->values[ out->size * out->dim + 3 ] = v4; out->values[ out->size * out->dim + 4 ] = v5; out->values[ out->size * out->dim + 5 ] = v6; out->values[ out->size * out->dim + 6 ] = v7; /* update number of tuples counter */ out->size++; } /*----------------------------------------------------------------------------*/ /*----------------------------- Image Data Types -----------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** char image data type The pixel value at (x,y) is accessed by: image->data[ x + y * image->xsize ] with x and y integer. */ typedef struct image_char_s { unsigned char * data; unsigned int xsize,ysize; } * image_char; /*----------------------------------------------------------------------------*/ /** Free memory used in image_char 'i'. */ static void free_image_char(image_char i) { if( i == NULL || i->data == NULL ) error("free_image_char: invalid input image."); free( (void *) i->data ); free( (void *) i ); } /*----------------------------------------------------------------------------*/ /** Create a new image_char of size 'xsize' times 'ysize'. */ static image_char new_image_char(unsigned int xsize, unsigned int ysize) { image_char image; /* check parameters */ if( xsize == 0 || ysize == 0 ) error("new_image_char: invalid image size."); /* get memory */ image = (image_char) malloc( sizeof(struct image_char_s) ); if( image == NULL ) error("not enough memory."); image->data = (unsigned char *) calloc( (size_t) (xsize*ysize), sizeof(unsigned char) ); if( image->data == NULL ) error("not enough memory."); /* set image size */ image->xsize = xsize; image->ysize = ysize; return image; } /*----------------------------------------------------------------------------*/ /** Create a new image_char of size 'xsize' times 'ysize', initialized to the value 'fill_value'. */ static image_char new_image_char_ini( unsigned int xsize, unsigned int ysize, unsigned char fill_value ) { image_char image = new_image_char(xsize,ysize); /* create image */ unsigned int N = xsize*ysize; unsigned int i; /* check parameters */ if( image == NULL || image->data == NULL ) error("new_image_char_ini: invalid image."); /* initialize */ for(i=0; idata[i] = fill_value; return image; } /*----------------------------------------------------------------------------*/ /** int image data type The pixel value at (x,y) is accessed by: image->data[ x + y * image->xsize ] with x and y integer. */ typedef struct image_int_s { int * data; unsigned int xsize,ysize; } * image_int; /*----------------------------------------------------------------------------*/ /** Create a new image_int of size 'xsize' times 'ysize'. */ static image_int new_image_int(unsigned int xsize, unsigned int ysize) { image_int image; /* check parameters */ if( xsize == 0 || ysize == 0 ) error("new_image_int: invalid image size."); /* get memory */ image = (image_int) malloc( sizeof(struct image_int_s) ); if( image == NULL ) error("not enough memory."); image->data = (int *) calloc( (size_t) (xsize*ysize), sizeof(int) ); if( image->data == NULL ) error("not enough memory."); /* set image size */ image->xsize = xsize; image->ysize = ysize; return image; } /*----------------------------------------------------------------------------*/ /** Create a new image_int of size 'xsize' times 'ysize', initialized to the value 'fill_value'. */ static image_int new_image_int_ini( unsigned int xsize, unsigned int ysize, int fill_value ) { image_int image = new_image_int(xsize,ysize); /* create image */ unsigned int N = xsize*ysize; unsigned int i; /* initialize */ for(i=0; idata[i] = fill_value; return image; } /*----------------------------------------------------------------------------*/ /** double image data type The pixel value at (x,y) is accessed by: image->data[ x + y * image->xsize ] with x and y integer. */ typedef struct image_double_s { double * data; unsigned int xsize,ysize; } * image_double; /*----------------------------------------------------------------------------*/ /** Free memory used in image_double 'i'. */ static void free_image_double(image_double i) { if( i == NULL || i->data == NULL ) error("free_image_double: invalid input image."); free( (void *) i->data ); free( (void *) i ); } /*----------------------------------------------------------------------------*/ /** Create a new image_double of size 'xsize' times 'ysize'. */ static image_double new_image_double(unsigned int xsize, unsigned int ysize) { image_double image; /* check parameters */ if( xsize == 0 || ysize == 0 ) error("new_image_double: invalid image size."); /* get memory */ image = (image_double) malloc( sizeof(struct image_double_s) ); if( image == NULL ) error("not enough memory."); image->data = (double *) calloc( (size_t) (xsize*ysize), sizeof(double) ); if( image->data == NULL ) error("not enough memory."); /* set image size */ image->xsize = xsize; image->ysize = ysize; return image; } /*----------------------------------------------------------------------------*/ /** Create a new image_double of size 'xsize' times 'ysize' with the data pointed by 'data'. */ static image_double new_image_double_ptr( unsigned int xsize, unsigned int ysize, double * data ) { image_double image; /* check parameters */ if( xsize == 0 || ysize == 0 ) error("new_image_double_ptr: invalid image size."); if( data == NULL ) error("new_image_double_ptr: NULL data pointer."); /* get memory */ image = (image_double) malloc( sizeof(struct image_double_s) ); if( image == NULL ) error("not enough memory."); /* set image */ image->xsize = xsize; image->ysize = ysize; image->data = data; return image; } /*----------------------------------------------------------------------------*/ /*----------------------------- Gaussian filter ------------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** Compute a Gaussian kernel of length 'kernel->dim', standard deviation 'sigma', and centered at value 'mean'. For example, if mean=0.5, the Gaussian will be centered in the middle point between values 'kernel->values[0]' and 'kernel->values[1]'. */ static void gaussian_kernel(ntuple_list kernel, double sigma, double mean) { double sum = 0.0; double val; unsigned int i; /* check parameters */ if( kernel == NULL || kernel->values == NULL ) error("gaussian_kernel: invalid n-tuple 'kernel'."); if( sigma <= 0.0 ) error("gaussian_kernel: 'sigma' must be positive."); /* compute Gaussian kernel */ if( kernel->max_size < 1 ) enlarge_ntuple_list(kernel); kernel->size = 1; for(i=0;idim;i++) { val = ( (double) i - mean ) / sigma; kernel->values[i] = exp( -0.5 * val * val ); sum += kernel->values[i]; } /* normalization */ if( sum >= 0.0 ) for(i=0;idim;i++) kernel->values[i] /= sum; } /*----------------------------------------------------------------------------*/ /** Scale the input image 'in' by a factor 'scale' by Gaussian sub-sampling. For example, scale=0.8 will give a result at 80% of the original size. The image is convolved with a Gaussian kernel @f[ G(x,y) = \frac{1}{2\pi\sigma^2} e^{-\frac{x^2+y^2}{2\sigma^2}} @f] before the sub-sampling to prevent aliasing. The standard deviation sigma given by: - sigma = sigma_scale / scale, if scale < 1.0 - sigma = sigma_scale, if scale >= 1.0 To be able to sub-sample at non-integer steps, some interpolation is needed. In this implementation, the interpolation is done by the Gaussian kernel, so both operations (filtering and sampling) are done at the same time. The Gaussian kernel is computed centered on the coordinates of the required sample. In this way, when applied, it gives directly the result of convolving the image with the kernel and interpolated to that particular position. A fast algorithm is done using the separability of the Gaussian kernel. Applying the 2D Gaussian kernel is equivalent to applying first a horizontal 1D Gaussian kernel and then a vertical 1D Gaussian kernel (or the other way round). The reason is that @f[ G(x,y) = G(x) * G(y) @f] where @f[ G(x) = \frac{1}{\sqrt{2\pi}\sigma} e^{-\frac{x^2}{2\sigma^2}}. @f] The algorithm first applies a combined Gaussian kernel and sampling in the x axis, and then the combined Gaussian kernel and sampling in the y axis. */ static image_double gaussian_sampler( image_double in, double scale, double sigma_scale ) { image_double aux,out; ntuple_list kernel; unsigned int N,M,h,n,x,y,i; int xc,yc,j,double_x_size,double_y_size; double sigma,xx,yy,sum,prec; /* check parameters */ if( in == NULL || in->data == NULL || in->xsize == 0 || in->ysize == 0 ) error("gaussian_sampler: invalid image."); if( scale <= 0.0 ) error("gaussian_sampler: 'scale' must be positive."); if( sigma_scale <= 0.0 ) error("gaussian_sampler: 'sigma_scale' must be positive."); /* compute new image size and get memory for images */ if( in->xsize * scale > (double) UINT_MAX || in->ysize * scale > (double) UINT_MAX ) error("gaussian_sampler: the output image size exceeds the handled size."); N = (unsigned int) ceil( in->xsize * scale ); M = (unsigned int) ceil( in->ysize * scale ); aux = new_image_double(N,in->ysize); out = new_image_double(N,M); /* sigma, kernel size and memory for the kernel */ sigma = scale < 1.0 ? sigma_scale / scale : sigma_scale; /* The size of the kernel is selected to guarantee that the the first discarded term is at least 10^prec times smaller than the central value. For that, h should be larger than x, with e^(-x^2/2sigma^2) = 1/10^prec. Then, x = sigma * sqrt( 2 * prec * ln(10) ). */ prec = 3.0; h = (unsigned int) ceil( sigma * sqrt( 2.0 * prec * log(10.0) ) ); n = 1+2*h; /* kernel size */ kernel = new_ntuple_list(n); /* auxiliary double image size variables */ double_x_size = (int) (2 * in->xsize); double_y_size = (int) (2 * in->ysize); /* First subsampling: x axis */ for(x=0;xxsize;x++) { /* x is the coordinate in the new image. xx is the corresponding x-value in the original size image. xc is the integer value, the pixel coordinate of xx. */ xx = (double) x / scale; /* coordinate (0.0,0.0) is in the center of pixel (0,0), so the pixel with xc=0 get the values of xx from -0.5 to 0.5 */ xc = (int) floor( xx + 0.5 ); gaussian_kernel( kernel, sigma, (double) h + xx - (double) xc ); /* the kernel must be computed for each x because the fine offset xx-xc is different in each case */ for(y=0;yysize;y++) { sum = 0.0; for(i=0;idim;i++) { j = xc - h + i; /* symmetry boundary condition */ while( j < 0 ) j += double_x_size; while( j >= double_x_size ) j -= double_x_size; if( j >= (int) in->xsize ) j = double_x_size-1-j; sum += in->data[ j + y * in->xsize ] * kernel->values[i]; } aux->data[ x + y * aux->xsize ] = sum; } } /* Second subsampling: y axis */ for(y=0;yysize;y++) { /* y is the coordinate in the new image. yy is the corresponding x-value in the original size image. yc is the integer value, the pixel coordinate of xx. */ yy = (double) y / scale; /* coordinate (0.0,0.0) is in the center of pixel (0,0), so the pixel with yc=0 get the values of yy from -0.5 to 0.5 */ yc = (int) floor( yy + 0.5 ); gaussian_kernel( kernel, sigma, (double) h + yy - (double) yc ); /* the kernel must be computed for each y because the fine offset yy-yc is different in each case */ for(x=0;xxsize;x++) { sum = 0.0; for(i=0;idim;i++) { j = yc - h + i; /* symmetry boundary condition */ while( j < 0 ) j += double_y_size; while( j >= double_y_size ) j -= double_y_size; if( j >= (int) in->ysize ) j = double_y_size-1-j; sum += aux->data[ x + j * aux->xsize ] * kernel->values[i]; } out->data[ x + y * out->xsize ] = sum; } } /* free memory */ free_ntuple_list(kernel); free_image_double(aux); return out; } /*----------------------------------------------------------------------------*/ /*--------------------------------- Gradient ---------------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** Computes the direction of the level line of 'in' at each point. The result is: - an image_double with the angle at each pixel, or NOTDEF if not defined. - the image_double 'modgrad' (a pointer is passed as argument) with the gradient magnitude at each point. - a list of pixels 'list_p' roughly ordered by decreasing gradient magnitude. (The order is made by classifying points into bins by gradient magnitude. The parameters 'n_bins' and 'max_grad' specify the number of bins and the gradient modulus at the highest bin. The pixels in the list would be in decreasing gradient magnitude, up to a precision of the size of the bins.) - a pointer 'mem_p' to the memory used by 'list_p' to be able to free the memory when it is not used anymore. */ static image_double ll_angle( image_double in, double threshold, struct coorlist ** list_p, void ** mem_p, image_double * modgrad, unsigned int n_bins ) { image_double g; unsigned int n,p,x,y,adr,i; double com1,com2,gx,gy,norm,norm2; /* the rest of the variables are used for pseudo-ordering the gradient magnitude values */ int list_count = 0; struct coorlist * list; struct coorlist ** range_l_s; /* array of pointers to start of bin list */ struct coorlist ** range_l_e; /* array of pointers to end of bin list */ struct coorlist * start; struct coorlist * end; double max_grad = 0.0; /* check parameters */ if( in == NULL || in->data == NULL || in->xsize == 0 || in->ysize == 0 ) error("ll_angle: invalid image."); if( threshold < 0.0 ) error("ll_angle: 'threshold' must be positive."); if( list_p == NULL ) error("ll_angle: NULL pointer 'list_p'."); if( mem_p == NULL ) error("ll_angle: NULL pointer 'mem_p'."); if( modgrad == NULL ) error("ll_angle: NULL pointer 'modgrad'."); if( n_bins == 0 ) error("ll_angle: 'n_bins' must be positive."); /* image size shortcuts */ n = in->ysize; p = in->xsize; /* allocate output image */ g = new_image_double(in->xsize,in->ysize); /* get memory for the image of gradient modulus */ *modgrad = new_image_double(in->xsize,in->ysize); /* get memory for "ordered" list of pixels */ list = (struct coorlist *) calloc( (size_t) (n*p), sizeof(struct coorlist) ); *mem_p = (void *) list; range_l_s = (struct coorlist **) calloc( (size_t) n_bins, sizeof(struct coorlist *) ); range_l_e = (struct coorlist **) calloc( (size_t) n_bins, sizeof(struct coorlist *) ); if( list == NULL || range_l_s == NULL || range_l_e == NULL ) error("not enough memory."); for(i=0;idata[(n-1)*p+x] = NOTDEF; for(y=0;ydata[p*y+p-1] = NOTDEF; /* compute gradient on the remaining pixels */ for(x=0;xdata[adr+p+1] - in->data[adr]; com2 = in->data[adr+1] - in->data[adr+p]; gx = com1+com2; /* gradient x component */ gy = com1-com2; /* gradient y component */ norm2 = gx*gx+gy*gy; norm = sqrt( norm2 / 4.0 ); /* gradient norm */ (*modgrad)->data[adr] = norm; /* store gradient norm */ if( norm <= threshold ) /* norm too small, gradient no defined */ g->data[adr] = NOTDEF; /* gradient angle not defined */ else { /* gradient angle computation */ g->data[adr] = atan2(gx,-gy); /* look for the maximum of the gradient */ if( norm > max_grad ) max_grad = norm; } } /* compute histogram of gradient values */ for(x=0;xdata[y*p+x]; /* store the point in the right bin according to its norm */ i = (unsigned int) (norm * (double) n_bins / max_grad); if( i >= n_bins ) i = n_bins-1; if( range_l_e[i] == NULL ) range_l_s[i] = range_l_e[i] = list+list_count++; else { range_l_e[i]->next = list+list_count; range_l_e[i] = list+list_count++; } range_l_e[i]->x = (int) x; range_l_e[i]->y = (int) y; range_l_e[i]->next = NULL; } /* Make the list of pixels (almost) ordered by norm value. It starts by the larger bin, so the list starts by the pixels with the highest gradient value. Pixels would be ordered by norm value, up to a precision given by max_grad/n_bins. */ for(i=n_bins-1; i>0 && range_l_s[i]==NULL; i--); start = range_l_s[i]; end = range_l_e[i]; if( start != NULL ) while(i>0) { --i; if( range_l_s[i] != NULL ) { end->next = range_l_s[i]; end = range_l_e[i]; } } *list_p = start; /* free memory */ free( (void *) range_l_s ); free( (void *) range_l_e ); return g; } /*----------------------------------------------------------------------------*/ /** Is point (x,y) aligned to angle theta, up to precision 'prec'? */ static int isaligned( int x, int y, image_double angles, double theta, double prec ) { double a; /* check parameters */ if( angles == NULL || angles->data == NULL ) error("isaligned: invalid image 'angles'."); if( x < 0 || y < 0 || x >= (int) angles->xsize || y >= (int) angles->ysize ) error("isaligned: (x,y) out of the image."); if( prec < 0.0 ) error("isaligned: 'prec' must be positive."); /* angle at pixel (x,y) */ a = angles->data[ x + y * angles->xsize ]; /* pixels whose level-line angle is not defined are considered as NON-aligned */ if( a == NOTDEF ) return FALSE; /* there is no need to call the function 'double_equal' here because there is no risk of problems related to the comparison doubles, we are only interested in the exact NOTDEF value */ /* it is assumed that 'theta' and 'a' are in the range [-pi,pi] */ theta -= a; if( theta < 0.0 ) theta = -theta; if( theta > M_3_2_PI ) { theta -= M_2__PI; if( theta < 0.0 ) theta = -theta; } return theta <= prec; } /*----------------------------------------------------------------------------*/ /** Absolute value angle difference. */ static double angle_diff(double a, double b) { a -= b; while( a <= -M_PI ) a += M_2__PI; while( a > M_PI ) a -= M_2__PI; if( a < 0.0 ) a = -a; return a; } /*----------------------------------------------------------------------------*/ /** Signed angle difference. */ static double angle_diff_signed(double a, double b) { a -= b; while( a <= -M_PI ) a += M_2__PI; while( a > M_PI ) a -= M_2__PI; return a; } /*----------------------------------------------------------------------------*/ /*----------------------------- NFA computation ------------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** Computes the natural logarithm of the absolute value of the gamma function of x using the Lanczos approximation. See http://www.rskey.org/gamma.htm The formula used is @f[ \Gamma(x) = \frac{ \sum_{n=0}^{N} q_n x^n }{ \Pi_{n=0}^{N} (x+n) } (x+5.5)^{x+0.5} e^{-(x+5.5)} @f] so @f[ \log\Gamma(x) = \log\left( \sum_{n=0}^{N} q_n x^n \right) + (x+0.5) \log(x+5.5) - (x+5.5) - \sum_{n=0}^{N} \log(x+n) @f] and q0 = 75122.6331530, q1 = 80916.6278952, q2 = 36308.2951477, q3 = 8687.24529705, q4 = 1168.92649479, q5 = 83.8676043424, q6 = 2.50662827511. */ static double log_gamma_lanczos(double x) { static double q[7] = { 75122.6331530, 80916.6278952, 36308.2951477, 8687.24529705, 1168.92649479, 83.8676043424, 2.50662827511 }; double a = (x+0.5) * log(x+5.5) - (x+5.5); double b = 0.0; int n; for(n=0;n<7;n++) { a -= log( x + (double) n ); b += q[n] * pow( x, (double) n ); } return a + log(b); } /*----------------------------------------------------------------------------*/ /** Computes the natural logarithm of the absolute value of the gamma function of x using Windschitl method. See http://www.rskey.org/gamma.htm The formula used is @f[ \Gamma(x) = \sqrt{\frac{2\pi}{x}} \left( \frac{x}{e} \sqrt{ x\sinh(1/x) + \frac{1}{810x^6} } \right)^x @f] so @f[ \log\Gamma(x) = 0.5\log(2\pi) + (x-0.5)\log(x) - x + 0.5x\log\left( x\sinh(1/x) + \frac{1}{810x^6} \right). @f] This formula is a good approximation when x > 15. */ static double log_gamma_windschitl(double x) { return 0.918938533204673 + (x-0.5)*log(x) - x + 0.5*x*log( x*sinh(1/x) + 1/(810.0*pow(x,6.0)) ); } /*----------------------------------------------------------------------------*/ /** Computes the natural logarithm of the absolute value of the gamma function of x. When x>15 use log_gamma_windschitl(), otherwise use log_gamma_lanczos(). */ #define log_gamma(x) ((x)>15.0?log_gamma_windschitl(x):log_gamma_lanczos(x)) /*----------------------------------------------------------------------------*/ /** Size of the table to store already computed inverse values. */ #define TABSIZE 100000 /*----------------------------------------------------------------------------*/ /** Computes -log10(NFA). NFA stands for Number of False Alarms: @f[ \mathrm{NFA} = NT \cdot B(n,k,p) @f] - NT - number of tests - B(n,k,p) - tail of binomial distribution with parameters n,k and p: @f[ B(n,k,p) = \sum_{j=k}^n \left(\begin{array}{c}n\\j\end{array}\right) p^{j} (1-p)^{n-j} @f] The value -log10(NFA) is equivalent but more intuitive than NFA: - -1 corresponds to 10 mean false alarms - 0 corresponds to 1 mean false alarm - 1 corresponds to 0.1 mean false alarms - 2 corresponds to 0.01 mean false alarms - ... Used this way, the bigger the value, better the detection, and a logarithmic scale is used. @param n,k,p binomial parameters. @param logNT logarithm of Number of Tests The computation is based in the gamma function by the following relation: @f[ \left(\begin{array}{c}n\\k\end{array}\right) = \frac{ \Gamma(n+1) }{ \Gamma(k+1) \cdot \Gamma(n-k+1) }. @f] We use efficient algorithms to compute the logarithm of the gamma function. To make the computation faster, not all the sum is computed, part of the terms are neglected based on a bound to the error obtained (an error of 10% in the result is accepted). */ static double nfa(int n, int k, double p, double logNT) { static double inv[TABSIZE]; /* table to keep computed inverse values */ double tolerance = 0.1; /* an error of 10% in the result is accepted */ double log1term,term,bin_term,mult_term,bin_tail,err,p_term; int i; /* check parameters */ if( n<0 || k<0 || k>n || p<=0.0 || p>=1.0 ) error("nfa: wrong n, k or p values."); /* trivial cases */ if( n==0 || k==0 ) return -logNT; if( n==k ) return -logNT - (double) n * log10(p); /* probability term */ p_term = p / (1.0-p); /* compute the first term of the series */ /* binomial_tail(n,k,p) = sum_{i=k}^n bincoef(n,i) * p^i * (1-p)^{n-i} where bincoef(n,i) are the binomial coefficients. But bincoef(n,k) = gamma(n+1) / ( gamma(k+1) * gamma(n-k+1) ). We use this to compute the first term. Actually the log of it. */ log1term = log_gamma( (double) n + 1.0 ) - log_gamma( (double) k + 1.0 ) - log_gamma( (double) (n-k) + 1.0 ) + (double) k * log(p) + (double) (n-k) * log(1.0-p); term = exp(log1term); /* in some cases no more computations are needed */ if( double_equal(term,0.0) ) /* the first term is almost zero */ { if( (double) k > (double) n * p ) /* at begin or end of the tail? */ return -log1term / M_LN10 - logNT; /* end: use just the first term */ else return -logNT; /* begin: the tail is roughly 1 */ } /* compute more terms if needed */ bin_tail = term; for(i=k+1;i<=n;i++) { /* As term_i = bincoef(n,i) * p^i * (1-p)^(n-i) and bincoef(n,i)/bincoef(n,i-1) = n-1+1 / i, then, term_i / term_i-1 = (n-i+1)/i * p/(1-p) and term_i = term_i-1 * (n-i+1)/i * p/(1-p). 1/i is stored in a table as they are computed, because divisions are expensive. p/(1-p) is computed only once and stored in 'p_term'. */ bin_term = (double) (n-i+1) * ( ii. Then, the error on the binomial tail when truncated at the i term can be bounded by a geometric series of form term_i * sum mult_term_i^j. */ err = term * ( ( 1.0 - pow( mult_term, (double) (n-i+1) ) ) / (1.0-mult_term) - 1.0 ); /* One wants an error at most of tolerance*final_result, or: tolerance * abs(-log10(bin_tail)-logNT). Now, the error that can be accepted on bin_tail is given by tolerance*final_result divided by the derivative of -log10(x) when x=bin_tail. that is: tolerance * abs(-log10(bin_tail)-logNT) / (1/bin_tail) Finally, we truncate the tail if the error is less than: tolerance * abs(-log10(bin_tail)-logNT) * bin_tail */ if( err < tolerance * fabs(-log10(bin_tail)-logNT) * bin_tail ) break; } } return -log10(bin_tail) - logNT; } /*----------------------------------------------------------------------------*/ /*--------------------------- Rectangle structure ----------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** Rectangle structure: line segment with width. */ struct rect { double x1,y1,x2,y2; /* first and second point of the line segment */ double width; /* rectangle width */ double x,y; /* center of the rectangle */ double theta; /* angle */ double dx,dy; /* (dx,dy) is vector oriented as the line segment */ double prec; /* tolerance angle */ double p; /* probability of a point with angle within 'prec' */ }; /*----------------------------------------------------------------------------*/ /** Copy one rectangle structure to another. */ static void rect_copy(struct rect * in, struct rect * out) { /* check parameters */ if( in == NULL || out == NULL ) error("rect_copy: invalid 'in' or 'out'."); /* copy values */ out->x1 = in->x1; out->y1 = in->y1; out->x2 = in->x2; out->y2 = in->y2; out->width = in->width; out->x = in->x; out->y = in->y; out->theta = in->theta; out->dx = in->dx; out->dy = in->dy; out->prec = in->prec; out->p = in->p; } /*----------------------------------------------------------------------------*/ /** Rectangle points iterator. The integer coordinates of pixels inside a rectangle are iteratively explored. This structure keep track of the process and functions ri_ini(), ri_inc(), ri_end(), and ri_del() are used in the process. An example of how to use the iterator is as follows: \code struct rect * rec = XXX; // some rectangle rect_iter * i; for( i=ri_ini(rec); !ri_end(i); ri_inc(i) ) { // your code, using 'i->x' and 'i->y' as coordinates } ri_del(i); // delete iterator \endcode The pixels are explored 'column' by 'column', where we call 'column' a set of pixels with the same x value that are inside the rectangle. The following is an schematic representation of a rectangle, the 'column' being explored is marked by colons, and the current pixel being explored is 'x,y'. \verbatim vx[1],vy[1] * * * * * * * ye * : * vx[0],vy[0] : * * : * * x,y * * : * * : vx[2],vy[2] * : * y ys * ^ * * | * * | * * +---> x vx[3],vy[3] \endverbatim The first 'column' to be explored is the one with the smaller x value. Each 'column' is explored starting from the pixel of the 'column' (inside the rectangle) with the smallest y value. The four corners of the rectangle are stored in order that rotates around the corners at the arrays 'vx[]' and 'vy[]'. The first point is always the one with smaller x value. 'x' and 'y' are the coordinates of the pixel being explored. 'ys' and 'ye' are the start and end values of the current column being explored. So, 'ys' < 'ye'. */ typedef struct { double vx[4]; /* rectangle's corner X coordinates in circular order */ double vy[4]; /* rectangle's corner Y coordinates in circular order */ double ys,ye; /* start and end Y values of current 'column' */ int x,y; /* coordinates of currently explored pixel */ } rect_iter; /*----------------------------------------------------------------------------*/ /** Interpolate y value corresponding to 'x' value given, in the line 'x1,y1' to 'x2,y2'; if 'x1=x2' return the smaller of 'y1' and 'y2'. The following restrictions are required: - x1 <= x2 - x1 <= x - x <= x2 */ static double inter_low(double x, double x1, double y1, double x2, double y2) { /* check parameters */ if( x1 > x2 || x < x1 || x > x2 ) error("inter_low: unsuitable input, 'x1>x2' or 'xx2'."); /* interpolation */ if( double_equal(x1,x2) && y1y2 ) return y2; return y1 + (x-x1) * (y2-y1) / (x2-x1); } /*----------------------------------------------------------------------------*/ /** Interpolate y value corresponding to 'x' value given, in the line 'x1,y1' to 'x2,y2'; if 'x1=x2' return the larger of 'y1' and 'y2'. The following restrictions are required: - x1 <= x2 - x1 <= x - x <= x2 */ static double inter_hi(double x, double x1, double y1, double x2, double y2) { /* check parameters */ if( x1 > x2 || x < x1 || x > x2 ) error("inter_hi: unsuitable input, 'x1>x2' or 'xx2'."); /* interpolation */ if( double_equal(x1,x2) && y1y2 ) return y1; return y1 + (x-x1) * (y2-y1) / (x2-x1); } /*----------------------------------------------------------------------------*/ /** Free memory used by a rectangle iterator. */ static void ri_del(rect_iter * iter) { if( iter == NULL ) error("ri_del: NULL iterator."); free( (void *) iter ); } /*----------------------------------------------------------------------------*/ /** Check if the iterator finished the full iteration. See details in \ref rect_iter */ static int ri_end(rect_iter * i) { /* check input */ if( i == NULL ) error("ri_end: NULL iterator."); /* if the current x value is larger than the largest x value in the rectangle (vx[2]), we know the full exploration of the rectangle is finished. */ return (double)(i->x) > i->vx[2]; } /*----------------------------------------------------------------------------*/ /** Increment a rectangle iterator. See details in \ref rect_iter */ static void ri_inc(rect_iter * i) { /* check input */ if( i == NULL ) error("ri_inc: NULL iterator."); /* if not at end of exploration, increase y value for next pixel in the 'column' */ if( !ri_end(i) ) i->y++; /* if the end of the current 'column' is reached, and it is not the end of exploration, advance to the next 'column' */ while( (double) (i->y) > i->ye && !ri_end(i) ) { /* increase x, next 'column' */ i->x++; /* if end of exploration, return */ if( ri_end(i) ) return; /* update lower y limit (start) for the new 'column'. We need to interpolate the y value that corresponds to the lower side of the rectangle. The first thing is to decide if the corresponding side is vx[0],vy[0] to vx[3],vy[3] or vx[3],vy[3] to vx[2],vy[2] Then, the side is interpolated for the x value of the 'column'. But, if the side is vertical (as it could happen if the rectangle is vertical and we are dealing with the first or last 'columns') then we pick the lower value of the side by using 'inter_low'. */ if( (double) i->x < i->vx[3] ) i->ys = inter_low((double)i->x,i->vx[0],i->vy[0],i->vx[3],i->vy[3]); else i->ys = inter_low((double)i->x,i->vx[3],i->vy[3],i->vx[2],i->vy[2]); /* update upper y limit (end) for the new 'column'. We need to interpolate the y value that corresponds to the upper side of the rectangle. The first thing is to decide if the corresponding side is vx[0],vy[0] to vx[1],vy[1] or vx[1],vy[1] to vx[2],vy[2] Then, the side is interpolated for the x value of the 'column'. But, if the side is vertical (as it could happen if the rectangle is vertical and we are dealing with the first or last 'columns') then we pick the lower value of the side by using 'inter_low'. */ if( (double)i->x < i->vx[1] ) i->ye = inter_hi((double)i->x,i->vx[0],i->vy[0],i->vx[1],i->vy[1]); else i->ye = inter_hi((double)i->x,i->vx[1],i->vy[1],i->vx[2],i->vy[2]); /* new y */ i->y = (int) ceil(i->ys); } } /*----------------------------------------------------------------------------*/ /** Create and initialize a rectangle iterator. See details in \ref rect_iter */ static rect_iter * ri_ini(struct rect * r) { double vx[4],vy[4]; int n,offset; rect_iter * i; /* check parameters */ if( r == NULL ) error("ri_ini: invalid rectangle."); /* get memory */ i = (rect_iter *) malloc(sizeof(rect_iter)); if( i == NULL ) error("ri_ini: Not enough memory."); /* build list of rectangle corners ordered in a circular way around the rectangle */ vx[0] = r->x1 - r->dy * r->width / 2.0; vy[0] = r->y1 + r->dx * r->width / 2.0; vx[1] = r->x2 - r->dy * r->width / 2.0; vy[1] = r->y2 + r->dx * r->width / 2.0; vx[2] = r->x2 + r->dy * r->width / 2.0; vy[2] = r->y2 - r->dx * r->width / 2.0; vx[3] = r->x1 + r->dy * r->width / 2.0; vy[3] = r->y1 - r->dx * r->width / 2.0; /* compute rotation of index of corners needed so that the first point has the smaller x. if one side is vertical, thus two corners have the same smaller x value, the one with the largest y value is selected as the first. */ if( r->x1 < r->x2 && r->y1 <= r->y2 ) offset = 0; else if( r->x1 >= r->x2 && r->y1 < r->y2 ) offset = 1; else if( r->x1 > r->x2 && r->y1 >= r->y2 ) offset = 2; else offset = 3; /* apply rotation of index. */ for(n=0; n<4; n++) { i->vx[n] = vx[(offset+n)%4]; i->vy[n] = vy[(offset+n)%4]; } /* Set an initial condition. The values are set to values that will cause 'ri_inc' (that will be called immediately) to initialize correctly the first 'column' and compute the limits 'ys' and 'ye'. 'y' is set to the integer value of vy[0], the starting corner. 'ys' and 'ye' are set to very small values, so 'ri_inc' will notice that it needs to start a new 'column'. The smallest integer coordinate inside of the rectangle is 'ceil(vx[0])'. The current 'x' value is set to that value minus one, so 'ri_inc' (that will increase x by one) will advance to the first 'column'. */ i->x = (int) ceil(i->vx[0]) - 1; i->y = (int) ceil(i->vy[0]); i->ys = i->ye = -DBL_MAX; /* advance to the first pixel */ ri_inc(i); return i; } /*----------------------------------------------------------------------------*/ /** Compute a rectangle's NFA value. */ static double rect_nfa(struct rect * rec, image_double angles, double logNT) { rect_iter * i; int pts = 0; int alg = 0; /* check parameters */ if( rec == NULL ) error("rect_nfa: invalid rectangle."); if( angles == NULL ) error("rect_nfa: invalid 'angles'."); /* compute the total number of pixels and of aligned points in 'rec' */ for(i=ri_ini(rec); !ri_end(i); ri_inc(i)) /* rectangle iterator */ if( i->x >= 0 && i->y >= 0 && i->x < (int) angles->xsize && i->y < (int) angles->ysize ) { ++pts; /* total number of pixels counter */ if( isaligned(i->x, i->y, angles, rec->theta, rec->prec) ) ++alg; /* aligned points counter */ } ri_del(i); /* delete iterator */ return nfa(pts,alg,rec->p,logNT); /* compute NFA value */ } /*----------------------------------------------------------------------------*/ /*---------------------------------- Regions ---------------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** Compute region's angle as the principal inertia axis of the region. The following is the region inertia matrix A: @f[ A = \left(\begin{array}{cc} Ixx & Ixy \\ Ixy & Iyy \\ \end{array}\right) @f] where Ixx = sum_i G(i).(y_i - cx)^2 Iyy = sum_i G(i).(x_i - cy)^2 Ixy = - sum_i G(i).(x_i - cx).(y_i - cy) and - G(i) is the gradient norm at pixel i, used as pixel's weight. - x_i and y_i are the coordinates of pixel i. - cx and cy are the coordinates of the center of th region. lambda1 and lambda2 are the eigenvalues of matrix A, with lambda1 >= lambda2. They are found by solving the characteristic polynomial: det( lambda I - A) = 0 that gives: lambda1 = ( Ixx + Iyy + sqrt( (Ixx-Iyy)^2 + 4.0*Ixy*Ixy) ) / 2 lambda2 = ( Ixx + Iyy - sqrt( (Ixx-Iyy)^2 + 4.0*Ixy*Ixy) ) / 2 To get the line segment direction we want to get the angle the eigenvector associated to the smallest eigenvalue. We have to solve for a,b in: a.Ixx + b.Ixy = a.lambda2 a.Ixy + b.Iyy = b.lambda2 We want the angle theta = atan(b/a). It can be computed with any of the two equations: theta = atan( (lambda2-Ixx) / Ixy ) or theta = atan( Ixy / (lambda2-Iyy) ) When |Ixx| > |Iyy| we use the first, otherwise the second (just to get better numeric precision). */ static double get_theta( struct point * reg, int reg_size, double x, double y, image_double modgrad, double reg_angle, double prec ) { double lambda,theta,weight; double Ixx = 0.0; double Iyy = 0.0; double Ixy = 0.0; int i; /* check parameters */ if( reg == NULL ) error("get_theta: invalid region."); if( reg_size <= 1 ) error("get_theta: region size <= 1."); if( modgrad == NULL || modgrad->data == NULL ) error("get_theta: invalid 'modgrad'."); if( prec < 0.0 ) error("get_theta: 'prec' must be positive."); /* compute inertia matrix */ for(i=0; idata[ reg[i].x + reg[i].y * modgrad->xsize ]; Ixx += ( (double) reg[i].y - y ) * ( (double) reg[i].y - y ) * weight; Iyy += ( (double) reg[i].x - x ) * ( (double) reg[i].x - x ) * weight; Ixy -= ( (double) reg[i].x - x ) * ( (double) reg[i].y - y ) * weight; } if( double_equal(Ixx,0.0) && double_equal(Iyy,0.0) && double_equal(Ixy,0.0) ) error("get_theta: null inertia matrix."); /* compute smallest eigenvalue */ lambda = 0.5 * ( Ixx + Iyy - sqrt( (Ixx-Iyy)*(Ixx-Iyy) + 4.0*Ixy*Ixy ) ); /* compute angle */ theta = fabs(Ixx)>fabs(Iyy) ? atan2(lambda-Ixx,Ixy) : atan2(Ixy,lambda-Iyy); /* The previous procedure doesn't cares about orientation, so it could be wrong by 180 degrees. Here is corrected if necessary. */ if( angle_diff(theta,reg_angle) > prec ) theta += M_PI; return theta; } /*----------------------------------------------------------------------------*/ /** Computes a rectangle that covers a region of points. */ static void region2rect( struct point * reg, int reg_size, image_double modgrad, double reg_angle, double prec, double p, struct rect * rec ) { double x,y,dx,dy,l,w,theta,weight,sum,l_min,l_max,w_min,w_max; int i; /* check parameters */ if( reg == NULL ) error("region2rect: invalid region."); if( reg_size <= 1 ) error("region2rect: region size <= 1."); if( modgrad == NULL || modgrad->data == NULL ) error("region2rect: invalid image 'modgrad'."); if( rec == NULL ) error("region2rect: invalid 'rec'."); /* center of the region: It is computed as the weighted sum of the coordinates of all the pixels in the region. The norm of the gradient is used as the weight of a pixel. The sum is as follows: cx = \sum_i G(i).x_i cy = \sum_i G(i).y_i where G(i) is the norm of the gradient of pixel i and x_i,y_i are its coordinates. */ x = y = sum = 0.0; for(i=0; idata[ reg[i].x + reg[i].y * modgrad->xsize ]; x += (double) reg[i].x * weight; y += (double) reg[i].y * weight; sum += weight; } if( sum <= 0.0 ) error("region2rect: weights sum equal to zero."); x /= sum; y /= sum; /* theta */ theta = get_theta(reg,reg_size,x,y,modgrad,reg_angle,prec); /* length and width: 'l' and 'w' are computed as the distance from the center of the region to pixel i, projected along the rectangle axis (dx,dy) and to the orthogonal axis (-dy,dx), respectively. The length of the rectangle goes from l_min to l_max, where l_min and l_max are the minimum and maximum values of l in the region. Analogously, the width is selected from w_min to w_max, where w_min and w_max are the minimum and maximum of w for the pixels in the region. */ dx = cos(theta); dy = sin(theta); l_min = l_max = w_min = w_max = 0.0; for(i=0; i l_max ) l_max = l; if( l < l_min ) l_min = l; if( w > w_max ) w_max = w; if( w < w_min ) w_min = w; } /* store values */ rec->x1 = x + l_min * dx; rec->y1 = y + l_min * dy; rec->x2 = x + l_max * dx; rec->y2 = y + l_max * dy; rec->width = w_max - w_min; rec->x = x; rec->y = y; rec->theta = theta; rec->dx = dx; rec->dy = dy; rec->prec = prec; rec->p = p; /* we impose a minimal width of one pixel A sharp horizontal or vertical step would produce a perfectly horizontal or vertical region. The width computed would be zero. But that corresponds to a one pixels width transition in the image. */ if( rec->width < 1.0 ) rec->width = 1.0; } /*----------------------------------------------------------------------------*/ /** Build a region of pixels that share the same angle, up to a tolerance 'prec', starting at point (x,y). */ static void region_grow( int x, int y, image_double angles, struct point * reg, int * reg_size, double * reg_angle, image_char used, double prec ) { double sumdx,sumdy; int xx,yy,i; /* check parameters */ if( x < 0 || y < 0 || x >= (int) angles->xsize || y >= (int) angles->ysize ) error("region_grow: (x,y) out of the image."); if( angles == NULL || angles->data == NULL ) error("region_grow: invalid image 'angles'."); if( reg == NULL ) error("region_grow: invalid 'reg'."); if( reg_size == NULL ) error("region_grow: invalid pointer 'reg_size'."); if( reg_angle == NULL ) error("region_grow: invalid pointer 'reg_angle'."); if( used == NULL || used->data == NULL ) error("region_grow: invalid image 'used'."); /* first point of the region */ *reg_size = 1; reg[0].x = x; reg[0].y = y; *reg_angle = angles->data[x+y*angles->xsize]; /* region's angle */ sumdx = cos(*reg_angle); sumdy = sin(*reg_angle); used->data[x+y*used->xsize] = USED; /* try neighbors as new region points */ for(i=0; i<*reg_size; i++) for(xx=reg[i].x-1; xx<=reg[i].x+1; xx++) for(yy=reg[i].y-1; yy<=reg[i].y+1; yy++) if( xx>=0 && yy>=0 && xx<(int)used->xsize && yy<(int)used->ysize && used->data[xx+yy*used->xsize] != USED && isaligned(xx,yy,angles,*reg_angle,prec) ) { /* add point */ used->data[xx+yy*used->xsize] = USED; reg[*reg_size].x = xx; reg[*reg_size].y = yy; ++(*reg_size); /* update region's angle */ sumdx += cos( angles->data[xx+yy*angles->xsize] ); sumdy += sin( angles->data[xx+yy*angles->xsize] ); *reg_angle = atan2(sumdy,sumdx); } } /*----------------------------------------------------------------------------*/ /** Try some rectangles variations to improve NFA value. Only if the rectangle is not meaningful (i.e., log_nfa <= log_eps). */ static double rect_improve( struct rect * rec, image_double angles, double logNT, double log_eps ) { struct rect r; double log_nfa,log_nfa_new; double delta = 0.5; double delta_2 = delta / 2.0; int n; log_nfa = rect_nfa(rec,angles,logNT); if( log_nfa > log_eps ) return log_nfa; /* try finer precisions */ rect_copy(rec,&r); for(n=0; n<5; n++) { r.p /= 2.0; r.prec = r.p * M_PI; log_nfa_new = rect_nfa(&r,angles,logNT); if( log_nfa_new > log_nfa ) { log_nfa = log_nfa_new; rect_copy(&r,rec); } } if( log_nfa > log_eps ) return log_nfa; /* try to reduce width */ rect_copy(rec,&r); for(n=0; n<5; n++) { if( (r.width - delta) >= 0.5 ) { r.width -= delta; log_nfa_new = rect_nfa(&r,angles,logNT); if( log_nfa_new > log_nfa ) { rect_copy(&r,rec); log_nfa = log_nfa_new; } } } if( log_nfa > log_eps ) return log_nfa; /* try to reduce one side of the rectangle */ rect_copy(rec,&r); for(n=0; n<5; n++) { if( (r.width - delta) >= 0.5 ) { r.x1 += -r.dy * delta_2; r.y1 += r.dx * delta_2; r.x2 += -r.dy * delta_2; r.y2 += r.dx * delta_2; r.width -= delta; log_nfa_new = rect_nfa(&r,angles,logNT); if( log_nfa_new > log_nfa ) { rect_copy(&r,rec); log_nfa = log_nfa_new; } } } if( log_nfa > log_eps ) return log_nfa; /* try to reduce the other side of the rectangle */ rect_copy(rec,&r); for(n=0; n<5; n++) { if( (r.width - delta) >= 0.5 ) { r.x1 -= -r.dy * delta_2; r.y1 -= r.dx * delta_2; r.x2 -= -r.dy * delta_2; r.y2 -= r.dx * delta_2; r.width -= delta; log_nfa_new = rect_nfa(&r,angles,logNT); if( log_nfa_new > log_nfa ) { rect_copy(&r,rec); log_nfa = log_nfa_new; } } } if( log_nfa > log_eps ) return log_nfa; /* try even finer precisions */ rect_copy(rec,&r); for(n=0; n<5; n++) { r.p /= 2.0; r.prec = r.p * M_PI; log_nfa_new = rect_nfa(&r,angles,logNT); if( log_nfa_new > log_nfa ) { log_nfa = log_nfa_new; rect_copy(&r,rec); } } return log_nfa; } /*----------------------------------------------------------------------------*/ /** Reduce the region size, by elimination the points far from the starting point, until that leads to rectangle with the right density of region points or to discard the region if too small. */ static int reduce_region_radius( struct point * reg, int * reg_size, image_double modgrad, double reg_angle, double prec, double p, struct rect * rec, image_char used, image_double angles, double density_th ) { double density,rad1,rad2,rad,xc,yc; int i; /* check parameters */ if( reg == NULL ) error("reduce_region_radius: invalid pointer 'reg'."); if( reg_size == NULL ) error("reduce_region_radius: invalid pointer 'reg_size'."); if( prec < 0.0 ) error("reduce_region_radius: 'prec' must be positive."); if( rec == NULL ) error("reduce_region_radius: invalid pointer 'rec'."); if( used == NULL || used->data == NULL ) error("reduce_region_radius: invalid image 'used'."); if( angles == NULL || angles->data == NULL ) error("reduce_region_radius: invalid image 'angles'."); /* compute region points density */ density = (double) *reg_size / ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width ); /* if the density criterion is satisfied there is nothing to do */ if( density >= density_th ) return TRUE; /* compute region's radius */ xc = (double) reg[0].x; yc = (double) reg[0].y; rad1 = dist( xc, yc, rec->x1, rec->y1 ); rad2 = dist( xc, yc, rec->x2, rec->y2 ); rad = rad1 > rad2 ? rad1 : rad2; /* while the density criterion is not satisfied, remove farther pixels */ while( density < density_th ) { rad *= 0.75; /* reduce region's radius to 75% of its value */ /* remove points from the region and update 'used' map */ for(i=0; i<*reg_size; i++) if( dist( xc, yc, (double) reg[i].x, (double) reg[i].y ) > rad ) { /* point not kept, mark it as NOTUSED */ used->data[ reg[i].x + reg[i].y * used->xsize ] = NOTUSED; /* remove point from the region */ reg[i].x = reg[*reg_size-1].x; /* if i==*reg_size-1 copy itself */ reg[i].y = reg[*reg_size-1].y; --(*reg_size); --i; /* to avoid skipping one point */ } /* reject if the region is too small. 2 is the minimal region size for 'region2rect' to work. */ if( *reg_size < 2 ) return FALSE; /* re-compute rectangle */ region2rect(reg,*reg_size,modgrad,reg_angle,prec,p,rec); /* re-compute region points density */ density = (double) *reg_size / ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width ); } /* if this point is reached, the density criterion is satisfied */ return TRUE; } /*----------------------------------------------------------------------------*/ /** Refine a rectangle. For that, an estimation of the angle tolerance is performed by the standard deviation of the angle at points near the region's starting point. Then, a new region is grown starting from the same point, but using the estimated angle tolerance. If this fails to produce a rectangle with the right density of region points, 'reduce_region_radius' is called to try to satisfy this condition. */ static int refine( struct point * reg, int * reg_size, image_double modgrad, double reg_angle, double prec, double p, struct rect * rec, image_char used, image_double angles, double density_th ) { double angle,ang_d,mean_angle,tau,density,xc,yc,ang_c,sum,s_sum; int i,n; /* check parameters */ if( reg == NULL ) error("refine: invalid pointer 'reg'."); if( reg_size == NULL ) error("refine: invalid pointer 'reg_size'."); if( prec < 0.0 ) error("refine: 'prec' must be positive."); if( rec == NULL ) error("refine: invalid pointer 'rec'."); if( used == NULL || used->data == NULL ) error("refine: invalid image 'used'."); if( angles == NULL || angles->data == NULL ) error("refine: invalid image 'angles'."); /* compute region points density */ density = (double) *reg_size / ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width ); /* if the density criterion is satisfied there is nothing to do */ if( density >= density_th ) return TRUE; /*------ First try: reduce angle tolerance ------*/ /* compute the new mean angle and tolerance */ xc = (double) reg[0].x; yc = (double) reg[0].y; ang_c = angles->data[ reg[0].x + reg[0].y * angles->xsize ]; sum = s_sum = 0.0; n = 0; for(i=0; i<*reg_size; i++) { used->data[ reg[i].x + reg[i].y * used->xsize ] = NOTUSED; if( dist( xc, yc, (double) reg[i].x, (double) reg[i].y ) < rec->width ) { angle = angles->data[ reg[i].x + reg[i].y * angles->xsize ]; ang_d = angle_diff_signed(angle,ang_c); sum += ang_d; s_sum += ang_d * ang_d; ++n; } } mean_angle = sum / (double) n; tau = 2.0 * sqrt( (s_sum - 2.0 * mean_angle * sum) / (double) n + mean_angle*mean_angle ); /* 2 * standard deviation */ /* find a new region from the same starting point and new angle tolerance */ region_grow(reg[0].x,reg[0].y,angles,reg,reg_size,®_angle,used,tau); /* if the region is too small, reject */ if( *reg_size < 2 ) return FALSE; /* re-compute rectangle */ region2rect(reg,*reg_size,modgrad,reg_angle,prec,p,rec); /* re-compute region points density */ density = (double) *reg_size / ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width ); /*------ Second try: reduce region radius ------*/ if( density < density_th ) return reduce_region_radius( reg, reg_size, modgrad, reg_angle, prec, p, rec, used, angles, density_th ); /* if this point is reached, the density criterion is satisfied */ return TRUE; } /*----------------------------------------------------------------------------*/ /*-------------------------- Line Segment Detector ---------------------------*/ /*----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** LSD full interface. */ double * LineSegmentDetection( int * n_out, double * img, int X, int Y, double scale, double sigma_scale, double quant, double ang_th, double log_eps, double density_th, int n_bins, int ** reg_img, int * reg_x, int * reg_y ) { image_double image; ntuple_list out = new_ntuple_list(7); double * return_value; image_double scaled_image,angles,modgrad; image_char used; image_int region = NULL; struct coorlist * list_p; void * mem_p; struct rect rec; struct point * reg; int reg_size,min_reg_size,i; unsigned int xsize,ysize; double rho,reg_angle,prec,p,log_nfa,logNT; int ls_count = 0; /* line segments are numbered 1,2,3,... */ /* check parameters */ if( img == NULL || X <= 0 || Y <= 0 ) error("invalid image input."); if( scale <= 0.0 ) error("'scale' value must be positive."); if( sigma_scale <= 0.0 ) error("'sigma_scale' value must be positive."); if( quant < 0.0 ) error("'quant' value must be positive."); if( ang_th <= 0.0 || ang_th >= 180.0 ) error("'ang_th' value must be in the range (0,180)."); if( density_th < 0.0 || density_th > 1.0 ) error("'density_th' value must be in the range [0,1]."); if( n_bins <= 0 ) error("'n_bins' value must be positive."); /* angle tolerance */ prec = M_PI * ang_th / 180.0; p = ang_th / 180.0; rho = quant / sin(prec); /* gradient magnitude threshold */ /* load and scale image (if necessary) and compute angle at each pixel */ image = new_image_double_ptr( (unsigned int) X, (unsigned int) Y, img ); if( scale != 1.0 ) { scaled_image = gaussian_sampler( image, scale, sigma_scale ); angles = ll_angle( scaled_image, rho, &list_p, &mem_p, &modgrad, (unsigned int) n_bins ); free_image_double(scaled_image); } else angles = ll_angle( image, rho, &list_p, &mem_p, &modgrad, (unsigned int) n_bins ); xsize = angles->xsize; ysize = angles->ysize; /* Number of Tests - NT The theoretical number of tests is Np.(XY)^(5/2) where X and Y are number of columns and rows of the image. Np corresponds to the number of angle precisions considered. As the procedure 'rect_improve' tests 5 times to halve the angle precision, and 5 more times after improving other factors, 11 different precision values are potentially tested. Thus, the number of tests is 11 * (X*Y)^(5/2) whose logarithm value is log10(11) + 5/2 * (log10(X) + log10(Y)). */ logNT = 5.0 * ( log10( (double) xsize ) + log10( (double) ysize ) ) / 2.0 + log10(11.0); min_reg_size = (int) (-logNT/log10(p)); /* minimal number of points in region that can give a meaningful event */ /* initialize some structures */ if( reg_img != NULL && reg_x != NULL && reg_y != NULL ) /* save region data */ region = new_image_int_ini(angles->xsize,angles->ysize,0); used = new_image_char_ini(xsize,ysize,NOTUSED); reg = (struct point *) calloc( (size_t) (xsize*ysize), sizeof(struct point) ); if( reg == NULL ) error("not enough memory!"); /* search for line segments */ for(; list_p != NULL; list_p = list_p->next ) if( used->data[ list_p->x + list_p->y * used->xsize ] == NOTUSED && angles->data[ list_p->x + list_p->y * angles->xsize ] != NOTDEF ) /* there is no risk of double comparison problems here because we are only interested in the exact NOTDEF value */ { /* find the region of connected point and ~equal angle */ region_grow( list_p->x, list_p->y, angles, reg, ®_size, ®_angle, used, prec ); /* reject small regions */ if( reg_size < min_reg_size ) continue; /* construct rectangular approximation for the region */ region2rect(reg,reg_size,modgrad,reg_angle,prec,p,&rec); /* Check if the rectangle exceeds the minimal density of region points. If not, try to improve the region. The rectangle will be rejected if the final one does not fulfill the minimal density condition. This is an addition to the original LSD algorithm published in "LSD: A Fast Line Segment Detector with a False Detection Control" by R. Grompone von Gioi, J. Jakubowicz, J.M. Morel, and G. Randall. The original algorithm is obtained with density_th = 0.0. */ if( !refine( reg, ®_size, modgrad, reg_angle, prec, p, &rec, used, angles, density_th ) ) continue; /* compute NFA value */ log_nfa = rect_improve(&rec,angles,logNT,log_eps); if( log_nfa <= log_eps ) continue; /* A New Line Segment was found! */ ++ls_count; /* increase line segment counter */ /* The gradient was computed with a 2x2 mask, its value corresponds to points with an offset of (0.5,0.5), that should be added to output. The coordinates origin is at the center of pixel (0,0). */ rec.x1 += 0.5; rec.y1 += 0.5; rec.x2 += 0.5; rec.y2 += 0.5; /* scale the result values if a subsampling was performed */ if( scale != 1.0 ) { rec.x1 /= scale; rec.y1 /= scale; rec.x2 /= scale; rec.y2 /= scale; rec.width /= scale; } /* add line segment found to output */ add_7tuple( out, rec.x1, rec.y1, rec.x2, rec.y2, rec.width, rec.p, log_nfa ); /* add region number to 'region' image if needed */ if( region != NULL ) for(i=0; idata[ reg[i].x + reg[i].y * region->xsize ] = ls_count; } /* free memory */ free( (void *) image ); /* only the double_image structure should be freed, the data pointer was provided to this functions and should not be destroyed. */ free_image_double(angles); free_image_double(modgrad); free_image_char(used); free( (void *) reg ); free( (void *) mem_p ); /* return the result */ if( reg_img != NULL && reg_x != NULL && reg_y != NULL ) { if( region == NULL ) error("'region' should be a valid image."); *reg_img = region->data; if( region->xsize > (unsigned int) INT_MAX || region->xsize > (unsigned int) INT_MAX ) error("region image to big to fit in INT sizes."); *reg_x = (int) (region->xsize); *reg_y = (int) (region->ysize); /* free the 'region' structure. we cannot use the function 'free_image_int' because we need to keep the memory with the image data to be returned by this function. */ free( (void *) region ); } if( out->size > (unsigned int) INT_MAX ) error("too many detections to fit in an INT."); *n_out = (int) (out->size); return_value = out->values; free( (void *) out ); /* only the 'ntuple_list' structure must be freed, but the 'values' pointer must be keep to return as a result. */ return return_value; } /*----------------------------------------------------------------------------*/ /** LSD Simple Interface with Scale and Region output. */ double * lsd_scale_region( int * n_out, double * img, int X, int Y, double scale, int ** reg_img, int * reg_x, int * reg_y ) { /* LSD parameters */ double sigma_scale = 0.6; /* Sigma for Gaussian filter is computed as sigma = sigma_scale/scale. */ double quant = 2.0; /* Bound to the quantization error on the gradient norm. */ double ang_th = 22.5; /* Gradient angle tolerance in degrees. */ double log_eps = 0.0; /* Detection threshold: -log10(NFA) > log_eps */ double density_th = 0.7; /* Minimal density of region points in rectangle. */ int n_bins = 1024; /* Number of bins in pseudo-ordering of gradient modulus. */ return LineSegmentDetection( n_out, img, X, Y, scale, sigma_scale, quant, ang_th, log_eps, density_th, n_bins, reg_img, reg_x, reg_y ); } /*----------------------------------------------------------------------------*/ /** LSD Simple Interface with Scale. */ double * lsd_scale(int * n_out, double * img, int X, int Y, double scale) { return lsd_scale_region(n_out,img,X,Y,scale,NULL,NULL,NULL); } /*----------------------------------------------------------------------------*/ /** LSD Simple Interface. */ double * lsd(int * n_out, double * img, int X, int Y) { /* LSD parameters */ double scale = 0.8; /* Scale the image by Gaussian filter to 'scale'. */ return lsd_scale(n_out,img,X,Y,scale); } /*----------------------------------------------------------------------------*/ colmap-3.9.1/src/thirdparty/LSD/lsd.h000066400000000000000000000344471454702036400173560ustar00rootroot00000000000000/*---------------------------------------------------------------------------- LSD - Line Segment Detector on digital images This code is part of the following publication and was subject to peer review: "LSD: a Line Segment Detector" by Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall, Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd Copyright (c) 2007-2011 rafael grompone von gioi This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . ----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------*/ /** @file lsd.h LSD module header @author rafael grompone von gioi */ /*----------------------------------------------------------------------------*/ #ifndef LSD_HEADER #define LSD_HEADER /*----------------------------------------------------------------------------*/ /** LSD Full Interface @param n_out Pointer to an int where LSD will store the number of line segments detected. @param img Pointer to input image data. It must be an array of doubles of size X x Y, and the pixel at coordinates (x,y) is obtained by img[x+y*X]. @param X X size of the image: the number of columns. @param Y Y size of the image: the number of rows. @param scale When different from 1.0, LSD will scale the input image by 'scale' factor by Gaussian filtering, before detecting line segments. Example: if scale=0.8, the input image will be subsampled to 80% of its size, before the line segment detector is applied. Suggested value: 0.8 @param sigma_scale When scale!=1.0, the sigma of the Gaussian filter is: sigma = sigma_scale / scale, if scale < 1.0 sigma = sigma_scale, if scale >= 1.0 Suggested value: 0.6 @param quant Bound to the quantization error on the gradient norm. Example: if gray levels are quantized to integer steps, the gradient (computed by finite differences) error due to quantization will be bounded by 2.0, as the worst case is when the error are 1 and -1, that gives an error of 2.0. Suggested value: 2.0 @param ang_th Gradient angle tolerance in the region growing algorithm, in degrees. Suggested value: 22.5 @param log_eps Detection threshold, accept if -log10(NFA) > log_eps. The larger the value, the more strict the detector is, and will result in less detections. (Note that the 'minus sign' makes that this behavior is opposite to the one of NFA.) The value -log10(NFA) is equivalent but more intuitive than NFA: - -1.0 gives an average of 10 false detections on noise - 0.0 gives an average of 1 false detections on noise - 1.0 gives an average of 0.1 false detections on nose - 2.0 gives an average of 0.01 false detections on noise . Suggested value: 0.0 @param density_th Minimal proportion of 'supporting' points in a rectangle. Suggested value: 0.7 @param n_bins Number of bins used in the pseudo-ordering of gradient modulus. Suggested value: 1024 @param reg_img Optional output: if desired, LSD will return an int image where each pixel indicates the line segment to which it belongs. Unused pixels have the value '0', while the used ones have the number of the line segment, numbered 1,2,3,..., in the same order as in the output list. If desired, a non NULL int** pointer must be assigned, and LSD will make that the pointer point to an int array of size reg_x x reg_y, where the pixel value at (x,y) is obtained with (*reg_img)[x+y*reg_x]. Note that the resulting image has the size of the image used for the processing, that is, the size of the input image scaled by the given factor 'scale'. If scale!=1 this size differs from XxY and that is the reason why its value is given by reg_x and reg_y. Suggested value: NULL @param reg_x Pointer to an int where LSD will put the X size 'reg_img' image, when asked for. Suggested value: NULL @param reg_y Pointer to an int where LSD will put the Y size 'reg_img' image, when asked for. Suggested value: NULL @return A double array of size 7 x n_out, containing the list of line segments detected. The array contains first 7 values of line segment number 1, then the 7 values of line segment number 2, and so on, and it finish by the 7 values of line segment number n_out. The seven values are: - x1,y1,x2,y2,width,p,-log10(NFA) . for a line segment from coordinates (x1,y1) to (x2,y2), a width 'width', an angle precision of p in (0,1) given by angle_tolerance/180 degree, and NFA value 'NFA'. If 'out' is the returned pointer, the 7 values of line segment number 'n+1' are obtained with 'out[7*n+0]' to 'out[7*n+6]'. */ double * LineSegmentDetection( int * n_out, double * img, int X, int Y, double scale, double sigma_scale, double quant, double ang_th, double log_eps, double density_th, int n_bins, int ** reg_img, int * reg_x, int * reg_y ); /*----------------------------------------------------------------------------*/ /** LSD Simple Interface with Scale and Region output. @param n_out Pointer to an int where LSD will store the number of line segments detected. @param img Pointer to input image data. It must be an array of doubles of size X x Y, and the pixel at coordinates (x,y) is obtained by img[x+y*X]. @param X X size of the image: the number of columns. @param Y Y size of the image: the number of rows. @param scale When different from 1.0, LSD will scale the input image by 'scale' factor by Gaussian filtering, before detecting line segments. Example: if scale=0.8, the input image will be subsampled to 80% of its size, before the line segment detector is applied. Suggested value: 0.8 @param reg_img Optional output: if desired, LSD will return an int image where each pixel indicates the line segment to which it belongs. Unused pixels have the value '0', while the used ones have the number of the line segment, numbered 1,2,3,..., in the same order as in the output list. If desired, a non NULL int** pointer must be assigned, and LSD will make that the pointer point to an int array of size reg_x x reg_y, where the pixel value at (x,y) is obtained with (*reg_img)[x+y*reg_x]. Note that the resulting image has the size of the image used for the processing, that is, the size of the input image scaled by the given factor 'scale'. If scale!=1 this size differs from XxY and that is the reason why its value is given by reg_x and reg_y. Suggested value: NULL @param reg_x Pointer to an int where LSD will put the X size 'reg_img' image, when asked for. Suggested value: NULL @param reg_y Pointer to an int where LSD will put the Y size 'reg_img' image, when asked for. Suggested value: NULL @return A double array of size 7 x n_out, containing the list of line segments detected. The array contains first 7 values of line segment number 1, then the 7 values of line segment number 2, and so on, and it finish by the 7 values of line segment number n_out. The seven values are: - x1,y1,x2,y2,width,p,-log10(NFA) . for a line segment from coordinates (x1,y1) to (x2,y2), a width 'width', an angle precision of p in (0,1) given by angle_tolerance/180 degree, and NFA value 'NFA'. If 'out' is the returned pointer, the 7 values of line segment number 'n+1' are obtained with 'out[7*n+0]' to 'out[7*n+6]'. */ double * lsd_scale_region( int * n_out, double * img, int X, int Y, double scale, int ** reg_img, int * reg_x, int * reg_y ); /*----------------------------------------------------------------------------*/ /** LSD Simple Interface with Scale @param n_out Pointer to an int where LSD will store the number of line segments detected. @param img Pointer to input image data. It must be an array of doubles of size X x Y, and the pixel at coordinates (x,y) is obtained by img[x+y*X]. @param X X size of the image: the number of columns. @param Y Y size of the image: the number of rows. @param scale When different from 1.0, LSD will scale the input image by 'scale' factor by Gaussian filtering, before detecting line segments. Example: if scale=0.8, the input image will be subsampled to 80% of its size, before the line segment detector is applied. Suggested value: 0.8 @return A double array of size 7 x n_out, containing the list of line segments detected. The array contains first 7 values of line segment number 1, then the 7 values of line segment number 2, and so on, and it finish by the 7 values of line segment number n_out. The seven values are: - x1,y1,x2,y2,width,p,-log10(NFA) . for a line segment from coordinates (x1,y1) to (x2,y2), a width 'width', an angle precision of p in (0,1) given by angle_tolerance/180 degree, and NFA value 'NFA'. If 'out' is the returned pointer, the 7 values of line segment number 'n+1' are obtained with 'out[7*n+0]' to 'out[7*n+6]'. */ double * lsd_scale(int * n_out, double * img, int X, int Y, double scale); /*----------------------------------------------------------------------------*/ /** LSD Simple Interface @param n_out Pointer to an int where LSD will store the number of line segments detected. @param img Pointer to input image data. It must be an array of doubles of size X x Y, and the pixel at coordinates (x,y) is obtained by img[x+y*X]. @param X X size of the image: the number of columns. @param Y Y size of the image: the number of rows. @return A double array of size 7 x n_out, containing the list of line segments detected. The array contains first 7 values of line segment number 1, then the 7 values of line segment number 2, and so on, and it finish by the 7 values of line segment number n_out. The seven values are: - x1,y1,x2,y2,width,p,-log10(NFA) . for a line segment from coordinates (x1,y1) to (x2,y2), a width 'width', an angle precision of p in (0,1) given by angle_tolerance/180 degree, and NFA value 'NFA'. If 'out' is the returned pointer, the 7 values of line segment number 'n+1' are obtained with 'out[7*n+0]' to 'out[7*n+6]'. */ double * lsd(int * n_out, double * img, int X, int Y); #endif /* !LSD_HEADER */ /*----------------------------------------------------------------------------*/ colmap-3.9.1/src/thirdparty/PoissonRecon/000077500000000000000000000000001454702036400204065ustar00rootroot00000000000000colmap-3.9.1/src/thirdparty/PoissonRecon/Allocator.h000066400000000000000000000131101454702036400224730ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #ifndef ALLOCATOR_INCLUDED #define ALLOCATOR_INCLUDED #include class AllocatorState{ public: int index,remains; }; /** This templated class assists in memory allocation and is well suited for instances * when it is known that the sequence of memory allocations is performed in a stack-based * manner, so that memory allocated last is released first. It also preallocates memory * in chunks so that multiple requests for small chunks of memory do not require separate * system calls to the memory manager. * The allocator is templated off of the class of objects that we would like it to allocate, * ensuring that appropriate constructors and destructors are called as necessary. */ template class Allocator { int blockSize; int index , remains; std::vector< T* > memory; public: Allocator( void ){ blockSize = index = remains = 0; } ~Allocator( void ){ reset(); } /** This method is the allocators destructor. It frees up any of the memory that * it has allocated. */ void reset( void ) { for(size_t i=0;iblockSize = blockSize; index=-1; remains=0; } /** This method returns a pointer to an array of elements objects. If there is left over pre-allocated * memory, this method simply returns a pointer to the next free piece of memory, otherwise it pre-allocates * more memory. Note that if the number of objects requested is larger than the value blockSize with which * the allocator was initialized, the request for memory will fail. */ T* newElements( int elements=1 ) { T* mem; if( !elements ) return NULL; if( elements>blockSize ) fprintf( stderr , "[ERROR] Allocator: elements bigger than block-size: %d>%d\n" , elements , blockSize ) , exit( 0 ); if( remains #define ARRAY_DEBUG 0 #ifdef _WIN64 #define ASSERT( x ) { if( !( x ) ) __debugbreak(); } #else // !_WIN64 #ifdef _WIN32 #define ASSERT( x ) { if( !( x ) ) _asm{ int 0x03 } } #else // !_WIN32 #define ASSERT( x ) { if( !( x ) ) exit(0); } #endif // _WIN32 #endif // _WIN64 // Code from http://stackoverflow.com void* aligned_malloc( size_t size , size_t align ) { // Align enough for the data, the alignment padding, and room to store a pointer to the actual start of the memory void* mem = malloc( size + align + sizeof( void* ) ); // The position at which we could potentially start addressing char* amem = ( (char*)mem ) + sizeof( void* ); // Add align-1 to the start of the address and then zero out at most of the first align-1 bits. amem = ( char* )( ( (size_t)( ( (char*)amem ) + (align-1) ) ) & ~( align-1 ) ); // Pre-write the actual address ( ( void** ) amem )[-1] = mem; return amem; } void aligned_free( void* mem ) { free( ( ( void** )mem )[-1] ); } #if ARRAY_DEBUG #pragma message ( "[WARNING] Array debugging is enabled" ) #include "Array.inl" #define Pointer( ... ) Array< __VA_ARGS__ > #define ConstPointer( ... ) ConstArray< __VA_ARGS__ > #define NullPointer( ... ) Array< __VA_ARGS__ >() template< class C > void FreePointer( Array< C >& a ){ a.Free( ); } template< class C > void AlignedFreePointer( Array< C >& a ){ a.Free( ); } template< class C > void VFreePointer( Array< C >& a ){ a.Free( ); } template< class C > void DeletePointer( Array< C >& a ){ a.Delete( ); } template< class C > Array< C > NewPointer( size_t size , const char* name=NULL ){ return Array< C >::New ( size , name ); } template< class C > Array< C > AllocPointer( size_t size , const char* name=NULL ){ return Array< C >::Alloc ( size , false , name ); } template< class C > Array< C > AlignedAllocPointer( size_t size , size_t alignment , const char* name=NULL ){ return Array< C >::AlignedAlloc( size , alignment , false , name ); } template< class C > Array< C > ReAllocPointer( Array< C >& a , size_t size , const char* name=NULL ){ return Array< C >::ReAlloc ( a , size , false , name ); } template< class C > C* PointerAddress( Array< C >& a ) { return a.pointer(); } template< class C > const C* PointerAddress( ConstArray< C >& a ) { return a.pointer(); } template< class C > Array< C > GetPointer( C& c ) { return Array< C >::FromPointer( &c , 1 ); } template< class C > ConstArray< C > GetPointer( const C& c ) { return ConstArray< C >::FromPointer( &c , 1 ); } template< class C > Array< C > GetPointer( std::vector< C >& v ){ return Array< C >::FromPointer( &v[0] , v.size() ); } template< class C > ConstArray< C > GetPointer( const std::vector< C >& v ){ return ConstArray< C >::FromPointer( &v[0] , v.size() ); } #else // !ARRAY_DEBUG #define Pointer( ... ) __VA_ARGS__* #define ConstPointer( ... ) const __VA_ARGS__* #define NullPointer( ... ) NULL #define FreePointer( ... ) { if( __VA_ARGS__ ) free( __VA_ARGS__ ) , __VA_ARGS__ = NULL; } #define AlignedFreePointer( ... ) { if( __VA_ARGS__ ) aligned_free( __VA_ARGS__ ) , __VA_ARGS__ = NULL; } #define DeletePointer( ... ) { if( __VA_ARGS__ ) delete[] __VA_ARGS__ , __VA_ARGS__ = NULL; } template< class C > C* NewPointer( size_t size , const char* name=NULL ){ return new C[size]; } template< class C > C* AllocPointer( size_t size , const char* name=NULL ){ return (C*) malloc( sizeof(C) * size ); } template< class C > C* AlignedAllocPointer( size_t size , size_t alignment , const char* name=NULL ){ return (C*)aligned_malloc( sizeof(C) * size , alignment ); } template< class C > C* ReAllocPointer( C* c , size_t size , const char* name=NULL ){ return (C*) realloc( c , sizeof(C) * size ); } //template< class C > C* NullPointer( void ){ return NULL; } template< class C > C* PointerAddress( C* c ){ return c; } template< class C > const C* PointerAddress( const C* c ){ return c; } template< class C > C* GetPointer( C& c ){ return &c; } template< class C > const C* GetPointer( const C& c ){ return &c; } template< class C > C* GetPointer( std::vector< C >& v ){ return &v[0]; } template< class C > const C* GetPointer( const std::vector< C >& v ){ return &v[0]; } #endif // ARRAY_DEBUG #endif // ARRAY_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/Array.inl000077500000000000000000000467621454702036400222120ustar00rootroot00000000000000/* Copyright (c) 2011, Michael Kazhdan and Ming Chuang 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 the Johns Hopkins University 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 OWNER 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. */ #define FULL_ARRAY_DEBUG 0 // Note that this is not thread-safe #include #include #include #ifdef _WIN32 #include #endif // _WIN32 #include inline bool isfinitef( float fp ){ float f=fp; return ((*(unsigned *)&f)&0x7f800000)!=0x7f800000; } template< class C > bool IsValid( const C& c ); #if _DEBUG template< > inline bool IsValid< float >( const float& f ) { return isfinitef( f ) && ( f==0.f || abs(f)>1e-31f ); } #else // !_DEBUG template< > inline bool IsValid< float >( const float& f ) { return isfinitef( f ); } #endif // _DEBUG template< > inline bool IsValid< __m128 >( const __m128& m ) { const __m128* addr = &m; if( size_t(addr) & 15 ) return false; else return true; } template< class C > inline bool IsValid( const C& c ){ return true; } #if FULL_ARRAY_DEBUG class DebugMemoryInfo { public: const void* address; char name[512]; }; static std::vector< DebugMemoryInfo > memoryInfo; #endif // FULL_ARRAY_DEBUG template< class C > class Array { void _assertBounds( long long idx ) const { if( idx=max ) { fprintf( stderr , "Array index out-of-bounds: %lld <= %lld < %lld\n" , min , idx , max ); ASSERT( 0 ); exit( 0 ); } } protected: C *data , *_data; long long min , max; #if FULL_ARRAY_DEBUG static void _AddMemoryInfo( const void* ptr , const char* name ) { size_t sz = memoryInfo.size(); memoryInfo.resize( sz + 1 ); memoryInfo[sz].address = ptr; if( name ) strcpy( memoryInfo[sz].name , name ); else memoryInfo[sz].name[0] = 0; } static void _RemoveMemoryInfo( const void* ptr ) { { size_t idx; for( idx=0 ; idx Array( Array< D >& a ) { _data = NULL; if( !a ) { data = NULL; min = max = 0; } else { // [WARNING] Chaning szC and szD to size_t causes some really strange behavior. long long szC = sizeof( C ); long long szD = sizeof( D ); data = (C*)a.data; min = ( a.minimum() * szD ) / szC; max = ( a.maximum() * szD ) / szC; if( min*szC!=a.minimum()*szD || max*szC!=a.maximum()*szD ) { fprintf( stderr , "Could not convert array [ %lld , %lld ] * %lld => [ %lld , %lld ] * %lld\n" , a.minimum() , a.maximum() , szD , min , max , szC ); ASSERT( 0 ); exit( 0 ); } } } static Array FromPointer( C* data , long long max ) { Array a; a._data = NULL; a.data = data; a.min = 0; a.max = max; return a; } static Array FromPointer( C* data , long long min , long long max ) { Array a; a._data = NULL; a.data = data; a.min = min; a.max = max; return a; } inline bool operator == ( const Array< C >& a ) const { return data==a.data; } inline bool operator != ( const Array< C >& a ) const { return data!=a.data; } inline bool operator == ( const C* c ) const { return data==c; } inline bool operator != ( const C* c ) const { return data!=c; } inline C* operator -> ( void ) { _assertBounds( 0 ); return data; } inline const C* operator -> ( ) const { _assertBounds( 0 ); return data; } inline C& operator[]( long long idx ) { _assertBounds( idx ); return data[idx]; } inline const C& operator[]( long long idx ) const { _assertBounds( idx ); return data[idx]; } inline Array operator + ( int idx ) const { Array a; a._data = _data; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline Array operator + ( long long idx ) const { Array a; a._data = _data; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline Array operator + ( unsigned int idx ) const { Array a; a._data = _data; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline Array operator + ( unsigned long long idx ) const { Array a; a._data = _data; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline Array& operator += ( int idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline Array& operator += ( long long idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline Array& operator += ( unsigned int idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline Array& operator += ( unsigned long long idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline Array& operator ++ ( void ) { return (*this) += 1; } inline Array operator++( int ){ Array< C > temp = (*this) ; (*this) +=1 ; return temp; } Array operator - ( int idx ) const { return (*this) + (-idx); } Array operator - ( long long idx ) const { return (*this) + (-idx); } Array operator - ( unsigned int idx ) const { return (*this) + (-idx); } Array operator - ( unsigned long long idx ) const { return (*this) + (-idx); } Array& operator -= ( int idx ) { return (*this) += (-idx); } Array& operator -= ( long long idx ) { return (*this) += (-idx); } Array& operator -= ( unsigned int idx ) { return (*this) += (-idx); } Array& operator -= ( unsigned long long idx ) { return (*this) += (-idx); } Array& operator -- ( void ) { return (*this) -= 1; } inline Array operator--( int ){ Array< C > temp = (*this) ; (*this) -=1 ; return temp; } long long operator - ( const Array& a ) const { return ( long long )( data - a.data ); } void Free( void ) { if( _data ) { free( _data ); #if FULL_ARRAY_DEBUG _RemoveMemoryInfo( _data ); #endif // FULL_ARRAY_DEBUG } (*this) = Array( ); } void Delete( void ) { if( _data ) { delete[] _data; #if FULL_ARRAY_DEBUG _RemoveMemoryInfo( _data ); #endif // FULL_ARRAY_DEBUG } (*this) = Array( ); } C* pointer( void ){ return data; } const C* pointer( void ) const { return data; } bool operator !( void ) const { return data==NULL; } operator bool( ) const { return data!=NULL; } }; template< class C > class ConstArray { void _assertBounds( long long idx ) const { if( idx=max ) { fprintf( stderr , "ConstArray index out-of-bounds: %lld <= %lld < %lld\n" , min , idx , max ); ASSERT( 0 ); exit( 0 ); } } protected: const C *data; long long min , max; public: long long minimum( void ) const { return min; } long long maximum( void ) const { return max; } inline ConstArray( void ) { data = NULL; min = max = 0; } inline ConstArray( const Array< C >& a ) { // [WARNING] Changing szC and szD to size_t causes some really strange behavior. data = ( const C* )a.pointer( ); min = a.minimum(); max = a.maximum(); } template< class D > inline ConstArray( const Array< D >& a ) { // [WARNING] Changing szC and szD to size_t causes some really strange behavior. long long szC = ( long long ) sizeof( C ); long long szD = ( long long ) sizeof( D ); data = ( const C* )a.pointer( ); min = ( a.minimum() * szD ) / szC; max = ( a.maximum() * szD ) / szC; if( min*szC!=a.minimum()*szD || max*szC!=a.maximum()*szD ) { // fprintf( stderr , "Could not convert const array [ %lld , %lld ] * %lld => [ %lld , %lld ] * %lld\n" , a.minimum() , a.maximum() , szD , min , max , szC ); fprintf( stderr , "Could not convert const array [ %lld , %lld ] * %lld => [ %lld , %lld ] * %lld\n %lld %lld %lld\n" , a.minimum() , a.maximum() , szD , min , max , szC , a.minimum() , a.minimum()*szD , (a.minimum()*szD)/szC ); ASSERT( 0 ); exit( 0 ); } } template< class D > inline ConstArray( const ConstArray< D >& a ) { // [WARNING] Chaning szC and szD to size_t causes some really strange behavior. long long szC = sizeof( C ); long long szD = sizeof( D ); data = ( const C*)a.pointer( ); min = ( a.minimum() * szD ) / szC; max = ( a.maximum() * szD ) / szC; if( min*szC!=a.minimum()*szD || max*szC!=a.maximum()*szD ) { fprintf( stderr , "Could not convert array [ %lld , %lld ] * %lld => [ %lld , %lld ] * %lld\n" , a.minimum() , a.maximum() , szD , min , max , szC ); ASSERT( 0 ); exit( 0 ); } } static ConstArray FromPointer( const C* data , long long max ) { ConstArray a; a.data = data; a.min = 0; a.max = max; return a; } static ConstArray FromPointer( const C* data , long long min , long long max ) { ConstArray a; a.data = data; a.min = min; a.max = max; return a; } inline bool operator == ( const ConstArray< C >& a ) const { return data==a.data; } inline bool operator != ( const ConstArray< C >& a ) const { return data!=a.data; } inline bool operator == ( const C* c ) const { return data==c; } inline bool operator != ( const C* c ) const { return data!=c; } inline const C* operator -> ( void ) { _assertBounds( 0 ); return data; } inline const C& operator[]( long long idx ) const { _assertBounds( idx ); return data[idx]; } inline ConstArray operator + ( int idx ) const { ConstArray a; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline ConstArray operator + ( long long idx ) const { ConstArray a; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline ConstArray operator + ( unsigned int idx ) const { ConstArray a; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline ConstArray operator + ( unsigned long long idx ) const { ConstArray a; a.data = data+idx; a.min = min-idx; a.max = max-idx; return a; } inline ConstArray& operator += ( int idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline ConstArray& operator += ( long long idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline ConstArray& operator += ( unsigned int idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline ConstArray& operator += ( unsigned long long idx ) { min -= idx; max -= idx; data += idx; return (*this); } inline ConstArray& operator ++ ( void ) { return (*this) += 1; } inline ConstArray operator++( int ){ ConstArray< C > temp = (*this) ; (*this) +=1 ; return temp; } ConstArray operator - ( int idx ) const { return (*this) + (-idx); } ConstArray operator - ( long long idx ) const { return (*this) + (-idx); } ConstArray operator - ( unsigned int idx ) const { return (*this) + (-idx); } ConstArray operator - ( unsigned long long idx ) const { return (*this) + (-idx); } ConstArray& operator -= ( int idx ) { return (*this) += (-idx); } ConstArray& operator -= ( long long idx ) { return (*this) += (-idx); } ConstArray& operator -= ( unsigned int idx ) { return (*this) += (-idx); } ConstArray& operator -= ( unsigned long long idx ) { return (*this) += (-idx); } ConstArray& operator -- ( void ) { return (*this) -= 1; } inline ConstArray operator--( int ){ ConstArray< C > temp = (*this) ; (*this) -=1 ; return temp; } long long operator - ( const ConstArray& a ) const { return ( long long )( data - a.data ); } long long operator - ( const Array< C >& a ) const { return ( long long )( data - a.pointer() ); } const C* pointer( void ) const { return data; } bool operator !( void ) { return data==NULL; } operator bool( ) { return data!=NULL; } }; #if FULL_ARRAY_DEBUG inline void PrintMemoryInfo( void ){ for( size_t i=0 ; i Array< C > memcpy( Array< C > destination , const void* source , size_t size ) { if( size>destination.maximum()*sizeof(C) ) { fprintf( stderr , "Size of copy exceeds destination maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( destination.maximum()*sizeof( C ) ) ); ASSERT( 0 ); exit( 0 ); } if( size ) memcpy( &destination[0] , source , size ); return destination; } template< class C , class D > Array< C > memcpy( Array< C > destination , Array< D > source , size_t size ) { if( size>destination.maximum()*sizeof( C ) ) { fprintf( stderr , "Size of copy exceeds destination maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( destination.maximum()*sizeof( C ) ) ); ASSERT( 0 ); exit( 0 ); } if( size>source.maximum()*sizeof( D ) ) { fprintf( stderr , "Size of copy exceeds source maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( source.maximum()*sizeof( D ) ) ); ASSERT( 0 ); exit( 0 ); } if( size ) memcpy( &destination[0] , &source[0] , size ); return destination; } template< class C , class D > Array< C > memcpy( Array< C > destination , ConstArray< D > source , size_t size ) { if( size>destination.maximum()*sizeof( C ) ) { fprintf( stderr , "Size of copy exceeds destination maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( destination.maximum()*sizeof( C ) ) ); ASSERT( 0 ); exit( 0 ); } if( size>source.maximum()*sizeof( D ) ) { fprintf( stderr , "Size of copy exceeds source maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( source.maximum()*sizeof( D ) ) ); ASSERT( 0 ); exit( 0 ); } if( size ) memcpy( &destination[0] , &source[0] , size ); return destination; } template< class D > void* memcpy( void* destination , Array< D > source , size_t size ) { if( size>source.maximum()*sizeof( D ) ) { fprintf( stderr , "Size of copy exceeds source maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( source.maximum()*sizeof( D ) ) ); ASSERT( 0 ); exit( 0 ); } if( size ) memcpy( destination , &source[0] , size ); return destination; } template< class D > void* memcpy( void* destination , ConstArray< D > source , size_t size ) { if( size>source.maximum()*sizeof( D ) ) { fprintf( stderr , "Size of copy exceeds source maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( source.maximum()*sizeof( D ) ) ); ASSERT( 0 ); exit( 0 ); } if( size ) memcpy( destination , &source[0] , size ); return destination; } template< class C > Array< C > memset( Array< C > destination , int value , size_t size ) { if( size>destination.maximum()*sizeof( C ) ) { fprintf( stderr , "Size of set exceeds destination maximum: %lld > %lld\n" , ( long long )( size ) , ( long long )( destination.maximum()*sizeof( C ) ) ); ASSERT( 0 ); exit( 0 ); } if( size ) memset( &destination[0] , value , size ); return destination; } template< class C > size_t fread( Array< C > destination , size_t eSize , size_t count , FILE* fp ) { if( count*eSize>destination.maximum()*sizeof( C ) ) { fprintf( stderr , "Size of read exceeds source maximum: %lld > %lld\n" , ( long long )( count*eSize ) , ( long long )( destination.maximum()*sizeof( C ) ) ); ASSERT( 0 ); exit( 0 ); } return fread( &destination[0] , eSize , count , fp ); } template< class C > size_t fwrite( Array< C > source , size_t eSize , size_t count , FILE* fp ) { if( count*eSize>source.maximum()*sizeof( C ) ) { fprintf( stderr , "Size of write exceeds source maximum: %lld > %lld\n" , ( long long )( count*eSize ) , ( long long )( source.maximum()*sizeof( C ) ) ); ASSERT( 0 ); exit( 0 ); } return fwrite( &source[0] , eSize , count , fp ); } template< class C > size_t fwrite( ConstArray< C > source , size_t eSize , size_t count , FILE* fp ) { if( count*eSize>source.maximum()*sizeof( C ) ) { fprintf( stderr , "Size of write exceeds source maximum: %lld > %lld\n" , ( long long )( count*eSize ) , ( long long )( source.maximum()*sizeof( C ) ) ); ASSERT( 0 ); exit( 0 ); } return fwrite( &source[0] , eSize , count , fp ); } template< class C > void qsort( Array< C > base , size_t numElements , size_t elementSize , int (*compareFunction)( const void* , const void* ) ) { if( sizeof(C)!=elementSize ) { fprintf( stderr , "Element sizes differ: %lld != %lld\n" , ( long long )( sizeof(C) ) , ( long long )( elementSize ) ); ASSERT( 0 ); exit( 0 ); } if( base.minimum()>0 || base.maximum() struct BSplineElementCoefficients { int coeffs[Degree+1]; BSplineElementCoefficients( void ){ memset( coeffs , 0 , sizeof( int ) * ( Degree+1 ) ); } int& operator[]( int idx ){ return coeffs[idx]; } const int& operator[]( int idx ) const { return coeffs[idx]; } }; // This class represents a function on the the interval, partitioned into "res" blocks. // On each block, the function is a degree-Degree polynomial, represented by the coefficients // in the associated BSplineElementCoefficients. // [NOTE] This representation of a function is agnostic to the type of boundary conditions (though the constructor is not). template< int Degree > struct BSplineElements : public std::vector< BSplineElementCoefficients< Degree > > { static const bool _Primal = (Degree&1)==1; static const int _Off = (Degree+1)/2; static int _ReflectLeft ( int offset , int res ); static int _ReflectRight( int offset , int res ); static int _RotateLeft ( int offset , int res ); static int _RotateRight ( int offset , int res ); template< bool Left > void _addPeriodic( int offset , bool negate ); public: // Coefficients are ordered as "/" "-" "\" // [WARNING] This is the opposite of the order in Polynomial::BSplineComponent int denominator; BSplineElements( void ) { denominator = 1; } BSplineElements( int res , int offset , bool dirichlet ); void upSample( BSplineElements& high ) const; void differentiate( BSplineElements< Degree-1 >& d ) const; void print( FILE* fp=stdout ) const { for( int i=0 ; i >::size() ; i++ ) { printf( "%d]" , i ); for( int j=0 ; j<=Degree ; j++ ) printf( " %d" , (*this)[i][j] ); printf( " (%d)\n" , denominator ); } } }; #define BSPLINE_SET_BOUNDS( name , s , e ) \ static const int name ## Start = (s); \ static const int name ## End = (e); \ static const int name ## Size = (e)-(s)+1 // Assumes that x is non-negative #define _FLOOR_OF_HALF( x ) ( (x) >>1 ) #define _CEIL_OF_HALF( x ) ( ( (x)+1 )>>1 ) // Done with the assumption #define FLOOR_OF_HALF( x ) ( (x)<0 ? - _CEIL_OF_HALF( -(x) ) : _FLOOR_OF_HALF( x ) ) #define CEIL_OF_HALF( x ) ( (x)<0 ? - _FLOOR_OF_HALF( -(x) ) : _CEIL_OF_HALF( x ) ) #define SMALLEST_INTEGER_LARGER_THAN_HALF( x ) ( CEIL_OF_HALF( (x)+1 ) ) #define LARGEST_INTEGER_SMALLER_THAN_HALF( x ) ( FLOOR_OF_HALF( (x)-1 ) ) #define SMALLEST_INTEGER_LARGER_THAN_OR_EQUAL_TO_HALF( x ) ( CEIL_OF_HALF( x ) ) #define LARGEST_INTEGER_SMALLER_THAN_OR_EQUAL_TO_HALF( x ) ( FLOOR_OF_HALF( x ) ) template< int Degree > class BSplineEvaluationData { public: BSplineEvaluationData( void ); static double Value( int depth , int off , double s , bool dirichlet , bool derivative ); static int Dimension( int depth ){ return ( 1< [-(Degree+1-Inset) , (Degree+1+Inset) ] CONTAINS [ J-(Degree+1-Inset)/2 , J+(Degree+1+Inset)/2 ] // Which is the same as the smallest/largest integers J such that: // J - (Degree+1-Inset)/2 >= -(Degree+1-Inset) | J + (Degree+1+Inset)/2 <= (Degree+1+Inset) // <=> J >= -(Degree+1-Inset)/2 | J <= (Degree+1+Inset)/2 BSPLINE_SET_BOUNDS( UpSample , - ( Degree + 1 - Inset ) / 2 , ( Degree + 1 + Inset ) /2 ); // Setting I=0/1, we are looking for the smallest/largest integers J such that: // Support( J ) CONTAINS Support( 0/1 ) // <=> [ 2*J - (Degree+1-Inset) , 2*J + (Degree+1+Inset) ] CONTAINS [ 0/1 - (Degree+1-Inset)/2 , 0/1 + (Degree+1+Inset)/2 ] // Which is the same as the smallest/largest integers J such that: // 2*J + (Degree+1+Inset) >= 0/1 + (Degree+1+Inset)/2 | 2*J - (Degree+1-Inset) <= 0/1 - (Degree+1-Inset)/2 // <=> 2*J >= 0/1 - (Degree+1+Inset)/2 | 2*J <= 0/1 + (Degree+1-Inset)/2 BSPLINE_SET_BOUNDS( DownSample0 , SMALLEST_INTEGER_LARGER_THAN_OR_EQUAL_TO_HALF( 0 - ( Degree + 1 + Inset ) / 2 ) , LARGEST_INTEGER_SMALLER_THAN_OR_EQUAL_TO_HALF( 0 + ( Degree + 1 - Inset ) / 2 ) ); BSPLINE_SET_BOUNDS( DownSample1 , SMALLEST_INTEGER_LARGER_THAN_OR_EQUAL_TO_HALF( 1 - ( Degree + 1 + Inset ) / 2 ) , LARGEST_INTEGER_SMALLER_THAN_OR_EQUAL_TO_HALF( 1 + ( Degree + 1 - Inset ) / 2 ) ); static const int DownSampleStart[] , DownSampleEnd[] , DownSampleSize[]; // Note that this struct stores the components in left-to-right order struct BSplineComponents { protected: Polynomial< Degree > _polys[Degree+1]; public: BSplineComponents( void ){ ; } BSplineComponents( int depth , int offset , bool dirichlet ); const Polynomial< Degree >& operator[] ( int idx ) const { return _polys[idx]; } void printnl( void ) const { for( int d=0 ; d<=Degree ; d++ ) printf( "[%d] " , d ) , _polys[d].printnl(); } }; struct BSplineUpSamplingCoefficients { protected: int _coefficients[ UpSampleSize ]; public: BSplineUpSamplingCoefficients( void ){ ; } BSplineUpSamplingCoefficients( int depth , int offset , bool dirichlet ); double operator[] ( int idx ){ return (double)_coefficients[idx] / (1<::Dimension( depth ); if ( offset=dim-Stop ) return Start + 1 + offset - ( dim-Stop ); else return Start; } struct Evaluator { protected: friend BSplineEvaluationData; int _depth; double _ccValues[2][Size][SupportSize]; public: double value( int fIdx , int cIdx , bool d ) const; int depth( void ) const { return _depth; } }; struct ChildEvaluator { protected: friend BSplineEvaluationData; int _parentDepth; double _pcValues[2][Size][ChildSupportSize]; public: double value( int fIdx , int cIdx , bool d ) const; int parentDepth( void ) const { return _parentDepth; } int childDepth( void ) const { return _parentDepth+1; } }; }; static void SetCenterEvaluator( typename CenterEvaluator::Evaluator& evaluator , int depth , bool dirichlet ); static void SetChildCenterEvaluator( typename CenterEvaluator::ChildEvaluator& evaluator , int parentDepth , bool dirichlet ); struct CornerEvaluator { static const int Start = -SupportStart , Stop = SupportEnd , Size = Start + Stop + 1; static const int Index( int depth , int offset ) { int dim = BSplineEvaluationData< Degree >::Dimension( depth ); if ( offset=dim-Stop ) return Start + 1 + offset - ( dim-Stop ); else return Start; } struct Evaluator { protected: friend BSplineEvaluationData; int _depth; double _ccValues[2][Size][CornerSize]; public: double value( int fIdx , int cIdx , bool d ) const; int depth( void ) const { return _depth; } }; struct ChildEvaluator { protected: friend BSplineEvaluationData; int _parentDepth; double _pcValues[2][Size][ChildCornerSize]; public: double value( int fIdx , int cIdx , bool d ) const; int parentDepth( void ) const { return _parentDepth; } int childDepth( void ) const { return _parentDepth+1; } }; }; static void SetCornerEvaluator( typename CornerEvaluator::Evaluator& evaluator , int depth , bool dirichlet ); static void SetChildCornerEvaluator( typename CornerEvaluator::ChildEvaluator& evaluator , int parentDepth , bool dirichlet ); struct Evaluator { typename CenterEvaluator::Evaluator centerEvaluator; typename CornerEvaluator::Evaluator cornerEvaluator; double centerValue( int fIdx , int cIdx , bool d ) const { return centerEvaluator.value( fIdx , cIdx , d ); } double cornerValue( int fIdx , int cIdx , bool d ) const { return cornerEvaluator.value( fIdx , cIdx , d ); } }; static void SetEvaluator( Evaluator& evaluator , int depth , bool dirichlet ){ SetCenterEvaluator( evaluator.centerEvaluator , depth , dirichlet ) , SetCornerEvaluator( evaluator.cornerEvaluator , depth , dirichlet ); } struct ChildEvaluator { typename CenterEvaluator::ChildEvaluator centerEvaluator; typename CornerEvaluator::ChildEvaluator cornerEvaluator; double centerValue( int fIdx , int cIdx , bool d ) const { return centerEvaluator.value( fIdx , cIdx , d ); } double cornerValue( int fIdx , int cIdx , bool d ) const { return cornerEvaluator.value( fIdx , cIdx , d ); } }; static void SetChildEvaluator( ChildEvaluator& evaluator , int depth , bool dirichlet ){ SetChildCenterEvaluator( evaluator.centerEvaluator , depth , dirichlet ) , SetChildCornerEvaluator( evaluator.cornerEvaluator , depth , dirichlet ); } struct UpSampleEvaluator { static const int Start = - SupportStart , Stop = SupportEnd , Size = Start + Stop + 1; static const int Index( int depth , int offset ) { int dim = BSplineEvaluationData< Degree >::Dimension( depth ); if ( offset=dim-Stop ) return Start + 1 + offset - ( dim-Stop ); else return Start; } protected: friend BSplineEvaluationData; int _lowDepth; double _pcValues[Size][UpSampleSize]; public: double value( int pIdx , int cIdx ) const; int lowDepth( void ) const { return _lowDepth; } }; static void SetUpSampleEvaluator( UpSampleEvaluator& evaluator , int lowDepth , bool dirichlet ); }; template< int Degree > const int BSplineEvaluationData< Degree >::DownSampleStart[] = { DownSample0Start , DownSample1Start }; template< int Degree > const int BSplineEvaluationData< Degree >::DownSampleEnd [] = { DownSample0End , DownSample1End }; template< int Degree > const int BSplineEvaluationData< Degree >::DownSampleSize [] = { DownSample0Size , DownSample1Size }; template< int Degree1 , int Degree2 > class BSplineIntegrationData { public: static double Dot( int depth1 , int off1 , bool dirichlet1 , bool d1 , int depth2 , int off2 , bool dirichlet2 , bool d2 ); // An index is interiorly overlapped if the support of its overlapping neighbors is in the range [0,1<::SupportStart , end = (1<::SupportEnd; } typedef BSplineEvaluationData< Degree1 > EData1; typedef BSplineEvaluationData< Degree2 > EData2; BSPLINE_SET_BOUNDS( Overlap , EData1:: SupportStart - EData2::SupportEnd , EData1:: SupportEnd - EData2::SupportStart ); BSPLINE_SET_BOUNDS( ChildOverlap , EData1::ChildSupportStart - EData2::SupportEnd , EData1::ChildSupportEnd - EData2::SupportStart ); BSPLINE_SET_BOUNDS( OverlapSupport , OverlapStart + EData2::SupportStart , OverlapEnd + EData2::SupportEnd ); BSPLINE_SET_BOUNDS( ChildOverlapSupport , ChildOverlapStart + EData2::SupportStart , ChildOverlapEnd + EData2::SupportEnd ); // Setting I=0/1, we are looking for the smallest/largest integers J such that: // Support( 2*J ) * 2 INTERSECTION Support( 0/1 ) NON-EMPTY // <=> [ 2*J - (Degree2+1-Inset2) , 2*J + (Degree2+1+Inset2) ] INTERSECTION [ 0/1 - (Degree1+1-Inset1)/2 , 0/1 + (Degree1+1+Inset1)/2 ] NON-EMPTY // Which is the same as the smallest/largest integers J such that: // 0/1 - (Degree1+1-Inset1)/2 < 2*J + (Degree2+1+Inset2) | 0/1 + (Degree1+1+Inset1)/2 > 2*J - (Degree2+1-Inset2) // <=> 2*J > 0/1 - ( 2*Degree2 + Degree1 + 3 + 2*Inset2 - Inset1 ) / 2 | 2*J < 0/1 + ( 2*Degree2 + Degree1 + 3 - 2*Inset2 + Inset1 ) / 2 BSPLINE_SET_BOUNDS( ParentOverlap0 , SMALLEST_INTEGER_LARGER_THAN_HALF( 0 - ( 2*Degree2 + Degree1 + 3 + 2*EData2::Inset - EData1::Inset ) / 2 ) , LARGEST_INTEGER_SMALLER_THAN_HALF( 0 + ( 2*Degree2 + Degree1 + 3 - 2*EData2::Inset + EData1::Inset ) / 2 ) ); BSPLINE_SET_BOUNDS( ParentOverlap1 , SMALLEST_INTEGER_LARGER_THAN_HALF( 1 - ( 2*Degree2 + Degree1 + 3 + 2*EData2::Inset - EData1::Inset ) / 2 ) , LARGEST_INTEGER_SMALLER_THAN_HALF( 1 + ( 2*Degree2 + Degree1 + 3 - 2*EData2::Inset + EData1::Inset ) / 2 ) ); static const int ParentOverlapStart[] , ParentOverlapEnd[] , ParentOverlapSize[]; struct FunctionIntegrator { static const int Start = - OverlapSupportStart , Stop = OverlapSupportEnd , Size = Start + Stop + 1; static const int Index( int depth , int offset ) { int dim = BSplineEvaluationData< Degree2 >::Dimension( depth ); if ( offset=dim-Stop ) return Start + 1 + offset - ( dim-Stop ); else return Start; } struct Integrator { protected: friend BSplineIntegrationData; int _depth; double _ccIntegrals[2][2][Size][OverlapSize]; public: double dot( int fIdx1 , int fidx2 , bool d1 , bool d2 ) const; int depth( void ) const { return _depth; } }; struct ChildIntegrator { protected: friend BSplineIntegrationData; int _parentDepth; double _pcIntegrals[2][2][Size][ChildOverlapSize]; public: double dot( int fIdx1 , int fidx2 , bool d1 , bool d2 ) const; int parentDepth( void ) const { return _parentDepth; } int childDepth( void ) const { return _parentDepth+1; } }; }; static void SetIntegrator( typename FunctionIntegrator::Integrator& integrator , int depth , bool dirichlet1 , bool dirichlet2 ); static void SetChildIntegrator( typename FunctionIntegrator::ChildIntegrator& integrator , int parentDepth , bool dirichlet1 , bool dirichlet2 ); }; template< int Degree1 , int Degree2 > const int BSplineIntegrationData< Degree1 , Degree2 >::ParentOverlapStart[] = { ParentOverlap0Start , ParentOverlap1Start }; template< int Degree1 , int Degree2 > const int BSplineIntegrationData< Degree1 , Degree2 >::ParentOverlapEnd [] = { ParentOverlap0End , ParentOverlap1End }; template< int Degree1 , int Degree2 > const int BSplineIntegrationData< Degree1 , Degree2 >::ParentOverlapSize [] = { ParentOverlap0Size , ParentOverlap1Size }; #undef BSPLINE_SET_BOUNDS #undef _FLOOR_OF_HALF #undef _CEIL_OF_HALF #undef FLOOR_OF_HALF #undef CEIL_OF_HALF #undef SMALLEST_INTEGER_LARGER_THAN_HALF #undef LARGEST_INTEGER_SMALLER_THAN_HALF #undef SMALLEST_INTEGER_LARGER_THAN_OR_EQUAL_TO_HALF #undef LARGEST_INTEGER_SMALLER_THAN_OR_EQUAL_TO_HALF template< int Degree > class BSplineData { bool _dirichlet; public: inline static int Centers ( int depth ){ return (1<0) ? TotalFunctionCount(depth-1) : 0 , fEnd = TotalFunctionCount(depth); } inline static void SampleSpan( int depth , int& sStart , int& sEnd ){ sStart = (depth>0) ? TotalSampleCount(depth-1) : 0 , sEnd = TotalSampleCount(depth); } inline static int RemapOffset( int depth , int idx , bool& reflect ); int depth; size_t functionCount , sampleCount; Pointer( typename BSplineEvaluationData< Degree >::BSplineComponents ) baseBSplines; BSplineData( void ); void set( int maxDepth , bool dirichlet=false ); }; template< int Degree1 , int Degree2 > void SetBSplineElementIntegrals( double integrals[Degree1+1][Degree2+1] ); #include "BSplineData.inl" #endif // BSPLINE_DATA_INCLUDEDcolmap-3.9.1/src/thirdparty/PoissonRecon/BSplineData.inl000077500000000000000000000525461454702036400232570ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ /////////////////////////// // BSplineEvaluationData // /////////////////////////// template< int Degree > double BSplineEvaluationData< Degree >::Value( int depth , int off , double s , bool dirichlet , bool derivative ) { if( s<0 || s>1 ) return 0.; int dim = Dimension(depth) , res = 1<=dim ) return 0; BSplineComponents components = BSplineComponents( depth , off , dirichlet ); // [NOTE] This is an ugly way to ensure that when s=1 we evaluate using a B-Spline component within the valid range. int ii = std::max< int >( 0 , std::min< int >( res-1 , (int)floor( s * res ) ) ) - off; if( iiSupportEnd ) return 0; if( derivative ) return components[ii-SupportStart].derivative()(s); else return components[ii-SupportStart](s); } template< int Degree > void BSplineEvaluationData< Degree >::SetCenterEvaluator( typename CenterEvaluator::Evaluator& evaluator , int depth , bool dirichlet ) { evaluator._depth = depth; int dim = BSplineEvaluationData< Degree >::Dimension( depth ) , res = 1< void BSplineEvaluationData< Degree >::SetChildCenterEvaluator( typename CenterEvaluator::ChildEvaluator& evaluator , int parentDepth , bool dirichlet ) { evaluator._parentDepth = parentDepth; int dim = BSplineEvaluationData< Degree >::Dimension( parentDepth ) , res = 1<<(parentDepth+1); for( int i=0 ; i double BSplineEvaluationData< Degree >::CenterEvaluator::Evaluator::value( int fIdx , int cIdx , bool d ) const { int dd = cIdx-fIdx , res = 1<<(_depth) , dim = Dimension(_depth); if( cIdx<0 || fIdx<0 || cIdx>=res || fIdx>=dim || ddSupportEnd ) return 0; return _ccValues[d?1:0][ CenterEvaluator::Index( _depth , fIdx ) ][dd-SupportStart]; } template< int Degree > double BSplineEvaluationData< Degree >::CenterEvaluator::ChildEvaluator::value( int fIdx , int cIdx , bool d ) const { int dd = cIdx-2*fIdx , res = 1<<(_parentDepth+1) , dim = Dimension(_parentDepth); if( cIdx<0 || fIdx<0 || cIdx>=res || fIdx>=dim || ddChildSupportEnd ) return 0; return _pcValues[d?1:0][ CenterEvaluator::Index( _parentDepth , fIdx ) ][dd-ChildSupportStart]; } template< int Degree > void BSplineEvaluationData< Degree >::SetCornerEvaluator( typename CornerEvaluator::Evaluator& evaluator , int depth , bool dirichlet ) { evaluator._depth = depth; int dim = BSplineEvaluationData< Degree >::Dimension( depth ) , res = 1< void BSplineEvaluationData< Degree >::SetChildCornerEvaluator( typename CornerEvaluator::ChildEvaluator& evaluator , int parentDepth , bool dirichlet ) { evaluator._parentDepth = parentDepth; int dim = BSplineEvaluationData< Degree >::Dimension( parentDepth ) , res = 1<<(parentDepth+1); for( int i=0 ; i void BSplineEvaluationData< Degree >::SetUpSampleEvaluator( UpSampleEvaluator& evaluator , int lowDepth , bool dirichlet ) { evaluator._lowDepth = lowDepth; int lowDim = Dimension(lowDepth); for( int i=0 ; i double BSplineEvaluationData< Degree >::CornerEvaluator::Evaluator::value( int fIdx , int cIdx , bool d ) const { int dd = cIdx-fIdx , res = ( 1<<_depth ) + 1 , dim = Dimension(_depth); if( cIdx<0 || fIdx<0 || cIdx>=res || fIdx>=dim || ddCornerEnd ) return 0; return _ccValues[d?1:0][ CornerEvaluator::Index( _depth , fIdx ) ][dd-CornerStart]; } template< int Degree > double BSplineEvaluationData< Degree >::CornerEvaluator::ChildEvaluator::value( int fIdx , int cIdx , bool d ) const { int dd = cIdx-2*fIdx , res = ( 1<<(_parentDepth+1) ) + 1 , dim = Dimension(_parentDepth); if( cIdx<0 || fIdx<0 || cIdx>=res || fIdx>=dim || ddChildCornerEnd ) return 0; return _pcValues[d?1:0][ CornerEvaluator::Index( _parentDepth , fIdx ) ][dd-ChildCornerStart]; } template< int Degree > double BSplineEvaluationData< Degree >::UpSampleEvaluator::value( int pIdx , int cIdx ) const { int dd = cIdx-2*pIdx , pDim = Dimension( _lowDepth ) , cDim = Dimension( _lowDepth+1 ); if( cIdx<0 || pIdx<0 || cIdx>=cDim || pIdx>=pDim || ddUpSampleEnd ) return 0; return _pcValues[ UpSampleEvaluator::Index( _lowDepth , pIdx ) ][dd-UpSampleStart]; } ////////////////////////////////////////////// // BSplineEvaluationData::BSplineComponents // ////////////////////////////////////////////// template< int Degree > BSplineEvaluationData< Degree >::BSplineComponents::BSplineComponents( int depth , int offset , bool dirichlet ) { int res = 1< elements( res , offset , dirichlet ); // The first index is the position, the second is the element type Polynomial< Degree > components[Degree+1][Degree+1]; // Generate the elements that can appear in the base function corresponding to the base function at (depth,offset) = (0,0) for( int d=0 ; d<=Degree ; d++ ) for( int dd=0 ; dd<=Degree ; dd++ ) components[d][dd] = Polynomial< Degree >::BSplineComponent( Degree-dd ).shift( -( (Degree+1)/2 ) + d ); // Now adjust to the desired depth and offset double width = 1. / res; for( int d=0 ; d<=Degree ; d++ ) for( int dd=0 ; dd<=Degree ; dd++ ) components[d][dd] = components[d][dd].scale( width ).shift( width*offset ); // Now write in the polynomials for( int d=0 ; d<=Degree ; d++ ) { int idx = offset + SupportStart + d; _polys[d] = Polynomial< Degree >(); if( idx>=0 && idx BSplineEvaluationData< Degree >::BSplineUpSamplingCoefficients::BSplineUpSamplingCoefficients( int depth , int offset , bool dirichlet ) { // [ 1/8 1/2 3/4 1/2 1/8] // [ 1 , 1 ] -> [ 3/4 , 1/2 , 1/8 ] + [ 1/8 , 1/2 , 3/4 ] = [ 7/8 , 1 , 7/8 ] int dim = Dimension(depth) , _dim = Dimension(depth+1); bool reflect; offset = BSplineData< Degree >::RemapOffset( depth , offset , reflect ); int multiplier = ( dirichlet && reflect ) ? -1 : 1; bool useReflected = Inset || ( offset % ( dim-1 ) ); int b[ UpSampleSize ]; Polynomial< Degree+1 >::BinomialCoefficients( b ); // Clear the values memset( _coefficients , 0 , sizeof(int) * UpSampleSize ); // Get the array of coefficients, relative to the origin int* coefficients = _coefficients - ( 2*offset + UpSampleStart ); for( int i=UpSampleStart ; i<=UpSampleEnd ; i++ ) { int _offset = 2*offset+i; _offset = BSplineData< Degree >::RemapOffset( depth+1 , _offset , reflect ); if( useReflected || !reflect ) { int _multiplier = multiplier * ( ( dirichlet && reflect ) ? -1 : 1 ); coefficients[ _offset ] += b[ i-UpSampleStart ] * _multiplier; } // If we are not inset and we are at the boundary, use the reflection as well if( !Inset && ( offset % (dim-1) ) && !( _offset % (_dim-1) ) ) { _offset = BSplineData< Degree >::RemapOffset( depth+1 , _offset , reflect ); int _multiplier = multiplier * ( ( dirichlet && reflect ) ? -1 : 1 ); if( dirichlet ) _multiplier *= -1; coefficients[ _offset ] += b[ i-UpSampleStart ] * _multiplier; } } } //////////////////////////// // BSplineIntegrationData // //////////////////////////// template< int Degree1 , int Degree2 > double BSplineIntegrationData< Degree1 , Degree2 >::Dot( int depth1 , int off1 , bool dirichlet1 , bool d1 , int depth2 , int off2 , bool dirichlet2 , bool d2 ) { const int _Degree1 = (d1 ? (Degree1-1) : Degree1) , _Degree2 = (d2 ? (Degree2-1) : Degree2); int sums[ Degree1+1 ][ Degree2+1 ]; int depth = std::max< int >( depth1 , depth2 ); BSplineElements< Degree1 > b1( 1< b2( 1< b; while( depth1 b; while( depth2 db1; BSplineElements< Degree2-1 > db2; b1.differentiate( db1 ) , b2.differentiate( db2 ); int start1=-1 , end1=-1 , start2=-1 , end2=-1; for( int i=0 ; i=end2 || start2>=end1 ) return 0.; int start = std::max< int >( start1 , start2 ) , end = std::min< int >( end1 , end2 ); memset( sums , 0 , sizeof( sums ) ); // Iterate over the support for( int i=start ; i( integrals ); for( int j=0 ; j<=_Degree1 ; j++ ) for( int k=0 ; k<=_Degree2 ; k++ ) _dot += integrals[j][k] * sums[j][k]; } else if( d1 ) { double integrals[ Degree1 ][ Degree2+1 ]; SetBSplineElementIntegrals< Degree1-1 , Degree2 >( integrals ); for( int j=0 ; j<=_Degree1 ; j++ ) for( int k=0 ; k<=_Degree2 ; k++ ) _dot += integrals[j][k] * sums[j][k]; } else if( d2 ) { double integrals[ Degree1+1 ][ Degree2 ]; SetBSplineElementIntegrals< Degree1 , Degree2-1 >( integrals ); for( int j=0 ; j<=_Degree1 ; j++ ) for( int k=0 ; k<=_Degree2 ; k++ ) _dot += integrals[j][k] * sums[j][k]; } else { double integrals[ Degree1+1 ][ Degree2+1 ]; SetBSplineElementIntegrals< Degree1 , Degree2 >( integrals ); for( int j=0 ; j<=_Degree1 ; j++ ) for( int k=0 ; k<=_Degree2 ; k++ ) _dot += integrals[j][k] * sums[j][k]; } _dot /= b1.denominator; _dot /= b2.denominator; if ( d1 && d2 ) return _dot * (1< void BSplineIntegrationData< Degree1, Degree2 >::SetIntegrator( typename FunctionIntegrator::Integrator& integrator , int depth , bool dirichlet1 , bool dirichlet2 ) { integrator._depth = depth; int dim = BSplineEvaluationData< Degree2 >::Dimension( depth ); for( int i=0 ; i void BSplineIntegrationData< Degree1, Degree2 >::SetChildIntegrator( typename FunctionIntegrator::ChildIntegrator& integrator , int parentDepth , bool dirichlet1 , bool dirichlet2 ) { integrator._parentDepth = parentDepth; int dim = BSplineEvaluationData< Degree2 >::Dimension( parentDepth ); for( int i=0 ; i double BSplineIntegrationData< Degree1 , Degree2 >::FunctionIntegrator::Integrator::dot( int off1 , int off2 , bool d1 , bool d2 ) const { int d = off2-off1 , dim1 = BSplineEvaluationData< Degree1 >::Dimension( _depth ) , dim2 = BSplineEvaluationData< Degree2 >::Dimension( _depth ); if( off1<0 || off2<0 || off1>=dim1 || off2>=dim2 || dOverlapEnd ) return 0; return _ccIntegrals[d1?1:0][d2?1:0][ FunctionIntegrator::Index( _depth , off1 ) ][d-OverlapStart]; } template< int Degree1 , int Degree2 > double BSplineIntegrationData< Degree1 , Degree2 >::FunctionIntegrator::ChildIntegrator::dot( int off1 , int off2 , bool d1 , bool d2 ) const { int d = off2-2*off1 , dim1 = BSplineEvaluationData< Degree1 >::Dimension( _parentDepth ) , dim2 = BSplineEvaluationData< Degree2 >::Dimension( _parentDepth+1 ); if( off1<0 || off2<0 || off1>=dim1 || off2>=dim2 || dChildOverlapEnd ) return 0; return _pcIntegrals[d1?1:0][d2?1:0][ FunctionIntegrator::Index( _parentDepth , off1 ) ][d-ChildOverlapStart]; } ///////////////// // BSplineData // ///////////////// #define MODULO( A , B ) ( (A)<0 ? ( (B)-((-(A))%(B)) ) % (B) : (A) % (B) ) template< int Degree > int BSplineData< Degree >::RemapOffset( int depth , int offset , bool& reflect ) { const int I = ( Degree&1 ) ? 0 : 1; int dim = Dimension( depth ); offset = MODULO( offset , 2*(dim-1+I) ); reflect = offset>=dim; if( reflect ) return 2*(dim-1+I) - (offset+I); else return offset; } #undef MODULO template< int Degree > BSplineData< Degree >::BSplineData( void ){ functionCount = sampleCount = 0; } template< int Degree > void BSplineData< Degree >::set( int maxDepth , bool dirichlet ) { _dirichlet = dirichlet; depth = maxDepth; functionCount = TotalFunctionCount( depth ); sampleCount = TotalSampleCount( depth ); baseBSplines = NewPointer< typename BSplineEvaluationData< Degree >::BSplineComponents >( functionCount ); for( size_t i=0 ; i::BSplineComponents( d , off , _dirichlet ); } } ///////////////////// // BSplineElements // ///////////////////// template< int Degree > BSplineElements< Degree >::BSplineElements( int res , int offset , bool dirichlet ) { denominator = 1; std::vector< BSplineElementCoefficients< Degree > >::resize( res , BSplineElementCoefficients< Degree >() ); // If we have primal dirichlet constraints, the boundary functions are necessarily zero if( _Primal && dirichlet && !(offset%res) ) return; // Construct the B-Spline for( int i=0 ; i<=Degree ; i++ ) { int idx = -_Off + offset + i; if( idx>=0 && idx( _RotateLeft ( offset , res ) , false ) , _addPeriodic< false >( _RotateRight( offset , res ) , false ); // Recursively fold in the boundaries if( _Primal && !(offset%res) ) return; // Fold in the reflected instance (which may require negation) _addPeriodic< true >( _ReflectLeft( offset , res ) , dirichlet ) , _addPeriodic< false >( _ReflectRight( offset , res ) , dirichlet ); } template< int Degree > int BSplineElements< Degree >::_ReflectLeft ( int offset , int res ){ return (Degree&1) ? -offset : -1-offset; } template< int Degree > int BSplineElements< Degree >::_ReflectRight( int offset , int res ){ return (Degree&1) ? 2*res-offset : 2*res-1-offset; } template< int Degree > int BSplineElements< Degree >::_RotateLeft ( int offset , int res ){ return offset-2*res; } template< int Degree > int BSplineElements< Degree >::_RotateRight ( int offset , int res ){ return offset+2*res; } template< int Degree > template< bool Left > void BSplineElements< Degree >::_addPeriodic( int offset , bool negate ) { int res = int( std::vector< BSplineElementCoefficients< Degree > >::size() ); bool set = false; // Add in the corresponding B-spline elements (possibly negated) for( int i=0 ; i<=Degree ; i++ ) { int idx = -_Off + offset + i; if( idx>=0 && idx( Left ? _RotateLeft( offset , res ) : _RotateRight( offset , res ) , negate ); } template< int Degree > void BSplineElements< Degree >::upSample( BSplineElements< Degree >& high ) const { int bCoefficients[ BSplineEvaluationData< Degree >::UpSampleSize ]; Polynomial< Degree+1 >::BinomialCoefficients( bCoefficients ); high.resize( std::vector< BSplineElementCoefficients< Degree > >::size()*2 ); high.assign( high.size() , BSplineElementCoefficients< Degree >() ); // [NOTE] We have flipped the order of the B-spline elements for( int i=0 ; i >::size()) ; i++ ) for( int j=0 ; j<=Degree ; j++ ) { // At index I , B-spline element J corresponds to a B-spline centered at: // I - SupportStart - J int idx = i - BSplineEvaluationData< Degree >::SupportStart - j; for( int k=BSplineEvaluationData< Degree >::UpSampleStart ; k<=BSplineEvaluationData< Degree >::UpSampleEnd ; k++ ) { // Index idx at the coarser resolution gets up-sampled into indices: // 2*idx + [UpSampleStart,UpSampleEnd] // at the finer resolution int _idx = 2*idx + k; // Compute the index of the B-spline element relative to 2*i and 2*i+1 int _j1 = -_idx + 2*i - BSplineEvaluationData< Degree >::SupportStart , _j2 = -_idx + 2*i + 1 - BSplineEvaluationData< Degree >::SupportStart; if( _j1>=0 && _j1<=Degree ) high[2*i+0][_j1] += (*this)[i][j] * bCoefficients[k-BSplineEvaluationData< Degree >::UpSampleStart]; if( _j2>=0 && _j2<=Degree ) high[2*i+1][_j2] += (*this)[i][j] * bCoefficients[k-BSplineEvaluationData< Degree >::UpSampleStart]; } } high.denominator = denominator< void BSplineElements< Degree >::differentiate( BSplineElements< Degree-1 >& d ) const { d.resize( std::vector< BSplineElementCoefficients< Degree > >::size() ); d.assign( d.size() , BSplineElementCoefficients< Degree-1 >() ); for( int i=0 ; i >::size()) ; i++ ) for( int j=0 ; j<=Degree ; j++ ) { if( j-1>=0 ) d[i][j-1] -= (*this)[i][j]; if( j void SetBSplineElementIntegrals( double integrals[Degree1+1][Degree2+1] ) { for( int i=0 ; i<=Degree1 ; i++ ) { Polynomial< Degree1 > p1 = Polynomial< Degree1 >::BSplineComponent( Degree1-i ); for( int j=0 ; j<=Degree2 ; j++ ) { Polynomial< Degree2 > p2 = Polynomial< Degree2 >::BSplineComponent( Degree2-j ); integrals[i][j] = ( p1 * p2 ).integral( 0 , 1 ); } } } colmap-3.9.1/src/thirdparty/PoissonRecon/BinaryNode.h000066400000000000000000000067421454702036400226220ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #ifndef BINARY_NODE_INCLUDED #define BINARY_NODE_INCLUDED class BinaryNode { public: static inline int CenterCount( int depth ) { return 1< static inline Real Width( int depth ){ return Real(1.0/(1< static inline void CenterAndWidth( int depth , int offset , Real& center , Real& width ){ width = Real (1.0/(1< static inline void CornerAndWidth( int depth , int offset , Real& corner , Real& width ){ width = Real(1.0/(1< static inline void CenterAndWidth( int idx , Real& center , Real& width ) { int depth , offset; CenterDepthAndOffset( idx , depth , offset ); CenterAndWidth( depth , offset , center , width ); } template< class Real > static inline void CornerAndWidth( int idx , Real& corner , Real& width ) { int depth , offset; CornerDepthAndOffset( idx , depth , offset ); CornerAndWidth( depth , offset , corner , width ); } static inline void CenterDepthAndOffset( int idx , int& depth , int& offset ) { offset = idx , depth = 0; while( offset>=(1<=( (1< #include #include #include #include "CmdLineParser.h" #ifdef _WIN32 int strcasecmp(char* c1,char* c2){return _stricmp(c1,c2);} #endif cmdLineReadable::cmdLineReadable(const char* name) { set=false; this->name=new char[strlen(name)+1]; strcpy(this->name,name); } cmdLineReadable::~cmdLineReadable(void) { if(name) delete[] name; name=NULL; } int cmdLineReadable::read(char**,int){ set=true; return 0; } void cmdLineReadable::writeValue(char* str) { str[0] = 0; } //////////////// // cmdLineInt // //////////////// cmdLineInt::cmdLineInt(const char* name) : cmdLineReadable(name) {value=0;} cmdLineInt::cmdLineInt(const char* name,const int& v) : cmdLineReadable(name) {value=v;} int cmdLineInt::read(char** argv,int argc){ if(argc>0){ value=atoi(argv[0]); set=true; return 1; } else{return 0;} } void cmdLineInt::writeValue(char* str) { sprintf(str,"%d",value); } ////////////////// // cmdLineFloat // ////////////////// cmdLineFloat::cmdLineFloat(const char* name) : cmdLineReadable(name) {value=0;} cmdLineFloat::cmdLineFloat(const char* name, const float& v) : cmdLineReadable(name) {value=v;} int cmdLineFloat::read(char** argv,int argc){ if(argc>0){ value=(float)atof(argv[0]); set=true; return 1; } else{return 0;} } void cmdLineFloat::writeValue(char* str) { sprintf(str,"%f",value); } /////////////////// // cmdLineString // /////////////////// cmdLineString::cmdLineString(const char* name) : cmdLineReadable(name) {value=NULL;} cmdLineString::~cmdLineString(void) { if(value) delete[] value; value=NULL; } int cmdLineString::read(char** argv,int argc){ if(argc>0) { value=new char[strlen(argv[0])+1]; strcpy(value,argv[0]); set=true; return 1; } else{return 0;} } void cmdLineString::writeValue(char* str) { sprintf(str,"%s",value); } //////////////////// // cmdLineStrings // //////////////////// cmdLineStrings::cmdLineStrings(const char* name,int Dim) : cmdLineReadable(name) { this->Dim=Dim; values=new char*[Dim]; for(int i=0;i=Dim) { for(int i=0;i 0) { if (argv[0][0] == '-' && argv[0][1]=='-') { for(i=0;iname)) { argv++, argc--; j=readable[i]->read(argv,argc); argv+=j,argc-=j; break; } } if(i==num){ if(dumpError) { fprintf(stderr, "invalid option: %s\n",*argv); fprintf(stderr, "possible options are:\n"); for(i=0;iname); } argv++, argc--; } } else { if(dumpError) { fprintf(stderr, "invalid option: %s\n", *argv); fprintf(stderr, " options must start with a \'--\'\n"); } argv++, argc--; } } } char** ReadWords(const char* fileName,int& cnt) { char** names; char temp[500]; FILE* fp; fp=fopen(fileName,"r"); if(!fp){return NULL;} cnt=0; while(fscanf(fp," %s ",temp)==1){cnt++;} fclose(fp); names=new char*[cnt]; if(!names){return NULL;} fp=fopen(fileName,"r"); if(!fp){ delete[] names; cnt=0; return NULL; } cnt=0; while(fscanf(fp," %s ",temp)==1){ names[cnt]=new char[strlen(temp)+1]; if(!names){ for(int j=0;j #include #ifdef _WIN32 int strcasecmp(char* c1,char* c2); #endif class cmdLineReadable{ public: bool set; char* name; cmdLineReadable(const char* name); virtual ~cmdLineReadable(void); virtual int read(char** argv,int argc); virtual void writeValue(char* str); }; class cmdLineInt : public cmdLineReadable { public: int value; cmdLineInt(const char* name); cmdLineInt(const char* name,const int& v); int read(char** argv,int argc); void writeValue(char* str); }; template class cmdLineIntArray : public cmdLineReadable { public: int values[Dim]; cmdLineIntArray(const char* name); cmdLineIntArray(const char* name,const int v[Dim]); int read(char** argv,int argc); void writeValue(char* str); }; class cmdLineFloat : public cmdLineReadable { public: float value; cmdLineFloat(const char* name); cmdLineFloat(const char* name,const float& f); int read(char** argv,int argc); void writeValue(char* str); }; template class cmdLineFloatArray : public cmdLineReadable { public: float values[Dim]; cmdLineFloatArray(const char* name); cmdLineFloatArray(const char* name,const float f[Dim]); int read(char** argv,int argc); void writeValue(char* str); }; class cmdLineString : public cmdLineReadable { public: char* value; cmdLineString(const char* name); ~cmdLineString(); int read(char** argv,int argc); void writeValue(char* str); }; class cmdLineStrings : public cmdLineReadable { int Dim; public: char** values; cmdLineStrings(const char* name,int Dim); ~cmdLineStrings(void); int read(char** argv,int argc); void writeValue(char* str); }; template class cmdLineStringArray : public cmdLineReadable { public: char* values[Dim]; cmdLineStringArray(const char* name); ~cmdLineStringArray(); int read(char** argv,int argc); void writeValue(char* str); }; // This reads the arguments in argc, matches them against "names" and sets // the values of "r" appropriately. Parameters start with "--" void cmdLineParse(int argc, char **argv,int num,cmdLineReadable** r,int dumpError=1); char* GetFileExtension(char* fileName); char* GetLocalFileName(char* fileName); char** ReadWords(const char* fileName,int& cnt); #include "CmdLineParser.inl" #endif // CMD_LINE_PARSER_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/CmdLineParser.inl000077500000000000000000000073501454702036400236120ustar00rootroot00000000000000/* -*- C++ -*- Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ ///////////////////// // cmdLineIntArray // ///////////////////// template cmdLineIntArray::cmdLineIntArray(const char* name) : cmdLineReadable(name) { for(int i=0;i cmdLineIntArray::cmdLineIntArray(const char* name,const int v[Dim]) : cmdLineReadable(name) { for(int i=0;i int cmdLineIntArray::read(char** argv,int argc) { if(argc>=Dim) { for(int i=0;i void cmdLineIntArray::writeValue(char* str) { char* temp=str; for(int i=0;i cmdLineFloatArray::cmdLineFloatArray(const char* name) : cmdLineReadable(name) { for(int i=0;i cmdLineFloatArray::cmdLineFloatArray(const char* name,const float f[Dim]) : cmdLineReadable(name) { for(int i=0;i int cmdLineFloatArray::read(char** argv,int argc) { if(argc>=Dim) { for(int i=0;i void cmdLineFloatArray::writeValue(char* str) { char* temp=str; for(int i=0;i cmdLineStringArray::cmdLineStringArray(const char* name) : cmdLineReadable(name) { for(int i=0;i cmdLineStringArray::~cmdLineStringArray(void) { for(int i=0;i int cmdLineStringArray::read(char** argv,int argc) { if(argc>=Dim) { for(int i=0;i void cmdLineStringArray::writeValue(char* str) { char* temp=str; for(int i=0;i #include "Factor.h" int Factor(double a1,double a0,double roots[1][2],double EPS){ if(fabs(a1)<=EPS){return 0;} roots[0][0]=-a0/a1; roots[0][1]=0; return 1; } int Factor(double a2,double a1,double a0,double roots[2][2],double EPS){ double d; if(fabs(a2)<=EPS){return Factor(a1,a0,roots,EPS);} d=a1*a1-4*a0*a2; a1/=(2*a2); if(d<0){ d=sqrt(-d)/(2*a2); roots[0][0]=roots[1][0]=-a1; roots[0][1]=-d; roots[1][1]= d; } else{ d=sqrt(d)/(2*a2); roots[0][1]=roots[1][1]=0; roots[0][0]=-a1-d; roots[1][0]=-a1+d; } return 2; } // Solution taken from: http://mathworld.wolfram.com/CubicFormula.html // and http://www.csit.fsu.edu/~burkardt/f_src/subpak/subpak.f90 int Factor(double a3,double a2,double a1,double a0,double roots[3][2],double EPS){ double q,r,r2,q3; if(fabs(a3)<=EPS){return Factor(a2,a1,a0,roots,EPS);} a2/=a3; a1/=a3; a0/=a3; q=-(3*a1-a2*a2)/9; r=-(9*a2*a1-27*a0-2*a2*a2*a2)/54; r2=r*r; q3=q*q*q; if(r20){return PI/2.0;} else{return -PI/2.0;} } if(x>=0){return atan(y/x);} else{ if(y>=0){return atan(y/x)+PI;} else{return atan(y/x)-PI;} } } double Angle(const double in[2]){ if((in[0]*in[0]+in[1]*in[1])==0.0){return 0;} else{return ArcTan2(in[1],in[0]);} } void Sqrt(const double in[2],double out[2]){ double r=sqrt(sqrt(in[0]*in[0]+in[1]*in[1])); double a=Angle(in)*0.5; out[0]=r*cos(a); out[1]=r*sin(a); } void Add(const double in1[2],const double in2[2],double out[2]){ out[0]=in1[0]+in2[0]; out[1]=in1[1]+in2[1]; } void Subtract(const double in1[2],const double in2[2],double out[2]){ out[0]=in1[0]-in2[0]; out[1]=in1[1]-in2[1]; } void Multiply(const double in1[2],const double in2[2],double out[2]){ out[0]=in1[0]*in2[0]-in1[1]*in2[1]; out[1]=in1[0]*in2[1]+in1[1]*in2[0]; } void Divide(const double in1[2],const double in2[2],double out[2]){ double temp[2]; double l=in2[0]*in2[0]+in2[1]*in2[1]; temp[0]= in2[0]/l; temp[1]=-in2[1]/l; Multiply(in1,temp,out); } // Solution taken from: http://mathworld.wolfram.com/QuarticEquation.html // and http://www.csit.fsu.edu/~burkardt/f_src/subpak/subpak.f90 int Factor(double a4,double a3,double a2,double a1,double a0,double roots[4][2],double EPS){ double R[2],D[2],E[2],R2[2]; if(fabs(a4)10e-8){ double temp1[2],temp2[2]; double p1[2],p2[2]; p1[0]=a3*a3*0.75-2.0*a2-R2[0]; p1[1]=0; temp2[0]=((4.0*a3*a2-8.0*a1-a3*a3*a3)/4.0); temp2[1]=0; Divide(temp2,R,p2); Add (p1,p2,temp1); Subtract(p1,p2,temp2); Sqrt(temp1,D); Sqrt(temp2,E); } else{ R[0]=R[1]=0; double temp1[2],temp2[2]; temp1[0]=roots[0][0]*roots[0][0]-4.0*a0; temp1[1]=0; Sqrt(temp1,temp2); temp1[0]=a3*a3*0.75-2.0*a2+2.0*temp2[0]; temp1[1]= 2.0*temp2[1]; Sqrt(temp1,D); temp1[0]=a3*a3*0.75-2.0*a2-2.0*temp2[0]; temp1[1]= -2.0*temp2[1]; Sqrt(temp1,E); } roots[0][0]=-a3/4.0+R[0]/2.0+D[0]/2.0; roots[0][1]= R[1]/2.0+D[1]/2.0; roots[1][0]=-a3/4.0+R[0]/2.0-D[0]/2.0; roots[1][1]= R[1]/2.0-D[1]/2.0; roots[2][0]=-a3/4.0-R[0]/2.0+E[0]/2.0; roots[2][1]= -R[1]/2.0+E[1]/2.0; roots[3][0]=-a3/4.0-R[0]/2.0-E[0]/2.0; roots[3][1]= -R[1]/2.0-E[1]/2.0; return 4; } int Solve(const double* eqns,const double* values,double* solutions,int dim){ int i,j,eIndex; double v,m; int *index=new int[dim]; int *set=new int[dim]; double* myEqns=new double[dim*dim]; double* myValues=new double[dim]; for(i=0;im){ m=fabs(myEqns[j*dim+i]); eIndex=j; } } if(eIndex==-1){ delete[] index; delete[] myValues; delete[] myEqns; delete[] set; return 0; } // The position in which the solution for the i-th variable can be found index[i]=eIndex; set[eIndex]=1; // Normalize the equation v=myEqns[eIndex*dim+i]; for(j=0;j class FunctionData{ bool useDotRatios; int normalize; #if BOUNDARY_CONDITIONS bool reflectBoundary; #endif // BOUNDARY_CONDITIONS public: const static int DOT_FLAG = 1; const static int D_DOT_FLAG = 2; const static int D2_DOT_FLAG = 4; const static int VALUE_FLAG = 1; const static int D_VALUE_FLAG = 2; int depth , res , res2; Real *dotTable , *dDotTable , *d2DotTable; Real *valueTables , *dValueTables; #if BOUNDARY_CONDITIONS PPolynomial baseFunction , leftBaseFunction , rightBaseFunction; PPolynomial dBaseFunction , dLeftBaseFunction , dRightBaseFunction; #else // !BOUNDARY_CONDITIONS PPolynomial baseFunction; PPolynomial dBaseFunction; #endif // BOUNDARY_CONDITIONS PPolynomial* baseFunctions; FunctionData(void); ~FunctionData(void); virtual void setDotTables(const int& flags); virtual void clearDotTables(const int& flags); virtual void setValueTables(const int& flags,const double& smooth=0); virtual void setValueTables(const int& flags,const double& valueSmooth,const double& normalSmooth); virtual void clearValueTables(void); /******************************************************** * Sets the translates and scales of the basis function * up to the prescribed depth * the maximum depth * the basis function * how the functions should be scaled * 0] Value at zero equals 1 * 1] Integral equals 1 * 2] L2-norm equals 1 * specifies if dot-products of derivatives * should be pre-divided by function integrals * spcifies if function space should be * forced to be reflectively symmetric across the boundary ********************************************************/ #if BOUNDARY_CONDITIONS void set( const int& maxDepth , const PPolynomial& F , const int& normalize , bool useDotRatios=true , bool reflectBoundary=false ); #else // !BOUNDARY_CONDITIONS void set(const int& maxDepth,const PPolynomial& F,const int& normalize , bool useDotRatios=true ); #endif // BOUNDARY_CONDITIONS #if BOUNDARY_CONDITIONS Real dotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 , int boundary1 , int boundary2 ) const; Real dDotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 , int boundary1 , int boundary2 ) const; Real d2DotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 , int boundary1 , int boundary2 ) const; #else // !BOUNDARY_CONDITIONS Real dotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 ) const; Real dDotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 ) const; Real d2DotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 ) const; #endif // BOUNDARY_CONDITIONS static inline int SymmetricIndex( const int& i1 , const int& i2 ); static inline int SymmetricIndex( const int& i1 , const int& i2 , int& index ); }; #include "FunctionData.inl" #endif // FUNCTION_DATA_INCLUDEDcolmap-3.9.1/src/thirdparty/PoissonRecon/FunctionData.inl000077500000000000000000000361061454702036400235020ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ ////////////////// // FunctionData // ////////////////// template FunctionData::FunctionData(void) { dotTable=dDotTable=d2DotTable=NULL; valueTables=dValueTables=NULL; res=0; } template FunctionData::~FunctionData(void) { if(res) { if( dotTable) delete[] dotTable; if( dDotTable) delete[] dDotTable; if(d2DotTable) delete[] d2DotTable; if( valueTables) delete[] valueTables; if(dValueTables) delete[] dValueTables; } dotTable=dDotTable=d2DotTable=NULL; valueTables=dValueTables=NULL; res=0; } template #if BOUNDARY_CONDITIONS void FunctionData::set( const int& maxDepth , const PPolynomial& F , const int& normalize , bool useDotRatios , bool reflectBoundary ) #else // !BOUNDARY_CONDITIONS void FunctionData::set(const int& maxDepth,const PPolynomial& F,const int& normalize , bool useDotRatios ) #endif // BOUNDARY_CONDITIONS { this->normalize = normalize; this->useDotRatios = useDotRatios; #if BOUNDARY_CONDITIONS this->reflectBoundary = reflectBoundary; #endif // BOUNDARY_CONDITIONS depth = maxDepth; res = BinaryNode::CumulativeCenterCount( depth ); res2 = (1<<(depth+1))+1; baseFunctions = new PPolynomial[res]; // Scale the function so that it has: // 0] Value 1 at 0 // 1] Integral equal to 1 // 2] Square integral equal to 1 switch( normalize ) { case 2: baseFunction=F/sqrt((F*F).integral(F.polys[0].start,F.polys[F.polyCount-1].start)); break; case 1: baseFunction=F/F.integral(F.polys[0].start,F.polys[F.polyCount-1].start); break; default: baseFunction=F/F(0); } dBaseFunction = baseFunction.derivative(); #if BOUNDARY_CONDITIONS leftBaseFunction = baseFunction + baseFunction.shift( -1 ); rightBaseFunction = baseFunction + baseFunction.shift( 1 ); dLeftBaseFunction = leftBaseFunction.derivative(); dRightBaseFunction = rightBaseFunction.derivative(); #endif // BOUNDARY_CONDITIONS double c1,w1; for( int i=0 ; i::CenterAndWidth( i , c1 , w1 ); #if BOUNDARY_CONDITIONS if( reflectBoundary ) { int d , off; BinaryNode< double >::DepthAndOffset( i , d , off ); if ( off==0 ) baseFunctions[i] = leftBaseFunction.scale( w1 ).shift( c1 ); else if( off==((1< void FunctionData::setDotTables( const int& flags ) { clearDotTables( flags ); int size; size = ( res*res + res )>>1; if( flags & DOT_FLAG ) { dotTable = new Real[size]; memset( dotTable , 0 , sizeof(Real)*size ); } if( flags & D_DOT_FLAG ) { dDotTable = new Real[size]; memset( dDotTable , 0 , sizeof(Real)*size ); } if( flags & D2_DOT_FLAG ) { d2DotTable = new Real[size]; memset( d2DotTable , 0 , sizeof(Real)*size ); } double t1 , t2; t1 = baseFunction.polys[0].start; t2 = baseFunction.polys[baseFunction.polyCount-1].start; for( int i=0 ; i::CenterAndWidth( i , c1 , w1 ); #if BOUNDARY_CONDITIONS int d1 , d2 , off1 , off2; BinaryNode< double >::DepthAndOffset( i , d1 , off1 ); int boundary1 = 0; if ( reflectBoundary && off1==0 ) boundary1 = -1; else if( reflectBoundary && off1==( (1<::CenterAndWidth( j , c2 , w2 ); #if BOUNDARY_CONDITIONS BinaryNode< double >::DepthAndOffset( j , d2 , off2 ); int boundary2 = 0; if ( reflectBoundary && off2==0 ) boundary2 = -1; else if( reflectBoundary && off2==( (1<1 ) start = 1; if( end <0 ) end = 0; if( end >1 ) end = 1; } #endif // BOUNDARY_CONDITIONS if( start< start1 ) start = start1; if( end > end1 ) end = end1; if( start>= end ) continue; #if BOUNDARY_CONDITIONS Real dot = dotProduct( c1 , w1 , c2 , w2 , boundary1 , boundary2 ); #else // !BOUNDARY_CONDITIONS Real dot = dotProduct( c1 , w1 , c2 , w2 ); #endif // BOUNDARY_CONDITIONS if( fabs(dot)<1e-15 ) continue; if( flags & DOT_FLAG ) dotTable[idx]=dot; if( useDotRatios ) { #if BOUNDARY_CONDITIONS if( flags & D_DOT_FLAG ) dDotTable[idx] = -dDotProduct( c1 , w1 , c2 , w2 , boundary1 , boundary2 ) / dot; if( flags & D2_DOT_FLAG ) d2DotTable[idx] = d2DotProduct( c1 , w1 , c2 , w2 , boundary1 , boundary2 ) / dot; #else // !BOUNDARY_CONDITIONS if( flags & D_DOT_FLAG ) dDotTable[idx] = -dDotProduct(c1,w1,c2,w2)/dot; if( flags & D2_DOT_FLAG ) d2DotTable[idx] = d2DotProduct(c1,w1,c2,w2)/dot; #endif // BOUNDARY_CONDITIONS } else { #if BOUNDARY_CONDITIONS if( flags & D_DOT_FLAG ) dDotTable[idx] = dDotProduct( c1 , w1 , c2 , w2 , boundary1 , boundary2 ); if( flags & D2_DOT_FLAG ) d2DotTable[idx] = d2DotProduct( c1 , w1 , c2 , w2 , boundary1 , boundary2 ); #else // !BOUNDARY_CONDTIONS if( flags & D_DOT_FLAG ) dDotTable[idx] = dDotProduct(c1,w1,c2,w2); if( flags & D2_DOT_FLAG ) d2DotTable[idx] = d2DotProduct(c1,w1,c2,w2); #endif // BOUNDARY_CONDITIONS } } } } template void FunctionData::clearDotTables( const int& flags ) { if((flags & DOT_FLAG) && dotTable) { delete[] dotTable; dotTable=NULL; } if((flags & D_DOT_FLAG) && dDotTable) { delete[] dDotTable; dDotTable=NULL; } if((flags & D2_DOT_FLAG) && d2DotTable) { delete[] d2DotTable; d2DotTable=NULL; } } template void FunctionData::setValueTables( const int& flags , const double& smooth ) { clearValueTables(); if( flags & VALUE_FLAG ) valueTables = new Real[res*res2]; if( flags & D_VALUE_FLAG ) dValueTables = new Real[res*res2]; PPolynomial function; PPolynomial dFunction; for( int i=0 ; i0) { function=baseFunctions[i].MovingAverage(smooth); dFunction=baseFunctions[i].derivative().MovingAverage(smooth); } else { function=baseFunctions[i]; dFunction=baseFunctions[i].derivative(); } for( int j=0 ; j void FunctionData::setValueTables(const int& flags,const double& valueSmooth,const double& normalSmooth){ clearValueTables(); if(flags & VALUE_FLAG){ valueTables=new Real[res*res2];} if(flags & D_VALUE_FLAG){dValueTables=new Real[res*res2];} PPolynomial function; PPolynomial dFunction; for(int i=0;i0) { function=baseFunctions[i].MovingAverage(valueSmooth);} else { function=baseFunctions[i];} if(normalSmooth>0) {dFunction=baseFunctions[i].derivative().MovingAverage(normalSmooth);} else {dFunction=baseFunctions[i].derivative();} for(int j=0;j void FunctionData::clearValueTables(void){ if( valueTables){delete[] valueTables;} if(dValueTables){delete[] dValueTables;} valueTables=dValueTables=NULL; } #if BOUNDARY_CONDITIONS template Real FunctionData::dotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 , int boundary1 , int boundary2 ) const { const PPolynomial< Degree > *b1 , *b2; if ( boundary1==-1 ) b1 = & leftBaseFunction; else if( boundary1== 0 ) b1 = & baseFunction; else if( boundary1== 1 ) b1 = &rightBaseFunction; if ( boundary2==-1 ) b2 = & leftBaseFunction; else if( boundary2== 0 ) b2 = & baseFunction; else if( boundary2== 1 ) b2 = &rightBaseFunction; double r=fabs( baseFunction.polys[0].start ); switch( normalize ) { case 2: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)*width1/sqrt(width1*width2)); case 1: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)*width1/(width1*width2)); default: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)*width1); } } template Real FunctionData::dDotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 , int boundary1 , int boundary2 ) const { const PPolynomial< Degree-1 > *b1; const PPolynomial< Degree > *b2; if ( boundary1==-1 ) b1 = & dLeftBaseFunction; else if( boundary1== 0 ) b1 = & dBaseFunction; else if( boundary1== 1 ) b1 = &dRightBaseFunction; if ( boundary2==-1 ) b2 = & leftBaseFunction; else if( boundary2== 0 ) b2 = & baseFunction; else if( boundary2== 1 ) b2 = & rightBaseFunction; double r=fabs(baseFunction.polys[0].start); switch(normalize){ case 2: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/sqrt(width1*width2)); case 1: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/(width1*width2)); default: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)); } } template Real FunctionData::d2DotProduct( const double& center1 , const double& width1 , const double& center2 , const double& width2 , int boundary1 , int boundary2 ) const { const PPolynomial< Degree-1 > *b1 , *b2; if ( boundary1==-1 ) b1 = & dLeftBaseFunction; else if( boundary1== 0 ) b1 = & dBaseFunction; else if( boundary1== 1 ) b1 = &dRightBaseFunction; if ( boundary2==-1 ) b2 = & dLeftBaseFunction; else if( boundary2== 0 ) b2 = & dBaseFunction; else if( boundary2== 1 ) b2 = &dRightBaseFunction; double r=fabs(baseFunction.polys[0].start); switch( normalize ) { case 2: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/width2/sqrt(width1*width2)); case 1: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/width2/(width1*width2)); default: return Real(((*b1)*b2->scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/width2); } } #else // !BOUNDARY_CONDITIONS template Real FunctionData::dotProduct(const double& center1,const double& width1,const double& center2,const double& width2) const{ double r=fabs(baseFunction.polys[0].start); switch( normalize ) { case 2: return Real((baseFunction*baseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)*width1/sqrt(width1*width2)); case 1: return Real((baseFunction*baseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)*width1/(width1*width2)); default: return Real((baseFunction*baseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)*width1); } } template Real FunctionData::dDotProduct(const double& center1,const double& width1,const double& center2,const double& width2) const{ double r=fabs(baseFunction.polys[0].start); switch(normalize){ case 2: return Real((dBaseFunction*baseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/sqrt(width1*width2)); case 1: return Real((dBaseFunction*baseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/(width1*width2)); default: return Real((dBaseFunction*baseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)); } } template Real FunctionData::d2DotProduct(const double& center1,const double& width1,const double& center2,const double& width2) const{ double r=fabs(baseFunction.polys[0].start); switch(normalize){ case 2: return Real((dBaseFunction*dBaseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/width2/sqrt(width1*width2)); case 1: return Real((dBaseFunction*dBaseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/width2/(width1*width2)); default: return Real((dBaseFunction*dBaseFunction.scale(width2/width1).shift((center2-center1)/width1)).integral(-2*r,2*r)/width2); } } #endif // BOUNDARY_CONDITIONS template inline int FunctionData::SymmetricIndex( const int& i1 , const int& i2 ) { if( i1>i2 ) return ((i1*i1+i1)>>1)+i2; else return ((i2*i2+i2)>>1)+i1; } template inline int FunctionData::SymmetricIndex( const int& i1 , const int& i2 , int& index ) { if( i1>1)+i1; return 1; } else{ index = ((i1*i1+i1)>>1)+i2; return 0; } } colmap-3.9.1/src/thirdparty/PoissonRecon/Geometry.cpp000066400000000000000000000075241454702036400227150ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include "Geometry.h" #include #include #ifdef _WIN32 #include #endif // _WIN32 /////////////////// // CoredMeshData // /////////////////// TriangulationEdge::TriangulationEdge(void){pIndex[0]=pIndex[1]=tIndex[0]=tIndex[1]=-1;} TriangulationTriangle::TriangulationTriangle(void){eIndex[0]=eIndex[1]=eIndex[2]=-1;} /////////////////////////// // BufferedReadWriteFile // /////////////////////////// BufferedReadWriteFile::BufferedReadWriteFile( char* fileName , int bufferSize ) { _bufferIndex = 0; _bufferSize = bufferSize; if( fileName ) strcpy( _fileName , fileName ) , tempFile = false , _fp = fopen( _fileName , "w+b" ); else { strcpy( _fileName , "PR_XXXXXX" ); #ifdef _WIN32 _mktemp( _fileName ); _fp = fopen( _fileName , "w+b" ); #else // !_WIN32 _fp = fdopen( mkstemp( _fileName ) , "w+b" ); #endif // _WIN32 tempFile = true; } if( !_fp ) fprintf( stderr , "[ERROR] Failed to open file: %s\n" , _fileName ) , exit( 0 ); _buffer = (char*) malloc( _bufferSize ); } BufferedReadWriteFile::~BufferedReadWriteFile( void ) { free( _buffer ); fclose( _fp ); if( tempFile ) remove( _fileName ); } void BufferedReadWriteFile::reset( void ) { if( _bufferIndex ) fwrite( _buffer , 1 , _bufferIndex , _fp ); _bufferIndex = 0; fseek( _fp , 0 , SEEK_SET ); _bufferIndex = 0; _bufferSize = fread( _buffer , 1 , _bufferSize , _fp ); } bool BufferedReadWriteFile::write( const void* data , size_t size ) { if( !size ) return true; char* _data = (char*) data; size_t sz = _bufferSize - _bufferIndex; while( sz<=size ) { memcpy( _buffer+_bufferIndex , _data , sz ); fwrite( _buffer , 1 , _bufferSize , _fp ); _data += sz; size -= sz; _bufferIndex = 0; sz = _bufferSize; } if( size ) { memcpy( _buffer+_bufferIndex , _data , size ); _bufferIndex += size; } return true; } bool BufferedReadWriteFile::read( void* data , size_t size ) { if( !size ) return true; char *_data = (char*) data; size_t sz = _bufferSize - _bufferIndex; while( sz<=size ) { if( size && !_bufferSize ) return false; memcpy( _data , _buffer+_bufferIndex , sz ); _bufferSize = fread( _buffer , 1 , _bufferSize , _fp ); _data += sz; size -= sz; _bufferIndex = 0; if( !size ) return true; sz = _bufferSize; } if( size ) { if( !_bufferSize ) return false; memcpy( _data , _buffer+_bufferIndex , size ); _bufferIndex += size; } return true; }colmap-3.9.1/src/thirdparty/PoissonRecon/Geometry.h000066400000000000000000000306461454702036400223630ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #ifndef GEOMETRY_INCLUDED #define GEOMETRY_INCLUDED #include #include #include #include #include "Hash.h" template Real Random(void); template< class Real > struct Point3D { Real coords[3]; Point3D( void ) { coords[0] = coords[1] = coords[2] = Real(0); } Point3D( Real v ) { coords[0] = coords[1] = coords[2] = v; } template< class _Real > Point3D( _Real v0 , _Real v1 , _Real v2 ){ coords[0] = Real(v0) , coords[1] = Real(v1) , coords[2] = Real(v2); } template< class _Real > Point3D( const Point3D< _Real >& p ){ coords[0] = Real( p[0] ) , coords[1] = Real( p[1] ) , coords[2] = Real( p[2] ); } inline Real& operator[] ( int i ) { return coords[i]; } inline const Real& operator[] ( int i ) const { return coords[i]; } inline Point3D operator - ( void ) const { Point3D q ; q.coords[0] = -coords[0] , q.coords[1] = -coords[1] , q.coords[2] = -coords[2] ; return q; } template< class _Real > inline Point3D& operator += ( Point3D< _Real > p ){ coords[0] += Real(p.coords[0]) , coords[1] += Real(p.coords[1]) , coords[2] += Real(p.coords[2]) ; return *this; } template< class _Real > inline Point3D operator + ( Point3D< _Real > p ) const { Point3D q ; q.coords[0] = coords[0] + Real(p.coords[0]) , q.coords[1] = coords[1] + Real(p.coords[1]) , q.coords[2] = coords[2] + Real(p.coords[2]) ; return q; } template< class _Real > inline Point3D& operator *= ( _Real r ) { coords[0] *= Real(r) , coords[1] *= Real(r) , coords[2] *= Real(r) ; return *this; } template< class _Real > inline Point3D operator * ( _Real r ) const { Point3D q ; q.coords[0] = coords[0] * Real(r) , q.coords[1] = coords[1] * Real(r) , q.coords[2] = coords[2] * Real(r) ; return q; } template< class _Real > inline Point3D& operator -= ( Point3D< _Real > p ){ return ( (*this)+=(-p) ); } template< class _Real > inline Point3D operator - ( Point3D< _Real > p ) const { return (*this)+(-p); } template< class _Real > inline Point3D& operator /= ( _Real r ){ return ( (*this)*=Real(1./r) ); } template< class _Real > inline Point3D operator / ( _Real r ) const { return (*this) * ( Real(1.)/r ); } static Real Dot( const Point3D< Real >& p1 , const Point3D< Real >& p2 ){ return p1.coords[0]*p2.coords[0] + p1.coords[1]*p2.coords[1] + p1.coords[2]*p2.coords[2]; } template< class Real1 , class Real2 > static Real Dot( const Point3D< Real1 >& p1 , const Point3D< Real2 >& p2 ){ return Real( p1.coords[0]*p2.coords[0] + p1.coords[1]*p2.coords[1] + p1.coords[2]*p2.coords[2] ); } }; template< class Real > struct XForm3x3 { Real coords[3][3]; XForm3x3( void ) { for( int i=0 ; i<3 ; i++ ) for( int j=0 ; j<3 ; j++ ) coords[i][j] = Real(0.); } static XForm3x3 Identity( void ) { XForm3x3 xForm; xForm(0,0) = xForm(1,1) = xForm(2,2) = Real(1.); return xForm; } Real& operator() ( int i , int j ){ return coords[i][j]; } const Real& operator() ( int i , int j ) const { return coords[i][j]; } template< class _Real > Point3D< _Real > operator * ( const Point3D< _Real >& p ) const { Point3D< _Real > q; for( int i=0 ; i<3 ; i++ ) for( int j=0 ; j<3 ; j++ ) q[i] += _Real( coords[j][i] * p[j] ); return q; } XForm3x3 operator * ( const XForm3x3& m ) const { XForm3x3 n; for( int i=0 ; i<3 ; i++ ) for( int j=0 ; j<3 ; j++ ) for( int k=0 ; k<3 ; k++ ) n.coords[i][j] += m.coords[i][k]*coords[k][j]; return n; } XForm3x3 transpose( void ) const { XForm3x3 xForm; for( int i=0 ; i<3 ; i++ ) for( int j=0 ; j<3 ; j++ ) xForm( i , j ) = coords[j][i]; return xForm; } Real subDeterminant( int i , int j ) const { int i1 = (i+1)%3 , i2 = (i+2)%3; int j1 = (j+1)%3 , j2 = (j+2)%3; return coords[i1][j1] * coords[i2][j2] - coords[i1][j2] * coords[i2][j1]; } Real determinant( void ) const { return coords[0][0] * subDeterminant( 0 , 0 ) + coords[1][0] * subDeterminant( 1 , 0 ) + coords[2][0] * subDeterminant( 2 , 0 ); } XForm3x3 inverse( void ) const { XForm3x3 xForm; Real d = determinant(); for( int i=0 ; i<3 ; i++ ) for( int j=0 ; j<3 ;j++ ) xForm.coords[j][i] = subDeterminant( i , j ) / d; return xForm; } }; template< class Real > struct XForm4x4 { Real coords[4][4]; XForm4x4( void ) { for( int i=0 ; i<4 ; i++ ) for( int j=0 ; j<4 ; j++ ) coords[i][j] = Real(0.); } static XForm4x4 Identity( void ) { XForm4x4 xForm; xForm(0,0) = xForm(1,1) = xForm(2,2) = xForm(3,3) = Real(1.); return xForm; } Real& operator() ( int i , int j ){ return coords[i][j]; } const Real& operator() ( int i , int j ) const { return coords[i][j]; } template< class _Real > Point3D< _Real > operator * ( const Point3D< _Real >& p ) const { Point3D< _Real > q; for( int i=0 ; i<3 ; i++ ) { for( int j=0 ; j<3 ; j++ ) q[i] += (_Real)( coords[j][i] * p[j] ); q[i] += (_Real)coords[3][i]; } return q; } XForm4x4 operator * ( const XForm4x4& m ) const { XForm4x4 n; for( int i=0 ; i<4 ; i++ ) for( int j=0 ; j<4 ; j++ ) for( int k=0 ; k<4 ; k++ ) n.coords[i][j] += m.coords[i][k]*coords[k][j]; return n; } XForm4x4 transpose( void ) const { XForm4x4 xForm; for( int i=0 ; i<4 ; i++ ) for( int j=0 ; j<4 ; j++ ) xForm( i , j ) = coords[j][i]; return xForm; } Real subDeterminant( int i , int j ) const { XForm3x3< Real > xForm; int ii[] = { (i+1)%4 , (i+2)%4 , (i+3)%4 } , jj[] = { (j+1)%4 , (j+2)%4 , (j+3)%4 }; for( int _i=0 ; _i<3 ; _i++ ) for( int _j=0 ; _j<3 ; _j++ ) xForm( _i , _j ) = coords[ ii[_i] ][ jj[_j] ]; return xForm.determinant(); } Real determinant( void ) const { return coords[0][0] * subDeterminant( 0 , 0 ) - coords[1][0] * subDeterminant( 1 , 0 ) + coords[2][0] * subDeterminant( 2 , 0 ) - coords[3][0] * subDeterminant( 3 , 0 ); } XForm4x4 inverse( void ) const { XForm4x4 xForm; Real d = determinant(); for( int i=0 ; i<4 ; i++ ) for( int j=0 ; j<4 ;j++ ) if( (i+j)%2==0 ) xForm.coords[j][i] = subDeterminant( i , j ) / d; else xForm.coords[j][i] = -subDeterminant( i , j ) / d; return xForm; } }; template Point3D RandomBallPoint(void); template Point3D RandomSpherePoint(void); template double Length(const Point3D& p); template double SquareLength(const Point3D& p); template double Distance(const Point3D& p1,const Point3D& p2); template double SquareDistance(const Point3D& p1,const Point3D& p2); template void CrossProduct(const Point3D& p1,const Point3D& p2,Point3D& p); class Edge{ public: double p[2][2]; double Length(void) const{ double d[2]; d[0]=p[0][0]-p[1][0]; d[1]=p[0][1]-p[1][1]; return sqrt(d[0]*d[0]+d[1]*d[1]); } }; class Triangle{ public: double p[3][3]; double Area(void) const{ double v1[3] , v2[3] , v[3]; for( int d=0 ; d<3 ; d++ ) { v1[d] = p[1][d] - p[0][d]; v2[d] = p[2][d] - p[0][d]; } v[0] = v1[1]*v2[2] - v1[2]*v2[1]; v[1] = -v1[0]*v2[2] + v1[2]*v2[0]; v[2] = v1[0]*v2[1] - v1[1]*v2[0]; return sqrt( v[0]*v[0] + v[1]*v[1] + v[2]*v[2] ) / 2; } double AspectRatio(void) const{ double d=0; int i,j; for(i=0;i<3;i++){ for(i=0;i<3;i++) for(j=0;j<3;j++){d+=(p[(i+1)%3][j]-p[i][j])*(p[(i+1)%3][j]-p[i][j]);} } return Area()/d; } }; class CoredPointIndex { public: int index; char inCore; int operator == (const CoredPointIndex& cpi) const {return (index==cpi.index) && (inCore==cpi.inCore);}; int operator != (const CoredPointIndex& cpi) const {return (index!=cpi.index) || (inCore!=cpi.inCore);}; }; class EdgeIndex{ public: int idx[2]; }; class CoredEdgeIndex { public: CoredPointIndex idx[2]; }; class TriangleIndex{ public: int idx[3]; }; class TriangulationEdge { public: TriangulationEdge(void); int pIndex[2]; int tIndex[2]; }; class TriangulationTriangle { public: TriangulationTriangle(void); int eIndex[3]; }; template class Triangulation { public: std::vector > points; std::vector edges; std::vector triangles; int factor( int tIndex,int& p1,int& p2,int& p3); double area(void); double area( int tIndex ); double area( int p1 , int p2 , int p3 ); int flipMinimize( int eIndex); int addTriangle( int p1 , int p2 , int p3 ); protected: hash_map edgeMap; static long long EdgeIndex( int p1 , int p2 ); double area(const Triangle& t); }; template void EdgeCollapse(const Real& edgeRatio,std::vector& triangles,std::vector< Point3D >& positions,std::vector >* normals); template void TriangleCollapse(const Real& edgeRatio,std::vector& triangles,std::vector >& positions,std::vector >* normals); struct CoredVertexIndex { int idx; bool inCore; }; template< class Vertex > class CoredMeshData { public: std::vector< Vertex > inCorePoints; virtual void resetIterator( void ) = 0; virtual int addOutOfCorePoint( const Vertex& p ) = 0; virtual int addOutOfCorePoint_s( const Vertex& p ) = 0; virtual int addPolygon_s( const std::vector< CoredVertexIndex >& vertices ) = 0; virtual int addPolygon_s( const std::vector< int >& vertices ) = 0; virtual int nextOutOfCorePoint( Vertex& p )=0; virtual int nextPolygon( std::vector< CoredVertexIndex >& vertices ) = 0; virtual int outOfCorePointCount(void)=0; virtual int polygonCount( void ) = 0; }; template< class Vertex > class CoredVectorMeshData : public CoredMeshData< Vertex > { std::vector< Vertex > oocPoints; std::vector< std::vector< int > > polygons; int polygonIndex; int oocPointIndex; public: CoredVectorMeshData(void); void resetIterator(void); int addOutOfCorePoint( const Vertex& p ); int addOutOfCorePoint_s( const Vertex& p ); int addPolygon_s( const std::vector< CoredVertexIndex >& vertices ); int addPolygon_s( const std::vector< int >& vertices ); int nextOutOfCorePoint( Vertex& p ); int nextPolygon( std::vector< CoredVertexIndex >& vertices ); int outOfCorePointCount(void); int polygonCount( void ); }; class BufferedReadWriteFile { bool tempFile; FILE* _fp; char *_buffer , _fileName[1024]; size_t _bufferIndex , _bufferSize; public: BufferedReadWriteFile( char* fileName=NULL , int bufferSize=(1<<20) ); ~BufferedReadWriteFile( void ); bool write( const void* data , size_t size ); bool read ( void* data , size_t size ); void reset( void ); }; template< class Vertex > class CoredFileMeshData : public CoredMeshData< Vertex > { char pointFileName[1024] , polygonFileName[1024]; BufferedReadWriteFile *oocPointFile , *polygonFile; int oocPoints , polygons; public: CoredFileMeshData( void ); ~CoredFileMeshData( void ); void resetIterator( void ); int addOutOfCorePoint( const Vertex& p ); int addOutOfCorePoint_s( const Vertex& p ); int addPolygon_s( const std::vector< CoredVertexIndex >& vertices ); int addPolygon_s( const std::vector< int >& vertices ); int nextOutOfCorePoint( Vertex& p ); int nextPolygon( std::vector< CoredVertexIndex >& vertices ); int outOfCorePointCount( void ); int polygonCount( void ); }; #include "Geometry.inl" #endif // GEOMETRY_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/Geometry.inl000077500000000000000000000444741454702036400227250ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include template Real Random(void){return Real(rand())/RAND_MAX;} template Point3D RandomBallPoint(void){ Point3D p; while(1){ p.coords[0]=Real(1.0-2.0*Random()); p.coords[1]=Real(1.0-2.0*Random()); p.coords[2]=Real(1.0-2.0*Random()); double l=SquareLength(p); if(l<=1){return p;} } } template Point3D RandomSpherePoint(void){ Point3D p=RandomBallPoint(); Real l=Real(Length(p)); p.coords[0]/=l; p.coords[1]/=l; p.coords[2]/=l; return p; } template double SquareLength(const Point3D& p){return p.coords[0]*p.coords[0]+p.coords[1]*p.coords[1]+p.coords[2]*p.coords[2];} template double Length(const Point3D& p){return sqrt(SquareLength(p));} template double SquareDistance(const Point3D& p1,const Point3D& p2){ return (p1.coords[0]-p2.coords[0])*(p1.coords[0]-p2.coords[0])+(p1.coords[1]-p2.coords[1])*(p1.coords[1]-p2.coords[1])+(p1.coords[2]-p2.coords[2])*(p1.coords[2]-p2.coords[2]); } template double Distance(const Point3D& p1,const Point3D& p2){return sqrt(SquareDistance(p1,p2));} template void CrossProduct(const Point3D& p1,const Point3D& p2,Point3D& p){ p.coords[0]= p1.coords[1]*p2.coords[2]-p1.coords[2]*p2.coords[1]; p.coords[1]=-p1.coords[0]*p2.coords[2]+p1.coords[2]*p2.coords[0]; p.coords[2]= p1.coords[0]*p2.coords[1]-p1.coords[1]*p2.coords[0]; } template void EdgeCollapse(const Real& edgeRatio,std::vector& triangles,std::vector< Point3D >& positions,std::vector< Point3D >* normals){ int i,j,*remapTable,*pointCount,idx[3]; Point3D p[3],q[2],c; double d[3],a; double Ratio=12.0/sqrt(3.0); // (Sum of Squares Length / Area) for and equilateral triangle remapTable=new int[positions.size()]; pointCount=new int[positions.size()]; for(i=0;i=0;i--){ for(j=0;j<3;j++){ idx[j]=triangles[i].idx[j]; while(remapTable[idx[j]] a*Ratio){ // Find the smallest edge j=0; if(d[1]=0;i--){ for(j=0;j<3;j++){ idx[j]=triangles[i].idx[j]; while(remapTable[idx[j]] void TriangleCollapse(const Real& edgeRatio,std::vector& triangles,std::vector< Point3D >& positions,std::vector< Point3D >* normals){ int i,j,*remapTable,*pointCount,idx[3]; Point3D p[3],q[2],c; double d[3],a; double Ratio=12.0/sqrt(3.0); // (Sum of Squares Length / Area) for and equilateral triangle remapTable=new int[positions.size()]; pointCount=new int[positions.size()]; for(i=0;i=0;i--){ for(j=0;j<3;j++){ idx[j]=triangles[i].idx[j]; while(remapTable[idx[j]] a*Ratio){ // Find the smallest edge j=0; if(d[1]=0;i--){ for(j=0;j<3;j++){ idx[j]=triangles[i].idx[j]; while(remapTable[idx[j]] long long Triangulation::EdgeIndex( int p1 , int p2 ) { if(p1>p2) {return ((long long)(p1)<<32) | ((long long)(p2));} else {return ((long long)(p2)<<32) | ((long long)(p1));} } template int Triangulation::factor(int tIndex,int& p1,int& p2,int & p3){ if(triangles[tIndex].eIndex[0]<0 || triangles[tIndex].eIndex[1]<0 || triangles[tIndex].eIndex[2]<0){return 0;} if(edges[triangles[tIndex].eIndex[0]].tIndex[0]==tIndex){p1=edges[triangles[tIndex].eIndex[0]].pIndex[0];} else {p1=edges[triangles[tIndex].eIndex[0]].pIndex[1];} if(edges[triangles[tIndex].eIndex[1]].tIndex[0]==tIndex){p2=edges[triangles[tIndex].eIndex[1]].pIndex[0];} else {p2=edges[triangles[tIndex].eIndex[1]].pIndex[1];} if(edges[triangles[tIndex].eIndex[2]].tIndex[0]==tIndex){p3=edges[triangles[tIndex].eIndex[2]].pIndex[0];} else {p3=edges[triangles[tIndex].eIndex[2]].pIndex[1];} return 1; } template double Triangulation::area(int p1,int p2,int p3){ Point3D q1,q2,q; for(int i=0;i<3;i++){ q1.coords[i]=points[p2].coords[i]-points[p1].coords[i]; q2.coords[i]=points[p3].coords[i]-points[p1].coords[i]; } CrossProduct(q1,q2,q); return Length(q); } template double Triangulation::area(int tIndex){ int p1,p2,p3; factor(tIndex,p1,p2,p3); return area(p1,p2,p3); } template double Triangulation::area(void){ double a=0; for(int i=0;i int Triangulation::addTriangle(int p1,int p2,int p3){ hash_map::iterator iter; int tIdx,eIdx,p[3]; p[0]=p1; p[1]=p2; p[2]=p3; triangles.push_back(TriangulationTriangle()); tIdx=int(triangles.size())-1; for(int i=0;i<3;i++) { long long e = EdgeIndex(p[i],p[(i+1)%3]); iter=edgeMap.find(e); if(iter==edgeMap.end()) { TriangulationEdge edge; edge.pIndex[0]=p[i]; edge.pIndex[1]=p[(i+1)%3]; edges.push_back(edge); eIdx=int(edges.size())-1; edgeMap[e]=eIdx; edges[eIdx].tIndex[0]=tIdx; } else{ eIdx=edgeMap[e]; if(edges[eIdx].pIndex[0]==p[i]){ if(edges[eIdx].tIndex[0]<0){edges[eIdx].tIndex[0]=tIdx;} else{printf("Edge Triangle in use 1\n");return 0;} } else{ if(edges[eIdx].tIndex[1]<0){edges[eIdx].tIndex[1]=tIdx;} else{printf("Edge Triangle in use 2\n");return 0;} } } triangles[tIdx].eIndex[i]=eIdx; } return tIdx; } template int Triangulation::flipMinimize(int eIndex){ double oldArea,newArea; int oldP[3],oldQ[3],newP[3],newQ[3]; TriangulationEdge newEdge; if(edges[eIndex].tIndex[0]<0 || edges[eIndex].tIndex[1]<0){return 0;} if(!factor(edges[eIndex].tIndex[0],oldP[0],oldP[1],oldP[2])){return 0;} if(!factor(edges[eIndex].tIndex[1],oldQ[0],oldQ[1],oldQ[2])){return 0;} oldArea=area(oldP[0],oldP[1],oldP[2])+area(oldQ[0],oldQ[1],oldQ[2]); int idxP,idxQ; for(idxP=0;idxP<3;idxP++){ int i; for(i=0;i<3;i++){if(oldP[idxP]==oldQ[i]){break;}} if(i==3){break;} } for(idxQ=0;idxQ<3;idxQ++){ int i; for(i=0;i<3;i++){if(oldP[i]==oldQ[idxQ]){break;}} if(i==3){break;} } if(idxP==3 || idxQ==3){return 0;} newP[0]=oldP[idxP]; newP[1]=oldP[(idxP+1)%3]; newP[2]=oldQ[idxQ]; newQ[0]=oldQ[idxQ]; newQ[1]=oldP[(idxP+2)%3]; newQ[2]=oldP[idxP]; newArea=area(newP[0],newP[1],newP[2])+area(newQ[0],newQ[1],newQ[2]); if(oldArea<=newArea){return 0;} // Remove the entry in the hash_table for the old edge edgeMap.erase(EdgeIndex(edges[eIndex].pIndex[0],edges[eIndex].pIndex[1])); // Set the new edge so that the zero-side is newQ edges[eIndex].pIndex[0]=newP[0]; edges[eIndex].pIndex[1]=newQ[0]; // Insert the entry into the hash_table for the new edge edgeMap[EdgeIndex(newP[0],newQ[0])]=eIndex; // Update the triangle information for(int i=0;i<3;i++){ int idx; idx=edgeMap[EdgeIndex(newQ[i],newQ[(i+1)%3])]; triangles[edges[eIndex].tIndex[0]].eIndex[i]=idx; if(idx!=eIndex){ if(edges[idx].tIndex[0]==edges[eIndex].tIndex[1]){edges[idx].tIndex[0]=edges[eIndex].tIndex[0];} if(edges[idx].tIndex[1]==edges[eIndex].tIndex[1]){edges[idx].tIndex[1]=edges[eIndex].tIndex[0];} } idx=edgeMap[EdgeIndex(newP[i],newP[(i+1)%3])]; triangles[edges[eIndex].tIndex[1]].eIndex[i]=idx; if(idx!=eIndex){ if(edges[idx].tIndex[0]==edges[eIndex].tIndex[0]){edges[idx].tIndex[0]=edges[eIndex].tIndex[1];} if(edges[idx].tIndex[1]==edges[eIndex].tIndex[0]){edges[idx].tIndex[1]=edges[eIndex].tIndex[1];} } } return 1; } ///////////////////////// // CoredVectorMeshData // ///////////////////////// template< class Vertex > CoredVectorMeshData< Vertex >::CoredVectorMeshData( void ) { oocPointIndex = polygonIndex = 0; } template< class Vertex > void CoredVectorMeshData< Vertex >::resetIterator ( void ) { oocPointIndex = polygonIndex = 0; } template< class Vertex > int CoredVectorMeshData< Vertex >::addOutOfCorePoint( const Vertex& p ) { oocPoints.push_back(p); return int(oocPoints.size())-1; } template< class Vertex > int CoredVectorMeshData< Vertex >::addOutOfCorePoint_s( const Vertex& p ) { size_t sz; #pragma omp critical (CoredVectorMeshData_addOutOfCorePoint_s ) { sz = oocPoints.size(); oocPoints.push_back(p); } return (int)sz; } template< class Vertex > int CoredVectorMeshData< Vertex >::addPolygon_s( const std::vector< int >& polygon ) { size_t sz; #pragma omp critical (CoredVectorMeshData_addPolygon_s) { sz = polygon.size(); polygons.push_back( polygon ); } return (int)sz; } template< class Vertex > int CoredVectorMeshData< Vertex >::addPolygon_s( const std::vector< CoredVertexIndex >& vertices ) { std::vector< int > polygon( vertices.size() ); for( int i=0 ; i<(int)vertices.size() ; i++ ) if( vertices[i].inCore ) polygon[i] = vertices[i].idx; else polygon[i] = -vertices[i].idx-1; return addPolygon_s( polygon ); } template< class Vertex > int CoredVectorMeshData< Vertex >::nextOutOfCorePoint( Vertex& p ) { if( oocPointIndex int CoredVectorMeshData< Vertex >::nextPolygon( std::vector< CoredVertexIndex >& vertices ) { if( polygonIndex& polygon = polygons[ polygonIndex++ ]; vertices.resize( polygon.size() ); for( int i=0 ; i int CoredVectorMeshData< Vertex >::outOfCorePointCount(void){return int(oocPoints.size());} template< class Vertex > int CoredVectorMeshData< Vertex >::polygonCount( void ) { return int( polygons.size() ); } /////////////////////// // CoredFileMeshData // /////////////////////// template< class Vertex > CoredFileMeshData< Vertex >::CoredFileMeshData( void ) { oocPoints = polygons = 0; oocPointFile = new BufferedReadWriteFile(); polygonFile = new BufferedReadWriteFile(); } template< class Vertex > CoredFileMeshData< Vertex >::~CoredFileMeshData( void ) { delete oocPointFile; delete polygonFile; } template< class Vertex > void CoredFileMeshData< Vertex >::resetIterator ( void ) { oocPointFile->reset(); polygonFile->reset(); } template< class Vertex > int CoredFileMeshData< Vertex >::addOutOfCorePoint( const Vertex& p ) { oocPointFile->write( &p , sizeof( Vertex ) ); oocPoints++; return oocPoints-1; } template< class Vertex > int CoredFileMeshData< Vertex >::addOutOfCorePoint_s( const Vertex& p ) { int sz; #pragma omp critical (CoredFileMeshData_addOutOfCorePoint_s) { sz = oocPoints; oocPointFile->write( &p , sizeof( Vertex ) ); oocPoints++; } return sz; } template< class Vertex > int CoredFileMeshData< Vertex >::addPolygon_s( const std::vector< int >& vertices ) { int sz , vSize = (int)vertices.size(); #pragma omp critical (CoredFileMeshData_addPolygon_s ) { sz = polygons; polygonFile->write( &vSize , sizeof(int) ); polygonFile->write( &vertices[0] , sizeof(int) * vSize ); polygons++; } return sz; } template< class Vertex > int CoredFileMeshData< Vertex >::addPolygon_s( const std::vector< CoredVertexIndex >& vertices ) { std::vector< int > polygon( vertices.size() ); for( int i=0 ; i<(int)vertices.size() ; i++ ) if( vertices[i].inCore ) polygon[i] = vertices[i].idx; else polygon[i] = -vertices[i].idx-1; return addPolygon_s( polygon ); } template< class Vertex > int CoredFileMeshData< Vertex >::nextOutOfCorePoint( Vertex& p ) { if( oocPointFile->read( &p , sizeof( Vertex ) ) ) return 1; else return 0; } template< class Vertex > int CoredFileMeshData< Vertex >::nextPolygon( std::vector< CoredVertexIndex >& vertices ) { int pSize; if( polygonFile->read( &pSize , sizeof(int) ) ) { std::vector< int > polygon( pSize ); if( polygonFile->read( &polygon[0] , sizeof(int)*pSize ) ) { vertices.resize( pSize ); for( int i=0 ; i int CoredFileMeshData< Vertex >::outOfCorePointCount( void ){ return oocPoints; } template< class Vertex > int CoredFileMeshData< Vertex >::polygonCount( void ) { return polygons; } colmap-3.9.1/src/thirdparty/PoissonRecon/Hash.h000066400000000000000000000002011454702036400214330ustar00rootroot00000000000000#ifndef HASH_INCLUDED #define HASH_INCLUDED #include #define hash_map std::unordered_map #endif // HASH_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/LICENSE000066400000000000000000000020631454702036400214140ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 mkazhdan 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 PARTICULAR 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. colmap-3.9.1/src/thirdparty/PoissonRecon/MAT.h000066400000000000000000000041761454702036400212100ustar00rootroot00000000000000/* Copyright (c) 2007, Michael Kazhdan 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 the Johns Hopkins University 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 OWNER 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. */ #ifndef MAT_INCLUDED #define MAT_INCLUDED #include "Geometry.h" template class MinimalAreaTriangulation { Real* bestTriangulation; int* midPoint; Real GetArea(const size_t& i,const size_t& j,const std::vector >& vertices); void GetTriangulation(const size_t& i,const size_t& j,const std::vector >& vertices,std::vector& triangles); public: MinimalAreaTriangulation(void); ~MinimalAreaTriangulation(void); Real GetArea(const std::vector >& vertices); void GetTriangulation(const std::vector >& vertices,std::vector& triangles); }; #include "MAT.inl" #endif // MAT_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/MAT.inl000077500000000000000000000131631454702036400215420ustar00rootroot00000000000000/* Copyright (c) 2007, Michael Kazhdan 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 the Johns Hopkins University 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 OWNER 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. */ ////////////////////////////// // MinimalAreaTriangulation // ////////////////////////////// template MinimalAreaTriangulation::MinimalAreaTriangulation(void) { bestTriangulation=NULL; midPoint=NULL; } template MinimalAreaTriangulation::~MinimalAreaTriangulation(void) { if(bestTriangulation) delete[] bestTriangulation; bestTriangulation=NULL; if(midPoint) delete[] midPoint; midPoint=NULL; } template void MinimalAreaTriangulation::GetTriangulation(const std::vector >& vertices,std::vector& triangles) { if(vertices.size()==3) { triangles.resize(1); triangles[0].idx[0]=0; triangles[0].idx[1]=1; triangles[0].idx[2]=2; return; } else if(vertices.size()==4) { TriangleIndex tIndex[2][2]; Real area[2]; area[0]=area[1]=0; triangles.resize(2); tIndex[0][0].idx[0]=0; tIndex[0][0].idx[1]=1; tIndex[0][0].idx[2]=2; tIndex[0][1].idx[0]=2; tIndex[0][1].idx[1]=3; tIndex[0][1].idx[2]=0; tIndex[1][0].idx[0]=0; tIndex[1][0].idx[1]=1; tIndex[1][0].idx[2]=3; tIndex[1][1].idx[0]=3; tIndex[1][1].idx[1]=1; tIndex[1][1].idx[2]=2; Point3D n,p1,p2; for(int i=0;i<2;i++) for(int j=0;j<2;j++) { p1=vertices[tIndex[i][j].idx[1]]-vertices[tIndex[i][j].idx[0]]; p2=vertices[tIndex[i][j].idx[2]]-vertices[tIndex[i][j].idx[0]]; CrossProduct(p1,p2,n); area[i] += Real( Length(n) ); } if(area[0]>area[1]) { triangles[0]=tIndex[1][0]; triangles[1]=tIndex[1][1]; } else { triangles[0]=tIndex[0][0]; triangles[1]=tIndex[0][1]; } return; } if(bestTriangulation) delete[] bestTriangulation; if(midPoint) delete[] midPoint; bestTriangulation=NULL; midPoint=NULL; size_t eCount=vertices.size(); bestTriangulation=new Real[eCount*eCount]; midPoint=new int[eCount*eCount]; for(size_t i=0;i Real MinimalAreaTriangulation::GetArea(const std::vector >& vertices) { if(bestTriangulation) delete[] bestTriangulation; if(midPoint) delete[] midPoint; bestTriangulation=NULL; midPoint=NULL; int eCount=vertices.size(); bestTriangulation=new double[eCount*eCount]; midPoint=new int[eCount*eCount]; for(int i=0;i void MinimalAreaTriangulation::GetTriangulation(const size_t& i,const size_t& j,const std::vector >& vertices,std::vector& triangles) { TriangleIndex tIndex; size_t eCount=vertices.size(); size_t ii=i; if(i=ii) return; ii=midPoint[i*eCount+j]; if(ii>=0) { tIndex.idx[0] = int( i ); tIndex.idx[1] = int( j ); tIndex.idx[2] = int( ii ); triangles.push_back(tIndex); GetTriangulation(i,ii,vertices,triangles); GetTriangulation(ii,j,vertices,triangles); } } template Real MinimalAreaTriangulation::GetArea(const size_t& i,const size_t& j,const std::vector >& vertices) { Real a=FLT_MAX,temp; size_t eCount=vertices.size(); size_t idx=i*eCount+j; size_t ii=i; if(i=ii) { bestTriangulation[idx]=0; return 0; } if(midPoint[idx]!=-1) return bestTriangulation[idx]; int mid=-1; for(size_t r=j+1;r p,p1,p2; p1=vertices[i]-vertices[rr]; p2=vertices[j]-vertices[rr]; CrossProduct(p1,p2,p); temp = Real( Length(p) ); if(bestTriangulation[idx1]>=0) { temp+=bestTriangulation[idx1]; if(temp>a) continue; if(bestTriangulation[idx2]>0) temp+=bestTriangulation[idx2]; else temp+=GetArea(rr,j,vertices); } else { if(bestTriangulation[idx2]>=0) temp+=bestTriangulation[idx2]; else temp+=GetArea(rr,j,vertices); if(temp>a) continue; temp+=GetArea(i,rr,vertices); } if(temp #include "MarchingCubes.h" //////////// // Square // //////////// int Square::AntipodalCornerIndex(int idx){ int x,y; FactorCornerIndex(idx,x,y); return CornerIndex( (x+1)%2 , (y+1)%2 ); } int Square::CornerIndex( int x , int y ){ return (y<<1)|x; } void Square::FactorCornerIndex( int idx , int& x , int& y ){ x=(idx>>0)&1 , y=(idx>>1)&1; } int Square::EdgeIndex( int orientation , int i ) { switch( orientation ) { case 0: // x if( !i ) return 0; // (0,0) -> (1,0) else return 2; // (0,1) -> (1,1) case 1: // y if( !i ) return 3; // (0,0) -> (0,1) else return 1; // (1,0) -> (1,1) }; return -1; } void Square::FactorEdgeIndex(int idx,int& orientation,int& i){ switch(idx){ case 0: case 2: orientation=0; i=idx/2; return; case 1: case 3: orientation=1; i=((idx/2)+1)%2; return; }; } void Square::EdgeCorners(int idx,int& c1,int& c2){ int orientation,i; FactorEdgeIndex(idx,orientation,i); switch(orientation){ case 0: c1 = CornerIndex(0,i); c2 = CornerIndex(1,i); break; case 1: c1 = CornerIndex(i,0); c2 = CornerIndex(i,1); break; }; } int Square::ReflectEdgeIndex(int idx,int edgeIndex){ int orientation=edgeIndex%2; int o,i; FactorEdgeIndex(idx,o,i); if(o!=orientation){return idx;} else{return EdgeIndex(o,(i+1)%2);} } int Square::ReflectCornerIndex(int idx,int edgeIndex){ int orientation=edgeIndex%2; int x,y; FactorCornerIndex(idx,x,y); switch(orientation){ case 0: return CornerIndex((x+1)%2,y); case 1: return CornerIndex(x,(y+1)%2); }; return -1; } ////////// // Cube // ////////// int Cube::CornerIndex( int x , int y , int z ){ return (z<<2)|(y<<1)|x; } void Cube::FactorCornerIndex( int idx , int& x , int& y , int& z ){ x = (idx>>0)&1 , y = (idx>>1)&1 , z = (idx>>2)&1; } int Cube::EdgeIndex(int orientation,int i,int j){return (i | (j<<1))|(orientation<<2);} void Cube::FactorEdgeIndex( int idx , int& orientation , int& i , int &j ) { orientation=idx>>2; i = (idx&1); j = (idx&2)>>1; } int Cube::FaceIndex( int x , int y , int z ) { if ( x<0 ) return 0; else if( x>0 ) return 1; else if( y<0 ) return 2; else if( y>0 ) return 3; else if( z<0 ) return 4; else if( z>0 ) return 5; else return -1; } int Cube::FaceIndex( int dir , int offSet ){ return (dir<<1)|offSet; } void Cube::FactorFaceIndex( int idx , int& x , int& y , int& z ) { x=y=z=0; switch( idx ) { case 0: x=-1; break; case 1: x= 1; break; case 2: y=-1; break; case 3: y= 1; break; case 4: z=-1; break; case 5: z= 1; break; }; } void Cube::FactorFaceIndex( int idx , int& dir , int& offSet ) { dir = idx>>1; offSet=idx &1; } bool Cube::IsEdgeCorner( int cIndex , int e ) { int o , i , j; FactorEdgeIndex( e , o , i , j ); switch( o ) { case 0: return (cIndex && 2)==(i<<1) && (cIndex && 4)==(j<<2); case 1: return (cIndex && 1)==(i<<0) && (cIndex && 4)==(j<<2); case 2: return (cIndex && 4)==(i<<2) && (cIndex && 2)==(j<<1); default: return false; } } bool Cube::IsFaceCorner( int cIndex , int f ) { int dir , off; FactorFaceIndex( f , dir , off ); return ( cIndex & (1< (1,0) 1} // (1,0) -> (1,1) 2} // (0,1) -> (1,1) 3} // (0,0) -> (0,1) */ const int MarchingSquares::edgeMask[1< -> -> 9, // 1 -> 0 -> (0,0) -> 0,3 -> 9 3, // 2 -> 1 -> (1,0) -> 0,1 -> 3 10, // 3 -> 0,1 -> (0,0) (1,0) -> 1,3 -> 10 12, // 4 -> 2 -> (0,1) -> 2,3 -> 12 5, // 5 -> 0,2 -> (0,0) (0,1) -> 0,2 -> 5 15, // 6 -> 1,2 -> (1,0) (0,1) -> 0,1,2,3 -> 15 6, // 7 -> 0,1,2 -> (0,0) (1,0) (0,1) -> 1,2 -> 6 6, // 8 -> 3 -> (1,1) -> 1,2 -> 6 15, // 9 -> 0,3 -> (0,0) (1,1) -> 0,1,2,3 -> 15 5, // 10 -> 1,3 -> (1,0) (1,1) -> 0,2 -> 5 12, // 11 -> 0,1,3 -> (0,0) (1,0) (1,1) -> 2,3 -> 12 10, // 12 -> 2,3 -> (0,1) (1,1) -> 1,3 -> 10 3, // 13 -> 0,2,3 -> (0,0) (0,1) (1,1) -> 0,1 -> 3 9, // 14 -> 1,2,3 -> (1,0) (0,1) (1,1) -> 0,3 -> 9 0, // 15 -> 0,1,2,3 -> (0,0) (1,0) (0,1) (1,1) -> }; #if NEW_ORDERING /* 0} // (0,0) -> (1,0) 1} // (1,0) -> (1,1) 2} // (0,1) -> (1,1) 3} // (0,0) -> (0,1) */ const int MarchingSquares::edges[1<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(1,i,j)];}}} else if (y<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,0,j)];}}} else if (y>0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,1,j)];}}} else if (z<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,j,0)];}}} else if (z>0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,j,1)];}}} if (v[0][0] < iso) idx |= 1; if (v[1][0] < iso) idx |= 2; if (v[1][1] < iso) idx |= 4; if (v[0][1] < iso) idx |= 8; return idx; } bool MarchingCubes::IsAmbiguous( const double v[Cube::CORNERS] , double isoValue , int faceIndex ){ return MarchingSquares::IsAmbiguous( GetFaceIndex( v , isoValue , faceIndex ) ); } bool MarchingCubes::HasRoots( const double v[Cube::CORNERS] , double isoValue , int faceIndex ){ return MarchingSquares::HasRoots( GetFaceIndex( v , isoValue , faceIndex ) ); } bool MarchingCubes::HasRoots( const double v[Cube::CORNERS] , double isoValue ){ return HasRoots( GetIndex( v , isoValue ) ); } bool MarchingCubes::HasRoots( unsigned char mcIndex ){ return !(mcIndex==0 || mcIndex==255); } int MarchingCubes::AddTriangles( const double v[Cube::CORNERS] , double iso , Triangle* isoTriangles ) { unsigned char idx; int ntriang=0; Triangle tri; idx=GetIndex(v,iso); /* Cube is entirely in/out of the surface */ if (!edgeMask[idx]) return 0; /* Find the vertices where the surface intersects the cube */ int i,j,ii=1; for(i=0;i<12;i++){ if(edgeMask[idx] & ii){SetVertex(i,v,iso);} ii<<=1; } /* Create the triangle */ for( i=0 ; triangles[idx][i]!=-1 ; i+=3 ) { for(j=0;j<3;j++){ tri.p[0][j]=vertexList[triangles[idx][i+0]][j]; tri.p[1][j]=vertexList[triangles[idx][i+1]][j]; tri.p[2][j]=vertexList[triangles[idx][i+2]][j]; } isoTriangles[ntriang++]=tri; } return ntriang; } int MarchingCubes::AddTriangleIndices(const double v[Cube::CORNERS],double iso,int* isoIndices){ unsigned char idx; int ntriang=0; idx=GetIndex(v,iso); /* Cube is entirely in/out of the surface */ if (!edgeMask[idx]) return 0; /* Create the triangle */ for(int i=0;triangles[idx][i]!=-1;i+=3){ for(int j=0;j<3;j++){isoIndices[i+j]=triangles[idx][i+j];} ntriang++; } return ntriang; } void MarchingCubes::SetVertex( int e , const double values[Cube::CORNERS] , double iso ) { double t; int o , i1 , i2; Cube::FactorEdgeIndex( e , o , i1 , i2 ); switch( o ) { case 0: t = Interpolate( values[ Cube::CornerIndex( 0 , i1 , i2 ) ] - iso , values[ Cube::CornerIndex( 1 , i1 , i2 ) ] - iso ); vertexList[e][0] = t , vertexList[e][1] = i1 , vertexList[e][2] = i2; break; case 1: t = Interpolate( values[ Cube::CornerIndex( i1 , 0 , i2 ) ] - iso , values[ Cube::CornerIndex( i1 , 1 , i2 ) ] - iso ); vertexList[e][0] = i1 , vertexList[e][1] = t , vertexList[e][2] = i2; break; case 2: t = Interpolate( values[ Cube::CornerIndex( i1 , i2 , 0 ) ] - iso , values[ Cube::CornerIndex( i1 , i2 , 1 ) ] - iso ); vertexList[e][0] = i1 , vertexList[e][1] = i2 , vertexList[e][2] = t; break; } } double MarchingCubes::Interpolate( double v1 , double v2 ) { return v1/(v1-v2); } /////////////////////////////////// unsigned char MarchingCubes::GetIndex(const float v[Cube::CORNERS],float iso){ unsigned char idx=0; if (v[Cube::CornerIndex(0,0,0)] < iso) idx |= 1; if (v[Cube::CornerIndex(1,0,0)] < iso) idx |= 2; if (v[Cube::CornerIndex(1,1,0)] < iso) idx |= 4; if (v[Cube::CornerIndex(0,1,0)] < iso) idx |= 8; if (v[Cube::CornerIndex(0,0,1)] < iso) idx |= 16; if (v[Cube::CornerIndex(1,0,1)] < iso) idx |= 32; if (v[Cube::CornerIndex(1,1,1)] < iso) idx |= 64; if (v[Cube::CornerIndex(0,1,1)] < iso) idx |= 128; return idx; } unsigned char MarchingCubes::GetFaceIndex( const float values[Cube::CORNERS] , float iso , int faceIndex ) { int i,j,x,y,z; unsigned char idx=0; double v[2][2]; Cube::FactorFaceIndex(faceIndex,x,y,z); if (x<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(0,i,j)];}}} else if (x>0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(1,i,j)];}}} else if (y<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,0,j)];}}} else if (y>0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,1,j)];}}} else if (z<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,j,0)];}}} else if (z>0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=values[Cube::CornerIndex(i,j,1)];}}} if (v[0][0] < iso) idx |= 1; if (v[1][0] < iso) idx |= 2; if (v[1][1] < iso) idx |= 4; if (v[0][1] < iso) idx |= 8; return idx; } unsigned char MarchingCubes::GetFaceIndex( unsigned char mcIndex , int faceIndex ) { int i,j,x,y,z; unsigned char idx=0; int v[2][2]; Cube::FactorFaceIndex(faceIndex,x,y,z); if (x<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=mcIndex&(1<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=mcIndex&(1<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=mcIndex&(1<0){for(i=0;i<2;i++){for(j=0;j<2;j++){v[i][j]=mcIndex&(1< #include "Geometry.h" #define NEW_ORDERING 1 class Square { public: const static unsigned int CORNERS=4 , EDGES=4 , FACES=1; static int CornerIndex (int x,int y); static int AntipodalCornerIndex(int idx); static void FactorCornerIndex (int idx,int& x,int& y); static int EdgeIndex (int orientation,int i); static void FactorEdgeIndex (int idx,int& orientation,int& i); static int ReflectCornerIndex (int idx,int edgeIndex); static int ReflectEdgeIndex (int idx,int edgeIndex); static void EdgeCorners(int idx,int& c1,int &c2); }; class Cube{ public: const static unsigned int CORNERS=8 , EDGES=12 , FACES=6; static int CornerIndex ( int x , int y , int z ); static void FactorCornerIndex ( int idx , int& x , int& y , int& z ); static int EdgeIndex ( int orientation , int i , int j ); static void FactorEdgeIndex ( int idx , int& orientation , int& i , int &j); static int FaceIndex ( int dir , int offSet ); static int FaceIndex ( int x , int y , int z ); static void FactorFaceIndex ( int idx , int& x , int &y , int& z ); static void FactorFaceIndex ( int idx , int& dir , int& offSet ); static int AntipodalCornerIndex ( int idx ); static int FaceReflectCornerIndex ( int idx , int faceIndex ); static int FaceReflectEdgeIndex ( int idx , int faceIndex ); static int FaceReflectFaceIndex ( int idx , int faceIndex ); static int EdgeReflectCornerIndex ( int idx , int edgeIndex ); static int EdgeReflectEdgeIndex ( int edgeIndex ); static int FaceAdjacentToEdges ( int eIndex1 , int eIndex2 ); static void FacesAdjacentToEdge ( int eIndex , int& f1Index , int& f2Index ); static void EdgeCorners( int idx , int& c1 , int &c2 ); static void FaceCorners( int idx , int& c1 , int &c2 , int& c3 , int& c4 ); static bool IsEdgeCorner( int cIndex , int e ); static bool IsFaceCorner( int cIndex , int f ); }; class MarchingSquares { static double Interpolate(double v1,double v2); static void SetVertex(int e,const double values[Square::CORNERS],double iso); public: const static unsigned int MAX_EDGES=2; static const int edgeMask[1< class MemoryInfo { public: size_t TotalPhysicalMemory; size_t FreePhysicalMemory; size_t TotalSwapSpace; size_t FreeSwapSpace; size_t TotalVirtualAddressSpace; size_t FreeVirtualAddressSpace; size_t PageSize; void set(void){ MEMORYSTATUSEX Mem; SYSTEM_INFO Info; ZeroMemory( &Mem, sizeof(Mem)); ZeroMemory( &Info, sizeof(Info)); Mem.dwLength = sizeof(Mem); ::GlobalMemoryStatusEx( &Mem ); ::GetSystemInfo( &Info ); TotalPhysicalMemory = (size_t)Mem.ullTotalPhys; FreePhysicalMemory = (size_t)Mem.ullAvailPhys; TotalSwapSpace = (size_t)Mem.ullTotalPageFile; FreeSwapSpace = (size_t)Mem.ullAvailPageFile; TotalVirtualAddressSpace = (size_t)Mem.ullTotalVirtual; FreeVirtualAddressSpace = (size_t)Mem.ullAvailVirtual; PageSize = (size_t)Info.dwPageSize; } size_t usage(void) const {return TotalVirtualAddressSpace-FreeVirtualAddressSpace;} static size_t Usage(void){ MEMORY_BASIC_INFORMATION mbi; size_t dwMemUsed = 0; PVOID pvAddress = 0; memset(&mbi, 0, sizeof(MEMORY_BASIC_INFORMATION)); while(VirtualQuery(pvAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == sizeof(MEMORY_BASIC_INFORMATION)){ if(mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE){dwMemUsed += mbi.RegionSize;} pvAddress = ((BYTE*)mbi.BaseAddress) + mbi.RegionSize; } return dwMemUsed; } }; #else // !WIN32 #ifndef __APPLE__ // Linux variants #include #include class MemoryInfo { public: static size_t Usage(void) { FILE* f = fopen("/proc/self/stat","rb"); int d; long ld; unsigned long lu; unsigned long long llu; char s[1024]; char c; int pid; unsigned long vm; int n = fscanf(f, "%d %s %c %d %d %d %d %d %lu %lu %lu %lu %lu %lu %lu %ld %ld %ld %ld %d %ld %llu %lu %ld %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %d %d %lu %lu" ,&pid ,s ,&c ,&d ,&d ,&d ,&d ,&d ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&ld ,&ld ,&ld ,&ld ,&d ,&ld ,&llu ,&vm ,&ld ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&lu ,&d ,&d ,&lu ,&lu ); fclose(f); /* pid %d comm %s state %c ppid %d pgrp %d session %d tty_nr %d tpgid %d flags %lu minflt %lu cminflt %lu majflt %lu cmajflt %lu utime %lu stime %lu cutime %ld cstime %ld priority %ld nice %ld 0 %ld itrealvalue %ld starttime %lu vsize %lu rss %ld rlim %lu startcode %lu endcode %lu startstack %lu kstkesp %lu kstkeip %lu signal %lu blocked %lu sigignore %lu sigcatch %lu wchan %lu nswap %lu cnswap %lu exit_signal %d processor %d rt_priority %lu (since kernel 2.5.19) policy %lu (since kernel 2.5.19) */ return vm; } }; #else // __APPLE__: has no "/proc" pseudo-file system // Thanks to David O'Gwynn for providing this fix. // This comes from a post by Michael Knight: // // http://miknight.blogspot.com/2005/11/resident-set-size-in-mac-os-x.html #include #include #include #include #include #include #include void getres(task_t task, unsigned long *rss, unsigned long *vs) { struct task_basic_info t_info; mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT; task_info(task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count); *rss = t_info.resident_size; *vs = t_info.virtual_size; } class MemoryInfo { public: static size_t Usage(void) { unsigned long rss, vs, psize; task_t task = MACH_PORT_NULL; if (task_for_pid(current_task(), getpid(), &task) != KERN_SUCCESS) abort(); getres(task, &rss, &vs); return rss; } }; #endif // !__APPLE__ #endif // WIN32 #endif // MEMORY_USAGE_INCLUDE colmap-3.9.1/src/thirdparty/PoissonRecon/MultiGridOctreeData.Evaluation.inl000077500000000000000000001124171454702036400270650ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ template< class Real > template< int FEMDegree > void Octree< Real >::_Evaluator< FEMDegree >::set( int depth , bool dirichlet ) { static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; BSplineEvaluationData< FEMDegree >::SetEvaluator( evaluator , depth , dirichlet ); if( depth>0 ) BSplineEvaluationData< FEMDegree >::SetChildEvaluator( childEvaluator , depth-1 , dirichlet ); int center = BSplineData< FEMDegree >::Dimension( depth )>>1; // First set the stencils for the current depth for( int x=-LeftPointSupportRadius ; x<=RightPointSupportRadius ; x++ ) for( int y=-LeftPointSupportRadius ; y<=RightPointSupportRadius ; y++ ) for( int z=-LeftPointSupportRadius ; z<=RightPointSupportRadius ; z++ ) { int fIdx[] = { center+x , center+y , center+z }; //// The cell stencil { double vv[3] , dv[3]; for( int dd=0 ; dd( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } //// The face stencil for( int f=0 ; f( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } //// The edge stencil for( int e=0 ; e( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } //// The corner stencil for( int c=0 ; c( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } } // Now set the stencils for the parents for( int child=0 ; child( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } //// The face stencil for( int f=0 ; f( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } //// The edge stencil for( int e=0 ; e( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } //// The corner stencil for( int c=0 ; c( dv[0] * vv[1] * vv[2] , vv[0] * dv[1] * vv[2] , vv[0] * vv[1] * dv[2] ); } } } } template< class Real > template< class V , int FEMDegree > V Octree< Real >::_getCenterValue( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , const DenseNodeData< V , FEMDegree >& solution , const DenseNodeData< V , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = - BSplineEvaluationData< FEMDegree >::SupportStart; if( node->children ) fprintf( stderr , "[WARNING] getCenterValue assumes leaf node\n" ); V value(0); int d = _Depth( node ); if( isInterior ) { const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d ); for( int i=0 ; inodeData.nodeIndex ] * Real( evaluator.cellStencil.values[i][j][k] ); } if( d>_minDepth-1 ) { int _corner = int( node - node->parent->children ); const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d-1 ); for( int i=0 ; inodeData.nodeIndex] * Real( evaluator.cellStencils[_corner].values[i][j][k] ); } } } else { int cIdx[3]; _DepthAndOffset( node , d , cIdx ); const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d ); for( int i=0 ; i( n ) ) { int _d , fIdx[3]; _DepthAndOffset( n , _d , fIdx ); value += solution[ n->nodeData.nodeIndex ] * Real( evaluator.evaluator.centerValue( fIdx[0] , cIdx[0] , false ) * evaluator.evaluator.centerValue( fIdx[1] , cIdx[1] , false ) * evaluator.evaluator.centerValue( fIdx[2] , cIdx[2] , false ) ); } } if( d>_minDepth-1 ) { const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d-1 ); for( int i=0 ; i( n ) ) { int _d , fIdx[3]; _DepthAndOffset( n , _d , fIdx ); value += metSolution[ n->nodeData.nodeIndex ] * Real( evaluator.childEvaluator.centerValue( fIdx[0] , cIdx[0] , false ) * evaluator.childEvaluator.centerValue( fIdx[1] , cIdx[1] , false ) * evaluator.childEvaluator.centerValue( fIdx[2] , cIdx[2] , false ) ); } } } } return value; } template< class Real > template< class V , int FEMDegree > V Octree< Real >::_getEdgeValue( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int edge , const DenseNodeData< V , FEMDegree >& solution , const DenseNodeData< V , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; V value(0); int d , cIdx[3]; _DepthAndOffset( node , d , cIdx ); int startX = 0 , endX = SupportSize , startY = 0 , endY = SupportSize , startZ = 0 , endZ = SupportSize; int orientation , i1 , i2; Cube::FactorEdgeIndex( edge , orientation , i1 , i2 ); switch( orientation ) { case 0: cIdx[1] += i1 , cIdx[2] += i2; if( i1 ) startY++ ; else endY--; if( i2 ) startZ++ ; else endZ--; break; case 1: cIdx[0] += i1 , cIdx[2] += i2; if( i1 ) startX++ ; else endX--; if( i2 ) startZ++ ; else endZ--; break; case 2: cIdx[0] += i1 , cIdx[1] += i2; if( i1 ) startX++ ; else endX--; if( i2 ) startY++ ; else endY--; break; } { const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d ); for( int x=startX ; x( _node ) ) { if( isInterior ) value += solution[ _node->nodeData.nodeIndex ] * evaluator.edgeStencil[edge].values[x][y][z]; else { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); switch( orientation ) { case 0: value += solution[ _node->nodeData.nodeIndex ] * Real( evaluator.evaluator.centerValue( fIdx[0] , cIdx[0] , false ) * evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , false ) * evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , false ) ); break; case 1: value += solution[ _node->nodeData.nodeIndex ] * Real( evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , false ) * evaluator.evaluator.centerValue( fIdx[1] , cIdx[1] , false ) * evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , false ) ); break; case 2: value += solution[ _node->nodeData.nodeIndex ] * Real( evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , false ) * evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , false ) * evaluator.evaluator.centerValue( fIdx[2] , cIdx[2] , false ) ); break; } } } } } if( d>_minDepth-1 ) { int _corner = int( node - node->parent->children ); int _cx , _cy , _cz; Cube::FactorCornerIndex( _corner , _cx , _cy , _cz ); // If the corner/child indices don't match, then the sample position is in the interior of the // coarser cell and so the full support resolution should be used. switch( orientation ) { case 0: if( _cy!=i1 ) startY = 0 , endY = SupportSize; if( _cz!=i2 ) startZ = 0 , endZ = SupportSize; break; case 1: if( _cx!=i1 ) startX = 0 , endX = SupportSize; if( _cz!=i2 ) startZ = 0 , endZ = SupportSize; break; case 2: if( _cx!=i1 ) startX = 0 , endX = SupportSize; if( _cy!=i2 ) startY = 0 , endY = SupportSize; break; } const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d-1 ); for( int x=startX ; x( _node ) ) { if( isInterior ) value += metSolution[ _node->nodeData.nodeIndex ] * evaluator.edgeStencils[_corner][edge].values[x][y][z]; else { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); switch( orientation ) { case 0: value += metSolution[ _node->nodeData.nodeIndex ] * Real( evaluator.childEvaluator.centerValue( fIdx[0] , cIdx[0] , false ) * evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , false ) * evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , false ) ); break; case 1: value += metSolution[ _node->nodeData.nodeIndex ] * Real( evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , false ) * evaluator.childEvaluator.centerValue( fIdx[1] , cIdx[1] , false ) * evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , false ) ); break; case 2: value += metSolution[ _node->nodeData.nodeIndex ] * Real( evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , false ) * evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , false ) * evaluator.childEvaluator.centerValue( fIdx[2] , cIdx[2] , false ) ); break; } } } } } return Real( value ); } template< class Real > template< int FEMDegree > std::pair< Real , Point3D< Real > > Octree< Real >::_getEdgeValueAndGradient( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int edge , const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; double value = 0; Point3D< double > gradient; int d , cIdx[3]; _DepthAndOffset( node , d , cIdx ); int startX = 0 , endX = SupportSize , startY = 0 , endY = SupportSize , startZ = 0 , endZ = SupportSize; int orientation , i1 , i2; Cube::FactorEdgeIndex( edge , orientation , i1 , i2 ); switch( orientation ) { case 0: cIdx[1] += i1 , cIdx[2] += i2; if( i1 ) startY++ ; else endY--; if( i2 ) startZ++ ; else endZ--; break; case 1: cIdx[0] += i1 , cIdx[2] += i2; if( i1 ) startX++ ; else endX--; if( i2 ) startZ++ ; else endZ--; break; case 2: cIdx[0] += i1 , cIdx[1] += i2; if( i1 ) startX++ ; else endX--; if( i2 ) startY++ ; else endY--; break; } { const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d ); for( int x=startX ; x( _node ) ) { if( isInterior ) { value += evaluator. edgeStencil[edge].values[x][y][z] * solution[ _node->nodeData.nodeIndex ]; gradient += evaluator.dEdgeStencil[edge].values[x][y][z] * solution[ _node->nodeData.nodeIndex ]; } else { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); double vv[3] , dv[3]; switch( orientation ) { case 0: vv[0] = evaluator.evaluator.centerValue( fIdx[0] , cIdx[0] , false ); vv[1] = evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , false ); vv[2] = evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , false ); dv[0] = evaluator.evaluator.centerValue( fIdx[0] , cIdx[0] , true ); dv[1] = evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , true ); dv[2] = evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , true ); break; case 1: vv[0] = evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , false ); vv[1] = evaluator.evaluator.centerValue( fIdx[1] , cIdx[1] , false ); vv[2] = evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , false ); dv[0] = evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , true ); dv[1] = evaluator.evaluator.centerValue( fIdx[1] , cIdx[1] , true ); dv[2] = evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , true ); break; case 2: vv[0] = evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , false ); vv[1] = evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , false ); vv[2] = evaluator.evaluator.centerValue( fIdx[2] , cIdx[2] , false ); dv[0] = evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , true ); dv[1] = evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , true ); dv[2] = evaluator.evaluator.centerValue( fIdx[2] , cIdx[2] , true ); break; } value += solution[ _node->nodeData.nodeIndex ] * vv[0] * vv[1] * vv[2]; gradient += Point3D< double >( dv[0]*vv[1]*vv[2] , vv[0]*dv[1]*vv[2] , vv[0]*vv[1]*dv[2] ) * solution[ _node->nodeData.nodeIndex ]; } } } } if( d>_minDepth-1 ) { int _corner = int( node - node->parent->children ); int _cx , _cy , _cz; Cube::FactorCornerIndex( _corner , _cx , _cy , _cz ); // If the corner/child indices don't match, then the sample position is in the interior of the // coarser cell and so the full support resolution should be used. switch( orientation ) { case 0: if( _cy!=i1 ) startY = 0 , endY = SupportSize; if( _cz!=i2 ) startZ = 0 , endZ = SupportSize; break; case 1: if( _cx!=i1 ) startX = 0 , endX = SupportSize; if( _cz!=i2 ) startZ = 0 , endZ = SupportSize; break; case 2: if( _cx!=i1 ) startX = 0 , endX = SupportSize; if( _cy!=i2 ) startY = 0 , endY = SupportSize; break; } const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d-1 ); for( int x=startX ; x( _node ) ) { if( isInterior ) { value += evaluator. edgeStencils[_corner][edge].values[x][y][z] * metSolution[ _node->nodeData.nodeIndex ]; gradient += evaluator.dEdgeStencils[_corner][edge].values[x][y][z] * metSolution[ _node->nodeData.nodeIndex ]; } else { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); double vv[3] , dv[3]; switch( orientation ) { case 0: vv[0] = evaluator.childEvaluator.centerValue( fIdx[0] , cIdx[0] , false ); vv[1] = evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , false ); vv[2] = evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , false ); dv[0] = evaluator.childEvaluator.centerValue( fIdx[0] , cIdx[0] , true ); dv[1] = evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , true ); dv[2] = evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , true ); break; case 1: vv[0] = evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , false ); vv[1] = evaluator.childEvaluator.centerValue( fIdx[1] , cIdx[1] , false ); vv[2] = evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , false ); dv[0] = evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , true ); dv[1] = evaluator.childEvaluator.centerValue( fIdx[1] , cIdx[1] , true ); dv[2] = evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , true ); break; case 2: vv[0] = evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , false ); vv[1] = evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , false ); vv[2] = evaluator.childEvaluator.centerValue( fIdx[2] , cIdx[2] , false ); dv[0] = evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , true ); dv[1] = evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , true ); dv[2] = evaluator.childEvaluator.centerValue( fIdx[2] , cIdx[2] , true ); break; } value += metSolution[ _node->nodeData.nodeIndex ] * vv[0] * vv[1] * vv[2]; gradient += Point3D< double >( dv[0]*vv[1]*vv[2] , vv[0]*dv[1]*vv[2] , vv[0]*vv[1]*dv[2] ) * metSolution[ _node->nodeData.nodeIndex ]; } } } } return std::pair< Real , Point3D< Real > >( Real( value ) , Point3D< Real >( gradient ) ); } template< class Real > template< class V , int FEMDegree > V Octree< Real >::_getCornerValue( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int corner , const DenseNodeData< V , FEMDegree >& solution , const DenseNodeData< V , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = - BSplineEvaluationData< FEMDegree >::SupportStart; V value(0); int d , cIdx[3]; _DepthAndOffset( node , d , cIdx ); int cx , cy , cz; int startX = 0 , endX = SupportSize , startY = 0 , endY = SupportSize , startZ = 0 , endZ = SupportSize; Cube::FactorCornerIndex( corner , cx , cy , cz ); cIdx[0] += cx , cIdx[1] += cy , cIdx[2] += cz; { const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d ); if( cx==0 ) endX--; else startX++; if( cy==0 ) endY--; else startY++; if( cz==0 ) endZ--; else startZ++; if( isInterior ) for( int x=startX ; xnodeData.nodeIndex ] * Real( evaluator.cornerStencil[corner].values[x][y][z] ); } else for( int x=startX ; x( _node ) ) { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); value += solution[ _node->nodeData.nodeIndex ] * Real( evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , false ) * evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , false ) * evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , false ) ); } } } if( d>_minDepth-1 ) { int _corner = int( node - node->parent->children ); int _cx , _cy , _cz; Cube::FactorCornerIndex( _corner , _cx , _cy , _cz ); // If the corner/child indices don't match, then the sample position is in the interior of the // coarser cell and so the full support resolution should be used. if( cx!=_cx ) startX = 0 , endX = SupportSize; if( cy!=_cy ) startY = 0 , endY = SupportSize; if( cz!=_cz ) startZ = 0 , endZ = SupportSize; const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d-1 ); if( isInterior ) for( int x=startX ; xnodeData.nodeIndex ] * Real( evaluator.cornerStencils[_corner][corner].values[x][y][z] ); } else for( int x=startX ; x( _node ) ) { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); value += metSolution[ _node->nodeData.nodeIndex ] * Real( evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , false ) * evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , false ) * evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , false ) ); } } } return Real( value ); } template< class Real > template< int FEMDegree > std::pair< Real , Point3D< Real > > Octree< Real >::_getCornerValueAndGradient( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int corner , const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = - BSplineEvaluationData< FEMDegree >::SupportStart; double value = 0; Point3D< double > gradient; int d , cIdx[3]; _DepthAndOffset( node , d , cIdx ); int cx , cy , cz; int startX = 0 , endX = SupportSize , startY = 0 , endY = SupportSize , startZ = 0 , endZ = SupportSize; Cube::FactorCornerIndex( corner , cx , cy , cz ); cIdx[0] += cx , cIdx[1] += cy , cIdx[2] += cz; { if( cx==0 ) endX--; else startX++; if( cy==0 ) endY--; else startY++; if( cz==0 ) endZ--; else startZ++; const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d ); if( isInterior ) for( int x=startX ; xnodeData.nodeIndex ] * evaluator.cornerStencil[corner].values[x][y][z] , gradient += evaluator.dCornerStencil[corner].values[x][y][z] * solution[ _node->nodeData.nodeIndex ]; } else for( int x=startX ; x( _node ) ) { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); double v [] = { evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , false ) , evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , false ) , evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , false ) }; double dv[] = { evaluator.evaluator.cornerValue( fIdx[0] , cIdx[0] , true ) , evaluator.evaluator.cornerValue( fIdx[1] , cIdx[1] , true ) , evaluator.evaluator.cornerValue( fIdx[2] , cIdx[2] , true ) }; value += solution[ _node->nodeData.nodeIndex ] * v[0] * v[1] * v[2]; gradient += Point3D< double >( dv[0]*v[1]*v[2] , v[0]*dv[1]*v[2] , v[0]*v[1]*dv[2] ) * solution[ _node->nodeData.nodeIndex ]; } } } if( d>_minDepth-1 ) { int _corner = int( node - node->parent->children ); int _cx , _cy , _cz; Cube::FactorCornerIndex( _corner , _cx , _cy , _cz ); if( cx!=_cx ) startX = 0 , endX = SupportSize; if( cy!=_cy ) startY = 0 , endY = SupportSize; if( cz!=_cz ) startZ = 0 , endZ = SupportSize; const typename TreeOctNode::ConstNeighbors< SupportSize >& neighbors = _Neighbors< LeftPointSupportRadius , RightPointSupportRadius >( neighborKey , d-1 ); if( isInterior ) for( int x=startX ; xnodeData.nodeIndex ] * evaluator.cornerStencils[_corner][corner].values[x][y][z] , gradient += evaluator.dCornerStencils[_corner][corner].values[x][y][z] * metSolution[ _node->nodeData.nodeIndex ]; } else for( int x=startX ; x( _node ) ) { int _d , fIdx[3]; _DepthAndOffset( _node , _d , fIdx ); double v [] = { evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , false ) , evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , false ) , evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , false ) }; double dv[] = { evaluator.childEvaluator.cornerValue( fIdx[0] , cIdx[0] , true ) , evaluator.childEvaluator.cornerValue( fIdx[1] , cIdx[1] , true ) , evaluator.childEvaluator.cornerValue( fIdx[2] , cIdx[2] , true ) }; value += metSolution[ _node->nodeData.nodeIndex ] * v[0] * v[1] * v[2]; gradient += Point3D< double >( dv[0]*v[1]*v[2] , v[0]*dv[1]*v[2] , v[0]*v[1]*dv[2] ) * metSolution[ _node->nodeData.nodeIndex ]; } } } return std::pair< Real , Point3D< Real > >( Real( value ) , Point3D< Real >( gradient ) ); } colmap-3.9.1/src/thirdparty/PoissonRecon/MultiGridOctreeData.IsoSurface.inl000077500000000000000000001555051454702036400270260ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include "Octree.h" #include "MyTime.h" #include "MemoryUsage.h" #include "MAT.h" template< class Real > template< class Vertex > Octree< Real >::SliceValues< Vertex >::SliceValues( void ) { _oldCCount = _oldECount = _oldFCount = _oldNCount = 0; cornerValues = NullPointer( Real ) ; cornerGradients = NullPointer( Point3D< Real > ) ; cornerSet = NullPointer( char ); edgeKeys = NullPointer( long long ) ; edgeSet = NullPointer( char ); faceEdges = NullPointer( FaceEdges ) ; faceSet = NullPointer( char ); mcIndices = NullPointer( char ); } template< class Real > template< class Vertex > Octree< Real >::SliceValues< Vertex >::~SliceValues( void ) { _oldCCount = _oldECount = _oldFCount = _oldNCount = 0; FreePointer( cornerValues ) ; FreePointer( cornerGradients ) ; FreePointer( cornerSet ); FreePointer( edgeKeys ) ; FreePointer( edgeSet ); FreePointer( faceEdges ) ; FreePointer( faceSet ); FreePointer( mcIndices ); } template< class Real > template< class Vertex > void Octree< Real >::SliceValues< Vertex >::reset( bool nonLinearFit ) { faceEdgeMap.clear() , edgeVertexMap.clear() , vertexPairMap.clear(); if( _oldNCount0 ) mcIndices = AllocPointer< char >( _oldNCount ); } if( _oldCCount0 ) { cornerValues = AllocPointer< Real >( _oldCCount ); if( nonLinearFit ) cornerGradients = AllocPointer< Point3D< Real > >( _oldCCount ); cornerSet = AllocPointer< char >( _oldCCount ); } } if( _oldECount( _oldECount ); edgeSet = AllocPointer< char >( _oldECount ); } if( _oldFCount( _oldFCount ); faceSet = AllocPointer< char >( _oldFCount ); } if( sliceData.cCount>0 ) memset( cornerSet , 0 , sizeof( char ) * sliceData.cCount ); if( sliceData.eCount>0 ) memset( edgeSet , 0 , sizeof( char ) * sliceData.eCount ); if( sliceData.fCount>0 ) memset( faceSet , 0 , sizeof( char ) * sliceData.fCount ); } template< class Real > template< class Vertex > Octree< Real >::XSliceValues< Vertex >::XSliceValues( void ) { _oldECount = _oldFCount = 0; edgeKeys = NullPointer( long long ) ; edgeSet = NullPointer( char ); faceEdges = NullPointer( FaceEdges ) ; faceSet = NullPointer( char ); } template< class Real > template< class Vertex > Octree< Real >::XSliceValues< Vertex >::~XSliceValues( void ) { _oldECount = _oldFCount = 0; FreePointer( edgeKeys ) ; FreePointer( edgeSet ); FreePointer( faceEdges ) ; FreePointer( faceSet ); } template< class Real > template< class Vertex > void Octree< Real >::XSliceValues< Vertex >::reset( void ) { faceEdgeMap.clear() , edgeVertexMap.clear() , vertexPairMap.clear(); if( _oldECount( _oldECount ); edgeSet = AllocPointer< char >( _oldECount ); } if( _oldFCount( _oldFCount ); faceSet = AllocPointer< char >( _oldFCount ); } if( xSliceData.eCount>0 ) memset( edgeSet , 0 , sizeof( char ) * xSliceData.eCount ); if( xSliceData.fCount>0 ) memset( faceSet , 0 , sizeof( char ) * xSliceData.fCount ); } template< class Real > template< int FEMDegree , int WeightDegree , int ColorDegree , class Vertex > void Octree< Real >::GetMCIsoSurface( const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , const DenseNodeData< Real , FEMDegree >& solution , Real isoValue , CoredMeshData< Vertex >& mesh , bool nonLinearFit , bool addBarycenter , bool polygonMesh ) { int maxDepth = _tree.maxDepth(); if( FEMDegree==1 && nonLinearFit ) fprintf( stderr , "[WARNING] First order B-Splines do not support non-linear interpolation\n" ) , nonLinearFit = false; BSplineData< ColorDegree >* colorBSData = NULL; if( colorData ) { colorBSData = new BSplineData< ColorDegree >(); colorBSData->set( maxDepth , _dirichlet ); } DenseNodeData< Real , FEMDegree > coarseSolution( _sNodes.end( maxDepth-1 ) ); memset( coarseSolution.data , 0 , sizeof(Real)*_sNodes.end( maxDepth-1 ) ); #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(_minDepth) ; i<_sNodes.end(maxDepth-1) ; i++ ) coarseSolution[i] = solution[i]; for( int d=_minDepth+1 ; d > evaluators( maxDepth+1 ); for( int d=_minDepth ; d<=maxDepth ; d++ ) evaluators[d].set( d-1 , _dirichlet ); int vertexOffset = 0; std::vector< SlabValues< Vertex > > slabValues( maxDepth+1 ); // Initialize the back slice for( int d=maxDepth ; d>=_minDepth ; d-- ) { _sNodes.setSliceTableData ( slabValues[d]. sliceValues(0). sliceData , d , 0 , threads ); _sNodes.setSliceTableData ( slabValues[d]. sliceValues(1). sliceData , d , 1 , threads ); _sNodes.setXSliceTableData( slabValues[d].xSliceValues(0).xSliceData , d , 0 , threads ); slabValues[d].sliceValues (0).reset( nonLinearFit ); slabValues[d].sliceValues (1).reset( nonLinearFit ); slabValues[d].xSliceValues(0).reset( ); } for( int d=maxDepth ; d>=_minDepth ; d-- ) { // Copy edges from finer if( d( colorBSData , densityWeights , colorData , isoValue , d , 0 , vertexOffset , mesh , slabValues , threads ); SetSliceIsoEdges( d , 0 , slabValues , threads ); } // Iterate over the slices at the finest level for( int slice=0 ; slice<( 1<<(maxDepth-1) ) ; slice++ ) { // Process at all depths that that contain this slice for( int d=maxDepth , o=slice+1 ; d>=_minDepth ; d-- , o>>=1 ) { // Copy edges from finer (required to ensure we correctly track edge cancellations) if( d( colorBSData , densityWeights , colorData , isoValue , d , o , vertexOffset , mesh , slabValues , threads ); SetSliceIsoEdges( d , o , slabValues , threads ); // Set the cross-slice edges SetXSliceIsoVertices< WeightDegree , ColorDegree >( colorBSData , densityWeights , colorData , isoValue , d , o-1 , vertexOffset , mesh , slabValues , threads ); SetXSliceIsoEdges( d , o-1 , slabValues , threads ); // Add the triangles SetIsoSurface( d , o-1 , slabValues[d].sliceValues(o-1) , slabValues[d].sliceValues(o) , slabValues[d].xSliceValues(o-1) , mesh , polygonMesh , addBarycenter , vertexOffset , threads ); if( o&1 ) break; } for( int d=maxDepth , o=slice+1 ; d>=_minDepth ; d-- , o>>=1 ) { // Initialize for the next pass if( o<(1< template< int FEMDegree , int NormalDegree > Real Octree< Real >::GetIsoValue( const DenseNodeData< Real , FEMDegree >& solution , const SparseNodeData< Real , NormalDegree >& nodeWeights ) { Real isoValue=0 , weightSum=0; int maxDepth = _tree.maxDepth(); Pointer( Real ) nodeValues = AllocPointer< Real >( _sNodes.end(maxDepth) ); memset( nodeValues , 0 , sizeof(Real) * _sNodes.end(maxDepth) ); DenseNodeData< Real , FEMDegree > metSolution( _sNodes.end( maxDepth-1 ) ); memset( metSolution.data , 0 , sizeof(Real)*_sNodes.end( maxDepth-1 ) ); #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(_minDepth) ; i<_sNodes.end(maxDepth-1) ; i++ ) metSolution[i] = solution[i]; for( int d=_minDepth+1 ; d=_minDepth ; d-- ) { _Evaluator< FEMDegree > evaluator; evaluator.set( d-1 , _dirichlet ); std::vector< ConstPointSupportKey< FEMDegree > > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { ConstPointSupportKey< FEMDegree >& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* node = _sNodes.treeNodes[i]; Real value = Real(0); if( node->children ) { if( NormalDegree&1 ) value = nodeValues[ node->children->nodeData.nodeIndex ]; else for( int c=0 ; cchildren[c].nodeData.nodeIndex ] / Cube::CORNERS; } else if( nodeWeights.index( _sNodes.treeNodes[i] )>=0 ) { neighborKey.getNeighbors( node ); int c=0 , x , y , z; if( node->parent ) c = int( node - node->parent->children ); Cube::FactorCornerIndex( c , x , y , z ); // Since evaluation requires parent indices, we need to check that the node's parent is interiorly supported bool isInterior = _IsInteriorlySupported< FEMDegree >( node->parent ); if( NormalDegree&1 ) value = _getCornerValue( neighborKey , node , 0 , solution , metSolution , evaluator , isInterior ); else value = _getCenterValue( neighborKey , node , solution , metSolution , evaluator , isInterior ); } nodeValues[i] = value; int idx = nodeWeights.index( _sNodes.treeNodes[i] ); if( idx!=-1 ) { Real w = nodeWeights.data[ idx ]; if( w!=0 ) isoValue += value * w , weightSum += w; } } } metSolution.resize( 0 ); FreePointer( nodeValues ); return isoValue / weightSum; } template< class Real > template< class Vertex , int FEMDegree > void Octree< Real >::SetSliceIsoCorners( const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& coarseSolution , Real isoValue , int depth , int slice , std::vector< SlabValues< Vertex > >& slabValues , const _Evaluator< FEMDegree >& evaluator , int threads ) { if( slice>0 ) SetSliceIsoCorners( solution , coarseSolution , isoValue , depth , slice , 1 , slabValues , evaluator , threads ); if( slice<(1< template< class Vertex , int FEMDegree > void Octree< Real >::SetSliceIsoCorners( const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& coarseSolution , Real isoValue , int depth , int slice , int z , std::vector< SlabValues< Vertex > >& slabValues , const struct _Evaluator< FEMDegree >& evaluator , int threads ) { typename Octree::template SliceValues< Vertex >& sValues = slabValues[depth].sliceValues( slice ); std::vector< ConstPointSupportKey< FEMDegree > > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { Real squareValues[ Square::CORNERS ]; ConstPointSupportKey< FEMDegree >& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* leaf = _sNodes.treeNodes[i]; if( !leaf->children ) { const typename SortedTreeNodes::SquareCornerIndices& cIndices = sValues.sliceData.cornerIndices( leaf ); bool isInterior = _IsInteriorlySupported< FEMDegree >( leaf->parent ); neighborKey.getNeighbors( leaf ); for( int x=0 ; x<2 ; x++ ) for( int y=0 ; y<2 ; y++ ) { int cc = Cube::CornerIndex( x , y , z ); int fc = Square::CornerIndex( x , y ); int vIndex = cIndices[fc]; if( !sValues.cornerSet[vIndex] ) { if( sValues.cornerGradients ) { std::pair< Real , Point3D< Real > > p = _getCornerValueAndGradient( neighborKey , leaf , cc , solution , coarseSolution , evaluator , isInterior ); sValues.cornerValues[vIndex] = p.first , sValues.cornerGradients[vIndex] = p.second; } else sValues.cornerValues[vIndex] = _getCornerValue( neighborKey , leaf , cc , solution , coarseSolution , evaluator , isInterior ); sValues.cornerSet[vIndex] = 1; } squareValues[fc] = sValues.cornerValues[ vIndex ]; TreeOctNode* node = leaf; int _depth = depth , _slice = slice; while( _IsValidNode< 0 >( node->parent ) && (node-node->parent->children)==cc ) { node = node->parent , _depth-- , _slice >>= 1; typename Octree::template SliceValues< Vertex >& _sValues = slabValues[_depth].sliceValues( _slice ); const typename SortedTreeNodes::SquareCornerIndices& _cIndices = _sValues.sliceData.cornerIndices( node ); int _vIndex = _cIndices[fc]; _sValues.cornerValues[_vIndex] = sValues.cornerValues[vIndex]; if( _sValues.cornerGradients ) _sValues.cornerGradients[_vIndex] = sValues.cornerGradients[vIndex]; _sValues.cornerSet[_vIndex] = 1; } } sValues.mcIndices[ i - sValues.sliceData.nodeOffset ] = MarchingSquares::GetIndex( squareValues , isoValue ); } } } template< class Real > template< int WeightDegree , int ColorDegree , class Vertex > void Octree< Real >::SetSliceIsoVertices( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , int depth , int slice , int& vOffset , CoredMeshData< Vertex >& mesh , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { if( slice>0 ) SetSliceIsoVertices< WeightDegree , ColorDegree >( colorBSData , densityWeights , colorData , isoValue , depth , slice , 1 , vOffset , mesh , slabValues , threads ); if( slice<(1<( colorBSData , densityWeights , colorData , isoValue , depth , slice , 0 , vOffset , mesh , slabValues , threads ); } template< class Real > template< int WeightDegree , int ColorDegree , class Vertex > void Octree< Real >::SetSliceIsoVertices( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , int depth , int slice , int z , int& vOffset , CoredMeshData< Vertex >& mesh , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { typename Octree::template SliceValues< Vertex >& sValues = slabValues[depth].sliceValues( slice ); // [WARNING] In the case Degree=2, these two keys are the same, so we don't have to maintain them separately. std::vector< ConstAdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); std::vector< ConstPointSupportKey< WeightDegree > > weightKeys( std::max< int >( 1 , threads ) ); std::vector< ConstPointSupportKey< ColorDegree > > colorKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { ConstAdjacenctNodeKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; ConstPointSupportKey< WeightDegree >& weightKey = weightKeys[ omp_get_thread_num() ]; ConstPointSupportKey< ColorDegree >& colorKey = colorKeys[ omp_get_thread_num() ]; TreeOctNode* leaf = _sNodes.treeNodes[i]; if( !leaf->children ) { int idx = i - sValues.sliceData.nodeOffset; const typename SortedTreeNodes::SquareEdgeIndices& eIndices = sValues.sliceData.edgeIndices( leaf ); if( MarchingSquares::HasRoots( sValues.mcIndices[idx] ) ) { neighborKey.getNeighbors( leaf ); if( densityWeights ) weightKey.getNeighbors( leaf ); if( colorData ) colorKey.getNeighbors( leaf ); for( int e=0 ; e hashed_vertex; #pragma omp critical (add_point_access) { if( !sValues.edgeSet[vIndex] ) { mesh.addOutOfCorePoint( vertex ); sValues.edgeSet[ vIndex ] = 1; sValues.edgeKeys[ vIndex ] = key; sValues.edgeVertexMap[key] = hashed_vertex = std::pair< int , Vertex >( vOffset , vertex ); vOffset++; stillOwner = true; } } if( stillOwner ) { // We only need to pass the iso-vertex down if the edge it lies on is adjacent to a coarser leaf bool isNeeded; switch( o ) { case 0: isNeeded = ( !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[1][2*y][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[1][2*y][2*z] ) || !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[1][1][2*z] ) ) ; break; case 1: isNeeded = ( !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[2*y][1][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[2*y][1][2*z] ) || !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[1][1][2*z] ) ) ; break; } if( isNeeded ) { int f[2]; Cube::FacesAdjacentToEdge( Cube::EdgeIndex( o , y , z ) , f[0] , f[1] ); for( int k=0 ; k<2 ; k++ ) { TreeOctNode* node = leaf; int _depth = depth , _slice = slice; bool _isNeeded = isNeeded; while( _isNeeded && node->parent && Cube::IsFaceCorner( (int)(node-node->parent->children) , f[k] ) ) { node = node->parent , _depth-- , _slice >>= 1; typename Octree::template SliceValues< Vertex >& _sValues = slabValues[_depth].sliceValues( _slice ); #pragma omp critical (add_coarser_point_access) _sValues.edgeVertexMap[key] = hashed_vertex; switch( o ) { case 0: _isNeeded = ( !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[1][2*y][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[1][2*y][2*z] ) || !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[1][1][2*z] ) ) ; break; case 1: _isNeeded = ( !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[2*y][1][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[2*y][1][2*z] ) || !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[1][1][2*z] ) ) ; break; } } } } } } } } } } } template< class Real > template< int WeightDegree , int ColorDegree , class Vertex > void Octree< Real >::SetXSliceIsoVertices( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , int depth , int slab , int& vOffset , CoredMeshData< Vertex >& mesh , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { typename Octree::template SliceValues< Vertex >& bValues = slabValues[depth].sliceValues ( slab ); typename Octree::template SliceValues< Vertex >& fValues = slabValues[depth].sliceValues ( slab+1 ); typename Octree::template XSliceValues< Vertex >& xValues = slabValues[depth].xSliceValues( slab ); // [WARNING] In the case Degree=2, these two keys are the same, so we don't have to maintain them separately. std::vector< ConstAdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); std::vector< ConstPointSupportKey< WeightDegree > > weightKeys( std::max< int >( 1 , threads ) ); std::vector< ConstPointSupportKey< ColorDegree > > colorKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { ConstAdjacenctNodeKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; ConstPointSupportKey< WeightDegree >& weightKey = weightKeys[ omp_get_thread_num() ]; ConstPointSupportKey< ColorDegree >& colorKey = colorKeys[ omp_get_thread_num() ]; TreeOctNode* leaf = _sNodes.treeNodes[i]; if( !leaf->children ) { unsigned char mcIndex = ( bValues.mcIndices[ i - bValues.sliceData.nodeOffset ] ) | ( fValues.mcIndices[ i - fValues.sliceData.nodeOffset ] )<<4; const typename SortedTreeNodes::SquareCornerIndices& eIndices = xValues.xSliceData.edgeIndices( leaf ); if( MarchingCubes::HasRoots( mcIndex ) ) { neighborKey.getNeighbors( leaf ); if( densityWeights ) weightKey.getNeighbors( leaf ); if( colorData ) colorKey.getNeighbors( leaf ); for( int x=0 ; x<2 ; x++ ) for( int y=0 ; y<2 ; y++ ) { int c = Square::CornerIndex( x , y ); int e = Cube::EdgeIndex( 2 , x , y ); if( MarchingCubes::HasEdgeRoots( mcIndex , e ) ) { int vIndex = eIndices[c]; if( !xValues.edgeSet[vIndex] ) { Vertex vertex; long long key = VertexData::EdgeIndex( leaf , e , _sNodes.levels() ); GetIsoVertex( colorBSData , densityWeights , colorData , isoValue , weightKey , colorKey , leaf , c , bValues , fValues , vertex ); vertex.point = vertex.point * _scale + _center; bool stillOwner = false; std::pair< int , Vertex > hashed_vertex; #pragma omp critical (add_x_point_access) { if( !xValues.edgeSet[vIndex] ) { mesh.addOutOfCorePoint( vertex ); xValues.edgeSet[ vIndex ] = 1; xValues.edgeKeys[ vIndex ] = key; xValues.edgeVertexMap[key] = hashed_vertex = std::pair< int , Vertex >( vOffset , vertex ); stillOwner = true; vOffset++; } } if( stillOwner ) { // We only need to pass the iso-vertex down if the edge it lies on is adjacent to a coarser leaf bool isNeeded = ( !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[2*x][1][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[2*x][2*y][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[depth].neighbors[1][2*y][1] ) ); if( isNeeded ) { int f[2]; Cube::FacesAdjacentToEdge( e , f[0] , f[1] ); for( int k=0 ; k<2 ; k++ ) { TreeOctNode* node = leaf; int _depth = depth , _slab = slab; bool _isNeeded = isNeeded; while( _isNeeded && node->parent && Cube::IsFaceCorner( (int)(node-node->parent->children) , f[k] ) ) { node = node->parent , _depth-- , _slab >>= 1; typename Octree::template XSliceValues< Vertex >& _xValues = slabValues[_depth].xSliceValues( _slab ); #pragma omp critical (add_x_coarser_point_access) _xValues.edgeVertexMap[key] = hashed_vertex; _isNeeded = ( !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[2*x][1][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[2*x][2*y][1] ) || !_IsValidNode< 0 >( neighborKey.neighbors[_depth].neighbors[1][2*y][1] ) ); } } } } } } } } } } } template< class Real > template< class Vertex > void Octree< Real >::CopyFinerSliceIsoEdgeKeys( int depth , int slice , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { if( slice>0 ) CopyFinerSliceIsoEdgeKeys( depth , slice , 1 , slabValues , threads ); if( slice<(1< template< class Vertex > void Octree< Real >::CopyFinerSliceIsoEdgeKeys( int depth , int slice , int z , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { SliceValues< Vertex >& pSliceValues = slabValues[depth ].sliceValues(slice ); SliceValues< Vertex >& cSliceValues = slabValues[depth+1].sliceValues(slice<<1); typename SortedTreeNodes::SliceTableData& pSliceData = pSliceValues.sliceData; typename SortedTreeNodes::SliceTableData& cSliceData = cSliceValues.sliceData; #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(depth,slice-z) ; i<_sNodes.end(depth,slice-z) ; i++ ) if( _IsValidNode< 0 >( _sNodes.treeNodes[i] ) ) if( _sNodes.treeNodes[i]->children ) { typename SortedTreeNodes::SquareEdgeIndices& pIndices = pSliceData.edgeIndices( i ); // Copy the edges that overlap the coarser edges for( int orientation=0 ; orientation<2 ; orientation++ ) for( int y=0 ; y<2 ; y++ ) { int fe = Square::EdgeIndex( orientation , y ); int pIndex = pIndices[fe]; if( !pSliceValues.edgeSet[ pIndex ] ) { int ce = Cube::EdgeIndex( orientation , y , z ); int c1 , c2; switch( orientation ) { case 0: c1 = Cube::CornerIndex( 0 , y , z ) , c2 = Cube::CornerIndex( 1 , y , z ) ; break; case 1: c1 = Cube::CornerIndex( y , 0 , z ) , c2 = Cube::CornerIndex( y , 1 , z ) ; break; } // [SANITY CHECK] // if( _IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c1 )!=_IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c2 ) ) fprintf( stderr , "[WARNING] Finer edges should both be valid or invalid\n" ) , exit( 0 ); if( !_IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c1 ) || !_IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c2 ) ) continue; int cIndex1 = cSliceData.edgeIndices( _sNodes.treeNodes[i]->children + c1 )[fe]; int cIndex2 = cSliceData.edgeIndices( _sNodes.treeNodes[i]->children + c2 )[fe]; if( cSliceValues.edgeSet[cIndex1] != cSliceValues.edgeSet[cIndex2] ) { long long key; if( cSliceValues.edgeSet[cIndex1] ) key = cSliceValues.edgeKeys[cIndex1]; else key = cSliceValues.edgeKeys[cIndex2]; std::pair< int , Vertex > vPair = cSliceValues.edgeVertexMap.find( key )->second; #pragma omp critical ( copy_finer_edge_keys ) pSliceValues.edgeVertexMap[key] = vPair; pSliceValues.edgeKeys[pIndex] = key; pSliceValues.edgeSet[pIndex] = 1; } else if( cSliceValues.edgeSet[cIndex1] && cSliceValues.edgeSet[cIndex2] ) { long long key1 = cSliceValues.edgeKeys[cIndex1] , key2 = cSliceValues.edgeKeys[cIndex2]; #pragma omp critical ( set_edge_pairs ) pSliceValues.vertexPairMap[ key1 ] = key2 , pSliceValues.vertexPairMap[ key2 ] = key1; const TreeOctNode* node = _sNodes.treeNodes[i]; int _depth = depth , _slice = slice; while( node->parent && Cube::IsEdgeCorner( (int)( node - node->parent->children ) , ce ) ) { node = node->parent , _depth-- , _slice >>= 1; SliceValues< Vertex >& _pSliceValues = slabValues[_depth].sliceValues(_slice); #pragma omp critical ( set_edge_pairs ) _pSliceValues.vertexPairMap[ key1 ] = key2 , _pSliceValues.vertexPairMap[ key2 ] = key1; } } } } } } template< class Real > template< class Vertex > void Octree< Real >::CopyFinerXSliceIsoEdgeKeys( int depth , int slab , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { XSliceValues< Vertex >& pSliceValues = slabValues[depth ].xSliceValues(slab); XSliceValues< Vertex >& cSliceValues0 = slabValues[depth+1].xSliceValues( (slab<<1)|0 ); XSliceValues< Vertex >& cSliceValues1 = slabValues[depth+1].xSliceValues( (slab<<1)|1 ); typename SortedTreeNodes::XSliceTableData& pSliceData = pSliceValues.xSliceData; typename SortedTreeNodes::XSliceTableData& cSliceData0 = cSliceValues0.xSliceData; typename SortedTreeNodes::XSliceTableData& cSliceData1 = cSliceValues1.xSliceData; #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(depth,slab) ; i<_sNodes.end(depth,slab) ; i++ ) if( _IsValidNode< 0 >( _sNodes.treeNodes[i] ) ) if( _sNodes.treeNodes[i]->children ) { typename SortedTreeNodes::SquareCornerIndices& pIndices = pSliceData.edgeIndices( i ); for( int x=0 ; x<2 ; x++ ) for( int y=0 ; y<2 ; y++ ) { int fc = Square::CornerIndex( x , y ); int pIndex = pIndices[fc]; if( !pSliceValues.edgeSet[pIndex] ) { int c0 = Cube::CornerIndex( x , y , 0 ) , c1 = Cube::CornerIndex( x , y , 1 ); // [SANITY CHECK] // if( _IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c0 )!=_IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c1 ) ) fprintf( stderr , "[ERROR] Finer edges should both be valid or invalid\n" ) , exit( 0 ); if( !_IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c0 ) || !_IsValidNode< 0 >( _sNodes.treeNodes[i]->children + c1 ) ) continue; int cIndex0 = cSliceData0.edgeIndices( _sNodes.treeNodes[i]->children + c0 )[fc]; int cIndex1 = cSliceData1.edgeIndices( _sNodes.treeNodes[i]->children + c1 )[fc]; if( cSliceValues0.edgeSet[cIndex0] != cSliceValues1.edgeSet[cIndex1] ) { long long key; std::pair< int , Vertex > vPair; if( cSliceValues0.edgeSet[cIndex0] ) key = cSliceValues0.edgeKeys[cIndex0] , vPair = cSliceValues0.edgeVertexMap.find( key )->second; else key = cSliceValues1.edgeKeys[cIndex1] , vPair = cSliceValues1.edgeVertexMap.find( key )->second; #pragma omp critical ( copy_finer_x_edge_keys ) pSliceValues.edgeVertexMap[key] = vPair; pSliceValues.edgeKeys[ pIndex ] = key; pSliceValues.edgeSet[ pIndex ] = 1; } else if( cSliceValues0.edgeSet[cIndex0] && cSliceValues1.edgeSet[cIndex1] ) { long long key0 = cSliceValues0.edgeKeys[cIndex0] , key1 = cSliceValues1.edgeKeys[cIndex1]; #pragma omp critical ( set_x_edge_pairs ) pSliceValues.vertexPairMap[ key0 ] = key1 , pSliceValues.vertexPairMap[ key1 ] = key0; const TreeOctNode* node = _sNodes.treeNodes[i]; int _depth = depth , _slab = slab , ce = Cube::CornerIndex( 2 , x , y ); while( node->parent && Cube::IsEdgeCorner( (int)( node - node->parent->children ) , ce ) ) { node = node->parent , _depth-- , _slab>>= 1; SliceValues< Vertex >& _pSliceValues = slabValues[_depth].sliceValues(_slab); #pragma omp critical ( set_x_edge_pairs ) _pSliceValues.vertexPairMap[ key0 ] = key1 , _pSliceValues.vertexPairMap[ key1 ] = key0; } } } } } } template< class Real > template< class Vertex > void Octree< Real >::SetSliceIsoEdges( int depth , int slice , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { if( slice>0 ) SetSliceIsoEdges( depth , slice , 1 , slabValues , threads ); if( slice<(1< template< class Vertex > void Octree< Real >::SetSliceIsoEdges( int depth , int slice , int z , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { typename Octree::template SliceValues< Vertex >& sValues = slabValues[depth].sliceValues( slice ); std::vector< ConstAdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { int isoEdges[ 2 * MarchingSquares::MAX_EDGES ]; ConstAdjacenctNodeKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* leaf = _sNodes.treeNodes[i]; if( !leaf->children ) { int idx = i - sValues.sliceData.nodeOffset; const typename SortedTreeNodes::SquareEdgeIndices& eIndices = sValues.sliceData.edgeIndices( leaf ); const typename SortedTreeNodes::SquareFaceIndices& fIndices = sValues.sliceData.faceIndices( leaf ); unsigned char mcIndex = sValues.mcIndices[idx]; if( !sValues.faceSet[ fIndices[0] ] ) { neighborKey.getNeighbors( leaf ); if( !neighborKey.neighbors[depth].neighbors[1][1][2*z] || !neighborKey.neighbors[depth].neighbors[1][1][2*z]->children ) { FaceEdges fe; fe.count = MarchingSquares::AddEdgeIndices( mcIndex , isoEdges ); for( int j=0 ; j edges; edges.resize( fe.count ); for( int j=0 ; jparent && Cube::IsFaceCorner( (int)(node-node->parent->children) , f ) ) { node = node->parent , _depth-- , _slice >>= 1; if( neighborKey.neighbors[_depth].neighbors[1][1][2*z] && neighborKey.neighbors[_depth].neighbors[1][1][2*z]->children ) break; long long key = VertexData::FaceIndex( node , f , _sNodes.levels() ); #pragma omp critical( add_iso_edge_access ) { typename Octree::template SliceValues< Vertex >& _sValues = slabValues[_depth].sliceValues( _slice ); typename hash_map< long long , std::vector< IsoEdge > >::iterator iter = _sValues.faceEdgeMap.find(key); if( iter==_sValues.faceEdgeMap.end() ) _sValues.faceEdgeMap[key] = edges; else for( int j=0 ; jsecond.push_back( fe.edges[j] ); } } } } } } } template< class Real > template< class Vertex > void Octree< Real >::SetXSliceIsoEdges( int depth , int slab , std::vector< SlabValues< Vertex > >& slabValues , int threads ) { typename Octree::template SliceValues< Vertex >& bValues = slabValues[depth].sliceValues ( slab ); typename Octree::template SliceValues< Vertex >& fValues = slabValues[depth].sliceValues ( slab+1 ); typename Octree::template XSliceValues< Vertex >& xValues = slabValues[depth].xSliceValues( slab ); std::vector< ConstAdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { int isoEdges[ 2 * MarchingSquares::MAX_EDGES ]; ConstAdjacenctNodeKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* leaf = _sNodes.treeNodes[i]; if( !leaf->children ) { const typename SortedTreeNodes::SquareCornerIndices& cIndices = xValues.xSliceData.edgeIndices( leaf ); const typename SortedTreeNodes::SquareEdgeIndices& eIndices = xValues.xSliceData.faceIndices( leaf ); unsigned char mcIndex = ( bValues.mcIndices[ i - bValues.sliceData.nodeOffset ] ) | ( fValues.mcIndices[ i - fValues.sliceData.nodeOffset ]<<4 ); { neighborKey.getNeighbors( leaf ); for( int o=0 ; o<2 ; o++ ) for( int x=0 ; x<2 ; x++ ) { int e = Square::EdgeIndex( o , x ); int f = Cube::FaceIndex( 1-o , x ); unsigned char _mcIndex = MarchingCubes::GetFaceIndex( mcIndex , f ); int xx = o==1 ? 2*x : 1 , yy = o==0 ? 2*x : 1 , zz = 1; if( !xValues.faceSet[ eIndices[e] ] && ( !neighborKey.neighbors[depth].neighbors[xx][yy][zz] || !neighborKey.neighbors[depth].neighbors[xx][yy][zz]->children ) ) { FaceEdges fe; fe.count = MarchingSquares::AddEdgeIndices( _mcIndex , isoEdges ); for( int j=0 ; j& sValues = (_x==0) ? bValues : fValues; int idx = sValues.sliceData.edgeIndices(i)[ Square::EdgeIndex(o,x) ]; if( !sValues.edgeSet[ idx ] ) fprintf( stderr , "[ERROR] Edge not set 5: %d / %d\n" , slab , 1< edges; edges.resize( fe.count ); for( int j=0 ; jparent && Cube::IsFaceCorner( (int)(node-node->parent->children) , f ) ) { node = node->parent , _depth-- , _slab >>= 1; if( neighborKey.neighbors[_depth].neighbors[xx][yy][zz] && neighborKey.neighbors[_depth].neighbors[xx][yy][zz]->children ) break; long long key = VertexData::FaceIndex( node , f , _sNodes.levels() ); #pragma omp critical( add_x_iso_edge_access ) { typename Octree::template XSliceValues< Vertex >& _xValues = slabValues[_depth].xSliceValues( _slab ); typename hash_map< long long , std::vector< IsoEdge > >::iterator iter = _xValues.faceEdgeMap.find(key); if( iter==_xValues.faceEdgeMap.end() ) _xValues.faceEdgeMap[key] = edges; else for( int j=0 ; jsecond.push_back( fe.edges[j] ); } } } } } } } } template< class Real > template< class Vertex > void Octree< Real >::SetIsoSurface( int depth , int offset , const SliceValues< Vertex >& bValues , const SliceValues< Vertex >& fValues , const XSliceValues< Vertex >& xValues , CoredMeshData< Vertex >& mesh , bool polygonMesh , bool addBarycenter , int& vOffset , int threads ) { std::vector< std::pair< int , Vertex > > polygon; std::vector< std::vector< IsoEdge > > edgess( std::max< int >( 1 , threads ) ); #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(depth,offset) ; i<_sNodes.end(depth,offset) ; i++ ) if( _IsValidNode< 0 >( _sNodes.treeNodes[i] ) ) { std::vector< IsoEdge >& edges = edgess[ omp_get_thread_num() ]; TreeOctNode* leaf = _sNodes.treeNodes[i]; int d , off[3]; leaf->depthAndOffset( d , off ); int res = _Resolution( depth ); bool inBounds = off[0]children ) { edges.clear(); unsigned char mcIndex = ( bValues.mcIndices[ i - bValues.sliceData.nodeOffset ] ) | ( fValues.mcIndices[ i - fValues.sliceData.nodeOffset ]<<4 ); // [WARNING] Just because the node looks empty doesn't mean it doesn't get eges from finer neighbors { // Gather the edges from the faces (with the correct orientation) for( int f=0 ; f& sValues = (o==0) ? bValues : fValues; int fIdx = sValues.sliceData.faceIndices(i)[0]; if( sValues.faceSet[fIdx] ) { const FaceEdges& fe = sValues.faceEdges[ fIdx ]; for( int j=0 ; j >::const_iterator iter = sValues.faceEdgeMap.find( key ); if( iter!=sValues.faceEdgeMap.end() ) { const std::vector< IsoEdge >& _edges = iter->second; for( size_t j=0 ; j<_edges.size() ; j++ ) edges.push_back( IsoEdge( _edges[j][flip] , _edges[j][1-flip] ) ); } else fprintf( stderr , "[ERROR] Invalid faces: %d %d %d\n" , i , d , o ) , exit( 0 ); } } else { int fIdx = xValues.xSliceData.faceIndices(i)[ Square::EdgeIndex( 1-d , o ) ]; if( xValues.faceSet[fIdx] ) { const FaceEdges& fe = xValues.faceEdges[ fIdx ]; for( int j=0 ; j >::const_iterator iter = xValues.faceEdgeMap.find( key ); if( iter!=xValues.faceEdgeMap.end() ) { const std::vector< IsoEdge >& _edges = iter->second; for( size_t j=0 ; j<_edges.size() ; j++ ) edges.push_back( IsoEdge( _edges[j][flip] , _edges[j][1-flip] ) ); } else fprintf( stderr , "[ERROR] Invalid faces: %d %d %d\n" , i , d , o ) , exit( 0 ); } } } // Get the edge loops std::vector< std::vector< long long > > loops; while( edges.size() ) { loops.resize( loops.size()+1 ); IsoEdge edge = edges.back(); edges.pop_back(); long long start = edge[0] , current = edge[1]; while( current!=start ) { int idx; for( idx=0 ; idx<(int)edges.size() ; idx++ ) if( edges[idx][0]==current ) break; if( idx==edges.size() ) { typename hash_map< long long , long long >::const_iterator iter; if ( (iter=bValues.vertexPairMap.find(current))!=bValues.vertexPairMap.end() ) loops.back().push_back( current ) , current = iter->second; else if( (iter=fValues.vertexPairMap.find(current))!=fValues.vertexPairMap.end() ) loops.back().push_back( current ) , current = iter->second; else if( (iter=xValues.vertexPairMap.find(current))!=xValues.vertexPairMap.end() ) loops.back().push_back( current ) , current = iter->second; else { int d , off[3]; leaf->depthAndOffset( d , off ); fprintf( stderr , "[ERROR] Failed to close loop [%d: %d %d %d] | (%d): %lld\n" , d-1 , off[0] , off[1] , off[2] , i , current ); exit( 0 ); } } else { loops.back().push_back( current ); current = edges[idx][1]; edges[idx] = edges.back() , edges.pop_back(); } } loops.back().push_back( start ); } // Add the loops to the mesh for( size_t j=0 ; j > polygon( loops[j].size() ); for( size_t k=0 ; k >::const_iterator iter; if ( ( iter=bValues.edgeVertexMap.find( key ) )!=bValues.edgeVertexMap.end() ) polygon[k] = iter->second; else if( ( iter=fValues.edgeVertexMap.find( key ) )!=fValues.edgeVertexMap.end() ) polygon[k] = iter->second; else if( ( iter=xValues.edgeVertexMap.find( key ) )!=xValues.edgeVertexMap.end() ) polygon[k] = iter->second; else fprintf( stderr , "[ERROR] Couldn't find vertex in edge map\n" ) , exit( 0 ); } AddIsoPolygons( mesh , polygon , polygonMesh , addBarycenter , vOffset ); } } } } } template< class Real > void SetColor( Point3D< Real >& color , unsigned char c[3] ){ for( int i=0 ; i<3 ; i++ ) c[i] = (unsigned char)std::max< int >( 0 , std::min< int >( 255 , (int)( color[i]+0.5 ) ) ); } template< class Real > void SetIsoVertex( PlyVertex< float >& vertex , Point3D< Real > color , Real value ){ ; } template< class Real > void SetIsoVertex( PlyColorVertex< float >& vertex , Point3D< Real > color , Real value ){ SetColor( color , vertex.color ); } template< class Real > void SetIsoVertex( PlyValueVertex< float >& vertex , Point3D< Real > color , Real value ){ vertex.value = float(value); } template< class Real > void SetIsoVertex( PlyColorAndValueVertex< float >& vertex , Point3D< Real > color , Real value ){ SetColor( color , vertex.color ) , vertex.value = float(value); } template< class Real > void SetIsoVertex( PlyVertex< double >& vertex , Point3D< Real > color , Real value ){ ; } template< class Real > void SetIsoVertex( PlyColorVertex< double >& vertex , Point3D< Real > color , Real value ){ SetColor( color , vertex.color ); } template< class Real > void SetIsoVertex( PlyValueVertex< double >& vertex , Point3D< Real > color , Real value ){ vertex.value = double(value); } template< class Real > void SetIsoVertex( PlyColorAndValueVertex< double >& vertex , Point3D< Real > color , Real value ){ SetColor( color , vertex.color ) , vertex.value = double(value); } template< class Real > template< int WeightDegree , int ColorDegree , class Vertex > bool Octree< Real >::GetIsoVertex( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , ConstPointSupportKey< WeightDegree >& weightKey , ConstPointSupportKey< ColorDegree >& colorKey , const TreeOctNode* node , int edgeIndex , int z , const SliceValues< Vertex >& sValues , Vertex& vertex ) { Point3D< Real > position; int c0 , c1; Square::EdgeCorners( edgeIndex , c0 , c1 ); bool nonLinearFit = sValues.cornerGradients!=NullPointer( Point3D< Real > ); const typename SortedTreeNodes::SquareCornerIndices& idx = sValues.sliceData.cornerIndices( node ); Real x0 = sValues.cornerValues[idx[c0]] , x1 = sValues.cornerValues[idx[c1]]; Point3D< Real > s; Real start , width; _StartAndWidth( node , s , width ); int o , y; Square::FactorEdgeIndex( edgeIndex , o , y ); start = s[o]; switch( o ) { case 0: position[1] = s[1] + width*y; position[2] = s[2] + width*z; break; case 1: position[0] = s[0] + width*y; position[2] = s[2] + width*z; break; } double averageRoot; if( nonLinearFit ) { double dx0 = sValues.cornerGradients[idx[c0]][o] * width , dx1 = sValues.cornerGradients[idx[c1]][o] * width; // The scaling will turn the Hermite Spline into a quadratic double scl = (x1-x0) / ( (dx1+dx0 ) / 2 ); dx0 *= scl , dx1 *= scl; // Hermite Spline Polynomial< 2 > P; P.coefficients[0] = x0; P.coefficients[1] = dx0; P.coefficients[2] = 3*(x1-x0)-dx1-2*dx0; double roots[2]; int rCount = 0 , rootCount = P.getSolutions( isoValue , roots , EPSILON ); averageRoot = 0; for( int i=0 ; i=0 && roots[i]<=1 ) averageRoot += roots[i] , rCount++; averageRoot /= rCount; } else { // We have a linear function L, with L(0) = x0 and L(1) = x1 // => L(t) = x0 + t * (x1-x0) // => L(t) = isoValue <=> t = ( isoValue - x0 ) / ( x1 - x0 ) if( x0==x1 ) fprintf( stderr , "[ERROR] Not a zero-crossing root: %g %g\n" , x0 , x1 ) , exit( 0 ); averageRoot = ( isoValue - x0 ) / ( x1 - x0 ); } if( averageRoot<0 || averageRoot>1 ) { fprintf( stderr , "[WARNING] Bad average root: %f\n" , averageRoot ); fprintf( stderr , "\t(%f %f) (%f)\n" , x0 , x1 , isoValue ); if( averageRoot<0 ) averageRoot = 0; if( averageRoot>1 ) averageRoot = 1; } position[o] = Real( start + width*averageRoot ); vertex.point = position; Point3D< Real > color; Real depth(0); if( densityWeights ) { Real weight; const TreeOctNode* temp = node; while( _Depth( temp )>_splatDepth ) temp=temp->parent; _GetSampleDepthAndWeight( *densityWeights , temp , position , weightKey , depth , weight ); } if( colorData ) color = Point3D< Real >( _Evaluate( *colorData , position , *colorBSData , colorKey ) ); SetIsoVertex( vertex , color , depth ); return true; } template< class Real > template< int WeightDegree , int ColorDegree , class Vertex > bool Octree< Real >::GetIsoVertex( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , ConstPointSupportKey< WeightDegree >& weightKey , ConstPointSupportKey< ColorDegree >& colorKey , const TreeOctNode* node , int cornerIndex , const SliceValues< Vertex >& bValues , const SliceValues< Vertex >& fValues , Vertex& vertex ) { Point3D< Real > position; bool nonLinearFit = bValues.cornerGradients!=NullPointer( Point3D< Real > ) && fValues.cornerGradients!=NullPointer( Point3D< Real > ); const typename SortedTreeNodes::SquareCornerIndices& idx0 = bValues.sliceData.cornerIndices( node ); const typename SortedTreeNodes::SquareCornerIndices& idx1 = fValues.sliceData.cornerIndices( node ); Real x0 = bValues.cornerValues[ idx0[cornerIndex] ] , x1 = fValues.cornerValues[ idx1[cornerIndex] ]; Point3D< Real > s; Real start , width; _StartAndWidth( node , s , width ); start = s[2]; int x , y; Square::FactorCornerIndex( cornerIndex , x , y ); position[0] = s[0] + width*x; position[1] = s[1] + width*y; double averageRoot; if( nonLinearFit ) { double dx0 = bValues.cornerGradients[ idx0[cornerIndex] ][2] * width , dx1 = fValues.cornerGradients[ idx1[cornerIndex] ][2] * width; // The scaling will turn the Hermite Spline into a quadratic double scl = (x1-x0) / ( (dx1+dx0 ) / 2 ); dx0 *= scl , dx1 *= scl; // Hermite Spline Polynomial< 2 > P; P.coefficients[0] = x0; P.coefficients[1] = dx0; P.coefficients[2] = 3*(x1-x0)-dx1-2*dx0; double roots[2]; int rCount = 0 , rootCount = P.getSolutions( isoValue , roots , EPSILON ); averageRoot = 0; for( int i=0 ; i=0 && roots[i]<=1 ) averageRoot += roots[i] , rCount++; averageRoot /= rCount; } else { // We have a linear function L, with L(0) = x0 and L(1) = x1 // => L(t) = x0 + t * (x1-x0) // => L(t) = isoValue <=> t = ( isoValue - x0 ) / ( x1 - x0 ) if( x0==x1 ) fprintf( stderr , "[ERROR] Not a zero-crossing root: %g %g\n" , x0 , x1 ) , exit( 0 ); averageRoot = ( isoValue - x0 ) / ( x1 - x0 ); } if( averageRoot<0 || averageRoot>1 ) { fprintf( stderr , "[WARNING] Bad average root: %f\n" , averageRoot ); fprintf( stderr , "\t(%f %f) (%f)\n" , x0 , x1 , isoValue ); if( averageRoot<0 ) averageRoot = 0; if( averageRoot>1 ) averageRoot = 1; } position[2] = Real( start + width*averageRoot ); vertex.point = position; Point3D< Real > color; Real depth(0); if( densityWeights ) { Real weight; const TreeOctNode* temp = node; while( _Depth( temp )>_splatDepth ) temp=temp->parent; _GetSampleDepthAndWeight( *densityWeights , temp , position , weightKey , depth , weight ); } if( colorData ) color = Point3D< Real >( _Evaluate( *colorData , position , *colorBSData , colorKey ) ); SetIsoVertex( vertex , color , depth ); return true; } template< class Real > template< class Vertex > int Octree< Real >::AddIsoPolygons( CoredMeshData< Vertex >& mesh , std::vector< std::pair< int , Vertex > >& polygon , bool polygonMesh , bool addBarycenter , int& vOffset ) { if( polygonMesh ) { std::vector< int > vertices( polygon.size() ); for( int i=0 ; i<(int)polygon.size() ; i++ ) vertices[i] = polygon[polygon.size()-1-i].first; mesh.addPolygon_s( vertices ); return 1; } if( polygon.size()>3 ) { bool isCoplanar = false; std::vector< int > triangle( 3 ); if( addBarycenter ) for( int i=0 ; i<(int)polygon.size() ; i++ ) for( int j=0 ; j MAT; std::vector< Point3D< Real > > vertices; std::vector< TriangleIndex > triangles; vertices.resize( polygon.size() ); // Add the points for( int i=0 ; i<(int)polygon.size() ; i++ ) vertices[i] = polygon[i].second.point; MAT.GetTriangulation( vertices , triangles ); for( int i=0 ; i<(int)triangles.size() ; i++ ) { for( int j=0 ; j<3 ; j++ ) triangle[2-j] = polygon[ triangles[i].idx[j] ].first; mesh.addPolygon_s( triangle ); } } } else if( polygon.size()==3 ) { std::vector< int > vertices( 3 ); for( int i=0 ; i<3 ; i++ ) vertices[2-i] = polygon[i].first; mesh.addPolygon_s( vertices ); } return (int)polygon.size()-2; } colmap-3.9.1/src/thirdparty/PoissonRecon/MultiGridOctreeData.SortedTreeNodes.inl000077500000000000000000000411211454702036400300200ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ ///////////////////// // SortedTreeNodes // ///////////////////// SortedTreeNodes::SortedTreeNodes( void ) { _sliceStart = NullPointer( Pointer( int ) ); treeNodes = NullPointer( TreeOctNode* ); _levels = 0; } SortedTreeNodes::~SortedTreeNodes( void ) { if( _sliceStart ) for( int d=0 ; d<_levels ; d++ ) FreePointer( _sliceStart[d] ); FreePointer( _sliceStart ); DeletePointer( treeNodes ); } void SortedTreeNodes::set( TreeOctNode& root , std::vector< int >* map ) { set( root ); if( map ) { map->resize( _sliceStart[_levels-1][(size_t)1<<(_levels-1)] ); for( int i=0 ; i<_sliceStart[_levels-1][(size_t)1<<(_levels-1)] ; i++ ) (*map)[i] = treeNodes[i]->nodeData.nodeIndex; } for( int i=0 ; i<_sliceStart[_levels-1][(size_t)1<<(_levels-1)] ; i++ ) treeNodes[i]->nodeData.nodeIndex = i; } void SortedTreeNodes::set( TreeOctNode& root ) { _levels = root.maxDepth()+1; if( _sliceStart ) for( int d=0 ; d<_levels ; d++ ) FreePointer( _sliceStart[d] ); FreePointer( _sliceStart ); DeletePointer( treeNodes ); _sliceStart = AllocPointer< Pointer( int ) >( _levels ); for( int l=0 ; l<_levels ; l++ ) { _sliceStart[l] = AllocPointer< int >( ((size_t)1<depthAndOffset( d , off ); _sliceStart[d][ off[2]+1 ]++; } // Get the start index for each slice { int levelOffset = 0; for( int l=0 ; l<_levels ; l++ ) { _sliceStart[l][0] = levelOffset; for( int s=0 ; s<((size_t)1<( _sliceStart[_levels-1][(size_t)1<<(_levels-1)] ); // Add the tree nodes for( TreeOctNode* node=root.nextNode() ; node ; node=root.nextNode( node ) ) { int d , off[3]; node->depthAndOffset( d , off ); treeNodes[ _sliceStart[d][ off[2] ]++ ] = node; } // Shift the slice offsets up since we incremented as we added for( int l=0 ; l<_levels ; l++ ) { for( int s=(1<0 ; s-- ) _sliceStart[l][s] = _sliceStart[l][s-1]; _sliceStart[l][0] = l>0 ? _sliceStart[l-1][(size_t)1<<(l-1)] : 0; } } SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::SliceTableData::cornerIndices( const TreeOctNode* node ) { return cTable[ node->nodeData.nodeIndex - nodeOffset ]; } SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::SliceTableData::cornerIndices( int idx ) { return cTable[ idx - nodeOffset ]; } const SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::SliceTableData::cornerIndices( const TreeOctNode* node ) const { return cTable[ node->nodeData.nodeIndex - nodeOffset ]; } const SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::SliceTableData::cornerIndices( int idx ) const { return cTable[ idx - nodeOffset ]; } SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::SliceTableData::edgeIndices( const TreeOctNode* node ) { return eTable[ node->nodeData.nodeIndex - nodeOffset ]; } SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::SliceTableData::edgeIndices( int idx ) { return eTable[ idx - nodeOffset ]; } const SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::SliceTableData::edgeIndices( const TreeOctNode* node ) const { return eTable[ node->nodeData.nodeIndex - nodeOffset ]; } const SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::SliceTableData::edgeIndices( int idx ) const { return eTable[ idx - nodeOffset ]; } SortedTreeNodes::SquareFaceIndices& SortedTreeNodes::SliceTableData::faceIndices( const TreeOctNode* node ) { return fTable[ node->nodeData.nodeIndex - nodeOffset ]; } SortedTreeNodes::SquareFaceIndices& SortedTreeNodes::SliceTableData::faceIndices( int idx ) { return fTable[ idx - nodeOffset ]; } const SortedTreeNodes::SquareFaceIndices& SortedTreeNodes::SliceTableData::faceIndices( const TreeOctNode* node ) const { return fTable[ node->nodeData.nodeIndex - nodeOffset ]; } const SortedTreeNodes::SquareFaceIndices& SortedTreeNodes::SliceTableData::faceIndices( int idx ) const { return fTable[ idx - nodeOffset ]; } SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::XSliceTableData::edgeIndices( const TreeOctNode* node ) { return eTable[ node->nodeData.nodeIndex - nodeOffset ]; } SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::XSliceTableData::edgeIndices( int idx ) { return eTable[ idx - nodeOffset ]; } const SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::XSliceTableData::edgeIndices( const TreeOctNode* node ) const { return eTable[ node->nodeData.nodeIndex - nodeOffset ]; } const SortedTreeNodes::SquareCornerIndices& SortedTreeNodes::XSliceTableData::edgeIndices( int idx ) const { return eTable[ idx - nodeOffset ]; } SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::XSliceTableData::faceIndices( const TreeOctNode* node ) { return fTable[ node->nodeData.nodeIndex - nodeOffset ]; } SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::XSliceTableData::faceIndices( int idx ) { return fTable[ idx - nodeOffset ]; } const SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::XSliceTableData::faceIndices( const TreeOctNode* node ) const { return fTable[ node->nodeData.nodeIndex - nodeOffset ]; } const SortedTreeNodes::SquareEdgeIndices& SortedTreeNodes::XSliceTableData::faceIndices( int idx ) const { return fTable[ idx - nodeOffset ]; } void SortedTreeNodes::setSliceTableData( SliceTableData& sData , int depth , int offset , int threads ) const { // [NOTE] This is structure is purely for determining adjacency and is independent of the FEM degree typedef OctNode< TreeNodeData >::template ConstNeighborKey< 1 , 1 > ConstAdjacenctNodeKey; if( offset<0 || offset>((size_t)1< span( _sliceStart[depth][ std::max< int >( 0 , offset-1 ) ] , _sliceStart[depth][ std::min< int >( (size_t)1<( sData.nodeCount * Square::CORNERS ); sData._eMap = NewPointer< int >( sData.nodeCount * Square::EDGES ); sData._fMap = NewPointer< int >( sData.nodeCount * Square::FACES ); sData.cTable = NewPointer< typename SortedTreeNodes::SquareCornerIndices >( sData.nodeCount ); sData.eTable = NewPointer< typename SortedTreeNodes::SquareCornerIndices >( sData.nodeCount ); sData.fTable = NewPointer< typename SortedTreeNodes::SquareFaceIndices >( sData.nodeCount ); memset( sData._cMap , 0 , sizeof(int) * sData.nodeCount * Square::CORNERS ); memset( sData._eMap , 0 , sizeof(int) * sData.nodeCount * Square::EDGES ); memset( sData._fMap , 0 , sizeof(int) * sData.nodeCount * Square::FACES ); } std::vector< ConstAdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i& neighbors = neighborKey.getNeighbors( node ); int d , off[3]; node->depthAndOffset( d , off ); int z; if ( off[2]==offset-1 ) z = 1; else if( off[2]==offset ) z = 0; else fprintf( stderr , "[ERROR] Node out of bounds: %d %d\n" , offset , off[2] ) , exit( 0 ); // Process the corners for( int x=0 ; x<2 ; x++ ) for( int y=0 ; y<2 ; y++ ) { int c = Cube::CornerIndex( x , y , z ); int fc = Square::CornerIndex( x , y ); bool cornerOwner = true; int ac = Cube::AntipodalCornerIndex(c); // The index of the node relative to the corner for( int cc=0 ; cc::template ConstNeighborKey< 1 , 1 > ConstAdjacenctNodeKey; if( offset<0 || offset>=((size_t)1< span( _sliceStart[depth][offset] , _sliceStart[depth][offset+1] ); sData.nodeOffset = span.first; sData.nodeCount = span.second - span.first; DeletePointer( sData._eMap ) ; DeletePointer( sData._fMap ); DeletePointer( sData.eTable ) ; DeletePointer( sData.fTable ); if( sData.nodeCount ) { sData._eMap = NewPointer< int >( sData.nodeCount * Square::CORNERS ); sData._fMap = NewPointer< int >( sData.nodeCount * Square::EDGES ); sData.eTable = NewPointer< typename SortedTreeNodes::SquareCornerIndices >( sData.nodeCount ); sData.fTable = NewPointer< typename SortedTreeNodes::SquareEdgeIndices >( sData.nodeCount ); memset( sData._eMap , 0 , sizeof(int) * sData.nodeCount * Square::CORNERS ); memset( sData._fMap , 0 , sizeof(int) * sData.nodeCount * Square::EDGES ); } std::vector< ConstAdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i& neighbors = neighborKey.getNeighbors( node ); int d , off[3]; node->depthAndOffset( d , off ); // Process the edges int o=2; for( int x=0 ; x<2 ; x++ ) for( int y=0 ; y<2 ; y++ ) { int fc = Square::CornerIndex( x , y ); bool edgeOwner = true; int ac = Square::AntipodalCornerIndex( Square::CornerIndex( x , y ) ); for( int cc=0 ; cc double SystemCoefficients< Degree1 , Degree2 >::GetLaplacian( const typename FunctionIntegrator::Integrator& integrator , const int off1[] , const int off2[] ) { double vv[] = { integrator.dot( off1[0] , off2[0] , false , false ) , integrator.dot( off1[1] , off2[1] , false , false ) , integrator.dot( off1[2] , off2[2] , false , false ) }; double dd[] = { integrator.dot( off1[0] , off2[0] , true , true ) , integrator.dot( off1[1] , off2[1] , true , true ) , integrator.dot( off1[2] , off2[2] , true , true ) }; return dd[0]*vv[1]*vv[2] + vv[0]*dd[1]*vv[2] + vv[0]*vv[1]*dd[2]; } template< int Degree1 , int Degree2 > double SystemCoefficients< Degree1 , Degree2 >::GetLaplacian( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[] , const int off2[] ) { double vv[] = { integrator.dot( off1[0] , off2[0] , false , false ) , integrator.dot( off1[1] , off2[1] , false , false ) , integrator.dot( off1[2] , off2[2] , false , false ) }; double dd[] = { integrator.dot( off1[0] , off2[0] , true , true ) , integrator.dot( off1[1] , off2[1] , true , true ) , integrator.dot( off1[2] , off2[2] , true , true ) }; return dd[0]*vv[1]*vv[2] + vv[0]*dd[1]*vv[2] + vv[0]*vv[1]*dd[2]; } template< int Degree1 , int Degree2 > double SystemCoefficients< Degree1 , Degree2 >::GetDivergence1( const typename FunctionIntegrator::Integrator& integrator , const int off1[] , const int off2[] , Point3D< double > normal1 ) { return Point3D< double >::Dot( GetDivergence1( integrator , off1 , off2 ) , normal1 ); } template< int Degree1 , int Degree2 > double SystemCoefficients< Degree1 , Degree2 >::GetDivergence1( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[] , const int off2[] , Point3D< double > normal1 ) { return Point3D< double >::Dot( GetDivergence1( integrator , off1 , off2 ) , normal1 ); } template< int Degree1 , int Degree2 > double SystemCoefficients< Degree1 , Degree2 >::GetDivergence2( const typename FunctionIntegrator::Integrator& integrator , const int off1[] , const int off2[] , Point3D< double > normal2 ) { return Point3D< double >::Dot( GetDivergence2( integrator , off1 , off2 ) , normal2 ); } template< int Degree1 , int Degree2 > double SystemCoefficients< Degree1 , Degree2 >::GetDivergence2( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[] , const int off2[] , Point3D< double > normal2 ) { return Point3D< double >::Dot( GetDivergence2( integrator , off1 , off2 ) , normal2 ); } template< int Degree1 , int Degree2 > Point3D< double > SystemCoefficients< Degree1 , Degree2 >::GetDivergence1( const typename FunctionIntegrator::Integrator& integrator , const int off1[] , const int off2[] ) { double vv[] = { integrator.dot( off1[0] , off2[0] , false , false ) , integrator.dot( off1[1] , off2[1] , false , false ) , integrator.dot( off1[2] , off2[2] , false , false ) }; #if GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the vector-field with the gradient of the basis function double vd[] = { integrator.dot( off1[0] , off2[0] , true , false ) , integrator.dot( off1[1] , off2[1] , true , false ) , integrator.dot( off1[2] , off2[2] , true , false ) }; return Point3D< double >( vd[0]*vv[1]*vv[2] , vv[0]*vd[1]*vv[2] , vv[0]*vv[1]*vd[2] ); #else // !GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the divergence of the vector-field with the basis function double dv[] = { integrator.dot( off1[0] , off2[0] , false , true ) , integrator.dot( off1[1] , off2[1] , false , true ) , integrator.dot( off1[2] , off2[2] , false , true ) }; return -Point3D< double >( dv[0]*vv[1]*vv[2] , vv[0]*dv[1]*vv[2] , vv[0]*vv[1]*dv[2] ); #endif // GRADIENT_DOMAIN_SOLUTION } template< int Degree1 , int Degree2 > Point3D< double > SystemCoefficients< Degree1 , Degree2 >::GetDivergence1( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[] , const int off2[] ) { double vv[] = { integrator.dot( off1[0] , off2[0] , false , false ) , integrator.dot( off1[1] , off2[1] , false , false ) , integrator.dot( off1[2] , off2[2] , false , false ) }; #if GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the vector-field with the gradient of the basis function double vd[] = { integrator.dot( off1[0] , off2[0] , true , false ) , integrator.dot( off1[1] , off2[1] , true , false ) , integrator.dot( off1[2] , off2[2] , true , false ) }; return Point3D< double >( vd[0]*vv[1]*vv[2] , vv[0]*vd[1]*vv[2] , vv[0]*vv[1]*vd[2] ); #else // !GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the divergence of the vector-field with the basis function double dv[] = { integrator.dot( off1[0] , off2[0] , false , true ) , integrator.dot( off1[1] , off2[1] , false , true ) , integrator.dot( off1[2] , off2[2] , false , true ) }; return -Point3D< double >( dv[0]*vv[1]*vv[2] , vv[0]*dv[1]*vv[2] , vv[0]*vv[1]*dv[2] ); #endif // GRADIENT_DOMAIN_SOLUTION } template< int Degree1 , int Degree2 > Point3D< double > SystemCoefficients< Degree1 , Degree2 >::GetDivergence2( const typename FunctionIntegrator::Integrator& integrator , const int off1[] , const int off2[] ) { double vv[] = { integrator.dot( off1[0] , off2[0] , false , false ) , integrator.dot( off1[1] , off2[1] , false , false ) , integrator.dot( off1[2] , off2[2] , false , false ) }; #if GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the vector-field with the gradient of the basis function double dv[] = { integrator.dot( off1[0] , off2[0] , false , true ) , integrator.dot( off1[1] , off2[1] , false , true ) , integrator.dot( off1[2] , off2[2] , false , true ) }; return Point3D< double >( dv[0]*vv[1]*vv[2] , vv[0]*dv[1]*vv[2] , vv[0]*vv[1]*dv[2] ); #else // !GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the divergence of the vector-field with the basis function double vd[] = { integrator.dot( off1[0] , off2[0] , true , false ) , integrator.dot( off1[1] , off2[1] , true , false ) , integrator.dot( off1[2] , off2[2] , true , false ) }; return -Point3D< double >( vd[0]*vv[1]*vv[2] , vv[0]*vd[1]*vv[2] , vv[0]*vv[1]*vd[2] ); #endif // GRADIENT_DOMAIN_SOLUTION } template< int Degree1 , int Degree2 > Point3D< double > SystemCoefficients< Degree1 , Degree2 >::GetDivergence2( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[] , const int off2[] ) { double vv[] = { integrator.dot( off1[0] , off2[0] , false , false ) , integrator.dot( off1[1] , off2[1] , false , false ) , integrator.dot( off1[2] , off2[2] , false , false ) }; #if GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the vector-field with the gradient of the basis function double dv[] = { integrator.dot( off1[0] , off2[0] , false , true ) , integrator.dot( off1[1] , off2[1] , false , true ) , integrator.dot( off1[2] , off2[2] , false , true ) }; return Point3D< double >( dv[0]*vv[1]*vv[2] , vv[0]*dv[1]*vv[2] , vv[0]*vv[1]*dv[2] ); #else // !GRADIENT_DOMAIN_SOLUTION // Take the dot-product of the divergence of the vector-field with the basis function double vd[] = { integrator.dot( off1[0] , off2[0] , true , false ) , integrator.dot( off1[1] , off2[1] , true , false ) , integrator.dot( off1[2] , off2[2] , true , false ) }; return -Point3D< double >( vd[0]*vv[1]*vv[2] , vv[0]*vd[1]*vv[2] , vv[0]*vv[1]*vd[2] ); #endif // GRADIENT_DOMAIN_SOLUTION } // if( scatter ) normals come from the center node // else normals come from the neighbors template< int Degree1 , int Degree2 > void SystemCoefficients< Degree1 , Degree2 >::SetCentralDivergenceStencil( const typename FunctionIntegrator::Integrator& integrator , Stencil< Point3D< double > , OverlapSize >& stencil , bool scatter ) { int center = ( 1<>1; int offset[] = { center , center , center }; for( int x=0 ; x void SystemCoefficients< Degree1 , Degree2 >::SetCentralDivergenceStencils( const typename FunctionIntegrator::ChildIntegrator& integrator , Stencil< Point3D< double > , OverlapSize > stencils[2][2][2] , bool scatter ) { int center = ( 1<>1; for( int i=0 ; i<2 ; i++ ) for( int j=0 ; j<2 ; j++ ) for( int k=0 ; k<2 ; k++ ) { int offset[] = { center+i , center+j , center+k }; for( int x=0 ; x void SystemCoefficients< Degree1 , Degree2 >::SetCentralLaplacianStencil( const typename FunctionIntegrator::Integrator& integrator , Stencil< double , OverlapSize >& stencil ) { int center = ( 1<>1; int offset[] = { center , center , center }; for( int x=0 ; x void SystemCoefficients< Degree1 , Degree2 >::SetCentralLaplacianStencils( const typename FunctionIntegrator::ChildIntegrator& integrator , Stencil< double , OverlapSize > stencils[2][2][2] ) { int center = ( 1<>1; for( int i=0 ; i<2 ; i++ ) for( int j=0 ; j<2 ; j++ ) for( int k=0 ; k<2 ; k++ ) { int offset[] = { center+i , center+j , center+k }; for( int x=0 ; x template< int FEMDegree > void Octree< Real >::_setMultiColorIndices( int start , int end , std::vector< std::vector< int > >& indices ) const { static const int OverlapRadius = - BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; const int modulus = OverlapRadius+1; indices.resize( modulus*modulus*modulus ); int count[modulus*modulus*modulus]; memset( count , 0 , sizeof(int)*modulus*modulus*modulus ); #pragma omp parallel for num_threads( threads ) for( int i=start ; i( _sNodes.treeNodes[i] ) ) { int d , off[3]; _sNodes.treeNodes[i]->depthAndOffset( d , off ); int idx = (modulus*modulus) * ( off[2]%modulus ) + modulus * ( off[1]%modulus ) + ( off[0]%modulus ); #pragma omp atomic count[idx]++; } for( int i=0 ; i( _sNodes.treeNodes[i] ) ) { int d , off[3]; _sNodes.treeNodes[i]->depthAndOffset( d , off ); int idx = (modulus*modulus) * ( off[2]%modulus ) + modulus * ( off[1]%modulus ) + ( off[0]%modulus ); indices[idx].push_back( i - start ); } } template< class Real > template< class C , int FEMDegree > void Octree< Real >::_DownSample( int highDepth , DenseNodeData< C , FEMDegree >& constraints ) const { typedef typename TreeOctNode::NeighborKey< -BSplineEvaluationData< FEMDegree >::UpSampleStart , BSplineEvaluationData< FEMDegree >::UpSampleEnd > UpSampleKey; int lowDepth = highDepth-1; if( lowDepth<_minDepth ) return; typename BSplineEvaluationData< FEMDegree >::UpSampleEvaluator upSampleEvaluator; BSplineEvaluationData< FEMDegree >::SetUpSampleEvaluator( upSampleEvaluator , lowDepth-1 , _dirichlet ); std::vector< UpSampleKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i::UpSampleSize > upSampleStencil; int lowCenter = _Dimension< FEMDegree >(lowDepth)>>1; for( int i=0 ; i::UpSampleSize ; i++ ) for( int j=0 ; j::UpSampleSize ; j++ ) for( int k=0 ; k::UpSampleSize ; k++ ) upSampleStencil.values[i][j][k] = upSampleEvaluator.value( lowCenter , 2*lowCenter + i + BSplineEvaluationData< FEMDegree >::UpSampleStart ) * upSampleEvaluator.value( lowCenter , 2*lowCenter + j + BSplineEvaluationData< FEMDegree >::UpSampleStart ) * upSampleEvaluator.value( lowCenter , 2*lowCenter + k + BSplineEvaluationData< FEMDegree >::UpSampleStart ); int dim = _Dimension< FEMDegree >(lowDepth); // Iterate over all (valid) parent nodes #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(lowDepth) ; i<_sNodes.end(lowDepth) ; i++ ) if( _IsValidNode< FEMDegree >( _sNodes.treeNodes[i] ) ) { TreeOctNode* pNode = _sNodes.treeNodes[i]; UpSampleKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; int d , off[3]; pNode->depthAndOffset( d , off ); neighborKey.template getNeighbors< false >( pNode ); // Get the child neighbors typename TreeOctNode::Neighbors< BSplineEvaluationData< FEMDegree >::UpSampleSize > neighbors; neighborKey.template getChildNeighbors< false >( 0 , d , neighbors ); C& coarseConstraint = constraints[i]; // Want to make sure test if contained children are interior. // This is more conservative because we are test that overlapping children are interior bool isInterior = _IsInteriorlyOverlapped< FEMDegree , FEMDegree >( pNode ); if( isInterior ) { for( int ii=0 ; ii::UpSampleSize ; ii++ ) for( int jj=0 ; jj::UpSampleSize ; jj++ ) for( int kk=0 ; kk::UpSampleSize ; kk++ ) { const TreeOctNode* cNode = neighbors.neighbors[ii][jj][kk]; if( cNode ) coarseConstraint += (C)( constraints[ cNode->nodeData.nodeIndex ] * upSampleStencil.values[ii][jj][kk] ); } } else { double upSampleValues[3][ BSplineEvaluationData< FEMDegree >::UpSampleSize ]; for( int ii=0 ; ii::UpSampleSize ; ii++ ) { upSampleValues[0][ii] = upSampleEvaluator.value( off[0] , 2*off[0] + ii + BSplineEvaluationData< FEMDegree >::UpSampleStart ); upSampleValues[1][ii] = upSampleEvaluator.value( off[1] , 2*off[1] + ii + BSplineEvaluationData< FEMDegree >::UpSampleStart ); upSampleValues[2][ii] = upSampleEvaluator.value( off[2] , 2*off[2] + ii + BSplineEvaluationData< FEMDegree >::UpSampleStart ); } for( int ii=0 ; ii::UpSampleSize ; ii++ ) for( int jj=0 ; jj::UpSampleSize ; jj++ ) { double dxy = upSampleValues[0][ii] * upSampleValues[1][jj]; for( int kk=0 ; kk::UpSampleSize ; kk++ ) { const TreeOctNode* cNode = neighbors.neighbors[ii][jj][kk]; if( _IsValidNode< FEMDegree >( cNode ) ) coarseConstraint += (C)( constraints[ cNode->nodeData.nodeIndex ] * dxy * upSampleValues[2][kk] ); } } } } } template< class Real > template< class C , int FEMDegree> void Octree< Real >::_UpSample( int highDepth , DenseNodeData< C , FEMDegree >& coefficients ) const { static const int LeftDownSampleRadius = -( ( BSplineEvaluationData< FEMDegree >::DownSample0Start < BSplineEvaluationData< FEMDegree >::DownSample1Start ) ? BSplineEvaluationData< FEMDegree >::DownSample0Start : BSplineEvaluationData< FEMDegree >::DownSample1Start ); static const int RightDownSampleRadius = ( ( BSplineEvaluationData< FEMDegree >::DownSample0End > BSplineEvaluationData< FEMDegree >::DownSample1End ) ? BSplineEvaluationData< FEMDegree >::DownSample0End : BSplineEvaluationData< FEMDegree >::DownSample1End ); typedef TreeOctNode::NeighborKey< LeftDownSampleRadius , RightDownSampleRadius > DownSampleKey; int lowDepth = highDepth-1; if( lowDepth<_minDepth ) return; typename BSplineEvaluationData< FEMDegree >::UpSampleEvaluator upSampleEvaluator; BSplineEvaluationData< FEMDegree >::SetUpSampleEvaluator( upSampleEvaluator , lowDepth-1 , _dirichlet ); std::vector< DownSampleKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i::DownSample0Size > BSplineEvaluationData< FEMDegree >::DownSample1Size ? BSplineEvaluationData< FEMDegree >::DownSample0Size : BSplineEvaluationData< FEMDegree >::DownSample1Size; Stencil< double , DownSampleSize > downSampleStencils[ Cube::CORNERS ]; int lowCenter = _Dimension< FEMDegree >( lowDepth )>>1; for( int c=0 ; c::DownSampleSize[cx] ; ii++ ) for( int jj=0 ; jj::DownSampleSize[cy] ; jj++ ) for( int kk=0 ; kk::DownSampleSize[cz] ; kk++ ) downSampleStencils[c].values[ii][jj][kk] = upSampleEvaluator.value( lowCenter + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx] , 2*lowCenter + cx ) * upSampleEvaluator.value( lowCenter + jj + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy] , 2*lowCenter + cy ) * upSampleEvaluator.value( lowCenter + kk + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz] , 2*lowCenter + cz ) ; } int dim = _Dimension< FEMDegree >( lowDepth ); // For Dirichlet constraints, can't get to all children from parents because boundary nodes are invalid #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(highDepth) ; i<_sNodes.end(highDepth) ; i++ ) if( _IsValidNode< FEMDegree >( _sNodes.treeNodes[i] ) ) { TreeOctNode *cNode = _sNodes.treeNodes[i] , *pNode = cNode->parent; int c = (int)( cNode-pNode->children ); DownSampleKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; int d , off[3]; pNode->depthAndOffset( d , off ); typename TreeOctNode::Neighbors< LeftDownSampleRadius + RightDownSampleRadius + 1 >& neighbors = neighborKey.template getNeighbors< false >( pNode ); // Want to make sure test if contained children are interior. // This is more conservative because we are test that overlapping children are interior bool isInterior = _IsInteriorlyOverlapped< FEMDegree , FEMDegree >( pNode ); C& fineCoefficient = coefficients[ cNode->nodeData.nodeIndex ]; int cx , cy , cz; Cube::FactorCornerIndex( c , cx , cy , cz ); if( isInterior ) { for( int ii=0 ; ii::DownSampleSize[cx] ; ii++ ) for( int jj=0 ; jj::DownSampleSize[cy] ; jj++ ) { int _ii = ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx] + LeftDownSampleRadius; int _jj = jj + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy] + LeftDownSampleRadius; for( int kk=0 ; kk::DownSampleSize[cz] ; kk++ ) { int _kk = kk + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz] + LeftDownSampleRadius; const TreeOctNode* _pNode = neighbors.neighbors[_ii][_jj][_kk]; if( _pNode ) fineCoefficient += (C)( coefficients[ _pNode->nodeData.nodeIndex ] * downSampleStencils[c].values[ii][jj][kk] ); } } } else { double downSampleValues[3][ BSplineEvaluationData< FEMDegree >::DownSample0Size > BSplineEvaluationData< FEMDegree >::DownSample1Size ? BSplineEvaluationData< FEMDegree >::DownSample0Size : BSplineEvaluationData< FEMDegree >::DownSample1Size ]; for( int ii=0 ; ii::DownSampleSize[cx] ; ii++ ) downSampleValues[0][ii] = upSampleEvaluator.value( off[0] + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx] , 2*off[0] + cx ); for( int ii=0 ; ii::DownSampleSize[cy] ; ii++ ) downSampleValues[1][ii] = upSampleEvaluator.value( off[1] + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy] , 2*off[1] + cy ); for( int ii=0 ; ii::DownSampleSize[cz] ; ii++ ) downSampleValues[2][ii] = upSampleEvaluator.value( off[2] + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz] , 2*off[2] + cz ); for( int ii=0 ; ii::DownSampleSize[cx] ; ii++ ) for( int jj=0 ; jj::DownSampleSize[cy] ; jj++ ) { double dxy = downSampleValues[0][ii] * downSampleValues[1][jj]; int _ii = ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx] + LeftDownSampleRadius; int _jj = jj + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy] + LeftDownSampleRadius; for( int kk=0 ; kk::DownSampleSize[cz] ; kk++ ) { int _kk = kk + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz] + LeftDownSampleRadius; const TreeOctNode* _pNode = neighbors.neighbors[_ii][_jj][_kk]; if( _IsValidNode< FEMDegree >( _pNode ) ) fineCoefficient += (C)( coefficients[ _pNode->nodeData.nodeIndex ] * dxy * downSampleValues[2][kk] ); } } } } } template< class Real > template< class C , int FEMDegree > void Octree< Real >::_UpSample( int highDepth , ConstPointer( C ) lowCoefficients , Pointer( C ) highCoefficients , bool dirichlet , int threads ) { static const int LeftDownSampleRadius = -( ( BSplineEvaluationData< FEMDegree >::DownSample0Start < BSplineEvaluationData< FEMDegree >::DownSample1Start ) ? BSplineEvaluationData< FEMDegree >::DownSample0Start : BSplineEvaluationData< FEMDegree >::DownSample1Start ); static const int RightDownSampleRadius = ( ( BSplineEvaluationData< FEMDegree >::DownSample0End > BSplineEvaluationData< FEMDegree >::DownSample1End ) ? BSplineEvaluationData< FEMDegree >::DownSample0End : BSplineEvaluationData< FEMDegree >::DownSample1End ); typedef TreeOctNode::NeighborKey< LeftDownSampleRadius , RightDownSampleRadius > DownSampleKey; int lowDepth = highDepth-1; if( lowDepth<1 ) return; typename BSplineEvaluationData< FEMDegree >::UpSampleEvaluator upSampleEvaluator; BSplineEvaluationData< FEMDegree >::SetUpSampleEvaluator( upSampleEvaluator , lowDepth-1 , dirichlet ); std::vector< DownSampleKey > neighborKeys( std::max< int >( 1 , threads ) ); static const int DownSampleSize = BSplineEvaluationData< FEMDegree >::DownSample0Size > BSplineEvaluationData< FEMDegree >::DownSample1Size ? BSplineEvaluationData< FEMDegree >::DownSample0Size : BSplineEvaluationData< FEMDegree >::DownSample1Size; Stencil< double , DownSampleSize > downSampleStencils[ Cube::CORNERS ]; int lowCenter = _Dimension< FEMDegree >( lowDepth )>>1; for( int c=0 ; c::DownSampleSize[cx] ; ii++ ) for( int jj=0 ; jj::DownSampleSize[cy] ; jj++ ) for( int kk=0 ; kk::DownSampleSize[cz] ; kk++ ) downSampleStencils[c].values[ii][jj][kk] = upSampleEvaluator.value( lowCenter + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx] , 2*lowCenter + cx ) * upSampleEvaluator.value( lowCenter + jj + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy] , 2*lowCenter + cy ) * upSampleEvaluator.value( lowCenter + kk + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz] , 2*lowCenter + cz ) ; } int lowDim = _Dimension< FEMDegree >( lowDepth ) , highDim = _Dimension< FEMDegree >( highDepth ); // Iterate over all parent nodes #pragma omp parallel for num_threads( threads ) for( int k=0 ; k( lowDepth , i , j , k ); // Iterate over all the children of the parent for( int c=0 ; c=highDim || jj<0 || jj>=highDim || kk<0 || kk>=highDim ) continue; C& highCoefficient = highCoefficients[ ii + jj*highDim + kk*highDim*highDim ]; if( isInterior ) { for( int ii=0 ; ii::DownSampleSize[cx] ; ii++ ) for( int jj=0 ; jj::DownSampleSize[cy] ; jj++ ) { int _i = i + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx]; int _j = j + jj + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy]; for( int kk=0 ; kk::DownSampleSize[cz] ; kk++ ) { int _k = k + kk + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz]; highCoefficient += (C)( lowCoefficients[ _i + _j*lowDim + _k*lowDim*lowDim ] * downSampleStencils[c].values[ii][jj][kk] ); } } } else { double downSampleValues[3][ BSplineEvaluationData< FEMDegree >::DownSample0Size > BSplineEvaluationData< FEMDegree >::DownSample1Size ? BSplineEvaluationData< FEMDegree >::DownSample0Size : BSplineEvaluationData< FEMDegree >::DownSample1Size ]; for( int ii=0 ; ii::DownSampleSize[cx] ; ii++ ) downSampleValues[0][ii] = upSampleEvaluator.value( off[0] + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx] , 2*off[0] + cx ); for( int ii=0 ; ii::DownSampleSize[cy] ; ii++ ) downSampleValues[1][ii] = upSampleEvaluator.value( off[1] + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy] , 2*off[1] + cy ); for( int ii=0 ; ii::DownSampleSize[cz] ; ii++ ) downSampleValues[2][ii] = upSampleEvaluator.value( off[2] + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz] , 2*off[2] + cz ); for( int ii=0 ; ii::DownSampleSize[cx] ; ii++ ) for( int jj=0 ; jj::DownSampleSize[cy] ; jj++ ) { double dxy = downSampleValues[0][ii] * downSampleValues[1][jj]; int _i = i + ii + BSplineEvaluationData< FEMDegree >::DownSampleStart[cx]; int _j = j + jj + BSplineEvaluationData< FEMDegree >::DownSampleStart[cy]; if( _i>=0 && _i=0 && _j::DownSampleSize[cz] ; kk++ ) { int _k = k + kk + BSplineEvaluationData< FEMDegree >::DownSampleStart[cz]; if( _k>=0 && _k template< int FEMDegree > Real Octree< Real >::_CoarserFunctionValue( Point3D< Real > p , const PointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* pointNode , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& upSampledCoefficients ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int LeftSupportRadius = - BSplineEvaluationData< FEMDegree >::SupportStart; static const int RightSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = - BSplineEvaluationData< FEMDegree >::SupportStart; double pointValue = 0; int depth = pointNode->depth(); if( depth<=_minDepth ) return Real(0.); // Iterate over all basis functions that overlap the point at the coarser resolution { const typename TreeOctNode::Neighbors< SupportSize >& neighbors = neighborKey.neighbors[depth-1]; int _d , _off[3]; pointNode->parent->depthAndOffset( _d , _off ); int fStart , fEnd; BSplineData< FEMDegree >::FunctionSpan( _d-1 , fStart , fEnd ); double pointValues[ DIMENSION ][SupportSize]; memset( pointValues , 0 , sizeof(double) * DIMENSION * SupportSize ); for( int dd=0 ; dd::FunctionIndex( _d-1 , _off[dd]+i ); if( fIdx>=fStart && fIdx( _node ) ) _pointValue += pointValues[2][l] * double( upSampledCoefficients[_node->nodeData.nodeIndex] ); } pointValue += _pointValue * xyValue; } } return Real( pointValue ); } template< class Real > template< int FEMDegree > Real Octree< Real >::_FinerFunctionValue( Point3D< Real > p , const PointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* pointNode , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& finerCoefficients ) const { typename TreeOctNode::Neighbors< BSplineEvaluationData< FEMDegree >::SupportSize > childNeighbors; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; static const int LeftSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; static const int RightSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; double pointValue = 0; int depth = pointNode->depth(); neighborKey.template getChildNeighbors< false >( p , depth , childNeighbors ); for( int j=-LeftPointSupportRadius ; j<=RightPointSupportRadius ; j++ ) for( int k=-LeftPointSupportRadius ; k<=RightPointSupportRadius ; k++ ) for( int l=-LeftPointSupportRadius ; l<=RightPointSupportRadius ; l++ ) { const TreeOctNode* _node = childNeighbors.neighbors[j+LeftPointSupportRadius][k+LeftPointSupportRadius][l+LeftPointSupportRadius]; if( _IsValidNode< FEMDegree >( _node ) ) { int fIdx[3]; FunctionIndex< FEMDegree >( _node , fIdx ); pointValue += bsData.baseBSplines[ fIdx[0] ][LeftSupportRadius-j]( p[0] ) * bsData.baseBSplines[ fIdx[1] ][LeftSupportRadius-k]( p[1] ) * bsData.baseBSplines[ fIdx[2] ][LeftSupportRadius-l]( p[2] ) * double( finerCoefficients[ _node->nodeData.nodeIndex ] ); } } return Real( pointValue ); } template< class Real > template< int FEMDegree > void Octree< Real >::_SetPointValuesFromCoarser( SparseNodeData< PointData< Real > , 0 >& pointInfo , int highDepth , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& upSampledCoefficients ) { int lowDepth = highDepth-1; if( lowDepth<_minDepth ) return; std::vector< PointData< Real > >& points = pointInfo.data; std::vector< PointSupportKey< FEMDegree > > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { PointSupportKey< FEMDegree >& neighborKey = neighborKeys[ omp_get_thread_num() ]; int pIdx = pointInfo.index( _sNodes.treeNodes[i] ); if( pIdx!=-1 ) { neighborKey.template getNeighbors< false >( _sNodes.treeNodes[i]->parent ); points[ pIdx ].weightedCoarserDValue = (Real)( _CoarserFunctionValue( points[pIdx].position , neighborKey , _sNodes.treeNodes[i] , bsData , upSampledCoefficients ) - 0.5 ) * points[pIdx].weight; } } } template< class Real > template< int FEMDegree > void Octree< Real >::_SetPointConstraintsFromFiner( const SparseNodeData< PointData< Real > , 0 >& pointInfo , int highDepth , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& finerCoefficients , DenseNodeData< Real , FEMDegree >& coarserConstraints ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; static const int LeftSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; static const int RightSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; const std::vector< PointData< Real > >& points = pointInfo.data; // Note: We can't iterate over the finer point nodes as the point weights might be // scaled incorrectly, due to the adaptive exponent. So instead, we will iterate // over the coarser nodes and evaluate the finer solution at the associated points. int lowDepth = highDepth-1; if( lowDepth<_minDepth ) return; size_t start = _sNodes.begin(lowDepth) , end = _sNodes.end(lowDepth) , range = end-start; memset( coarserConstraints.data+start , 0 , sizeof( Real ) * range ); std::vector< PointSupportKey< FEMDegree > > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { PointSupportKey< FEMDegree >& neighborKey = neighborKeys[ omp_get_thread_num() ]; int pIdx = pointInfo.index( _sNodes.treeNodes[i] ); if( pIdx!=-1 ) { typename TreeOctNode::Neighbors< SupportSize >& neighbors = neighborKey.template getNeighbors< false >( _sNodes.treeNodes[i] ); // Evaluate the solution @( depth ) at the current point @( depth-1 ) { Real finerPointDValue = (Real)( _FinerFunctionValue( points[pIdx].position , neighborKey , _sNodes.treeNodes[i] , bsData , finerCoefficients ) - 0.5 ) * points[pIdx].weight; Point3D< Real > p = points[ pIdx ].position; // Update constraints for all nodes @( depth-1 ) that overlap the point int d , idx[3]; neighbors.neighbors[LeftPointSupportRadius][LeftPointSupportRadius][LeftPointSupportRadius]->depthAndOffset( d, idx ); // Set the (offset) index to the top-left-front corner of the 3x3x3 block of b-splines // overlapping the point. idx[0] = BinaryNode::CenterIndex( d , idx[0] ); idx[1] = BinaryNode::CenterIndex( d , idx[1] ); idx[2] = BinaryNode::CenterIndex( d , idx[2] ); for( int x=-LeftPointSupportRadius ; x<=RightPointSupportRadius ; x++ ) for( int y=-LeftPointSupportRadius ; y<=RightPointSupportRadius ; y++ ) for( int z=-LeftPointSupportRadius ; z<=RightPointSupportRadius ; z++ ) if( _IsValidNode< FEMDegree >( neighbors.neighbors[x+LeftPointSupportRadius][y+LeftPointSupportRadius][z+LeftPointSupportRadius] ) ) { #pragma omp atomic coarserConstraints[ neighbors.neighbors[x+LeftPointSupportRadius][y+LeftPointSupportRadius][z+LeftPointSupportRadius]->nodeData.nodeIndex - _sNodes.begin(lowDepth) ] += Real( bsData.baseBSplines[idx[0]+x][LeftSupportRadius-x]( p[0] ) * bsData.baseBSplines[idx[1]+y][LeftSupportRadius-y]( p[1] ) * bsData.baseBSplines[idx[2]+z][LeftSupportRadius-z]( p[2] ) * finerPointDValue ); } } } } } template< class Real > template< int FEMDegree > int Octree< Real >::_SetMatrixRow( const SparseNodeData< PointData< Real > , 0 >& pointInfo , const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& neighbors , Pointer( MatrixEntry< Real > ) row , int offset , const typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator& integrator , const Stencil< double , BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& stencil , const BSplineData< FEMDegree >& bsData ) const { static const int SupportSize = BSplineEvaluationData< FEMDegree >::SupportSize; static const int OverlapRadius = - BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; static const int OverlapSize = BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize; static const int LeftSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; static const int RightSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int LeftPointSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int RightPointSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; const std::vector< PointData< Real > >& points = pointInfo.data; bool hasYZPoints[SupportSize] , hasZPoints[SupportSize][SupportSize]; Real diagonal = 0; // Given a node: // -- for each node in its support: // ---- if the supporting node contains a point: // ------ evaluate the x, y, and z B-splines of the nodes supporting the point // splineValues \in [-LeftSupportRadius,RightSupportRadius] x [-LeftSupportRadius,RightSupportRadius] x [-LeftSupportRadius,RightSupportRadius] x [0,Dimension) x [-LeftPointSupportRadius,RightPointSupportRadius] Real splineValues[SupportSize][SupportSize][SupportSize][DIMENSION][SupportSize]; memset( splineValues , 0 , sizeof( Real ) * SupportSize * SupportSize * SupportSize * DIMENSION *SupportSize ); int count = 0; const TreeOctNode* node = neighbors.neighbors[OverlapRadius][OverlapRadius][OverlapRadius]; int d , off[3]; node->depthAndOffset( d , off ); int fStart , fEnd; BSplineData< FEMDegree >::FunctionSpan( d-1 , fStart , fEnd ); bool isInterior = _IsInteriorlyOverlapped< FEMDegree , FEMDegree >( node ); if( _constrainValues ) { // Iterate over all neighboring nodes that may have a constraining point // -- For each one, compute the values of the spline functions supported on the point for( int j=0 ; j( _node ) && pointInfo.index( _node )!=-1 ) { int pOff[] = { off[0]+j , off[1]+k , off[2]+l }; hasYZPoints[jj] = hasZPoints[jj][kk] = true; const PointData< Real >& pData = points[ pointInfo.index( _node ) ]; Real (*_splineValues)[SupportSize] = splineValues[jj][kk][ll]; Real weight = pData.weight; Point3D< Real > p = pData.position; // Evaluate the point p at all the nodes whose functions have it in their support for( int s=-LeftPointSupportRadius ; s<=RightPointSupportRadius ; s++ ) for( int dd=0 ; dd::FunctionIndex( d-1 , pOff[dd]+s ); if( fIdx>=fStart && fIdx( _node ) && pointInfo.index( _node )!=-1 ) // Iterate over all neighbors whose support contains the point and accumulate the mutual integral for( int ii=-LeftPointSupportRadius ; ii<=RightPointSupportRadius ; ii++ ) for( int jj=-LeftPointSupportRadius ; jj<=RightPointSupportRadius ; jj++ ) for( int kk=-LeftPointSupportRadius ; kk<=RightPointSupportRadius ; kk++ ) { TreeOctNode* _node = neighbors.neighbors[i+ii+OverlapRadius][j+jj+OverlapRadius][k+kk+OverlapRadius]; if( _IsValidNode< FEMDegree >( _node ) ) pointValues[i+ii+OverlapRadius][j+jj+OverlapRadius][k+kk+OverlapRadius] += _splineValues[0][ii+LeftPointSupportRadius ] * _splineValues[1][jj+LeftPointSupportRadius ] * _splineValues[2][kk+LeftPointSupportRadius ]; } } } pointValues[OverlapRadius][OverlapRadius][OverlapRadius] = diagonal; int nodeIndex = neighbors.neighbors[OverlapRadius][OverlapRadius][OverlapRadius]->nodeData.nodeIndex; if( isInterior ) // General case, so try to make fast { const TreeOctNode* const * _nodes = &neighbors.neighbors[0][0][0]; const double* _stencil = &stencil.values[0][0][0]; Real* _values = &pointValues[0][0][0]; const static int CenterIndex = OverlapSize*OverlapSize*OverlapRadius + OverlapSize*OverlapRadius + OverlapRadius; if( _constrainValues ) for( int i=0 ; i( nodeIndex-offset , _values[CenterIndex] ); for( int i=0 ; i( _nodes[i]->nodeData.nodeIndex-offset , _values[i] ); } else { int d , off[3]; node->depthAndOffset( d , off ); Real temp = Real( SystemCoefficients< FEMDegree , FEMDegree >::GetLaplacian( integrator , off , off ) ); if( _constrainValues ) temp += pointValues[OverlapRadius][OverlapRadius][OverlapRadius]; row[count++] = MatrixEntry< Real >( nodeIndex-offset , temp ); for( int x=0 ; x( neighbors.neighbors[x][y][z] ) ) { const TreeOctNode* _node = neighbors.neighbors[x][y][z]; int _d , _off[3]; _node->depthAndOffset( _d , _off ); Real temp = Real( SystemCoefficients< FEMDegree , FEMDegree >::GetLaplacian( integrator , _off , off ) ); if( _constrainValues ) temp += pointValues[x][y][z]; row[count++] = MatrixEntry< Real >( _node->nodeData.nodeIndex-offset , temp ); } } return count; } template< class Real > template< int FEMDegree > int Octree< Real >::_GetMatrixAndUpdateConstraints( const SparseNodeData< PointData< Real > , 0 >& pointInfo , SparseMatrix< Real >& matrix , DenseNodeData< Real , FEMDegree >& constraints , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator& integrator , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const BSplineData< FEMDegree >& bsData , int depth , const DenseNodeData< Real , FEMDegree >* metSolution , bool coarseToFine ) { static const int OverlapRadius = - BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; static const int OverlapSize = BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize; size_t start = _sNodes.begin(depth) , end = _sNodes.end(depth) , range = end-start; Stencil< double , OverlapSize > stencil , stencils[2][2][2]; SystemCoefficients< FEMDegree , FEMDegree >::SetCentralLaplacianStencil ( integrator , stencil ); SystemCoefficients< FEMDegree , FEMDegree >::SetCentralLaplacianStencils( childIntegrator , stencils ); matrix.Resize( (int)range ); std::vector< AdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i+start] ) ) { AdjacenctNodeKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* node = _sNodes.treeNodes[i+start]; // Get the matrix row size typename TreeOctNode::Neighbors< OverlapSize > neighbors; neighborKey.template getNeighbors< false , OverlapRadius , OverlapRadius >( node , neighbors ); int count = _GetMatrixRowSize< FEMDegree >( neighbors ); // Allocate memory for the row #pragma omp critical (matrix_set_row_size) matrix.SetRowSize( i , count ); // Set the row entries matrix.rowSizes[i] = _SetMatrixRow( pointInfo , neighbors , matrix[i] , (int)start , integrator , stencil , bsData ); if( depth>_minDepth ) { // Offset the constraints using the solution from lower resolutions. int x , y , z , c; if( node->parent ) { c = int( node - node->parent->children ); Cube::FactorCornerIndex( c , x , y , z ); } else x = y = z = 0; if( coarseToFine ) { typename TreeOctNode::Neighbors< OverlapSize > pNeighbors; neighborKey.template getNeighbors< false , OverlapRadius , OverlapRadius >( node->parent , pNeighbors ); _UpdateConstraintsFromCoarser( pointInfo , neighbors , pNeighbors , node , constraints , *metSolution , childIntegrator , stencils[x][y][z] , bsData ); } } } return 1; } template< class Real > template< int FEMDegree > int Octree< Real >::_GetSliceMatrixAndUpdateConstraints( const SparseNodeData< PointData< Real > , 0 >& pointInfo , SparseMatrix< Real >& matrix , DenseNodeData< Real , FEMDegree >& constraints , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator& integrator , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const BSplineData< FEMDegree >& bsData , int depth , int slice , const DenseNodeData< Real , FEMDegree >& metSolution , bool coarseToFine ) { static const int OverlapSize = BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize; static const int OverlapRadius = -BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; int nStart = _sNodes.begin( depth , slice ) , nEnd = _sNodes.end( depth , slice ); size_t range = nEnd-nStart; Stencil< double , OverlapSize > stencil , stencils[2][2][2]; SystemCoefficients< FEMDegree , FEMDegree >::SetCentralLaplacianStencil ( integrator , stencil ); SystemCoefficients< FEMDegree , FEMDegree >::SetCentralLaplacianStencils( childIntegrator , stencils ); matrix.Resize( (int)range ); std::vector< AdjacenctNodeKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i+nStart] ) ) { AdjacenctNodeKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* node = _sNodes.treeNodes[i+nStart]; // Get the matrix row size typename TreeOctNode::Neighbors< OverlapSize > neighbors; neighborKey.template getNeighbors< false , OverlapRadius , OverlapRadius >( node , neighbors ); int count = _GetMatrixRowSize< FEMDegree >( neighbors ); // Allocate memory for the row #pragma omp critical (matrix_set_row_size) { matrix.SetRowSize( i , count ); } // Set the row entries matrix.rowSizes[i] = _SetMatrixRow( pointInfo , neighbors , matrix[i] , _sNodes.begin(depth,slice) , integrator , stencil , bsData ); if( depth>_minDepth ) { // Offset the constraints using the solution from lower resolutions. int x , y , z , c; if( node->parent ) { c = int( node - node->parent->children ); Cube::FactorCornerIndex( c , x , y , z ); } else x = y = z = 0; if( coarseToFine ) { typename TreeOctNode::Neighbors< OverlapSize > pNeighbors; neighborKey.template getNeighbors< false, OverlapRadius , OverlapRadius >( node->parent , pNeighbors ); _UpdateConstraintsFromCoarser( pointInfo , neighbors , pNeighbors , node , constraints , metSolution , childIntegrator , stencils[x][y][z] , bsData ); } } } return 1; } template< class Real > template< int FEMDegree > int Octree< Real >::_SolveSystemGS( const BSplineData< FEMDegree >& bsData , SparseNodeData< PointData< Real > , 0 >& pointInfo , int depth , DenseNodeData< Real , FEMDegree >& solution , DenseNodeData< Real , FEMDegree >& constraints , DenseNodeData< Real , FEMDegree >& metSolutionConstraints , int iters , bool coarseToFine , bool showResidual , double* bNorm2 , double* inRNorm2 , double* outRNorm2 , bool forceSilent ) { const int OverlapRadius = -BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator integrator; typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator childIntegrator; BSplineIntegrationData< FEMDegree , FEMDegree >::SetIntegrator( integrator , depth-1 , _dirichlet , _dirichlet ); if( depth>_minDepth ) BSplineIntegrationData< FEMDegree , FEMDegree >::SetChildIntegrator( childIntegrator , depth-2 , _dirichlet , _dirichlet ); DenseNodeData< Real , FEMDegree > metSolution , metConstraints; if( coarseToFine ) metSolution = metSolutionConstraints; // This stores the up-sampled solution up to depth-2 else metConstraints = metSolutionConstraints; // This stores the down-sampled constraints up to depth double _maxMemoryUsage = maxMemoryUsage; maxMemoryUsage = 0; int slices = _Dimension< FEMDegree >(depth); double systemTime=0. , solveTime=0. , updateTime=0. , evaluateTime = 0.; if( coarseToFine ) { if( depth>_minDepth ) { // Up-sample the cumulative change in solution @(depth-2) into the cumulative change in solution @(depth-1) if( depth-2>=_minDepth ) _UpSample( depth-1 , metSolution ); // Add in the change in solution @(depth-1) #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(depth-1) ; i<_sNodes.end(depth-1) ; i++ ) metSolution[i] += solution[i]; // Evaluate the points @(depth) using the cumulative change in solution @(depth-1) if( _constrainValues ) { evaluateTime = Time(); _SetPointValuesFromCoarser( pointInfo , depth , bsData , metSolution ); evaluateTime = Time() - evaluateTime; } } } else if( depth<_sNodes.levels()-1 ) for( int i=_sNodes.begin(depth) ; i<_sNodes.end(depth) ; i++ ) constraints[i] -= metConstraints[i]; double bNorm=0 , inRNorm=0 , outRNorm=0; if( depth>=_minDepth ) { // Add padding space if we are computing residuals int frontOffset = ( showResidual || inRNorm2 ) ? OverlapRadius : 0; int backOffset = ( showResidual || outRNorm2 ) ? OverlapRadius : 0; // Set the number of in-memory slices required for a temporally blocked solver int solveSlices = std::min< int >( OverlapRadius*iters - (OverlapRadius-1) , slices ) , matrixSlices = std::max< int >( 1 , std::min< int >( solveSlices+frontOffset+backOffset , slices ) ); // The list of matrices for each in-memory slices std::vector< SparseMatrix< Real > > _M( matrixSlices ); // The list of multi-colored indices for each in-memory slice std::vector< std::vector< std::vector< int > > > __mcIndices( std::max< int >( 0 , solveSlices ) ); int dir = coarseToFine ? -1 : 1 , start = coarseToFine ? slices-1 : 0 , end = coarseToFine ? -1 : slices; for( int frontSlice=start-frontOffset*dir , backSlice = frontSlice-OverlapRadius*(iters-1)*dir ; backSlice!=end+backOffset*dir ; frontSlice+=dir , backSlice+=dir ) { double t; if( frontSlice+frontOffset*dir>=0 && frontSlice+frontOffset*dir ) start = _M[_s][j]; ConstPointer( MatrixEntry< Real > ) end = start + _M[_s].rowSizes[j]; ConstPointer( MatrixEntry< Real > ) e; for( e=start ; e!=end ; e++ ) temp += X[ e->N ] * e->Value; bNorm += B[j]*B[j]; inRNorm += (temp-B[j]) * (temp-B[j]); } else if( bNorm2 ) #pragma omp parallel for num_threads( threads ) reduction( + : bNorm ) for( int j=0 ; j<_M[_s].rows ; j++ ) bNorm += B[j]*B[j]; } t = Time(); // Compute the multicolor indices if( iters && frontSlice>=0 && frontSlice( _sNodes.begin(depth,s) , _sNodes.end(depth,s) , __mcIndices[__s] ); } // Advance through the in-memory slices, taking an appropriately sized stride for( int slice=frontSlice ; slice*dir>=backSlice*dir ; slice-=OverlapRadius*dir ) if( slice>=0 && slice::SolveGS( __mcIndices[__s] , _M[_s] , B , X , !coarseToFine , threads ); } solveTime += Time() - t; // Compute residuals if( (showResidual || outRNorm2) && backSlice-backOffset*dir>=0 && backSlice-backOffset*dir ) start = _M[_s][j]; ConstPointer( MatrixEntry< Real > ) end = start + _M[_s].rowSizes[j]; ConstPointer( MatrixEntry< Real > ) e; for( e=start ; e!=end ; e++ ) temp += X[ e->N ] * e->Value; outRNorm += (temp-B[j]) * (temp-B[j]); } } } } if( bNorm2 ) bNorm2[depth] = bNorm; if( inRNorm2 ) inRNorm2[depth] = inRNorm; if( outRNorm2 ) outRNorm2[depth] = outRNorm; if( showResidual && iters ) { for( int i=0 ; i %.4e -> %.4e (%.2e) [%d]\n" , sqrt( bNorm ) , sqrt( inRNorm ) , sqrt( outRNorm ) , sqrt( outRNorm/bNorm ) , iters ); } if( !coarseToFine && depth>_minDepth ) { // Explicitly compute the restriction of the met solution onto the coarser nodes // and down-sample the previous accumulation { _UpdateConstraintsFromFiner( childIntegrator , bsData , depth , solution , metConstraints ); if( _constrainValues ) _SetPointConstraintsFromFiner( pointInfo , depth , bsData , solution , metConstraints ); if( depth<_sNodes.levels()-1 ) _DownSample( depth , metConstraints ); } } MemoryUsage(); if( !forceSilent ) DumpOutput( "\tEvaluated / Got / Solved in: %6.3f / %6.3f / %6.3f\t(%.3f MB)\n" , evaluateTime , systemTime , solveTime , float( maxMemoryUsage ) ); maxMemoryUsage = std::max< double >( maxMemoryUsage , _maxMemoryUsage ); return iters; } template< class Real > template< int FEMDegree > int Octree< Real >::_SolveSystemCG( const BSplineData< FEMDegree >& bsData , SparseNodeData< PointData< Real > , 0 >& pointInfo , int depth , DenseNodeData< Real , FEMDegree >& solution , DenseNodeData< Real , FEMDegree >& constraints , DenseNodeData< Real , FEMDegree >& metSolutionConstraints , int iters , bool coarseToFine , bool showResidual , double* bNorm2 , double* inRNorm2 , double* outRNorm2 , double accuracy ) { typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator integrator; typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator childIntegrator; BSplineIntegrationData< FEMDegree , FEMDegree >::SetIntegrator( integrator , depth-1 , _dirichlet , _dirichlet ); if( depth>_minDepth ) BSplineIntegrationData< FEMDegree , FEMDegree >::SetChildIntegrator( childIntegrator , depth-2 , _dirichlet , _dirichlet ); DenseNodeData< Real , FEMDegree > metSolution , metConstraints; if( coarseToFine ) metSolution = metSolutionConstraints; // This stores the up-sampled solution up to depth-2 else metConstraints = metSolutionConstraints; // This stores the down-sampled constraints up to depth double _maxMemoryUsage = maxMemoryUsage; maxMemoryUsage = 0; int iter = 0; Pointer( Real ) X = solution.data + _sNodes.begin( depth ); Pointer( Real ) B = constraints.data + _sNodes.begin( depth ); SparseMatrix< Real > M; double systemTime=0. , solveTime=0. , updateTime=0. , evaluateTime = 0.; if( coarseToFine ) { if( depth>_minDepth ) { // Up-sample the cumulative change in solution @(depth-2) into the cumulative change in solution @(depth-1) if( depth-2>=_minDepth ) _UpSample( depth-1 , metSolution ); // Add in the change in solution @(depth-1) #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(depth-1) ; i<_sNodes.end(depth-1) ; i++ ) metSolution[i] += solution[i]; // Evaluate the points @(depth) using the cumulative change in solution @(depth-1) if( _constrainValues ) { evaluateTime = Time(); _SetPointValuesFromCoarser( pointInfo , depth , bsData , metSolution ); evaluateTime = Time() - evaluateTime; } } } else if( depth<_sNodes.levels()-1 ) for( int i=_sNodes.begin(depth) ; i<_sNodes.end(depth) ; i++ ) constraints[i] -= metConstraints[i]; // Get the system matrix (and adjust the right-hand-side based on the coarser solution if prolonging) systemTime = Time(); _GetMatrixAndUpdateConstraints( pointInfo , M , constraints , integrator , childIntegrator , bsData , depth , coarseToFine ? &metSolution : NULL , coarseToFine ); systemTime = Time()-systemTime; solveTime = Time(); // Solve the linear system accuracy = Real( accuracy / 100000 ) * M.rows; int dim = _Dimension< FEMDegree >( depth ); int nonZeroRows = 0; for( int i=0 ; i( _sNodes.size(depth) ); if( addDCTerm ) M.MultiplyAndAddAverage( ( ConstPointer( Real ) )X , temp , threads ); else M.Multiply( ( ConstPointer( Real ) )X , temp , threads ); #pragma omp parallel for num_threads( threads ) for( int i=0 ; i<_sNodes.size(depth) ; i++ ) temp[i] -= B[i]; #pragma omp parallel for num_threads( threads ) reduction( + : inRNorm ) for( int i=0 ; i<_sNodes.size(depth) ; i++ ) inRNorm += temp[i] * temp[i]; FreePointer( temp ); } iters = std::min< int >( nonZeroRows , iters ); if( iters ) iter += SparseMatrix< Real >::SolveCG( M , ( ConstPointer( Real ) )B , iters , X , Real( accuracy ) , 0 , addDCTerm , false , threads ); solveTime = Time()-solveTime; if( showResidual || outRNorm2 ) { outRNorm = 0; Pointer( Real ) temp = AllocPointer< Real >( _sNodes.size(depth) ); if( addDCTerm ) M.MultiplyAndAddAverage( ( ConstPointer( Real ) )X , temp , threads ); else M.Multiply( ( ConstPointer( Real ) )X , temp , threads ); #pragma omp parallel for num_threads( threads ) for( int i=0 ; i<_sNodes.size(depth) ; i++ ) temp[i] -= B[i]; #pragma omp parallel for num_threads( threads ) reduction( + : outRNorm ) for( int i=0 ; i<_sNodes.size(depth) ; i++ ) outRNorm += temp[i] * temp[i]; FreePointer( temp ); } if( bNorm2 ) bNorm2[depth] = bNorm * bNorm; if( inRNorm2 ) inRNorm2[depth] = inRNorm * inRNorm; if( outRNorm2 ) outRNorm2[depth] = outRNorm * outRNorm; if( showResidual && iters ) { for( int i=0 ; i %.4e -> %.4e (%.2e) [%d]\n" , bNorm , inRNorm , outRNorm , outRNorm/bNorm , iter ); } if( !coarseToFine && depth>_minDepth ) { // Explicitly compute the restriction of the met solution onto the coarser nodes // and down-sample the previous accumulation { _UpdateConstraintsFromFiner( childIntegrator , bsData , depth , solution , metConstraints ); if( _constrainValues ) _SetPointConstraintsFromFiner( pointInfo , depth , bsData , solution , metConstraints ); if( depth<_sNodes.levels()-1 ) _DownSample( depth , metConstraints ); } } MemoryUsage(); DumpOutput( "\tEvaluated / Got / Solved in: %6.3f / %6.3f / %6.3f\t(%.3f MB)\n" , evaluateTime , systemTime , solveTime , float( maxMemoryUsage ) ); maxMemoryUsage = std::max< double >( maxMemoryUsage , _maxMemoryUsage ); return iter; } template< class Real > template< int FEMDegree > int Octree< Real >::_GetMatrixRowSize( const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& neighbors ) const { static const int OverlapSize = BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize; static const int OverlapRadius = - BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; int count = 0; int nodeIndex = neighbors.neighbors[OverlapRadius][OverlapRadius][OverlapRadius]->nodeData.nodeIndex; const TreeOctNode* const * _nodes = &neighbors.neighbors[0][0][0]; for( int i=0 ; i( _nodes[i] ) ) count++; return count; } template< class Real > template< int FEMDegree1 , int FEMDegree2 > void Octree< Real >::_SetParentOverlapBounds( const TreeOctNode* node , int& startX , int& endX , int& startY , int& endY , int& startZ , int& endZ ) { const int OverlapStart = BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::OverlapStart; if( node->parent ) { int x , y , z , c = int( node - node->parent->children ); Cube::FactorCornerIndex( c , x , y , z ); startX = BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::ParentOverlapStart[x]-OverlapStart , endX = BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::ParentOverlapEnd[x]-OverlapStart+1; startY = BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::ParentOverlapStart[y]-OverlapStart , endY = BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::ParentOverlapEnd[y]-OverlapStart+1; startZ = BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::ParentOverlapStart[z]-OverlapStart , endZ = BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::ParentOverlapEnd[z]-OverlapStart+1; } } // It is assumed that at this point, the evaluationg of the current depth's points, using the coarser resolution solution // has already happened template< class Real > template< int FEMDegree > void Octree< Real >::_UpdateConstraintsFromCoarser( const SparseNodeData< PointData< Real > , 0 >& pointInfo , const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& neighbors , const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& pNeighbors , TreeOctNode* node , DenseNodeData< Real , FEMDegree >& constraints , const DenseNodeData< Real , FEMDegree >& metSolution , const typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const Stencil< double , BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& lapStencil , const BSplineData< FEMDegree >& bsData ) const { static const int OverlapSize = BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; static const int LeftSupportRadius = -BSplineEvaluationData< FEMDegree >::SupportStart; static const int RightSupportRadius = BSplineEvaluationData< FEMDegree >::SupportEnd; static const int OverlapRadius = - BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; const std::vector< PointData< Real > >& points = pointInfo.data; if( node->depth()<=_minDepth ) return; // This is a conservative estimate as we only need to make sure that the parent nodes don't overlap the child (not the parent itself) bool isInterior = _IsInteriorlyOverlapped< FEMDegree , FEMDegree >( node->parent ); int d , off[3]; node->depthAndOffset( d , off ); Real constraint = Real( 0 ); // Offset the constraints using the solution from lower resolutions. int startX , endX , startY , endY , startZ , endZ; _SetParentOverlapBounds< FEMDegree , FEMDegree >( node , startX , endX , startY , endY , startZ , endZ ); for( int x=startX ; x( pNeighbors.neighbors[x][y][z] ) ) { const TreeOctNode* _node = pNeighbors.neighbors[x][y][z]; Real _solution = metSolution[ _node->nodeData.nodeIndex ]; { if( isInterior ) constraints[ node->nodeData.nodeIndex ] -= Real( lapStencil.values[x][y][z] * _solution ); else { int _d , _off[3]; _node->depthAndOffset( _d , _off ); constraints[ node->nodeData.nodeIndex ] -= Real( SystemCoefficients< FEMDegree , FEMDegree >::GetLaplacian( childIntegrator , _off , off ) * _solution ); } } } if( _constrainValues ) { double constraint = 0; int fIdx[3]; FunctionIndex< FEMDegree >( node , fIdx ); // Evaluate the current node's basis function at adjacent points for( int x=-LeftSupportRadius ; x<=RightSupportRadius ; x++ ) for( int y=-LeftSupportRadius ; y<=RightSupportRadius ; y++ ) for( int z=-LeftSupportRadius ; z<=RightSupportRadius ; z++ ) { const TreeOctNode* _node = neighbors.neighbors[x+OverlapRadius][y+OverlapRadius][z+OverlapRadius]; if( _IsValidNode< 0 >( _node ) && pointInfo.index( _node )!=-1 ) { const PointData< Real >& pData = points[ pointInfo.index( _node ) ]; Point3D< Real > p = pData.position; constraint += bsData.baseBSplines[ fIdx[0] ][x+LeftSupportRadius]( p[0] ) * bsData.baseBSplines[ fIdx[1] ][y+LeftSupportRadius]( p[1] ) * bsData.baseBSplines[ fIdx[2] ][z+LeftSupportRadius]( p[2] ) * pData.weightedCoarserDValue; } } constraints[ node->nodeData.nodeIndex ] -= Real( constraint ); } } // Given the solution @( depth ) add to the met constraints @( depth-1 ) template< class Real > template< int FEMDegree > void Octree< Real >::_UpdateConstraintsFromFiner( const typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const BSplineData< FEMDegree >& bsData , int depth , const DenseNodeData< Real , FEMDegree >& fineSolution , DenseNodeData< Real , FEMDegree >& coarseConstraints ) const { static const int OverlapSize = BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize; static const int OverlapRadius = - BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; typedef typename TreeOctNode::NeighborKey< -BSplineEvaluationData< FEMDegree >::SupportStart , BSplineEvaluationData< FEMDegree >::SupportEnd >SupportKey; if( depth<=_minDepth ) return; // Get the stencil describing the Laplacian relating coefficients @(depth) with coefficients @(depth-1) Stencil< double , OverlapSize > stencils[2][2][2]; SystemCoefficients< FEMDegree , FEMDegree >::SetCentralLaplacianStencils( childIntegrator , stencils ); size_t start = _sNodes.begin(depth) , end = _sNodes.end(depth) , range = end-start; int lStart = _sNodes.begin(depth-1); memset( coarseConstraints.data + _sNodes.begin(depth-1) , 0 , sizeof(Real)*_sNodes.size(depth-1) ); // Iterate over the nodes @( depth ) std::vector< SupportKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { SupportKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* node = _sNodes.treeNodes[i]; // Offset the coarser constraints using the solution from the current resolutions. int x , y , z , c; c = int( node - node->parent->children ); Cube::FactorCornerIndex( c , x , y , z ); { typename TreeOctNode::Neighbors< OverlapSize > pNeighbors; neighborKey.template getNeighbors< false , OverlapRadius , OverlapRadius >( node->parent , pNeighbors ); const Stencil< double , OverlapSize >& lapStencil = stencils[x][y][z]; bool isInterior = _IsInteriorlyOverlapped< FEMDegree , FEMDegree >( node->parent ); int d , off[3]; node->depthAndOffset( d , off ); // Offset the constraints using the solution from finer resolutions. int startX , endX , startY , endY , startZ , endZ; _SetParentOverlapBounds< FEMDegree , FEMDegree >( node , startX , endX , startY , endY , startZ , endZ ); Real solution = fineSolution[ node->nodeData.nodeIndex ]; for( int x=startX ; x( pNeighbors.neighbors[x][y][z] ) ) { const TreeOctNode* _node = pNeighbors.neighbors[x][y][z]; if( isInterior ) #pragma omp atomic coarseConstraints[ _node->nodeData.nodeIndex ] += Real( lapStencil.values[x][y][z] * solution ); else { int _d , _off[3]; _node->depthAndOffset( _d , _off ); #pragma omp atomic coarseConstraints[ _node->nodeData.nodeIndex ] += Real( SystemCoefficients< FEMDegree , FEMDegree >::GetLaplacian( childIntegrator , _off , off ) * solution ); } } } } } template< class Real > template< int FEMDegree > DenseNodeData< Real , FEMDegree > Octree< Real >::SolveSystem( SparseNodeData< PointData< Real > , 0 >& pointInfo , DenseNodeData< Real , FEMDegree >& constraints , bool showResidual , int iters , int maxSolveDepth , int cgDepth , double accuracy ) { BSplineData< FEMDegree > bsData; bsData.set( maxSolveDepth , _dirichlet ); maxSolveDepth++; int iter=0; iters = std::max< int >( 0 , iters ); DenseNodeData< Real , FEMDegree > solution( _sNodes.size() ); memset( solution.data , 0 , sizeof(Real)*_sNodes.size() ); solution[0] = 0; DenseNodeData< Real , FEMDegree > metSolution( _sNodes.end( _sNodes.levels()-2 ) ); memset( metSolution.data , 0 , sizeof(Real)*_sNodes.end( _sNodes.levels()-2 ) ); for( int d=_minDepth ; d<_sNodes.levels() ; d++ ) { DumpOutput( "Depth[%d/%d]: %d\n" , d-1 , _sNodes.levels()-2 , _sNodes.size( d ) ); if( d==_minDepth ) _SolveSystemCG( bsData , pointInfo , d , solution , constraints , metSolution , _sNodes.size(_minDepth) , true , showResidual , NULL , NULL , NULL ); else { if( d>cgDepth ) iter += _SolveSystemGS( bsData , pointInfo , d , solution , constraints , metSolution , d>maxSolveDepth ? 0 : iters , true , showResidual , NULL , NULL , NULL ); else iter += _SolveSystemCG( bsData , pointInfo , d , solution , constraints , metSolution , d>maxSolveDepth ? 0 : iters , true , showResidual , NULL , NULL , NULL , accuracy ); } } metSolution.resize( 0 ); return solution; } template< class Real > template< int FEMDegree , int NormalDegree > DenseNodeData< Real , FEMDegree > Octree< Real >::SetLaplacianConstraints( const SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo ) { typedef typename TreeOctNode::NeighborKey< -BSplineEvaluationData< FEMDegree >::SupportStart , BSplineEvaluationData< FEMDegree >::SupportEnd > SupportKey; const int OverlapSize = BSplineIntegrationData< NormalDegree , FEMDegree >::OverlapSize; const int LeftNormalFEMOverlapRadius = -BSplineIntegrationData< NormalDegree , FEMDegree >::OverlapStart; const int RightNormalFEMOverlapRadius = BSplineIntegrationData< NormalDegree , FEMDegree >::OverlapEnd; const int LeftFEMNormalOverlapRadius = -BSplineIntegrationData< FEMDegree , NormalDegree >::OverlapStart; const int RightFEMNormalOverlapRadius = BSplineIntegrationData< FEMDegree , NormalDegree >::OverlapEnd; // To set the Laplacian constraints, we iterate over the // splatted normals and compute the dot-product of the // divergence of the normal field with all the basis functions. // Within the same depth: set directly as a gather // Coarser depths int maxDepth = _sNodes.levels()-1; DenseNodeData< Real , FEMDegree > constraints( _sNodes.size() ) , _constraints( _sNodes.end( maxDepth-1 ) ); memset( constraints.data , 0 , sizeof(Real)*_sNodes.size() ); memset( _constraints.data , 0 , sizeof(Real)*( _sNodes.end(maxDepth-1) ) ); MemoryUsage(); for( int d=maxDepth ; d>=_minDepth ; d-- ) { int offset = d>0 ? _sNodes.begin(d-1) : 0; Stencil< Point3D< double > , OverlapSize > stencil , stencils[2][2][2]; typename BSplineIntegrationData< NormalDegree , FEMDegree >::FunctionIntegrator::Integrator integrator; typename BSplineIntegrationData< FEMDegree , NormalDegree >::FunctionIntegrator::ChildIntegrator childIntegrator; BSplineIntegrationData< NormalDegree , FEMDegree >::SetIntegrator( integrator , d-1 , _dirichlet , _dirichlet ); if( d>_minDepth ) BSplineIntegrationData< FEMDegree , NormalDegree >::SetChildIntegrator( childIntegrator , d-2 , _dirichlet , _dirichlet ); SystemCoefficients< NormalDegree , FEMDegree >::SetCentralDivergenceStencil ( integrator , stencil , false ); SystemCoefficients< FEMDegree , NormalDegree >::SetCentralDivergenceStencils( childIntegrator , stencils , true ); std::vector< SupportKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; idepth(); typename TreeOctNode::Neighbors< OverlapSize > neighbors; neighborKey.template getNeighbors< false , LeftFEMNormalOverlapRadius , RightFEMNormalOverlapRadius >( node , neighbors ); bool isInterior = _IsInteriorlyOverlapped< FEMDegree , NormalDegree >( node ) , isInterior2 = _IsInteriorlyOverlapped< NormalDegree , FEMDegree >( node->parent ); int cx , cy , cz; if( d>_minDepth ) Cube::FactorCornerIndex( (int)( node-node->parent->children) , cx , cy ,cz ); else cx = cy = cz = 0; Stencil< Point3D< double > , OverlapSize >& _stencil = stencils[cx][cy][cz]; int d , off[3]; node->depthAndOffset( d , off ); // Set constraints from current depth // Gather the constraints from the vector-field at _node into the constraint stored with node if( _IsValidNode< FEMDegree >( node ) ) { for( int x=startX ; x( _node ) ) { int _idx = normalInfo.index( _node ); if( _idx>=0 ) if( isInterior ) constraints[i] += Point3D< Real >::Dot( stencil.values[x][y][z] , normalInfo.data[ _idx ] ); else { int _d , _off[3]; _node->depthAndOffset( _d , _off ); constraints[i] += Real( SystemCoefficients< NormalDegree , FEMDegree >::GetDivergence2( integrator , _off , off , normalInfo.data[ _idx ] ) ); } } } _SetParentOverlapBounds< NormalDegree , FEMDegree >( node , startX , endX , startY , endY , startZ , endZ ); } if( !_IsValidNode< NormalDegree >( node ) ) continue; int idx = normalInfo.index( node ); if( idx<0 ) continue; const Point3D< Real >& normal = normalInfo.data[ idx ]; if( normal[0]==0 && normal[1]==0 && normal[2]==0 ) continue; // Set the _constraints for the parents if( depth>_minDepth ) { neighborKey.template getNeighbors< false , LeftNormalFEMOverlapRadius , RightNormalFEMOverlapRadius >( node->parent , neighbors ); for( int x=startX ; x( _node ) ) ) { TreeOctNode* _node = neighbors.neighbors[x][y][z]; Real c; if( isInterior2 ) c = Point3D< Real >::Dot( _stencil.values[x][y][z] , normal ); else { int _d , _off[3]; _node->depthAndOffset( _d , _off ); c = Real( SystemCoefficients< FEMDegree , NormalDegree >::GetDivergence1( childIntegrator , _off , off , normal ) ); } #pragma omp atomic _constraints[ _node->nodeData.nodeIndex ] += c; } } } } MemoryUsage(); } // Fine-to-coarse down-sampling of constraints for( int d=maxDepth-1 ; d>_minDepth ; d-- ) _DownSample( d , _constraints ); // Add the accumulated constraints from all finer depths #pragma omp parallel for num_threads( threads ) for( int i=0 ; i<_sNodes.end(maxDepth-1) ; i++ ) constraints[i] += _constraints[i]; _constraints.resize( 0 ); DenseNodeData< Point3D< Real > , NormalDegree > coefficients( _sNodes.end( maxDepth-1 ) ); for( int d=maxDepth-1 ; d>=_minDepth ; d-- ) { #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(d) ; i<_sNodes.end(d) ; i++ ) if( _IsValidNode< NormalDegree >( _sNodes.treeNodes[i] ) ) { int idx = normalInfo.index( _sNodes.treeNodes[i] ); if( idx<0 ) continue; coefficients[i] = normalInfo.data[ idx ]; } } // Coarse-to-fine up-sampling of coefficients for( int d=_minDepth+1 ; d , OverlapSize > stencils[2][2][2]; typename BSplineIntegrationData< NormalDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator childIntegrator; if( d>_minDepth ) BSplineIntegrationData< NormalDegree , FEMDegree >::SetChildIntegrator( childIntegrator , d-2 , _dirichlet , _dirichlet ); SystemCoefficients< NormalDegree , FEMDegree >::SetCentralDivergenceStencils( childIntegrator , stencils , false ); std::vector< SupportKey > neighborKeys( std::max< int >( 1 , threads ) ); for( size_t i=0 ; i( _sNodes.treeNodes[i] ) ) { SupportKey& neighborKey = neighborKeys[ omp_get_thread_num() ]; TreeOctNode* node = _sNodes.treeNodes[i]; int depth = node->depth(); if( !depth ) continue; int startX , endX , startY , endY , startZ , endZ; _SetParentOverlapBounds< FEMDegree , NormalDegree >( node , startX , endX , startY , endY , startZ , endZ ); typename TreeOctNode::Neighbors< OverlapSize > neighbors; neighborKey.template getNeighbors< false , LeftFEMNormalOverlapRadius , RightFEMNormalOverlapRadius >( node->parent , neighbors ); bool isInterior = _IsInteriorlyOverlapped< FEMDegree , NormalDegree >( node->parent ); int cx , cy , cz; if( d ) { int c = int( node - node->parent->children ); Cube::FactorCornerIndex( c , cx , cy , cz ); } else cx = cy = cz = 0; Stencil< Point3D< double > , OverlapSize >& _stencil = stencils[cx][cy][cz]; Real constraint = Real(0); int d , off[3]; node->depthAndOffset( d , off ); for( int x=startX ; x( _node ) ) { int _i = _node->nodeData.nodeIndex; if( isInterior ) constraint += Point3D< Real >::Dot( coefficients[_i] , _stencil.values[x][y][z] ); else { int _d , _off[3]; _node->depthAndOffset( _d , _off ); constraint += Real( SystemCoefficients< NormalDegree , FEMDegree >::GetDivergence2( childIntegrator , _off , off , coefficients[_i] ) ); } } } constraints[ node->nodeData.nodeIndex ] += constraint; } } MemoryUsage(); coefficients.resize( 0 ); return constraints; } colmap-3.9.1/src/thirdparty/PoissonRecon/MultiGridOctreeData.WeightedSamples.inl000077500000000000000000000614511454702036400300440ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ // Evaluate the result of splatting along a plane and then evaluating at a point on the plane. template< int Degree > double GetScaleValue( void ) { double centerValues[Degree+1]; Polynomial< Degree >::BSplineComponentValues( 0.5 , centerValues ); double scaleValue = 0; for( int i=0 ; i<=Degree ; i++ ) scaleValue += centerValues[i] * centerValues[i]; return 1./ scaleValue; } template< class Real > template< int WeightDegree > void Octree< Real >::_AddWeightContribution( SparseNodeData< Real , WeightDegree >& densityWeights , TreeOctNode* node , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey , Real weight ) { static const double ScaleValue = GetScaleValue< WeightDegree >(); double dx[ DIMENSION ][ PointSupportKey< WeightDegree >::Size ]; typename TreeOctNode::Neighbors< PointSupportKey< WeightDegree >::Size >& neighbors = weightKey.template getNeighbors< true >( node ); if( densityWeights.indices.size() start; Real w; _StartAndWidth( node , start , w ); for( int dim=0 ; dim::BSplineComponentValues( ( position[dim]-start[dim] ) / w , dx[dim] ); weight *= (Real)ScaleValue; for( int i=0 ; i::Size ; i++ ) for( int j=0 ; j::Size ; j++ ) { double dxdy = dx[0][i] * dx[1][j] * weight; TreeOctNode** _neighbors = neighbors.neighbors[i][j]; for( int k=0 ; k::Size ; k++ ) if( _neighbors[k] ) { int idx = densityWeights.index( _neighbors[k] ); if( idx<0 ) { densityWeights.indices[ _neighbors[k]->nodeData.nodeIndex ] = (int)densityWeights.data.size(); densityWeights.data.push_back( (Real)( dxdy * dx[2][k] ) ); } else densityWeights.data[idx] += Real( dxdy * dx[2][k] ); } } } template< class Real > template< int WeightDegree > Real Octree< Real >::_GetSamplesPerNode( const SparseNodeData< Real , WeightDegree >& densityWeights , TreeOctNode* node , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey ) { Real weight = 0; double dx[ DIMENSION ][ PointSupportKey< WeightDegree >::Size ]; typename TreeOctNode::Neighbors< PointSupportKey< WeightDegree >::Size >& neighbors = weightKey.template getNeighbors< true >( node ); Point3D< Real > start; Real w; _StartAndWidth( node , start , w ); for( int dim=0 ; dim::BSplineComponentValues( ( position[dim]-start[dim] ) / w , dx[dim] ); for( int i=0 ; i::Size ; i++ ) for( int j=0 ; j::Size ; j++ ) { double dxdy = dx[0][i] * dx[1][j]; for( int k=0 ; k::Size ; k++ ) if( neighbors.neighbors[i][j][k] ) { int idx = densityWeights.index( neighbors.neighbors[i][j][k] ); if( idx>=0 ) weight += Real( dxdy * dx[2][k] * densityWeights.data[idx] ); } } return weight; } template< class Real > template< int WeightDegree > Real Octree< Real >::_GetSamplesPerNode( const SparseNodeData< Real , WeightDegree >& densityWeights , const TreeOctNode* node , Point3D< Real > position , ConstPointSupportKey< WeightDegree >& weightKey ) const { Real weight = 0; double dx[ DIMENSION ][ PointSupportKey< WeightDegree >::Size ]; typename TreeOctNode::ConstNeighbors< PointSupportKey< WeightDegree >::Size >& neighbors = weightKey.getNeighbors( node ); Point3D< Real > start; Real w; _StartAndWidth( node , start , w ); for( int dim=0 ; dim::BSplineComponentValues( ( position[dim]-start[dim] ) / w , dx[dim] ); for( int i=0 ; i::Size ; i++ ) for( int j=0 ; j::Size ; j++ ) { double dxdy = dx[0][i] * dx[1][j]; for( int k=0 ; k::Size ; k++ ) if( neighbors.neighbors[i][j][k] ) { int idx = densityWeights.index( neighbors.neighbors[i][j][k] ); if( idx>=0 ) weight += Real( dxdy * dx[2][k] * densityWeights.data[idx] ); } } return weight; } template< class Real > template< int WeightDegree > void Octree< Real >::_GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , const TreeOctNode* node , Point3D< Real > position , ConstPointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ) const { const TreeOctNode* temp = node; weight = _GetSamplesPerNode( densityWeights , temp , position , weightKey ); if( weight>=(Real)1. ) depth = Real( _Depth( temp ) + log( weight ) / log(double(1<<(DIMENSION-1))) ); else { Real oldWeight , newWeight; oldWeight = newWeight = weight; while( newWeight<(Real)1. && temp->parent ) { temp=temp->parent; oldWeight = newWeight; newWeight = _GetSamplesPerNode( densityWeights , temp , position , weightKey ); } depth = Real( _Depth( temp ) + log( newWeight ) / log( newWeight / oldWeight ) ); } weight = Real( pow( double(1<<(DIMENSION-1)) , -double(depth) ) ); } template< class Real > template< int WeightDegree > void Octree< Real >::_GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , TreeOctNode* node , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ) { TreeOctNode* temp = node; weight = _GetSamplesPerNode( densityWeights , temp , position , weightKey ); if( weight>=(Real)1. ) depth = Real( _Depth( temp ) + log( weight ) / log(double(1<<(DIMENSION-1))) ); else { Real oldWeight , newWeight; oldWeight = newWeight = weight; while( newWeight<(Real)1. && temp->parent ) { temp=temp->parent; oldWeight = newWeight; newWeight = _GetSamplesPerNode( densityWeights , temp , position, weightKey ); } depth = Real( _Depth( temp ) + log( newWeight ) / log( newWeight / oldWeight ) ); } weight = Real( pow( double(1<<(DIMENSION-1)) , -double(depth) ) ); } template< class Real > template< int WeightDegree > void Octree< Real >::_GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , Point3D< Real > position , ConstPointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ) { TreeOctNode* temp; Point3D< Real > myCenter( (Real)0.5 , (Real)0.5 , (Real)0.5 ); Real myWidth = Real( 1. ); // Get the finest node with depth less than or equal to the splat depth that contains the point temp = _spaceRoot; while( _Depth( temp )<_splatDepth ) { if( !temp->children ) break;// fprintf( stderr , "[ERROR] Octree::GetSampleDepthAndWeight\n" ) , exit( 0 ); int cIndex = TreeOctNode::CornerIndex( myCenter , position ); temp = &temp->children[cIndex]; myWidth /= 2; if( cIndex&1 ) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if( cIndex&2 ) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if( cIndex&4 ) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; } return _GetSampleDepthAndWeight( densityWeights , temp , position , weightKey , depth , weight ); } template< class Real > template< int WeightDegree > void Octree< Real >::_GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ) { TreeOctNode* temp; Point3D< Real > myCenter( (Real)0.5 , (Real)0.5 , (Real)0.5 ); Real myWidth = Real( 1. ); // Get the finest node with depth less than or equal to the splat depth that contains the point temp = _spaceRoot; while( _Depth( temp )<_splatDepth ) { if( !temp->children ) break;// fprintf( stderr , "[ERROR] Octree::GetSampleDepthAndWeight\n" ) , exit( 0 ); int cIndex = TreeOctNode::CornerIndex( myCenter , position ); temp = &temp->children[cIndex]; myWidth /= 2; if( cIndex&1 ) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if( cIndex&2 ) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if( cIndex&4 ) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; } return _GetSampleDepthAndWeight( densityWeights , temp , position , weightKey , depth , weight ); } template< class Real > template< int DataDegree , class V > void Octree< Real >::_SplatPointData( TreeOctNode* node , Point3D< Real > position , V v , SparseNodeData< V , DataDegree >& dataInfo , PointSupportKey< DataDegree >& dataKey ) { double dx[ DIMENSION ][ PointSupportKey< DataDegree >::Size ]; typename TreeOctNode::Neighbors< PointSupportKey< DataDegree >::Size >& neighbors = dataKey.template getNeighbors< true >( node ); Point3D< Real > start; Real w; _StartAndWidth( node , start , w ); for( int dd=0 ; dd::BSplineComponentValues( ( position[dd]-start[dd] ) / w , dx[dd] ); for( int i=0 ; i::Size ; i++ ) for( int j=0 ; j::Size ; j++ ) { double dxdy = dx[0][i] * dx[1][j]; for( int k=0 ; k::Size ; k++ ) if( neighbors.neighbors[i][j][k] ) { TreeOctNode* _node = neighbors.neighbors[i][j][k]; double dxdydz = dxdy * dx[2][k]; if( (int)dataInfo.indices.size()nodeData.nodeIndex ] = (int)dataInfo.data.size(); dataInfo.data.push_back( v * Real(dxdydz) ); } else dataInfo.data[idx] += v * Real( dxdydz ); } } } template< class Real > template< int WeightDegree , int DataDegree , class V > Real Octree< Real >::_SplatPointData( const SparseNodeData< Real , WeightDegree >& densityWeights , Point3D< Real > position , V v , SparseNodeData< V , DataDegree >& dataInfo , PointSupportKey< WeightDegree >& weightKey , PointSupportKey< DataDegree >& dataKey , int minDepth , int maxDepth , int dim ) { double dx; V _v; TreeOctNode* temp; int cnt=0; double width; Point3D< Real > myCenter( (Real)0.5 , (Real)0.5 , (Real)0.5 ); Real myWidth = (Real)1.; temp = _spaceRoot; while( _Depth( temp )<_splatDepth ) { if( !temp->children ) fprintf( stderr , "[ERROR] Octree::SplatPointData\n" ) , exit( 0 ); int cIndex = TreeOctNode::CornerIndex( myCenter , position ); temp = &temp->children[cIndex]; myWidth /= 2; if( cIndex&1 ) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if( cIndex&2 ) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if( cIndex&4 ) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; } Real weight , depth; _GetSampleDepthAndWeight( densityWeights , temp , position , weightKey , depth , weight ); if( depthmaxDepth ) depth = Real(maxDepth); int topDepth = int(ceil(depth)); dx = 1.0-(topDepth-depth); if ( topDepth<=minDepth ) topDepth = minDepth , dx = 1; else if( topDepth> maxDepth ) topDepth = maxDepth , dx = 1; while( _Depth( temp )>topDepth ) temp=temp->parent; while( _Depth( temp )children ) temp->initChildren(); int cIndex = TreeOctNode::CornerIndex( myCenter , position ); temp = &temp->children[cIndex]; myWidth/=2; if( cIndex&1 ) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if( cIndex&2 ) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if( cIndex&4 ) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; } width = 1.0 / ( ( 1<<( _Depth( temp ) ) ) ); _v = v * weight / Real( pow( width , dim ) ) * Real( dx ); _SplatPointData( temp , position , _v , dataInfo , dataKey ); if( fabs(1.0-dx) > EPSILON ) { dx = Real(1.0-dx); temp = temp->parent; width = 1.0 / ( ( 1<<( _Depth( temp ) ) ) ); _v = v * weight / Real( pow( width , dim ) ) * Real( dx ); _SplatPointData( temp , position , _v , dataInfo , dataKey ); } return weight; } template< class Real > template< int WeightDegree , int DataDegree , class V > void Octree< Real >::_MultiSplatPointData( const SparseNodeData< Real , WeightDegree >* densityWeights , Point3D< Real > position , V v , SparseNodeData< V , DataDegree >& dataInfo , PointSupportKey< WeightDegree >& weightKey , PointSupportKey< DataDegree >& dataKey , int maxDepth , int dim ) { Real _depth , weight; if( densityWeights ) _GetSampleDepthAndWeight( *densityWeights , position , weightKey , _depth , weight ); else weight = (Real)1. , _depth = (Real)maxDepth; int depth = std::min< int >( maxDepth , (int)ceil( _depth ) ); V _v = v * weight; Point3D< Real > myCenter( (Real)0.5 , (Real)0.5 , (Real)0.5 ); Real myWidth = (Real)1.; TreeOctNode* temp = _spaceRoot; while( _Depth( temp )<=depth ) { _SplatPointData( temp , position , _v * Real( pow( 1<<_Depth( temp ) , dim ) ) , dataInfo , dataKey ); if( _Depth( temp )children ) temp->initChildren(); int cIndex = TreeOctNode::CornerIndex( myCenter , position ); temp = &temp->children[cIndex]; myWidth /= 2; if( cIndex&1 ) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if( cIndex&2 ) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if( cIndex&4 ) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; } else break; } } template< class Real > template< class V , int DataDegree > V Octree< Real >::_Evaluate( const DenseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData , const ConstPointSupportKey< DataDegree >& neighborKey ) const { V value = V(0); for( int d=0 ; d<=neighborKey.depth() ; d++ ) for( int i=0 ; i::Size ; i++ ) for( int j=0 ; j::Size ; j++ ) for( int k=0 ; k::Size ; k++ ) { const TreeOctNode* n = neighborKey.neighbors[d].neighbors[i][j][k]; if( _IsValidNode< DataDegree >( n ) ) { int fIdx[3]; FunctionIndex< DataDegree >( n , fIdx ); value += ( coefficients[ n->nodeData.nodeIndex ] * (Real) ( bsData.baseBSplines[ fIdx[0] ][PointSupportKey< DataDegree >::Size-1-i]( p[0] ) * bsData.baseBSplines[ fIdx[1] ][PointSupportKey< DataDegree >::Size-1-j]( p[1] ) * bsData.baseBSplines[ fIdx[2] ][PointSupportKey< DataDegree >::Size-1-k]( p[2] ) ) ); } } return value; } template< class Real > template< class V , int DataDegree > V Octree< Real >::_Evaluate( const SparseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData , const ConstPointSupportKey< DataDegree >& dataKey ) const { V value = V(0); for( int d=0 ; d<=dataKey.depth() ; d++ ) { double dx[ DIMENSION ][ PointSupportKey< DataDegree >::Size ]; memset( dx , 0 , sizeof( double ) * DIMENSION * PointSupportKey< DataDegree >::Size ); { const TreeOctNode* n = dataKey.neighbors[d].neighbors[ PointSupportKey< DataDegree >::LeftRadius ][ PointSupportKey< DataDegree >::LeftRadius ][ PointSupportKey< DataDegree >::LeftRadius ]; if( !n ) fprintf( stderr , "[ERROR] Point is not centered on a node\n" ) , exit( 0 ); int fIdx[3]; FunctionIndex< DataDegree >( n , fIdx ); int fStart , fEnd; BSplineData< DataDegree >::FunctionSpan( d-1 , fStart , fEnd ); for( int dd=0 ; dd::LeftRadius ; i<=PointSupportKey< DataDegree >::RightRadius ; i++ ) if( fIdx[dd]+i>=fStart && fIdx[dd]+i::RightRadius ]( p[dd] ); } for( int i=0 ; i::Size ; i++ ) for( int j=0 ; j::Size ; j++ ) for( int k=0 ; k::Size ; k++ ) { const TreeOctNode* n = dataKey.neighbors[d].neighbors[i][j][k]; if( _IsValidNode< DataDegree >( n ) ) { int idx = coefficients.index( n ); if( idx>=0 ) value += coefficients.data[ idx ] * (Real) ( dx[0][i] * dx[1][j] * dx[2][k] ); } } } return value; } template< class Real > template< class V , int DataDegree > V Octree< Real >::Evaluate( const DenseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData ) const { static const int SupportSize = BSplineEvaluationData< DataDegree >::SupportSize; static const int LeftSupportRadius = -BSplineEvaluationData< DataDegree >::SupportStart; static const int RightSupportRadius = BSplineEvaluationData< DataDegree >::SupportEnd; V value = V(0); // [WARNING] This is required because the B-Spline components are not continuous at the domain boundaries // so we need to nudge things inward a tiny bit. for( int dd=0 ; dd<3 ; dd++ ) if ( p[dd]==0 ) p[dd] = 0.+1e-6; else if( p[dd]==1 ) p[dd] = 1.-1e-6; const TreeOctNode* n = _tree.nextNode(); while( n ) { Point3D< Real > s; Real w; _StartAndWidth( n , s , w ); double left = (LeftSupportRadius+0.)*w , right = (RightSupportRadius+1.)*w; if( p[0]<=s[0]-left || p[0]>=s[0]+right || p[1]<=s[1]-left || p[1]>=s[1]+right || p[2]<=s[2]-left || p[2]>=s[2]+right ) { n = _tree.nextBranch( n ); continue; } if( _IsValidNode< DataDegree >( n ) ) { int d , fIdx[3] , pIdx[3]; _DepthAndOffset( n , d , fIdx ); for( int dd=0 ; dd<3 ; dd++ ) pIdx[dd] = std::max< int >( 0 , std::min< int >( SupportSize-1 , LeftSupportRadius + (int)floor( ( p[dd]-s[dd] ) / w ) ) ); value += coefficients[ n->nodeData.nodeIndex ] * (Real) ( bsData.baseBSplines[ BSplineData< DataDegree >::FunctionIndex( d , fIdx[0] ) ][ pIdx[0] ]( p[0] ) * bsData.baseBSplines[ BSplineData< DataDegree >::FunctionIndex( d , fIdx[1] ) ][ pIdx[1] ]( p[1] ) * bsData.baseBSplines[ BSplineData< DataDegree >::FunctionIndex( d , fIdx[2] ) ][ pIdx[2] ]( p[2] ) ); } n = _tree.nextNode( n ); } return value; } template< class Real > template< class V , int DataDegree > V Octree< Real >::Evaluate( const SparseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData ) const { V value = V(0); const TreeOctNode* n = _tree.nextNode(); while( n ) { Point3D< Real > s; Real w; _StartAndWidth( n , s , w ); if( !_IsValidNode< DataDegree >( n ) || p[0]::SupportStart*w || p[0]>s[0]+(BSplineData< DataDegree >::SupportEnd+1.0)*w || p[1]::SupportStart*w || p[1]>s[1]+(BSplineData< DataDegree >::SupportEnd+1.0)*w || p[2]::SupportStart*w || p[2]>s[2]+(BSplineData< DataDegree >::SupportEnd+1.0)*w ) { n = _tree.nextBranch( n ); continue; } int idx = coefficients.index( n ); if( idx>=0 ) { int d , off[3] , pIdx[3]; _DepthAndOffset( n , d , off ); for( int dd=0 ; dd<3 ; dd++ ) pIdx[dd] = std::max< int >( 0 , std::min< int >( BSplineData< DataDegree >::SupportSize-1 , -BSplineData< DataDegree >::SupportStart + (int)floor( ( p[dd]-s[dd] ) / w ) ) ); value += coefficients.data[idx] * (Real) ( bsData.baseBSplines[ BSplineData< DataDegree >::FunctionIndex( d , off[0] ) ][ pIdx[0] ]( p[0] ) * bsData.baseBSplines[ BSplineData< DataDegree >::FunctionIndex( d , off[1] ) ][ pIdx[1] ]( p[1] ) * bsData.baseBSplines[ BSplineData< DataDegree >::FunctionIndex( d , off[2] ) ][ pIdx[2] ]( p[2] ) ); } n = _tree.nextNode( n ); } return value; } template< class Real > template< class V , int DataDegree > Pointer( V ) Octree< Real >::Evaluate( const DenseNodeData< V , DataDegree >& coefficients , int& res , Real isoValue , int depth , bool primal ) { int dim; if( depth>=0 ) depth++; int maxDepth = _tree.maxDepth(); if( depth<=0 || depth>maxDepth ) depth = maxDepth; BSplineData< DataDegree > fData; fData.set( depth , _dirichlet ); // Initialize the coefficients at the coarsest level Pointer( V ) _coefficients = NullPointer( V ); { int d = _minDepth; dim = _Dimension< DataDegree >( d ); _coefficients = NewPointer< V >( dim * dim * dim ); memset( _coefficients , 0 , sizeof( V ) * dim * dim * dim ); #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(d) ; i<_sNodes.end(d) ; i++ ) if( _IsValidNode< DataDegree >( _sNodes.treeNodes[i] ) ) { int _d , _off[3]; _sNodes.treeNodes[i]->depthAndOffset( _d , _off ); _coefficients[ _off[0] + _off[1]*dim + _off[2]*dim*dim ] = coefficients[i]; } } // Up-sample and add in the existing coefficients for( int d=_minDepth+1 ; d<=depth ; d++ ) { dim = _Dimension< DataDegree >( d ); Pointer( V ) __coefficients = NewPointer< V >( dim * dim *dim ); memset( __coefficients , 0 , sizeof( V ) * dim * dim * dim ); #pragma omp parallel for num_threads( threads ) for( int i=_sNodes.begin(d) ; i<_sNodes.end(d) ; i++ ) if( _IsValidNode< DataDegree >( _sNodes.treeNodes[i] ) ) { int _d , _off[3]; _sNodes.treeNodes[i]->depthAndOffset( _d , _off ); __coefficients[ _off[0] + _off[1]*dim + _off[2]*dim*dim ] = coefficients[i]; } _UpSample< V , DataDegree >( d , ( ConstPointer(V) )_coefficients , __coefficients , _dirichlet , threads ); DeletePointer( _coefficients ); _coefficients = __coefficients; } res = 1<<(depth-1); if( primal ) res++; Pointer( V ) values = NewPointer< V >( res*res*res ); memset( values , 0 , sizeof(V)*res*res*res ); if( primal ) { // Evaluate at the cell corners typename BSplineEvaluationData< DataDegree >::CornerEvaluator::Evaluator evaluator; BSplineEvaluationData< DataDegree >::SetCornerEvaluator( evaluator , depth-1 , _dirichlet ); #pragma omp parallel for num_threads( threads ) for( int k=0 ; k::CornerEnd ; kk<=-BSplineEvaluationData< DataDegree >::CornerStart ; kk++ ) if( k+kk>=0 && k+kk::CornerEnd ; jj<=-BSplineEvaluationData< DataDegree >::CornerStart ; jj++ ) if( j+jj>=0 && j+jj::CornerEnd ; ii<=-BSplineEvaluationData< DataDegree >::CornerStart ; ii++ ) if( i+ii>=0 && i+ii::CenterEvaluator::Evaluator evaluator; BSplineEvaluationData< DataDegree >::SetCenterEvaluator( evaluator , depth-1 , _dirichlet ); #pragma omp parallel for num_threads( threads ) for( int k=0 ; k::SupportEnd ; kk<=-BSplineEvaluationData< DataDegree >::SupportStart ; kk++ ) if( k+kk>=0 && k+kk::SupportEnd ; jj<=-BSplineEvaluationData< DataDegree >::SupportStart ; jj++ ) if( j+jj>=0 && j+jj::SupportEnd ; ii<=-BSplineEvaluationData< DataDegree >::SupportStart ; ii++ ) if( i+ii>=0 && i+ii // b_1[i] = < \nabla B_i(p) , V(p) > // 2] Formulate this as a Poisson equation: // \sum_i x_i \Delta B_i(p) = \nabla \cdot V(p) // which is solved by the system A_2x = b_2 where: // A_2[i,j] = - < \Delta B_i(p) , B_j(p) > // b_2[i] = - < B_i(p) , \nabla \cdot V(p) > // Although the two system matrices should be the same (assuming that the B_i satisfy dirichlet/neumann boundary conditions) // the constraint vectors can differ when V does not satisfy the Neumann boundary conditions: // A_1[i,j] = \int_R < \nabla B_i(p) , \nabla B_j(p) > // = \int_R [ \nabla \cdot ( B_i(p) \nabla B_j(p) ) - B_i(p) \Delta B_j(p) ] // = \int_dR < N(p) , B_i(p) \nabla B_j(p) > + A_2[i,j] // and the first integral is zero if either f_i is zero on the boundary dR or the derivative of B_i across the boundary is zero. // However, for the constraints we have: // b_1(i) = \int_R < \nabla B_i(p) , V(p) > // = \int_R [ \nabla \cdot ( B_i(p) V(p) ) - B_i(p) \nabla \cdot V(p) ] // = \int_dR < N(p) , B_i(p) V(p) > + b_2[i] // In particular, this implies that if the B_i satisfy the Neumann boundary conditions (rather than Dirichlet), // and V is not zero across the boundary, then the two constraints are different. // Forcing the < V(p) , N(p) > = 0 on the boundary, by killing off the component of the vector-field in the normal direction // (FORCE_NEUMANN_FIELD), makes the two systems equal, and the value of this flag should be immaterial. // Note that under interpretation 1, we have: // \sum_i b_1(i) = < \nabla \sum_ i B_i(p) , V(p) > = 0 // because the B_i's sum to one. However, in general, we could have // \sum_i b_2(i) \neq 0. // This could cause trouble because the constant functions are in the kernel of the matrix A, so CG will misbehave if the constraint // has a non-zero DC term. (Again, forcing < V(p) , N(p) > = 0 along the boundary resolves this problem.) #define FORCE_NEUMANN_FIELD 1 // This flag forces the normal component across the boundary of the integration domain to be zero. // This should be enabled if GRADIENT_DOMAIN_SOLUTION is not, so that CG doesn't run into trouble. #if !FORCE_NEUMANN_FIELD #pragma message( "[WARNING] Not zeroing out normal component on boundary" ) #endif // !FORCE_NEUMANN_FIELD #include "Hash.h" #include "BSplineData.h" #include "PointStream.h" #ifndef _OPENMP int omp_get_num_procs( void ){ return 1; } int omp_get_thread_num( void ){ return 0; } #endif // _OPENMP class TreeNodeData { public: static size_t NodeCount; int nodeIndex; char flags; TreeNodeData( void ); ~TreeNodeData( void ); }; class VertexData { typedef OctNode< TreeNodeData > TreeOctNode; public: static const int VERTEX_COORDINATE_SHIFT = ( sizeof( long long ) * 8 ) / 3; static long long EdgeIndex( const TreeOctNode* node , int eIndex , int maxDepth , int index[DIMENSION] ); static long long EdgeIndex( const TreeOctNode* node , int eIndex , int maxDepth ); static long long FaceIndex( const TreeOctNode* node , int fIndex , int maxDepth,int index[DIMENSION] ); static long long FaceIndex( const TreeOctNode* node , int fIndex , int maxDepth ); static long long CornerIndex( const TreeOctNode* node , int cIndex , int maxDepth , int index[DIMENSION] ); static long long CornerIndex( const TreeOctNode* node , int cIndex , int maxDepth ); static long long CenterIndex( const TreeOctNode* node , int maxDepth , int index[DIMENSION] ); static long long CenterIndex( const TreeOctNode* node , int maxDepth ); static long long CornerIndex( int depth , const int offSet[DIMENSION] , int cIndex , int maxDepth , int index[DIMENSION] ); static long long CenterIndex( int depth , const int offSet[DIMENSION] , int maxDepth , int index[DIMENSION] ); static long long CornerIndexKey( const int index[DIMENSION] ); }; // This class stores the octree nodes, sorted by depth and then by z-slice. // To support primal representations, the initializer takes a function that // determines if a node should be included/indexed in the sorted list. class SortedTreeNodes { typedef OctNode< TreeNodeData > TreeOctNode; protected: Pointer( Pointer( int ) ) _sliceStart; int _levels; public: Pointer( TreeOctNode* ) treeNodes; int begin( int depth ) const{ return _sliceStart[depth][0]; } int end( int depth ) const{ return _sliceStart[depth][(size_t)1<=_levels||slice<0||slice>=(1<=_levels) printf( "uhoh\n" ); return _sliceStart[depth][(size_t)1<* map ); void set( TreeOctNode& root ); template< int Indices > struct _Indices { int idx[Indices]; _Indices( void ){ memset( idx , -1 , sizeof( int ) * Indices ); } int& operator[] ( int i ) { return idx[i]; } const int& operator[] ( int i ) const { return idx[i]; } }; typedef _Indices< Square::CORNERS > SquareCornerIndices; typedef _Indices< Square::EDGES > SquareEdgeIndices; typedef _Indices< Square::FACES > SquareFaceIndices; struct SliceTableData { Pointer( SquareCornerIndices ) cTable; Pointer( SquareEdgeIndices ) eTable; Pointer( SquareFaceIndices ) fTable; int cCount , eCount , fCount , nodeOffset , nodeCount; SliceTableData( void ){ fCount = eCount = cCount = 0 , cTable = NullPointer( SquareCornerIndices ) , eTable = NullPointer( SquareEdgeIndices ) , fTable = NullPointer( SquareFaceIndices ) , _cMap = _eMap = _fMap = NullPointer( int ); } ~SliceTableData( void ){ clear(); } void clear( void ){ DeletePointer( cTable ) ; DeletePointer( eTable ) ; DeletePointer( fTable ) ; fCount = eCount = cCount = 0; } SquareCornerIndices& cornerIndices( const TreeOctNode* node ); SquareCornerIndices& cornerIndices( int idx ); const SquareCornerIndices& cornerIndices( const TreeOctNode* node ) const; const SquareCornerIndices& cornerIndices( int idx ) const; SquareEdgeIndices& edgeIndices( const TreeOctNode* node ); SquareEdgeIndices& edgeIndices( int idx ); const SquareEdgeIndices& edgeIndices( const TreeOctNode* node ) const; const SquareEdgeIndices& edgeIndices( int idx ) const; SquareFaceIndices& faceIndices( const TreeOctNode* node ); SquareFaceIndices& faceIndices( int idx ); const SquareFaceIndices& faceIndices( const TreeOctNode* node ) const; const SquareFaceIndices& faceIndices( int idx ) const; protected: Pointer( int ) _cMap; Pointer( int ) _eMap; Pointer( int ) _fMap; friend class SortedTreeNodes; }; struct XSliceTableData { Pointer( SquareCornerIndices ) eTable; Pointer( SquareEdgeIndices ) fTable; int fCount , eCount , nodeOffset , nodeCount; XSliceTableData( void ){ fCount = eCount = 0 , eTable = NullPointer( SquareCornerIndices ) , fTable = NullPointer( SquareEdgeIndices ) , _eMap = _fMap = NullPointer( int ); } ~XSliceTableData( void ){ clear(); } void clear( void ) { DeletePointer( fTable ) ; DeletePointer( eTable ) ; fCount = eCount = 0; } SquareCornerIndices& edgeIndices( const TreeOctNode* node ); SquareCornerIndices& edgeIndices( int idx ); const SquareCornerIndices& edgeIndices( const TreeOctNode* node ) const; const SquareCornerIndices& edgeIndices( int idx ) const; SquareEdgeIndices& faceIndices( const TreeOctNode* node ); SquareEdgeIndices& faceIndices( int idx ); const SquareEdgeIndices& faceIndices( const TreeOctNode* node ) const; const SquareEdgeIndices& faceIndices( int idx ) const; protected: Pointer( int ) _eMap; Pointer( int ) _fMap; friend class SortedTreeNodes; }; void setSliceTableData ( SliceTableData& sData , int depth , int offset , int threads ) const; void setXSliceTableData( XSliceTableData& sData , int depth , int offset , int threads ) const; }; template< int Degree > struct PointSupportKey : public OctNode< TreeNodeData >::NeighborKey< BSplineEvaluationData< Degree >::SupportEnd , -BSplineEvaluationData< Degree >::SupportStart > { static const int LeftRadius = BSplineEvaluationData< Degree >::SupportEnd; static const int RightRadius = -BSplineEvaluationData< Degree >::SupportStart; static const int Size = LeftRadius + RightRadius + 1; }; template< int Degree > struct ConstPointSupportKey : public OctNode< TreeNodeData >::ConstNeighborKey< BSplineEvaluationData< Degree >::SupportEnd , -BSplineEvaluationData< Degree >::SupportStart > { static const int LeftRadius = BSplineEvaluationData< Degree >::SupportEnd; static const int RightRadius = -BSplineEvaluationData< Degree >::SupportStart; static const int Size = LeftRadius + RightRadius + 1; }; template< class Real > struct PointData { Point3D< Real > position; Real weightedCoarserDValue; Real weight; PointData( Point3D< Real > p=Point3D< Real >() , Real w=0 ) { position = p , weight = w , weightedCoarserDValue = Real(0); } }; template< class Data , int Degree > struct SparseNodeData { std::vector< int > indices; std::vector< Data > data; template< class TreeNodeData > int index( const OctNode< TreeNodeData >* node ) const { return ( !node || node->nodeData.nodeIndex<0 || node->nodeData.nodeIndex>=(int)indices.size() ) ? -1 : indices[ node->nodeData.nodeIndex ]; } #if NEW_NEW_CODE int index( int nodeIndex ) const { return ( nodeIndex<0 || nodeIndex>=(int)indices.size() ) ? -1 : indices[ nodeIndex ]; } #endif // NEW_NEW_CODE void resize( size_t sz ){ indices.resize( sz , -1 ); } void remapIndices( const std::vector< int >& map ) { std::vector< int > temp = indices; indices.resize( map.size() ); for( size_t i=0 ; i struct DenseNodeData { Pointer( Data ) data; DenseNodeData( void ) { data = NullPointer( Data ); } DenseNodeData( size_t sz ){ if( sz ) data = NewPointer< Data >( sz ) ; else data = NullPointer( Data ); } void resize( size_t sz ){ DeletePointer( data ) ; if( sz ) data = NewPointer< Data >( sz ) ; else data = NullPointer( Data ); } Data& operator[] ( int idx ) { return data[idx]; } const Data& operator[] ( int idx ) const { return data[idx]; } }; template< class C , int N > struct Stencil{ C values[N][N][N]; }; template< int Degree1 , int Degree2 > class SystemCoefficients { typedef typename BSplineIntegrationData< Degree1 , Degree2 >::FunctionIntegrator FunctionIntegrator; static const int OverlapSize = BSplineIntegrationData< Degree1 , Degree2 >::OverlapSize; static const int OverlapStart = BSplineIntegrationData< Degree1 , Degree2 >::OverlapStart; static const int OverlapEnd = BSplineIntegrationData< Degree1 , Degree2 >::OverlapEnd; public: static double GetLaplacian ( const typename FunctionIntegrator:: Integrator& integrator , const int off1[3] , const int off2[3] ); static double GetLaplacian ( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[3] , const int off2[3] ); static double GetDivergence1( const typename FunctionIntegrator:: Integrator& integrator , const int off1[3] , const int off2[3] , Point3D< double > normal1 ); static double GetDivergence1( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[3] , const int off2[3] , Point3D< double > normal1 ); static double GetDivergence2( const typename FunctionIntegrator:: Integrator& integrator , const int off1[3] , const int off2[3] , Point3D< double > normal2 ); static double GetDivergence2( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[3] , const int off2[3] , Point3D< double > normal2 ); static Point3D< double > GetDivergence1 ( const typename FunctionIntegrator:: Integrator& integrator , const int off1[3] , const int off2[3] ); static Point3D< double > GetDivergence1 ( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[3] , const int off2[3] ); static Point3D< double > GetDivergence2 ( const typename FunctionIntegrator:: Integrator& integrator , const int off1[3] , const int off2[3] ); static Point3D< double > GetDivergence2 ( const typename FunctionIntegrator::ChildIntegrator& integrator , const int off1[3] , const int off2[3] ); static void SetCentralDivergenceStencil ( const typename FunctionIntegrator:: Integrator& integrator , Stencil< Point3D< double > , OverlapSize >& stencil , bool scatter ); static void SetCentralDivergenceStencils( const typename FunctionIntegrator::ChildIntegrator& integrator , Stencil< Point3D< double > , OverlapSize > stencil[2][2][2] , bool scatter ); static void SetCentralLaplacianStencil ( const typename FunctionIntegrator:: Integrator& integrator , Stencil< double , OverlapSize >& stencil ); static void SetCentralLaplacianStencils ( const typename FunctionIntegrator::ChildIntegrator& integrator , Stencil< double , OverlapSize > stencil[2][2][2] ); }; // Note that throughout this code, the "depth" parameter refers to the depth in the octree, not the corresponding depth // of the B-Spline element template< class Real > class Octree { typedef OctNode< TreeNodeData > TreeOctNode; public: template< int FEMDegree > static void FunctionIndex( const TreeOctNode* node , int idx[3] ); typedef typename TreeOctNode:: NeighborKey< 1 , 1 > AdjacenctNodeKey; typedef typename TreeOctNode::ConstNeighborKey< 1 , 1 > ConstAdjacenctNodeKey; template< class V > struct ProjectiveData { V v; Real w; ProjectiveData( V vv=V(0) , Real ww=Real(0) ) : v(vv) , w(ww) { } operator V (){ return w!=0 ? v/w : v*w; } ProjectiveData& operator += ( const ProjectiveData& p ){ v += p.v , w += p.w ; return *this; } ProjectiveData& operator -= ( const ProjectiveData& p ){ v -= p.v , w -= p.w ; return *this; } ProjectiveData& operator *= ( Real s ){ v *= s , w *= s ; return *this; } ProjectiveData& operator /= ( Real s ){ v /= s , w /= s ; return *this; } ProjectiveData operator + ( const ProjectiveData& p ) const { return ProjectiveData( v+p.v , w+p.w ); } ProjectiveData operator - ( const ProjectiveData& p ) const { return ProjectiveData( v-p.v , w-p.w ); } ProjectiveData operator * ( Real s ) const { return ProjectiveData( v*s , w*s ); } ProjectiveData operator / ( Real s ) const { return ProjectiveData( v/s , w/s ); } }; template< int FEMDegree > static bool IsValidNode( const TreeOctNode* node , bool dirichlet ); protected: template< int FEMDegree > bool _IsValidNode( const TreeOctNode* node ) const { return node && ( node->nodeData.flags & ( 1<<( FEMDegree&1 ) ) ) ; } TreeOctNode _tree; TreeOctNode* _spaceRoot; SortedTreeNodes _sNodes; int _splatDepth; int _maxDepth; int _minDepth; int _fullDepth; bool _constrainValues; bool _dirichlet; Real _scale; Point3D< Real > _center; int _multigridDegree; bool _InBounds( Point3D< Real > ) const; template< int FEMDegree > static int _Dimension( int depth ){ return BSplineData< FEMDegree >::Dimension( depth-1 ); } static int _Resolution( int depth ){ return 1<<(depth-1); } template< int FEMDegree > static bool _IsInteriorlySupported( int d , int x , int y , int z ) { if( d-1>=0 ) { int begin , end; BSplineEvaluationData< FEMDegree >::InteriorSupportedSpan( d-1 , begin , end ); return ( x>=begin && x=begin && y=begin && z static bool _IsInteriorlySupported( const TreeOctNode* node ) { if( !node ) return false; int d , off[3]; node->depthAndOffset( d , off ); return _IsInteriorlySupported< FEMDegree >( d , off[0] , off[1] , off[2] ); } template< int FEMDegree1 , int FEMDegree2 > static bool _IsInteriorlyOverlapped( int d , int x , int y , int z ) { if( d-1>=0 ) { int begin , end; BSplineIntegrationData< FEMDegree1 , FEMDegree2 >::InteriorOverlappedSpan( d-1 , begin , end ); return ( x>=begin && x=begin && y=begin && z static bool _IsInteriorlyOverlapped( const TreeOctNode* node ) { if( !node ) return false; int d , off[3]; node->depthAndOffset( d , off ); return _IsInteriorlyOverlapped< FEMDegree1 , FEMDegree2 >( d , off[0] , off[1] , off[2] ); } static void _DepthAndOffset( const TreeOctNode* node , int& d , int off[3] ){ node->depthAndOffset( d , off ) ; d -= 1; } static int _Depth( const TreeOctNode* node ){ return node->depth()-1; } static void _StartAndWidth( const TreeOctNode* node , Point3D< Real >& start , Real& width ) { int d , off[3]; _DepthAndOffset( node , d , off ); if( d>=0 ) width = Real( 1.0 / (1<< d ) ); else width = Real( 1.0 * (1<<(-d)) ); for( int dd=0 ; dd& center , Real& width ) { int d , off[3]; _DepthAndOffset( node , d , off ); width = Real( 1.0 / (1< static typename TreeOctNode::ConstNeighbors< LeftRadius + RightRadius + 1 >& _Neighbors( TreeOctNode::ConstNeighborKey< LeftRadius , RightRadius >& key , int depth ){ return key.neighbors[ depth + 1 ]; } template< int LeftRadius , int RightRadius > static typename TreeOctNode::Neighbors< LeftRadius + RightRadius + 1 >& _Neighbors( TreeOctNode::NeighborKey< LeftRadius , RightRadius >& key , int depth ){ return key.neighbors[ depth + 1 ]; } template< int LeftRadius , int RightRadius > static const typename TreeOctNode::template Neighbors< LeftRadius + RightRadius + 1 >& _Neighbors( const typename TreeOctNode::template NeighborKey< LeftRadius , RightRadius >& key , int depth ){ return key.neighbors[ depth + 1 ]; } template< int LeftRadius , int RightRadius > static const typename TreeOctNode::template ConstNeighbors< LeftRadius + RightRadius + 1 >& _Neighbors( const typename TreeOctNode::template ConstNeighborKey< LeftRadius , RightRadius >& key , int depth ){ return key.neighbors[ depth + 1 ]; } static void _SetFullDepth( TreeOctNode* node , int depth ); void _setFullDepth( int depth ); //////////////////////////////////// // System construction code // // MultiGridOctreeData.System.inl // //////////////////////////////////// template< int FEMDegree > void _setMultiColorIndices( int start , int end , std::vector< std::vector< int > >& indices ) const; template< int FEMDegree > int _SolveSystemGS( const BSplineData< FEMDegree >& bsData , SparseNodeData< PointData< Real > , 0 >& pointInfo , int depth , DenseNodeData< Real , FEMDegree >& solution , DenseNodeData< Real , FEMDegree >& constraints , DenseNodeData< Real , FEMDegree >& metSolutionConstraints , int iters , bool coarseToFine , bool showResidual=false , double* bNorm2=NULL , double* inRNorm2=NULL , double* outRNorm2=NULL , bool forceSilent=false ); template< int FEMDegree > int _SolveSystemCG( const BSplineData< FEMDegree >& bsData , SparseNodeData< PointData< Real > , 0 >& pointInfo , int depth , DenseNodeData< Real , FEMDegree >& solution , DenseNodeData< Real , FEMDegree >& constraints , DenseNodeData< Real , FEMDegree >& metSolutionConstraints , int iters , bool coarseToFine , bool showResidual=false , double* bNorm2=NULL , double* inRNorm2=NULL , double* outRNorm2=NULL , double accuracy=0 ); template< int FEMDegree > int _SetMatrixRow( const SparseNodeData< PointData< Real > , 0 >& pointInfo , const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& neighbors , Pointer( MatrixEntry< Real > ) row , int offset , const typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator& integrator , const Stencil< double , BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& stencil , const BSplineData< FEMDegree >& bsData ) const; template< int FEMDegree > int _GetMatrixRowSize( const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& neighbors ) const; template< int FEMDegree1 , int FEMDegree2 > static void _SetParentOverlapBounds( const TreeOctNode* node , int& startX , int& endX , int& startY , int& endY , int& startZ , int& endZ ); template< int FEMDegree > void _UpdateConstraintsFromCoarser( const SparseNodeData< PointData< Real > , 0 >& pointInfo , const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& neighbors , const typename TreeOctNode::Neighbors< BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& pNeighbors , TreeOctNode* node , DenseNodeData< Real , FEMDegree >& constraints , const DenseNodeData< Real , FEMDegree >& metSolution , const typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const Stencil< double , BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapSize >& stencil , const BSplineData< FEMDegree >& bsData ) const; // Updates the constraints @(depth-1) based on the solution coefficients @(depth) template< int FEMDegree > void _UpdateConstraintsFromFiner( const typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const BSplineData< FEMDegree >& bsData , int highDepth , const DenseNodeData< Real , FEMDegree >& fineSolution , DenseNodeData< Real , FEMDegree >& coarseConstraints ) const; // Evaluate the points @(depth) using coefficients @(depth-1) template< int FEMDegree > void _SetPointValuesFromCoarser( SparseNodeData< PointData< Real > , 0 >& pointInfo , int highDepth , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& upSampledCoefficients ); // Evalutes the solution @(depth) at the points @(depth-1) and updates the met constraints @(depth-1) template< int FEMDegree > void _SetPointConstraintsFromFiner( const SparseNodeData< PointData< Real > , 0 >& pointInfo , int highDepth , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& finerCoefficients , DenseNodeData< Real , FEMDegree >& metConstraints ) const; template< int FEMDegree > Real _CoarserFunctionValue( Point3D< Real > p , const PointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& upSampledCoefficients ) const; template< int FEMDegree > Real _FinerFunctionValue ( Point3D< Real > p , const PointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , const BSplineData< FEMDegree >& bsData , const DenseNodeData< Real , FEMDegree >& coefficients ) const; template< int FEMDegree > int _GetSliceMatrixAndUpdateConstraints( const SparseNodeData< PointData< Real > , 0 >& pointInfo , SparseMatrix< Real >& matrix , DenseNodeData< Real , FEMDegree >& constraints , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator& integrator , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const BSplineData< FEMDegree >& bsData , int depth , int slice , const DenseNodeData< Real , FEMDegree >& metSolution , bool coarseToFine ); template< int FEMDegree > int _GetMatrixAndUpdateConstraints( const SparseNodeData< PointData< Real > , 0 >& pointInfo , SparseMatrix< Real >& matrix , DenseNodeData< Real , FEMDegree >& constraints , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::Integrator& integrator , typename BSplineIntegrationData< FEMDegree , FEMDegree >::FunctionIntegrator::ChildIntegrator& childIntegrator , const BSplineData< FEMDegree >& bsData , int depth , const DenseNodeData< Real , FEMDegree >* metSolution , bool coarseToFine ); // Down samples constraints @(depth) to constraints @(depth-1) template< class C , int FEMDegree > void _DownSample( int highDepth , DenseNodeData< C , FEMDegree >& constraints ) const; // Up samples coefficients @(depth-1) to coefficients @(depth) template< class C , int FEMDegree > void _UpSample( int highDepth , DenseNodeData< C , FEMDegree >& coefficients ) const; template< class C , int FEMDegree > static void _UpSample( int highDepth , ConstPointer( C ) lowCoefficients , Pointer( C ) highCoefficients , bool dirichlet , int threads ); ///////////////////////////////////////////// // Code for splatting point-sample data // // MultiGridOctreeData.WeightedSamples.inl // ///////////////////////////////////////////// template< int WeightDegree > void _AddWeightContribution( SparseNodeData< Real , WeightDegree >& densityWeights , TreeOctNode* node , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey , Real weight=Real(1.0) ); template< int WeightDegree > Real _GetSamplesPerNode( const SparseNodeData< Real , WeightDegree >& densityWeights , const TreeOctNode* node , Point3D< Real > position , ConstPointSupportKey< WeightDegree >& weightKey ) const; template< int WeightDegree > Real _GetSamplesPerNode( const SparseNodeData< Real , WeightDegree >& densityWeights , TreeOctNode* node , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey ); template< int WeightDegree > void _GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , const TreeOctNode* node , Point3D< Real > position , ConstPointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ) const; template< int WeightDegree > void _GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , TreeOctNode* node , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ); public: template< int WeightDegree > void _GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , Point3D< Real > position , PointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ); template< int WeightDegree > void _GetSampleDepthAndWeight( const SparseNodeData< Real , WeightDegree >& densityWeights , Point3D< Real > position , ConstPointSupportKey< WeightDegree >& weightKey , Real& depth , Real& weight ); protected: template< int DataDegree , class V > void _SplatPointData( TreeOctNode* node , Point3D< Real > point , V v , SparseNodeData< V , DataDegree >& data , PointSupportKey< DataDegree >& dataKey ); template< int WeightDegree , int DataDegree , class V > Real _SplatPointData( const SparseNodeData< Real , WeightDegree >& densityWeights , Point3D< Real > point , V v , SparseNodeData< V , DataDegree >& data , PointSupportKey< WeightDegree >& weightKey , PointSupportKey< DataDegree >& dataKey , int minDepth , int maxDepth , int dim=DIMENSION ); template< int WeightDegree , int DataDegree , class V > void _MultiSplatPointData( const SparseNodeData< Real , WeightDegree >* densityWeights , Point3D< Real > point , V v , SparseNodeData< V , DataDegree >& data , PointSupportKey< WeightDegree >& weightKey , PointSupportKey< DataDegree >& dataKey , int maxDepth , int dim=DIMENSION ); template< class V , int DataDegree > V _Evaluate( const DenseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData , const ConstPointSupportKey< DataDegree >& neighborKey ) const; template< class V , int DataDegree > V _Evaluate( const SparseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData , const ConstPointSupportKey< DataDegree >& dataKey ) const; public: template< class V , int DataDegree > V Evaluate( const DenseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData ) const; template< class V , int DataDegree > V Evaluate( const SparseNodeData< V , DataDegree >& coefficients , Point3D< Real > p , const BSplineData< DataDegree >& bsData ) const; template< class V , int DataDegree > Pointer( V ) Evaluate( const DenseNodeData< V , DataDegree >& coefficients , int& res , Real isoValue=0.f , int depth=-1 , bool primal=false ); template< int NormalDegree > int _HasNormals( TreeOctNode* node , const SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo ); void _MakeComplete( void ); void _SetValidityFlags( void ); template< int NormalDegree > void _ClipTree( const SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo ); //////////////////////////////////// // Evaluation Methods // // MultiGridOctreeData.Evaluation // //////////////////////////////////// static const int CHILDREN = Cube::CORNERS; template< int FEMDegree > struct _Evaluator { typename BSplineEvaluationData< FEMDegree >::Evaluator evaluator; typename BSplineEvaluationData< FEMDegree >::ChildEvaluator childEvaluator; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > cellStencil; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > cellStencils [CHILDREN]; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > edgeStencil [Cube::EDGES ]; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > edgeStencils [CHILDREN][Cube::EDGES ]; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > faceStencil [Cube::FACES ]; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > faceStencils [CHILDREN][Cube::FACES ]; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > cornerStencil [Cube::CORNERS]; Stencil< double , BSplineEvaluationData< FEMDegree >::SupportSize > cornerStencils[CHILDREN][Cube::CORNERS]; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dCellStencil; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dCellStencils [CHILDREN]; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dEdgeStencil [Cube::EDGES ]; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dEdgeStencils [CHILDREN][Cube::EDGES ]; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dFaceStencil [Cube::FACES ]; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dFaceStencils [CHILDREN][Cube::FACES ]; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dCornerStencil [Cube::CORNERS]; Stencil< Point3D< double > , BSplineEvaluationData< FEMDegree >::SupportSize > dCornerStencils[CHILDREN][Cube::CORNERS]; void set( int depth , bool dirichlet ); }; template< class V , int FEMDegree > V _getCenterValue( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , const DenseNodeData< V , FEMDegree >& solution , const DenseNodeData< V , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const; template< class V , int FEMDegree > V _getCornerValue( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int corner , const DenseNodeData< V , FEMDegree >& solution , const DenseNodeData< V , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const; template< class V , int FEMDegree > V _getEdgeValue ( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int edge , const DenseNodeData< V , FEMDegree >& solution , const DenseNodeData< V , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const; template< int FEMDegree > std::pair< Real , Point3D< Real > > _getCornerValueAndGradient( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int corner , const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const; template< int FEMDegree > std::pair< Real , Point3D< Real > > _getEdgeValueAndGradient ( const ConstPointSupportKey< FEMDegree >& neighborKey , const TreeOctNode* node , int edge , const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& metSolution , const _Evaluator< FEMDegree >& evaluator , bool isInterior ) const; //////////////////////////////////////// // Iso-Surfacing Methods // // MultiGridOctreeData.IsoSurface.inl // //////////////////////////////////////// struct IsoEdge { long long edges[2]; IsoEdge( void ){ edges[0] = edges[1] = 0; } IsoEdge( long long v1 , long long v2 ){ edges[0] = v1 , edges[1] = v2; } long long& operator[]( int idx ){ return edges[idx]; } const long long& operator[]( int idx ) const { return edges[idx]; } }; struct FaceEdges { IsoEdge edges[2]; int count; }; template< class Vertex > struct SliceValues { typename SortedTreeNodes::SliceTableData sliceData; Pointer( Real ) cornerValues ; Pointer( Point3D< Real > ) cornerGradients ; Pointer( char ) cornerSet; Pointer( long long ) edgeKeys ; Pointer( char ) edgeSet; Pointer( FaceEdges ) faceEdges ; Pointer( char ) faceSet; Pointer( char ) mcIndices; hash_map< long long , std::vector< IsoEdge > > faceEdgeMap; hash_map< long long , std::pair< int , Vertex > > edgeVertexMap; hash_map< long long , long long > vertexPairMap; SliceValues( void ); ~SliceValues( void ); void reset( bool nonLinearFit ); protected: int _oldCCount , _oldECount , _oldFCount , _oldNCount; }; template< class Vertex > struct XSliceValues { typename SortedTreeNodes::XSliceTableData xSliceData; Pointer( long long ) edgeKeys ; Pointer( char ) edgeSet; Pointer( FaceEdges ) faceEdges ; Pointer( char ) faceSet; hash_map< long long , std::vector< IsoEdge > > faceEdgeMap; hash_map< long long , std::pair< int , Vertex > > edgeVertexMap; hash_map< long long , long long > vertexPairMap; XSliceValues( void ); ~XSliceValues( void ); void reset( void ); protected: int _oldECount , _oldFCount; }; template< class Vertex > struct SlabValues { XSliceValues< Vertex > _xSliceValues[2]; SliceValues< Vertex > _sliceValues[2]; SliceValues< Vertex >& sliceValues( int idx ){ return _sliceValues[idx&1]; } const SliceValues< Vertex >& sliceValues( int idx ) const { return _sliceValues[idx&1]; } XSliceValues< Vertex >& xSliceValues( int idx ){ return _xSliceValues[idx&1]; } const XSliceValues< Vertex >& xSliceValues( int idx ) const { return _xSliceValues[idx&1]; } }; template< class Vertex , int FEMDegree > void SetSliceIsoCorners( const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& coarseSolution , Real isoValue , int depth , int slice , std::vector< SlabValues< Vertex > >& sValues , const _Evaluator< FEMDegree >& evaluator , int threads ); template< class Vertex , int FEMDegree > void SetSliceIsoCorners( const DenseNodeData< Real , FEMDegree >& solution , const DenseNodeData< Real , FEMDegree >& coarseSolution , Real isoValue , int depth , int slice , int z , std::vector< SlabValues< Vertex > >& sValues , const _Evaluator< FEMDegree >& evaluator , int threads ); template< int WeightDegree , int ColorDegree , class Vertex > void SetSliceIsoVertices( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , int depth , int slice , int& vOffset , CoredMeshData< Vertex >& mesh , std::vector< SlabValues< Vertex > >& sValues , int threads ); template< int WeightDegree , int ColorDegree , class Vertex > void SetSliceIsoVertices( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , int depth , int slice , int z , int& vOffset , CoredMeshData< Vertex >& mesh , std::vector< SlabValues< Vertex > >& sValues , int threads ); template< int WeightDegree , int ColorDegree , class Vertex > void SetXSliceIsoVertices( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , int depth , int slab , int& vOffset , CoredMeshData< Vertex >& mesh , std::vector< SlabValues< Vertex > >& sValues , int threads ); template< class Vertex > void CopyFinerSliceIsoEdgeKeys( int depth , int slice , std::vector< SlabValues< Vertex > >& sValues , int threads ); template< class Vertex > void CopyFinerSliceIsoEdgeKeys( int depth , int slice , int z , std::vector< SlabValues< Vertex > >& sValues , int threads ); template< class Vertex > void CopyFinerXSliceIsoEdgeKeys( int depth , int slab , std::vector< SlabValues< Vertex > >& sValues , int threads ); template< class Vertex > void SetSliceIsoEdges( int depth , int slice , std::vector< SlabValues< Vertex > >& slabValues , int threads ); template< class Vertex > void SetSliceIsoEdges( int depth , int slice , int z , std::vector< SlabValues< Vertex > >& slabValues , int threads ); template< class Vertex > void SetXSliceIsoEdges( int depth , int slice , std::vector< SlabValues< Vertex > >& slabValues , int threads ); template< class Vertex > void SetIsoSurface( int depth , int offset , const SliceValues< Vertex >& bValues , const SliceValues< Vertex >& fValues , const XSliceValues< Vertex >& xValues , CoredMeshData< Vertex >& mesh , bool polygonMesh , bool addBarycenter , int& vOffset , int threads ); template< class Vertex > static int AddIsoPolygons( CoredMeshData< Vertex >& mesh , std::vector< std::pair< int , Vertex > >& polygon , bool polygonMesh , bool addBarycenter , int& vOffset ); template< int WeightDegree , int ColorDegree , class Vertex > bool GetIsoVertex( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , ConstPointSupportKey< WeightDegree >& weightKey , ConstPointSupportKey< ColorDegree >& colorKey , const TreeOctNode* node , int edgeIndex , int z , const SliceValues< Vertex >& sValues , Vertex& vertex ); template< int WeightDegree , int ColorDegree , class Vertex > bool GetIsoVertex( const BSplineData< ColorDegree >* colorBSData , const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , Real isoValue , ConstPointSupportKey< WeightDegree >& weightKey , ConstPointSupportKey< ColorDegree >& colorKey , const TreeOctNode* node , int cornerIndex , const SliceValues< Vertex >& bValues , const SliceValues< Vertex >& fValues , Vertex& vertex ); public: static double maxMemoryUsage; int threads; static double MemoryUsage( void ); Octree( void ); // After calling set tree, the indices of the octree node will be stored by depth, and within depth they will be sorted by slice template< class PointReal , int NormalDegree , int WeightDegree , int DataDegree , class Data , class _Data > int SetTree( OrientedPointStream< PointReal >* pointStream , int minDepth , int maxDepth , int fullDepth , int splatDepth , Real samplesPerNode , Real scaleFactor , bool useConfidence , bool useNormalWeight , Real constraintWeight , int adaptiveExponent , SparseNodeData< Real , WeightDegree >& densityWeights , SparseNodeData< PointData< Real > , 0 >& pointInfo , SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo , SparseNodeData< Real , NormalDegree >& nodeWeights , SparseNodeData< ProjectiveData< _Data > , DataDegree >* dataValues , XForm4x4< Real >& xForm , bool dirichlet=false , bool makeComplete=false ); template< int FEMDegree > void EnableMultigrid( std::vector< int >* map ); template< int FEMDegree , int NormalDegree > DenseNodeData< Real , FEMDegree > SetLaplacianConstraints( const SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo ); template< int FEMDegree > DenseNodeData< Real , FEMDegree > SolveSystem( SparseNodeData< PointData< Real > , 0 >& pointInfo , DenseNodeData< Real , FEMDegree >& constraints , bool showResidual , int iters , int maxSolveDepth , int cgDepth=0 , double cgAccuracy=0 ); template< int FEMDegree , int NormalDegree > Real GetIsoValue( const DenseNodeData< Real , FEMDegree >& solution , const SparseNodeData< Real , NormalDegree >& nodeWeights ); template< int FEMDegree , int WeightDegree , int ColorDegree , class Vertex > void GetMCIsoSurface( const SparseNodeData< Real , WeightDegree >* densityWeights , const SparseNodeData< ProjectiveData< Point3D< Real > > , ColorDegree >* colorData , const DenseNodeData< Real , FEMDegree >& solution , Real isoValue , CoredMeshData< Vertex >& mesh , bool nonLinearFit=true , bool addBarycenter=false , bool polygonMesh=false ); const TreeOctNode& tree( void ) const{ return _tree; } size_t leaves( void ) const { return _tree.leaves(); } size_t nodes( void ) const { return _tree.nodes(); } }; template< class Real > void Reset( void ) { TreeNodeData::NodeCount=0; Octree< Real >::maxMemoryUsage = 0; } #include "MultiGridOctreeData.inl" #include "MultiGridOctreeData.SortedTreeNodes.inl" #include "MultiGridOctreeData.WeightedSamples.inl" #include "MultiGridOctreeData.System.inl" #include "MultiGridOctreeData.IsoSurface.inl" #include "MultiGridOctreeData.Evaluation.inl" #endif // MULTI_GRID_OCTREE_DATA_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/MultiGridOctreeData.inl000077500000000000000000000522061454702036400247560ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include "PointStream.h" #define ITERATION_POWER 1.0/3 #define MEMORY_ALLOCATOR_BLOCK_SIZE 1<<12 const double MATRIX_ENTRY_EPSILON = 0; const double EPSILON = 1e-6; const double ROUND_EPS = 1e-5; ////////////////// // TreeNodeData // ////////////////// size_t TreeNodeData::NodeCount = 0; TreeNodeData::TreeNodeData( void ){ nodeIndex = (int)NodeCount ; NodeCount++; } TreeNodeData::~TreeNodeData( void ) { } //////////// // Octree // //////////// template< class Real > double Octree< Real >::maxMemoryUsage=0; template< class Real > double Octree< Real >::MemoryUsage( void ) { double mem = double( MemoryInfo::Usage() ) / (1<<20); if( mem>maxMemoryUsage ) maxMemoryUsage=mem; return mem; } template< class Real > Octree< Real >::Octree( void ) { threads = 1; _constrainValues = false; _multigridDegree = 0; } template< class Real > template< int FEMDegree > void Octree< Real >::FunctionIndex( const TreeOctNode* node , int idx[3] ) { int d; node->depthAndOffset( d , idx ); for( int dd=0 ; dd::FunctionIndex( d-1 , idx[dd] ); } template< class Real > bool Octree< Real >::_InBounds( Point3D< Real > p ) const { return p[0]>=Real(0.) && p[0]<=Real(1.0) && p[1]>=Real(0.) && p[1]<=Real(1.0) && p[2]>=Real(0.) && p[2]<=Real(1.0); } template< class Real > template< int FEMDegree > bool Octree< Real >::IsValidNode( const TreeOctNode* node , bool dirichlet ) { if( !node || node->depth()<1 ) return false; int d , off[3]; node->depthAndOffset( d , off ); int dim = BSplineData< FEMDegree >::Dimension( d-1 ); if( FEMDegree&1 && dirichlet ) return !( off[0]<=0 || off[1]<=0 || off[2]<=0 || off[0]>=dim-1 || off[1]>=dim-1 || off[2]>=dim-1 ); else return !( off[0]< 0 || off[1]< 0 || off[2]< 0 || off[0]> dim-1 || off[1]> dim-1 || off[2]> dim-1 ); } template< class Real > void Octree< Real >::_SetFullDepth( TreeOctNode* node , int depth ) { if( node->depth()==0 || _Depth( node )children ) node->initChildren(); for( int c=0 ; cchildren+c , depth ); } } template< class Real > void Octree< Real >::_setFullDepth( int depth ) { if( !_tree.children ) _tree.initChildren(); for( int c=0 ; c template< class PointReal , int NormalDegree , int WeightDegree , int DataDegree , class Data , class _Data > int Octree< Real >::SetTree( OrientedPointStream< PointReal >* pointStream , int minDepth , int maxDepth , int fullDepth , int splatDepth , Real samplesPerNode , Real scaleFactor , bool useConfidence , bool useNormalWeights , Real constraintWeight , int adaptiveExponent , SparseNodeData< Real , WeightDegree >& densityWeights , SparseNodeData< PointData< Real > , 0 >& pointInfo , SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo , SparseNodeData< Real , NormalDegree >& nodeWeights , SparseNodeData< ProjectiveData< _Data > , DataDegree >* dataValues , XForm4x4< Real >& xForm , bool dirichlet , bool makeComplete ) { OrientedPointStreamWithData< PointReal , Data >* pointStreamWithData = ( OrientedPointStreamWithData< PointReal , Data >* )pointStream; _tree.initChildren() , _spaceRoot = _tree.children; splatDepth = std::max< int >( 0 , std::min< int >( splatDepth , maxDepth ) ); _dirichlet = dirichlet; _constrainValues = (constraintWeight>0); XForm3x3< Real > xFormN; for( int i=0 ; i<3 ; i++ ) for( int j=0 ; j<3 ; j++ ) xFormN(i,j) = xForm(i,j); xFormN = xFormN.transpose().inverse(); minDepth = std::max< int >( 0 , std::min< int >( minDepth , maxDepth ) ); // 0<=minDepth <= maxDepth fullDepth = std::max< int >( minDepth , std::min< int >( fullDepth , maxDepth ) ); // minDepth <= fullDepth <= maxDepth #if 0 // For Neumann constraints, the function at depth 0 is constant so the system matrix is zero if there is no screening. if( !_dirichlet && !_constrainValues ) minDepth = std::max< int >( minDepth , 1 ); #endif minDepth++ , maxDepth++; _minDepth = minDepth; _fullDepth = fullDepth; _splatDepth = splatDepth; double pointWeightSum = 0; int i , cnt=0; PointSupportKey< WeightDegree > weightKey; PointSupportKey< DataDegree > dataKey; PointSupportKey< NormalDegree > normalKey; weightKey.set( maxDepth ) , dataKey.set( maxDepth ) , normalKey.set( maxDepth ); _setFullDepth( _fullDepth ); // Read through once to get the center and scale { Point3D< Real > min , max; double t = Time(); Point3D< Real > p; OrientedPoint3D< PointReal > _p; while( pointStream->nextPoint( _p ) ) { p = xForm * Point3D< Real >(_p.p); for( i=0 ; imax[i] ) max[i] = p[i]; } cnt++; } _scale = std::max< Real >( max[0]-min[0] , std::max< Real >( max[1]-min[1] , max[2]-min[2] ) ); _center = ( max+min ) /2; } _scale *= scaleFactor; for( i=0 ; i trans = XForm4x4< Real >::Identity() , scale = XForm4x4< Real >::Identity(); for( int i=0 ; i<3 ; i++ ) scale(i,i) = (Real)(1./_scale ) , trans(3,i) = -_center[i]; xForm = scale * trans * xForm; } { double t = Time(); cnt = 0; pointStream->reset(); Point3D< Real > p , n; OrientedPoint3D< PointReal > _p; while( pointStream->nextPoint( _p ) ) { p = xForm * Point3D< Real >(_p.p) , n = xFormN * Point3D< Real >(_p.n); if( !_InBounds(p) ) continue; Point3D< Real > myCenter = Point3D< Real >( Real(0.5) , Real(0.5) , Real(0.5) ); Real myWidth = Real(1.0); Real weight=Real( 1. ); if( useConfidence ) weight = Real( Length(n) ); if( samplesPerNode>0 ) weight /= (Real)samplesPerNode; TreeOctNode* temp = _spaceRoot; int d=0; while( dchildren ) temp->initChildren(); int cIndex=TreeOctNode::CornerIndex( myCenter , p ); temp = temp->children + cIndex; myWidth/=2; if( cIndex&1 ) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if( cIndex&2 ) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if( cIndex&4 ) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; d++; } _AddWeightContribution( densityWeights , temp , p , weightKey , weight ); cnt++; } } std::vector< PointData< Real > >& points = pointInfo.data; cnt = 0; pointStream->reset(); Point3D< Real > p , n; OrientedPoint3D< PointReal > _p; Data _d; while( ( dataValues ? pointStreamWithData->nextPoint( _p , _d ) : pointStream->nextPoint( _p ) ) ) { p = xForm * Point3D< Real >(_p.p) , n = xFormN * Point3D< Real >(_p.n); n *= Real(-1.); if( !_InBounds(p) ) continue; Real normalLength = Real( Length( n ) ); if(std::isnan( normalLength ) || !std::isfinite( normalLength ) || normalLength<=EPSILON ) continue; if( !useConfidence ) n /= normalLength; Real pointWeight = Real(1.f); if( samplesPerNode>0 ) { if( dataValues ) _MultiSplatPointData( &densityWeights , p , ProjectiveData< _Data >( _Data( _d ) , (Real)1. ) , *dataValues , weightKey , dataKey , maxDepth-1 , 2 ); pointWeight = _SplatPointData( densityWeights , p , n , normalInfo , weightKey , normalKey , _minDepth-1 , maxDepth-1 , 3 ); } else { if( dataValues ) _MultiSplatPointData( ( SparseNodeData< Real , WeightDegree >* )NULL , p , ProjectiveData< _Data >( _Data( _d ) , (Real)1. ) , *dataValues , weightKey , dataKey , maxDepth-1 , 2 ); Point3D< Real > myCenter = Point3D< Real >( Real(0.5) , Real(0.5) , Real(0.5) ); Real myWidth = Real(1.0); TreeOctNode* temp = _spaceRoot; int d=0; if( splatDepth ) { while( dchildren[cIndex]; myWidth /= 2; if(cIndex&1) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if(cIndex&2) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if(cIndex&4) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; d++; } pointWeight = (Real)1.0/_GetSamplesPerNode( densityWeights , temp , p , weightKey ); } for( i=0 ; ichildren ) temp->initChildren(); int cIndex=TreeOctNode::CornerIndex( myCenter , p ); temp=&temp->children[cIndex]; myWidth/=2; if(cIndex&1) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if(cIndex&2) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if(cIndex&4) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; d++; } _SplatPointData( temp , p , n , normalInfo , normalKey ); } pointWeightSum += pointWeight; if( _constrainValues ) { Real pointScreeningWeight = useNormalWeights ? Real( normalLength ) : Real(1.f); TreeOctNode* temp = _spaceRoot; Point3D< Real > myCenter = Point3D< Real >( Real(0.5) , Real(0.5) , Real(0.5) ); Real myWidth = Real(1.0); while( 1 ) { if( (int)pointInfo.indices.size()( p*pointScreeningWeight , pointScreeningWeight ) ); pointInfo.indices[ temp->nodeData.nodeIndex ] = idx; } else { points[idx].position += p*pointScreeningWeight; points[idx].weight += pointScreeningWeight; } int cIndex = TreeOctNode::CornerIndex( myCenter , p ); if( !temp->children ) break; temp = &temp->children[cIndex]; myWidth /= 2; if( cIndex&1 ) myCenter[0] += myWidth/2; else myCenter[0] -= myWidth/2; if( cIndex&2 ) myCenter[1] += myWidth/2; else myCenter[1] -= myWidth/2; if( cIndex&4 ) myCenter[2] += myWidth/2; else myCenter[2] -= myWidth/2; } } cnt++; } constraintWeight *= Real( pointWeightSum ); constraintWeight /= cnt; MemoryUsage( ); if( _constrainValues ) // Set the average position and scale the weights for( TreeOctNode* node=_tree.nextNode() ; node ; node=_tree.nextNode(node) ) if( pointInfo.index( node )!=-1 ) { int idx = pointInfo.index( node ); points[idx].position /= points[idx].weight; int e = _Depth( node ) * adaptiveExponent - ( maxDepth - 1 ) * (adaptiveExponent-1); if( e<0 ) points[idx].weight /= Real( 1<<(-e) ); else points[idx].weight *= Real( 1<< e ); points[idx].weight *= Real( constraintWeight ); } #if FORCE_NEUMANN_FIELD // #pragma message( "[WARNING] This is likely wrong as it only forces the normal component of the coefficient to be zero, not the actual vector-field" ) if( !_dirichlet ) for( TreeOctNode* node=_tree.nextNode() ; node ; node=_tree.nextNode( node ) ) { int d , off[3] , res; node->depthAndOffset( d , off ); res = 1<& normal = normalInfo.data[ idx ]; for( int d=0 ; d<3 ; d++ ) if( off[d]==0 || off[d]==res-1 ) normal[d] = 0; } #endif // FORCE_NEUMANN_FIELD nodeWeights.resize( TreeNodeData::NodeCount ); // Set the point weights for evaluating the iso-value for( TreeOctNode* node=_tree.nextNode() ; node ; node=_tree.nextNode(node) ) { int nIdx = normalInfo.index( node ); if( nIdx>=0 ) { Real l = Real( Length( normalInfo.data[ nIdx ] ) ); if( l ) { int nIdx = nodeWeights.index( node ); if( nIdx<0 ) { nodeWeights.indices[ node->nodeData.nodeIndex ] = (int)nodeWeights.data.size(); nodeWeights.data.push_back( l ); } else nodeWeights.data[ nIdx ] = l; } } } MemoryUsage(); if( makeComplete ) _MakeComplete( ); else _ClipTree< NormalDegree >( normalInfo ); _maxDepth = _tree.maxDepth(); return cnt; } template< class Real > void Octree< Real >::_SetValidityFlags( void ) { for( int i=0 ; i<_sNodes.end( _sNodes.levels()-1 ) ; i++ ) { _sNodes.treeNodes[i]->nodeData.flags = 0; if( IsValidNode< 0 >( _sNodes.treeNodes[i] , _dirichlet ) ) _sNodes.treeNodes[i]->nodeData.flags |= (1<<0); if( IsValidNode< 1 >( _sNodes.treeNodes[i] , _dirichlet ) ) _sNodes.treeNodes[i]->nodeData.flags |= (1<<1); } } template< class Real > void Octree< Real >::_MakeComplete( void ){ _tree.setFullDepth( _spaceRoot->maxDepth() ) ; MemoryUsage(); } // Trim off the branches of the tree (finer than _fullDepth) that don't contain normal points template< class Real > template< int NormalDegree > void Octree< Real >::_ClipTree( const SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo ) { #if NEW_NEW_CODE #define ABS_INDEX( idx ) ( (idx<0) ? -(idx) : (idx) ) static const int SupportSize = BSplineEvaluationData< NormalDegree >::SupportSize; static const int LeftSupportRadius = -BSplineEvaluationData< NormalDegree >::SupportStart; static const int RightSupportRadius = BSplineEvaluationData< NormalDegree >::SupportEnd; int maxDepth = _tree.maxDepth(); typename TreeOctNode::NeighborKey< LeftSupportRadius , RightSupportRadius > neighborKey; neighborKey.set( maxDepth ); // Set all nodes to invalid (negative indices) for( TreeOctNode* node=_tree.nextNode() ; node ; node=_tree.nextNode(node) ) node->nodeData.nodeIndex = -node->nodeData.nodeIndex; // Iterate over the nodes and, if they contain a normal, make sure that the supported nodes exist and are set to valid for( TreeOctNode* node=_tree.nextNode() ; node ; node=_tree.nextNode(node) ) if( normalInfo.index( ABS_INDEX( node->nodeData.nodeIndex ) )>=0 ) { int depth = node->depth(); neighborKey.getNeighbors< true >( node ); for( int d=0 ; d<=depth ; d++ ) { TreeOctNode::template Neighbors< SupportSize >& neighbors = neighborKey.neighbors[d]; for( int i=0 ; inodeData.nodeIndex = ABS_INDEX( neighbors.neighbors[i][j][k]->nodeData.nodeIndex ); } } // Remove the invalid nodes for( TreeOctNode* node=_tree.nextNode() ; node ; node=_tree.nextNode( node ) ) { if( node->children && _Depth(node)>=_fullDepth ) { bool hasValidChildren = false; for( int c=0 ; cchildren[c].nodeData.nodeIndex>0 ); if( !hasValidChildren ) node->children = NULL; } node->nodeData.nodeIndex = ABS_INDEX( node->nodeData.nodeIndex ); } MemoryUsage(); #undef ABS_INDEX #else // !NEW_NEW_CODE int maxDepth = _tree.maxDepth(); for( TreeOctNode* temp=_tree.nextNode() ; temp ; temp=_tree.nextNode(temp) ) if( temp->children && _Depth( temp )>=_fullDepth ) { int hasNormals=0; for( int i=0 ; ichildren[i] , normalInfo ); if( !hasNormals ) temp->children=NULL; } MemoryUsage(); #endif // NEW_NEW_CODE } template< class Real > template< int FEMDegree > void Octree< Real >::EnableMultigrid( std::vector< int >* map ) { if( FEMDegree<=_multigridDegree ) return; _multigridDegree = FEMDegree; const int OverlapRadius = -BSplineIntegrationData< FEMDegree , FEMDegree >::OverlapStart; int maxDepth = _tree.maxDepth( ); typename TreeOctNode::NeighborKey< OverlapRadius , OverlapRadius > neighborKey; neighborKey.set( maxDepth-1 ); for( int d=maxDepth-1 ; d>=0 ; d-- ) for( TreeOctNode* node=_tree.nextNode() ; node ; node=_tree.nextNode( node ) ) if( node->depth()==d && node->children ) neighborKey.template getNeighbors< true >( node ); _sNodes.set( _tree , map ); _SetValidityFlags(); } template< class Real > template< int NormalDegree > int Octree< Real >::_HasNormals( TreeOctNode* node , const SparseNodeData< Point3D< Real > , NormalDegree >& normalInfo ) { int idx = normalInfo.index( node ); if( idx>=0 ) { const Point3D< Real >& normal = normalInfo.data[ idx ]; if( normal[0]!=0 || normal[1]!=0 || normal[2]!=0 ) return 1; } if( node->children ) for( int i=0 ; ichildren[i] , normalInfo ) ) return 1; return 0; } //////////////// // VertexData // //////////////// long long VertexData::CenterIndex(const TreeOctNode* node,int maxDepth) { int idx[DIMENSION]; return CenterIndex(node,maxDepth,idx); } long long VertexData::CenterIndex(const TreeOctNode* node,int maxDepth,int idx[DIMENSION]) { int d,o[3]; node->depthAndOffset(d,o); for(int i=0;idepthAndOffset( d , o ); for( int i=0 ; idepthAndOffset(d,o); for(int i=0;idepthAndOffset( d ,off ); Cube::FactorEdgeIndex( eIndex , o , i1 , i2 ); for( int i=0 ; i #include #ifndef _WIN32 #include #endif // _WIN32 inline double Time( void ) { #ifdef _WIN32 struct _timeb t; _ftime( &t ); return double( t.time ) + double( t.millitm ) / 1000.0; #else // _WIN32 struct timeval t; gettimeofday( &t , NULL ); return t.tv_sec + double( t.tv_usec ) / 1000000; #endif // _WIN32 } #endif // MY_TIME_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/Octree.h000066400000000000000000000152641454702036400220100ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #ifndef OCT_NODE_INCLUDED #define OCT_NODE_INCLUDED #include "Allocator.h" #include "BinaryNode.h" #include "MarchingCubes.h" #define NEW_OCTNODE_CODE 1 #define DIMENSION 3 template< class NodeData > class OctNode { private: static int UseAlloc; unsigned long long _depthAndOffset; const OctNode* __faceNeighbor( int dir , int off ) const; const OctNode* __edgeNeighbor( int o , const int i[2] , const int idx[2] ) const; OctNode* __faceNeighbor( int dir , int off , int forceChildren ); OctNode* __edgeNeighbor( int o , const int i[2] , const int idx[2] , int forceChildren); public: static const int DepthShift , OffsetShift , OffsetShift1 , OffsetShift2 , OffsetShift3; static const int DepthMask , OffsetMask; static Allocator< OctNode > NodeAllocator; static int UseAllocator( void ); static void SetAllocator( int blockSize ); OctNode* parent; OctNode* children; NodeData nodeData; OctNode( void ); ~OctNode( void ); int initChildren( void ); void depthAndOffset( int& depth , int offset[DIMENSION] ) const; void centerIndex( int index[DIMENSION] ) const; int depth( void ) const; static inline void DepthAndOffset( const long long& index , int& depth , int offset[DIMENSION] ); template< class Real > static inline void CenterAndWidth( const long long& index , Point3D< Real >& center , Real& width ); template< class Real > static inline void StartAndWidth( const long long& index , Point3D< Real >& start , Real& width ); static inline int Depth( const long long& index ); static inline void Index( int depth , const int offset[3] , short& d , short off[DIMENSION] ); static inline unsigned long long Index( int depth , const int offset[3] ); template< class Real > void centerAndWidth( Point3D& center , Real& width ) const; template< class Real > void startAndWidth( Point3D< Real >& start , Real& width ) const; template< class Real > bool isInside( Point3D< Real > p ) const; size_t leaves( void ) const; size_t maxDepthLeaves( int maxDepth ) const; size_t nodes( void ) const; int maxDepth( void ) const; const OctNode* root( void ) const; const OctNode* nextLeaf( const OctNode* currentLeaf=NULL ) const; OctNode* nextLeaf( OctNode* currentLeaf=NULL ); const OctNode* nextNode( const OctNode* currentNode=NULL ) const; OctNode* nextNode( OctNode* currentNode=NULL ); const OctNode* nextBranch( const OctNode* current ) const; OctNode* nextBranch( OctNode* current ); const OctNode* prevBranch( const OctNode* current ) const; OctNode* prevBranch( OctNode* current ); void setFullDepth( int maxDepth ); void printLeaves( void ) const; void printRange( void ) const; template< class Real > static int CornerIndex( const Point3D& center , const Point3D &p ); OctNode* faceNeighbor( int faceIndex , int forceChildren=0 ); const OctNode* faceNeighbor( int faceIndex ) const; OctNode* edgeNeighbor( int edgeIndex , int forceChildren=0 ); const OctNode* edgeNeighbor( int edgeIndex ) const; OctNode* cornerNeighbor( int cornerIndex , int forceChildren=0 ); const OctNode* cornerNeighbor( int cornerIndex ) const; int write( const char* fileName ) const; int write( FILE* fp ) const; int read( const char* fileName ); int read( FILE* fp ); template< unsigned int Width > struct Neighbors { OctNode* neighbors[Width][Width][Width]; Neighbors( void ); void clear( void ); }; template< unsigned int Width > struct ConstNeighbors { const OctNode* neighbors[Width][Width][Width]; ConstNeighbors( void ); void clear( void ); }; template< unsigned int LeftRadius , unsigned int RightRadius > class NeighborKey { int _depth; public: static const int Width = LeftRadius + RightRadius + 1; Neighbors< Width >* neighbors; NeighborKey( void ); NeighborKey( const NeighborKey& key ); ~NeighborKey( void ); int depth( void ) const { return _depth; } void set( int depth ); template< bool CreateNodes > typename OctNode< NodeData >::template Neighbors< LeftRadius+RightRadius+1 >& getNeighbors( OctNode* node ); template< bool CreateNodes , unsigned int _LeftRadius , unsigned int _RightRadius > void getNeighbors( OctNode* node , Neighbors< _LeftRadius + _RightRadius + 1 >& neighbors ); template< bool CreateNodes > bool getChildNeighbors( int cIdx , int d , Neighbors< Width >& childNeighbors ) const; template< bool CreateNodes , class Real > bool getChildNeighbors( Point3D< Real > p , int d , Neighbors< Width >& childNeighbors ) const; }; template< unsigned int LeftRadius , unsigned int RightRadius > class ConstNeighborKey { int _depth; public: static const int Width = LeftRadius + RightRadius + 1; ConstNeighbors* neighbors; ConstNeighborKey( void ); ConstNeighborKey( const ConstNeighborKey& key ); ~ConstNeighborKey( void ); int depth( void ) const { return _depth; } void set( int depth ); typename OctNode< NodeData >::template ConstNeighbors< LeftRadius+RightRadius+1 >& getNeighbors( const OctNode* node ); template< unsigned int _LeftRadius , unsigned int _RightRadius > void getNeighbors( const OctNode* node , ConstNeighbors< _LeftRadius + _RightRadius + 1 >& neighbors ); }; void centerIndex( int maxDepth , int index[DIMENSION] ) const; int width( int maxDepth ) const; }; #include "Octree.inl" #endif // OCT_NODE_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/Octree.inl000077500000000000000000001201141454702036400223350ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include #include #include ///////////// // OctNode // ///////////// template< class NodeData > const int OctNode< NodeData >::DepthShift=5; template< class NodeData > const int OctNode< NodeData >::OffsetShift = ( sizeof(long long)*8 - DepthShift ) / 3; template< class NodeData > const int OctNode< NodeData >::DepthMask=(1< const int OctNode< NodeData >::OffsetMask=(1< const int OctNode< NodeData >::OffsetShift1=DepthShift; template< class NodeData > const int OctNode< NodeData >::OffsetShift2=OffsetShift1+OffsetShift; template< class NodeData > const int OctNode< NodeData >::OffsetShift3=OffsetShift2+OffsetShift; template< class NodeData > int OctNode< NodeData >::UseAlloc=0; template< class NodeData > Allocator > OctNode< NodeData >::NodeAllocator; template< class NodeData > void OctNode< NodeData >::SetAllocator(int blockSize) { if(blockSize>0) { UseAlloc=1; NodeAllocator.set(blockSize); } else{UseAlloc=0;} } template< class NodeData > int OctNode< NodeData >::UseAllocator(void){return UseAlloc;} template< class NodeData > OctNode< NodeData >::OctNode(void){ parent=children=NULL; _depthAndOffset = 0; } template< class NodeData > OctNode< NodeData >::~OctNode(void){ if(!UseAlloc){if(children){delete[] children;}} parent=children=NULL; } template< class NodeData > void OctNode< NodeData >::setFullDepth( int maxDepth ) { if( maxDepth ) { if( !children ) initChildren(); for( int i=0 ; i<8 ; i++ ) children[i].setFullDepth( maxDepth-1 ); } } template< class NodeData > int OctNode< NodeData >::initChildren( void ) { if( UseAlloc ) children=NodeAllocator.newElements(8); else { if( children ) delete[] children; children = NULL; children = new OctNode[Cube::CORNERS]; } if( !children ) { fprintf(stderr,"Failed to initialize children in OctNode::initChildren\n"); exit(0); return 0; } int d , off[3]; depthAndOffset( d , off ); for( int i=0 ; i<2 ; i++ ) for( int j=0 ; j<2 ; j++ ) for( int k=0 ; k<2 ; k++ ) { int idx=Cube::CornerIndex(i,j,k); children[idx].parent = this; children[idx].children = NULL; int off2[3]; off2[0] = (off[0]<<1)+i; off2[1] = (off[1]<<1)+j; off2[2] = (off[2]<<1)+k; children[idx]._depthAndOffset = Index( d+1 , off2 ); } return 1; } template< class NodeData > inline void OctNode< NodeData >::Index(int depth,const int offset[3],short& d,short off[3]){ d=short(depth); off[0]=short((1< inline void OctNode< NodeData >::depthAndOffset( int& depth , int offset[DIMENSION] ) const { depth = int( _depthAndOffset & DepthMask ); offset[0] = int( (_depthAndOffset>>OffsetShift1) & OffsetMask ); offset[1] = int( (_depthAndOffset>>OffsetShift2) & OffsetMask ); offset[2] = int( (_depthAndOffset>>OffsetShift3) & OffsetMask ); } template< class NodeData > inline void OctNode< NodeData >::centerIndex( int index[DIMENSION] ) const { int d , off[DIMENSION]; depthAndOffset( d , off ); for( int i=0 ; i inline unsigned long long OctNode< NodeData >::Index( int depth , const int offset[3] ) { unsigned long long idx=0; idx |= ( ( (unsigned long long)(depth ) ) & DepthMask ); idx |= ( ( (unsigned long long)(offset[0]) ) & OffsetMask ) << OffsetShift1; idx |= ( ( (unsigned long long)(offset[1]) ) & OffsetMask ) << OffsetShift2; idx |= ( ( (unsigned long long)(offset[2]) ) & OffsetMask ) << OffsetShift3; return idx; } template< class NodeData > inline int OctNode< NodeData >::depth( void ) const {return int( _depthAndOffset & DepthMask );} template< class NodeData > inline void OctNode< NodeData >::DepthAndOffset(const long long& index,int& depth,int offset[3]){ depth=int(index&DepthMask); offset[0]=(int((index>>OffsetShift1)&OffsetMask)+1)&(~(1<>OffsetShift2)&OffsetMask)+1)&(~(1<>OffsetShift3)&OffsetMask)+1)&(~(1< inline int OctNode< NodeData >::Depth(const long long& index){return int(index&DepthMask);} template< class NodeData > template< class Real > void OctNode< NodeData >::centerAndWidth( Point3D& center , Real& width ) const { int depth , offset[3]; depthAndOffset( depth , offset ); width = Real( 1.0 / (1< template< class Real > void OctNode< NodeData >::startAndWidth( Point3D& start , Real& width ) const { int depth , offset[3]; depthAndOffset( depth , offset ); width = Real( 1.0 / (1< template< class Real > bool OctNode< NodeData >::isInside( Point3D< Real > p ) const { Point3D< Real > c; Real w; centerAndWidth( c , w ); w /= 2; return (c[0]-w) template< class Real > inline void OctNode< NodeData >::CenterAndWidth(const long long& index,Point3D& center,Real& width){ int depth,offset[3]; depth=index&DepthMask; offset[0]=(int((index>>OffsetShift1)&OffsetMask)+1)&(~(1<>OffsetShift2)&OffsetMask)+1)&(~(1<>OffsetShift3)&OffsetMask)+1)&(~(1< template< class Real > inline void OctNode< NodeData >::StartAndWidth( const long long& index , Point3D< Real >& start , Real& width ) { int depth,offset[3]; depth = index&DepthMask; offset[0] = (int((index>>OffsetShift1)&OffsetMask)+1)&(~(1<>OffsetShift2)&OffsetMask)+1)&(~(1<>OffsetShift3)&OffsetMask)+1)&(~(1< int OctNode< NodeData >::maxDepth(void) const{ if(!children){return 0;} else{ int c,d; for(int i=0;ic){c=d;} } return c+1; } } template< class NodeData > size_t OctNode< NodeData >::nodes( void ) const { if( !children ) return 1; else { size_t c=0; for( int i=0 ; i size_t OctNode< NodeData >::leaves( void ) const { if( !children ) return 1; else { size_t c=0; for( int i=0 ; i size_t OctNode< NodeData >::maxDepthLeaves( int maxDepth ) const { if( depth()>maxDepth ) return 0; if( !children ) return 1; else { size_t c=0; for( int i=0 ; i const OctNode< NodeData >* OctNode< NodeData >::root(void) const{ const OctNode* temp=this; while(temp->parent){temp=temp->parent;} return temp; } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::nextBranch( const OctNode* current ) const { if( !current->parent || current==this ) return NULL; if(current-current->parent->children==Cube::CORNERS-1) return nextBranch( current->parent ); else return current+1; } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::nextBranch(OctNode* current){ if(!current->parent || current==this){return NULL;} if(current-current->parent->children==Cube::CORNERS-1){return nextBranch(current->parent);} else{return current+1;} } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::prevBranch( const OctNode* current ) const { if( !current->parent || current==this ) return NULL; if( current-current->parent->children==0 ) return prevBranch( current->parent ); else return current-1; } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::prevBranch( OctNode* current ) { if( !current->parent || current==this ) return NULL; if( current-current->parent->children==0 ) return prevBranch( current->parent ); else return current-1; } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::nextLeaf(const OctNode* current) const{ if(!current){ const OctNode< NodeData >* temp=this; while(temp->children){temp=&temp->children[0];} return temp; } if(current->children){return current->nextLeaf();} const OctNode* temp=nextBranch(current); if(!temp){return NULL;} else{return temp->nextLeaf();} } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::nextLeaf(OctNode* current){ if(!current){ OctNode< NodeData >* temp=this; while(temp->children){temp=&temp->children[0];} return temp; } if(current->children){return current->nextLeaf();} OctNode* temp=nextBranch(current); if(!temp){return NULL;} else{return temp->nextLeaf();} } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::nextNode( const OctNode* current ) const { if( !current ) return this; else if( current->children ) return ¤t->children[0]; else return nextBranch(current); } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::nextNode( OctNode* current ) { if( !current ) return this; else if( current->children ) return ¤t->children[0]; else return nextBranch( current ); } template< class NodeData > void OctNode< NodeData >::printRange(void) const { Point3D< float > center; float width; centerAndWidth(center,width); for(int dim=0;dim template< class Real > int OctNode< NodeData >::CornerIndex(const Point3D& center,const Point3D& p){ int cIndex=0; if(p.coords[0]>center.coords[0]){cIndex|=1;} if(p.coords[1]>center.coords[1]){cIndex|=2;} if(p.coords[2]>center.coords[2]){cIndex|=4;} return cIndex; } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::faceNeighbor(int faceIndex,int forceChildren){return __faceNeighbor(faceIndex>>1,faceIndex&1,forceChildren);} template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::faceNeighbor(int faceIndex) const {return __faceNeighbor(faceIndex>>1,faceIndex&1);} template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::__faceNeighbor(int dir,int off,int forceChildren){ if(!parent){return NULL;} int pIndex=int(this-parent->children); pIndex^=(1<children[pIndex];} else{ OctNode* temp=parent->__faceNeighbor(dir,off,forceChildren); if(!temp){return NULL;} if(!temp->children){ if(forceChildren){temp->initChildren();} else{return temp;} } return &temp->children[pIndex]; } } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::__faceNeighbor(int dir,int off) const { if(!parent){return NULL;} int pIndex=int(this-parent->children); pIndex^=(1<children[pIndex];} else{ const OctNode* temp=parent->__faceNeighbor(dir,off); if(!temp || !temp->children){return temp;} else{return &temp->children[pIndex];} } } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::edgeNeighbor(int edgeIndex,int forceChildren){ int idx[2],o,i[2]; Cube::FactorEdgeIndex(edgeIndex,o,i[0],i[1]); switch(o){ case 0: idx[0]=1; idx[1]=2; break; case 1: idx[0]=0; idx[1]=2; break; case 2: idx[0]=0; idx[1]=1; break; }; return __edgeNeighbor(o,i,idx,forceChildren); } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::edgeNeighbor(int edgeIndex) const { int idx[2],o,i[2]; Cube::FactorEdgeIndex(edgeIndex,o,i[0],i[1]); switch(o){ case 0: idx[0]=1; idx[1]=2; break; case 1: idx[0]=0; idx[1]=2; break; case 2: idx[0]=0; idx[1]=1; break; }; return __edgeNeighbor(o,i,idx); } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::__edgeNeighbor(int o,const int i[2],const int idx[2]) const{ if(!parent){return NULL;} int pIndex=int(this-parent->children); int aIndex,x[DIMENSION]; Cube::FactorCornerIndex(pIndex,x[0],x[1],x[2]); aIndex=(~((i[0] ^ x[idx[0]]) | ((i[1] ^ x[idx[1]])<<1))) & 3; pIndex^=(7 ^ (1<__faceNeighbor(idx[0],i[0]); if(!temp || !temp->children){return NULL;} else{return &temp->children[pIndex];} } else if(aIndex==2) { // I can get the neighbor from the parent's face adjacent neighbor const OctNode* temp=parent->__faceNeighbor(idx[1],i[1]); if(!temp || !temp->children){return NULL;} else{return &temp->children[pIndex];} } else if(aIndex==0) { // I can get the neighbor from the parent return &parent->children[pIndex]; } else if(aIndex==3) { // I can get the neighbor from the parent's edge adjacent neighbor const OctNode* temp=parent->__edgeNeighbor(o,i,idx); if(!temp || !temp->children){return temp;} else{return &temp->children[pIndex];} } else{return NULL;} } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::__edgeNeighbor(int o,const int i[2],const int idx[2],int forceChildren){ if(!parent){return NULL;} int pIndex=int(this-parent->children); int aIndex,x[DIMENSION]; Cube::FactorCornerIndex(pIndex,x[0],x[1],x[2]); aIndex=(~((i[0] ^ x[idx[0]]) | ((i[1] ^ x[idx[1]])<<1))) & 3; pIndex^=(7 ^ (1<__faceNeighbor(idx[0],i[0],0); if(!temp || !temp->children){return NULL;} else{return &temp->children[pIndex];} } else if(aIndex==2) { // I can get the neighbor from the parent's face adjacent neighbor OctNode* temp=parent->__faceNeighbor(idx[1],i[1],0); if(!temp || !temp->children){return NULL;} else{return &temp->children[pIndex];} } else if(aIndex==0) { // I can get the neighbor from the parent return &parent->children[pIndex]; } else if(aIndex==3) { // I can get the neighbor from the parent's edge adjacent neighbor OctNode* temp=parent->__edgeNeighbor(o,i,idx,forceChildren); if(!temp){return NULL;} if(!temp->children){ if(forceChildren){temp->initChildren();} else{return temp;} } return &temp->children[pIndex]; } else{return NULL;} } template< class NodeData > const OctNode< NodeData >* OctNode< NodeData >::cornerNeighbor(int cornerIndex) const { int pIndex,aIndex=0; if(!parent){return NULL;} pIndex=int(this-parent->children); aIndex=(cornerIndex ^ pIndex); // The disagreement bits pIndex=(~pIndex)&7; // The antipodal point if(aIndex==7){ // Agree on no bits return &parent->children[pIndex]; } else if(aIndex==0){ // Agree on all bits const OctNode* temp=((const OctNode*)parent)->cornerNeighbor(cornerIndex); if(!temp || !temp->children){return temp;} else{return &temp->children[pIndex];} } else if(aIndex==6){ // Agree on face 0 const OctNode* temp=((const OctNode*)parent)->__faceNeighbor(0,cornerIndex & 1); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==5){ // Agree on face 1 const OctNode* temp=((const OctNode*)parent)->__faceNeighbor(1,(cornerIndex & 2)>>1); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==3){ // Agree on face 2 const OctNode* temp=((const OctNode*)parent)->__faceNeighbor(2,(cornerIndex & 4)>>2); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==4){ // Agree on edge 2 const OctNode* temp=((const OctNode*)parent)->edgeNeighbor(8 | (cornerIndex & 1) | (cornerIndex & 2) ); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==2){ // Agree on edge 1 const OctNode* temp=((const OctNode*)parent)->edgeNeighbor(4 | (cornerIndex & 1) | ((cornerIndex & 4)>>1) ); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==1){ // Agree on edge 0 const OctNode* temp=((const OctNode*)parent)->edgeNeighbor(((cornerIndex & 2) | (cornerIndex & 4))>>1 ); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else{return NULL;} } template< class NodeData > OctNode< NodeData >* OctNode< NodeData >::cornerNeighbor(int cornerIndex,int forceChildren){ int pIndex,aIndex=0; if(!parent){return NULL;} pIndex=int(this-parent->children); aIndex=(cornerIndex ^ pIndex); // The disagreement bits pIndex=(~pIndex)&7; // The antipodal point if(aIndex==7){ // Agree on no bits return &parent->children[pIndex]; } else if(aIndex==0){ // Agree on all bits OctNode* temp=((OctNode*)parent)->cornerNeighbor(cornerIndex,forceChildren); if(!temp){return NULL;} if(!temp->children){ if(forceChildren){temp->initChildren();} else{return temp;} } return &temp->children[pIndex]; } else if(aIndex==6){ // Agree on face 0 OctNode* temp=((OctNode*)parent)->__faceNeighbor(0,cornerIndex & 1,0); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==5){ // Agree on face 1 OctNode* temp=((OctNode*)parent)->__faceNeighbor(1,(cornerIndex & 2)>>1,0); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==3){ // Agree on face 2 OctNode* temp=((OctNode*)parent)->__faceNeighbor(2,(cornerIndex & 4)>>2,0); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==4){ // Agree on edge 2 OctNode* temp=((OctNode*)parent)->edgeNeighbor(8 | (cornerIndex & 1) | (cornerIndex & 2) ); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==2){ // Agree on edge 1 OctNode* temp=((OctNode*)parent)->edgeNeighbor(4 | (cornerIndex & 1) | ((cornerIndex & 4)>>1) ); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else if(aIndex==1){ // Agree on edge 0 OctNode* temp=((OctNode*)parent)->edgeNeighbor(((cornerIndex & 2) | (cornerIndex & 4))>>1 ); if(!temp || !temp->children){return NULL;} else{return & temp->children[pIndex];} } else{return NULL;} } //////////////////////// // OctNode::Neighbors // //////////////////////// template< class NodeData > template< unsigned int Width > OctNode< NodeData >::Neighbors< Width >::Neighbors( void ){ clear(); } template< class NodeData > template< unsigned int Width > void OctNode< NodeData >::Neighbors< Width >::clear( void ){ for( int i=0 ; i template< unsigned int Width > OctNode< NodeData >::ConstNeighbors< Width >::ConstNeighbors( void ){ clear(); } template< class NodeData > template< unsigned int Width > void OctNode< NodeData >::ConstNeighbors< Width >::clear( void ){ for( int i=0 ; i template< unsigned int LeftRadius , unsigned int RightRadius > OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::NeighborKey( void ){ _depth=-1 , neighbors=NULL; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::NeighborKey( const NeighborKey& nKey ) { _depth = 0 , neighbors = NULL; set( nKey._depth ); for( int d=0 ; d<=_depth ; d++ ) memcpy( &neighbors[d] , &nKey.neighbors[d] , sizeof( Neighbors< Width > ) ); } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::~NeighborKey( void ) { if( neighbors ) delete[] neighbors; neighbors = NULL; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > void OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::set( int d ) { if( neighbors ) delete[] neighbors; neighbors = NULL; _depth = d; if( d<0 ) return; neighbors = new Neighbors< Width >[d+1]; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > template< bool CreateNodes > bool OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::getChildNeighbors( int cIdx , int d , Neighbors< Width >& cNeighbors ) const { Neighbors< Width >& pNeighbors = neighbors[d]; // Check that we actuall have a center node if( !pNeighbors.neighbors[LeftRadius][LeftRadius][LeftRadius] ) return false; // Get the indices of the child node that would contain the point (and its antipode) int cx , cy , cz; Cube::FactorCornerIndex( cIdx , cx , cy , cz ); // Iterate over the finer neighbors and set them (if you can) // Here: // (x,y,z) give the position of the finer nodes relative to the center, // (_x,_y,_z) give a positive global position, up to an even offset, and // (px-LeftRadius,py-LeftRadius,pz-LeftRadius) give the positions of their parents relative to the parent of the center for( int z=-(int)LeftRadius ; z<=(int)RightRadius ; z++ ) { int _z = (z+cz) + (LeftRadius<<1) , pz = ( _z>>1 ) , zz = z+LeftRadius; for( int y=-(int)LeftRadius ; y<=(int)RightRadius ; y++ ) { int _y = (y+cy) + (LeftRadius<<1) , py = ( _y>>1 ) , yy = y+LeftRadius; int cornerIndex = ( (_z&1)<<2 ) | ( (_y&1)<<1 ); for( int x=-(int)LeftRadius ; x<=(int)RightRadius ; x++ ) { int _x = (x+cx) + (LeftRadius<<1) , px = ( _x>>1 ) , xx = x+LeftRadius; if( CreateNodes ) { if( pNeighbors.neighbors[px][py][pz] ) { if( !pNeighbors.neighbors[px][py][pz]->children ) pNeighbors.neighbors[px][py][pz]->initChildren(); cNeighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); } else cNeighbors.neighbors[xx][yy][zz] = NULL; } else { if( pNeighbors.neighbors[px][py][pz] && pNeighbors.neighbors[px][py][pz]->children ) cNeighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); else cNeighbors.neighbors[xx][yy][zz] = NULL; } } } } return true; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > template< bool CreateNodes , class Real > bool OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::getChildNeighbors( Point3D< Real > p , int d , Neighbors< Width >& cNeighbors ) const { Neighbors< Width >& pNeighbors = neighbors[d]; // Check that we actuall have a center node if( !pNeighbors.neighbors[LeftRadius][LeftRadius][LeftRadius] ) return false; Point3D< Real > c; Real w; pNeighbors.neighbors[LeftRadius][LeftRadius][LeftRadius]->centerAndWidth( c , w ); return getChildNeighbors< CreateNodes >( CornerIndex( c , p ) , d , cNeighbors ); } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > template< bool CreateNodes > typename OctNode< NodeData >::template Neighbors< LeftRadius+RightRadius+1 >& OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::getNeighbors( OctNode< NodeData >* node ) { Neighbors< Width >& neighbors = this->neighbors[ node->depth() ]; if( node==neighbors.neighbors[LeftRadius][LeftRadius][LeftRadius] ) { bool reset = false; for( int i=0 ; iparent ) neighbors.neighbors[LeftRadius][LeftRadius][LeftRadius] = node; else { Neighbors< Width >& pNeighbors = getNeighbors< CreateNodes >( node->parent ); // Get the indices of the child node that would contain the point (and its antipode) int cx , cy , cz; Cube::FactorCornerIndex( (int)( node - node->parent->children ) , cx , cy , cz ); // Iterate over the finer neighbors and set them (if you can) // Here: // (x,y,z) give the position of the finer nodes relative to the center, // (_x,_y,_z) give a positive global position, up to an even offset, and // (px-LeftRadius,py-LeftRadius,pz-LeftRadius) give the positions of their parents relative to the parent of the center for( int z=-(int)LeftRadius ; z<=(int)RightRadius ; z++ ) { int _z = (z+cz) + (LeftRadius<<1) , pz = ( _z>>1 ) , zz = z+LeftRadius; for( int y=-(int)LeftRadius ; y<=(int)RightRadius ; y++ ) { int _y = (y+cy) + (LeftRadius<<1) , py = ( _y>>1 ) , yy = y+LeftRadius; int cornerIndex = ( (_z&1)<<2 ) | ( (_y&1)<<1 ); for( int x=-(int)LeftRadius ; x<=(int)RightRadius ; x++ ) { int _x = (x+cx) + (LeftRadius<<1) , px = ( _x>>1 ) , xx = x+LeftRadius; if( CreateNodes ) { if( pNeighbors.neighbors[px][py][pz] ) { if( !pNeighbors.neighbors[px][py][pz]->children ) pNeighbors.neighbors[px][py][pz]->initChildren(); neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); } else neighbors.neighbors[xx][yy][zz] = NULL; } else { if( pNeighbors.neighbors[px][py][pz] && pNeighbors.neighbors[px][py][pz]->children ) neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); else neighbors.neighbors[xx][yy][zz] = NULL; } } } } } } return neighbors; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > template< bool CreateNodes , unsigned int _LeftRadius , unsigned int _RightRadius > void OctNode< NodeData >::NeighborKey< LeftRadius , RightRadius >::getNeighbors( OctNode< NodeData >* node , Neighbors< _LeftRadius + _RightRadius + 1 >& neighbors ) { neighbors.clear(); if( !node ) return; // [WARNING] This estimate of the required radius is somewhat conservative if the radius is odd (depending on where the node is relative to its parent) const unsigned int _PLeftRadius = (_LeftRadius+1)/2 , _PRightRadius = (_RightRadius+1)/2; // If we are at the root of the tree, we are done if( !node->parent ) neighbors.neighbors[_LeftRadius][_LeftRadius][_LeftRadius] = node; // If we can get the data from the the key for the parent node, do that else if( _PLeftRadius<=LeftRadius && _PRightRadius<=RightRadius ) { getNeighbors< CreateNodes >( node->parent ); const Neighbors< LeftRadius + RightRadius + 1 >& pNeighbors = this->neighbors[ node->depth()-1 ]; // Get the indices of the child node that would contain the point (and its antipode) int cx , cy , cz; Cube::FactorCornerIndex( (int)( node - node->parent->children ) , cx , cy , cz ); // Iterate over the finer neighbors // Here: // (x,y,z) give the position of the finer nodes relative to the center, // (_x,_y,_z) give a positive global position, up to an even offset, and // (px-LeftRadius,py-LeftRadius,pz-LeftRadius) give the positions of their parents relative to the parent of the center for( int z=-(int)_LeftRadius ; z<=(int)_RightRadius ; z++ ) { int _z = (z+cz) + (_LeftRadius<<1) , pz = ( _z>>1 ) - _LeftRadius + LeftRadius , zz = z + _LeftRadius; for( int y=-(int)_LeftRadius ; y<=(int)_RightRadius ; y++ ) { int _y = (y+cy) + (_LeftRadius<<1) , py = ( _y>>1 ) - _LeftRadius + LeftRadius , yy = y + _LeftRadius; int cornerIndex = ( (_z&1)<<2 ) | ( (_y&1)<<1 ); for( int x=-(int)_LeftRadius ; x<=(int)_RightRadius ; x++ ) { int _x = (x+cx) + (_LeftRadius<<1) , px = ( _x>>1 ) - _LeftRadius + LeftRadius , xx = x + _LeftRadius; if( CreateNodes ) { if( pNeighbors.neighbors[px][py][pz] ) { if( !pNeighbors.neighbors[px][py][pz]->children ) pNeighbors.neighbors[px][py][pz]->initChildren(); neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); } else neighbors.neighbors[xx][yy][zz] = NULL; } else { if( pNeighbors.neighbors[px][py][pz] && pNeighbors.neighbors[px][py][pz]->children ) neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); else neighbors.neighbors[xx][yy][zz] = NULL; } } } } } // Otherwise recurse else { Neighbors< _PLeftRadius + _PRightRadius + 1 > pNeighbors; getNeighbors< CreateNodes , _PLeftRadius , _PRightRadius >( node->parent , pNeighbors ); // Get the indices of the child node that would contain the point (and its antipode) int cx , cy , cz; Cube::FactorCornerIndex( (int)( node - node->parent->children ) , cx , cy , cz ); // Iterate over the finer neighbors // Here: // (x,y,z) give the position of the finer nodes relative to the center, // (_x,_y,_z) give a positive global position, up to an even offset, and // (px-LeftRadius,py-LeftRadius,pz-LeftRadius) give the positions of their parents relative to the parent of the center for( int z=-(int)_LeftRadius ; z<=(int)_RightRadius ; z++ ) { int _z = (z+cz) + (_LeftRadius<<1) , pz = ( _z>>1 ) - _LeftRadius + _PLeftRadius , zz = z + _LeftRadius; for( int y=-(int)_LeftRadius ; y<=(int)_RightRadius ; y++ ) { int _y = (y+cy) + (_LeftRadius<<1) , py = ( _y>>1 ) - _LeftRadius + _PLeftRadius , yy = y + _LeftRadius; int cornerIndex = ( (_z&1)<<2 ) | ( (_y&1)<<1 ); for( int x=-(int)_LeftRadius ; x<=(int)_RightRadius ; x++ ) { int _x = (x+cx) + (_LeftRadius<<1) , px = ( _x>>1 ) - _LeftRadius + _PLeftRadius , xx = x + _LeftRadius; if( CreateNodes ) { if( pNeighbors.neighbors[px][py][pz] ) { if( !pNeighbors.neighbors[px][py][pz]->children ) pNeighbors.neighbors[px][py][pz]->initChildren(); neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); } else neighbors.neighbors[xx][yy][zz] = NULL; } else { if( pNeighbors.neighbors[px][py][pz] && pNeighbors.neighbors[px][py][pz]->children ) neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); else neighbors.neighbors[xx][yy][zz] = NULL; } } } } } } /////////////////////////////// // OctNode::ConstNeighborKey // /////////////////////////////// template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > OctNode< NodeData >::ConstNeighborKey< LeftRadius , RightRadius >::ConstNeighborKey( void ){ _depth=-1 , neighbors=NULL; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > OctNode< NodeData >::ConstNeighborKey< LeftRadius , RightRadius >::ConstNeighborKey( const ConstNeighborKey& key ) { _depth = 0 , neighbors = NULL; set( key._depth ); for( int d=0 ; d<=_depth ; d++ ) memcpy( &neighbors[d] , &key.neighbors[d] , sizeof( ConstNeighbors< Width > ) ); } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > OctNode< NodeData >::ConstNeighborKey< LeftRadius , RightRadius >::~ConstNeighborKey( void ) { if( neighbors ) delete[] neighbors; neighbors=NULL; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > void OctNode< NodeData >::ConstNeighborKey< LeftRadius , RightRadius >::set( int d ) { if( neighbors ) delete[] neighbors; neighbors = NULL; _depth = d; if( d<0 ) return; neighbors = new ConstNeighbors< Width >[d+1]; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > typename OctNode< NodeData >::template ConstNeighbors< LeftRadius+RightRadius+1 >& OctNode< NodeData >::ConstNeighborKey< LeftRadius , RightRadius >::getNeighbors( const OctNode< NodeData >* node ) { ConstNeighbors< Width >& neighbors = this->neighbors[ node->depth() ]; if( node!=neighbors.neighbors[LeftRadius][LeftRadius][LeftRadius]) { neighbors.clear(); if( !node->parent ) neighbors.neighbors[LeftRadius][LeftRadius][LeftRadius] = node; else { ConstNeighbors< Width >& pNeighbors = getNeighbors( node->parent ); // Get the indices of the child node that would contain the point (and its antipode) int cx , cy , cz; Cube::FactorCornerIndex( (int)( node - node->parent->children ) , cx , cy , cz ); // Iterate over the finer neighbors and set them (if you can) // Here: // (x,y,z) give the position of the finer nodes relative to the center, // (_x,_y,_z) give a positive global position, up to an even offset, and // (px-LeftRadius,py-LeftRadius,pz-LeftRadius) give the positions of their parents relative to the parent of the center for( int z=-(int)LeftRadius ; z<=(int)RightRadius ; z++ ) { int _z = (z+cz) + (LeftRadius<<1) , pz = ( _z>>1 ) , zz = z+LeftRadius; for( int y=-(int)LeftRadius ; y<=(int)RightRadius ; y++ ) { int _y = (y+cy) + (LeftRadius<<1) , py = ( _y>>1 ) , yy = y+LeftRadius; int cornerIndex = ( (_z&1)<<2 ) | ( (_y&1)<<1 ); for( int x=-(int)LeftRadius ; x<=(int)RightRadius ; x++ ) { int _x = (x+cx) + (LeftRadius<<1) , px = ( _x>>1 ) , xx = x+LeftRadius; if( pNeighbors.neighbors[px][py][pz] && pNeighbors.neighbors[px][py][pz]->children ) neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); else neighbors.neighbors[xx][yy][zz] = NULL; } } } } } return neighbors; } template< class NodeData > template< unsigned int LeftRadius , unsigned int RightRadius > template< unsigned int _LeftRadius , unsigned int _RightRadius > void OctNode< NodeData >::ConstNeighborKey< LeftRadius , RightRadius >::getNeighbors( const OctNode< NodeData >* node , ConstNeighbors< _LeftRadius+_RightRadius+1 >& neighbors ) { neighbors.clear(); if( !node ) return; // [WARNING] This estimate of the required radius is somewhat conservative if the readius is odd (depending on where the node is relative to its parent) const unsigned int _PLeftRadius = (_LeftRadius+1)/2 , _PRightRadius = (_RightRadius+1)/2; // If we are at the root of the tree, we are done if( !node->parent ) neighbors.neighbors[_LeftRadius][_LeftRadius][_LeftRadius] = node; // If we can get the data from the the key for the parent node, do that else if( _PLeftRadius<=LeftRadius && _PRightRadius<=RightRadius ) { getNeighbors( node->parent ); const ConstNeighbors< LeftRadius + RightRadius + 1 >& pNeighbors = this->neighbors[ node->depth()-1 ]; // Get the indices of the child node that would contain the point (and its antipode) int cx , cy , cz; Cube::FactorCornerIndex( (int)( node - node->parent->children ) , cx , cy , cz ); // Iterate over the finer neighbors // Here: // (x,y,z) give the position of the finer nodes relative to the center, // (_x,_y,_z) give a positive global position, up to an even offset, and // (px-LeftRadius,py-LeftRadius,pz-LeftRadius) give the positions of their parents relative to the parent of the center for( int z=-(int)_LeftRadius ; z<=(int)_RightRadius ; z++ ) { int _z = (z+cz) + (_LeftRadius<<1) , pz = ( _z>>1 ) - _LeftRadius + LeftRadius , zz = z + _LeftRadius; for( int y=-(int)_LeftRadius ; y<=(int)_RightRadius ; y++ ) { int _y = (y+cy) + (_LeftRadius<<1) , py = ( _y>>1 ) - _LeftRadius + LeftRadius , yy = y + _LeftRadius; int cornerIndex = ( (_z&1)<<2 ) | ( (_y&1)<<1 ); for( int x=-(int)_LeftRadius ; x<=(int)_RightRadius ; x++ ) { int _x = (x+cx) + (_LeftRadius<<1) , px = ( _x>>1 ) - _LeftRadius + LeftRadius , xx = x + _LeftRadius; if( pNeighbors.neighbors[px][py][pz] && pNeighbors.neighbors[px][py][pz]->children ) neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); else neighbors.neighbors[xx][yy][zz] = NULL; } } } } // Otherwise recurse else { ConstNeighbors< _PLeftRadius + _PRightRadius + 1 > pNeighbors; getNeighbors< _PLeftRadius , _PRightRadius >( node->parent , pNeighbors ); // Get the indices of the child node that would contain the point (and its antipode) int cx , cy , cz; Cube::FactorCornerIndex( (int)( node - node->parent->children ) , cx , cy , cz ); // Iterate over the finer neighbors // Here: // (x,y,z) give the position of the finer nodes relative to the center, // (_x,_y,_z) give a positive global position, up to an even offset, and // (px-LeftRadius,py-LeftRadius,pz-LeftRadius) give the positions of their parents relative to the parent of the center for( int z=-(int)_LeftRadius ; z<=(int)_RightRadius ; z++ ) { int _z = (z+cz) + (_LeftRadius<<1) , pz = ( _z>>1 ) - _LeftRadius + _PLeftRadius , zz = z + _LeftRadius; for( int y=-(int)_LeftRadius ; y<=(int)_RightRadius ; y++ ) { int _y = (y+cy) + (_LeftRadius<<1) , py = ( _y>>1 ) - _LeftRadius + _PLeftRadius , yy = y + _LeftRadius; int cornerIndex = ( (_z&1)<<2 ) | ( (_y&1)<<1 ); for( int x=-(int)_LeftRadius ; x<=(int)_RightRadius ; x++ ) { int _x = (x+cx) + (_LeftRadius<<1) , px = ( _x>>1 ) - _LeftRadius + _PLeftRadius , xx = x + _LeftRadius; if( pNeighbors.neighbors[px][py][pz] && pNeighbors.neighbors[px][py][pz]->children ) neighbors.neighbors[xx][yy][zz] = pNeighbors.neighbors[px][py][pz]->children + ( cornerIndex | (_x&1) ); else neighbors.neighbors[xx][yy][zz] = NULL; } } } } return; } template< class NodeData > int OctNode< NodeData >::write(const char* fileName) const{ FILE* fp=fopen(fileName,"wb"); if(!fp){return 0;} int ret=write(fp); fclose(fp); return ret; } template< class NodeData > int OctNode< NodeData >::write(FILE* fp) const{ fwrite(this,sizeof(OctNode< NodeData >),1,fp); if(children){for(int i=0;i int OctNode< NodeData >::read(const char* fileName){ FILE* fp=fopen(fileName,"rb"); if(!fp){return 0;} int ret=read(fp); fclose(fp); return ret; } template< class NodeData > int OctNode< NodeData >::read(FILE* fp){ fread(this,sizeof(OctNode< NodeData >),1,fp); parent=NULL; if(children){ children=NULL; initChildren(); for(int i=0;i int OctNode< NodeData >::width(int maxDepth) const { int d=depth(); return 1<<(maxDepth-d); } template< class NodeData > void OctNode< NodeData >::centerIndex(int maxDepth,int index[DIMENSION]) const { int d,o[3]; depthAndOffset(d,o); for(int i=0;i #include "Polynomial.h" #include "Array.h" template< int Degree > class StartingPolynomial { public: Polynomial< Degree > p; double start; template< int Degree2 > StartingPolynomial< Degree+Degree2 > operator * ( const StartingPolynomial< Degree2 >& p ) const; StartingPolynomial scale( double s ) const; StartingPolynomial shift( double t ) const; int operator < ( const StartingPolynomial& sp ) const; static int Compare( const void* v1 , const void* v2 ); }; template< int Degree > class PPolynomial { public: size_t polyCount; Pointer( StartingPolynomial< Degree > ) polys; PPolynomial( void ); PPolynomial( const PPolynomial& p ); ~PPolynomial( void ); PPolynomial& operator = ( const PPolynomial& p ); int size( void ) const; void set( size_t size ); // Note: this method will sort the elements in sps void set( Pointer( StartingPolynomial ) sps , int count ); void reset( size_t newSize ); PPolynomial& compress( double delta=0. ); double operator()( double t ) const; double integral( double tMin , double tMax ) const; double Integral( void ) const; template< int Degree2 > PPolynomial< Degree >& operator = ( const PPolynomial< Degree2 >& p ); PPolynomial operator + ( const PPolynomial& p ) const; PPolynomial operator - ( const PPolynomial& p ) const; template< int Degree2 > PPolynomial< Degree+Degree2 > operator * ( const Polynomial< Degree2 >& p ) const; template< int Degree2 > PPolynomial< Degree+Degree2 > operator * ( const PPolynomial< Degree2 >& p) const; PPolynomial& operator += ( double s ); PPolynomial& operator -= ( double s ); PPolynomial& operator *= ( double s ); PPolynomial& operator /= ( double s ); PPolynomial operator + ( double s ) const; PPolynomial operator - ( double s ) const; PPolynomial operator * ( double s ) const; PPolynomial operator / ( double s ) const; PPolynomial& addScaled( const PPolynomial& poly , double scale ); PPolynomial scale( double s ) const; PPolynomial shift( double t ) const; PPolynomial reflect( double r=0. ) const; PPolynomial< Degree-1 > derivative(void) const; PPolynomial< Degree+1 > integral(void) const; void getSolutions( double c , std::vector< double >& roots , double EPS , double min=-DBL_MAX , double max=DBL_MAX ) const; void printnl( void ) const; PPolynomial< Degree+1 > MovingAverage( double radius ) const; static PPolynomial BSpline( double radius=0.5 ); void write( FILE* fp , int samples , double min , double max ) const; }; #include "PPolynomial.inl" #endif // P_POLYNOMIAL_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/PPolynomial.inl000077500000000000000000000332101454702036400233570ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include "Factor.h" //////////////////////// // StartingPolynomial // //////////////////////// template template StartingPolynomial StartingPolynomial::operator * (const StartingPolynomial& p) const{ StartingPolynomial sp; if(start>p.start){sp.start=start;} else{sp.start=p.start;} sp.p=this->p*p.p; return sp; } template StartingPolynomial StartingPolynomial::scale( double s ) const { StartingPolynomial q; q.start = start*s; q.p = p.scale(s); return q; } template StartingPolynomial StartingPolynomial::shift(double s) const{ StartingPolynomial q; q.start=start+s; q.p=p.shift(s); return q; } template int StartingPolynomial::operator < (const StartingPolynomial& sp) const{ if(start int StartingPolynomial::Compare(const void* v1,const void* v2){ double d=((StartingPolynomial*)(v1))->start-((StartingPolynomial*)(v2))->start; if ( d<0 ) return -1; else if( d>0 ) return 1; else return 0; } ///////////////// // PPolynomial // ///////////////// template< int Degree > PPolynomial< Degree >::PPolynomial( void ) { polyCount = 0; polys = NullPointer( StartingPolynomial< Degree > ); } template< int Degree > PPolynomial::PPolynomial( const PPolynomial& p ) { polyCount = 0; polys = NullPointer( StartingPolynomial< Degree > ); set( p.polyCount ); memcpy( polys , p.polys , sizeof( StartingPolynomial ) * p.polyCount ); } template< int Degree > PPolynomial< Degree >::~PPolynomial( void ) { FreePointer( polys ); polyCount = 0; } template< int Degree > void PPolynomial< Degree >::set( Pointer( StartingPolynomial< Degree > ) sps , int count ) { int c=0; set( count ); qsort( sps , count , sizeof( StartingPolynomial< Degree > ) , StartingPolynomial< Degree >::Compare ); for( int i=0 ; i int PPolynomial< Degree >::size( void ) const{ return int(sizeof(StartingPolynomial)*polyCount); } template< int Degree > void PPolynomial::set( size_t size ) { FreePointer( polys ); polyCount = size; if( size ) { polys = AllocPointer< StartingPolynomial< Degree > >( size ); memset( polys , 0 , sizeof( StartingPolynomial< Degree > )*size ); } } template< int Degree > void PPolynomial::reset( size_t newSize ) { polyCount = newSize; polys = ReAllocPointer< StartingPolynomial< Degree > >( polys , newSize ); } template< int Degree > PPolynomial< Degree >& PPolynomial< Degree >::compress( double delta ) { int _polyCount = polyCount; Pointer( StartingPolynomial< Degree > ) _polys = polys; polyCount = 1 , polys = NullPointer( StartingPolynomial< Degree > ); for( int i=1 ; i<_polyCount ; i++ ) if( _polys[i].start-_polys[i-1].start>delta ) polyCount++; if( polyCount==_polyCount ) polys = _polys; else { polys = AllocPointer< StartingPolynomial< Degree > >( polyCount ); polys[0] = _polys[0] , polyCount = 0; for( int i=1 ; i<_polyCount ; i++ ) { if( _polys[i].start-_polys[i-1].start>delta ) polys[ ++polyCount ] = _polys[i]; else polys[ polyCount ].p += _polys[i].p; } polyCount++; FreePointer( _polys ); } return *this; } template< int Degree > PPolynomial& PPolynomial::operator = (const PPolynomial& p){ set(p.polyCount); memcpy(polys,p.polys,sizeof(StartingPolynomial)*p.polyCount); return *this; } template template PPolynomial& PPolynomial::operator = (const PPolynomial& p){ set(p.polyCount); for(int i=0;i double PPolynomial::operator ()( double t ) const { double v=0; for( int i=0 ; ipolys[i].start ; i++ ) v+=polys[i].p(t); return v; } template double PPolynomial::integral( double tMin , double tMax ) const { int m=1; double start,end,s,v=0; start=tMin; end=tMax; if(tMin>tMax){ m=-1; start=tMax; end=tMin; } for(int i=0;i double PPolynomial::Integral(void) const{return integral(polys[0].start,polys[polyCount-1].start);} template PPolynomial PPolynomial::operator + (const PPolynomial& p) const{ PPolynomial q; int i,j; size_t idx=0; q.set(polyCount+p.polyCount); i=j=-1; while(idx=int(p.polyCount)-1) {q.polys[idx]= polys[++i];} else if (i>=int( polyCount)-1) {q.polys[idx]=p.polys[++j];} else if(polys[i+1].start PPolynomial PPolynomial::operator - (const PPolynomial& p) const{ PPolynomial q; int i,j; size_t idx=0; q.set(polyCount+p.polyCount); i=j=-1; while(idx=int(p.polyCount)-1) {q.polys[idx]= polys[++i];} else if (i>=int( polyCount)-1) {q.polys[idx].start=p.polys[++j].start;q.polys[idx].p=p.polys[j].p*(-1.0);} else if(polys[i+1].start PPolynomial& PPolynomial::addScaled(const PPolynomial& p,double scale){ int i,j; StartingPolynomial* oldPolys=polys; size_t idx=0,cnt=0,oldPolyCount=polyCount; polyCount=0; polys=NULL; set(oldPolyCount+p.polyCount); i=j=-1; while(cnt=int( p.polyCount)-1) {polys[idx]=oldPolys[++i];} else if (i>=int(oldPolyCount)-1) {polys[idx].start= p.polys[++j].start;polys[idx].p=p.polys[j].p*scale;} else if (oldPolys[i+1].start template PPolynomial PPolynomial::operator * (const PPolynomial& p) const{ PPolynomial q; StartingPolynomial *sp; int i,j,spCount=int(polyCount*p.polyCount); sp=(StartingPolynomial*)malloc(sizeof(StartingPolynomial)*spCount); for(i=0;i template PPolynomial PPolynomial::operator * (const Polynomial& p) const{ PPolynomial q; q.set(polyCount); for(int i=0;i PPolynomial PPolynomial::scale( double s ) const { PPolynomial q; q.set( polyCount ); for( size_t i=0 ; i ) , StartingPolynomial< Degree >::Compare ); return q; } template< int Degree > PPolynomial< Degree > PPolynomial< Degree >::reflect( double r ) const { PPolynomial q; q.set( polyCount ); for( size_t i=0 ; i ) , StartingPolynomial< Degree >::Compare ); return q; } template PPolynomial PPolynomial::shift( double s ) const { PPolynomial q; q.set(polyCount); for(size_t i=0;i PPolynomial PPolynomial::derivative(void) const{ PPolynomial q; q.set(polyCount); for(size_t i=0;i PPolynomial PPolynomial::integral(void) const{ int i; PPolynomial q; q.set(polyCount); for(i=0;i PPolynomial& PPolynomial::operator += ( double s ) {polys[0].p+=s;} template PPolynomial& PPolynomial::operator -= ( double s ) {polys[0].p-=s;} template PPolynomial& PPolynomial::operator *= ( double s ) { for(int i=0;i PPolynomial& PPolynomial::operator /= ( double s ) { for(size_t i=0;i PPolynomial PPolynomial::operator + ( double s ) const { PPolynomial q=*this; q+=s; return q; } template PPolynomial PPolynomial::operator - ( double s ) const { PPolynomial q=*this; q-=s; return q; } template PPolynomial PPolynomial::operator * ( double s ) const { PPolynomial q=*this; q*=s; return q; } template PPolynomial PPolynomial::operator / ( double s ) const { PPolynomial q=*this; q/=s; return q; } template void PPolynomial::printnl(void) const{ Polynomial p; if(!polyCount){ Polynomial p; printf("[-Infinity,Infinity]\n"); } else{ for(size_t i=0;i PPolynomial< 0 > PPolynomial< 0 >::BSpline( double radius ) { PPolynomial q; q.set(2); q.polys[0].start=-radius; q.polys[1].start= radius; q.polys[0].p.coefficients[0]= 1.0; q.polys[1].p.coefficients[0]=-1.0; return q; } template< int Degree > PPolynomial< Degree > PPolynomial::BSpline( double radius ) { return PPolynomial< Degree-1 >::BSpline().MovingAverage( radius ); } template PPolynomial PPolynomial::MovingAverage( double radius ) const { PPolynomial A; Polynomial p; Pointer( StartingPolynomial< Degree+1 > ) sps; sps = AllocPointer< StartingPolynomial< Degree+1 > >( polyCount*2 ); for(int i=0;i void PPolynomial::getSolutions(double c,std::vector& roots,double EPS,double min,double max) const{ Polynomial p; std::vector tempRoots; p.setZero(); for(size_t i=0;imax){break;} if(ipolys[i].start && (i+1==polyCount || tempRoots[j]<=polys[i+1].start)){ if(tempRoots[j]>min && tempRoots[j] void PPolynomial::write(FILE* fp,int samples,double min,double max) const{ fwrite(&samples,sizeof(int),1,fp); for(int i=0;i #include #include #include #define PLY_ASCII 1 /* ascii PLY file */ #define PLY_BINARY_BE 2 /* binary PLY file, big endian */ #define PLY_BINARY_LE 3 /* binary PLY file, little endian */ #define PLY_BINARY_NATIVE 4 /* binary PLY file, same endianness as current architecture */ #define PLY_OKAY 0 /* ply routine worked okay */ #define PLY_ERROR -1 /* error in ply routine */ /* scalar data types supported by PLY format */ #define PLY_START_TYPE 0 #define PLY_CHAR 1 #define PLY_SHORT 2 #define PLY_INT 3 #define PLY_UCHAR 4 #define PLY_USHORT 5 #define PLY_UINT 6 #define PLY_FLOAT 7 #define PLY_DOUBLE 8 #define PLY_INT_8 9 #define PLY_UINT_8 10 #define PLY_INT_16 11 #define PLY_UINT_16 12 #define PLY_INT_32 13 #define PLY_UINT_32 14 #define PLY_FLOAT_32 15 #define PLY_FLOAT_64 16 #define PLY_END_TYPE 17 #define PLY_SCALAR 0 #define PLY_LIST 1 #define PLY_STRIP_COMMENT_HEADER 0 typedef struct PlyProperty { /* description of a property */ char *name; /* property name */ int external_type; /* file's data type */ int internal_type; /* program's data type */ int offset; /* offset bytes of prop in a struct */ int is_list; /* 1 = list, 0 = scalar */ int count_external; /* file's count type */ int count_internal; /* program's count type */ int count_offset; /* offset byte for list count */ } PlyProperty; typedef struct PlyElement { /* description of an element */ char *name; /* element name */ int num; /* number of elements in this object */ int size; /* size of element (bytes) or -1 if variable */ int nprops; /* number of properties for this element */ PlyProperty **props; /* list of properties in the file */ char *store_prop; /* flags: property wanted by user? */ int other_offset; /* offset to un-asked-for props, or -1 if none*/ int other_size; /* size of other_props structure */ } PlyElement; typedef struct PlyOtherProp { /* describes other properties in an element */ char *name; /* element name */ int size; /* size of other_props */ int nprops; /* number of properties in other_props */ PlyProperty **props; /* list of properties in other_props */ } PlyOtherProp; typedef struct OtherData { /* for storing other_props for an other element */ void *other_props; } OtherData; typedef struct OtherElem { /* data for one "other" element */ char *elem_name; /* names of other elements */ int elem_count; /* count of instances of each element */ OtherData **other_data; /* actual property data for the elements */ PlyOtherProp *other_props; /* description of the property data */ } OtherElem; typedef struct PlyOtherElems { /* "other" elements, not interpreted by user */ int num_elems; /* number of other elements */ OtherElem *other_list; /* list of data for other elements */ } PlyOtherElems; typedef struct PlyFile { /* description of PLY file */ FILE *fp; /* file pointer */ int file_type; /* ascii or binary */ float version; /* version number of file */ int nelems; /* number of elements of object */ PlyElement **elems; /* list of elements */ int num_comments; /* number of comments */ char **comments; /* list of comments */ int num_obj_info; /* number of items of object information */ char **obj_info; /* list of object info items */ PlyElement *which_elem; /* which element we're currently writing */ PlyOtherElems *other_elems; /* "other" elements from a PLY file */ } PlyFile; /* memory allocation */ extern char *my_alloc(); #define myalloc(mem_size) my_alloc((mem_size), __LINE__, __FILE__) #ifndef ALLOCN #define REALLOCN(PTR,TYPE,OLD_N,NEW_N) \ { \ if ((OLD_N) == 0) \ { ALLOCN((PTR),TYPE,(NEW_N));} \ else \ { \ (PTR) = (TYPE *)realloc((PTR),(NEW_N)*sizeof(TYPE)); \ if (((PTR) == NULL) && ((NEW_N) != 0)) \ { \ fprintf(stderr, "Memory reallocation failed on line %d in %s\n", \ __LINE__, __FILE__); \ fprintf(stderr, " tried to reallocate %d->%d\n", \ (OLD_N), (NEW_N)); \ exit(-1); \ } \ if ((NEW_N)>(OLD_N)) \ memset((char *)(PTR)+(OLD_N)*sizeof(TYPE), 0, \ ((NEW_N)-(OLD_N))*sizeof(TYPE)); \ } \ } #define ALLOCN(PTR,TYPE,N) \ { (PTR) = (TYPE *) calloc(((unsigned)(N)),sizeof(TYPE));\ if ((PTR) == NULL) { \ fprintf(stderr, "Memory allocation failed on line %d in %s\n", \ __LINE__, __FILE__); \ exit(-1); \ } \ } #define FREE(PTR) { free((PTR)); (PTR) = NULL; } #endif /*** delcaration of routines ***/ extern PlyFile *ply_write(FILE *, int, const char **, int); extern PlyFile *ply_open_for_writing(char *, int, const char **, int, float *); extern void ply_describe_element(PlyFile *, char *, int, int, PlyProperty *); extern void ply_describe_property(PlyFile *, const char *, PlyProperty *); extern void ply_element_count(PlyFile *, const char *, int); extern void ply_header_complete(PlyFile *); extern void ply_put_element_setup(PlyFile *, const char *); extern void ply_put_element(PlyFile *, void *); extern void ply_put_comment(PlyFile *, char *); extern void ply_put_obj_info(PlyFile *, char *); extern PlyFile *ply_read(FILE *, int *, char ***); extern PlyFile *ply_open_for_reading( char *, int *, char ***, int *, float *); extern PlyProperty **ply_get_element_description(PlyFile *, char *, int*, int*); extern void ply_get_element_setup( PlyFile *, char *, int, PlyProperty *); extern int ply_get_property(PlyFile *, char *, PlyProperty *); extern PlyOtherProp *ply_get_other_properties(PlyFile *, char *, int); extern void ply_get_element(PlyFile *, void *); extern char **ply_get_comments(PlyFile *, int *); extern char **ply_get_obj_info(PlyFile *, int *); extern void ply_close(PlyFile *); extern void ply_get_info(PlyFile *, float *, int *); extern PlyOtherElems *ply_get_other_element (PlyFile *, char *, int); extern void ply_describe_other_elements ( PlyFile *, PlyOtherElems *); extern void ply_put_other_elements (PlyFile *); extern void ply_free_other_elements (PlyOtherElems *); extern void ply_describe_other_properties(PlyFile *, PlyOtherProp *, int); extern int equal_strings(const char *, const char *); #ifdef __cplusplus } #endif #include "Geometry.h" #include template< class Real > int PLYType( void ); template<> inline int PLYType< int >( void ){ return PLY_INT ; } template<> inline int PLYType< char >( void ){ return PLY_CHAR ; } template<> inline int PLYType< unsigned char >( void ){ return PLY_UCHAR ; } template<> inline int PLYType< float >( void ){ return PLY_FLOAT ; } template<> inline int PLYType< double >( void ){ return PLY_DOUBLE; } template< class Real > inline int PLYType( void ){ fprintf( stderr , "[ERROR] Unrecognized type\n" ) , exit( 0 ); } typedef struct PlyFace { unsigned char nr_vertices; int *vertices; int segment; } PlyFace; static PlyProperty face_props[] = { { _strdup( "vertex_indices" ) , PLY_INT , PLY_INT , offsetof( PlyFace , vertices ) , 1 , PLY_UCHAR, PLY_UCHAR , offsetof(PlyFace,nr_vertices) }, }; /////////////////// // PlyVertexType // /////////////////// // The "Wrapper" class indicates the class to cast to/from in order to support linear operations. template< class Real > class PlyVertex { public: typedef PlyVertex Wrapper; const static int ReadComponents=3; const static int WriteComponents=3; static PlyProperty ReadProperties[]; static PlyProperty WriteProperties[]; Point3D< Real > point; PlyVertex( void ) { ; } PlyVertex( Point3D< Real > p ) { point=p; } PlyVertex operator + ( PlyVertex p ) const { return PlyVertex( point+p.point ); } PlyVertex operator - ( PlyVertex p ) const { return PlyVertex( point-p.point ); } template< class _Real > PlyVertex operator * ( _Real s ) const { return PlyVertex( point*s ); } template< class _Real > PlyVertex operator / ( _Real s ) const { return PlyVertex( point/s ); } PlyVertex& operator += ( PlyVertex p ) { point += p.point ; return *this; } PlyVertex& operator -= ( PlyVertex p ) { point -= p.point ; return *this; } template< class _Real > PlyVertex& operator *= ( _Real s ) { point *= s ; return *this; } template< class _Real > PlyVertex& operator /= ( _Real s ) { point /= s ; return *this; } }; template< class Real , class _Real > PlyVertex< Real > operator * ( XForm4x4< _Real > xForm , PlyVertex< Real > v ) { return PlyVertex< Real >( xForm * v.point ); } template< class Real > PlyProperty PlyVertex< Real >::ReadProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Real > PlyProperty PlyVertex< Real >::WriteProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Real > class PlyValueVertex { public: typedef PlyValueVertex Wrapper; const static int ReadComponents=4; const static int WriteComponents=4; static PlyProperty ReadProperties[]; static PlyProperty WriteProperties[]; Point3D point; Real value; PlyValueVertex( void ) : value( Real(0) ) { ; } PlyValueVertex( Point3D< Real > p , Real v ) : point(p) , value(v) { ; } PlyValueVertex operator + ( PlyValueVertex p ) const { return PlyValueVertex( point+p.point , value+p.value ); } PlyValueVertex operator - ( PlyValueVertex p ) const { return PlyValueVertex( point-p.value , value-p.value ); } template< class _Real > PlyValueVertex operator * ( _Real s ) const { return PlyValueVertex( point*s , Real(value*s) ); } template< class _Real > PlyValueVertex operator / ( _Real s ) const { return PlyValueVertex( point/s , Real(value/s) ); } PlyValueVertex& operator += ( PlyValueVertex p ) { point += p.point , value += p.value ; return *this; } PlyValueVertex& operator -= ( PlyValueVertex p ) { point -= p.point , value -= p.value ; return *this; } template< class _Real > PlyValueVertex& operator *= ( _Real s ) { point *= s , value *= Real(s) ; return *this; } template< class _Real > PlyValueVertex& operator /= ( _Real s ) { point /= s , value /= Real(s) ; return *this; } }; template< class Real , class _Real > PlyValueVertex< Real > operator * ( XForm4x4< _Real > xForm , PlyValueVertex< Real > v ) { return PlyValueVertex< Real >( xForm * v.point , v.value ); } template< class Real > PlyProperty PlyValueVertex< Real >::ReadProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "value" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , value ) ) , 0 , 0 , 0 , 0 } }; template< class Real > PlyProperty PlyValueVertex< Real >::WriteProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "value" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyValueVertex , value ) ) , 0 , 0 , 0 , 0 } }; template< class Real > class PlyOrientedVertex { public: typedef PlyOrientedVertex Wrapper; const static int ReadComponents=6; const static int WriteComponents=6; static PlyProperty ReadProperties[]; static PlyProperty WriteProperties[]; Point3D point , normal; PlyOrientedVertex( void ) { ; } PlyOrientedVertex( Point3D< Real > p , Point3D< Real > n ) : point(p) , normal(n) { ; } PlyOrientedVertex operator + ( PlyOrientedVertex p ) const { return PlyOrientedVertex( point+p.point , normal+p.normal ); } PlyOrientedVertex operator - ( PlyOrientedVertex p ) const { return PlyOrientedVertex( point-p.value , normal-p.normal ); } template< class _Real > PlyOrientedVertex operator * ( _Real s ) const { return PlyOrientedVertex( point*s , normal*s ); } template< class _Real > PlyOrientedVertex operator / ( _Real s ) const { return PlyOrientedVertex( point/s , normal/s ); } PlyOrientedVertex& operator += ( PlyOrientedVertex p ) { point += p.point , normal += p.normal ; return *this; } PlyOrientedVertex& operator -= ( PlyOrientedVertex p ) { point -= p.point , normal -= p.normal ; return *this; } template< class _Real > PlyOrientedVertex& operator *= ( _Real s ) { point *= s , normal *= s ; return *this; } template< class _Real > PlyOrientedVertex& operator /= ( _Real s ) { point /= s , normal /= s ; return *this; } }; template< class Real , class _Real > PlyOrientedVertex< Real > operator * ( XForm4x4< _Real > xForm , PlyOrientedVertex< Real > v ) { return PlyOrientedVertex< Real >( xForm * v.point , xForm.inverse().transpose() * v.normal ); } template< class Real > PlyProperty PlyOrientedVertex< Real >::ReadProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "nx" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , normal.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "ny" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , normal.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "nz" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , normal.coords[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Real > PlyProperty PlyOrientedVertex< Real >::WriteProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "nx" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , normal.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "ny" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , normal.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "nz" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyOrientedVertex , normal.coords[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Real > class PlyColorVertex { public: struct _PlyColorVertex { Point3D< Real > point , color; _PlyColorVertex( void ) { ; } _PlyColorVertex( Point3D< Real > p , Point3D< Real > c ) : point(p) , color(c) { ; } _PlyColorVertex( PlyColorVertex< Real > p ){ point = p.point ; for( int c=0 ; c<3 ; c++ ) color[c] = (Real) p.color[c]; } operator PlyColorVertex< Real > () { PlyColorVertex< Real > p; p.point = point; for( int c=0 ; c<3 ; c++ ) p.color[c] = (unsigned char)std::max< int >( 0 , std::min< int >( 255 , (int)( color[c]+0.5 ) ) ); return p; } _PlyColorVertex operator + ( _PlyColorVertex p ) const { return _PlyColorVertex( point+p.point , color+p.color ); } _PlyColorVertex operator - ( _PlyColorVertex p ) const { return _PlyColorVertex( point-p.value , color-p.color ); } template< class _Real > _PlyColorVertex operator * ( _Real s ) const { return _PlyColorVertex( point*s , color*s ); } template< class _Real > _PlyColorVertex operator / ( _Real s ) const { return _PlyColorVertex( point/s , color/s ); } _PlyColorVertex& operator += ( _PlyColorVertex p ) { point += p.point , color += p.color ; return *this; } _PlyColorVertex& operator -= ( _PlyColorVertex p ) { point -= p.point , color -= p.color ; return *this; } template< class _Real > _PlyColorVertex& operator *= ( _Real s ) { point *= s , color *= s ; return *this; } template< class _Real > _PlyColorVertex& operator /= ( _Real s ) { point /= s , color /= s ; return *this; } }; typedef _PlyColorVertex Wrapper; const static int ReadComponents=9; const static int WriteComponents=6; static PlyProperty ReadProperties[]; static PlyProperty WriteProperties[]; Point3D< Real > point; unsigned char color[3]; operator Point3D< Real >& (){ return point; } operator const Point3D< Real >& () const { return point; } PlyColorVertex( void ) { point.coords[0] = point.coords[1] = point.coords[2] = 0 , color[0] = color[1] = color[2] = 0; } PlyColorVertex( const Point3D& p ) { point=p; } PlyColorVertex( const Point3D< Real >& p , const unsigned char c[3] ) { point = p , color[0] = c[0] , color[1] = c[1] , color[2] = c[2]; } }; template< class Real , class _Real > PlyColorVertex< Real > operator * ( XForm4x4< _Real > xForm , PlyColorVertex< Real > v ) { return PlyColorVertex< Real >( xForm * v.point , v.color ); } template< class Real > PlyProperty PlyColorVertex< Real >::ReadProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >(), int( offsetof( PlyColorVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >(), int( offsetof( PlyColorVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >(), int( offsetof( PlyColorVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "red" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "green" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "blue" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[2] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "r" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "g" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "b" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Real > PlyProperty PlyColorVertex< Real >::WriteProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >(), int( offsetof( PlyColorVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >(), int( offsetof( PlyColorVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >(), int( offsetof( PlyColorVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "red" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[0] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "green" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[1] ) ) , 0 , 0 , 0 , 0 }, { _strdup( "blue" ) , PLYType< unsigned char >() , PLYType< unsigned char >(), int( offsetof( PlyColorVertex , color[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Real > class PlyColorAndValueVertex { public: struct _PlyColorAndValueVertex { Point3D< Real > point , color; Real value; _PlyColorAndValueVertex( void ) : value(0) { ; } _PlyColorAndValueVertex( Point3D< Real > p , Point3D< Real > c , Real v ) : point(p) , color(c) , value(v) { ; } _PlyColorAndValueVertex( PlyColorAndValueVertex< Real > p ){ point = p.point ; for( int c=0 ; c<3 ; c++ ) color[c] = (Real) p.color[c] ; value = p.value; } operator PlyColorAndValueVertex< Real > () { PlyColorAndValueVertex< Real > p; p.point = point; for( int c=0 ; c<3 ; c++ ) p.color[c] = (unsigned char)std::max< int >( 0 , std::min< int >( 255 , (int)( color[c]+0.5 ) ) ); p.value = value; return p; } _PlyColorAndValueVertex operator + ( _PlyColorAndValueVertex p ) const { return _PlyColorAndValueVertex( point+p.point , color+p.color , value+p.value ); } _PlyColorAndValueVertex operator - ( _PlyColorAndValueVertex p ) const { return _PlyColorAndValueVertex( point-p.value , color-p.color , value+p.value ); } template< class _Real > _PlyColorAndValueVertex operator * ( _Real s ) const { return _PlyColorAndValueVertex( point*s , color*s , value*s ); } template< class _Real > _PlyColorAndValueVertex operator / ( _Real s ) const { return _PlyColorAndValueVertex( point/s , color/s , value/s ); } _PlyColorAndValueVertex& operator += ( _PlyColorAndValueVertex p ) { point += p.point , color += p.color , value += p.value ; return *this; } _PlyColorAndValueVertex& operator -= ( _PlyColorAndValueVertex p ) { point -= p.point , color -= p.color , value -= p.value ; return *this; } template< class _Real > _PlyColorAndValueVertex& operator *= ( _Real s ) { point *= s , color *= s , value *= (Real)s ; return *this; } template< class _Real > _PlyColorAndValueVertex& operator /= ( _Real s ) { point /= s , color /= s , value /= (Real)s ; return *this; } }; typedef _PlyColorAndValueVertex Wrapper; const static int ReadComponents=10; const static int WriteComponents=7; static PlyProperty ReadProperties[]; static PlyProperty WriteProperties[]; Point3D< Real > point; unsigned char color[3]; Real value; operator Point3D< Real >& (){ return point; } operator const Point3D< Real >& () const { return point; } PlyColorAndValueVertex( void ) { point.coords[0] = point.coords[1] = point.coords[2] = (Real)0 , color[0] = color[1] = color[2] = 0 , value = (Real)0; } PlyColorAndValueVertex( const Point3D< Real >& p ) { point=p; } PlyColorAndValueVertex( const Point3D< Real >& p , const unsigned char c[3] , Real v) { point = p , color[0] = c[0] , color[1] = c[1] , color[2] = c[2] , value = v; } }; template< class Real , class _Real > PlyColorAndValueVertex< Real > operator * ( XForm4x4< _Real > xForm , PlyColorAndValueVertex< Real > v ) { return PlyColorAndValueVertex< Real >( xForm * v.point , v.color , v.value ); } template< class Real > PlyProperty PlyColorAndValueVertex< Real >::ReadProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "value" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , value ) ) , 0 , 0 , 0 , 0 } , { _strdup( "red" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[0] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "green" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[1] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "blue" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[2] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "r" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[0] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "g" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[1] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "b" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Real > PlyProperty PlyColorAndValueVertex< Real >::WriteProperties[]= { { _strdup( "x" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , point.coords[0] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "y" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , point.coords[1] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "z" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , point.coords[2] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "value" ) , PLYType< Real >() , PLYType< Real >() , int( offsetof( PlyColorAndValueVertex , value ) ) , 0 , 0 , 0 , 0 } , { _strdup( "red" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[0] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "green" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[1] ) ) , 0 , 0 , 0 , 0 } , { _strdup( "blue" ) , PLYType< unsigned char >() , PLYType< unsigned char >() , int( offsetof( PlyColorAndValueVertex , color[2] ) ) , 0 , 0 , 0 , 0 } }; template< class Vertex , class Real > int PlyWritePolygons( char* fileName , CoredMeshData< Vertex >* mesh , int file_type , const Point3D< float >& translate , float scale , char** comments=NULL , int commentNum=0 , XForm4x4< Real > xForm=XForm4x4< Real >::Identity() ); template< class Vertex , class Real > int PlyWritePolygons( char* fileName , CoredMeshData< Vertex >* mesh , int file_type , char** comments=NULL , int commentNum=0 , XForm4x4< Real > xForm=XForm4x4< Real >::Identity() ); inline bool PlyReadHeader( char* fileName , PlyProperty* properties , int propertyNum , bool* readFlags , int& file_type ) { int nr_elems; char **elist; float version; PlyFile* ply; char* elem_name; int num_elems; int nr_props; PlyProperty** plist; ply = ply_open_for_reading( fileName , &nr_elems , &elist , &file_type , &version ); if( !ply ) return false; for( int i=0 ; ielems[i]->name ); free( ply->elems[i]->store_prop ); for( int j=0 ; jelems[i]->nprops ; j++ ) { free( ply->elems[i]->props[j]->name ); free( ply->elems[i]->props[j] ); } free( ply->elems[i]->props ); } for( int i=0 ; ielems[i] ); free( ply->elems ); for( int i=0 ; inum_comments ; i++ ) free( ply->comments[i] ); free( ply->comments ); for( int i=0 ; inum_obj_info ; i++ ) free( ply->obj_info[i] ); free( ply->obj_info ); ply_free_other_elements( ply->other_elems ); for( int i=0 ; iname ); free( plist[j] ); } free( plist ); } // for each type of element for( int i=0 ; ielems[i]->name ); free( ply->elems[i]->store_prop ); for( int j=0 ; jelems[i]->nprops ; j++ ) { free( ply->elems[i]->props[j]->name ); free( ply->elems[i]->props[j] ); } if( ply->elems[i]->props && ply->elems[i]->nprops ) free(ply->elems[i]->props); } for( int i=0 ; ielems[i]); free( ply->elems) ; for( int i=0 ; inum_comments ; i++ ) free( ply->comments[i] ); free( ply->comments ); for( int i=0 ; inum_obj_info ; i++ ) free( ply->obj_info[i] ); free( ply->obj_info ); ply_free_other_elements(ply->other_elems); for( int i=0 ; i int PlyReadPolygons(char* fileName, std::vector& vertices,std::vector >& polygons, PlyProperty* properties,int propertyNum, int& file_type, char*** comments=NULL,int* commentNum=NULL , bool* readFlags=NULL ); template int PlyWritePolygons(char* fileName, const std::vector& vertices,const std::vector >& polygons, PlyProperty* properties,int propertyNum, int file_type, char** comments=NULL,const int& commentNum=0); template int PlyWritePolygons(char* fileName, const std::vector& vertices , const std::vector< std::vector< int > >& polygons, PlyProperty* properties,int propertyNum, int file_type, char** comments,const int& commentNum) { int nr_vertices=int(vertices.size()); int nr_faces=int(polygons.size()); float version; const char *elem_names[] = { "vertex" , "face" }; PlyFile *ply = ply_open_for_writing( fileName , 2 , elem_names , file_type , &version ); if (!ply){return 0;} // // describe vertex and face properties // ply_element_count(ply, "vertex", nr_vertices); for(int i=0;imaxFaceVerts) { delete[] ply_face.vertices; maxFaceVerts=int(polygons[i].size()); ply_face.vertices=new int[maxFaceVerts]; } ply_face.nr_vertices=int(polygons[i].size()); for(int j=0;j int PlyReadPolygons(char* fileName, std::vector& vertices , std::vector >& polygons , PlyProperty* properties , int propertyNum , int& file_type , char*** comments , int* commentNum , bool* readFlags ) { int nr_elems; char **elist; float version; int i,j,k; PlyFile* ply; char* elem_name; int num_elems; int nr_props; PlyProperty** plist; PlyFace ply_face; ply = ply_open_for_reading(fileName, &nr_elems, &elist, &file_type, &version); if(!ply) return 0; if(comments) { (*comments)=new char*[*commentNum+ply->num_comments]; for(int i=0;inum_comments;i++) (*comments)[i]=_strdup(ply->comments[i]); *commentNum=ply->num_comments; } for (i=0; i < nr_elems; i++) { elem_name = elist[i]; plist = ply_get_element_description(ply, elem_name, &num_elems, &nr_props); if(!plist) { for(i=0;ielems[i]->name); free(ply->elems[i]->store_prop); for(j=0;jelems[i]->nprops;j++){ free(ply->elems[i]->props[j]->name); free(ply->elems[i]->props[j]); } free(ply->elems[i]->props); } for(i=0;ielems[i]);} free(ply->elems); for(i=0;inum_comments;i++){free(ply->comments[i]);} free(ply->comments); for(i=0;inum_obj_info;i++){free(ply->obj_info[i]);} free(ply->obj_info); ply_free_other_elements (ply->other_elems); for(i=0;iname); free(plist[j]); } free(plist); } // for each type of element for(i=0;ielems[i]->name); free(ply->elems[i]->store_prop); for(j=0;jelems[i]->nprops;j++){ free(ply->elems[i]->props[j]->name); free(ply->elems[i]->props[j]); } if(ply->elems[i]->props && ply->elems[i]->nprops){free(ply->elems[i]->props);} } for(i=0;ielems[i]);} free(ply->elems); for(i=0;inum_comments;i++){free(ply->comments[i]);} free(ply->comments); for(i=0;inum_obj_info;i++){free(ply->obj_info[i]);} free(ply->obj_info); ply_free_other_elements (ply->other_elems); for(i=0;i int PlyWritePolygons( char* fileName , CoredMeshData< Vertex >* mesh , int file_type , const Point3D& translate , float scale , char** comments , int commentNum , XForm4x4< Real > xForm ) { int i; int nr_vertices=int(mesh->outOfCorePointCount()+mesh->inCorePoints.size()); int nr_faces=mesh->polygonCount(); float version; const char *elem_names[] = { "vertex" , "face" }; PlyFile *ply = ply_open_for_writing( fileName , 2 , elem_names , file_type , &version ); if( !ply ) return 0; mesh->resetIterator(); // // describe vertex and face properties // ply_element_count( ply , "vertex" , nr_vertices ); for( int i=0 ; iinCorePoints.size() ) ; i++ ) { Vertex vertex = xForm * ( mesh->inCorePoints[i] * scale + translate ); ply_put_element(ply, (void *) &vertex); } for( i=0; ioutOfCorePointCount() ; i++ ) { Vertex vertex; mesh->nextOutOfCorePoint( vertex ); vertex = xForm * ( vertex * scale +translate ); ply_put_element(ply, (void *) &vertex); } // for, write vertices // write faces std::vector< CoredVertexIndex > polygon; ply_put_element_setup( ply , "face" ); for( i=0 ; inextPolygon( polygon ); ply_face.nr_vertices = int( polygon.size() ); ply_face.vertices = new int[ polygon.size() ]; for( int i=0 ; iinCorePoints.size() ); ply_put_element( ply, (void *) &ply_face ); delete[] ply_face.vertices; } // for, write faces ply_close( ply ); return 1; } template< class Vertex , class Real > int PlyWritePolygons( char* fileName , CoredMeshData< Vertex >* mesh , int file_type , char** comments , int commentNum , XForm4x4< Real > xForm ) { int i; int nr_vertices=int(mesh->outOfCorePointCount()+mesh->inCorePoints.size()); int nr_faces=mesh->polygonCount(); float version; const char *elem_names[] = { "vertex" , "face" }; PlyFile *ply = ply_open_for_writing( fileName , 2 , elem_names , file_type , &version ); if( !ply ) return 0; mesh->resetIterator(); // // describe vertex and face properties // ply_element_count( ply , "vertex" , nr_vertices ); for( int i=0 ; iinCorePoints.size() ) ; i++ ) { Vertex vertex = xForm * mesh->inCorePoints[i]; ply_put_element(ply, (void *) &vertex); } for( i=0; ioutOfCorePointCount() ; i++ ) { Vertex vertex; mesh->nextOutOfCorePoint( vertex ); vertex = xForm * ( vertex ); ply_put_element(ply, (void *) &vertex); } // for, write vertices // write faces std::vector< CoredVertexIndex > polygon; ply_put_element_setup( ply , "face" ); for( i=0 ; inextPolygon( polygon ); ply_face.nr_vertices = int( polygon.size() ); ply_face.vertices = new int[ polygon.size() ]; for( int i=0 ; iinCorePoints.size() ); ply_put_element( ply, (void *) &ply_face ); delete[] ply_face.vertices; } // for, write faces ply_close( ply ); return 1; } inline int PlyDefaultFileType(void){return PLY_ASCII;} #endif /* !__PLY_H__ */ colmap-3.9.1/src/thirdparty/PoissonRecon/PlyFile.cpp000066400000000000000000002207111454702036400224610ustar00rootroot00000000000000/* The interface routines for reading and writing PLY polygon files. Greg Turk, February 1994 --------------------------------------------------------------- A PLY file contains a single polygonal _object_. An object is composed of lists of _elements_. Typical elements are vertices, faces, edges and materials. Each type of element for a given object has one or more _properties_ associated with the element type. For instance, a vertex element may have as properties the floating-point values x,y,z and the three unsigned chars representing red, green and blue. --------------------------------------------------------------- Copyright (c) 1994 The Board of Trustees of The Leland Stanford Junior University. All rights reserved. Permission to use, copy, modify and distribute this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice and this permission notice appear in all copies of this software and that you do not sell the software. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. */ #include #include #include #include #include "Ply.h" const char *type_names[] = { "invalid", "char", "short", "int", "uchar", "ushort", "uint", "float", "double", "int8", // character 1 "uint8", // unsigned character 1 "int16", // short integer 2 "uint16", // unsigned short integer 2 "int32", // integer 4 "uint32", // unsigned integer 4 "float32", // single-precision float 4 "float64", // double-precision float 8 }; int ply_type_size[] = { 0, 1, 2, 4, 1, 2, 4, 4, 8, 1, 1, 2, 2, 4, 4, 4, 8 }; typedef union { int int_value; char byte_values[sizeof(int)]; } endian_test_type; static int native_binary_type = -1; static int types_checked = 0; #define NO_OTHER_PROPS -1 #define DONT_STORE_PROP 0 #define STORE_PROP 1 #define OTHER_PROP 0 #define NAMED_PROP 1 /* returns 1 if strings are equal, 0 if not */ int equal_strings(const char *, const char *); /* find an element in a plyfile's list */ PlyElement *find_element(PlyFile *, const char *); /* find a property in an element's list */ PlyProperty *find_property(PlyElement *, const char *, int *); /* write to a file the word describing a PLY file data type */ void write_scalar_type (FILE *, int); /* read a line from a file and break it up into separate words */ char **get_words(FILE *, int *, char **); char **old_get_words(FILE *, int *); /* write an item to a file */ void write_binary_item(FILE *, int, int, unsigned int, double, int); void write_ascii_item(FILE *, int, unsigned int, double, int); double old_write_ascii_item(FILE *, char *, int); /* add information to a PLY file descriptor */ void add_element(PlyFile *, char **); void add_property(PlyFile *, char **); void add_comment(PlyFile *, char *); void add_obj_info(PlyFile *, char *); /* copy a property */ void copy_property(PlyProperty *, PlyProperty *); /* store a value into where a pointer and a type specify */ void store_item(char *, int, int, unsigned int, double); /* return the value of a stored item */ void get_stored_item( void *, int, int *, unsigned int *, double *); /* return the value stored in an item, given ptr to it and its type */ double get_item_value(char *, int); /* get binary or ascii item and store it according to ptr and type */ void get_ascii_item(char *, int, int *, unsigned int *, double *); void get_binary_item(FILE *, int, int, int *, unsigned int *, double *); /* get a bunch of elements from a file */ void ascii_get_element(PlyFile *, char *); void binary_get_element(PlyFile *, char *); /* memory allocation */ char *my_alloc(int, int, const char *); /* byte ordering */ void get_native_binary_type(); void swap_bytes(char *, int); void check_types(); /*************/ /* Writing */ /*************/ /****************************************************************************** Given a file pointer, get ready to write PLY data to the file. Entry: fp - the given file pointer nelems - number of elements in object elem_names - list of element names file_type - file type, either ascii or binary Exit: returns a pointer to a PlyFile, used to refer to this file, or NULL if error ******************************************************************************/ PlyFile *ply_write( FILE *fp, int nelems, const char **elem_names, int file_type ) { int i; PlyFile *plyfile; PlyElement *elem; /* check for NULL file pointer */ if (fp == NULL) return (NULL); if (native_binary_type == -1) get_native_binary_type(); if (!types_checked) check_types(); /* create a record for this object */ plyfile = (PlyFile *) myalloc (sizeof (PlyFile)); if (file_type == PLY_BINARY_NATIVE) plyfile->file_type = native_binary_type; else plyfile->file_type = file_type; plyfile->num_comments = 0; plyfile->num_obj_info = 0; plyfile->nelems = nelems; plyfile->version = 1.0; plyfile->fp = fp; plyfile->other_elems = NULL; /* tuck aside the names of the elements */ plyfile->elems = (PlyElement **) myalloc (sizeof (PlyElement *) * nelems); for (i = 0; i < nelems; i++) { elem = (PlyElement *) myalloc (sizeof (PlyElement)); plyfile->elems[i] = elem; elem->name = _strdup (elem_names[i]); elem->num = 0; elem->nprops = 0; } /* return pointer to the file descriptor */ return (plyfile); } /****************************************************************************** Open a polygon file for writing. Entry: filename - name of file to read from nelems - number of elements in object elem_names - list of element names file_type - file type, either ascii or binary Exit: version - version number of PLY file returns a file identifier, used to refer to this file, or NULL if error ******************************************************************************/ PlyFile *ply_open_for_writing( char *filename, int nelems, const char **elem_names, int file_type, float *version ) { PlyFile *plyfile; char *name; FILE *fp; /* tack on the extension .ply, if necessary */ name = (char *) myalloc (int(sizeof (char) * (strlen (filename)) + 5)); strcpy (name, filename); if (strlen (name) < 4 || strcmp (name + strlen (name) - 4, ".ply") != 0) strcat (name, ".ply"); /* open the file for writing */ fp = fopen (name, "wb"); free(name); if (fp == NULL) { return (NULL); } /* create the actual PlyFile structure */ plyfile = ply_write (fp, nelems, elem_names, file_type); if (plyfile == NULL) return (NULL); /* say what PLY file version number we're writing */ *version = plyfile->version; /* return pointer to the file descriptor */ return (plyfile); } /****************************************************************************** Describe an element, including its properties and how many will be written to the file. Entry: plyfile - file identifier elem_name - name of element that information is being specified about nelems - number of elements of this type to be written nprops - number of properties contained in the element prop_list - list of properties ******************************************************************************/ void ply_describe_element( PlyFile *plyfile, char *elem_name, int nelems, int nprops, PlyProperty *prop_list ) { int i; PlyElement *elem; PlyProperty *prop; /* look for appropriate element */ elem = find_element (plyfile, elem_name); if (elem == NULL) { fprintf(stderr,"ply_describe_element: can't find element '%s'\n",elem_name); exit (-1); } elem->num = nelems; /* copy the list of properties */ elem->nprops = nprops; elem->props = (PlyProperty **) myalloc (sizeof (PlyProperty *) * nprops); elem->store_prop = (char *) myalloc (sizeof (char) * nprops); for (i = 0; i < nprops; i++) { prop = (PlyProperty *) myalloc (sizeof (PlyProperty)); elem->props[i] = prop; elem->store_prop[i] = NAMED_PROP; copy_property (prop, &prop_list[i]); } } /****************************************************************************** Describe a property of an element. Entry: plyfile - file identifier elem_name - name of element that information is being specified about prop - the new property ******************************************************************************/ void ply_describe_property( PlyFile *plyfile, const char *elem_name, PlyProperty *prop ) { PlyElement *elem; PlyProperty *elem_prop; /* look for appropriate element */ elem = find_element (plyfile, elem_name); if (elem == NULL) { fprintf(stderr, "ply_describe_property: can't find element '%s'\n", elem_name); return; } /* create room for new property */ if (elem->nprops == 0) { elem->props = (PlyProperty **) myalloc (sizeof (PlyProperty *)); elem->store_prop = (char *) myalloc (sizeof (char)); elem->nprops = 1; } else { elem->nprops++; elem->props = (PlyProperty **) realloc (elem->props, sizeof (PlyProperty *) * elem->nprops); elem->store_prop = (char *) realloc (elem->store_prop, sizeof (char) * elem->nprops); } /* copy the new property */ elem_prop = (PlyProperty *) myalloc (sizeof (PlyProperty)); elem->props[elem->nprops - 1] = elem_prop; elem->store_prop[elem->nprops - 1] = NAMED_PROP; copy_property (elem_prop, prop); } /****************************************************************************** Describe what the "other" properties are that are to be stored, and where they are in an element. ******************************************************************************/ void ply_describe_other_properties( PlyFile *plyfile, PlyOtherProp *other, int offset ) { int i; PlyElement *elem; PlyProperty *prop; /* look for appropriate element */ elem = find_element (plyfile, other->name); if (elem == NULL) { fprintf(stderr, "ply_describe_other_properties: can't find element '%s'\n", other->name); return; } /* create room for other properties */ if (elem->nprops == 0) { elem->props = (PlyProperty **) myalloc (sizeof (PlyProperty *) * other->nprops); elem->store_prop = (char *) myalloc (sizeof (char) * other->nprops); elem->nprops = 0; } else { int newsize; newsize = elem->nprops + other->nprops; elem->props = (PlyProperty **) realloc (elem->props, sizeof (PlyProperty *) * newsize); elem->store_prop = (char *) realloc (elem->store_prop, sizeof (char) * newsize); } /* copy the other properties */ for (i = 0; i < other->nprops; i++) { prop = (PlyProperty *) myalloc (sizeof (PlyProperty)); copy_property (prop, other->props[i]); elem->props[elem->nprops] = prop; elem->store_prop[elem->nprops] = OTHER_PROP; elem->nprops++; } /* save other info about other properties */ elem->other_size = other->size; elem->other_offset = offset; } /****************************************************************************** State how many of a given element will be written. Entry: plyfile - file identifier elem_name - name of element that information is being specified about nelems - number of elements of this type to be written ******************************************************************************/ void ply_element_count( PlyFile *plyfile, const char *elem_name, int nelems ) { PlyElement *elem; /* look for appropriate element */ elem = find_element (plyfile, elem_name); if (elem == NULL) { fprintf(stderr,"ply_element_count: can't find element '%s'\n",elem_name); exit (-1); } elem->num = nelems; } /****************************************************************************** Signal that we've described everything a PLY file's header and that the header should be written to the file. Entry: plyfile - file identifier ******************************************************************************/ void ply_header_complete(PlyFile *plyfile) { int i,j; FILE *fp = plyfile->fp; PlyElement *elem; PlyProperty *prop; fprintf (fp, "ply\n"); switch (plyfile->file_type) { case PLY_ASCII: fprintf (fp, "format ascii 1.0\n"); break; case PLY_BINARY_BE: fprintf (fp, "format binary_big_endian 1.0\n"); break; case PLY_BINARY_LE: fprintf (fp, "format binary_little_endian 1.0\n"); break; default: fprintf (stderr, "ply_header_complete: bad file type = %d\n", plyfile->file_type); exit (-1); } /* write out the comments */ for (i = 0; i < plyfile->num_comments; i++) fprintf (fp, "comment %s\n", plyfile->comments[i]); /* write out object information */ for (i = 0; i < plyfile->num_obj_info; i++) fprintf (fp, "obj_info %s\n", plyfile->obj_info[i]); /* write out information about each element */ for (i = 0; i < plyfile->nelems; i++) { elem = plyfile->elems[i]; fprintf (fp, "element %s %d\n", elem->name, elem->num); /* write out each property */ for (j = 0; j < elem->nprops; j++) { prop = elem->props[j]; if (prop->is_list) { fprintf (fp, "property list "); write_scalar_type (fp, prop->count_external); fprintf (fp, " "); write_scalar_type (fp, prop->external_type); fprintf (fp, " %s\n", prop->name); } else { fprintf (fp, "property "); write_scalar_type (fp, prop->external_type); fprintf (fp, " %s\n", prop->name); } } } fprintf (fp, "end_header\n"); } /****************************************************************************** Specify which elements are going to be written. This should be called before a call to the routine ply_put_element(). Entry: plyfile - file identifier elem_name - name of element we're talking about ******************************************************************************/ void ply_put_element_setup(PlyFile *plyfile, const char *elem_name) { PlyElement *elem; elem = find_element (plyfile, elem_name); if (elem == NULL) { fprintf(stderr, "ply_elements_setup: can't find element '%s'\n", elem_name); exit (-1); } plyfile->which_elem = elem; } /****************************************************************************** Write an element to the file. This routine assumes that we're writing the type of element specified in the last call to the routine ply_put_element_setup(). Entry: plyfile - file identifier elem_ptr - pointer to the element ******************************************************************************/ void ply_put_element(PlyFile *plyfile, void *elem_ptr) { int j,k; FILE *fp = plyfile->fp; PlyElement *elem; PlyProperty *prop; char *elem_data,*item; char **item_ptr; int list_count; int item_size; int int_val; unsigned int uint_val; double double_val; char **other_ptr; elem = plyfile->which_elem; elem_data = (char *)elem_ptr; other_ptr = (char **) (((char *) elem_ptr) + elem->other_offset); /* write out either to an ascii or binary file */ if (plyfile->file_type == PLY_ASCII) { /* write an ascii file */ /* write out each property of the element */ for (j = 0; j < elem->nprops; j++) { prop = elem->props[j]; if (elem->store_prop[j] == OTHER_PROP) elem_data = *other_ptr; else elem_data = (char *)elem_ptr; if (prop->is_list) { item = elem_data + prop->count_offset; get_stored_item ((void *) item, prop->count_internal, &int_val, &uint_val, &double_val); write_ascii_item (fp, int_val, uint_val, double_val, prop->count_external); list_count = uint_val; item_ptr = (char **) (elem_data + prop->offset); item = item_ptr[0]; item_size = ply_type_size[prop->internal_type]; for (k = 0; k < list_count; k++) { get_stored_item ((void *) item, prop->internal_type, &int_val, &uint_val, &double_val); write_ascii_item (fp, int_val, uint_val, double_val, prop->external_type); item += item_size; } } else { item = elem_data + prop->offset; get_stored_item ((void *) item, prop->internal_type, &int_val, &uint_val, &double_val); write_ascii_item (fp, int_val, uint_val, double_val, prop->external_type); } } fprintf (fp, "\n"); } else { /* write a binary file */ /* write out each property of the element */ for (j = 0; j < elem->nprops; j++) { prop = elem->props[j]; if (elem->store_prop[j] == OTHER_PROP) elem_data = *other_ptr; else elem_data = (char *)elem_ptr; if (prop->is_list) { item = elem_data + prop->count_offset; item_size = ply_type_size[prop->count_internal]; get_stored_item ((void *) item, prop->count_internal, &int_val, &uint_val, &double_val); write_binary_item (fp, plyfile->file_type, int_val, uint_val, double_val, prop->count_external); list_count = uint_val; item_ptr = (char **) (elem_data + prop->offset); item = item_ptr[0]; item_size = ply_type_size[prop->internal_type]; for (k = 0; k < list_count; k++) { get_stored_item ((void *) item, prop->internal_type, &int_val, &uint_val, &double_val); write_binary_item (fp, plyfile->file_type, int_val, uint_val, double_val, prop->external_type); item += item_size; } } else { item = elem_data + prop->offset; item_size = ply_type_size[prop->internal_type]; get_stored_item ((void *) item, prop->internal_type, &int_val, &uint_val, &double_val); write_binary_item (fp, plyfile->file_type, int_val, uint_val, double_val, prop->external_type); } } } } /****************************************************************************** Specify a comment that will be written in the header. Entry: plyfile - file identifier comment - the comment to be written ******************************************************************************/ void ply_put_comment(PlyFile *plyfile, char *comment) { /* (re)allocate space for new comment */ if (plyfile->num_comments == 0) plyfile->comments = (char **) myalloc (sizeof (char *)); else plyfile->comments = (char **) realloc (plyfile->comments, sizeof (char *) * (plyfile->num_comments + 1)); /* add comment to list */ plyfile->comments[plyfile->num_comments] = _strdup (comment); plyfile->num_comments++; } /****************************************************************************** Specify a piece of object information (arbitrary text) that will be written in the header. Entry: plyfile - file identifier obj_info - the text information to be written ******************************************************************************/ void ply_put_obj_info(PlyFile *plyfile, char *obj_info) { /* (re)allocate space for new info */ if (plyfile->num_obj_info == 0) plyfile->obj_info = (char **) myalloc (sizeof (char *)); else plyfile->obj_info = (char **) realloc (plyfile->obj_info, sizeof (char *) * (plyfile->num_obj_info + 1)); /* add info to list */ plyfile->obj_info[plyfile->num_obj_info] = _strdup (obj_info); plyfile->num_obj_info++; } /*************/ /* Reading */ /*************/ /****************************************************************************** Given a file pointer, get ready to read PLY data from the file. Entry: fp - the given file pointer Exit: nelems - number of elements in object elem_names - list of element names returns a pointer to a PlyFile, used to refer to this file, or NULL if error ******************************************************************************/ PlyFile *ply_read(FILE *fp, int *nelems, char ***elem_names) { int i,j; PlyFile *plyfile; int nwords; char **words; char **elist; PlyElement *elem; char *orig_line; /* check for NULL file pointer */ if (fp == NULL) return (NULL); if (native_binary_type == -1) get_native_binary_type(); if (!types_checked) check_types(); /* create record for this object */ plyfile = (PlyFile *) myalloc (sizeof (PlyFile)); plyfile->nelems = 0; plyfile->comments = NULL; plyfile->num_comments = 0; plyfile->obj_info = NULL; plyfile->num_obj_info = 0; plyfile->fp = fp; plyfile->other_elems = NULL; /* read and parse the file's header */ words = get_words (plyfile->fp, &nwords, &orig_line); if (!words || !equal_strings (words[0], "ply")) { if (words) free(words); return (NULL); } while (words) { /* parse words */ if (equal_strings (words[0], "format")) { if (nwords != 3) { free(words); return (NULL); } if (equal_strings (words[1], "ascii")) plyfile->file_type = PLY_ASCII; else if (equal_strings (words[1], "binary_big_endian")) plyfile->file_type = PLY_BINARY_BE; else if (equal_strings (words[1], "binary_little_endian")) plyfile->file_type = PLY_BINARY_LE; else { free(words); return (NULL); } plyfile->version = (float)atof (words[2]); } else if (equal_strings (words[0], "element")) add_element (plyfile, words); else if (equal_strings (words[0], "property")) add_property (plyfile, words); else if (equal_strings (words[0], "comment")) add_comment (plyfile, orig_line); else if (equal_strings (words[0], "obj_info")) add_obj_info (plyfile, orig_line); else if (equal_strings (words[0], "end_header")) { free(words); break; } /* free up words space */ free (words); words = get_words (plyfile->fp, &nwords, &orig_line); } /* create tags for each property of each element, to be used */ /* later to say whether or not to store each property for the user */ for (i = 0; i < plyfile->nelems; i++) { elem = plyfile->elems[i]; elem->store_prop = (char *) myalloc (sizeof (char) * elem->nprops); for (j = 0; j < elem->nprops; j++) elem->store_prop[j] = DONT_STORE_PROP; elem->other_offset = NO_OTHER_PROPS; /* no "other" props by default */ } /* set return values about the elements */ elist = (char **) myalloc (sizeof (char *) * plyfile->nelems); for (i = 0; i < plyfile->nelems; i++) elist[i] = _strdup (plyfile->elems[i]->name); *elem_names = elist; *nelems = plyfile->nelems; /* return a pointer to the file's information */ return (plyfile); } /****************************************************************************** Open a polygon file for reading. Entry: filename - name of file to read from Exit: nelems - number of elements in object elem_names - list of element names file_type - file type, either ascii or binary version - version number of PLY file returns a file identifier, used to refer to this file, or NULL if error ******************************************************************************/ PlyFile *ply_open_for_reading( char *filename, int *nelems, char ***elem_names, int *file_type, float *version ) { FILE *fp; PlyFile *plyfile; char *name; /* tack on the extension .ply, if necessary */ name = (char *) myalloc (int(sizeof (char) * (strlen (filename) + 5))); strcpy (name, filename); if (strlen (name) < 4 || strcmp (name + strlen (name) - 4, ".ply") != 0) strcat (name, ".ply"); /* open the file for reading */ fp = fopen (name, "rb"); free(name); if (fp == NULL) return (NULL); /* create the PlyFile data structure */ plyfile = ply_read (fp, nelems, elem_names); /* determine the file type and version */ *file_type = plyfile->file_type; *version = plyfile->version; /* return a pointer to the file's information */ return (plyfile); } /****************************************************************************** Get information about a particular element. Entry: plyfile - file identifier elem_name - name of element to get information about Exit: nelems - number of elements of this type in the file nprops - number of properties returns a list of properties, or NULL if the file doesn't contain that elem ******************************************************************************/ PlyProperty **ply_get_element_description( PlyFile *plyfile, char *elem_name, int *nelems, int *nprops ) { int i; PlyElement *elem; PlyProperty *prop; PlyProperty **prop_list; /* find information about the element */ elem = find_element (plyfile, elem_name); if (elem == NULL) return (NULL); *nelems = elem->num; *nprops = elem->nprops; /* make a copy of the element's property list */ prop_list = (PlyProperty **) myalloc (sizeof (PlyProperty *) * elem->nprops); for (i = 0; i < elem->nprops; i++) { prop = (PlyProperty *) myalloc (sizeof (PlyProperty)); copy_property (prop, elem->props[i]); prop_list[i] = prop; } /* return this duplicate property list */ return (prop_list); } /****************************************************************************** Specify which properties of an element are to be returned. This should be called before a call to the routine ply_get_element(). Entry: plyfile - file identifier elem_name - which element we're talking about nprops - number of properties prop_list - list of properties ******************************************************************************/ void ply_get_element_setup( PlyFile *plyfile, char *elem_name, int nprops, PlyProperty *prop_list ) { int i; PlyElement *elem; PlyProperty *prop; int index; /* find information about the element */ elem = find_element (plyfile, elem_name); plyfile->which_elem = elem; /* deposit the property information into the element's description */ for (i = 0; i < nprops; i++) { /* look for actual property */ prop = find_property (elem, prop_list[i].name, &index); if (prop == NULL) { fprintf (stderr, "Warning: Can't find property '%s' in element '%s'\n", prop_list[i].name, elem_name); continue; } /* store its description */ prop->internal_type = prop_list[i].internal_type; prop->offset = prop_list[i].offset; prop->count_internal = prop_list[i].count_internal; prop->count_offset = prop_list[i].count_offset; /* specify that the user wants this property */ elem->store_prop[index] = STORE_PROP; } } /****************************************************************************** Specify a property of an element that is to be returned. This should be called (usually multiple times) before a call to the routine ply_get_element(). This routine should be used in preference to the less flexible old routine called ply_get_element_setup(). Entry: plyfile - file identifier elem_name - which element we're talking about prop - property to add to those that will be returned ******************************************************************************/ int ply_get_property( PlyFile *plyfile, char *elem_name, PlyProperty *prop ) { PlyElement *elem; PlyProperty *prop_ptr; int index; /* find information about the element */ elem = find_element (plyfile, elem_name); plyfile->which_elem = elem; /* deposit the property information into the element's description */ prop_ptr = find_property (elem, prop->name, &index); if (prop_ptr == NULL) { // fprintf (stderr, "Warning: Can't find property '%s' in element '%s'\n", // prop->name, elem_name); // return; return 0; } prop_ptr->internal_type = prop->internal_type; prop_ptr->offset = prop->offset; prop_ptr->count_internal = prop->count_internal; prop_ptr->count_offset = prop->count_offset; /* specify that the user wants this property */ elem->store_prop[index] = STORE_PROP; return 1; } /****************************************************************************** Read one element from the file. This routine assumes that we're reading the type of element specified in the last call to the routine ply_get_element_setup(). Entry: plyfile - file identifier elem_ptr - pointer to location where the element information should be put ******************************************************************************/ void ply_get_element(PlyFile *plyfile, void *elem_ptr) { if (plyfile->file_type == PLY_ASCII) ascii_get_element (plyfile, (char *) elem_ptr); else binary_get_element (plyfile, (char *) elem_ptr); } /****************************************************************************** Extract the comments from the header information of a PLY file. Entry: plyfile - file identifier Exit: num_comments - number of comments returned returns a pointer to a list of comments ******************************************************************************/ char **ply_get_comments(PlyFile *plyfile, int *num_comments) { *num_comments = plyfile->num_comments; return (plyfile->comments); } /****************************************************************************** Extract the object information (arbitrary text) from the header information of a PLY file. Entry: plyfile - file identifier Exit: num_obj_info - number of lines of text information returned returns a pointer to a list of object info lines ******************************************************************************/ char **ply_get_obj_info(PlyFile *plyfile, int *num_obj_info) { *num_obj_info = plyfile->num_obj_info; return (plyfile->obj_info); } /****************************************************************************** Make ready for "other" properties of an element-- those properties that the user has not explicitly asked for, but that are to be stashed away in a special structure to be carried along with the element's other information. Entry: plyfile - file identifier elem - element for which we want to save away other properties ******************************************************************************/ void setup_other_props(PlyElement *elem) { int i; PlyProperty *prop; int size = 0; int type_size; /* Examine each property in decreasing order of size. */ /* We do this so that all data types will be aligned by */ /* word, half-word, or whatever within the structure. */ for (type_size = 8; type_size > 0; type_size /= 2) { /* add up the space taken by each property, and save this information */ /* away in the property descriptor */ for (i = 0; i < elem->nprops; i++) { /* don't bother with properties we've been asked to store explicitly */ if (elem->store_prop[i]) continue; prop = elem->props[i]; /* internal types will be same as external */ prop->internal_type = prop->external_type; prop->count_internal = prop->count_external; /* check list case */ if (prop->is_list) { /* pointer to list */ if (type_size == sizeof (void *)) { prop->offset = size; size += sizeof (void *); /* always use size of a pointer here */ } /* count of number of list elements */ if (type_size == ply_type_size[prop->count_external]) { prop->count_offset = size; size += ply_type_size[prop->count_external]; } } /* not list */ else if (type_size == ply_type_size[prop->external_type]) { prop->offset = size; size += ply_type_size[prop->external_type]; } } } /* save the size for the other_props structure */ elem->other_size = size; } /****************************************************************************** Specify that we want the "other" properties of an element to be tucked away within the user's structure. The user needn't be concerned for how these properties are stored. Entry: plyfile - file identifier elem_name - name of element that we want to store other_props in offset - offset to where other_props will be stored inside user's structure Exit: returns pointer to structure containing description of other_props ******************************************************************************/ PlyOtherProp *ply_get_other_properties( PlyFile *plyfile, char *elem_name, int offset ) { int i; PlyElement *elem; PlyOtherProp *other; PlyProperty *prop; int nprops; /* find information about the element */ elem = find_element (plyfile, elem_name); if (elem == NULL) { fprintf (stderr, "ply_get_other_properties: Can't find element '%s'\n", elem_name); return (NULL); } /* remember that this is the "current" element */ plyfile->which_elem = elem; /* save the offset to where to store the other_props */ elem->other_offset = offset; /* place the appropriate pointers, etc. in the element's property list */ setup_other_props (elem); /* create structure for describing other_props */ other = (PlyOtherProp *) myalloc (sizeof (PlyOtherProp)); other->name = _strdup (elem_name); other->size = elem->other_size; other->props = (PlyProperty **) myalloc (sizeof(PlyProperty) * elem->nprops); /* save descriptions of each "other" property */ nprops = 0; for (i = 0; i < elem->nprops; i++) { if (elem->store_prop[i]) continue; prop = (PlyProperty *) myalloc (sizeof (PlyProperty)); copy_property (prop, elem->props[i]); other->props[nprops] = prop; nprops++; } other->nprops = nprops; /* set other_offset pointer appropriately if there are NO other properties */ if (other->nprops == 0) { elem->other_offset = NO_OTHER_PROPS; } /* return structure */ return (other); } /*************************/ /* Other Element Stuff */ /*************************/ /****************************************************************************** Grab all the data for an element that a user does not want to explicitly read in. Entry: plyfile - pointer to file elem_name - name of element whose data is to be read in elem_count - number of instances of this element stored in the file Exit: returns pointer to ALL the "other" element data for this PLY file ******************************************************************************/ PlyOtherElems *ply_get_other_element ( PlyFile *plyfile, char *elem_name, int elem_count ) { int i; PlyElement *elem; PlyOtherElems *other_elems; OtherElem *other; /* look for appropriate element */ elem = find_element (plyfile, elem_name); if (elem == NULL) { fprintf (stderr, "ply_get_other_element: can't find element '%s'\n", elem_name); exit (-1); } /* create room for the new "other" element, initializing the */ /* other data structure if necessary */ if (plyfile->other_elems == NULL) { plyfile->other_elems = (PlyOtherElems *) myalloc (sizeof (PlyOtherElems)); other_elems = plyfile->other_elems; other_elems->other_list = (OtherElem *) myalloc (sizeof (OtherElem)); other = &(other_elems->other_list[0]); other_elems->num_elems = 1; } else { other_elems = plyfile->other_elems; other_elems->other_list = (OtherElem *) realloc (other_elems->other_list, sizeof (OtherElem) * other_elems->num_elems + 1); other = &(other_elems->other_list[other_elems->num_elems]); other_elems->num_elems++; } /* count of element instances in file */ other->elem_count = elem_count; /* save name of element */ other->elem_name = _strdup (elem_name); /* create a list to hold all the current elements */ other->other_data = (OtherData **) malloc (sizeof (OtherData *) * other->elem_count); /* set up for getting elements */ other->other_props = ply_get_other_properties (plyfile, elem_name, offsetof(OtherData,other_props)); /* grab all these elements */ for (i = 0; i < other->elem_count; i++) { /* grab and element from the file */ other->other_data[i] = (OtherData *) malloc (sizeof (OtherData)); ply_get_element (plyfile, (void *) other->other_data[i]); } /* return pointer to the other elements data */ return (other_elems); } /****************************************************************************** Pass along a pointer to "other" elements that we want to save in a given PLY file. These other elements were presumably read from another PLY file. Entry: plyfile - file pointer in which to store this other element info other_elems - info about other elements that we want to store ******************************************************************************/ void ply_describe_other_elements ( PlyFile *plyfile, PlyOtherElems *other_elems ) { int i; OtherElem *other; PlyElement *elem; /* ignore this call if there is no other element */ if (other_elems == NULL) return; /* save pointer to this information */ plyfile->other_elems = other_elems; /* describe the other properties of this element */ /* store them in the main element list as elements with only other properties */ REALLOCN(plyfile->elems, PlyElement *, plyfile->nelems, plyfile->nelems + other_elems->num_elems); for (i = 0; i < other_elems->num_elems; i++) { other = &(other_elems->other_list[i]); elem = (PlyElement *) myalloc (sizeof (PlyElement)); plyfile->elems[plyfile->nelems++] = elem; elem->name = _strdup (other->elem_name); elem->num = other->elem_count; elem->nprops = 0; ply_describe_other_properties (plyfile, other->other_props, offsetof(OtherData,other_props)); } } /****************************************************************************** Write out the "other" elements specified for this PLY file. Entry: plyfile - pointer to PLY file to write out other elements for ******************************************************************************/ void ply_put_other_elements (PlyFile *plyfile) { int i,j; OtherElem *other; /* make sure we have other elements to write */ if (plyfile->other_elems == NULL) return; /* write out the data for each "other" element */ for (i = 0; i < plyfile->other_elems->num_elems; i++) { other = &(plyfile->other_elems->other_list[i]); ply_put_element_setup (plyfile, other->elem_name); /* write out each instance of the current element */ for (j = 0; j < other->elem_count; j++) ply_put_element (plyfile, (void *) other->other_data[j]); } } /****************************************************************************** Free up storage used by an "other" elements data structure. Entry: other_elems - data structure to free up ******************************************************************************/ void ply_free_other_elements (PlyOtherElems *other_elems) { other_elems = other_elems; } /*******************/ /* Miscellaneous */ /*******************/ /****************************************************************************** Close a PLY file. Entry: plyfile - identifier of file to close ******************************************************************************/ void ply_close(PlyFile *plyfile) { fclose (plyfile->fp); /* free up memory associated with the PLY file */ free (plyfile); } /****************************************************************************** Get version number and file type of a PlyFile. Entry: ply - pointer to PLY file Exit: version - version of the file file_type - PLY_ASCII, PLY_BINARY_BE, or PLY_BINARY_LE ******************************************************************************/ void ply_get_info(PlyFile *ply, float *version, int *file_type) { if (ply == NULL) return; *version = ply->version; *file_type = ply->file_type; } /****************************************************************************** Compare two strings. Returns 1 if they are the same, 0 if not. ******************************************************************************/ int equal_strings(const char *s1, const char *s2) { while (*s1 && *s2) if (*s1++ != *s2++) return (0); if (*s1 != *s2) return (0); else return (1); } /****************************************************************************** Find an element from the element list of a given PLY object. Entry: plyfile - file id for PLY file element - name of element we're looking for Exit: returns the element, or NULL if not found ******************************************************************************/ PlyElement *find_element(PlyFile *plyfile, const char *element) { int i; for (i = 0; i < plyfile->nelems; i++) if (equal_strings (element, plyfile->elems[i]->name)) return (plyfile->elems[i]); return (NULL); } /****************************************************************************** Find a property in the list of properties of a given element. Entry: elem - pointer to element in which we want to find the property prop_name - name of property to find Exit: index - index to position in list returns a pointer to the property, or NULL if not found ******************************************************************************/ PlyProperty *find_property(PlyElement *elem, const char *prop_name, int *index) { int i; for (i = 0; i < elem->nprops; i++) if (equal_strings (prop_name, elem->props[i]->name)) { *index = i; return (elem->props[i]); } *index = -1; return (NULL); } /****************************************************************************** Read an element from an ascii file. Entry: plyfile - file identifier elem_ptr - pointer to element ******************************************************************************/ void ascii_get_element(PlyFile *plyfile, char *elem_ptr) { int j,k; PlyElement *elem; PlyProperty *prop; char **words; int nwords; int which_word; char *elem_data,*item=NULL; char *item_ptr; int item_size; int int_val; unsigned int uint_val; double double_val; int list_count; int store_it; char **store_array; char *orig_line; char *other_data=NULL; int other_flag; /* the kind of element we're reading currently */ elem = plyfile->which_elem; /* do we need to setup for other_props? */ if (elem->other_offset != NO_OTHER_PROPS) { char **ptr; other_flag = 1; /* make room for other_props */ other_data = (char *) myalloc (elem->other_size); /* store pointer in user's structure to the other_props */ ptr = (char **) (elem_ptr + elem->other_offset); *ptr = other_data; } else other_flag = 0; /* read in the element */ words = get_words (plyfile->fp, &nwords, &orig_line); if (words == NULL) { fprintf (stderr, "ply_get_element: unexpected end of file\n"); exit (-1); } which_word = 0; for (j = 0; j < elem->nprops; j++) { prop = elem->props[j]; store_it = (elem->store_prop[j] | other_flag); /* store either in the user's structure or in other_props */ if (elem->store_prop[j]) elem_data = elem_ptr; else elem_data = other_data; if (prop->is_list) { /* a list */ /* get and store the number of items in the list */ get_ascii_item (words[which_word++], prop->count_external, &int_val, &uint_val, &double_val); if (store_it) { item = elem_data + prop->count_offset; store_item(item, prop->count_internal, int_val, uint_val, double_val); } /* allocate space for an array of items and store a ptr to the array */ list_count = int_val; item_size = ply_type_size[prop->internal_type]; store_array = (char **) (elem_data + prop->offset); if (list_count == 0) { if (store_it) *store_array = NULL; } else { if (store_it) { item_ptr = (char *) myalloc (sizeof (char) * item_size * list_count); item = item_ptr; *store_array = item_ptr; } /* read items and store them into the array */ for (k = 0; k < list_count; k++) { get_ascii_item (words[which_word++], prop->external_type, &int_val, &uint_val, &double_val); if (store_it) { store_item (item, prop->internal_type, int_val, uint_val, double_val); item += item_size; } } } } else { /* not a list */ get_ascii_item (words[which_word++], prop->external_type, &int_val, &uint_val, &double_val); if (store_it) { item = elem_data + prop->offset; store_item (item, prop->internal_type, int_val, uint_val, double_val); } } } free (words); } /****************************************************************************** Read an element from a binary file. Entry: plyfile - file identifier elem_ptr - pointer to an element ******************************************************************************/ void binary_get_element(PlyFile *plyfile, char *elem_ptr) { int j,k; PlyElement *elem; PlyProperty *prop; FILE *fp = plyfile->fp; char *elem_data,*item=NULL; char *item_ptr; int item_size; int int_val; unsigned int uint_val; double double_val; int list_count; int store_it; char **store_array; char *other_data=NULL; int other_flag; /* the kind of element we're reading currently */ elem = plyfile->which_elem; /* do we need to setup for other_props? */ if (elem->other_offset != NO_OTHER_PROPS) { char **ptr; other_flag = 1; /* make room for other_props */ other_data = (char *) myalloc (elem->other_size); /* store pointer in user's structure to the other_props */ ptr = (char **) (elem_ptr + elem->other_offset); *ptr = other_data; } else other_flag = 0; /* read in a number of elements */ for (j = 0; j < elem->nprops; j++) { prop = elem->props[j]; store_it = (elem->store_prop[j] | other_flag); /* store either in the user's structure or in other_props */ if (elem->store_prop[j]) elem_data = elem_ptr; else elem_data = other_data; if (prop->is_list) { /* a list */ /* get and store the number of items in the list */ get_binary_item (fp, plyfile->file_type, prop->count_external, &int_val, &uint_val, &double_val); if (store_it) { item = elem_data + prop->count_offset; store_item(item, prop->count_internal, int_val, uint_val, double_val); } /* allocate space for an array of items and store a ptr to the array */ list_count = int_val; item_size = ply_type_size[prop->internal_type]; store_array = (char **) (elem_data + prop->offset); if (list_count == 0) { if (store_it) *store_array = NULL; } else { if (store_it) { item_ptr = (char *) myalloc (sizeof (char) * item_size * list_count); item = item_ptr; *store_array = item_ptr; } /* read items and store them into the array */ for (k = 0; k < list_count; k++) { get_binary_item (fp, plyfile->file_type, prop->external_type, &int_val, &uint_val, &double_val); if (store_it) { store_item (item, prop->internal_type, int_val, uint_val, double_val); item += item_size; } } } } else { /* not a list */ get_binary_item (fp, plyfile->file_type, prop->external_type, &int_val, &uint_val, &double_val); if (store_it) { item = elem_data + prop->offset; store_item (item, prop->internal_type, int_val, uint_val, double_val); } } } } /****************************************************************************** Write to a file the word that represents a PLY data type. Entry: fp - file pointer code - code for type ******************************************************************************/ void write_scalar_type (FILE *fp, int code) { /* make sure this is a valid code */ if (code <= PLY_START_TYPE || code >= PLY_END_TYPE) { fprintf (stderr, "write_scalar_type: bad data code = %d\n", code); exit (-1); } /* write the code to a file */ fprintf (fp, "%s", type_names[code]); } /****************************************************************************** Reverse the order in an array of bytes. This is the conversion from big endian to little endian and vice versa Entry: bytes - array of bytes to reverse (in place) num_bytes - number of bytes in array ******************************************************************************/ void swap_bytes(char *bytes, int num_bytes) { int i; char temp; for (i=0; i < num_bytes/2; i++) { temp = bytes[i]; bytes[i] = bytes[(num_bytes-1)-i]; bytes[(num_bytes-1)-i] = temp; } } /****************************************************************************** Find out if this machine is big endian or little endian Exit: set global variable, native_binary_type = either PLY_BINARY_BE or PLY_BINARY_LE ******************************************************************************/ void get_native_binary_type() { endian_test_type test; test.int_value = 0; test.int_value = 1; if (test.byte_values[0] == 1) native_binary_type = PLY_BINARY_LE; else if (test.byte_values[sizeof(int)-1] == 1) native_binary_type = PLY_BINARY_BE; else { fprintf(stderr, "ply: Couldn't determine machine endianness.\n"); fprintf(stderr, "ply: Exiting...\n"); exit(1); } } /****************************************************************************** Verify that all the native types are the sizes we need ******************************************************************************/ void check_types() { if ((ply_type_size[PLY_CHAR] != sizeof(char)) || (ply_type_size[PLY_SHORT] != sizeof(short)) || (ply_type_size[PLY_INT] != sizeof(int)) || (ply_type_size[PLY_UCHAR] != sizeof(unsigned char)) || (ply_type_size[PLY_USHORT] != sizeof(unsigned short)) || (ply_type_size[PLY_UINT] != sizeof(unsigned int)) || (ply_type_size[PLY_FLOAT] != sizeof(float)) || (ply_type_size[PLY_DOUBLE] != sizeof(double))) { fprintf(stderr, "ply: Type sizes do not match built-in types\n"); fprintf(stderr, "ply: Exiting...\n"); exit(1); } types_checked = 1; } /****************************************************************************** Get a text line from a file and break it up into words. IMPORTANT: The calling routine call "free" on the returned pointer once finished with it. Entry: fp - file to read from Exit: nwords - number of words returned orig_line - the original line of characters returns a list of words from the line, or NULL if end-of-file ******************************************************************************/ char **get_words(FILE *fp, int *nwords, char **orig_line) { #define BIG_STRING 4096 static char str[BIG_STRING]; static char str_copy[BIG_STRING]; char **words; int max_words = 10; int num_words = 0; char *ptr,*ptr2; char *result; words = (char **) myalloc (sizeof (char *) * max_words); /* read in a line */ result = fgets (str, BIG_STRING, fp); if (result == NULL) { free(words); *nwords = 0; *orig_line = NULL; return (NULL); } /* convert line-feed and tabs into spaces */ /* (this guarentees that there will be a space before the */ /* null character at the end of the string) */ str[BIG_STRING-2] = ' '; str[BIG_STRING-1] = '\0'; for (ptr = str, ptr2 = str_copy; *ptr != '\0'; ptr++, ptr2++) { *ptr2 = *ptr; // Added line here to manage carriage returns if (*ptr == '\t' || *ptr == '\r') { *ptr = ' '; *ptr2 = ' '; } else if (*ptr == '\n') { *ptr = ' '; *ptr2 = '\0'; break; } } /* find the words in the line */ ptr = str; while (*ptr != '\0') { /* jump over leading spaces */ while (*ptr == ' ') ptr++; /* break if we reach the end */ if (*ptr == '\0') break; /* save pointer to beginning of word */ if (num_words >= max_words) { max_words += 10; words = (char **) realloc (words, sizeof (char *) * max_words); } words[num_words++] = ptr; /* jump over non-spaces */ while (*ptr != ' ') ptr++; /* place a null character here to mark the end of the word */ *ptr++ = '\0'; } /* return the list of words */ *nwords = num_words; *orig_line = str_copy; return (words); } /****************************************************************************** Return the value of an item, given a pointer to it and its type. Entry: item - pointer to item type - data type that "item" points to Exit: returns a double-precision float that contains the value of the item ******************************************************************************/ double get_item_value(char *item, int type) { unsigned char *puchar; char *pchar; short int *pshort; unsigned short int *pushort; int *pint; unsigned int *puint; float *pfloat; double *pdouble; int int_value; unsigned int uint_value; double double_value; switch (type) { case PLY_CHAR: case PLY_INT_8: pchar = (char *) item; int_value = *pchar; return ((double) int_value); case PLY_UCHAR: case PLY_UINT_8: puchar = (unsigned char *) item; int_value = *puchar; return ((double) int_value); case PLY_SHORT: case PLY_INT_16: pshort = (short int *) item; int_value = *pshort; return ((double) int_value); case PLY_USHORT: case PLY_UINT_16: pushort = (unsigned short int *) item; int_value = *pushort; return ((double) int_value); case PLY_INT: case PLY_INT_32: pint = (int *) item; int_value = *pint; return ((double) int_value); case PLY_UINT: case PLY_UINT_32: puint = (unsigned int *) item; uint_value = *puint; return ((double) uint_value); case PLY_FLOAT: case PLY_FLOAT_32: pfloat = (float *) item; double_value = *pfloat; return (double_value); case PLY_DOUBLE: case PLY_FLOAT_64: pdouble = (double *) item; double_value = *pdouble; return (double_value); default: fprintf (stderr, "get_item_value: bad type = %d\n", type); exit (-1); } } /****************************************************************************** Write out an item to a file as raw binary bytes. Entry: fp - file to write to int_val - integer version of item uint_val - unsigned integer version of item double_val - double-precision float version of item type - data type to write out ******************************************************************************/ void write_binary_item( FILE *fp, int file_type, int int_val, unsigned int uint_val, double double_val, int type ) { unsigned char uchar_val; char char_val; unsigned short ushort_val; short short_val; float float_val; void *value; switch (type) { case PLY_CHAR: case PLY_INT_8: char_val = char(int_val); value = &char_val; break; case PLY_SHORT: case PLY_INT_16: short_val = short(int_val); value = &short_val; break; case PLY_INT: case PLY_INT_32: value = &int_val; break; case PLY_UCHAR: case PLY_UINT_8: uchar_val = (unsigned char)(uint_val); value = &uchar_val; break; case PLY_USHORT: case PLY_UINT_16: ushort_val = (unsigned short)(uint_val); value = &ushort_val; break; case PLY_UINT: case PLY_UINT_32: value = &uint_val; break; case PLY_FLOAT: case PLY_FLOAT_32: float_val = (float)double_val; value = &float_val; break; case PLY_DOUBLE: case PLY_FLOAT_64: value = &double_val; break; default: fprintf (stderr, "write_binary_item: bad type = %d\n", type); exit (-1); } if ((file_type != native_binary_type) && (ply_type_size[type] > 1)) swap_bytes((char *)value, ply_type_size[type]); if (fwrite (value, ply_type_size[type], 1, fp) != 1) { fprintf(stderr, "PLY ERROR: fwrite() failed -- aborting.\n"); exit(1); } } /****************************************************************************** Write out an item to a file as ascii characters. Entry: fp - file to write to int_val - integer version of item uint_val - unsigned integer version of item double_val - double-precision float version of item type - data type to write out ******************************************************************************/ void write_ascii_item( FILE *fp, int int_val, unsigned int uint_val, double double_val, int type ) { switch (type) { case PLY_CHAR: case PLY_INT_8: case PLY_SHORT: case PLY_INT_16: case PLY_INT: case PLY_INT_32: if (fprintf (fp, "%d ", int_val) <= 0) { fprintf(stderr, "PLY ERROR: fprintf() failed -- aborting.\n"); exit(1); } break; case PLY_UCHAR: case PLY_UINT_8: case PLY_USHORT: case PLY_UINT_16: case PLY_UINT: case PLY_UINT_32: if (fprintf (fp, "%u ", uint_val) <= 0) { fprintf(stderr, "PLY ERROR: fprintf() failed -- aborting.\n"); exit(1); } break; case PLY_FLOAT: case PLY_FLOAT_32: case PLY_DOUBLE: case PLY_FLOAT_64: if (fprintf (fp, "%g ", double_val) <= 0) { fprintf(stderr, "PLY ERROR: fprintf() failed -- aborting.\n"); exit(1); } break; default: fprintf (stderr, "write_ascii_item: bad type = %d\n", type); exit (-1); } } /****************************************************************************** Write out an item to a file as ascii characters. Entry: fp - file to write to item - pointer to item to write type - data type that "item" points to Exit: returns a double-precision float that contains the value of the written item ******************************************************************************/ double old_write_ascii_item(FILE *fp, char *item, int type) { unsigned char *puchar; char *pchar; short int *pshort; unsigned short int *pushort; int *pint; unsigned int *puint; float *pfloat; double *pdouble; int int_value; unsigned int uint_value; double double_value; switch (type) { case PLY_CHAR: case PLY_INT_8: pchar = (char *) item; int_value = *pchar; fprintf (fp, "%d ", int_value); return ((double) int_value); case PLY_UCHAR: case PLY_UINT_8: puchar = (unsigned char *) item; int_value = *puchar; fprintf (fp, "%d ", int_value); return ((double) int_value); case PLY_SHORT: case PLY_INT_16: pshort = (short int *) item; int_value = *pshort; fprintf (fp, "%d ", int_value); return ((double) int_value); case PLY_USHORT: case PLY_UINT_16: pushort = (unsigned short int *) item; int_value = *pushort; fprintf (fp, "%d ", int_value); return ((double) int_value); case PLY_INT: case PLY_INT_32: pint = (int *) item; int_value = *pint; fprintf (fp, "%d ", int_value); return ((double) int_value); case PLY_UINT: case PLY_UINT_32: puint = (unsigned int *) item; uint_value = *puint; fprintf (fp, "%u ", uint_value); return ((double) uint_value); case PLY_FLOAT: case PLY_FLOAT_32: pfloat = (float *) item; double_value = *pfloat; fprintf (fp, "%g ", double_value); return (double_value); case PLY_DOUBLE: case PLY_FLOAT_64: pdouble = (double *) item; double_value = *pdouble; fprintf (fp, "%g ", double_value); return (double_value); default: fprintf (stderr, "old_write_ascii_item: bad type = %d\n", type); exit (-1); } } /****************************************************************************** Get the value of an item that is in memory, and place the result into an integer, an unsigned integer and a double. Entry: ptr - pointer to the item type - data type supposedly in the item Exit: int_val - integer value uint_val - unsigned integer value double_val - double-precision floating point value ******************************************************************************/ void get_stored_item( void *ptr, int type, int *int_val, unsigned int *uint_val, double *double_val ) { switch (type) { case PLY_CHAR: case PLY_INT_8: *int_val = *((char *) ptr); *uint_val = *int_val; *double_val = *int_val; break; case PLY_UCHAR: case PLY_UINT_8: *uint_val = *((unsigned char *) ptr); *int_val = *uint_val; *double_val = *uint_val; break; case PLY_SHORT: case PLY_INT_16: *int_val = *((short int *) ptr); *uint_val = *int_val; *double_val = *int_val; break; case PLY_USHORT: case PLY_UINT_16: *uint_val = *((unsigned short int *) ptr); *int_val = *uint_val; *double_val = *uint_val; break; case PLY_INT: case PLY_INT_32: *int_val = *((int *) ptr); *uint_val = *int_val; *double_val = *int_val; break; case PLY_UINT: case PLY_UINT_32: *uint_val = *((unsigned int *) ptr); *int_val = *uint_val; *double_val = *uint_val; break; case PLY_FLOAT: case PLY_FLOAT_32: *double_val = *((float *) ptr); *int_val = (int) *double_val; *uint_val = (unsigned int) *double_val; break; case PLY_DOUBLE: case PLY_FLOAT_64: *double_val = *((double *) ptr); *int_val = (int) *double_val; *uint_val = (unsigned int) *double_val; break; default: fprintf (stderr, "get_stored_item: bad type = %d\n", type); exit (-1); } } /****************************************************************************** Get the value of an item from a binary file, and place the result into an integer, an unsigned integer and a double. Entry: fp - file to get item from type - data type supposedly in the word Exit: int_val - integer value uint_val - unsigned integer value double_val - double-precision floating point value ******************************************************************************/ void get_binary_item( FILE *fp, int file_type, int type, int *int_val, unsigned int *uint_val, double *double_val ) { char c[8]; void *ptr; ptr = (void *) c; if (fread (ptr, ply_type_size[type], 1, fp) != 1) { fprintf(stderr, "PLY ERROR: fread() failed -- aborting.\n"); exit(1); } if ((file_type != native_binary_type) && (ply_type_size[type] > 1)) swap_bytes((char *)ptr, ply_type_size[type]); switch (type) { case PLY_CHAR: case PLY_INT_8: *int_val = *((char *) ptr); *uint_val = *int_val; *double_val = *int_val; break; case PLY_UCHAR: case PLY_UINT_8: *uint_val = *((unsigned char *) ptr); *int_val = *uint_val; *double_val = *uint_val; break; case PLY_SHORT: case PLY_INT_16: *int_val = *((short int *) ptr); *uint_val = *int_val; *double_val = *int_val; break; case PLY_USHORT: case PLY_UINT_16: *uint_val = *((unsigned short int *) ptr); *int_val = *uint_val; *double_val = *uint_val; break; case PLY_INT: case PLY_INT_32: *int_val = *((int *) ptr); *uint_val = *int_val; *double_val = *int_val; break; case PLY_UINT: case PLY_UINT_32: *uint_val = *((unsigned int *) ptr); *int_val = *uint_val; *double_val = *uint_val; break; case PLY_FLOAT: case PLY_FLOAT_32: *double_val = *((float *) ptr); *int_val = (int) *double_val; *uint_val = (unsigned int) *double_val; break; case PLY_DOUBLE: case PLY_FLOAT_64: *double_val = *((double *) ptr); *int_val = (int) *double_val; *uint_val = (unsigned int) *double_val; break; default: fprintf (stderr, "get_binary_item: bad type = %d\n", type); exit (-1); } } /****************************************************************************** Extract the value of an item from an ascii word, and place the result into an integer, an unsigned integer and a double. Entry: word - word to extract value from type - data type supposedly in the word Exit: int_val - integer value uint_val - unsigned integer value double_val - double-precision floating point value ******************************************************************************/ void get_ascii_item( char *word, int type, int *int_val, unsigned int *uint_val, double *double_val ) { switch (type) { case PLY_CHAR: case PLY_INT_8: case PLY_UCHAR: case PLY_UINT_8: case PLY_SHORT: case PLY_INT_16: case PLY_USHORT: case PLY_UINT_16: case PLY_INT: case PLY_INT_32: *int_val = atoi (word); *uint_val = (unsigned int) *int_val; *double_val = (double) *int_val; break; case PLY_UINT: case PLY_UINT_32: *uint_val = strtol (word, (char **) NULL, 10); *int_val = (int) *uint_val; *double_val = (double) *uint_val; break; case PLY_FLOAT: case PLY_FLOAT_32: case PLY_DOUBLE: case PLY_FLOAT_64: *double_val = atof (word); *int_val = (int) *double_val; *uint_val = (unsigned int) *double_val; break; default: fprintf (stderr, "get_ascii_item: bad type = %d\n", type); exit (-1); } } /****************************************************************************** Store a value into a place being pointed to, guided by a data type. Entry: item - place to store value type - data type int_val - integer version of value uint_val - unsigned integer version of value double_val - double version of value Exit: item - pointer to stored value ******************************************************************************/ void store_item ( char *item, int type, int int_val, unsigned int uint_val, double double_val ) { unsigned char *puchar; short int *pshort; unsigned short int *pushort; int *pint; unsigned int *puint; float *pfloat; double *pdouble; switch (type) { case PLY_CHAR: case PLY_INT_8: *item = char(int_val); break; case PLY_UCHAR: case PLY_UINT_8: puchar = (unsigned char *) item; *puchar = (unsigned char)(uint_val); break; case PLY_SHORT: case PLY_INT_16: pshort = (short *) item; *pshort = short(int_val); break; case PLY_USHORT: case PLY_UINT_16: pushort = (unsigned short *) item; *pushort = (unsigned short)(uint_val); break; case PLY_INT: case PLY_INT_32: pint = (int *) item; *pint = int_val; break; case PLY_UINT: case PLY_UINT_32: puint = (unsigned int *) item; *puint = uint_val; break; case PLY_FLOAT: case PLY_FLOAT_32: pfloat = (float *) item; *pfloat = (float)double_val; break; case PLY_DOUBLE: case PLY_FLOAT_64: pdouble = (double *) item; *pdouble = double_val; break; default: fprintf (stderr, "store_item: bad type = %d\n", type); exit (-1); } } /****************************************************************************** Add an element to a PLY file descriptor. Entry: plyfile - PLY file descriptor words - list of words describing the element nwords - number of words in the list ******************************************************************************/ void add_element (PlyFile *plyfile, char **words) { PlyElement *elem; /* create the new element */ elem = (PlyElement *) myalloc (sizeof (PlyElement)); elem->name = _strdup (words[1]); elem->num = atoi (words[2]); elem->nprops = 0; /* make room for new element in the object's list of elements */ if (plyfile->nelems == 0) plyfile->elems = (PlyElement **) myalloc (sizeof (PlyElement *)); else plyfile->elems = (PlyElement **) realloc (plyfile->elems, sizeof (PlyElement *) * (plyfile->nelems + 1)); /* add the new element to the object's list */ plyfile->elems[plyfile->nelems] = elem; plyfile->nelems++; } /****************************************************************************** Return the type of a property, given the name of the property. Entry: name - name of property type Exit: returns integer code for property, or 0 if not found ******************************************************************************/ int get_prop_type(char *type_name) { int i; for (i = PLY_START_TYPE + 1; i < PLY_END_TYPE; i++) if (equal_strings (type_name, type_names[i])) return (i); /* if we get here, we didn't find the type */ return (0); } /****************************************************************************** Add a property to a PLY file descriptor. Entry: plyfile - PLY file descriptor words - list of words describing the property nwords - number of words in the list ******************************************************************************/ void add_property (PlyFile *plyfile, char **words) { PlyProperty *prop; PlyElement *elem; /* create the new property */ prop = (PlyProperty *) myalloc (sizeof (PlyProperty)); if (equal_strings (words[1], "list")) { /* is a list */ prop->count_external = get_prop_type (words[2]); prop->external_type = get_prop_type (words[3]); prop->name = _strdup (words[4]); prop->is_list = 1; } else { /* not a list */ prop->external_type = get_prop_type (words[1]); prop->name = _strdup (words[2]); prop->is_list = 0; } /* add this property to the list of properties of the current element */ elem = plyfile->elems[plyfile->nelems - 1]; if (elem->nprops == 0) elem->props = (PlyProperty **) myalloc (sizeof (PlyProperty *)); else elem->props = (PlyProperty **) realloc (elem->props, sizeof (PlyProperty *) * (elem->nprops + 1)); elem->props[elem->nprops] = prop; elem->nprops++; } /****************************************************************************** Add a comment to a PLY file descriptor. Entry: plyfile - PLY file descriptor line - line containing comment ******************************************************************************/ void add_comment (PlyFile *plyfile, char *line) { int i; /* skip over "comment" and leading spaces and tabs */ i = 7; while (line[i] == ' ' || line[i] == '\t') i++; ply_put_comment (plyfile, &line[i]); } /****************************************************************************** Add a some object information to a PLY file descriptor. Entry: plyfile - PLY file descriptor line - line containing text info ******************************************************************************/ void add_obj_info (PlyFile *plyfile, char *line) { int i; /* skip over "obj_info" and leading spaces and tabs */ i = 8; while (line[i] == ' ' || line[i] == '\t') i++; ply_put_obj_info (plyfile, &line[i]); } /****************************************************************************** Copy a property. ******************************************************************************/ void copy_property(PlyProperty *dest, PlyProperty *src) { dest->name = _strdup (src->name); dest->external_type = src->external_type; dest->internal_type = src->internal_type; dest->offset = src->offset; dest->is_list = src->is_list; dest->count_external = src->count_external; dest->count_internal = src->count_internal; dest->count_offset = src->count_offset; } /****************************************************************************** Allocate some memory. Entry: size - amount of memory requested (in bytes) lnum - line number from which memory was requested fname - file name from which memory was requested ******************************************************************************/ char *my_alloc(int size, int lnum, const char *fname) { char *ptr; ptr = (char *) malloc (size); if (ptr == 0) { fprintf(stderr, "Memory allocation bombed on line %d in %s\n", lnum, fname); } return (ptr); } colmap-3.9.1/src/thirdparty/PoissonRecon/PointStream.h000066400000000000000000000134651454702036400230350ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior writften 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 OWNER 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. */ #ifndef POINT_STREAM_INCLUDED #define POINT_STREAM_INCLUDED #include "Ply.h" template< class Real > struct OrientedPoint3D { Point3D< Real > p , n; OrientedPoint3D( Point3D< Real > pp=Point3D< Real >() , Point3D< Real > nn=Point3D< Real >() ) : p(pp) , n(nn) { ; } }; template< class Real > class OrientedPointStream { public: virtual ~OrientedPointStream( void ){} virtual void reset( void ) = 0; virtual bool nextPoint( OrientedPoint3D< Real >& p ) = 0; }; template< class Real , class Data > class OrientedPointStreamWithData : public OrientedPointStream< Real > { public: virtual ~OrientedPointStreamWithData( void ){} virtual void reset( void ) = 0; virtual bool nextPoint( OrientedPoint3D< Real >& p , Data& d ) = 0; virtual bool nextPoint( OrientedPoint3D< Real >& p ){ Data d ; return nextPoint( p , d ); } }; template< class Real > class MemoryOrientedPointStream : public OrientedPointStream< Real > { const OrientedPoint3D< Real >* _points; size_t _pointCount; size_t _current; public: MemoryOrientedPointStream( size_t pointCount , const OrientedPoint3D< Real >* points ); ~MemoryOrientedPointStream( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p ); }; template< class Real , class Data > class MemoryOrientedPointStreamWithData : public OrientedPointStreamWithData< Real , Data > { const std::pair< OrientedPoint3D< Real > , Data >* _points; size_t _pointCount; size_t _current; public: MemoryOrientedPointStreamWithData( size_t pointCount , const std::pair< OrientedPoint3D< Real > , Data >* points ); ~MemoryOrientedPointStreamWithData( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p , Data& d ); }; template< class Real > class ASCIIOrientedPointStream : public OrientedPointStream< Real > { FILE* _fp; public: ASCIIOrientedPointStream( const char* fileName ); ~ASCIIOrientedPointStream( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p ); }; template< class Real , class Data > class ASCIIOrientedPointStreamWithData : public OrientedPointStreamWithData< Real , Data > { FILE* _fp; Data (*_readData)( FILE* ); public: ASCIIOrientedPointStreamWithData( const char* fileName , Data (*readData)( FILE* ) ); ~ASCIIOrientedPointStreamWithData( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p , Data& d ); }; template< class Real > class BinaryOrientedPointStream : public OrientedPointStream< Real > { FILE* _fp; static const int POINT_BUFFER_SIZE=1024; OrientedPoint3D< Real > _pointBuffer[ POINT_BUFFER_SIZE ]; int _pointsInBuffer , _currentPointIndex; public: BinaryOrientedPointStream( const char* filename ); ~BinaryOrientedPointStream( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p ); }; template< class Real , class Data > class BinaryOrientedPointStreamWithData : public OrientedPointStreamWithData< Real , Data > { FILE* _fp; static const int POINT_BUFFER_SIZE=1024; std::pair< OrientedPoint3D< Real > , Data > _pointBuffer[ POINT_BUFFER_SIZE ]; int _pointsInBuffer , _currentPointIndex; public: BinaryOrientedPointStreamWithData( const char* filename ); ~BinaryOrientedPointStreamWithData( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p , Data& d ); }; template< class Real > class PLYOrientedPointStream : public OrientedPointStream< Real > { char* _fileName; PlyFile* _ply; int _nr_elems; char **_elist; int _pCount , _pIdx; void _free( void ); public: PLYOrientedPointStream( const char* fileName ); ~PLYOrientedPointStream( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p ); }; template< class Real , class Data > class PLYOrientedPointStreamWithData : public OrientedPointStreamWithData< Real , Data > { struct _PlyOrientedVertexWithData : public PlyOrientedVertex< Real > { Data data; }; char* _fileName; PlyFile* _ply; int _nr_elems; char **_elist; PlyProperty* _dataProperties; int _dataPropertiesCount; bool (*_validationFunction)( const bool* ); int _pCount , _pIdx; void _free( void ); public: PLYOrientedPointStreamWithData( const char* fileName , const PlyProperty* dataProperties , int dataPropertiesCount , bool (*validationFunction)( const bool* )=NULL ); ~PLYOrientedPointStreamWithData( void ); void reset( void ); bool nextPoint( OrientedPoint3D< Real >& p , Data& d ); }; #include "PointStream.inl" #endif // POINT_STREAM_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/PointStream.inl000077500000000000000000000330311454702036400233620ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ /////////////////////////////// // MemoryOrientedPointStream // /////////////////////////////// template< class Real > MemoryOrientedPointStream< Real >::MemoryOrientedPointStream( size_t pointCount , const OrientedPoint3D< Real >* points ){ _points = points , _pointCount = pointCount , _current = 0; } template< class Real > MemoryOrientedPointStream< Real >::~MemoryOrientedPointStream( void ){ ; } template< class Real > void MemoryOrientedPointStream< Real >::reset( void ) { _current=0; } template< class Real > bool MemoryOrientedPointStream< Real >::nextPoint( OrientedPoint3D< Real >& p ) { if( _current>=_pointCount ) return false; p = _points[_current]; _current++; return true; } ////////////////////////////// // ASCIIOrientedPointStream // ////////////////////////////// template< class Real > ASCIIOrientedPointStream< Real >::ASCIIOrientedPointStream( const char* fileName ) { _fp = fopen( fileName , "r" ); if( !_fp ) fprintf( stderr , "Failed to open file for reading: %s\n" , fileName ) , exit( 0 ); } template< class Real > ASCIIOrientedPointStream< Real >::~ASCIIOrientedPointStream( void ) { fclose( _fp ); _fp = NULL; } template< class Real > void ASCIIOrientedPointStream< Real >::reset( void ) { fseek( _fp , SEEK_SET , 0 ); } template< class Real > bool ASCIIOrientedPointStream< Real >::nextPoint( OrientedPoint3D< Real >& p ) { float c[2*3]; if( fscanf( _fp , " %f %f %f %f %f %f " , &c[0] , &c[1] , &c[2] , &c[3] , &c[4] , &c[5] )!=2*3 ) return false; p.p[0] = c[0] , p.p[1] = c[1] , p.p[2] = c[2]; p.n[0] = c[3] , p.n[1] = c[4] , p.n[2] = c[5]; return true; } /////////////////////////////// // BinaryOrientedPointStream // /////////////////////////////// template< class Real > BinaryOrientedPointStream< Real >::BinaryOrientedPointStream( const char* fileName ) { _pointsInBuffer = _currentPointIndex = 0; _fp = fopen( fileName , "rb" ); if( !_fp ) fprintf( stderr , "Failed to open file for reading: %s\n" , fileName ) , exit( 0 ); } template< class Real > BinaryOrientedPointStream< Real >::~BinaryOrientedPointStream( void ) { fclose( _fp ); _fp = NULL; } template< class Real > void BinaryOrientedPointStream< Real >::reset( void ) { fseek( _fp , SEEK_SET , 0 ); _pointsInBuffer = _currentPointIndex = 0; } template< class Real > bool BinaryOrientedPointStream< Real >::nextPoint( OrientedPoint3D< Real >& p ) { if( _currentPointIndex<_pointsInBuffer ) { p = _pointBuffer[ _currentPointIndex ]; _currentPointIndex++; return true; } else { _currentPointIndex = 0; _pointsInBuffer = int( fread( _pointBuffer , sizeof( OrientedPoint3D< Real > ) , POINT_BUFFER_SIZE , _fp ) ); if( !_pointsInBuffer ) return false; else return nextPoint( p ); } } //////////////////////////// // PLYOrientedPointStream // //////////////////////////// template< class Real > PLYOrientedPointStream< Real >::PLYOrientedPointStream( const char* fileName ) { _fileName = new char[ strlen( fileName )+1 ]; strcpy( _fileName , fileName ); _ply = NULL; reset(); } template< class Real > void PLYOrientedPointStream< Real >::reset( void ) { int fileType; float version; PlyProperty** plist; if( _ply ) _free(); _ply = ply_open_for_reading( _fileName, &_nr_elems, &_elist, &fileType, &version ); if( !_ply ) { fprintf( stderr, "[ERROR] Failed to open ply file for reading: %s\n" , _fileName ); exit( 0 ); } bool foundVertices = false; for( int i=0 ; i<_nr_elems ; i++ ) { int num_elems; int nr_props; char* elem_name = _elist[i]; plist = ply_get_element_description( _ply , elem_name , &num_elems , &nr_props ); if( !plist ) { fprintf( stderr , "[ERROR] Failed to get element description: %s\n" , elem_name ); exit( 0 ); } if( equal_strings( "vertex" , elem_name ) ) { foundVertices = true; _pCount = num_elems , _pIdx = 0; for( int i=0 ; i::ReadComponents ; i++ ) if( !ply_get_property( _ply , elem_name , &(PlyOrientedVertex< Real >::ReadProperties[i]) ) ) { fprintf( stderr , "[ERROR] Failed to find property in ply file: %s\n" , PlyOrientedVertex< Real >::ReadProperties[i].name ); exit( 0 ); } } for( int j=0 ; jname ); free( plist[j] ); } free( plist ); if( foundVertices ) break; } if( !foundVertices ) { fprintf( stderr , "[ERROR] Could not find vertices in ply file\n" ); exit( 0 ); } } template< class Real > void PLYOrientedPointStream< Real >::_free( void ) { if( _ply ) ply_close( _ply ) , _ply = NULL; if( _elist ) { for( int i=0 ; i<_nr_elems ; i++ ) free( _elist[i] ); free( _elist ); } } template< class Real > PLYOrientedPointStream< Real >::~PLYOrientedPointStream( void ) { _free(); if( _fileName ) delete[] _fileName , _fileName = NULL; } template< class Real > bool PLYOrientedPointStream< Real >::nextPoint( OrientedPoint3D< Real >& p ) { if( _pIdx<_pCount ) { PlyOrientedVertex< Real > op; ply_get_element( _ply, (void *)&op ); p.p = op.point; p.n = op.normal; _pIdx++; return true; } else return false; } /////////////////////////////////////// // MemoryOrientedPointStreamWithData // /////////////////////////////////////// template< class Real , class Data > MemoryOrientedPointStreamWithData< Real , Data >::MemoryOrientedPointStreamWithData( size_t pointCount , const std::pair< OrientedPoint3D< Real > , Data >* points ){ _points = points , _pointCount = pointCount , _current = 0; } template< class Real , class Data > MemoryOrientedPointStreamWithData< Real , Data >::~MemoryOrientedPointStreamWithData( void ){ ; } template< class Real , class Data > void MemoryOrientedPointStreamWithData< Real , Data >::reset( void ) { _current=0; } template< class Real , class Data > bool MemoryOrientedPointStreamWithData< Real , Data >::nextPoint( OrientedPoint3D< Real >& p , Data& d ) { if( _current>=_pointCount ) return false; p = _points[_current].first; d = _points[_current].second; _current++; return true; } ////////////////////////////////////// // ASCIIOrientedPointStreamWithData // ////////////////////////////////////// template< class Real , class Data > ASCIIOrientedPointStreamWithData< Real , Data >::ASCIIOrientedPointStreamWithData( const char* fileName , Data (*readData)( FILE* ) ) : _readData( readData ) { _fp = fopen( fileName , "r" ); if( !_fp ) fprintf( stderr , "Failed to open file for reading: %s\n" , fileName ) , exit( 0 ); } template< class Real , class Data > ASCIIOrientedPointStreamWithData< Real , Data >::~ASCIIOrientedPointStreamWithData( void ) { fclose( _fp ); _fp = NULL; } template< class Real , class Data > void ASCIIOrientedPointStreamWithData< Real , Data >::reset( void ) { fseek( _fp , SEEK_SET , 0 ); } template< class Real , class Data > bool ASCIIOrientedPointStreamWithData< Real , Data >::nextPoint( OrientedPoint3D< Real >& p , Data& d ) { float c[2*3]; if( fscanf( _fp , " %f %f %f %f %f %f " , &c[0] , &c[1] , &c[2] , &c[3] , &c[4] , &c[5] )!=2*3 ) return false; p.p[0] = c[0] , p.p[1] = c[1] , p.p[2] = c[2]; p.n[0] = c[3] , p.n[1] = c[4] , p.n[2] = c[5]; d = _readData( _fp ); return true; } /////////////////////////////////////// // BinaryOrientedPointStreamWithData // /////////////////////////////////////// template< class Real , class Data > BinaryOrientedPointStreamWithData< Real , Data >::BinaryOrientedPointStreamWithData( const char* fileName ) { _pointsInBuffer = _currentPointIndex = 0; _fp = fopen( fileName , "rb" ); if( !_fp ) fprintf( stderr , "Failed to open file for reading: %s\n" , fileName ) , exit( 0 ); } template< class Real , class Data > BinaryOrientedPointStreamWithData< Real , Data >::~BinaryOrientedPointStreamWithData( void ) { fclose( _fp ); _fp = NULL; } template< class Real , class Data > void BinaryOrientedPointStreamWithData< Real , Data >::reset( void ) { fseek( _fp , SEEK_SET , 0 ); _pointsInBuffer = _currentPointIndex = 0; } template< class Real , class Data > bool BinaryOrientedPointStreamWithData< Real , Data >::nextPoint( OrientedPoint3D< Real >& p , Data& d ) { if( _currentPointIndex<_pointsInBuffer ) { p = _pointBuffer[ _currentPointIndex ].first; d = _pointBuffer[ _currentPointIndex ].second; _currentPointIndex++; return true; } else { _currentPointIndex = 0; _pointsInBuffer = int( fread( _pointBuffer , sizeof( std::pair< OrientedPoint3D< Real > , Data > ) , POINT_BUFFER_SIZE , _fp ) ); if( !_pointsInBuffer ) return false; else return nextPoint( p , d ); } } //////////////////////////////////// // PLYOrientedPointStreamWithData // //////////////////////////////////// template< class Real , class Data > PLYOrientedPointStreamWithData< Real , Data >::PLYOrientedPointStreamWithData( const char* fileName , const PlyProperty* dataProperties , int dataPropertiesCount , bool (*validationFunction)( const bool* ) ) : _dataPropertiesCount( dataPropertiesCount ) , _validationFunction( validationFunction ) { _dataProperties = new PlyProperty[ _dataPropertiesCount ]; memcpy( _dataProperties , dataProperties , sizeof(PlyProperty) * _dataPropertiesCount ); for( int i=0 ; i<_dataPropertiesCount ; i++ ) _dataProperties[i].offset += sizeof( PlyOrientedVertex< Real > ); _fileName = new char[ strlen( fileName )+1 ]; strcpy( _fileName , fileName ); _ply = NULL; reset(); } template< class Real , class Data > void PLYOrientedPointStreamWithData< Real , Data >::reset( void ) { int fileType; float version; PlyProperty** plist; if( _ply ) _free(); _ply = ply_open_for_reading( _fileName, &_nr_elems, &_elist, &fileType, &version ); if( !_ply ) { fprintf( stderr, "[ERROR] Failed to open ply file for reading: %s\n" , _fileName ); exit( 0 ); } bool foundVertices = false; for( int i=0 ; i<_nr_elems ; i++ ) { int num_elems; int nr_props; char* elem_name = _elist[i]; plist = ply_get_element_description( _ply , elem_name , &num_elems , &nr_props ); if( !plist ) { fprintf( stderr , "[ERROR] Failed to get element description: %s\n" , elem_name ); exit( 0 ); } if( equal_strings( "vertex" , elem_name ) ) { foundVertices = true; _pCount = num_elems , _pIdx = 0; for( int i=0 ; i::ReadComponents ; i++ ) if( !ply_get_property( _ply , elem_name , &(PlyOrientedVertex< Real >::ReadProperties[i]) ) ) { fprintf( stderr , "[ERROR] Failed to find property in ply file: %s\n" , PlyOrientedVertex< Real >::ReadProperties[i].name ); exit( 0 ); } if( _validationFunction ) { bool* properties = new bool[_dataPropertiesCount]; for( int i=0 ; i<_dataPropertiesCount ; i++ ) if( !ply_get_property( _ply , elem_name , &(_dataProperties[i]) ) ) properties[i] = false; else properties[i] = true; bool valid = _validationFunction( properties ); delete[] properties; if( !valid ) fprintf( stderr , "[ERROR] Failed to validate properties in file\n" ) , exit( 0 ); } else { for( int i=0 ; i<_dataPropertiesCount ; i++ ) if( !ply_get_property( _ply , elem_name , &(_dataProperties[i]) ) ) fprintf( stderr , "[WARNING] Failed to find property in ply file: %s\n" , _dataProperties[i].name ); } } for( int j=0 ; jname ); free( plist[j] ); } free( plist ); if( foundVertices ) break; } if( !foundVertices ) { fprintf( stderr , "[ERROR] Could not find vertices in ply file\n" ); exit( 0 ); } } template< class Real , class Data > void PLYOrientedPointStreamWithData< Real , Data >::_free( void ) { if( _ply ) ply_close( _ply ) , _ply = NULL; if( _elist ) { for( int i=0 ; i<_nr_elems ; i++ ) free( _elist[i] ); free( _elist ); } } template< class Real , class Data > PLYOrientedPointStreamWithData< Real , Data >::~PLYOrientedPointStreamWithData( void ) { _free(); if( _fileName ) delete[] _fileName , _fileName = NULL; if( _dataProperties ) delete[] _dataProperties , _dataProperties = NULL; } template< class Real , class Data > bool PLYOrientedPointStreamWithData< Real , Data >::nextPoint( OrientedPoint3D< Real >& p , Data& d ) { if( _pIdx<_pCount ) { _PlyOrientedVertexWithData op; ply_get_element( _ply, (void *)&op ); p.p = op.point; p.n = op.normal; d = op.data; _pIdx++; return true; } else return false; } colmap-3.9.1/src/thirdparty/PoissonRecon/PoissonRecon.cpp000066400000000000000000000550311454702036400235370ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include #include #include #include #include #ifdef _WIN32 #include #include #endif // _WIN32 #include "MyTime.h" #include "MarchingCubes.h" #include "Octree.h" #include "SparseMatrix.h" #include "CmdLineParser.h" #include "PPolynomial.h" #include "Ply.h" #include "MemoryUsage.h" #ifdef _OPENMP #include "omp.h" #endif // _OPENMP namespace { void DumpOutput( const char* format , ... ); void DumpOutput2( std::vector< char* >& comments , const char* format , ... ); #include "MultiGridOctreeData.h" #define DEFAULT_FULL_DEPTH 5 #define XSTR(x) STR(x) #define STR(x) #x #if DEFAULT_FULL_DEPTH // #pragma message ( "[WARNING] Setting default full depth to " XSTR(DEFAULT_FULL_DEPTH) ) #endif // DEFAULT_FULL_DEPTH #include char* outputFile=NULL; int echoStdout=0; void DumpOutput( const char* format , ... ) { if( outputFile ) { FILE* fp = fopen( outputFile , "a" ); va_list args; va_start( args , format ); vfprintf( fp , format , args ); fclose( fp ); va_end( args ); } if( echoStdout ) { va_list args; va_start( args , format ); vprintf( format , args ); va_end( args ); } } void DumpOutput2( std::vector< char* >& comments , const char* format , ... ) { if( outputFile ) { FILE* fp = fopen( outputFile , "a" ); va_list args; va_start( args , format ); vfprintf( fp , format , args ); fclose( fp ); va_end( args ); } if( echoStdout ) { va_list args; va_start( args , format ); vprintf( format , args ); va_end( args ); } comments.push_back( new char[1024] ); char* str = comments.back(); va_list args; va_start( args , format ); vsprintf( str , format , args ); va_end( args ); if( str[strlen(str)-1]=='\n' ) str[strlen(str)-1] = 0; } cmdLineString In( "in" ) , Out( "out" ) , VoxelGrid( "voxel" ) , XForm( "xForm" ); cmdLineReadable #ifdef _WIN32 Performance( "performance" ) , #endif // _WIN32 Complete( "complete" ) , ShowResidual( "showResidual" ) , NoComments( "noComments" ) , PolygonMesh( "polygonMesh" ) , Confidence( "confidence" ) , NormalWeights( "nWeights" ) , NonManifold( "nonManifold" ) , Dirichlet( "dirichlet" ) , ASCII( "ascii" ) , Density( "density" ) , LinearFit( "linearFit" ) , PrimalVoxel( "primalVoxel" ) , Verbose( "verbose" ) , Double( "double" ); cmdLineInt Degree( "degree" , 2 ) , Depth( "depth" , 8 ) , CGDepth( "cgDepth" , 0 ) , KernelDepth( "kernelDepth" ) , AdaptiveExponent( "adaptiveExp" , 1 ) , Iters( "iters" , 8 ) , VoxelDepth( "voxelDepth" , -1 ) , FullDepth( "fullDepth" , DEFAULT_FULL_DEPTH ) , MinDepth( "minDepth" , 0 ) , MaxSolveDepth( "maxSolveDepth" ) , Threads( "threads" , omp_get_num_procs() ); cmdLineFloat Color( "color" , 16.f ) , SamplesPerNode( "samplesPerNode" , 1.5f ) , Scale( "scale" , 1.1f ) , CSSolverAccuracy( "cgAccuracy" , float(1e-3) ) , PointWeight( "pointWeight" , 4.f ); cmdLineReadable* params[] = { &In , &Degree , &Depth , &Out , &XForm , &Scale , &Verbose , &CSSolverAccuracy , &NoComments , &Double , &KernelDepth , &SamplesPerNode , &Confidence , &NormalWeights , &NonManifold , &PolygonMesh , &ASCII , &ShowResidual , &VoxelDepth , &PointWeight , &VoxelGrid , &Threads , &MaxSolveDepth , &AdaptiveExponent , &Dirichlet , &Density , &FullDepth , &MinDepth , &CGDepth , &Iters , &Complete , &Color , &LinearFit , &PrimalVoxel , #ifdef _WIN32 &Performance , #endif // _WIN32 }; void ShowUsage(char* ex) { printf( "Usage: %s\n" , ex ); printf( "\t --%s \n" , In.name ); printf( "\t[--%s ]\n" , Out.name ); printf( "\t[--%s ]\n" , VoxelGrid.name ); printf( "\t[--%s =%d]\n" , Degree.name , Degree.value ); printf( "\t[--%s =%d]\n" , Depth.name , Depth.value ); printf( "\t\t Running at depth d corresponds to solving on a 2^d x 2^d x 2^d\n" ); printf( "\t\t voxel grid.\n" ); printf( "\t[--%s =%d]\n" , FullDepth.name , FullDepth.value ); printf( "\t\t This flag specifies the depth up to which the octree should be complete.\n" ); printf( "\t[--%s =<%s>]\n" , VoxelDepth.name , Depth.name ); printf( "\t[--%s =%d]\n" , CGDepth.name , CGDepth.value ); printf( "\t\t The depth up to which a conjugate-gradients solver should be used.\n"); printf( "\t[--%s =%f]\n" , Scale.name , Scale.value ); printf( "\t\t Specifies the factor of the bounding cube that the input\n" ); printf( "\t\t samples should fit into.\n" ); printf( "\t[--%s =%f]\n" , SamplesPerNode.name, SamplesPerNode.value ); printf( "\t\t This parameter specifies the minimum number of points that\n" ); printf( "\t\t should fall within an octree node.\n" ); printf( "\t[--%s =%f]\n" , PointWeight.name , PointWeight.value ); printf( "\t\t This value specifies the weight that point interpolation constraints are\n" ); printf( "\t\t given when defining the (screened) Poisson system.\n" ); printf( "\t[--%s =%d]\n" , Iters.name , Iters.value ); printf( "\t\t This flag specifies the (maximum if CG) number of solver iterations.\n" ); printf( "\t[--%s ]\n" , Color.name ); printf( "\t\t This flag specifies the pull factor for color interpolation\n" ); #ifdef _OPENMP printf( "\t[--%s =%d]\n" , Threads.name , Threads.value ); printf( "\t\t This parameter specifies the number of threads across which\n" ); printf( "\t\t the solver should be parallelized.\n" ); #endif // _OPENMP printf( "\t[--%s]\n" , Confidence.name ); printf( "\t\t If this flag is enabled, the size of a sample's normals is\n" ); printf( "\t\t used as a confidence value, affecting the sample's\n" ); printf( "\t\t constribution to the reconstruction process.\n" ); printf( "\t[--%s]\n" , NormalWeights.name ); printf( "\t\t If this flag is enabled, the size of a sample's normals is\n" ); printf( "\t\t used as to modulate the interpolation weight.\n" ); #if 0 printf( "\t[--%s]\n" , NonManifold.name ); printf( "\t\t If this flag is enabled, the isosurface extraction does not add\n" ); printf( "\t\t a planar polygon's barycenter in order to ensure that the output\n" ); printf( "\t\t mesh is manifold.\n" ); #endif printf( "\t[--%s]\n" , PolygonMesh.name); printf( "\t\t If this flag is enabled, the isosurface extraction returns polygons\n" ); printf( "\t\t rather than triangles.\n" ); #if 0 printf( "\t[--%s =%d]\n" , MinDepth.name , MinDepth.value ); printf( "\t\t This flag specifies the coarsest depth at which the system is to be solved.\n" ); printf( "\t[--%s =%g]\n" , CSSolverAccuracy.name , CSSolverAccuracy.value ); printf( "\t\t This flag specifies the accuracy cut-off to be used for CG.\n" ); printf( "\t[--%s =%d]\n", AdaptiveExponent.name , AdaptiveExponent.value ); printf( "\t\t This flag specifies the exponent scale for the adaptive weighting.\n" ); #ifdef _WIN32 printf( "\t[--%s]\n" , Performance.name ); printf( "\t\t If this flag is enabled, the running time and peak memory usage\n" ); printf( "\t\t is output after the reconstruction.\n" ); #endif // _WIN32 #endif printf( "\t[--%s]\n" , Dirichlet.name); printf( "\t\t If this flag is enabled, Dirichlet boundary constraints are used for reconstruction.\n" ); printf( "\t[--%s]\n" , Density.name ); printf( "\t\t If this flag is enabled, the sampling density is written out with the vertices.\n" ); printf( "\t[--%s]\n" , LinearFit.name ); printf( "\t\t If this flag is enabled, the iso-surfacing will be performed using linear fitting.\n" ); printf( "\t[--%s]\n" , PrimalVoxel.name ); printf( "\t\t If this flag is enabled, voxel sampling is performed at corners rather than centers.\n" ); #if 0 printf( "\t[--%s]\n" , ASCII.name ); printf( "\t\t If this flag is enabled, the output file is written out in ASCII format.\n" ); printf( "\t[--%s]\n" , NoComments.name ); printf( "\t\t If this flag is enabled, the output file will not include comments.\n" ); #endif printf( "\t[--%s]\n" , Double.name ); printf( "\t\t If this flag is enabled, the reconstruction will be performed with double-precision floats.\n" ); printf( "\t[--%s]\n" , Verbose.name ); printf( "\t\t If this flag is enabled, the progress of the reconstructor will be output to STDOUT.\n" ); } Point3D< unsigned char > ReadASCIIColor( FILE* fp ) { Point3D< unsigned char > c; if( fscanf( fp , " %c %c %c " , &c[0] , &c[1] , &c[2] )!=3 ) fprintf( stderr , "[ERROR] Failed to read color\n" ) , exit( 0 ); return c; } PlyProperty PlyColorProperties[]= { { "r" , PLY_UCHAR , PLY_UCHAR , int( offsetof( Point3D< unsigned char > , coords[0] ) ) , 0 , 0 , 0 , 0 } , { "g" , PLY_UCHAR , PLY_UCHAR , int( offsetof( Point3D< unsigned char > , coords[1] ) ) , 0 , 0 , 0 , 0 } , { "b" , PLY_UCHAR , PLY_UCHAR , int( offsetof( Point3D< unsigned char > , coords[2] ) ) , 0 , 0 , 0 , 0 } , { "red" , PLY_UCHAR , PLY_UCHAR , int( offsetof( Point3D< unsigned char > , coords[0] ) ) , 0 , 0 , 0 , 0 } , { "green" , PLY_UCHAR , PLY_UCHAR , int( offsetof( Point3D< unsigned char > , coords[1] ) ) , 0 , 0 , 0 , 0 } , { "blue" , PLY_UCHAR , PLY_UCHAR , int( offsetof( Point3D< unsigned char > , coords[2] ) ) , 0 , 0 , 0 , 0 } }; bool ValidPlyColorProperties( const bool* props ){ return ( props[0] || props[3] ) && ( props[1] || props[4] ) && ( props[2] || props[5] ); } template< class Real , int Degree , class Vertex > int _Execute( int argc , char* argv[] ) { Reset< Real >(); int paramNum = sizeof(params)/sizeof(cmdLineReadable*); std::vector< char* > comments; if( Verbose.set ) echoStdout=1; XForm4x4< Real > xForm , iXForm; if( XForm.set ) { FILE* fp = fopen( XForm.value , "r" ); if( !fp ) { fprintf( stderr , "[WARNING] Could not read x-form from: %s\n" , XForm.value ); xForm = XForm4x4< Real >::Identity(); } else { for( int i=0 ; i<4 ; i++ ) for( int j=0 ; j<4 ; j++ ) { float f; if( fscanf( fp , " %f " , &f )!=1 ) fprintf( stderr , "[ERROR] Execute: Failed to read xform\n" ) , exit( 0 ); xForm(i,j) = (Real)f; } fclose( fp ); } } else xForm = XForm4x4< Real >::Identity(); iXForm = xForm.inverse(); DumpOutput2( comments , "Running Screened Poisson Reconstruction (Version 8.0)\n" ); char str[1024]; for( int i=0 ; iset ) { params[i]->writeValue( str ); if( strlen( str ) ) DumpOutput2( comments , "\t--%s %s\n" , params[i]->name , str ); else DumpOutput2( comments , "\t--%s\n" , params[i]->name ); } double t; double tt=Time(); Real isoValue = 0; Octree< Real > tree; tree.threads = Threads.value; if( !In.set ) { ShowUsage( argv[0] ); return 0; } if( !MaxSolveDepth.set ) MaxSolveDepth.value = Depth.value; OctNode< TreeNodeData >::SetAllocator( MEMORY_ALLOCATOR_BLOCK_SIZE ); t=Time(); int kernelDepth = KernelDepth.set ? KernelDepth.value : Depth.value-2; if( kernelDepth>Depth.value ) { fprintf( stderr,"[WARNING] %s can't be greater than %s: %d <= %d\n" , KernelDepth.name , Depth.name , KernelDepth.value , Depth.value ); kernelDepth = Depth.value; } double maxMemoryUsage; t=Time() , tree.maxMemoryUsage=0; SparseNodeData< PointData< Real > , 0 >* pointInfo = new SparseNodeData< PointData< Real > , 0 >(); SparseNodeData< Point3D< Real > , NORMAL_DEGREE >* normalInfo = new SparseNodeData< Point3D< Real > , NORMAL_DEGREE >(); SparseNodeData< Real , WEIGHT_DEGREE >* densityWeights = new SparseNodeData< Real , WEIGHT_DEGREE >(); SparseNodeData< Real , NORMAL_DEGREE >* nodeWeights = new SparseNodeData< Real , NORMAL_DEGREE >(); int pointCount; typedef typename Octree< Real >::template ProjectiveData< Point3D< Real > > ProjectiveColor; SparseNodeData< ProjectiveColor , DATA_DEGREE >* colorData = NULL; char* ext = GetFileExtension( In.value ); if( Color.set && Color.value>0 ) { colorData = new SparseNodeData< ProjectiveColor , DATA_DEGREE >(); OrientedPointStreamWithData< float , Point3D< unsigned char > >* pointStream; if ( !strcasecmp( ext , "bnpts" ) ) pointStream = new BinaryOrientedPointStreamWithData< float , Point3D< unsigned char > >( In.value ); else if( !strcasecmp( ext , "ply" ) ) pointStream = new PLYOrientedPointStreamWithData< float , Point3D< unsigned char > >( In.value , PlyColorProperties , 6 , ValidPlyColorProperties ); else pointStream = new ASCIIOrientedPointStreamWithData< float , Point3D< unsigned char > >( In.value , ReadASCIIColor ); pointCount = tree.template SetTree< float , NORMAL_DEGREE , WEIGHT_DEGREE , DATA_DEGREE , Point3D< unsigned char > >( pointStream , MinDepth.value , Depth.value , FullDepth.value , kernelDepth , Real(SamplesPerNode.value) , Scale.value , Confidence.set , NormalWeights.set , PointWeight.value , AdaptiveExponent.value , *densityWeights , *pointInfo , *normalInfo , *nodeWeights , colorData , xForm , Dirichlet.set , Complete.set ); delete pointStream; for( const OctNode< TreeNodeData >* n = tree.tree().nextNode() ; n!=NULL ; n=tree.tree().nextNode( n ) ) { int idx = colorData->index( n ); if( idx>=0 ) colorData->data[idx] *= (Real)pow( Color.value , n->depth() ); } } else { OrientedPointStream< float >* pointStream; if ( !strcasecmp( ext , "bnpts" ) ) pointStream = new BinaryOrientedPointStream< float >( In.value ); else if( !strcasecmp( ext , "ply" ) ) pointStream = new PLYOrientedPointStream< float >( In.value ); else pointStream = new ASCIIOrientedPointStream< float >( In.value ); pointCount = tree.template SetTree< float , NORMAL_DEGREE , WEIGHT_DEGREE , DATA_DEGREE , Point3D< unsigned char > >( pointStream , MinDepth.value , Depth.value , FullDepth.value , kernelDepth , Real(SamplesPerNode.value) , Scale.value , Confidence.set , NormalWeights.set , PointWeight.value , AdaptiveExponent.value , *densityWeights , *pointInfo , *normalInfo , *nodeWeights , colorData , xForm , Dirichlet.set , Complete.set ); delete pointStream; } delete[] ext; if( !Density.set ) delete densityWeights , densityWeights = NULL; { std::vector< int > indexMap; if( NORMAL_DEGREE>Degree ) tree.template EnableMultigrid< NORMAL_DEGREE >( &indexMap ); else tree.template EnableMultigrid< Degree >( &indexMap ); if( pointInfo ) pointInfo->remapIndices( indexMap ); if( normalInfo ) normalInfo->remapIndices( indexMap ); if( densityWeights ) densityWeights->remapIndices( indexMap ); if( nodeWeights ) nodeWeights->remapIndices( indexMap ); if( colorData ) colorData->remapIndices( indexMap ); } DumpOutput2( comments , "# Tree set in: %9.1f (s), %9.1f (MB)\n" , Time()-t , tree.maxMemoryUsage ); DumpOutput( "Input Points: %d\n" , pointCount ); DumpOutput( "Leaves/Nodes: %d/%d\n" , tree.leaves() , tree.nodes() ); DumpOutput( "Memory Usage: %.3f MB\n" , float( MemoryInfo::Usage() )/(1<<20) ); maxMemoryUsage = tree.maxMemoryUsage; t=Time() , tree.maxMemoryUsage=0; DenseNodeData< Real , Degree > constraints = tree.template SetLaplacianConstraints< Degree >( *normalInfo ); delete normalInfo; DumpOutput2( comments , "# Constraints set in: %9.1f (s), %9.1f (MB)\n" , Time()-t , tree.maxMemoryUsage ); DumpOutput( "Memory Usage: %.3f MB\n" , float( MemoryInfo::Usage())/(1<<20) ); maxMemoryUsage = std::max< double >( maxMemoryUsage , tree.maxMemoryUsage ); t=Time() , tree.maxMemoryUsage=0; DenseNodeData< Real , Degree > solution = tree.SolveSystem( *pointInfo , constraints , ShowResidual.set , Iters.value , MaxSolveDepth.value , CGDepth.value , CSSolverAccuracy.value ); delete pointInfo; constraints.resize( 0 ); DumpOutput2( comments , "# Linear system solved in: %9.1f (s), %9.1f (MB)\n" , Time()-t , tree.maxMemoryUsage ); DumpOutput( "Memory Usage: %.3f MB\n" , float( MemoryInfo::Usage() )/(1<<20) ); maxMemoryUsage = std::max< double >( maxMemoryUsage , tree.maxMemoryUsage ); CoredFileMeshData< Vertex > mesh; if( Verbose.set ) tree.maxMemoryUsage=0; t=Time(); isoValue = tree.GetIsoValue( solution , *nodeWeights ); delete nodeWeights; DumpOutput( "Got average in: %f\n" , Time()-t ); DumpOutput( "Iso-Value: %e\n" , isoValue ); if( VoxelGrid.set ) { double t = Time(); FILE* fp = fopen( VoxelGrid.value , "wb" ); if( !fp ) fprintf( stderr , "Failed to open voxel file for writing: %s\n" , VoxelGrid.value ); else { int res = 0; Pointer( Real ) values = tree.Evaluate( solution , res , isoValue , VoxelDepth.value , PrimalVoxel.set ); fwrite( &res , sizeof(int) , 1 , fp ); if( sizeof(Real)==sizeof(float) ) fwrite( values , sizeof(float) , res*res*res , fp ); else { float *fValues = new float[res*res*res]; for( int i=0 ; i( densityWeights , colorData , solution , isoValue , mesh , !LinearFit.set , !NonManifold.set , PolygonMesh.set ); if( PolygonMesh.set ) DumpOutput2( comments , "# Got polygons in: %9.1f (s), %9.1f (MB)\n" , Time()-t , tree.maxMemoryUsage ); else DumpOutput2( comments , "# Got triangles in: %9.1f (s), %9.1f (MB)\n" , Time()-t , tree.maxMemoryUsage ); maxMemoryUsage = std::max< double >( maxMemoryUsage , tree.maxMemoryUsage ); DumpOutput2( comments , "# Total Solve: %9.1f (s), %9.1f (MB)\n" , Time()-tt , maxMemoryUsage ); if( NoComments.set ) { if( ASCII.set ) PlyWritePolygons( Out.value , &mesh , PLY_ASCII , NULL , 0 , iXForm ); else PlyWritePolygons( Out.value , &mesh , PLY_BINARY_NATIVE , NULL , 0 , iXForm ); } else { if( ASCII.set ) PlyWritePolygons( Out.value , &mesh , PLY_ASCII , &comments[0] , (int)comments.size() , iXForm ); else PlyWritePolygons( Out.value , &mesh , PLY_BINARY_NATIVE , &comments[0] , (int)comments.size() , iXForm ); } DumpOutput( "Vertices / Polygons: %d / %d\n" , mesh.outOfCorePointCount()+mesh.inCorePoints.size() , mesh.polygonCount() ); } solution.resize( 0 ); if( colorData ){ delete colorData ; colorData = NULL; } return 1; } #ifdef _WIN32 inline double to_seconds( const FILETIME& ft ) { const double low_to_sec=100e-9; // 100 nanoseconds const double high_to_sec=low_to_sec*4294967296.0; return ft.dwLowDateTime*low_to_sec+ft.dwHighDateTime*high_to_sec; } #endif // _WIN32 template< class Real , class Vertex > int Execute( int argc , char* argv[] ) { switch( Degree.value ) { case 1: return _Execute< Real , 1 , Vertex >( argc , argv ); case 2: return _Execute< Real , 2 , Vertex >( argc , argv ); case 3: return _Execute< Real , 3 , Vertex >( argc , argv ); case 4: return _Execute< Real , 4 , Vertex >( argc , argv ); default: fprintf( stderr , "[ERROR] Only B-Splines of degree 1 - 4 are supported" ); return EXIT_FAILURE; } } } // namespace int PoissonRecon( int argc , char* argv[] ) { #if defined(WIN32) && defined(MAX_MEMORY_GB) if( MAX_MEMORY_GB>0 ) { SIZE_T peakMemory = 1; peakMemory <<= 30; peakMemory *= MAX_MEMORY_GB; printf( "Limiting memory usage to %.2f GB\n" , float( peakMemory>>30 ) ); HANDLE h = CreateJobObject( NULL , NULL ); AssignProcessToJobObject( h , GetCurrentProcess() ); JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = { 0 }; jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY; jeli.JobMemoryLimit = peakMemory; if( !SetInformationJobObject( h , JobObjectExtendedLimitInformation , &jeli , sizeof( jeli ) ) ) fprintf( stderr , "Failed to set memory limit\n" ); } #endif // defined(WIN32) && defined(MAX_MEMORY_GB) double t = Time(); cmdLineParse( argc-1 , &argv[1] , sizeof(params)/sizeof(cmdLineReadable*) , params , 1 ); if( Density.set ) if( Color.set ) if( Double.set ) Execute< double , PlyColorAndValueVertex< float > >( argc , argv ); else Execute< float , PlyColorAndValueVertex< float > >( argc , argv ); else if( Double.set ) Execute< double , PlyValueVertex< float > >( argc , argv ); else Execute< float , PlyValueVertex< float > >( argc , argv ); else if( Color.set ) if( Double.set ) Execute< double , PlyColorVertex< float > >( argc , argv ); else Execute< float , PlyColorVertex< float > >( argc , argv ); else if( Double.set ) Execute< double , PlyVertex< float > >( argc , argv ); else Execute< float , PlyVertex< float > >( argc , argv ); #ifdef _WIN32 if( Performance.set ) { HANDLE cur_thread=GetCurrentThread(); FILETIME tcreat, texit, tkernel, tuser; if( GetThreadTimes( cur_thread , &tcreat , &texit , &tkernel , &tuser ) ) printf( "Time (Wall/User/Kernel): %.2f / %.2f / %.2f\n" , Time()-t , to_seconds( tuser ) , to_seconds( tkernel ) ); else printf( "Time: %.2f\n" , Time()-t ); HANDLE h = GetCurrentProcess(); PROCESS_MEMORY_COUNTERS pmc; if( GetProcessMemoryInfo( h , &pmc , sizeof(pmc) ) ) printf( "Peak Memory (MB): %d\n" , pmc.PeakWorkingSetSize>>20 ); } #endif // _WIN32 return EXIT_SUCCESS; } colmap-3.9.1/src/thirdparty/PoissonRecon/PoissonRecon.h000066400000000000000000000000521454702036400231750ustar00rootroot00000000000000int PoissonRecon(int argc, char* argv[]); colmap-3.9.1/src/thirdparty/PoissonRecon/Polynomial.h000066400000000000000000000077441454702036400227160ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #ifndef POLYNOMIAL_INCLUDED #define POLYNOMIAL_INCLUDED #define NEW_POLYNOMIAL_CODE 1 #include template< int Degree > class Polynomial { public: double coefficients[Degree+1]; Polynomial(void); template Polynomial(const Polynomial& P); double operator()( double t ) const; double integral( double tMin , double tMax ) const; int operator == (const Polynomial& p) const; int operator != (const Polynomial& p) const; int isZero(void) const; void setZero(void); template Polynomial& operator = (const Polynomial &p); Polynomial& operator += (const Polynomial& p); Polynomial& operator -= (const Polynomial& p); Polynomial operator - (void) const; Polynomial operator + (const Polynomial& p) const; Polynomial operator - (const Polynomial& p) const; template Polynomial operator * (const Polynomial& p) const; Polynomial& operator += ( double s ); Polynomial& operator -= ( double s ); Polynomial& operator *= ( double s ); Polynomial& operator /= ( double s ); Polynomial operator + ( double s ) const; Polynomial operator - ( double s ) const; Polynomial operator * ( double s ) const; Polynomial operator / ( double s ) const; Polynomial scale( double s ) const; Polynomial shift( double t ) const; Polynomial derivative(void) const; Polynomial integral(void) const; void printnl(void) const; Polynomial& addScaled(const Polynomial& p,double scale); static void Negate(const Polynomial& in,Polynomial& out); static void Subtract(const Polynomial& p1,const Polynomial& p2,Polynomial& q); static void Scale(const Polynomial& p,double w,Polynomial& q); static void AddScaled(const Polynomial& p1,double w1,const Polynomial& p2,double w2,Polynomial& q); static void AddScaled(const Polynomial& p1,const Polynomial& p2,double w2,Polynomial& q); static void AddScaled(const Polynomial& p1,double w1,const Polynomial& p2,Polynomial& q); void getSolutions(double c,std::vector& roots,double EPS) const; int getSolutions( double c , double* roots , double EPS ) const; // [NOTE] Both of these methods define the indexing according to DeBoor's algorithm, so that // Polynomial< Degree >BSplineComponent( 0 )( 1.0 )=0 for all Degree>0. static Polynomial BSplineComponent( int i ); static void BSplineComponentValues( double x , double* values ); static void BinomialCoefficients( int bCoefficients[Degree+1] ); }; #include "Polynomial.inl" #endif // POLYNOMIAL_INCLUDED colmap-3.9.1/src/thirdparty/PoissonRecon/Polynomial.inl000077500000000000000000000300121454702036400232340ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include #include #include #include "Factor.h" //////////////// // Polynomial // //////////////// template Polynomial::Polynomial(void){memset(coefficients,0,sizeof(double)*(Degree+1));} template template Polynomial::Polynomial(const Polynomial& P){ memset(coefficients,0,sizeof(double)*(Degree+1)); for(int i=0;i<=Degree && i<=Degree2;i++){coefficients[i]=P.coefficients[i];} } template template Polynomial& Polynomial::operator = (const Polynomial &p){ int d=Degree Polynomial Polynomial::derivative(void) const{ Polynomial p; for(int i=0;i Polynomial Polynomial::integral(void) const{ Polynomial p; p.coefficients[0]=0; for(int i=0;i<=Degree;i++){p.coefficients[i+1]=coefficients[i]/(i+1);} return p; } template<> double Polynomial< 0 >::operator() ( double t ) const { return coefficients[0]; } template<> double Polynomial< 1 >::operator() ( double t ) const { return coefficients[0]+coefficients[1]*t; } template<> double Polynomial< 2 >::operator() ( double t ) const { return coefficients[0]+(coefficients[1]+coefficients[2]*t)*t; } template double Polynomial::operator() ( double t ) const{ double v=coefficients[Degree]; for( int d=Degree-1 ; d>=0 ; d-- ) v = v*t + coefficients[d]; return v; } template double Polynomial::integral( double tMin , double tMax ) const { double v=0; double t1,t2; t1=tMin; t2=tMax; for(int i=0;i<=Degree;i++){ v+=coefficients[i]*(t2-t1)/(i+1); if(t1!=-DBL_MAX && t1!=DBL_MAX){t1*=tMin;} if(t2!=-DBL_MAX && t2!=DBL_MAX){t2*=tMax;} } return v; } template int Polynomial::operator == (const Polynomial& p) const{ for(int i=0;i<=Degree;i++){if(coefficients[i]!=p.coefficients[i]){return 0;}} return 1; } template int Polynomial::operator != (const Polynomial& p) const{ for(int i=0;i<=Degree;i++){if(coefficients[i]==p.coefficients[i]){return 0;}} return 1; } template int Polynomial::isZero(void) const{ for(int i=0;i<=Degree;i++){if(coefficients[i]!=0){return 0;}} return 1; } template void Polynomial::setZero(void){memset(coefficients,0,sizeof(double)*(Degree+1));} template Polynomial& Polynomial::addScaled(const Polynomial& p,double s){ for(int i=0;i<=Degree;i++){coefficients[i]+=p.coefficients[i]*s;} return *this; } template Polynomial& Polynomial::operator += (const Polynomial& p){ for(int i=0;i<=Degree;i++){coefficients[i]+=p.coefficients[i];} return *this; } template Polynomial& Polynomial::operator -= (const Polynomial& p){ for(int i=0;i<=Degree;i++){coefficients[i]-=p.coefficients[i];} return *this; } template Polynomial Polynomial::operator + (const Polynomial& p) const{ Polynomial q; for(int i=0;i<=Degree;i++){q.coefficients[i]=(coefficients[i]+p.coefficients[i]);} return q; } template Polynomial Polynomial::operator - (const Polynomial& p) const{ Polynomial q; for(int i=0;i<=Degree;i++) {q.coefficients[i]=coefficients[i]-p.coefficients[i];} return q; } template void Polynomial::Scale(const Polynomial& p,double w,Polynomial& q){ for(int i=0;i<=Degree;i++){q.coefficients[i]=p.coefficients[i]*w;} } template void Polynomial::AddScaled(const Polynomial& p1,double w1,const Polynomial& p2,double w2,Polynomial& q){ for(int i=0;i<=Degree;i++){q.coefficients[i]=p1.coefficients[i]*w1+p2.coefficients[i]*w2;} } template void Polynomial::AddScaled(const Polynomial& p1,double w1,const Polynomial& p2,Polynomial& q){ for(int i=0;i<=Degree;i++){q.coefficients[i]=p1.coefficients[i]*w1+p2.coefficients[i];} } template void Polynomial::AddScaled(const Polynomial& p1,const Polynomial& p2,double w2,Polynomial& q){ for(int i=0;i<=Degree;i++){q.coefficients[i]=p1.coefficients[i]+p2.coefficients[i]*w2;} } template void Polynomial::Subtract(const Polynomial &p1,const Polynomial& p2,Polynomial& q){ for(int i=0;i<=Degree;i++){q.coefficients[i]=p1.coefficients[i]-p2.coefficients[i];} } template void Polynomial::Negate(const Polynomial& in,Polynomial& out){ out=in; for(int i=0;i<=Degree;i++){out.coefficients[i]=-out.coefficients[i];} } template Polynomial Polynomial::operator - (void) const{ Polynomial q=*this; for(int i=0;i<=Degree;i++){q.coefficients[i]=-q.coefficients[i];} return q; } template template Polynomial Polynomial::operator * (const Polynomial& p) const{ Polynomial q; for(int i=0;i<=Degree;i++){for(int j=0;j<=Degree2;j++){q.coefficients[i+j]+=coefficients[i]*p.coefficients[j];}} return q; } template Polynomial& Polynomial::operator += ( double s ) { coefficients[0]+=s; return *this; } template Polynomial& Polynomial::operator -= ( double s ) { coefficients[0]-=s; return *this; } template Polynomial& Polynomial::operator *= ( double s ) { for(int i=0;i<=Degree;i++){coefficients[i]*=s;} return *this; } template Polynomial& Polynomial::operator /= ( double s ) { for(int i=0;i<=Degree;i++){coefficients[i]/=s;} return *this; } template Polynomial Polynomial::operator + ( double s ) const { Polynomial q=*this; q.coefficients[0]+=s; return q; } template Polynomial Polynomial::operator - ( double s ) const { Polynomial q=*this; q.coefficients[0]-=s; return q; } template Polynomial Polynomial::operator * ( double s ) const { Polynomial q; for(int i=0;i<=Degree;i++){q.coefficients[i]=coefficients[i]*s;} return q; } template Polynomial Polynomial::operator / ( double s ) const { Polynomial q; for( int i=0 ; i<=Degree ; i++ ) q.coefficients[i] = coefficients[i]/s; return q; } template Polynomial Polynomial::scale( double s ) const { Polynomial q=*this; double s2=1.0; for(int i=0;i<=Degree;i++){ q.coefficients[i]*=s2; s2/=s; } return q; } template Polynomial Polynomial::shift( double t ) const { Polynomial q; for(int i=0;i<=Degree;i++){ double temp=1; for(int j=i;j>=0;j--){ q.coefficients[j]+=coefficients[i]*temp; temp*=-t*j; temp/=(i-j+1); } } return q; } template void Polynomial::printnl(void) const{ for(int j=0;j<=Degree;j++){ printf("%6.4f x^%d ",coefficients[j],j); if(j=0){printf("+");} } printf("\n"); } template void Polynomial::getSolutions(double c,std::vector& roots,double EPS) const { double r[4][2]; int rCount=0; roots.clear(); switch(Degree){ case 1: rCount=Factor(coefficients[1],coefficients[0]-c,r,EPS); break; case 2: rCount=Factor(coefficients[2],coefficients[1],coefficients[0]-c,r,EPS); break; case 3: rCount=Factor(coefficients[3],coefficients[2],coefficients[1],coefficients[0]-c,r,EPS); break; // case 4: // rCount=Factor(coefficients[4],coefficients[3],coefficients[2],coefficients[1],coefficients[0]-c,r,EPS); // break; default: printf("Can't solve polynomial of degree: %d\n",Degree); } for(int i=0;i int Polynomial::getSolutions( double c , double* roots , double EPS ) const { double _roots[4][2]; int _rCount=0; switch( Degree ) { case 1: _rCount = Factor( coefficients[1] , coefficients[0]-c , _roots , EPS ) ; break; case 2: _rCount = Factor( coefficients[2] , coefficients[1] , coefficients[0]-c , _roots , EPS ) ; break; case 3: _rCount = Factor( coefficients[3] , coefficients[2] , coefficients[1] , coefficients[0]-c , _roots , EPS ) ; break; // case 4: _rCount = Factor( coefficients[4] , coefficients[3] , coefficients[2] , coefficients[1] , coefficients[0]-c , _roots , EPS ) ; break; default: printf( "Can't solve polynomial of degree: %d\n" , Degree ); } int rCount = 0; for( int i=0 ; i<_rCount ; i++ ) if( fabs(_roots[i][1])<=EPS ) roots[rCount++] = _roots[i][0]; return rCount; } // The 0-th order B-spline template< > Polynomial< 0 > Polynomial< 0 >::BSplineComponent( int i ) { Polynomial p; p.coefficients[0] = 1.; return p; } // The Degree-th order B-spline template< int Degree > Polynomial< Degree > Polynomial< Degree >::BSplineComponent( int i ) { // B_d^i(x) = \int_x^1 B_{d-1}^{i}(y) dy + \int_0^x B_{d-1}^{i-1} y dy // = \int_0^1 B_{d-1}^{i}(y) dy - \int_0^x B_{d-1}^{i}(y) dy + \int_0^x B_{d-1}^{i-1} y dy Polynomial p; if( i _p = Polynomial< Degree-1 >::BSplineComponent( i ).integral(); p -= _p; p.coefficients[0] += _p(1); } if( i>0 ) { Polynomial< Degree > _p = Polynomial< Degree-1 >::BSplineComponent( i-1 ).integral(); p += _p; } return p; } // The 0-th order B-spline values template< > void Polynomial< 0 >::BSplineComponentValues( double x , double* values ){ values[0] = 1.; } // The Degree-th order B-spline template< int Degree > void Polynomial< Degree >::BSplineComponentValues( double x , double* values ) { const double Scale = 1./Degree; Polynomial< Degree-1 >::BSplineComponentValues( x , values+1 ); values[0] = values[1] * (1.-x) * Scale; for( int i=1 ; i void Polynomial< 0 >::BinomialCoefficients( int bCoefficients[1] ){ bCoefficients[0] = 1; } template< int Degree > void Polynomial< Degree >::BinomialCoefficients( int bCoefficients[Degree+1] ) { Polynomial< Degree-1 >::BinomialCoefficients( bCoefficients ); int leftValue = 0; for( int i=0 ; i struct MatrixEntry { MatrixEntry( void ) { N =-1; Value = 0; } MatrixEntry( int i ) { N = i; Value = 0; } MatrixEntry( int i , T v ) { N = i; Value = v; } int N; T Value; }; template class SparseMatrix { private: bool _contiguous; int _maxEntriesPerRow; void _init( void ); public: int rows; Pointer( int ) rowSizes; Pointer( Pointer( MatrixEntry< T > ) ) m_ppElements; Pointer( MatrixEntry< T > ) operator[] ( int idx ) { return m_ppElements[idx]; } ConstPointer( MatrixEntry< T > ) operator[] ( int idx ) const { return m_ppElements[idx]; } SparseMatrix( void ); SparseMatrix( int rows ); SparseMatrix( int rows , int maxEntriesPerRow ); void Resize( int rows ); void Resize( int rows , int maxEntriesPerRow ); void SetRowSize( int row , int count ); int Entries( void ) const; SparseMatrix( const SparseMatrix& M ); ~SparseMatrix(); void SetZero(); SparseMatrix& operator = (const SparseMatrix& M); SparseMatrix operator * (const T& V) const; SparseMatrix& operator *= (const T& V); template< class T2 > void Multiply( ConstPointer( T2 ) in , Pointer( T2 ) out , int threads=1 ) const; template< class T2 > void MultiplyAndAddAverage( ConstPointer( T2 ) in , Pointer( T2 ) out , int threads=1 ) const; bool write( FILE* fp ) const; bool write( const char* fileName ) const; bool read( FILE* fp ); bool read( const char* fileName ); template< class T2 > void getDiagonal( Pointer( T2 ) diagonal , int threads=1 ) const; template< class T2 > static int SolveJacobi( const SparseMatrix& M , ConstPointer( T2 ) b , Pointer( T2 ) x , Pointer( T2 ) Mx , T2 sor , int threads=1 ); template< class T2 > static int SolveJacobi( const SparseMatrix& M , ConstPointer( T2 ) diagonal , ConstPointer( T2 ) b , Pointer( T2 ) x , Pointer( T2 ) Mx , T2 sor , int threads=1 ); template< class T2 > static int SolveGS( const SparseMatrix& M , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward ); template< class T2 > static int SolveGS( const SparseMatrix& M , ConstPointer( T2 ) diagonal , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward ); template< class T2 > static int SolveGS( const std::vector< std::vector< int > >& mcIndices , const SparseMatrix& M , ConstPointer( T2 ) diagonal , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward , int threads=1 ); template< class T2 > static int SolveGS( const std::vector< std::vector< int > >& mcIndices , const SparseMatrix& M , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward , int threads=1 ); template< class T2 > static int SolveCG( const SparseMatrix& M , ConstPointer( T2 ) b , int iters , Pointer( T2 ) x , T2 eps=1e-8 , int reset=1 , bool addDCTerm=false , bool solveNormal=false , int threads=1 ); }; #if !NEW_SPARSE_MATRIX template< class T2 > struct MapReduceVector { private: int _dim; public: std::vector< T2* > out; MapReduceVector( void ) { _dim = 0; } ~MapReduceVector( void ) { if( _dim ) for( int t=0 ; t class SparseSymmetricMatrix : public SparseMatrix< T > { public: template< class T2 > Vector< T2 > operator * ( const Vector& V ) const; template< class T2 > Vector< T2 > Multiply( const Vector& V ) const; template< class T2 > void Multiply( const Vector& In, Vector& Out , bool addDCTerm=false ) const; template< class T2 > void Multiply( const Vector& In, Vector& Out , MapReduceVector< T2 >& OutScratch , bool addDCTerm=false ) const; template< class T2 > void Multiply( const Vector& In, Vector& Out , std::vector< T2* >& OutScratch , const std::vector< int >& bounds ) const; template< class T2 > static int SolveCG( const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , T2 eps=1e-8 , int reset=1 , int threads=0 , bool addDCTerm=false , bool solveNormal=false ); template< class T2 > static int SolveCG( const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , MapReduceVector& scratch , T2 eps=1e-8 , int reset=1 , bool addDCTerm=false , bool solveNormal=false ); #ifdef WIN32 template< class T2 > static int SolveCGAtomic( const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , T2 eps=1e-8 , int reset=1 , int threads=0 , bool solveNormal=false ); #endif // WIN32 template< class T2 > static int SolveJacobi( const SparseSymmetricMatrix& M , const Vector& diagonal , const Vector& b , Vector& x , MapReduceVector& scratch , Vector& Mx , T2 sor , int reset ); template< class T2 > static int SolveJacobi( const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , MapReduceVector& scratch , T2 sor=T2(1.) , int reset=1 ); template< class T2 > static int SolveJacobi( const SparseSymmetricMatrix& M , const Vector& diagonal , const Vector& b , Vector& x , Vector& Mx , T2 sor , int reset ); template< class T2 > static int SolveJacobi( const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , T2 sor=T2(1.) , int reset=1 ); enum { ORDERING_UPPER_TRIANGULAR , ORDERING_LOWER_TRIANGULAR , ORDERING_NONE }; template< class T2 > static int SolveGS( const std::vector< std::vector< int > >& mcIndices , const SparseSymmetricMatrix& M , const Vector& diagonal , const Vector& b , Vector& x , MapReduceVector& scratch , Vector& Mx , Vector& dx , bool forward , int reset ); template< class T2 > static int SolveGS( const std::vector< std::vector< int > >& mcIndices , const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , MapReduceVector& scratch , bool forward , int reset=1 ); template< class T2 > static int SolveGS( const SparseSymmetricMatrix& M , const Vector& diagonal , const Vector& b , Vector& x , MapReduceVector& scratch , Vector& Mx , Vector& dx , bool forward , int reset , int ordering ); template< class T2 > static int SolveGS( const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , MapReduceVector& scratch , bool forward , int reset=1 , int ordering=ORDERING_NONE ); template< class T2 > static int SolveGS( const SparseSymmetricMatrix& M , const Vector& diagonal , const Vector& b , Vector& x , Vector& Mx , Vector& dx , bool forward , int reset , int ordering ); template< class T2 > static int SolveGS( const SparseSymmetricMatrix& M , const Vector& b , int iters , Vector& x , bool forward , int reset=1 , int ordering=ORDERING_NONE ); template< class T2 > void getDiagonal( Vector< T2 >& diagonal , int threads=1 ) const; }; #endif // !NEW_SPARSE_MATRIX #include "SparseMatrix.inl" #endif colmap-3.9.1/src/thirdparty/PoissonRecon/SparseMatrix.inl000077500000000000000000000437241454702036400235510ustar00rootroot00000000000000/* Copyright (c) 2006, Michael Kazhdan and Matthew Bolitho 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 the Johns Hopkins University 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 OWNER 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. */ #include #include /////////////////// // SparseMatrix // /////////////////// /////////////////////////////////////// // SparseMatrix Methods and Memebers // /////////////////////////////////////// template< class T > void SparseMatrix< T >::_init( void ) { _contiguous = false; _maxEntriesPerRow = 0; rows = 0; rowSizes = NullPointer( int ); m_ppElements = NullPointer( Pointer( MatrixEntry< T > ) ); } template< class T > SparseMatrix< T >::SparseMatrix( void ){ _init(); } template< class T > SparseMatrix< T >::SparseMatrix( int rows ){ _init() , Resize( rows ); } template< class T > SparseMatrix< T >::SparseMatrix( int rows , int maxEntriesPerRow ){ _init() , Resize( rows , maxEntriesPerRow ); } template< class T > SparseMatrix< T >::SparseMatrix( const SparseMatrix& M ) { _init(); if( M._contiguous ) Resize( M.rows , M._maxEntriesPerRow ); else Resize( M.rows ); for( int i=0 ; i ) * rowSizes[i] ); } } template int SparseMatrix::Entries( void ) const { int e = 0; for( int i=0 ; i SparseMatrix& SparseMatrix::operator = (const SparseMatrix& M) { if( M._contiguous ) Resize( M.rows , M._maxEntriesPerRow ); else Resize( M.rows ); for( int i=0 ; i ) * rowSizes[i] ); } return *this; } template SparseMatrix::~SparseMatrix( void ){ Resize( 0 ); } template< class T > bool SparseMatrix< T >::write( const char* fileName ) const { FILE* fp = fopen( fileName , "wb" ); if( !fp ) return false; bool ret = write( fp ); fclose( fp ); return ret; } template< class T > bool SparseMatrix< T >::read( const char* fileName ) { FILE* fp = fopen( fileName , "rb" ); if( !fp ) return false; bool ret = read( fp ); fclose( fp ); return ret; } template< class T > bool SparseMatrix< T >::write( FILE* fp ) const { if( fwrite( &rows , sizeof( int ) , 1 , fp )!=1 ) return false; if( fwrite( rowSizes , sizeof( int ) , rows , fp )!=rows ) return false; for( int i=0 ; i ) , rowSizes[i] , fp )!=rowSizes[i] ) return false; return true; } template< class T > bool SparseMatrix< T >::read( FILE* fp ) { int r; if( fread( &r , sizeof( int ) , 1 , fp )!=1 ) return false; Resize( r ); if( fread( rowSizes , sizeof( int ) , rows , fp )!=rows ) return false; for( int i=0 ; i ) , rowSizes[i] , fp )!=rowSizes[i] ) return false; } return true; } template< class T > void SparseMatrix< T >::Resize( int r ) { if( rows>0 ) { if( _contiguous ){ if( _maxEntriesPerRow ) FreePointer( m_ppElements[0] ); } else for( int i=0 ; i( r ); m_ppElements = AllocPointer< Pointer( MatrixEntry< T > ) >( r ); memset( rowSizes , 0 , sizeof( int ) * r ); } _contiguous = false; _maxEntriesPerRow = 0; } template< class T > void SparseMatrix< T >::Resize( int r , int e ) { if( rows>0 ) { if( _contiguous ){ if( _maxEntriesPerRow ) FreePointer( m_ppElements[0] ); } else for( int i=0 ; i( r ); m_ppElements = AllocPointer< Pointer( MatrixEntry< T > ) >( r ); m_ppElements[0] = AllocPointer< MatrixEntry< T > >( r * e ); memset( rowSizes , 0 , sizeof( int ) * r ); for( int i=1 ; i void SparseMatrix< T >::SetRowSize( int row , int count ) { if( _contiguous ) { if( count>_maxEntriesPerRow ) fprintf( stderr , "[ERROR] Cannot set row size on contiguous matrix: %d<=%d\n" , count , _maxEntriesPerRow ) , exit( 0 ); rowSizes[row] = count; } else if( row>=0 && row0 ) m_ppElements[row] = AllocPointer< MatrixEntry< T > >( count ); // [WARNING] Why wasn't this line here before??? rowSizes[row] = count; } } template void SparseMatrix::SetZero() { Resize(this->m_N, this->m_M); } template SparseMatrix SparseMatrix::operator * (const T& V) const { SparseMatrix M(*this); M *= V; return M; } template SparseMatrix& SparseMatrix::operator *= (const T& V) { for( int i=0 ; i template< class T2 > void SparseMatrix< T >::Multiply( ConstPointer( T2 ) in , Pointer( T2 ) out , int threads ) const { #pragma omp parallel for num_threads( threads ) for( int i=0 ; i ) start = m_ppElements[i]; ConstPointer( MatrixEntry< T > ) end = start + rowSizes[i]; ConstPointer( MatrixEntry< T > ) e; for( e=start ; e!=end ; e++ ) _out += in[ e->N ] * e->Value; out[i] = _out; } } template< class T > template< class T2 > void SparseMatrix< T >::MultiplyAndAddAverage( ConstPointer( T2 ) in , Pointer( T2 ) out , int threads ) const { T2 average = 0; for( int i=0 ; i template< class T2 > int SparseMatrix::SolveJacobi( const SparseMatrix& M , ConstPointer( T2 ) diagonal , ConstPointer( T2 ) b , Pointer( T2 ) x , Pointer( T2 ) Mx , T2 sor , int threads ) { M.Multiply( x , Mx , threads ); #if ZERO_TESTING_JACOBI for( int j=0 ; j template< class T2 > int SparseMatrix::SolveJacobi( const SparseMatrix& M , ConstPointer( T2 ) b , Pointer( T2 ) x , Pointer( T2 ) Mx , T2 sor , int threads ) { M.Multiply( x , Mx , threads ); #if ZERO_TESTING_JACOBI for( int j=0 ; j template int SparseMatrix::SolveGS( const SparseMatrix& M , ConstPointer( T2 ) diagonal , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward ) { #define ITERATE \ { \ ConstPointer( MatrixEntry< T > ) start = M[j]; \ ConstPointer( MatrixEntry< T > ) end = start + M.rowSizes[j]; \ ConstPointer( MatrixEntry< T > ) e; \ T2 _b = b[j]; \ for( e=start ; e!=end ; e++ ) _b -= x[ e->N ] * e->Value; \ x[j] += _b / diagonal[j]; \ } #if ZERO_TESTING_JACOBI if( forward ) for( int j=0 ; j=0 ; j-- ){ if( diagonal[j] ){ ITERATE; } } #else // !ZERO_TESTING_JACOBI if( forward ) for( int j=0 ; j=0 ; j-- ){ ITERATE; } #endif // ZERO_TESTING_JACOBI #undef ITERATE return M.rows; } template template int SparseMatrix::SolveGS( const std::vector< std::vector< int > >& mcIndices , const SparseMatrix& M , ConstPointer( T2 ) diagonal , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward , int threads ) { int sum=0; #ifdef _WIN32 #define SetOMPParallel __pragma( omp parallel for num_threads( threads ) ) #else // !_WIN32 #define SetOMPParallel _Pragma( "omp parallel for num_threads( threads )" ) #endif // _WIN32 #if ZERO_TESTING_JACOBI #define ITERATE( indices ) \ { \ SetOMPParallel \ for( int k=0 ; k ) start = M[jj]; \ ConstPointer( MatrixEntry< T > ) end = start + M.rowSizes[jj]; \ ConstPointer( MatrixEntry< T > ) e; \ T2 _b = b[jj]; \ for( e=start ; e!=end ; e++ ) _b -= x[ e->N ] * e->Value; \ x[jj] += _b / diagonal[jj]; \ } \ } #else // !ZERO_TESTING_JACOBI #define ITERATE( indices ) \ { \ SetOMPParallel \ for( int k=0 ; k ) start = M[jj]; \ ConstPointer( MatrixEntry< T > ) end = start + M.rowSizes[jj]; \ ConstPointer( MatrixEntry< T > ) e; \ T2 _b = b[jj]; \ for( e=start ; e!=end ; e++ ) _b -= x[ e->N ] * e->Value; \ x[jj] += _b / diagonal[jj]; \ } \ } #endif // ZERO_TESTING_JACOBI if( forward ) for( int j=0 ; j=0 ; j-- ){ sum += int( mcIndices[j].size() ) ; ITERATE( mcIndices[j] ); } #undef ITERATE #undef SetOMPParallel return sum; } template template int SparseMatrix::SolveGS( const SparseMatrix& M , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward ) { int start = forward ? 0 : M.rows-1 , end = forward ? M.rows : -1 , dir = forward ? 1 : -1; for( int j=start ; j!=end ; j+=dir ) { T diagonal = M[j][0].Value; #if ZERO_TESTING_JACOBI if( diagonal ) #endif // ZERO_TESTING_JACOBI { ConstPointer( MatrixEntry< T > ) start = M[j]; ConstPointer( MatrixEntry< T > ) end = start + M.rowSizes[j]; ConstPointer( MatrixEntry< T > ) e; start++; T2 _b = b[j]; for( e=start ; e!=end ; e++ ) _b -= x[ e->N ] * e->Value; x[j] = _b / diagonal; } } return M.rows; } template template int SparseMatrix::SolveGS( const std::vector< std::vector< int > >& mcIndices , const SparseMatrix& M , ConstPointer( T2 ) b , Pointer( T2 ) x , bool forward , int threads ) { int sum=0 , start = forward ? 0 : int( mcIndices.size() )-1 , end = forward ? int( mcIndices.size() ) : -1 , dir = forward ? 1 : -1; for( int j=start ; j!=end ; j+=dir ) { const std::vector< int >& _mcIndices = mcIndices[j]; sum += int( _mcIndices.size() ); { #pragma omp parallel for num_threads( threads ) for( int k=0 ; k ) start = M[jj]; ConstPointer( MatrixEntry< T > ) end = start + M.rowSizes[jj]; ConstPointer( MatrixEntry< T > ) e; start++; T2 _b = b[jj]; for( e=start ; e!=end ; e++ ) _b -= x[ e->N ] * e->Value; x[jj] = _b / diagonal; } } } } return sum; } template< class T > template< class T2 > void SparseMatrix< T >::getDiagonal( Pointer( T2 ) diagonal , int threads ) const { #pragma omp parallel for num_threads( threads ) for( int i=0 ; i ) start = m_ppElements[i]; ConstPointer( MatrixEntry< T > ) end = start + rowSizes[i]; ConstPointer( MatrixEntry< T > ) e; for( e=start ; e!=end ; e++ ) if( e->N==i ) d += e->Value; diagonal[i] = d; } } template< class T > template< class T2 > int SparseMatrix< T >::SolveCG( const SparseMatrix& A , ConstPointer( T2 ) b , int iters , Pointer( T2 ) x , T2 eps , int reset , bool addDCTerm , bool solveNormal , int threads ) { eps *= eps; int dim = A.rows; Pointer( T2 ) r = AllocPointer< T2 >( dim ); Pointer( T2 ) d = AllocPointer< T2 >( dim ); Pointer( T2 ) q = AllocPointer< T2 >( dim ); Pointer( T2 ) temp = NullPointer( T2 ); if( reset ) memset( x , 0 , sizeof(T2)* dim ); if( solveNormal ) temp = AllocPointer< T2 >( dim ); double delta_new = 0 , delta_0; if( solveNormal ) { if( addDCTerm ) A.MultiplyAndAddAverage( ( ConstPointer( T2 ) )x , temp , threads ) , A.MultiplyAndAddAverage( ( ConstPointer( T2 ) )temp , r , threads ) , A.MultiplyAndAddAverage( ( ConstPointer( T2 ) )b , temp , threads ); else A.Multiply( ( ConstPointer( T2 ) )x , temp , threads ) , A.Multiply( ( ConstPointer( T2 ) )temp , r , threads ) , A.Multiply( ( ConstPointer( T2 ) )b , temp , threads ); #pragma omp parallel for num_threads( threads ) reduction( + : delta_new ) for( int i=0 ; ieps*delta_0 ; ii++ ) { if( solveNormal ) if( addDCTerm ) A.MultiplyAndAddAverage( ( ConstPointer( T2 ) )d , temp , threads ) , A.MultiplyAndAddAverage( ( ConstPointer( T2 ) )temp , q , threads ); else A.Multiply( ( ConstPointer( T2 ) )d , temp , threads ) , A.Multiply( ( ConstPointer( T2 ) )temp , q , threads ); else if( addDCTerm ) A.MultiplyAndAddAverage( ( ConstPointer( T2 ) )d , q , threads ); else A.Multiply( ( ConstPointer( T2 ) )d , q , threads ); double dDotQ = 0; #pragma omp parallel for num_threads( threads ) reduction( + : dDotQ ) for( int i=0 ; i #include #include #ifdef _OPENMP #include #endif // _OPENMP #include #include "CmdLineParser.h" #include "Geometry.h" #include "Ply.h" #include "MAT.h" #include "MyTime.h" cmdLineString In( "in" ) , Out( "out" ); cmdLineInt Smooth( "smooth" , 5 ); cmdLineFloat Trim( "trim" ) , IslandAreaRatio( "aRatio" , 0.001f ); cmdLineReadable PolygonMesh( "polygonMesh" ); cmdLineReadable* params[] = { &In , &Out , &Trim , &PolygonMesh , &Smooth , &IslandAreaRatio }; void ShowUsage( char* ex ) { printf( "Usage: %s\n" , ex ); printf( "\t --%s \n" , In.name ); printf( "\t[--%s ]\n" , Out.name ); printf( "\t[--%s =%d]\n" , Smooth.name , Smooth.value ); printf( "\t[--%s ]\n" , Trim.name ); printf( "\t[--%s =%f]\n" , IslandAreaRatio.name , IslandAreaRatio.value ); printf( "\t[--%s]\n" , PolygonMesh.name ); } long long EdgeKey( int key1 , int key2 ) { if( key1 Vertex InterpolateVertices( const Vertex& v1 , const Vertex& v2 , Real value ) { typename Vertex::Wrapper _v1(v1) , _v2(v2); if( _v1.value==_v2.value ) return Vertex( (_v1+_v2)/Real(2.) ); Real dx = ( _v1.value-value ) / ( _v1.value-_v2.value ); return Vertex( _v1*(1.f-dx) + _v2*dx ); } template< class Real , class Vertex > void SmoothValues( std::vector< Vertex >& vertices , const std::vector< std::vector< int > >& polygons ) { std::vector< int > count( vertices.size() ); std::vector< Real > sums( vertices.size() , 0 ); for( size_t i=0 ; i void SplitPolygon ( const std::vector< int >& polygon , std::vector< Vertex >& vertices , std::vector< std::vector< int > >* ltPolygons , std::vector< std::vector< int > >* gtPolygons , std::vector< bool >* ltFlags , std::vector< bool >* gtFlags , hash_map< long long , int >& vertexTable , Real trimValue ) { int sz = int( polygon.size() ); std::vector< bool > gt( sz ); int gtCount = 0; for( int j=0 ; jtrimValue ); if( gt[j] ) gtCount++; } if ( gtCount==sz ){ if( gtPolygons ) gtPolygons->push_back( polygon ) ; if( gtFlags ) gtFlags->push_back( false ); } else if( gtCount==0 ){ if( ltPolygons ) ltPolygons->push_back( polygon ) ; if( ltFlags ) ltFlags->push_back( false ); } else { int start; for( start=0 ; start poly; // Add the initial vertex { int j1 = (start+int(sz)-1)%sz , j2 = start; int v1 = polygon[j1] , v2 = polygon[j2]; int vIdx; hash_map< long long , int >::iterator iter = vertexTable.find( EdgeKey( v1 , v2 ) ); if( iter==vertexTable.end() ) { vertexTable[ EdgeKey( v1 , v2 ) ] = vIdx = int( vertices.size() ); vertices.push_back( InterpolateVertices( vertices[v1] , vertices[v2] , trimValue ) ); } else vIdx = iter->second; poly.push_back( vIdx ); } for( int _j=0 ; _j<=sz ; _j++ ) { int j1 = (_j+start+sz-1)%sz , j2 = (_j+start)%sz; int v1 = polygon[j1] , v2 = polygon[j2]; if( gt[j2]==gtFlag ) poly.push_back( v2 ); else { int vIdx; hash_map< long long , int >::iterator iter = vertexTable.find( EdgeKey( v1 , v2 ) ); if( iter==vertexTable.end() ) { vertexTable[ EdgeKey( v1 , v2 ) ] = vIdx = int( vertices.size() ); vertices.push_back( InterpolateVertices( vertices[v1] , vertices[v2] , trimValue ) ); } else vIdx = iter->second; poly.push_back( vIdx ); if( gtFlag ){ if( gtPolygons ) gtPolygons->push_back( poly ) ; if( ltFlags ) ltFlags->push_back( true ); } else { if( ltPolygons ) ltPolygons->push_back( poly ) ; if( gtFlags ) gtFlags->push_back( true ); } poly.clear() , poly.push_back( vIdx ) , poly.push_back( v2 ); gtFlag = !gtFlag; } } } } template< class Real , class Vertex > void Triangulate( const std::vector< Vertex >& vertices , const std::vector< std::vector< int > >& polygons , std::vector< std::vector< int > >& triangles ) { triangles.clear(); for( size_t i=0 ; i3 ) { MinimalAreaTriangulation< Real > mat; std::vector< Point3D< Real > > _vertices( polygons[i].size() ); std::vector< TriangleIndex > _triangles; for( int j=0 ; j double PolygonArea( const std::vector< Vertex >& vertices , const std::vector< int >& polygon ) { if( polygon.size()<3 ) return 0.; else if( polygon.size()==3 ) return TriangleArea( vertices[polygon[0]].point , vertices[polygon[1]].point , vertices[polygon[2]].point ); else { Point3D< Real > center; for( size_t i=0 ; i void RemoveHangingVertices( std::vector< Vertex >& vertices , std::vector< std::vector< int > >& polygons ) { hash_map< int , int > vMap; std::vector< bool > vertexFlags( vertices.size() , false ); for( size_t i=0 ; i _vertices( vCount ); for( int i=0 ; i >& polygons , std::vector< std::vector< int > >& components ) { std::vector< int > polygonRoots( polygons.size() ); for( size_t i=0 ; i edgeTable; for( size_t i=0 ; i::iterator iter = edgeTable.find( eKey ); if( iter==edgeTable.end() ) edgeTable[ eKey ] = int(i); else { int p = iter->second; while( polygonRoots[p]!=p ) { int temp = polygonRoots[p]; polygonRoots[p] = int(i); p = temp; } polygonRoots[p] = int(i); } } } for( size_t i=0 ; i vMap; for( int i= 0 ; i inline Point3D< Real > CrossProduct( Point3D< Real > p1 , Point3D< Real > p2 ){ return Point3D< Real >( p1[1]*p2[2]-p1[2]*p2[1] , p1[2]*p2[0]-p1[0]*p2[2] , p1[0]*p1[1]-p1[1]*p2[0] ); } template< class Real > double TriangleArea( Point3D< Real > v1 , Point3D< Real > v2 , Point3D< Real > v3 ) { Point3D< Real > n = CrossProduct( v2-v1 , v3-v1 ); return sqrt( n[0]*n[0] + n[1]*n[1] + n[2]*n[2] ) / 2.; } template< class Vertex > int Execute( void ) { float min , max; int paramNum = sizeof(params)/sizeof(cmdLineReadable*); std::vector< Vertex > vertices; std::vector< std::vector< int > > polygons; int ft , commentNum = paramNum+2; char** comments; PlyReadPolygons( In.value , vertices , polygons , Vertex::ReadProperties , Vertex::ReadComponents , ft , &comments , &commentNum ); for( int i=0 ; i( vertices , polygons ); min = max = vertices[0].value; for( size_t i=0 ; i( min , vertices[i].value ) , max = std::max< float >( max , vertices[i].value ); printf( "Value Range: [%f,%f]\n" , min , max ); hash_map< long long , int > vertexTable; std::vector< std::vector< int > > ltPolygons , gtPolygons; std::vector< bool > ltFlags , gtFlags; for( int i=0 ; i0 ) { std::vector< std::vector< int > > _ltPolygons , _gtPolygons; std::vector< std::vector< int > > ltComponents , gtComponents; SetConnectedComponents( ltPolygons , ltComponents ); SetConnectedComponents( gtPolygons , gtComponents ); std::vector< double > ltAreas( ltComponents.size() , 0. ) , gtAreas( gtComponents.size() , 0. ); std::vector< bool > ltComponentFlags( ltComponents.size() , false ) , gtComponentFlags( gtComponents.size() , false ); double area = 0.; for( size_t i=0 ; i( vertices , ltPolygons[ ltComponents[i][j] ] ); ltComponentFlags[i] = ( ltComponentFlags[i] || ltFlags[ ltComponents[i][j] ] ); } area += ltAreas[i]; } for( size_t i=0 ; i( vertices , gtPolygons[ gtComponents[i][j] ] ); gtComponentFlags[i] = ( gtComponentFlags[i] || gtFlags[ gtComponents[i][j] ] ); } area += gtAreas[i]; } for( size_t i=0 ; i > polys = ltPolygons; Triangulate< float , Vertex >( vertices , ltPolygons , polys ) , ltPolygons = polys; } { std::vector< std::vector< int > > polys = gtPolygons; Triangulate< float , Vertex >( vertices , gtPolygons , polys ) , gtPolygons = polys; } } RemoveHangingVertices( vertices , gtPolygons ); sprintf( comments[commentNum++] , "#Trimmed In: %9.1f (s)" , Time()-t ); if( Out.set ) PlyWritePolygons( Out.value , vertices , gtPolygons , Vertex::WriteProperties , Vertex::WriteComponents , ft , comments , commentNum ); return EXIT_SUCCESS; } int SurfaceTrimmer( int argc , char* argv[] ) { int paramNum = sizeof(params)/sizeof(cmdLineReadable*); cmdLineParse( argc-1 , &argv[1] , paramNum , params , 0 ); if( !In.set || !Trim.set ) { ShowUsage( argv[0] ); return EXIT_FAILURE; } bool readFlags[ PlyColorAndValueVertex< float >::ReadComponents ]; if( !PlyReadHeader( In.value , PlyColorAndValueVertex< float >::ReadProperties , PlyColorAndValueVertex< float >::ReadComponents , readFlags ) ) fprintf( stderr , "[ERROR] Failed to read ply header: %s\n" , In.value ) , exit( 0 ); bool hasValue = readFlags[3]; bool hasColor = ( readFlags[4] || readFlags[7] ) && ( readFlags[5] || readFlags[8] ) && ( readFlags[6] || readFlags[9] ); if( !hasValue ) fprintf( stderr , "[ERROR] Ply file does not contain values\n" ) , exit( 0 ); if( hasColor ) return Execute< PlyColorAndValueVertex< float > >(); else return Execute< PlyValueVertex< float > >(); } colmap-3.9.1/src/thirdparty/PoissonRecon/SurfaceTrimmer.h000066400000000000000000000000541454702036400235060ustar00rootroot00000000000000int SurfaceTrimmer(int argc, char* argv[]); colmap-3.9.1/src/thirdparty/SiftGPU/000077500000000000000000000000001454702036400172465ustar00rootroot00000000000000colmap-3.9.1/src/thirdparty/SiftGPU/CLTexImage.cpp000066400000000000000000000154671454702036400217110ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: CLTexImage.cpp // Author: Changchang Wu // Description : implementation of the CLTexImage class. // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CL_SIFTGPU_ENABLED) #include "GL/glew.h" #include #include #include #include #include using namespace std; #include #include "CLTexImage.h" #include "ProgramCL.h" #include "GlobalUtil.h" CLTexImage::CLTexImage() { _context = NULL; _queue = NULL; _clData = NULL; _numChannel = _bufferLen = _fromGL = 0; _imgWidth = _imgHeight = _texWidth = _texHeight = 0; } CLTexImage::CLTexImage(cl_context context, cl_command_queue queue) { _context = context; _queue = queue; _clData = NULL; _numChannel = _bufferLen = _fromGL = 0; _imgWidth = _imgHeight = _texWidth = _texHeight = 0; } void CLTexImage::SetContext(cl_context context, cl_command_queue queue) { _context = context; _queue = queue; } CLTexImage::~CLTexImage() { ReleaseTexture(); } void CLTexImage::ReleaseTexture() { if(_fromGL) clEnqueueReleaseGLObjects(_queue, 1, &_clData, 0, NULL, NULL); if(_clData) clReleaseMemObject(_clData); } void CLTexImage::SetImageSize(int width, int height) { _imgWidth = width; _imgHeight = height; } void CLTexImage::InitBufferTex(int width, int height, int nchannel) { if(width == 0 || height == 0 || nchannel <= 0 || _fromGL) return; _imgWidth = width; _imgHeight = height; _texWidth = _texHeight = _fromGL = 0; _numChannel = min(nchannel, 4); int size = width * height * _numChannel * sizeof(float); if (size <= _bufferLen) return; //allocate the buffer data cl_int status; if(_clData) status = clReleaseMemObject(_clData); _clData = clCreateBuffer(_context, CL_MEM_READ_WRITE, _bufferLen = size, NULL, &status); ProgramBagCL::CheckErrorCL(status, "CLTexImage::InitBufferTex"); } void CLTexImage::InitTexture(int width, int height, int nchannel) { if(width == 0 || height == 0 || nchannel <= 0 || _fromGL) return; if(_clData && width == _texWidth && height == _texHeight && _numChannel == nchannel) return; if(_clData) clReleaseMemObject(_clData); _texWidth = _imgWidth = width; _texHeight = _imgHeight = height; _numChannel = nchannel; _bufferLen = _fromGL = 0; cl_int status; cl_image_format format; if(nchannel == 1) format.image_channel_order = CL_R; else if(nchannel == 2) format.image_channel_order = CL_RG; else if(nchannel == 3) format.image_channel_order = CL_RGB; else format.image_channel_order = CL_RGBA; format.image_channel_data_type = CL_FLOAT; _clData = clCreateImage2D(_context, CL_MEM_READ_WRITE, & format, _texWidth, _texHeight, 0, 0, &status); ProgramBagCL::CheckErrorCL(status, "CLTexImage::InitTexture"); } void CLTexImage::InitPackedTex(int width, int height, int packed) { if(packed) InitTexture((width + 1) >> 1, (height + 1) >> 1, 4); else InitTexture(width, height, 1); } void CLTexImage::SetPackedSize(int width, int height, int packed) { if(packed) SetImageSize((width + 1) >> 1, (height + 1) >> 1); else SetImageSize(width, height); } void CLTexImage::InitTextureGL(GLuint tex, int width, int height, int nchannel) { if(tex == 0) return; if(_clData) clReleaseMemObject(_clData); ////create the memory object cl_int status; _clData = clCreateFromGLTexture2D(_context, CL_MEM_WRITE_ONLY, GlobalUtil::_texTarget, 0 , tex, &status); ProgramBagCL::CheckErrorCL(status, "CLTexImage::InitTextureGL->clCreateFromGLTexture2D"); if(status != CL_SUCCESS) return; _texWidth = _imgWidth = width; _texHeight = _imgHeight = height; _numChannel = nchannel; _bufferLen = 0; _fromGL = 1; ////acquire object status = clEnqueueAcquireGLObjects(_queue, 1, &_clData, 0, NULL, NULL); ProgramBagCL::CheckErrorCL(status, "CLTexImage::InitTextureGL->clEnqueueAcquireGLObjects"); } void CLTexImage::CopyFromHost(const void * buf) { if(_clData == NULL) return; cl_int status; if(_bufferLen) { status = clEnqueueWriteBuffer(_queue, _clData, false, 0, _imgWidth * _imgHeight * _numChannel * sizeof(float), buf, 0, NULL, NULL); }else { size_t origin[3] = {0, 0, 0}, region[3] = {_imgWidth, _imgHeight, 1}; size_t row_pitch = _imgWidth * _numChannel * sizeof(float); status = clEnqueueWriteImage(_queue, _clData, false, origin, region, row_pitch, 0, buf, 0, 0, 0); } ProgramBagCL::CheckErrorCL(status, "CLTexImage::CopyFromHost"); } int CLTexImage::GetImageDataSize() { return _imgWidth * _imgHeight * _numChannel * sizeof(float); } int CLTexImage::CopyToPBO(GLuint pbo) { glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pbo); int esize = GetImageDataSize(), bsize; glGetBufferParameteriv(GL_PIXEL_UNPACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); if(bsize < esize) { glBufferData(GL_PIXEL_UNPACK_BUFFER_ARB, esize, NULL, GL_STATIC_DRAW_ARB); glGetBufferParameteriv(GL_PIXEL_UNPACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); } if(bsize >= esize) { // map the buffer object into client's memory void* ptr = glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, GL_WRITE_ONLY_ARB); CopyToHost(ptr); clFinish(_queue); glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB); } glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0); GlobalUtil::CheckErrorsGL("CLTexImage::CopyToPBO"); return esize >= bsize; } void CLTexImage::CopyToHost(void * buf) { if(_clData == NULL) return; cl_int status; if(_bufferLen) { status = clEnqueueReadBuffer(_queue, _clData, true, 0, _imgWidth * _imgHeight * _numChannel * sizeof(float), buf, 0, NULL, NULL); }else { size_t origin[3] = {0, 0, 0}, region[3] = {_imgWidth, _imgHeight, 1}; size_t row_pitch = _imgWidth * _numChannel * sizeof(float); status = clEnqueueReadImage(_queue, _clData, true, origin, region, row_pitch, 0, buf, 0, 0, 0); } ProgramBagCL::CheckErrorCL(status, "CLTexImage::CopyToHost"); } #endif colmap-3.9.1/src/thirdparty/SiftGPU/CLTexImage.h000066400000000000000000000053401454702036400213430ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: CLTexImage.h // Author: Changchang Wu // Description : interface for the CLTexImage class. // class for storing data in CUDA. // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CL_SIFTGPU_ENABLED) #ifndef CL_TEX_IMAGE_H #define CL_TEX_IMAGE_H class GLTexImage; class CLTexImage { protected: cl_context _context; cl_command_queue _queue; cl_mem _clData; int _numChannel; int _imgWidth; int _imgHeight; int _texWidth; int _texHeight; int _bufferLen; int _fromGL; private: void ReleaseTexture(); public: void SetImageSize(int width, int height); void SetPackedSize(int width, int height, int packed); void InitBufferTex(int width, int height, int nchannel); void InitTexture(int width, int height, int nchannel); void InitPackedTex(int width, int height, int packed); void InitTextureGL(GLuint tex, int width, int height, int nchannel); void CopyToHost(void* buf); void CopyFromHost(const void* buf); public: int CopyToPBO(GLuint pbo); int GetImageDataSize(); public: inline operator cl_mem(){return _clData; } inline int GetImgWidth(){return _imgWidth;} inline int GetImgHeight(){return _imgHeight;} inline int GetTexWidth(){return _texWidth;} inline int GetTexHeight(){return _texHeight;} inline int GetDataSize(){return _bufferLen;} inline bool IsImage2D() {return _bufferLen == 0;} inline int GetImgPixelCount(){return _imgWidth*_imgHeight;} inline int GetTexPixelCount(){return _texWidth*_texHeight;} public: CLTexImage(); CLTexImage(cl_context context, cl_command_queue queue); void SetContext(cl_context context, cl_command_queue queue); virtual ~CLTexImage(); friend class ProgramCL; friend class PyramidCL; friend class ProgramBagCL; friend class ProgramBagCLN; }; ////////////////////////////////////////////////// //transfer OpenGL Texture to PBO, then to CUDA vector //#endif #endif // !defined(CU_TEX_IMAGE_H) #endif colmap-3.9.1/src/thirdparty/SiftGPU/CMakeLists.txt000066400000000000000000000016711454702036400220130ustar00rootroot00000000000000add_definitions("-DSIFTGPU_NO_DEVIL") set(OPTIONAL_CUDA_SRCS) set(OPTIONAL_CUDA_LINK_LIBS) if(CUDA_ENABLED) add_definitions("-DCUDA_SIFTGPU_ENABLED") set(OPTIONAL_CUDA_SRCS CuTexImage.h CuTexImage.cpp ProgramCU.cu ProgramCU.h PyramidCU.h PyramidCU.cpp SiftMatchCU.h SiftMatchCU.cpp) set(OPTIONAL_CUDA_LINK_LIBS CUDA::cudart CUDA::curand ) endif() COLMAP_ADD_LIBRARY( NAME colmap_sift_gpu SRCS FrameBufferObject.h FrameBufferObject.cpp GlobalUtil.h GlobalUtil.cpp GLTexImage.h GLTexImage.cpp ProgramGLSL.h ProgramGLSL.cpp ProgramGPU.h PyramidGL.h PyramidGL.cpp ShaderMan.h ShaderMan.cpp SiftGPU.h SiftGPU.cpp SiftMatch.h SiftMatch.cpp SiftPyramid.h SiftPyramid.cpp ${OPTIONAL_CUDA_SRCS} PRIVATE_LINK_LIBS OpenGL::GL GLEW::GLEW ${OPTIONAL_CUDA_LINK_LIBS} ) colmap-3.9.1/src/thirdparty/SiftGPU/CuTexImage.cpp000066400000000000000000000145661454702036400217610ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: CuTexImage.cpp // Author: Changchang Wu // Description : implementation of the CuTexImage class. // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CUDA_SIFTGPU_ENABLED) #include "GL/glew.h" #include #include #include #include #include #include using namespace std; #include #include #include #include "GlobalUtil.h" #include "GLTexImage.h" #include "CuTexImage.h" #include "ProgramCU.h" CuTexImage::CuTexObj::~CuTexObj() { cudaDestroyTextureObject(handle); } CuTexImage::CuTexObj CuTexImage::BindTexture(const cudaTextureDesc& textureDesc, const cudaChannelFormatDesc& channelFmtDesc) { CuTexObj texObj; cudaResourceDesc resourceDesc; memset(&resourceDesc, 0, sizeof(resourceDesc)); resourceDesc.resType = cudaResourceTypeLinear; resourceDesc.res.linear.devPtr = _cuData; resourceDesc.res.linear.desc = channelFmtDesc; resourceDesc.res.linear.sizeInBytes = _numBytes; cudaCreateTextureObject(&texObj.handle, &resourceDesc, &textureDesc, nullptr); ProgramCU::CheckErrorCUDA("CuTexImage::BindTexture"); return texObj; } CuTexImage::CuTexObj CuTexImage::BindTexture2D(const cudaTextureDesc& textureDesc, const cudaChannelFormatDesc& channelFmtDesc) { CuTexObj texObj; cudaResourceDesc resourceDesc; memset(&resourceDesc, 0, sizeof(resourceDesc)); resourceDesc.resType = cudaResourceTypePitch2D; resourceDesc.res.pitch2D.devPtr = _cuData; resourceDesc.res.pitch2D.width = _imgWidth; resourceDesc.res.pitch2D.height = _imgHeight; resourceDesc.res.pitch2D.pitchInBytes = _imgWidth * _numChannel * sizeof(float); resourceDesc.res.pitch2D.desc = channelFmtDesc; cudaCreateTextureObject(&texObj.handle, &resourceDesc, &textureDesc, nullptr); ProgramCU::CheckErrorCUDA("CuTexImage::BindTexture2D"); return texObj; } CuTexImage::CuTexImage() { _cuData = NULL; _cuData2D = NULL; _fromPBO = 0; _numChannel = _numBytes = 0; _imgWidth = _imgHeight = _texWidth = _texHeight = 0; } CuTexImage::CuTexImage(int width, int height, int nchannel, GLuint pbo) { _cuData = NULL; //check size of pbo GLint bsize, esize = width * height * nchannel * sizeof(float); glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, pbo); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); if(bsize < esize) { glBufferData(GL_PIXEL_PACK_BUFFER_ARB, esize, NULL, GL_STATIC_DRAW_ARB); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); } glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, 0); if(bsize >=esize) { cudaGLRegisterBufferObject(pbo); cudaGLMapBufferObject(&_cuData, pbo); ProgramCU::CheckErrorCUDA("cudaGLMapBufferObject"); _fromPBO = pbo; }else { _cuData = NULL; _fromPBO = 0; } if(_cuData) { _numBytes = bsize; _imgWidth = width; _imgHeight = height; _numChannel = nchannel; }else { _numBytes = 0; _imgWidth = 0; _imgHeight = 0; _numChannel = 0; } _texWidth = _texHeight =0; _cuData2D = NULL; } CuTexImage::~CuTexImage() { if(_fromPBO) { cudaGLUnmapBufferObject(_fromPBO); cudaGLUnregisterBufferObject(_fromPBO); }else if(_cuData) { cudaFree(_cuData); } if(_cuData2D) cudaFreeArray(_cuData2D); } void CuTexImage::SetImageSize(int width, int height) { _imgWidth = width; _imgHeight = height; } bool CuTexImage::InitTexture(int width, int height, int nchannel) { _imgWidth = width; _imgHeight = height; _numChannel = min(max(nchannel, 1), 4); const size_t size = width * height * _numChannel * sizeof(float); if (size < 0) { return false; } // SiftGPU uses int for all indexes and // this ensures that all elements can be accessed. if (size >= INT_MAX * sizeof(float)) { return false; } if(size <= _numBytes) return true; if(_cuData) cudaFree(_cuData); //allocate the array data const cudaError_t status = cudaMalloc(&_cuData, _numBytes = size); if (status != cudaSuccess) { _cuData = NULL; _numBytes = 0; return false; } return true; } void CuTexImage::CopyFromHost(const void * buf) { if(_cuData == NULL) return; cudaMemcpy( _cuData, buf, _imgWidth * _imgHeight * _numChannel * sizeof(float), cudaMemcpyHostToDevice); } void CuTexImage::CopyToHost(void * buf) { if(_cuData == NULL) return; cudaMemcpy(buf, _cuData, _imgWidth * _imgHeight * _numChannel * sizeof(float), cudaMemcpyDeviceToHost); } void CuTexImage::CopyToHost(void * buf, int stream) { if(_cuData == NULL) return; cudaMemcpyAsync(buf, _cuData, _imgWidth * _imgHeight * _numChannel * sizeof(float), cudaMemcpyDeviceToHost, (cudaStream_t)stream); } void CuTexImage::CopyFromPBO(int width, int height, GLuint pbo) { void* pbuf =NULL; GLint esize = width * height * sizeof(float); cudaGLRegisterBufferObject(pbo); cudaGLMapBufferObject(&pbuf, pbo); cudaMemcpy(_cuData, pbuf, esize, cudaMemcpyDeviceToDevice); cudaGLUnmapBufferObject(pbo); cudaGLUnregisterBufferObject(pbo); } int CuTexImage::CopyToPBO(GLuint pbo) { void* pbuf =NULL; GLint bsize, esize = _imgWidth * _imgHeight * sizeof(float) * _numChannel; glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, pbo); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); if(bsize < esize) { glBufferData(GL_PIXEL_PACK_BUFFER_ARB, esize*3/2, NULL, GL_STATIC_DRAW_ARB); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); } glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, 0); if(bsize >= esize) { cudaGLRegisterBufferObject(pbo); cudaGLMapBufferObject(&pbuf, pbo); cudaMemcpy(pbuf, _cuData, esize, cudaMemcpyDeviceToDevice); cudaGLUnmapBufferObject(pbo); cudaGLUnregisterBufferObject(pbo); return 1; }else { return 0; } } #endif colmap-3.9.1/src/thirdparty/SiftGPU/CuTexImage.h000066400000000000000000000044231454702036400214150ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: CuTexImage.h // Author: Changchang Wu // Description : interface for the CuTexImage class. // class for storing data in CUDA. // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef CU_TEX_IMAGE_H #define CU_TEX_IMAGE_H #include class GLTexImage; class CuTexImage { protected: void* _cuData; cudaArray* _cuData2D; int _numChannel; size_t _numBytes; int _imgWidth; int _imgHeight; int _texWidth; int _texHeight; GLuint _fromPBO; public: struct CuTexObj { cudaTextureObject_t handle; ~CuTexObj(); }; virtual void SetImageSize(int width, int height); virtual bool InitTexture(int width, int height, int nchannel = 1); CuTexObj BindTexture(const cudaTextureDesc& textureDesc, const cudaChannelFormatDesc& channelFmtDesc); CuTexObj BindTexture2D(const cudaTextureDesc& textureDesc, const cudaChannelFormatDesc& channelFmtDesc); void CopyToHost(void* buf); void CopyToHost(void* buf, int stream); void CopyFromHost(const void* buf); int CopyToPBO(GLuint pbo); void CopyFromPBO(int width, int height, GLuint pbo); public: inline int GetImgWidth(){return _imgWidth;} inline int GetImgHeight(){return _imgHeight;} inline int GetDataSize(){return _numBytes;} public: CuTexImage(); CuTexImage(int width, int height, int nchannel, GLuint pbo); virtual ~CuTexImage(); friend class ProgramCU; friend class PyramidCU; }; ////////////////////////////////////////////////// //transfer OpenGL Texture to PBO, then to CUDA vector //#endif #endif // !defined(CU_TEX_IMAGE_H) colmap-3.9.1/src/thirdparty/SiftGPU/FrameBufferObject.cpp000066400000000000000000000055111454702036400232670ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: FrameBufferObject.cpp // Author: Changchang Wu // Description : Implementation of FrameBufferObject Class // // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include "GlobalUtil.h" #include "FrameBufferObject.h" //whether use only one FBO globally int FrameBufferObject::UseSingleFBO=1; GLuint FrameBufferObject::GlobalFBO=0; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// FrameBufferObject::FrameBufferObject(int autobind) { if(UseSingleFBO && GlobalFBO) { _fboID = GlobalFBO; }else { glGenFramebuffersEXT(1, &_fboID); if(UseSingleFBO )GlobalFBO = _fboID; } if(autobind ) glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _fboID); } FrameBufferObject::~FrameBufferObject() { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); if(!UseSingleFBO ) { glDeleteFramebuffersEXT (1,&_fboID); } } void FrameBufferObject::DeleteGlobalFBO() { if(UseSingleFBO) { glDeleteFramebuffersEXT (1,&GlobalFBO); GlobalFBO = 0; } } void FrameBufferObject::AttachDepthTexture(GLenum textureTarget, GLuint texID) { glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, textureTarget, texID, 0); } void FrameBufferObject::AttachTexture(GLenum textureTarget, GLenum attachment, GLuint texId) { glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, attachment, textureTarget, texId, 0); } void FrameBufferObject::BindFBO() { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _fboID); } void FrameBufferObject::UnbindFBO() { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); } void FrameBufferObject::UnattachTex(GLenum attachment) { glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, attachment, GL_TEXTURE_2D, 0, 0 ); } void FrameBufferObject::AttachRenderBuffer(GLenum attachment, GLuint buffID) { glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, attachment, GL_RENDERBUFFER_EXT, buffID); } void FrameBufferObject:: UnattachRenderBuffer(GLenum attachment) { glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, attachment, GL_RENDERBUFFER_EXT, 0); } colmap-3.9.1/src/thirdparty/SiftGPU/FrameBufferObject.h000066400000000000000000000031111454702036400227260ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: FrameBufferObject.h // Author: Changchang Wu // Description : interface for the FrameBufferObject class. // // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if !defined(_FRAME_BUFFER_OBJECT_H) #define _FRAME_BUFFER_OBJECT_H class FrameBufferObject { static GLuint GlobalFBO; //not thread-safe GLuint _fboID; public: static int UseSingleFBO; public: static void DeleteGlobalFBO(); static void UnattachTex(GLenum attachment); static void UnbindFBO(); static void AttachDepthTexture(GLenum textureTarget, GLuint texID); static void AttachTexture( GLenum textureTarget, GLenum attachment, GLuint texID); static void AttachRenderBuffer(GLenum attachment, GLuint buffID ); static void UnattachRenderBuffer(GLenum attachment); public: void BindFBO(); FrameBufferObject(int autobind = 1); ~FrameBufferObject(); }; #endif colmap-3.9.1/src/thirdparty/SiftGPU/GLTexImage.cpp000066400000000000000000001046361454702036400217120ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: GLTexImage.cpp // Author: Changchang Wu // Description : implementation of the GLTexImage class. // // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "GLTexImage.h" #include "FrameBufferObject.h" #include "ShaderMan.h" //#define SIFTGPU_NO_DEVIL #ifndef SIFTGPU_NO_DEVIL #include "IL/il.h" #else #include #endif ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// GLTexImage::GLTexImage() { _imgWidth = _imgHeight = 0; _texWidth = _texHeight = 0; _drawWidth = _drawHeight = 0; _texID = 0; } GLTexImage::~GLTexImage() { if(_texID) glDeleteTextures(1, &_texID); } int GLTexImage::CheckTexture() { if(_texID) { GLint tw, th; BindTex(); glGetTexLevelParameteriv(_texTarget, 0, GL_TEXTURE_WIDTH , &tw); glGetTexLevelParameteriv(_texTarget, 0, GL_TEXTURE_HEIGHT , &th); UnbindTex(); return tw == _texWidth && th == _texHeight; }else { return _texWidth == 0 && _texHeight ==0; } } //set a dimension that is smaller than the actuall size //for drawQuad void GLTexImage::SetImageSize( int width, int height) { _drawWidth = _imgWidth = width; _drawHeight = _imgHeight = height; } void GLTexImage::InitTexture( int width, int height, int clamp_to_edge) { if(_texID && width == _texWidth && height == _texHeight ) return; if(_texID==0) glGenTextures(1, &_texID); _texWidth = _imgWidth = _drawWidth = width; _texHeight = _imgHeight = _drawHeight = height; BindTex(); if(clamp_to_edge) { glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); }else { //out of bound tex read returns 0?? glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); } glTexParameteri(_texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(_texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glTexImage2D(_texTarget, 0, _iTexFormat, _texWidth, _texHeight, 0, GL_RGBA, GL_FLOAT, NULL); CheckErrorsGL("glTexImage2D"); UnbindTex(); } void GLTexImage::InitTexture( int width, int height, int clamp_to_edge, GLuint format) { if(_texID && width == _texWidth && height == _texHeight ) return; if(_texID==0) glGenTextures(1, &_texID); _texWidth = _imgWidth = _drawWidth = width; _texHeight = _imgHeight = _drawHeight = height; BindTex(); if(clamp_to_edge) { glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); }else { //out of bound tex read returns 0?? glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); } glTexParameteri(_texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(_texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glTexImage2D(_texTarget, 0, format, _texWidth, _texHeight, 0, GL_RGBA, GL_FLOAT, NULL); UnbindTex(); } void GLTexImage::BindTex() { glBindTexture(_texTarget, _texID); } void GLTexImage::UnbindTex() { glBindTexture(_texTarget, 0); } void GLTexImage::DrawQuad() { glBegin (GL_QUADS); glTexCoord2i ( 0 , 0 ); glVertex2i ( 0 , 0 ); glTexCoord2i ( 0 , _drawHeight ); glVertex2i ( 0 , _drawHeight ); glTexCoord2i ( _drawWidth , _drawHeight ); glVertex2i ( _drawWidth , _drawHeight ); glTexCoord2i ( _drawWidth , 0 ); glVertex2i ( _drawWidth , 0 ); glEnd (); glFlush(); } void GLTexImage::FillMargin(int marginx, int marginy) { // marginx = min(marginx, _texWidth - _imgWidth); marginy = min(marginy, _texHeight - _imgHeight); if(marginx >0 || marginy > 0) { GlobalUtil::FitViewPort(_imgWidth + marginx, _imgHeight + marginy); AttachToFBO(0); BindTex(); ShaderMan::UseShaderMarginCopy(_imgWidth, _imgHeight); DrawMargin(_imgWidth + marginx, _imgHeight + marginy); } } void GLTexImage::ZeroHistoMargin() { ZeroHistoMargin(_imgWidth, _imgHeight); } void GLTexImage::ZeroHistoMargin(int width, int height) { int marginx = width & 0x01; int marginy = height & 0x01; if(marginx >0 || marginy > 0) { int right = width + marginx; int bottom = height + marginy; GlobalUtil::FitViewPort(right, bottom); AttachToFBO(0); ShaderMan::UseShaderZeroPass(); glBegin(GL_QUADS); if(right > width && _texWidth > width) { glTexCoord2i ( width , 0 ); glVertex2i ( width , 0 ); glTexCoord2i ( width , bottom ); glVertex2i ( width , bottom ); glTexCoord2i ( right , bottom ); glVertex2i ( right , bottom ); glTexCoord2i ( right , 0 ); glVertex2i ( right , 0 ); } if(bottom>height && _texHeight > height) { glTexCoord2i ( 0 , height ); glVertex2i ( 0 , height ); glTexCoord2i ( 0 , bottom ); glVertex2i ( 0 , bottom ); glTexCoord2i ( width , bottom ); glVertex2i ( width , bottom ); glTexCoord2i ( width , height ); glVertex2i ( width , height ); } glEnd(); glFlush(); } } void GLTexImage::DrawMargin(int right, int bottom) { glBegin(GL_QUADS); if(right > _drawWidth) { glTexCoord2i ( _drawWidth , 0 ); glVertex2i ( _drawWidth , 0 ); glTexCoord2i ( _drawWidth , bottom ); glVertex2i ( _drawWidth , bottom ); glTexCoord2i ( right , bottom ); glVertex2i ( right , bottom ); glTexCoord2i ( right , 0 ); glVertex2i ( right , 0 ); } if(bottom>_drawHeight) { glTexCoord2i ( 0 , _drawHeight ); glVertex2i ( 0 , _drawHeight ); glTexCoord2i ( 0 , bottom ); glVertex2i ( 0 , bottom ); glTexCoord2i ( _drawWidth , bottom ); glVertex2i ( _drawWidth , bottom ); glTexCoord2i ( _drawWidth , _drawHeight ); glVertex2i ( _drawWidth , _drawHeight ); } glEnd(); glFlush(); } void GLTexImage::DrawQuadMT4() { int w = _drawWidth, h = _drawHeight; glBegin (GL_QUADS); glMultiTexCoord2i( GL_TEXTURE0, 0 , 0 ); glMultiTexCoord2i( GL_TEXTURE1, -1 , 0 ); glMultiTexCoord2i( GL_TEXTURE2, 1 , 0 ); glMultiTexCoord2i( GL_TEXTURE3, 0 , -1 ); glMultiTexCoord2i( GL_TEXTURE4, 0 , 1 ); glVertex2i ( 0 , 0 ); glMultiTexCoord2i( GL_TEXTURE0, 0 , h ); glMultiTexCoord2i( GL_TEXTURE1, -1 , h ); glMultiTexCoord2i( GL_TEXTURE2, 1 , h ); glMultiTexCoord2i( GL_TEXTURE3, 0 , h -1 ); glMultiTexCoord2i( GL_TEXTURE4, 0 , h +1 ); glVertex2i ( 0 , h ); glMultiTexCoord2i( GL_TEXTURE0, w , h ); glMultiTexCoord2i( GL_TEXTURE1, w-1 , h ); glMultiTexCoord2i( GL_TEXTURE2, w+1 , h ); glMultiTexCoord2i( GL_TEXTURE3, w , h-1 ); glMultiTexCoord2i( GL_TEXTURE4, w , h+1 ); glVertex2i ( w , h ); glMultiTexCoord2i( GL_TEXTURE0, w , 0 ); glMultiTexCoord2i( GL_TEXTURE1, w-1 , 0 ); glMultiTexCoord2i( GL_TEXTURE2, w+1 , 0 ); glMultiTexCoord2i( GL_TEXTURE3, w , -1 ); glMultiTexCoord2i( GL_TEXTURE4, w , 1 ); glVertex2i ( w , 0 ); glEnd (); glFlush(); } void GLTexImage::DrawQuadMT8() { int w = _drawWidth; int h = _drawHeight; glBegin (GL_QUADS); glMultiTexCoord2i( GL_TEXTURE0, 0 , 0 ); glMultiTexCoord2i( GL_TEXTURE1, -1 , 0 ); glMultiTexCoord2i( GL_TEXTURE2, 1 , 0 ); glMultiTexCoord2i( GL_TEXTURE3, 0 , -1 ); glMultiTexCoord2i( GL_TEXTURE4, 0 , 1 ); glMultiTexCoord2i( GL_TEXTURE5, -1 , -1 ); glMultiTexCoord2i( GL_TEXTURE6, -1 , 1 ); glMultiTexCoord2i( GL_TEXTURE7, 1 , -1 ); glVertex2i ( 0 , 0 ); glMultiTexCoord2i( GL_TEXTURE0, 0 , h ); glMultiTexCoord2i( GL_TEXTURE1, -1 , h ); glMultiTexCoord2i( GL_TEXTURE2, 1 , h ); glMultiTexCoord2i( GL_TEXTURE3, 0 , h -1 ); glMultiTexCoord2i( GL_TEXTURE4, 0 , h +1 ); glMultiTexCoord2i( GL_TEXTURE5, -1 , h -1 ); glMultiTexCoord2i( GL_TEXTURE6, -1 , h +1 ); glMultiTexCoord2i( GL_TEXTURE7, 1 , h -1 ); glVertex2i ( 0 , h ); glMultiTexCoord2i( GL_TEXTURE0, w , h ); glMultiTexCoord2i( GL_TEXTURE1, w-1 , h ); glMultiTexCoord2i( GL_TEXTURE2, w+1 , h ); glMultiTexCoord2i( GL_TEXTURE3, w , h -1 ); glMultiTexCoord2i( GL_TEXTURE4, w , h +1 ); glMultiTexCoord2i( GL_TEXTURE5, w-1 , h -1 ); glMultiTexCoord2i( GL_TEXTURE6, w-1 , h +1 ); glMultiTexCoord2i( GL_TEXTURE7, w+1 , h -1 ); glVertex2i ( w , h ); glMultiTexCoord2i( GL_TEXTURE0, w , 0 ); glMultiTexCoord2i( GL_TEXTURE1, w-1 , 0 ); glMultiTexCoord2i( GL_TEXTURE2, w+1 , 0 ); glMultiTexCoord2i( GL_TEXTURE3, w , -1 ); glMultiTexCoord2i( GL_TEXTURE4, w , 1 ); glMultiTexCoord2i( GL_TEXTURE5, w-1 , -1 ); glMultiTexCoord2i( GL_TEXTURE6, w-1 , 1 ); glMultiTexCoord2i( GL_TEXTURE7, w+1 , -1 ); glVertex2i ( w , 0 ); glEnd (); glFlush(); } void GLTexImage::DrawImage() { DrawQuad(); } void GLTexImage::FitTexViewPort() { GlobalUtil::FitViewPort(_drawWidth, _drawHeight); } void GLTexImage::FitRealTexViewPort() { GlobalUtil::FitViewPort(_texWidth, _texHeight); } void GLTexImage::AttachToFBO(int i) { glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, i+GL_COLOR_ATTACHMENT0_EXT, _texTarget, _texID, 0 ); } void GLTexImage::DetachFBO(int i) { glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, i+GL_COLOR_ATTACHMENT0_EXT, _texTarget, 0, 0 ); } void GLTexImage::DrawQuad(float x1, float x2, float y1, float y2) { glBegin (GL_QUADS); glTexCoord2f ( x1 , y1 ); glVertex2f ( x1 , y1 ); glTexCoord2f ( x1 , y2 ); glVertex2f ( x1 , y2 ); glTexCoord2f ( x2 , y2 ); glVertex2f ( x2 , y2 ); glTexCoord2f ( x2 , y1 ); glVertex2f ( x2 , y1 ); glEnd (); glFlush(); } void GLTexImage::TexConvertRGB() { //change 3/22/09 FrameBufferObject fbo; //GlobalUtil::FitViewPort(1, 1); FitTexViewPort(); AttachToFBO(0); ShaderMan::UseShaderRGB2Gray(); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); DrawQuad(); ShaderMan::UnloadProgram(); DetachFBO(0); } void GLTexImage::DrawQuadDS(int scale) { DrawScaledQuad(float(scale)); } void GLTexImage::DrawQuadUS(int scale) { DrawScaledQuad(1.0f/scale); } void GLTexImage::DrawScaledQuad(float texscale) { ////the texture coordinate for 0.5 is to + 0.5*texscale float to = 0.5f -0.5f * texscale; float tx = _imgWidth*texscale +to; float ty = _imgHeight*texscale +to; glBegin (GL_QUADS); glTexCoord2f ( to , to ); glVertex2i ( 0 , 0 ); glTexCoord2f ( to , ty ); glVertex2i ( 0 , _imgHeight ); glTexCoord2f ( tx , ty ); glVertex2i ( _imgWidth , _imgHeight ); glTexCoord2f ( tx , to ); glVertex2i ( _imgWidth , 0 ); glEnd (); glFlush(); } void GLTexImage::DrawQuadReduction(int w , int h) { float to = -0.5f; float tx = w*2 +to; float ty = h*2 +to; glBegin (GL_QUADS); glMultiTexCoord2f ( GL_TEXTURE0, to , to ); glMultiTexCoord2f ( GL_TEXTURE1, to +1, to ); glMultiTexCoord2f ( GL_TEXTURE2, to , to+1 ); glMultiTexCoord2f ( GL_TEXTURE3, to +1, to+1 ); glVertex2i ( 0 , 0 ); glMultiTexCoord2f ( GL_TEXTURE0, to , ty ); glMultiTexCoord2f ( GL_TEXTURE1, to +1, ty ); glMultiTexCoord2f ( GL_TEXTURE2, to , ty +1 ); glMultiTexCoord2f ( GL_TEXTURE3, to +1, ty +1 ); glVertex2i ( 0 , h ); glMultiTexCoord2f ( GL_TEXTURE0, tx , ty ); glMultiTexCoord2f ( GL_TEXTURE1, tx +1, ty ); glMultiTexCoord2f ( GL_TEXTURE2, tx , ty +1); glMultiTexCoord2f ( GL_TEXTURE3, tx +1, ty +1); glVertex2i ( w , h ); glMultiTexCoord2f ( GL_TEXTURE0, tx , to ); glMultiTexCoord2f ( GL_TEXTURE1, tx +1, to ); glMultiTexCoord2f ( GL_TEXTURE2, tx , to +1 ); glMultiTexCoord2f ( GL_TEXTURE3, tx +1, to +1 ); glVertex2i ( w , 0 ); glEnd (); glFlush(); } void GLTexImage::DrawQuadReduction() { float to = -0.5f; float tx = _drawWidth*2 +to; float ty = _drawHeight*2 +to; glBegin (GL_QUADS); glMultiTexCoord2f ( GL_TEXTURE0, to , to ); glMultiTexCoord2f ( GL_TEXTURE1, to +1, to ); glMultiTexCoord2f ( GL_TEXTURE2, to , to+1 ); glMultiTexCoord2f ( GL_TEXTURE3, to +1, to+1 ); glVertex2i ( 0 , 0 ); glMultiTexCoord2f ( GL_TEXTURE0, to , ty ); glMultiTexCoord2f ( GL_TEXTURE1, to +1, ty ); glMultiTexCoord2f ( GL_TEXTURE2, to , ty +1 ); glMultiTexCoord2f ( GL_TEXTURE3, to +1, ty +1 ); glVertex2i ( 0 , _drawHeight ); glMultiTexCoord2f ( GL_TEXTURE0, tx , ty ); glMultiTexCoord2f ( GL_TEXTURE1, tx +1, ty ); glMultiTexCoord2f ( GL_TEXTURE2, tx , ty +1); glMultiTexCoord2f ( GL_TEXTURE3, tx +1, ty +1); glVertex2i ( _drawWidth , _drawHeight ); glMultiTexCoord2f ( GL_TEXTURE0, tx , to ); glMultiTexCoord2f ( GL_TEXTURE1, tx +1, to ); glMultiTexCoord2f ( GL_TEXTURE2, tx , to +1 ); glMultiTexCoord2f ( GL_TEXTURE3, tx +1, to +1 ); glVertex2i ( _drawWidth , 0 ); glEnd (); glFlush(); } void GLTexPacked::TexConvertRGB() { //update the actual size of daw area _drawWidth = (1 + _imgWidth) >> 1; _drawHeight = (1 + _imgHeight) >> 1; /// FrameBufferObject fbo; GLuint oldTexID = _texID; glGenTextures(1, &_texID); glBindTexture(_texTarget, _texID); glTexImage2D(_texTarget, 0, _iTexFormat, _texWidth, _texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); //input glBindTexture(_texTarget, oldTexID); //output AttachToFBO(0); //program ShaderMan::UseShaderRGB2Gray(); //draw buffer glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); //run DrawQuadDS(2); ShaderMan::UnloadProgram(); glDeleteTextures(1, &oldTexID); DetachFBO(0); } void GLTexPacked::SetImageSize( int width, int height) { _imgWidth = width; _drawWidth = (width + 1) >> 1; _imgHeight = height; _drawHeight = (height + 1) >> 1; } void GLTexPacked::InitTexture( int width, int height, int clamp_to_edge) { if(_texID && width == _imgWidth && height == _imgHeight ) return; if(_texID==0) glGenTextures(1, &_texID); _imgWidth = width; _imgHeight = height; if(GlobalUtil::_PreciseBorder) { _texWidth = (width + 2) >> 1; _texHeight = (height + 2) >> 1; }else { _texWidth = (width + 1) >> 1; _texHeight = (height + 1) >> 1; } _drawWidth = (width + 1) >> 1; _drawHeight = (height + 1) >> 1; BindTex(); if(clamp_to_edge) { glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); }else { //out of bound tex read returns 0?? glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); } glTexParameteri(_texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(_texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glTexImage2D(_texTarget, 0, _iTexFormat, _texWidth, _texHeight, 0, GL_RGBA, GL_FLOAT, NULL); UnbindTex(); } void GLTexPacked::DrawImage() { float x1 =0, y1 = 0; //border.. float x2 = _imgWidth*0.5f +x1; float y2 = _imgHeight*0.5f + y1; glBegin (GL_QUADS); glTexCoord2f ( x1 , y1 ); glVertex2i ( 0 , 0 ); glTexCoord2f ( x1 , y2 ); glVertex2i ( 0 , _imgHeight ); glTexCoord2f ( x2 , y2 ); glVertex2i ( _imgWidth , _imgHeight ); glTexCoord2f ( x2 , y1 ); glVertex2i ( _imgWidth , 0 ); glEnd (); glFlush(); } void GLTexPacked::DrawQuadUS(int scale) { int tw =_drawWidth, th = _drawHeight; float texscale = 1.0f / scale; float x1 = 0.5f - 0.5f*scale, y1 = x1; float x2 = tw * texscale + x1; float y2 = th * texscale + y1; float step = texscale *0.5f; glBegin (GL_QUADS); glMultiTexCoord2f( GL_TEXTURE0, x1 , y1 ); glMultiTexCoord2f( GL_TEXTURE1, x1+step , y1 ); glMultiTexCoord2f( GL_TEXTURE2, x1 , y1 +step); glMultiTexCoord2f( GL_TEXTURE3, x1+step , y1 +step); glVertex2i ( 0 , 0 ); glMultiTexCoord2f( GL_TEXTURE0, x1 , y2 ); glMultiTexCoord2f( GL_TEXTURE1, x1+step , y2 ); glMultiTexCoord2f( GL_TEXTURE2, x1 , y2 +step); glMultiTexCoord2f( GL_TEXTURE3, x1+step , y2 +step); glVertex2i ( 0 , th ); glMultiTexCoord2f( GL_TEXTURE0, x2 , y2 ); glMultiTexCoord2f( GL_TEXTURE1, x2+step , y2 ); glMultiTexCoord2f( GL_TEXTURE2, x2 , y2 +step); glMultiTexCoord2f( GL_TEXTURE3, x2+step , y2 +step); glVertex2i ( tw , th ); glMultiTexCoord2f( GL_TEXTURE0, x2 , y1 ); glMultiTexCoord2f( GL_TEXTURE1, x2+step , y1 ); glMultiTexCoord2f( GL_TEXTURE2, x2 , y1 +step); glMultiTexCoord2f( GL_TEXTURE3, x2+step , y1 +step); glVertex2i ( tw , 0 ); glEnd (); } void GLTexPacked::DrawQuadDS(int scale) { int tw = _drawWidth; int th = _drawHeight; float x1 = 0.5f - 0.5f*scale; float x2 = tw * scale + x1; float y1 = 0.5f - 0.5f * scale; float y2 = th * scale + y1; int step = scale / 2; glBegin (GL_QUADS); glMultiTexCoord2f( GL_TEXTURE0, x1 , y1 ); glMultiTexCoord2f( GL_TEXTURE1, x1+step , y1 ); glMultiTexCoord2f( GL_TEXTURE2, x1 , y1 +step); glMultiTexCoord2f( GL_TEXTURE3, x1+step , y1 +step); glVertex2i ( 0 , 0 ); glMultiTexCoord2f( GL_TEXTURE0, x1 , y2 ); glMultiTexCoord2f( GL_TEXTURE1, x1+step , y2 ); glMultiTexCoord2f( GL_TEXTURE2, x1 , y2 +step); glMultiTexCoord2f( GL_TEXTURE3, x1+step , y2 +step); glVertex2i ( 0 , th ); glMultiTexCoord2f( GL_TEXTURE0, x2 , y2 ); glMultiTexCoord2f( GL_TEXTURE1, x2+step , y2 ); glMultiTexCoord2f( GL_TEXTURE2, x2 , y2 +step); glMultiTexCoord2f( GL_TEXTURE3, x2+step , y2 +step); glVertex2i ( tw , th ); glMultiTexCoord2f( GL_TEXTURE0, x2 , y1 ); glMultiTexCoord2f( GL_TEXTURE1, x2+step , y1 ); glMultiTexCoord2f( GL_TEXTURE2, x2 , y1 +step); glMultiTexCoord2f( GL_TEXTURE3, x2+step , y1 +step); glVertex2i ( tw , 0 ); glEnd (); } void GLTexPacked::ZeroHistoMargin() { int marginx = (((_imgWidth + 3) /4)*4) - _imgWidth; int marginy = (((-_imgHeight + 3)/4)*4) - _imgHeight; if(marginx >0 || marginy > 0) { int tw = (_imgWidth + marginx ) >> 1; int th = (_imgHeight + marginy ) >> 1; tw = min(_texWidth, tw ); th = min(_texHeight, th); GlobalUtil::FitViewPort(tw, th); AttachToFBO(0); BindTex(); ShaderMan::UseShaderZeroPass(); DrawMargin(tw, th, 1, 1); } } void GLTexPacked::FillMargin(int marginx, int marginy) { // marginx = min(marginx, _texWidth * 2 - _imgWidth); marginy = min(marginy, _texHeight * 2 - _imgHeight); if(marginx >0 || marginy > 0) { int tw = (_imgWidth + marginx + 1) >> 1; int th = (_imgHeight + marginy + 1) >> 1; GlobalUtil::FitViewPort(tw, th); BindTex(); AttachToFBO(0); ShaderMan::UseShaderMarginCopy(_imgWidth , _imgHeight); DrawMargin(tw, th, marginx, marginy); } } void GLTexPacked::DrawMargin(int right, int bottom, int mx, int my) { int tw = (_imgWidth >>1); int th = (_imgHeight >>1); glBegin(GL_QUADS); if(right>tw && mx) { glTexCoord2i ( tw , 0 ); glVertex2i ( tw , 0 ); glTexCoord2i ( tw , bottom ); glVertex2i ( tw , bottom ); glTexCoord2i ( right, bottom ); glVertex2i ( right, bottom ); glTexCoord2i ( right, 0 ); glVertex2i ( right, 0 ); } if(bottom>th && my) { glTexCoord2i ( 0 , th ); glVertex2i ( 0 , th ); glTexCoord2i ( 0 , bottom ); glVertex2i ( 0 , bottom ); glTexCoord2i ( tw , bottom ); glVertex2i ( tw , bottom ); glTexCoord2i ( tw , th ); glVertex2i ( tw , th ); } glEnd(); glFlush(); } void GLTexImage::UnbindMultiTex(int n) { for(int i = n-1; i>=0; i--) { glActiveTexture(GL_TEXTURE0+i); glBindTexture(_texTarget, 0); } } template int #if !defined(_MSC_VER) || _MSC_VER > 1200 GLTexInput:: #endif DownSamplePixelDataI(unsigned int gl_format, int width, int height, int ds, const Uint * pin, Uint * pout) { int step, linestep; int i, j; int ws = width/ds; int hs = height/ds; const Uint * line = pin, * p; Uint *po = pout; switch(gl_format) { case GL_LUMINANCE: case GL_LUMINANCE_ALPHA: step = ds * (gl_format == GL_LUMINANCE? 1: 2); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { *po++ = *p; } } break; case GL_RGB: case GL_RGBA: step = ds * (gl_format == GL_RGB? 3: 4); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { //*po++ = int(p[0]*0.299 + p[1] * 0.587 + p[2]* 0.114 + 0.5); *po++ = ((19595*p[0] + 38470*p[1] + 7471*p[2]+ 32768)>>16); } } break; case GL_BGR: case GL_BGRA: step = ds * (gl_format == GL_BGR? 3: 4); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { *po++ = ((7471*p[0] + 38470*p[1] + 19595*p[2]+ 32768)>>16); } } break; default: return 0; } return 1; } template int #if !defined(_MSC_VER) || _MSC_VER > 1200 GLTexInput:: #endif DownSamplePixelDataI2F(unsigned int gl_format, int width, int height, int ds, const Uint * pin, float * pout, int skip) { int step, linestep; int i, j; int ws = width/ds - skip; int hs = height/ds; const Uint * line = pin, * p; float *po = pout; const float factor = (sizeof(Uint) == 1? 255.0f : 65535.0f); switch(gl_format) { case GL_LUMINANCE: case GL_LUMINANCE_ALPHA: step = ds * (gl_format == GL_LUMINANCE? 1: 2); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { *po++ = (*p) / factor; } } break; case GL_RGB: case GL_RGBA: step = ds * (gl_format == GL_RGB? 3: 4); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { //*po++ = int(p[0]*0.299 + p[1] * 0.587 + p[2]* 0.114 + 0.5); *po++ = ((19595*p[0] + 38470*p[1] + 7471*p[2]) / (65535.0f * factor)); } } break; case GL_BGR: case GL_BGRA: step = ds * (gl_format == GL_BGR? 3: 4); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { *po++ = ((7471*p[0] + 38470*p[1] + 19595*p[2]) / (65535.0f * factor)); } } break; default: return 0; } return 1; } int GLTexInput::DownSamplePixelDataF(unsigned int gl_format, int width, int height, int ds, const float * pin, float * pout, int skip) { int step, linestep; int i, j; int ws = width/ds - skip; int hs = height/ds; const float * line = pin, * p; float *po = pout; switch(gl_format) { case GL_LUMINANCE: case GL_LUMINANCE_ALPHA: step = ds * (gl_format == GL_LUMINANCE? 1: 2); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { *po++ = *p; } } break; case GL_RGB: case GL_RGBA: step = ds * (gl_format == GL_RGB? 3: 4); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { *po++ = (0.299f*p[0] + 0.587f*p[1] + 0.114f*p[2]); } } break; case GL_BGR: case GL_BGRA: step = ds * (gl_format == GL_BGR? 3: 4); linestep = width * step; for(i = 0 ; i < hs; i++, line+=linestep) { for(j = 0, p = line; j < ws; j++, p+=step) { *po++ = (0.114f*p[0] + 0.587f*p[1] + 0.299f * p[2]); } } break; default: return 0; } return 1; } int GLTexInput::SetImageData( int width, int height, const void * data, unsigned int gl_format, unsigned int gl_type ) { int simple_format = IsSimpleGlFormat(gl_format, gl_type);//no cpu code to handle other formats int ws, hs, done = 1; if(_converted_data) {delete [] _converted_data; _converted_data = NULL; } _rgb_converted = 1; _data_modified = 0; if( simple_format && ( width > _texMaxDim || height > _texMaxDim || GlobalUtil::_PreProcessOnCPU) && GlobalUtil::_octave_min_default >0 ) { _down_sampled = GlobalUtil::_octave_min_default; ws = width >> GlobalUtil::_octave_min_default; hs = height >> GlobalUtil::_octave_min_default; }else { _down_sampled = 0; ws = width; hs = height; } if ( ws > _texMaxDim || hs > _texMaxDim) { if(simple_format) { if(GlobalUtil::_verbose) std::cout<<"Automatic down-sampling is used\n"; do { _down_sampled ++; ws >>= 1; hs >>= 1; }while(ws > _texMaxDim || hs > _texMaxDim); }else { std::cerr<<"Input images is too big to fit into a texture\n"; return 0; } } _texWidth = _imgWidth = _drawWidth = ws; _texHeight = _imgHeight = _drawHeight = hs; if(GlobalUtil::_verbose) { std::cout<<"Image size :\t"<0) std::cout<<"Down sample to \t"< 0 || gl_format != GL_LUMINANCE || gl_type != GL_FLOAT) { _converted_data = new float [_imgWidth * _imgHeight]; if(gl_type == GL_UNSIGNED_BYTE) DownSamplePixelDataI2F(gl_format, width, height, 1<<_down_sampled, ((const unsigned char*) data), _converted_data, skip); else if(gl_type == GL_UNSIGNED_SHORT) DownSamplePixelDataI2F(gl_format, width, height, 1<<_down_sampled, ((const unsigned short*) data), _converted_data, skip); else DownSamplePixelDataF(gl_format, width, height, 1<<_down_sampled, (float*)data, _converted_data, skip); _rgb_converted = 2; //indidates a new data copy _pixel_data = _converted_data; }else { //Luminance data that doesn't need to down sample _rgb_converted = 1; _pixel_data = data; if(skip > 0) { for(int i = 1; i < _imgHeight; ++i) { float * dst = ((float*)data) + i * tWidth, * src = ((float*)data) + i * _imgWidth; for(int j = 0; j < tWidth; ++j) *dst++ = * src++; } } } _texWidth = _imgWidth = _drawWidth = tWidth; _data_modified = 1; }else { if(_texID ==0) glGenTextures(1, &_texID); glBindTexture(_texTarget, _texID); CheckErrorsGL("glBindTexture"); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glPixelStorei(GL_UNPACK_ALIGNMENT , 1); if(simple_format && ( _down_sampled> 0 || (gl_format != GL_LUMINANCE && GlobalUtil::_PreProcessOnCPU) )) { if(gl_type == GL_UNSIGNED_BYTE) { unsigned char * newdata = new unsigned char [_imgWidth * _imgHeight]; DownSamplePixelDataI(gl_format, width, height, 1<<_down_sampled, ((const unsigned char*) data), newdata); glTexImage2D(_texTarget, 0, GL_LUMINANCE32F_ARB, //internal format changed _imgWidth, _imgHeight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, newdata); delete[] newdata; }else if(gl_type == GL_UNSIGNED_SHORT) { unsigned short * newdata = new unsigned short [_imgWidth * _imgHeight]; DownSamplePixelDataI(gl_format, width, height, 1<<_down_sampled, ((const unsigned short*) data), newdata); glTexImage2D(_texTarget, 0, GL_LUMINANCE32F_ARB, //internal format changed _imgWidth, _imgHeight, 0, GL_LUMINANCE, GL_UNSIGNED_SHORT, newdata); delete[] newdata; }else if(gl_type == GL_FLOAT) { float * newdata = new float [_imgWidth * _imgHeight]; DownSamplePixelDataF(gl_format, width, height, 1<<_down_sampled, (float*)data, newdata); glTexImage2D(_texTarget, 0, GL_LUMINANCE32F_ARB, //internal format changed _imgWidth, _imgHeight, 0, GL_LUMINANCE, GL_FLOAT, newdata); delete[] newdata; }else { //impossible done = 0; _rgb_converted = 0; } GlobalUtil::FitViewPort(1, 1); //this used to be necessary }else { //ds must be 0 here if not simpleformat if(gl_format == GL_LUMINANCE || gl_format == GL_LUMINANCE_ALPHA) { //use one channel internal format if data is intensity image glTexImage2D(_texTarget, 0, GL_LUMINANCE32F_ARB, _imgWidth, _imgHeight, 0, gl_format, gl_type, data); GlobalUtil::FitViewPort(1, 1); //this used to be necessary } else { //convert RGB 2 GRAY if needed glTexImage2D(_texTarget, 0, _iTexFormat, _imgWidth, _imgHeight, 0, gl_format, gl_type, data); if(ShaderMan::HaveShaderMan()) TexConvertRGB(); else _rgb_converted = 0; //In CUDA mode, the conversion will be done by CUDA kernel } } UnbindTex(); } return done; } GLTexInput::~GLTexInput() { if(_converted_data) delete [] _converted_data; } int GLTexInput::LoadImageFile(char *imagepath, int &w, int &h ) { #ifndef SIFTGPU_NO_DEVIL static int devil_loaded = 0; unsigned int imID; int done = 1; if(devil_loaded == 0) { ilInit(); ilOriginFunc(IL_ORIGIN_UPPER_LEFT); ilEnable(IL_ORIGIN_SET); devil_loaded = 1; } /// ilGenImages(1, &imID); ilBindImage(imID); if(ilLoadImage(imagepath)) { w = ilGetInteger(IL_IMAGE_WIDTH); h = ilGetInteger(IL_IMAGE_HEIGHT); int ilformat = ilGetInteger(IL_IMAGE_FORMAT); if(SetImageData(w, h, ilGetData(), ilformat, GL_UNSIGNED_BYTE)==0) { done =0; }else if(GlobalUtil::_verbose) { std::cout<<"Image loaded :\t"< 255 || width < 0 || height < 0) { fclose(file); std::cerr << "ERROR: fileformat not supported\n"; return 0; }else { w = width; h = height; } unsigned char * data = new unsigned char[width * height]; unsigned char * pixels = data; if (strcmp(buf, "P5")==0 ) { fscanf(file, "%c",buf);//skip one byte fread(pixels, 1, width*height, file); }else if (strcmp(buf, "P2")==0 ) { for (int i = 0 ; i< height; i++) { for ( int j = 0; j < width; j++) { fscanf(file, "%d", &g); *pixels++ = (unsigned char) g; } } }else if (strcmp(buf, "P6")==0 ) { fscanf(file, "%c", buf);//skip one byte int j, num = height*width; unsigned char buf[3]; for ( j =0 ; j< num; j++) { fread(buf,1,3, file); *pixels++=int(0.10454f* buf[2]+0.60581f* buf[1]+0.28965f* buf[0]); } }else if (strcmp(buf, "P3")==0 ) { int r, g, b; int i , num =height*width; for ( i = 0 ; i< num; i++) { fscanf(file, "%d %d %d", &r, &g, &b); *pixels++ = int(0.10454f* b+0.60581f* g+0.28965f* r); } }else { std::cerr << "ERROR: fileformat not supported\n"; done = 0; } if(done) SetImageData(width, height, data, GL_LUMINANCE, GL_UNSIGNED_BYTE); fclose(file); delete data; if(GlobalUtil::_verbose && done) std::cout<< "Image loaded :\t" << imagepath << "\n"; return 1; #endif } int GLTexImage::CopyToPBO(GLuint pbo, int width, int height, GLenum format) { ///////// if(format != GL_RGBA && format != GL_LUMINANCE) return 0; FrameBufferObject fbo; GLint bsize, esize = width * height * sizeof(float) * (format == GL_RGBA ? 4 : 1); AttachToFBO(0); glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, pbo); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); if(bsize < esize) { glBufferData(GL_PIXEL_PACK_BUFFER_ARB, esize, NULL, GL_STATIC_DRAW_ARB); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); } if(bsize >= esize) { glReadPixels(0, 0, width, height, format, GL_FLOAT, 0); } glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, 0); DetachFBO(0); return bsize >= esize; } void GLTexImage::SaveToASCII(const char* path) { vector buf(GetImgWidth() * GetImgHeight() * 4); FrameBufferObject fbo; AttachToFBO(0); glReadPixels(0, 0, GetImgWidth(), GetImgHeight(), GL_RGBA, GL_FLOAT, &buf[0]); ofstream out(path); for(int i = 0, idx = 0; i < GetImgHeight(); ++i) { for(int j = 0; j < GetImgWidth(); ++j, idx += 4) { out << i << " " << j << " " << buf[idx] << " " << buf[idx + 1] << " " << buf[idx + 2] << " " << buf[idx + 3] << "\n"; } } } void GLTexInput::VerifyTexture() { //for CUDA or OpenCL the texture is not generated by default if(!_data_modified) return; if(_pixel_data== NULL) return; InitTexture(_imgWidth, _imgHeight); BindTex(); glTexImage2D( _texTarget, 0, GL_LUMINANCE32F_ARB, //internal format changed _imgWidth, _imgHeight, 0, GL_LUMINANCE, GL_FLOAT, _pixel_data); UnbindTex(); _data_modified = 0; } void GLTexImage::CopyFromPBO(GLuint pbo, int width, int height, GLenum format) { InitTexture(max(width, _texWidth), max(height, _texHeight)); SetImageSize(width, height); if(width > 0 && height > 0) { BindTex(); glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, pbo); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, width, height, format, GL_FLOAT, 0); GlobalUtil::CheckErrorsGL("GLTexImage::CopyFromPBO->glTexSubImage2D"); glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, 0); UnbindTex(); } } colmap-3.9.1/src/thirdparty/SiftGPU/GLTexImage.h000066400000000000000000000130711454702036400213470ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: GLTexImage.h // Author: Changchang Wu // Description : interface for the GLTexImage class. // GLTexImage: naive texture class. // sevral different quad drawing functions are provied // GLTexPacked: packed version (four value packed as four channels of a pixel) // GLTexInput: GLTexImage + some input information // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef GL_TEX_IMAGE_H #define GL_TEX_IMAGE_H class GlobalUtil; class GLTexImage :public GlobalUtil { protected: GLuint _texID; int _imgWidth; int _imgHeight; int _texWidth; int _texHeight; int _drawWidth; int _drawHeight; public: static void DetachFBO(int i); static void UnbindTex(); static void UnbindMultiTex(int n); static void DrawQuad(float x1, float x2, float y1, float y2); public: virtual void DrawQuadUS(int scale); virtual void DrawQuadDS(int scale); virtual void DrawImage(); virtual void TexConvertRGB(); virtual void ZeroHistoMargin(); virtual void SetImageSize(int width, int height); virtual void InitTexture(int width, int height, int clamp_to_edge =1 ); void InitTexture(int width, int height, int clamp_to_edge, GLuint format); virtual void FillMargin(int marginx, int marginy); public: void DrawScaledQuad(float scale); int CopyToPBO(GLuint pbo, int width, int height, GLenum format = GL_RGBA); void CopyFromPBO(GLuint pbo, int width, int height, GLenum format = GL_RGBA); void FitRealTexViewPort(); void DrawQuadMT8(); void DrawQuadMT4(); void DrawQuadReduction(); void DrawQuadReduction(int w, int h); void DrawMargin(int right, int bottom); void DrawQuad(); void FitTexViewPort(); void ZeroHistoMargin(int hw, int hh); int CheckTexture(); void SaveToASCII(const char* path); public: void AttachToFBO(int i ); void BindTex(); operator GLuint (){return _texID;} GLuint GetTexID(){return _texID;} int GetImgPixelCount(){return _imgWidth*_imgHeight;} int GetTexPixelCount(){return _texWidth*_texHeight;} int GetImgWidth(){return _imgWidth;} int GetImgHeight(){return _imgHeight;} int GetTexWidth(){return _texWidth;} int GetTexHeight(){return _texHeight;} int GetDrawWidth(){return _drawWidth;} int GetDrawHeight(){return _drawHeight;} //int IsTexTight(){return _texWidth == _drawWidth && _texHeight == _drawHeight;} int IsTexPacked(){return _drawWidth != _imgWidth;} GLTexImage(); virtual ~GLTexImage(); friend class SiftGPU; }; //class for handle data input, to support all openGL-supported data format, //data are first uploaded to an openGL texture then converted, and optionally //when the datatype is simple, we downsample/convert on cpu class GLTexInput:public GLTexImage { public: int _down_sampled; int _rgb_converted; int _data_modified; ////////////////////////// float * _converted_data; const void* _pixel_data; public: static int IsSimpleGlFormat(unsigned int gl_format, unsigned int gl_type) { //the formats there is a cpu code to conver rgb and downsample return (gl_format ==GL_LUMINANCE ||gl_format == GL_LUMINANCE_ALPHA|| gl_format == GL_RGB|| gl_format == GL_RGBA|| gl_format == GL_BGR || gl_format == GL_BGRA) && (gl_type == GL_UNSIGNED_BYTE || gl_type == GL_FLOAT || gl_type == GL_UNSIGNED_SHORT); } //in vc6, template member function doesn't work #if !defined(_MSC_VER) || _MSC_VER > 1200 template static int DownSamplePixelDataI(unsigned int gl_format, int width, int height, int ds, const Uint * pin, Uint * pout); template static int DownSamplePixelDataI2F(unsigned int gl_format, int width, int height, int ds, const Uint * pin, float * pout, int skip = 0); #endif static int DownSamplePixelDataF(unsigned int gl_format, int width, int height, int ds, const float * pin, float * pout, int skip = 0); static int TruncateWidthCU(int w) {return w & 0xfffffffc; } public: GLTexInput() : _down_sampled(0), _rgb_converted(0), _data_modified(0), _converted_data(0), _pixel_data(0){} int SetImageData(int width, int height, const void * data, unsigned int gl_format, unsigned int gl_type); int LoadImageFile(char * imagepath, int & w, int &h); void VerifyTexture(); virtual ~GLTexInput(); }; //GLTexPacked doesn't have any data //so that we can use the GLTexImage* pointer to index a GLTexPacked Vector class GLTexPacked:public GLTexImage { public: virtual void DrawImage(); virtual void DrawQuadUS(int scale); virtual void DrawQuadDS(int scale); virtual void FillMargin(int marginx, int marginy); virtual void InitTexture(int width, int height, int clamp_to_edge =1); virtual void TexConvertRGB(); virtual void SetImageSize(int width, int height); virtual void ZeroHistoMargin(); //virtual void GetHistWH(int& w, int& h){return w = (3 + sz)>>1;} public: void DrawMargin(int right, int bottom, int mx, int my); GLTexPacked():GLTexImage(){} }; #endif // !defined(GL_TEX_IMAGE_H) colmap-3.9.1/src/thirdparty/SiftGPU/GlobalUtil.cpp000066400000000000000000000352711454702036400220200ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: GlobalUtil.cpp // Author: Changchang Wu // Description : Global Utility class for SiftGPU // // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include #include using std::cout; #include "GL/glew.h" #include "GlobalUtil.h" #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include #else #include #endif #include "LiteWindow.h" // int GlobalParam:: _verbose = 1; int GlobalParam:: _timingS = 1; //print out information of each step int GlobalParam:: _timingO = 0; //print out information of each octave int GlobalParam:: _timingL = 0; //print out information of each level GLuint GlobalParam:: _texTarget = GL_TEXTURE_RECTANGLE_ARB; //only this one is supported GLuint GlobalParam:: _iTexFormat =GL_RGBA32F_ARB; //or GL_RGBA16F_ARB int GlobalParam:: _debug = 0; //enable debug code? int GlobalParam:: _usePackedTex = 1;//packed implementation int GlobalParam:: _UseCUDA = 0; int GlobalParam:: _UseOpenCL = 0; int GlobalParam:: _MaxFilterWidth = -1; //maximum filter width, use when GPU is not good enough float GlobalParam:: _FilterWidthFactor = 4.0f; //the filter size will be _FilterWidthFactor*sigma*2+1 float GlobalParam:: _DescriptorWindowFactor = 3.0f; //descriptor sampling window factor int GlobalParam:: _SubpixelLocalization = 1; //sub-pixel and sub-scale localization int GlobalParam:: _MaxOrientation = 2; //whether we find multiple orientations for each feature int GlobalParam:: _OrientationPack2 = 0; //use one float to store two orientations float GlobalParam:: _MaxFeaturePercent = 0.005f;//at most 0.005 of all pixels int GlobalParam:: _MaxLevelFeatureNum = 4096; //maximum number of features of a level int GlobalParam:: _FeatureTexBlock = 4; //feature texture storagte alignment int GlobalParam:: _NarrowFeatureTex = 0; //if _ForceTightPyramid is not 0, pyramid will be reallocated to fit the size of input images. //otherwise, pyramid can be reused for smaller input images. int GlobalParam:: _ForceTightPyramid = 0; //use gpu or cpu to generate feature list ...gpu is a little bit faster int GlobalParam:: _ListGenGPU = 1; int GlobalParam:: _ListGenSkipGPU = 6; //how many levels are skipped on gpu int GlobalParam:: _PreProcessOnCPU = 1; //convert rgb 2 intensity on gpu, down sample on GPU //hardware parameter, automatically retrieved int GlobalParam:: _texMaxDim = 3200; //Maximum working size for SiftGPU, 3200 for packed int GlobalParam:: _texMaxDimGL = 4096; //GPU texture limit int GlobalParam:: _texMinDim = 16; // int GlobalParam:: _MemCapGPU = 0; int GlobalParam:: _FitMemoryCap = 0; int GlobalParam:: _IsNvidia = 0; //GPU vendor int GlobalParam:: _KeepShaderLoop = 0; //you can't change the following 2 values //all other versions of code are now dropped int GlobalParam:: _DescriptorPPR = 8; int GlobalParam:: _DescriptorPPT = 16; //whether orientation/descriptor is supported by hardware int GlobalParam:: _SupportNVFloat = 0; int GlobalParam:: _SupportTextureRG = 0; int GlobalParam:: _UseDynamicIndexing = 0; int GlobalParam:: _FullSupported = 1; //when SiftGPUEX is not used, display VBO generation is skipped int GlobalParam:: _UseSiftGPUEX = 0; int GlobalParam:: _InitPyramidWidth=0; int GlobalParam:: _InitPyramidHeight=0; int GlobalParam:: _octave_min_default=0; int GlobalParam:: _octave_num_default=-1; ////////////////////////////////////////////////////////////////// int GlobalParam:: _GoodOpenGL = -1; //indicates OpenGl initialization status int GlobalParam:: _FixedOrientation = 0; //upright int GlobalParam:: _LoweOrigin = 0; //(0, 0) to be at the top-left corner. int GlobalParam:: _NormalizedSIFT = 1; //normalize descriptor int GlobalParam:: _BinarySIFT = 0; //saving binary format int GlobalParam:: _ExitAfterSIFT = 0; //exif after saving result int GlobalParam:: _KeepExtremumSign = 0; // if 1, scales of dog-minimum will be multiplied by -1 /// int GlobalParam:: _KeyPointListForceLevel0 = 0; int GlobalParam:: _DarknessAdaption = 0; int GlobalParam:: _ProcessOBO = 0; int GlobalParam:: _TruncateMethod = 0; int GlobalParam:: _PreciseBorder = 1; // parameter changing for better matching with Lowe's SIFT float GlobalParam:: _OrientationWindowFactor = 2.0f; // 1.0(-v292), 2(v293-), float GlobalParam:: _OrientationGaussianFactor = 1.5f; // 4.5(-v292), 1.5(v293-) float GlobalParam:: _MulitiOrientationThreshold = 0.8f; /// int GlobalParam:: _FeatureCountThreshold = -1; /////////////////////////////////////////////// int GlobalParam:: _WindowInitX = -1; int GlobalParam:: _WindowInitY = -1; int GlobalParam:: _DeviceIndex = 0; const char * GlobalParam:: _WindowDisplay = NULL; ///////////////// //// ClockTimer GlobalUtil:: _globalTimer; #ifdef _DEBUG void GlobalUtil::CheckErrorsGL(const char* location) { GLuint errnum; const char *errstr; while (errnum = glGetError()) { errstr = (const char *)(gluErrorString(errnum)); if(errstr) { std::cerr << errstr; } else { std::cerr << "Error " << errnum; } if(location) std::cerr << " at " << location; std::cerr << "\n"; } return; } #endif void GlobalUtil::CleanupOpenGL() { glActiveTexture(GL_TEXTURE0); } void GlobalUtil::SetDeviceParam(int argc, char** argv) { if(GlobalParam::_GoodOpenGL!= -1) return; #define CHAR1_TO_INT(x) ((x >= 'A' && x <= 'Z') ? x + 32 : x) #define CHAR2_TO_INT(str, i) (str[i] ? CHAR1_TO_INT(str[i]) + (CHAR1_TO_INT(str[i+1]) << 8) : 0) #define CHAR3_TO_INT(str, i) (str[i] ? CHAR1_TO_INT(str[i]) + (CHAR2_TO_INT(str, i + 1) << 8) : 0) #define STRING_TO_INT(str) (CHAR1_TO_INT(str[0]) + (CHAR3_TO_INT(str, 1) << 8)) char* arg, * opt; for(int i = 0; i< argc; i++) { arg = argv[i]; if(arg == NULL || arg[0] != '-')continue; opt = arg+1; //////////////////////////////// switch( STRING_TO_INT(opt)) { case 'w' + ('i' << 8) + ('n' << 16) + ('p' << 24): if(_GoodOpenGL != 2 && i + 1 < argc) { int x =0, y=0; if(sscanf(argv[++i], "%dx%d", &x, &y) == 2) { GlobalParam::_WindowInitX = x; GlobalParam::_WindowInitY = y; } } break; case 'd' + ('i' << 8) + ('s' << 16) + ('p' << 24): if(_GoodOpenGL != 2 && i + 1 < argc) { GlobalParam::_WindowDisplay = argv[++i]; } break; case 'c' + ('u' << 8) + ('d' << 16) + ('a' << 24): if(i + 1 < argc) { int device = 0; scanf(argv[++i], "%d", &device) ; GlobalParam::_DeviceIndex = device; } break; default: break; } } } void GlobalUtil::SetTextureParameter() { glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(_texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(_texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); } //if image need to be up sampled ..use this one void GlobalUtil::SetTextureParameterUS() { glTexParameteri (_texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (_texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(_texTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(_texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); } void GlobalUtil::FitViewPort(int width, int height) { GLint port[4]; glGetIntegerv(GL_VIEWPORT, port); if(port[2] !=width || port[3] !=height) { glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, width, 0, height, 0, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } } bool GlobalUtil::CheckFramebufferStatus() { GLenum status; status=(GLenum)glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); switch(status) { case GL_FRAMEBUFFER_COMPLETE_EXT: return true; case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT: std::cerr<<("Framebuffer incomplete,incomplete attachment\n"); return false; case GL_FRAMEBUFFER_UNSUPPORTED_EXT: std::cerr<<("Unsupported framebuffer format\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT: std::cerr<<("Framebuffer incomplete,missing attachment\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT: std::cerr<<("Framebuffer incomplete,attached images must have same dimensions\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT: std::cerr<<("Framebuffer incomplete,attached images must have same format\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT: std::cerr<<("Framebuffer incomplete,missing draw buffer\n"); return false; case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT: std::cerr<<("Framebuffer incomplete,missing read buffer\n"); return false; } return false; } int ClockTimer::ClockMS() { return 0; } double ClockTimer::CLOCK() { return 0; } void ClockTimer::InitHighResolution() { } void ClockTimer::StartTimer(const char* event, int verb) { } void ClockTimer::StopTimer(int verb) { } float ClockTimer::GetElapsedTime() { return 0; } void GlobalUtil::SetGLParam() { if(GlobalUtil::_UseCUDA) return; else if(GlobalUtil::_UseOpenCL) return; glEnable(GlobalUtil::_texTarget); glActiveTexture(GL_TEXTURE0); } void GlobalUtil::InitGLParam(int NotTargetGL) { //IF the OpenGL context passed the check if(GlobalUtil::_GoodOpenGL == 2) return; //IF the OpenGl context failed the check if(GlobalUtil::_GoodOpenGL == 0) return; //IF se use CUDA or OpenCL if(NotTargetGL && !GlobalUtil::_UseSiftGPUEX) { GlobalUtil::_GoodOpenGL = 1; }else { //first time in this function glewInit(); GlobalUtil::_GoodOpenGL = 2; const char * vendor = (const char * )glGetString(GL_VENDOR); if(vendor) { GlobalUtil::_IsNvidia = (strstr(vendor, "NVIDIA") !=NULL ? 1 : 0); // Let nVidia compiler to take care of the unrolling. if (GlobalUtil::_IsNvidia) GlobalUtil::_KeepShaderLoop = 1; #ifndef WIN32 else if(!strstr(vendor, "ATI") ) { // For non-nVidia non-ATI cards...simply assume it is Mesa // Keep the original shader loop, because some of the unrolled // loopes are too large, and it may take too much time to compile GlobalUtil::_KeepShaderLoop = 1; } #endif if(GlobalUtil::_IsNvidia && glewGetExtension("GL_NVX_gpu_memory_info")) { glGetIntegerv(0x9049/*GL_GPU_MEM_INFO_CURRENT_AVAILABLE_MEM_NVX*/, &_MemCapGPU); _MemCapGPU /= (1024); if(GlobalUtil::_verbose) std::cout << "[GPU VENDOR]:\t" << vendor << ' ' <<_MemCapGPU << "MB\n"; }else if(strstr(vendor, "ATI") && glewGetExtension("GL_ATI_meminfo")) { int info[4]; glGetIntegerv(0x87FC/*GL_TEXTURE_FREE_MEMORY_ATI*/, info); _MemCapGPU = info[0] / (1024); if(GlobalUtil::_verbose) std::cout << "[GPU VENDOR]:\t" << vendor << ' ' <<_MemCapGPU << "MB\n"; }else { if(GlobalUtil::_verbose) std::cout << "[GPU VENDOR]:\t" << vendor << "\n"; } } if(GlobalUtil::_IsNvidia == 0 )GlobalUtil::_UseCUDA = 0; if (glewGetExtension("GL_ARB_fragment_shader") != GL_TRUE || glewGetExtension("GL_ARB_shader_objects") != GL_TRUE || glewGetExtension("GL_ARB_shading_language_100") != GL_TRUE) { std::cerr << "Shader not supported by your hardware!\n"; GlobalUtil::_GoodOpenGL = 0; } if (glewGetExtension("GL_EXT_framebuffer_object") != GL_TRUE) { std::cerr << "Framebuffer object not supported!\n"; GlobalUtil::_GoodOpenGL = 0; } if(glewGetExtension("GL_ARB_texture_rectangle")==GL_TRUE) { GLint value; GlobalUtil::_texTarget = GL_TEXTURE_RECTANGLE_ARB; glGetIntegerv(GL_MAX_RECTANGLE_TEXTURE_SIZE_EXT, &value); GlobalUtil::_texMaxDimGL = value; if(GlobalUtil::_verbose) std::cout << "TEXTURE:\t" << GlobalUtil::_texMaxDimGL << "\n"; if(GlobalUtil::_texMaxDim == 0 || GlobalUtil::_texMaxDim > GlobalUtil::_texMaxDimGL) { GlobalUtil::_texMaxDim = GlobalUtil::_texMaxDimGL; } glEnable(GlobalUtil::_texTarget); }else { std::cerr << "GL_ARB_texture_rectangle not supported!\n"; GlobalUtil::_GoodOpenGL = 0; } GlobalUtil::_SupportNVFloat = glewGetExtension("GL_NV_float_buffer"); GlobalUtil::_SupportTextureRG = glewGetExtension("GL_ARB_texture_rg"); glShadeModel(GL_FLAT); glPolygonMode(GL_FRONT, GL_FILL); GlobalUtil::SetTextureParameter(); } } void GlobalUtil::SelectDisplay() { #ifdef WIN32 if(_WindowDisplay == NULL) return; HDC hdc = CreateDC(_WindowDisplay, _WindowDisplay, NULL, NULL); _WindowDisplay = NULL; if(hdc == NULL) { std::cout << "ERROR: invalid dispaly specified\n"; return; } PIXELFORMATDESCRIPTOR pfd = { sizeof(PIXELFORMATDESCRIPTOR),1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER, PFD_TYPE_RGBA,24,0, 0, 0, 0, 0, 0,0,0,0,0, 0, 0, 0,16,0,0, PFD_MAIN_PLANE,0,0, 0, 0 }; ChoosePixelFormat(hdc, &pfd); #endif } int GlobalUtil::CreateWindowEZ(LiteWindow* window) { if(window == NULL) return 0; if(!window->IsValid())window->Create(_WindowInitX, _WindowInitY, _WindowDisplay); if(window->IsValid()) { window->MakeCurrent(); return 1; } else { std::cerr << "Unable to create OpenGL Context!\n"; std::cerr << "For nVidia cards, you can try change to CUDA mode in this case\n"; return 0; } } int GlobalUtil::CreateWindowEZ() { static LiteWindow window; return CreateWindowEZ(&window); } int CreateLiteWindow(LiteWindow* window) { return GlobalUtil::CreateWindowEZ(window); } colmap-3.9.1/src/thirdparty/SiftGPU/GlobalUtil.h000066400000000000000000000113021454702036400214520ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: GlobalUtil.h // Author: Changchang Wu // Description : // GlobalParam: Global parameters // ClockTimer: Timer // GlobalUtil: Global Function wrapper // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _GLOBAL_UTILITY_H #define _GLOBAL_UTILITY_H //wrapper for some shader function //class ProgramGPU; class LiteWindow; class GlobalParam { public: static GLuint _texTarget; static GLuint _iTexFormat; static int _texMaxDim; static int _texMaxDimGL; static int _texMinDim; static int _MemCapGPU; static int _FitMemoryCap; static int _verbose; static int _timingS; static int _timingO; static int _timingL; static int _usePackedTex; static int _IsNvidia; static int _KeepShaderLoop; static int _UseCUDA; static int _UseOpenCL; static int _UseDynamicIndexing; static int _debug; static int _MaxFilterWidth; static float _FilterWidthFactor; static float _OrientationWindowFactor; static float _DescriptorWindowFactor; static int _MaxOrientation; static int _OrientationPack2; static int _ListGenGPU; static int _ListGenSkipGPU; static int _SupportNVFloat; static int _SupportTextureRG; static int _FullSupported; static float _MaxFeaturePercent; static int _MaxLevelFeatureNum; static int _DescriptorPPR; static int _DescriptorPPT; //pixel per texture for one descriptor static int _FeatureTexBlock; static int _NarrowFeatureTex; //implemented but no performance improvement static int _SubpixelLocalization; static int _ProcessOBO; //not implemented yet static int _TruncateMethod; static int _PreciseBorder; //implemented static int _UseSiftGPUEX; static int _ForceTightPyramid; static int _octave_min_default; static int _octave_num_default; static int _InitPyramidWidth; static int _InitPyramidHeight; static int _PreProcessOnCPU; static int _GoodOpenGL; static int _FixedOrientation; static int _LoweOrigin; static int _ExitAfterSIFT; static int _NormalizedSIFT; static int _BinarySIFT; static int _KeepExtremumSign; static int _FeatureCountThreshold; static int _KeyPointListForceLevel0; static int _DarknessAdaption; //for compatability with old version: static float _OrientationExtraFactor; static float _OrientationGaussianFactor; static float _MulitiOrientationThreshold; //////////////////////////////////////// static int _WindowInitX; static int _WindowInitY; static const char* _WindowDisplay; static int _DeviceIndex; }; class ClockTimer { private: char _current_event[256]; int _time_start; int _time_stop; public: static int ClockMS(); static double CLOCK(); static void InitHighResolution(); void StopTimer(int verb = 1); void StartTimer(const char * event, int verb=0); float GetElapsedTime(); }; class GlobalUtil:public GlobalParam { static ClockTimer _globalTimer; public: inline static double CLOCK() { return ClockTimer::CLOCK(); } inline static void StopTimer() { _globalTimer.StopTimer(_timingS); } inline static void StartTimer(const char * event) { _globalTimer.StartTimer(event, _timingO); } inline static float GetElapsedTime() { return _globalTimer.GetElapsedTime(); } static void FitViewPort(int width, int height); static void SetTextureParameter(); static void SetTextureParameterUS(); #ifdef _DEBUG static void CheckErrorsGL(const char* location = NULL); #else static void inline CheckErrorsGL(const char* location = NULL){}; #endif static bool CheckFramebufferStatus(); //initialize Opengl parameters static void SelectDisplay(); static void InitGLParam(int NotTargetGL = 0); static void SetGLParam(); static int CreateWindowEZ(); static void CleanupOpenGL(); static void SetDeviceParam(int argc, char** argv); static int CreateWindowEZ(LiteWindow* window); }; #if defined(_MSC_VER) && _MSC_VER == 1200 #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) #endif #endif colmap-3.9.1/src/thirdparty/SiftGPU/LICENSE000077500000000000000000000014541454702036400202620ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// colmap-3.9.1/src/thirdparty/SiftGPU/LiteWindow.h000066400000000000000000000003741454702036400215100ustar00rootroot00000000000000#ifndef LITE_WINDOW_H #define LITE_WINDOW_H class LiteWindow { public: LiteWindow() {} virtual ~LiteWindow() {} int IsValid() { return 0; } void MakeCurrent() {} void Create(int x = -1, int y = -1, const char* display = NULL) {} }; #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramCG.cpp000066400000000000000000002712601454702036400216030ustar00rootroot00000000000000////////////////////////////////////////////////////////////////////////////// // File: ProgramCG.cpp // Author: Changchang Wu // Description : implementation of cg related class. // class ProgramCG A simple wrapper of Cg programs // class ShaderBagCG cg shaders for SIFT // class FilterCGGL cg gaussian filters for SIFT // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CG_SIFTGPU_ENABLED) #include "GL/glew.h" #include #include #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "ProgramCG.h" #include "GLTexImage.h" #include "ShaderMan.h" #include "FrameBufferObject.h" #if defined(_WIN32) #pragma comment (lib, "../../lib/cg.lib") #pragma comment (lib, "../../lib/cggl.lib") #endif CGcontext ProgramCG::_Context =0; CGprofile ProgramCG::_FProfile; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// ProgramCG::ProgramCG() { _programID = NULL; } ProgramCG::~ProgramCG() { if(_programID) cgDestroyProgram(_programID); } ProgramCG::ProgramCG(const char *code, const char** cg_compile_args, CGprofile profile) { _valid = 0; _profile = profile; GLint epos; const char* ati_args[] = {"-po", "ATI_draw_buffers",0}; const char* fp40_args[] = {"-ifcvt", "none","-unroll", "all", GlobalUtil::_UseFastMath? "-fastmath" : 0, 0}; if(cg_compile_args == NULL) cg_compile_args = GlobalUtil::_IsNvidia? (GlobalUtil::_SupportFP40? fp40_args:NULL) : ati_args; _programID = ::cgCreateProgram(_Context, CG_SOURCE, code, profile, NULL, cg_compile_args); if(_programID) { cgGLLoadProgram(_programID ); //_texParamID = cgGetNamedParameter(_programID, "tex"); glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB, &epos); if(epos >=0) { std::cout<=0) { std::cout< 0.9)? size : -size);\n" " dxy.y = type < 0.2 ? 0 : ((type < 0.3 || type > 0.7 )? -size :size); \n" " sincos(cc.b, s, c);\n" " FragColor.x = cc.x + c*dxy.x-s*dxy.y;\n" " FragColor.y = cc.y + c*dxy.y+s*dxy.x;}\n" "}\n\0"); /*FragColor = float4(tpos, 0.0, 1.0);}\n\0");*/ _param_genvbo_size = cgGetNamedParameter(*program, "sizes"); s_display_gaussian = new ProgramCG( "void main(float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float r = texRECT(tex, TexCoord0.xy).r;\n" "FragColor = float4(r, r, r, 1.0);}"); s_display_dog = new ProgramCG( "void main(float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float g = (0.5+20.0*texRECT(tex, TexCoord0.xy).g);\n" "FragColor = float4(g, g, g, 1.0);}" ); s_display_grad = new ProgramCG( "void main(float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float4 cc = texRECT(tex, TexCoord0.xy); FragColor = float4(5.0 * cc.bbb, 1.0); }"); s_display_keys= new ProgramCG( "void main(float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float4 cc = texRECT(tex, TexCoord0.xy);\n" "if(cc.r ==1.0) FragColor = float4(1.0, 0, 0,1.0); \n" "else {if (cc.r ==0.5) FragColor = float4(0.0,1.0,0.0,1.0); else discard;}}"); } void ShaderBagCG::SetMarginCopyParam(int xmax, int ymax) { float truncate[2] = {xmax - 0.5f , ymax - 0.5f}; cgGLSetParameter2fv(_param_margin_copy_truncate, truncate); } int ShaderBagCG::LoadKeypointShaderMR(float threshold, float edge_threshold) { char buffer[10240]; float threshold0 = threshold * 0.8f; float threshold1 = threshold; float threshold2 = (edge_threshold+1)*(edge_threshold+1)/edge_threshold; int max_refine = max(2, GlobalUtil::_SubpixelLocalization); ostrstream out(buffer, 10240); out << "#define THRESHOLD0 " << threshold0 << "\n" "#define THRESHOLD1 " << threshold1 << "\n" "#define THRESHOLD2 " << threshold2 << "\n" "#define MAX_REFINE " << max_refine << "\n"; out<< "void main (\n" "float4 TexCC : TEXCOORD0, float4 TexLC : TEXCOORD1,\n" "float4 TexRC : TEXCOORD2, float4 TexCD : TEXCOORD3, \n" "float4 TexCU : TEXCOORD4, float4 TexLD : TEXCOORD5, \n" "float4 TexLU : TEXCOORD6, float4 TexRD : TEXCOORD7,\n" "out float4 FragData0 : COLOR0, out float4 FragData1 : COLOR1, \n" "uniform samplerRECT tex, uniform samplerRECT texU, uniform samplerRECT texD)\n" "{\n" " float4 v1, v2, gg;\n" " float2 TexRU = float2(TexRC.x, TexCU.y); \n" " float4 cc = texRECT(tex, TexCC.xy);\n" " v1.x = texRECT(tex, TexLC.xy).g;\n" " gg.x = texRECT(tex, TexLC.xy).r;\n" " v1.y = texRECT(tex, TexRC.xy).g;\n" " gg.y = texRECT(tex, TexRC.xy).r;\n" " v1.z = texRECT(tex, TexCD.xy).g;\n" " gg.z = texRECT(tex, TexCD.xy).r;\n" " v1.w = texRECT(tex, TexCU.xy).g;\n" " gg.w = texRECT(tex, TexCU.xy).r;\n" " v2.x = texRECT(tex, TexLD.xy).g;\n" " v2.y = texRECT(tex, TexLU.xy).g;\n" " v2.z = texRECT(tex, TexRD.xy).g;\n" " v2.w = texRECT(tex, TexRU.xy).g;\n" " float2 dxdy = 0.5*(gg.yw - gg.xz); \n" " float grad = length(dxdy);\n" " float theta = grad==0? 0: atan2(dxdy.y, dxdy.x);\n" " FragData0 = float4(cc.rg, grad, theta);\n" << " float dog = 0.0; \n" " FragData1 = float4(0, 0, 0, 0); \n" " float2 v3; float4 v4, v5, v6;\n" << " if( cc.g > THRESHOLD0 && all(cc.gggg > max(v1, v2)))\n" " {\n" " v3.x = texRECT(texU, TexCC.xy).g;\n" " v4.x = texRECT(texU, TexLC.xy).g;\n" " v4.y = texRECT(texU, TexRC.xy).g;\n" " v4.z = texRECT(texU, TexCD.xy).g;\n" " v4.w = texRECT(texU, TexCU.xy).g;\n" " v6.x = texRECT(texU, TexLD.xy).g;\n" " v6.y = texRECT(texU, TexLU.xy).g;\n" " v6.z = texRECT(texU, TexRD.xy).g;\n" " v6.w = texRECT(texU, TexRU.xy).g;\n" " if(cc.g < v3.x || any(cc.gggg v3.x || any(cc.gggg>v4.xyzw || cc.gggg>v6.xyzw))return; \n" " v3.y = texRECT(texD, TexCC.xy).g;\n" " v5.x = texRECT(texD, TexLC.xy).g;\n" " v5.y = texRECT(texD, TexRC.xy).g;\n" " v5.z = texRECT(texD, TexCD.xy).g;\n" " v5.w = texRECT(texD, TexCU.xy).g;\n" " v6.x = texRECT(texD, TexLD.xy).g;\n" " v6.y = texRECT(texD, TexLU.xy).g;\n" " v6.z = texRECT(texD, TexRD.xy).g;\n" " v6.w = texRECT(texD, TexRU.xy).g;\n" " if(cc.g > v3.y || any(cc.gggg>v5.xyzw || cc.gggg>v6.xyzw))return; \n" " dog = 0.5 ; \n" " }\n" " else\n" " return;\n" << " int i = 0; \n" " float2 offset = float2(0, 0);\n" " float2 offsets = float2(0, 0);\n" " float3 dxys; bool key_moved; \n" " float fx, fy, fs; \n" " float fxx, fyy, fxy; \n" " float fxs, fys, fss; \n" " do\n" " {\n" " dxys = float3(0, 0, 0);\n" " offset = float2(0, 0);\n" " float4 D2 = v1.xyzw - cc.gggg;\n" " fxx = D2.x + D2.y;\n" " fyy = D2.z + D2.w;\n" " float2 D4 = v2.xw - v2.yz;\n" " fxy = 0.25*(D4.x + D4.y);\n" " float2 D5 = 0.5*(v1.yw-v1.xz); \n" " fx = D5.x;\n" " fy = D5.y ; \n" " fs = 0.5*( v3.x - v3.y ); \n" " fss = v3.x + v3.y - cc.g - cc.g;\n" " fxs = 0.25 * ( v4.y + v5.x - v4.x - v5.y);\n" " fys = 0.25 * ( v4.w + v5.z - v4.z - v5.w);\n" " float4 A0, A1, A2 ; \n" " A0 = float4(fxx, fxy, fxs, -fx); \n" " A1 = float4(fxy, fyy, fys, -fy); \n" " A2 = float4(fxs, fys, fss, -fs); \n" " float3 x3 = abs(float3(fxx, fxy, fxs)); \n" " float maxa = max(max(x3.x, x3.y), x3.z); \n" " if(maxa > 1e-10 ) \n" " {\n" " if(x3.y ==maxa ) \n" " { \n" " float4 TEMP = A1; A1 = A0; A0 = TEMP; \n" " }else if( x3.z == maxa ) \n" " { \n" " float4 TEMP = A2; A2 = A0; A0 = TEMP; \n" " } \n" " A0 /= A0.x; \n" " A1 -= A1.x * A0; \n" " A2 -= A2.x * A0; \n" " float2 x2 = abs(float2(A1.y, A2.y)); \n" " if( x2.y > x2.x ) \n" " { \n" " float3 TEMP = A2.yzw; \n" " A2.yzw = A1.yzw; \n" " A1.yzw = TEMP; \n" " x2.x = x2.y; \n" " } \n" " if(x2.x > 1e-10) \n" " {\n" " A1.yzw /= A1.y; \n" " A2.yzw -= A2.y * A1.yzw; \n" " if(abs(A2.z) > 1e-10) \n" " {\n" // compute dx, dy, ds: << " dxys.z = A2.w /A2.z; \n" " dxys.y = A1.w - dxys.z*A1.z; \n" " dxys.x = A0.w - dxys.z*A0.z - dxys.y*A0.y; \n" " }\n" " }\n" " }\n" " offset.x = dxys.x > 0.6 ? 1 : 0 + dxys.x < -0.6 ? -1 : 0;\n" " offset.y = dxys.y > 0.6 ? 1 : 0 + dxys.y < - 0.6? -1 : 0;\n" " i++; key_moved = i < MAX_REFINE && any(abs(offset)>0) ; \n" " if(key_moved)\n" " {\n" " offsets += offset; \n" " cc = texRECT(tex, TexCC.xy + offsets);\n" " v1.x = texRECT(tex , TexLC.xy + offsets).g;\n" " v1.y = texRECT(tex , TexRC.xy + offsets).g;\n" " v1.z = texRECT(tex , TexCD.xy + offsets).g;\n" " v1.w = texRECT(tex , TexCU.xy + offsets).g;\n" " v2.x = texRECT(tex , TexLD.xy + offsets).g;\n" " v2.y = texRECT(tex , TexLU.xy + offsets).g;\n" " v2.z = texRECT(tex , TexRD.xy + offsets).g;\n" " v2.w = texRECT(tex , TexRU.xy + offsets).g;\n" " v3.x = texRECT(texU, TexCC.xy + offsets).g;\n" " v4.x = texRECT(texU, TexLC.xy + offsets).g;\n" " v4.y = texRECT(texU, TexRC.xy + offsets).g;\n" " v4.z = texRECT(texU, TexCD.xy + offsets).g;\n" " v4.w = texRECT(texU, TexCU.xy + offsets).g;\n" " v3.y = texRECT(texD, TexCC.xy + offsets).g;\n" " v5.x = texRECT(texD, TexLC.xy + offsets).g;\n" " v5.y = texRECT(texD, TexRC.xy + offsets).g;\n" " v5.z = texRECT(texD, TexCD.xy + offsets).g;\n" " v5.w = texRECT(texD, TexCU.xy + offsets).g;\n" " }\n" " }while(key_moved);\n" << " bool test1 = (abs(cc.g + 0.5*dot(float3(fx, fy, fs), dxys ))> THRESHOLD1) ;\n" " float test2_v1= fxx*fyy - fxy *fxy; \n" " float test2_v2 = (fxx+fyy); \n" " test2_v2 = test2_v2*test2_v2;\n" " bool test2 = test2_v1>0 && test2_v2 < THRESHOLD2 * test2_v1; \n " //keep the point when the offset is less than 1 << " FragData1 = test1 && test2 && all( abs(dxys) < 1)? float4( dog, dxys.xy+offsets, dxys.z) : float4(0, 0, 0, 0); \n" "}\n" <<'\0'; ProgramCG * program; s_keypoint = program = new ProgramCG(buffer); //parameter _param_dog_texu = cgGetNamedParameter(*program, "texU"); _param_dog_texd = cgGetNamedParameter(*program, "texD"); return 1; } //keypoint detection shader //1. compare with 26 neighbours //2. sub-pixel sub-scale localization //3. output: [dog, offset(x,y,s)] void ShaderBagCG:: LoadKeypointShader(float threshold, float edge_threshold) { char buffer[10240]; float threshold0 = threshold* (GlobalUtil::_SubpixelLocalization?0.8f:1.0f); float threshold1 = threshold; float threshold2 = (edge_threshold+1)*(edge_threshold+1)/edge_threshold; ostrstream out(buffer, 10240); out< THRESHOLD0 && all(cc.gggg > max(v1, v2))?1.0: 0.0;\n" " dog = cc.g < -THRESHOLD0 && all(cc.gggg < min(v1, v2))?0.5: dog;\n"; pos = out.tellp(); //do edge supression first.. //vector v1 is < (-1, 0), (1, 0), (0,-1), (0, 1)> //vector v2 is < (-1,-1), (-1,1), (1,-1), (1, 1)> out<< " if(dog == 0.0) return;\n" " float fxx, fyy, fxy; \n" " float4 D2 = v1.xyzw - cc.gggg;\n" " float2 D4 = v2.xw - v2.yz;\n" " fxx = D2.x + D2.y;\n" " fyy = D2.z + D2.w;\n" " fxy = 0.25*(D4.x + D4.y);\n" " float fxx_plus_fyy = fxx + fyy;\n" " float score_up = fxx_plus_fyy*fxx_plus_fyy; \n" " float score_down = (fxx*fyy - fxy*fxy);\n" " if( score_down <= 0 || score_up > THRESHOLD2 * score_down)return;\n" //... << " float2 D5 = 0.5*(v1.yw-v1.xz); \n" " float fx = D5.x, fy = D5.y ; \n" " float fs, fss , fxs, fys ; \n" " float2 v3; float4 v4, v5, v6;\n" //read 9 pixels of upper level << " v3.x = texRECT(texU, TexCC.xy).g;\n" " v4.x = texRECT(texU, TexLC.xy).g;\n" " v4.y = texRECT(texU, TexRC.xy).g;\n" " v4.z = texRECT(texU, TexCD.xy).g;\n" " v4.w = texRECT(texU, TexCU.xy).g;\n" " v6.x = texRECT(texU, TexLD.xy).g;\n" " v6.y = texRECT(texU, TexLU.xy).g;\n" " v6.z = texRECT(texU, TexRD.xy).g;\n" " v6.w = texRECT(texU, TexRU.xy).g;\n" //compare with 9 pixels of upper level //read and compare with 9 pixels of lower level //the maximum case << " if(dog == 1.0)\n" " {\n" " bool4 test = cc.gggg < max(v4, v6); \n" " if(cc.g < v3.x || any(test.xy||test.zw))return; \n" " v3.y = texRECT(texD, TexCC.xy).g;\n" " v5.x = texRECT(texD, TexLC.xy).g;\n" " v5.y = texRECT(texD, TexRC.xy).g;\n" " v5.z = texRECT(texD, TexCD.xy).g;\n" " v5.w = texRECT(texD, TexCU.xy).g;\n" " v6.x = texRECT(texD, TexLD.xy).g;\n" " v6.y = texRECT(texD, TexLU.xy).g;\n" " v6.z = texRECT(texD, TexRD.xy).g;\n" " v6.w = texRECT(texD, TexRU.xy).g;\n" " test = cc.ggggmin(v4, v6); \n" " if(cc.g > v3.x || any(test.xy||test.zw))return; \n" " v3.y = texRECT(texD, TexCC.xy).g;\n" " v5.x = texRECT(texD, TexLC.xy).g;\n" " v5.y = texRECT(texD, TexRC.xy).g;\n" " v5.z = texRECT(texD, TexCD.xy).g;\n" " v5.w = texRECT(texD, TexCU.xy).g;\n" " v6.x = texRECT(texD, TexLD.xy).g;\n" " v6.y = texRECT(texD, TexLU.xy).g;\n" " v6.z = texRECT(texD, TexRD.xy).g;\n" " v6.w = texRECT(texD, TexRU.xy).g;\n" " test = cc.gggg>min(v5, v6); \n" " if(cc.g > v3.y || any(test.xy||test.zw))return; \n" " }\n"; if(GlobalUtil::_SubpixelLocalization) // sub-pixel localization FragData1 = float4(dog, 0, 0, 0); return; out << " fs = 0.5*( v3.x - v3.y ); //bug fix 9/12/2007 \n" " fss = v3.x + v3.y - cc.g - cc.g;\n" " fxs = 0.25 * ( v4.y + v5.x - v4.x - v5.y);\n" " fys = 0.25 * ( v4.w + v5.z - v4.z - v5.w);\n" ///////////////////////////////////////////////////////////////// // let dog difference be quatratic function of dx, dy, ds; // df(dx, dy, ds) = fx * dx + fy*dy + fs * ds + // + 0.5 * ( fxx * dx * dx + fyy * dy * dy + fss * ds * ds) // + (fxy * dx * dy + fxs * dx * ds + fys * dy * ds) // (fx, fy, fs, fxx, fyy, fss, fxy, fxs, fys are the derivatives) //the local extremum satisfies // df/dx = 0, df/dy = 0, df/dz = 0 //that is // |-fx| | fxx fxy fxs | |dx| // |-fy| = | fxy fyy fys | * |dy| // |-fs| | fxs fys fss | |ds| // need to solve dx, dy, ds // Use Gauss elimination to solve the linear system << " float3 dxys = float3(0.0); \n" " float4 A0, A1, A2 ; \n" " A0 = float4(fxx, fxy, fxs, -fx); \n" " A1 = float4(fxy, fyy, fys, -fy); \n" " A2 = float4(fxs, fys, fss, -fs); \n" " float3 x3 = abs(float3(fxx, fxy, fxs)); \n" " float maxa = max(max(x3.x, x3.y), x3.z); \n" " if(maxa >= 1e-10 ) { \n" " if(x3.y ==maxa ) \n" " { \n" " float4 TEMP = A1; A1 = A0; A0 = TEMP; \n" " }else if( x3.z == maxa ) \n" " { \n" " float4 TEMP = A2; A2 = A0; A0 = TEMP; \n" " } \n" " A0 /= A0.x; \n" " A1 -= A1.x * A0; \n" " A2 -= A2.x * A0; \n" " float2 x2 = abs(float2(A1.y, A2.y)); \n" " if( x2.y > x2.x ) \n" " { \n" " float3 TEMP = A2.yzw; \n" " A2.yzw = A1.yzw; \n" " A1.yzw = TEMP; \n" " x2.x = x2.y; \n" " } \n" " if(x2.x >= 1e-10) { \n" " A1.yzw /= A1.y; \n" " A2.yzw -= A2.y * A1.yzw; \n" " if(abs(A2.z) >= 1e-10) { \n" // compute dx, dy, ds: << " dxys.z = A2.w /A2.z; \n" " dxys.y = A1.w - dxys.z*A1.z; \n" " dxys.x = A0.w - dxys.z*A0.z - dxys.y*A0.y; \n" //one more threshold which I forgot in versions prior to 286 << " bool bugfix_test = (abs(cc.g + 0.5*dot(float3(fx, fy, fs), dxys )) < THRESHOLD1) ;\n" " if(bugfix_test || any(abs(dxys) >= 1.0)) dog = 0; \n" " }}}\n" //keep the point when the offset is less than 1 << " FragData1 = float4( dog, dxys); \n" "}\n" <<'\0'; else out<< " FragData1 = float4( dog, 0, 0, 0) ; \n" "}\n" <<'\0'; ProgramCG * program; s_keypoint = program = new ProgramCG(buffer); if(!program->IsValidProgram()) { delete program; out.seekp(pos); out << " FragData1 = float4( fabs(cc.g) > 2.0 * THRESHOLD0? dog : 0, 0, 0, 0) ; \n" "}\n" <<'\0'; s_keypoint = program = new ProgramCG(buffer); GlobalUtil::_SubpixelLocalization = 0; std::cerr<<"Detection simplified on this hardware"<= width) { out<<"0"; }else if(offset[j]==0.0) { out<<"or"; }else { out<<"texRECT(tex, TexCoord0.xy + float2(float("<= width) out<<"0"; else out<= height) { out<<"0"; }else if(offset[j]==0.0) { out<<"orb.y"; }else { out<<"texRECT(tex, TexCoord0.xy + float2(0, float("<= height) out<<"0"; else out<>1; float * pf = kernel + halfwidth; int nhpixel = (halfwidth+1)>>1; //how many neighbour pixels need to be looked up int npixel = (nhpixel<<1)+1;// char buffer[10240]; float weight[3]; ostrstream out(buffer, 10240); out< halfwidth? 0 : pf[xwn]; } //if(weight[1]!=0.0) out<<"FragColor += "<>1; float * pf = kernel + halfh; int nhpixel = (halfh+1)>>1; //how many neighbour pixels need to be looked up int npixel = (nhpixel<<1)+1;// char buffer[10240]; float weight[3]; ostrstream out(buffer, 10240); out< halfh? 0 : pf[ywn]; } //if(weight[1]!=0.0) out<<"FragColor += "<0.0);\n" "}"); s_genlist_init_ex = program = new ProgramCG( "void main (uniform float2 bbox, \n" "uniform samplerRECT tex, \n" "in float4 TexCoord0 : TEXCOORD0,\n" "in float4 TexCoord1 : TEXCOORD1, \n" "in float4 TexCoord2 : TEXCOORD2, \n" "in float4 TexCoord3 : TEXCOORD3,\n" "out float4 FragColor : COLOR0){\n" "float4 helper = float4( \n" "texRECT(tex, TexCoord0.xy).r, texRECT(tex, TexCoord1.xy).r,\n" "texRECT(tex, TexCoord2.xy).r, texRECT(tex, TexCoord3.xy).r);\n" "bool4 helper4 = bool4(TexCoord0.xy < bbox, TexCoord3.xy < bbox); \n" "bool4 helper2 = helper4.xzxz && helper4.yyww; \n" "FragColor = float4(helper2 && (helper>0.0 ));\n" "}"); _param_genlist_init_bbox = cgGetNamedParameter( *program, "bbox"); //reduction ... s_genlist_histo = new ProgramCG( "void main (\n" "uniform samplerRECT tex, in float2 TexCoord0 : TEXCOORD0,\n" "in float2 TexCoord1 : TEXCOORD1, in float2 TexCoord2 : TEXCOORD2, in float2 TexCoord3 : TEXCOORD3,\n" "out float4 FragColor : COLOR0){\n" "float4 helper; float4 helper2; \n" "helper = texRECT(tex, TexCoord0); helper2.xy = helper.xy + helper.zw; \n" "helper = texRECT(tex, TexCoord1); helper2.zw = helper.xy + helper.zw; \n" "FragColor.rg = helper2.xz + helper2.yw;\n" "helper = texRECT(tex, TexCoord2); helper2.xy = helper.xy + helper.zw; \n" "helper = texRECT(tex, TexCoord3); helper2.zw = helper.xy + helper.zw; \n" "FragColor.ba= helper2.xz+helper2.yw;\n" "}"); //read of the first part, which generates tex coordinates s_genlist_start= program = LoadGenListStepShader(1, 1); _param_ftex_width= cgGetNamedParameter(*program, "width"); _param_genlist_start_tex0 = cgGetNamedParameter(*program, "tex0"); //stepping s_genlist_step = program = LoadGenListStepShader(0, 1); _param_genlist_step_tex= cgGetNamedParameter(*program, "tex"); _param_genlist_step_tex0= cgGetNamedParameter(*program, "tex0"); } ProgramCG* ShaderBagCG::LoadGenListStepShader(int start, int step) { int i; char buffer[10240]; //char chanels[5] = "rgba"; ostrstream out(buffer, 10240); out<<"void main(out float4 FragColor : COLOR0, \n"; for(i = 0; i < step; i++) out<<"uniform samplerRECT tex"<0) { out<<"float2 cpos = float2(-0.5, 0.5);\t float2 opos;\n"; for(i = 0; i < step; i++) { //#define SETP_CODE_2 #ifndef SETP_CODE_2 /* out<<"cc = texRECT(tex"< float3(sum3[0], sum3[1], sum3[2]));\n"; out<<"opos.y = -0.5 + cmp.y; opos.x = -0.5 + cmp.x + (cmp.z - cmp.y);\n"; out<<"index -= dot(cmp, cc.rgb);\n"; out<<"pos = (pos + pos + opos);\n";*/ out<<"cc = texRECT(tex"<=dim-1)) " " //discard; \n" " { FragData0 = FragData1 = float4(0.0); return; }\n" " float anglef = texRECT(tex, coord).z;\n" " if(anglef > M_PI) anglef -= TWO_PI;\n" " float sigma = texRECT(tex, coord).w; \n" " float spt = abs(sigma * WF); //default to be 3*sigma \n"; //rotation out<< " float4 cscs, rots; \n" " sincos(anglef, cscs.y, cscs.x); \n" " cscs.zw = - cscs.xy; \n" " rots = cscs /spt; \n" " cscs *= spt; \n"; //here cscs is actually (cos, sin, -cos, -sin) * (factor: 3)*sigma //and rots is (cos, sin, -cos, -sin ) /(factor*sigma) //devide the 4x4 sift grid into 16 1x1 block, and each corresponds to a shader thread //To use linear interoplation, 1x1 is increased to 2x2, by adding 0.5 to each side out<< " float4 temp; float2 pt, offsetpt; \n" " /*the fraction part of idx is .5*/ \n" " offsetpt.x = 4.0 * frac(idx*0.25) - 2.0; \n" " offsetpt.y = floor(idx*0.25) - 1.5; \n" " temp = cscs.xwyx*offsetpt.xyxy; \n" " pt = pos + temp.xz + temp.yw; \n"; //get a horizontal bounding box of the rotated rectangle out<< " float2 bwin = abs(cscs.xy); \n" " float bsz = bwin.x + bwin.y; \n" " float4 sz; float2 spos; \n" " sz.xy = max(pt - bsz, float2(1,1));\n" " sz.zw = min(pt + bsz, dim - 2); \n" " sz = floor(sz)+0.5;"; //move sample point to pixel center //get voting for two box out<<"\n" " float4 DA, DB; \n" " DA = DB = float4(0, 0, 0, 0); \n" " for(spos.y = sz.y; spos.y <= sz.w; spos.y+=1.0) \n" " { \n" " for(spos.x = sz.x; spos.x <= sz.z; spos.x+=1.0) \n" " { \n" " float2 diff = spos - pt; \n" " temp = rots.xywx * diff.xyxy; \n" " float2 nxy = (temp.xz + temp.yw); \n" " float2 nxyn = abs(nxy); \n" " if(all(nxyn < float2(1.0)))\n" " {\n" " float4 cc = texRECT(gradTex, spos); \n" " float mod = cc.b; float angle = cc.a; \n" " float theta0 = (anglef - angle)*RPI; \n" " float theta = theta0 < 0? theta0 + 8.0 : theta0; // fmod(theta0 + 8.0, 8.0); \n" " diff = nxy + offsetpt.xy; \n" " float ww = exp(-0.125*dot(diff, diff));\n" " float2 weights = 1 - nxyn;\n" " float weight = weights.x * weights.y *mod*ww; \n" " float theta1 = floor(theta); \n" " float weight2 = (theta - theta1) * weight; \n" " float weight1 = weight - weight2;\n" " DA += float4(theta1 == float4(0, 1, 2, 3))*weight1; \n" " DA += float4(theta1 == float4(7, 0, 1, 2))*weight2; \n" " DB += float4(theta1 == float4(4, 5, 6, 7))*weight1; \n" " DB += float4(theta1 == float4(3, 4, 5, 6))*weight2; \n" " }\n" " }\n" " }\n"; out<< " FragData0 = DA; FragData1 = DB;\n" "}\n"<<'\0'; ProgramCG * program; s_descriptor_fp = program = new ProgramCG(buffer); _param_descriptor_gtex = cgGetNamedParameter(*program, "gradTex"); _param_descriptor_size = cgGetNamedParameter(*program, "size"); _param_descriptor_dsize = cgGetNamedParameter(*program, "dsize"); } //the shader that computes the descriptors void ShaderBagCG::LoadDescriptorShader() { GlobalUtil::_DescriptorPPT = 16; LoadDescriptorShaderF2(); } void ShaderBagCG::LoadOrientationShader() { char buffer[10240]; ostrstream out(buffer,10240); out<<"\n" "#define GAUSSIAN_WF "<1 && GlobalUtil::_OrientationPack2 == 0) out<<", out float4 OrientationData : COLOR1"; if(GlobalUtil::_SubpixelLocalization || GlobalUtil::_KeepExtremumSign) { //data for sub-pixel localization out<<", uniform samplerRECT texS"; } //use 9 float4 to store histogram of 36 directions out<<") \n" "{ \n" " float4 bins[10]; \n" " for (int i=0; i<9; i++) bins[i] = float4(0,0,0,0); \n" " const float4 loc = texRECT(tex, TexCoord0); \n" " const bool orientation_mode = (size.z != 0); \n" " float2 pos = loc.xy; \n" " float sigma = orientation_mode? abs(size.z) : loc.w; \n"; if(GlobalUtil::_SubpixelLocalization || GlobalUtil::_KeepExtremumSign) { out<< " if(orientation_mode) {\n" " float4 keyx = texRECT(texS, pos);\n" " sigma = sigma * pow(size.w, keyx.w); \n" " pos.xy = pos.xy + keyx.yz; \n" " #if " << GlobalUtil::_KeepExtremumSign << "\n" " if(keyx.x<0.6) sigma = - sigma;\n" " #endif\n" " }\n"; } out<< " //bool fixed_orientation = (size.z < 0); \n" " if(size.z < 0) {FeatureData = float4(pos, 0, sigma); return;}" " const float gsigma = sigma * GAUSSIAN_WF; \n" " const float2 win = abs(sigma.xx) * (SAMPLE_WF * GAUSSIAN_WF); \n" " const float2 dim = size.xy; \n" " const float dist_threshold = win.x*win.x+0.5; \n" " const float factor = -0.5/(gsigma*gsigma); \n" " float4 sz; float2 spos; \n" " //if(any(pos.xy <= 1)) discard; \n" " sz.xy = max( pos - win, float2(1,1)); \n" " sz.zw = min( pos + win, dim-2); \n" " sz = floor(sz)+0.5;"; //loop to get the histogram out<<"\n" " for(spos.y = sz.y; spos.y <= sz.w; spos.y+=1.0) \n" " { \n" " for(spos.x = sz.x; spos.x <= sz.z; spos.x+=1.0) \n" " { \n" " const float2 offset = spos - pos; \n" " const float sq_dist = dot(offset,offset); \n" " if( sq_dist < dist_threshold){ \n" " const float4 cc = texRECT(gradTex, spos); \n" " const float grad = cc.b; float theta = cc.a; \n" " float idx = floor(degrees(theta)*0.1); \n" " const float weight = grad*exp(sq_dist * factor); \n" " if(idx < 0 ) idx += 36; \n" " const float vidx = 4.0 * fract(idx * 0.25);//fmod(idx, 4); \n" " const float4 inc = weight*float4(vidx == float4(0,1,2,3)); "; if(GlobalUtil::_UseDynamicIndexing && strcmp(cgGetProfileString(ProgramCG::_FProfile), "gp4fp")==0) // if(ProgramCG::_FProfile == CG_PROFILE_GPU_FP) this enumerant is not defined in cg1.5 { //gp_fp supports dynamic indexing out<<"\n" " int iidx = int(floor(idx*0.25)); \n" " bins[iidx]+=inc; \n" " } \n" " } \n" " }"; }else { //nvfp40 still does not support dynamic array indexing //unrolled binary search... out<<"\n" " if(idx < 16) \n" " { \n" " if(idx < 8) \n" " { \n" " if(idx < 4) { bins[0]+=inc;} \n" " else { bins[1]+=inc;} \n" " }else \n" " { \n" " if(idx < 12){ bins[2]+=inc;} \n" " else { bins[3]+=inc;} \n" " } \n" " }else if(idx < 32) \n" " { \n" " if(idx < 24) \n" " { \n" " if(idx <20) { bins[4]+=inc;} \n" " else { bins[5]+=inc;} \n" " }else \n" " { \n" " if(idx < 28){ bins[6]+=inc;} \n" " else { bins[7]+=inc;} \n" " } \n" " }else \n" " { \n" " bins[8]+=inc; \n" " } \n" " } \n" " } \n" " }"; } WriteOrientationCodeToStream(out); ProgramCG * program; s_orientation = program = new ProgramCG(buffer); _param_orientation_gtex = cgGetNamedParameter(*program, "gradTex"); _param_orientation_size = cgGetNamedParameter(*program, "size"); _param_orientation_stex = cgGetNamedParameter(*program, "texS"); } void ShaderBagCG::WriteOrientationCodeToStream(std::ostream& out) { //smooth histogram and find the largest /* smoothing kernel: (1 3 6 7 6 3 1 )/27 the same as 3 pass of (1 1 1)/3 averaging maybe better to use 4 pass on the vectors... */ //the inner loop on different array numbers is always unrolled in fp40 //bug fixed here:) out<<"\n" " float3x3 mat1 = float3x3(1, 0, 0, 3, 1, 0, 6, 3, 1)/27.0;; //bug fix.. \n" " float4x4 mat2 = float4x4( 7, 6, 3, 1, 6, 7, 6, 3, 3, 6, 7, 6, 1, 3, 6, 7)/27.0;;\n" " for (int j=0; j<2; j++) \n" " { \n" " float4 prev = bins[8]; \n" " bins[9] = bins[0]; \n" " for (int i=0; i<9; i++) \n" " { \n" " float4 newb = mul ( bins[i], mat2); \n" " newb.xyz += mul ( prev.yzw, mat1); \n" " prev = bins[i]; \n" " newb.wzy += mul ( bins[i+1].zyx, mat1); \n" " bins[i] = newb; \n" " } \n" " }"; //find the maximum voting out<<"\n" " float4 maxh; float2 maxh2; float4 maxh4 = bins[0]; \n" " for (int i=1; i<9; i++) maxh4 = max(maxh4, bins[i]); \n" " maxh2 = max(maxh4.xy, maxh4.zw); maxh = float4(max(maxh2.x, maxh2.y));"; char *testpeak_code; char *savepeak_code; //save two/three/four orientations with the largest votings? // if(GlobalUtil::_MaxOrientation>1) { out<<"\n" " float4 Orientations = float4(0, 0, 0, 0); \n" " float4 weights = float4(0,0,0,0); "; testpeak_code = "\n" " {test = bins[i]>hh;"; //save the orientations in weight-decreasing order if(GlobalUtil::_MaxOrientation ==2) { savepeak_code = "\n" " if(weight <=weights.g){}\n" " else if(weight >weights.r)\n" " {weights.rg = float2(weight, weights.r); Orientations.rg = float2(th, Orientations.r);}\n" " else {weights.g = weight; Orientations.g = th;}"; }else if(GlobalUtil::_MaxOrientation ==3) { savepeak_code = "\n" " if(weight <=weights.b){}\n" " else if(weight >weights.r)\n" " {weights.rgb = float3(weight, weights.rg); Orientations.rgb = float3(th, Orientations.rg);}\n" " else if(weight >weights.g)\n" " {weights.gb = float2(weight, weights.g); Orientations.gb = float2(th, Orientations.g);}\n" " else {weights.b = weight; Orientations.b = th;}"; }else { savepeak_code = "\n" " if(weight <=weights.a){}\n" " else if(weight >weights.r)\n" " {weights = float4(weight, weights.rgb); Orientations = float4(th, Orientations.rgb);}\n" " else if(weight >weights.g)\n" " {weights.gba = float3(weight, weights.gb); Orientations.gba = float3(th, Orientations.gb);}\n" " else if(weight >weights.b)\n" " {weights.ba = float2(weight, weights.b); Orientations.ba = float2(th, Orientations.b);}\n" " else {weights.a = weight; Orientations.a = th;}"; } }else { out<<"\n" " float Orientations = 0; "; testpeak_code ="\n" " if(npeaks==0){ \n" " test = (bins[i] >= maxh) ;"; savepeak_code="\n" " npeaks++; \n" " Orientations = th.x;"; } //find the peaks //the following loop will be unrolled out<<"\n" " const float4 hh = maxh * ORIENTATION_THRESHOLD; bool4 test; \n" " bins[9] = bins[0]; \n" " float npeaks = 0, k = 0; \n" " float prevb = bins[8].w; \n" " for (int i = 0; i <9 ; i++) \n" " {" < prevb && bins[i].x > bins[i].y ) \n" " { \n" " float di = 0.5 * (bins[i].y-prevb) / (bins[i].x *2.0 -bins[i].y -prevb) ; \n" " float th = (k+di+0.5); float weight = bins[i].x;" < bins[i].xz) ) \n" " { \n" " float di = 0.5 * (bins[i].z-bins[i].x) / (bins[i].y * 2.0 - bins[i].z - bins[i].x) ; \n" " float th = (k+di+1.5); float weight = bins[i].y; " < bins[i].yw) ) \n" " { \n" " float di = 0.5 * (bins[i].w-bins[i].y) / (bins[i].z * 2.0-bins[i].w-bins[i].y) ; \n" " float th = (k+di+2.5); float weight = bins[i].z; " < bins[i].z && bins[i].w > bins[i+1].x ) \n" " { \n" " float di = 0.5 * (bins[i+1].x-bins[i].z) / (bins[i].w * 2.0- bins[i+1].x-bins[i].z) ; \n" " float th = (k+di+3.5); float weight = bins[i].w; " <1) { out<<"\n" " if(orientation_mode){\n" " npeaks = dot(float4(1,1," <<(GlobalUtil::_MaxOrientation>2 ? 1 : 0)<<"," <<(GlobalUtil::_MaxOrientation >3? 1 : 0)<<"), float4(weights>hh));\n" " OrientationData = radians((Orientations )*10.0);\n" " FeatureData = float4(pos, npeaks, sigma);\n" " }else{\n" " FeatureData = float4(pos, radians((Orientations.x)*10.0), sigma);\n" " }\n"; }else { out<<"\n" " FeatureData = float4(pos, radians((Orientations.x)*10.0), sigma);"; } //end out<<"\n" "}\n"<<'\0'; } void ShaderBagCG::SetSimpleOrientationInput(int oTex, float sigma, float sigma_step) { cgGLSetTextureParameter(_param_orientation_gtex, oTex); cgGLEnableTextureParameter(_param_orientation_gtex); cgGLSetParameter1f(_param_orientation_size, sigma); } void ShaderBagCG::SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex, float step) { /// cgGLSetTextureParameter(_param_orientation_gtex, gtex); cgGLEnableTextureParameter(_param_orientation_gtex); if((GlobalUtil::_SubpixelLocalization || GlobalUtil::_KeepExtremumSign)&& stex) { //specify texutre for subpixel subscale localization cgGLSetTextureParameter(_param_orientation_stex, stex); cgGLEnableTextureParameter(_param_orientation_stex); } float size[4]; size[0] = (float)width; size[1] = (float)height; size[2] = sigma; size[3] = step; cgGLSetParameter4fv(_param_orientation_size, size); } void ShaderBagCG::SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma) { /// cgGLSetTextureParameter(_param_descriptor_gtex, gtex); cgGLEnableTextureParameter(_param_descriptor_gtex); float dsize[4] ={dwidth, 1.0f/dwidth, fwidth, 1.0f/fwidth}; cgGLSetParameter4fv(_param_descriptor_dsize, dsize); float size[3]; size[0] = width; size[1] = height; size[2] = GlobalUtil::_DescriptorWindowFactor; cgGLSetParameter3fv(_param_descriptor_size, size); } /////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////PACKED VERSION?/////////////////////////////////// ShaderBagPKCG::ShaderBagPKCG() { ProgramCG::InitContext(); } void ShaderBagPKCG::UnloadProgram() { cgGLUnbindProgram(ProgramCG::_FProfile); cgGLDisableProfile(ProgramCG::_FProfile); } void ShaderBagPKCG::LoadFixedShaders() { ProgramCG * program; /* char *rgb2gray_packing_code = "void main(uniform samplerRECT rgbTex, in float4 TexCoord0 : TEXCOORD0, \n" " in float4 TexCoord1 : TEXCOORD1, in float4 TexCoord2 : TEXCOORD2, \n" " in float4 TexCoord3 : TEXCOORD3, out float4 FragData : COLOR0){\n" " const float3 weight = vec3(0.299, 0.587, 0.114);\n" " FragData.r = dot(weight, texRECT(rgbTex,TexCoord0.st ).rgb);\n" " FragData.g = dot(weight, texRECT(rgbTex,TexCoord1.st ).rgb);\n" " FragData.b = dot(weight, texRECT(rgbTex,TexCoord2.st ).rgb);\n" " FragData.a = dot(weight, texRECT(rgbTex,TexCoord3.st ).rgb);}";// s_gray = new ProgramCG( rgb2gray_packing_code); */ s_gray = new ProgramCG( "void main(float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float intensity = dot(float3(0.299, 0.587, 0.114), texRECT(tex,TexCoord0.xy ).rgb);\n" "FragColor= float4(intensity, intensity, intensity, 1.0);}" ); s_sampling = new ProgramCG( "void main(uniform samplerRECT tex, in float4 TexCoord0 : TEXCOORD0, \n" " in float4 TexCoord1 : TEXCOORD1, in float4 TexCoord2 : TEXCOORD2, \n" " in float4 TexCoord3 : TEXCOORD3, out float4 FragData : COLOR0 ){\n" " FragData= float4( texRECT(tex,TexCoord0.st ).r,texRECT(tex,TexCoord1.st ).r,\n" " texRECT(tex,TexCoord2.st ).r,texRECT(tex,TexCoord3.st ).r);}" ); s_margin_copy = program = new ProgramCG( "void main(in float4 texCoord0: TEXCOORD0, out float4 FragColor: COLOR0, \n" "uniform samplerRECT tex, uniform float4 truncate){\n" "float4 cc = texRECT(tex, min(texCoord0.xy, truncate.xy)); \n" "bool2 ob = texCoord0.xy < truncate.xy;\n" "if(ob.y) { FragColor = (truncate.z ==0 ? cc.rrbb : cc.ggaa); } \n" "else if(ob.x) {FragColor = (truncate.w <1.5 ? cc.rgrg : cc.baba);} \n" "else { float4 weights = float4(float4(0, 1, 2, 3) == truncate.w);\n" "float v = dot(weights, cc); FragColor = v.xxxx;}}"); _param_margin_copy_truncate = cgGetNamedParameter(*program, "truncate"); s_zero_pass = new ProgramCG("void main(out float4 FragColor : COLOR0){FragColor = 0;}"); s_grad_pass = program = new ProgramCG( "void main (\n" "float4 TexCC : TEXCOORD0, float4 TexLC : TEXCOORD1,\n" "float4 TexRC : TEXCOORD2, float4 TexCD : TEXCOORD3, float4 TexCU : TEXCOORD4,\n" "out float4 FragData0 : COLOR0, out float4 FragData1 : COLOR1, \n" "out float4 FragData2 : COLOR2, uniform samplerRECT tex, uniform samplerRECT texp)\n" "{\n" " float4 v1, v2, gg;\n" " float4 cc = texRECT(tex, TexCC.xy);\n" " float4 cp = texRECT(texp, TexCC.xy);\n" " FragData0 = cc - cp; \n" " float4 cl = texRECT(tex, TexLC.xy); float4 cr = texRECT(tex, TexRC.xy);\n" " float4 cd = texRECT(tex, TexCD.xy); float4 cu = texRECT(tex, TexCU.xy);\n" " float4 dx = (float4(cr.rb, cc.ga) - float4(cc.rb, cl.ga)).zxwy;\n" " float4 dy = (float4(cu.rg, cc.ba) - float4(cc.rg, cd.ba)).zwxy;\n" " FragData1 = 0.5 * sqrt(dx*dx + dy * dy);\n" " FragData2 = FragData1 > 0? atan2(dy, dx) : float4(0);\n" "}\n\0"); _param_grad_pass_texp = cgGetNamedParameter(*program, "texp"); s_dog_pass = program = new ProgramCG( "void main (float4 TexCC : TEXCOORD0, out float4 FragData0 : COLOR0, \n" " uniform samplerRECT tex, uniform samplerRECT texp)\n" "{\n" " float4 cc = texRECT(tex, TexCC.xy);\n" " float4 cp = texRECT(texp, TexCC.xy);\n" " FragData0 = cc - cp; \n" "}\n\0"); //// if(GlobalUtil::_SupportFP40) { LoadOrientationShader(); if(GlobalUtil::_DescriptorPPT) LoadDescriptorShader(); }else { s_orientation = program = new ProgramCG( "void main(out float4 FragColor : COLOR0, \n" " uniform samplerRECT fTex, uniform samplerRECT oTex, \n" " uniform float2 size, \n" " in float2 tpos : TEXCOORD0){\n" " float4 cc = texRECT(fTex, tpos);\n" " float2 co = cc.xy * 0.5; \n" " float4 oo = texRECT(oTex, co);\n" " bool2 bo = frac(co) < 0.5; \n" " float o = bo.y? (bo.x? oo.r : oo.g) : (bo.x? oo.b : oo.a); \n" " FragColor = float4(cc.rg, o, size.x * pow(size.y, cc.a));}"); _param_orientation_gtex= cgGetNamedParameter(*program, "oTex"); _param_orientation_size= cgGetNamedParameter(*program, "size"); GlobalUtil::_FullSupported = 0; GlobalUtil::_MaxOrientation = 0; GlobalUtil::_DescriptorPPT = 0; std::cerr<<"Orientation simplified on this hardware"< 0.9)? size : -size);\n" " dxy.y = type < 0.2 ? 0 : ((type < 0.3 || type > 0.7 )? -size :size); \n" " sincos(cc.b, s, c);\n" " FragColor.x = cc.x + c*dxy.x-s*dxy.y;\n" " FragColor.y = cc.y + c*dxy.y+s*dxy.x;}\n" "}\n\0"); /*FragColor = float4(tpos, 0.0, 1.0);}\n\0");*/ _param_genvbo_size = cgGetNamedParameter(*program, "sizes"); s_display_gaussian = new ProgramCG( "void main(uniform samplerRECT tex, in float4 TexCoord0:TEXCOORD0, out float4 FragData: COLOR0 ){\n" "float4 pc = texRECT(tex, TexCoord0.xy); bool2 ff = (frac(TexCoord0.xy) < 0.5);\n" "float v = ff.y?(ff.x? pc.r : pc.g):(ff.x?pc.b:pc.a); FragData = float4(v.xxx, 1.0);}"); s_display_dog = new ProgramCG( "void main(in float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float4 pc = texRECT(tex, TexCoord0.xy); bool2 ff = (frac(TexCoord0.xy) < 0.5);\n" "float v = ff.y ?(ff.x ? pc.r : pc.g):(ff.x ? pc.b : pc.a);float g = (0.5+20.0*v);\n" "FragColor = float4(g, g, g, 1.0);}" ); s_display_grad = new ProgramCG( "void main(in float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float4 pc = texRECT(tex, TexCoord0.xy); bool2 ff = (frac(TexCoord0.xy) < 0.5);\n" "float v = ff.y ?(ff.x ? pc.r : pc.g):(ff.x ? pc.b : pc.a); FragColor = float4(5.0 *v.xxx, 1.0); }"); s_display_keys= new ProgramCG( "void main(in float4 TexCoord0 : TEXCOORD0, out float4 FragColor : COLOR0, uniform samplerRECT tex){\n" "float4 oc = texRECT(tex, TexCoord0.xy); \n" "float4 cc = float4(abs(oc.r) == float4(1.0, 2.0, 3.0, 4.0));\n" "bool2 ff = (frac(TexCoord0.xy) < 0.5);\n" "float v = ff.y ?(ff.x ? cc.r : cc.g):(ff.x ? cc.b : cc.a);\n" "if(oc.r == 0) discard;\n" "else if(oc.r > 0) FragColor = float4(1.0, 0, 0,1.0); \n" "else FragColor = float4(0.0,1.0,0.0,1.0); }" ); } void ShaderBagPKCG::LoadGenListShader(int ndoglev, int nlev) { //the V2 algorithms are only slightly faster, but way more complicated //LoadGenListShaderV2(ndoglev, nlev); return; ProgramCG * program; s_genlist_init_tight = new ProgramCG( "void main (uniform samplerRECT tex, in float4 TexCoord0 : TEXCOORD0,\n" "in float4 TexCoord1 : TEXCOORD1, in float4 TexCoord2 : TEXCOORD2, \n" "in float4 TexCoord3 : TEXCOORD3, out float4 FragColor : COLOR0)\n" "{\n" " float4 data = float4( texRECT(tex, TexCoord0.xy).r,\n" " texRECT(tex, TexCoord1.xy).r,\n" " texRECT(tex, TexCoord2.xy).r,\n" " texRECT(tex, TexCoord3.xy).r);\n" " FragColor = float4(data != 0);\n" "}"); s_genlist_init_ex = program = new ProgramCG( "void main (uniform float4 bbox, uniform samplerRECT tex, \n" "in float4 TexCoord0 : TEXCOORD0, in float4 TexCoord1 : TEXCOORD1, \n" "in float4 TexCoord2 : TEXCOORD2, in float4 TexCoord3 : TEXCOORD3,\n" "out float4 FragColor : COLOR0)\n" "{\n" " bool4 helper1 = abs(texRECT(tex, TexCoord0.xy).r)== float4(1.0, 2.0, 3.0, 4.0); \n" " bool4 helper2 = abs(texRECT(tex, TexCoord1.xy).r)== float4(1.0, 2.0, 3.0, 4.0);\n" " bool4 helper3 = abs(texRECT(tex, TexCoord2.xy).r)== float4(1.0, 2.0, 3.0, 4.0);\n" " bool4 helper4 = abs(texRECT(tex, TexCoord3.xy).r)== float4(1.0, 2.0, 3.0, 4.0);\n" " bool4 bx1 = TexCoord0.xxyy < bbox; \n" " bool4 bx4 = TexCoord3.xxyy < bbox; \n" " bool4 bx2 = bool4(bx4.xy, bx1.zw); \n" " bool4 bx3 = bool4(bx1.xy, bx4.zw);\n" " helper1 = (bx1.xyxy && bx1.zzww && helper1);\n" " helper2 = (bx2.xyxy && bx2.zzww && helper2);\n" " helper3 = (bx3.xyxy && bx3.zzww && helper3);\n" " helper4 = (bx4.xyxy && bx4.zzww && helper4);\n" " FragColor.r = any(helper1.xy || helper1.zw); \n" " FragColor.g = any(helper2.xy || helper2.zw); \n" " FragColor.b = any(helper3.xy || helper3.zw); \n" " FragColor.a = any(helper4.xy || helper4.zw); \n" "}"); _param_genlist_init_bbox = cgGetNamedParameter( *program, "bbox"); s_genlist_end = program = new ProgramCG( GlobalUtil::_KeepExtremumSign == 0 ? "void main( uniform samplerRECT tex, uniform samplerRECT ktex,\n" " in float4 tpos : TEXCOORD0, out float4 FragColor : COLOR0)\n" "{\n" " float4 tc = texRECT( tex, tpos.xy);\n" " float2 pos = tc.rg; float index = tc.b;\n" " float4 tk = texRECT( ktex, pos); \n" " float4 keys = float4(abs(tk.x) == float4(1.0, 2.0, 3.0, 4.0)); \n" " float2 opos; \n" " opos.x = dot(keys, float4(-0.5, 0.5, -0.5, 0.5));\n" " opos.y = dot(keys, float4(-0.5, -0.5, 0.5, 0.5));\n" " FragColor = float4(opos + pos + pos + tk.yz, 1.0, tk.w);\n" "}" : "void main( uniform samplerRECT tex, uniform samplerRECT ktex,\n" " in float4 tpos : TEXCOORD0, out float4 FragColor : COLOR0)\n" "{\n" " float4 tc = texRECT( tex, tpos.xy);\n" " float2 pos = tc.rg; float index = tc.b;\n" " float4 tk = texRECT( ktex, pos); \n" " float4 keys = float4(abs(tk.x) == float4(1.0, 2.0, 3.0, 4.0)); \n" " float2 opos; \n" " opos.x = dot(keys, float4(-0.5, 0.5, -0.5, 0.5));\n" " opos.y = dot(keys, float4(-0.5, -0.5, 0.5, 0.5));\n" " FragColor = float4(opos + pos + pos + tk.yz, sign(tk.x), tk.w);\n" "}" ); _param_genlist_end_ktex = cgGetNamedParameter(*program, "ktex"); //reduction ... s_genlist_histo = new ProgramCG( "void main (uniform samplerRECT tex, in float2 TexCoord0 : TEXCOORD0,\n" "in float2 TexCoord1 : TEXCOORD1, in float2 TexCoord2 : TEXCOORD2, \n" "in float2 TexCoord3 : TEXCOORD3, out float4 FragColor : COLOR0)\n" "{\n" " float4 helper; float4 helper2; \n" " helper = texRECT(tex, TexCoord0); helper2.xy = helper.xy + helper.zw; \n" " helper = texRECT(tex, TexCoord1); helper2.zw = helper.xy + helper.zw; \n" " FragColor.rg = helper2.xz + helper2.yw;\n" " helper = texRECT(tex, TexCoord2); helper2.xy = helper.xy + helper.zw; \n" " helper = texRECT(tex, TexCoord3); helper2.zw = helper.xy + helper.zw; \n" " FragColor.ba= helper2.xz+helper2.yw;\n" "}"); //read of the first part, which generates tex coordinates s_genlist_start= program = ShaderBagCG::LoadGenListStepShader(1, 1); _param_ftex_width= cgGetNamedParameter(*program, "width"); _param_genlist_start_tex0 = cgGetNamedParameter(*program, "tex0"); //stepping s_genlist_step = program = ShaderBagCG::LoadGenListStepShader(0, 1); _param_genlist_step_tex= cgGetNamedParameter(*program, "tex"); _param_genlist_step_tex0= cgGetNamedParameter(*program, "tex0"); } void ShaderBagPKCG::LoadGenListShaderV2(int ndoglev, int nlev) { ProgramCG * program; s_genlist_init_tight = new ProgramCG( "void main (uniform samplerRECT tex, in float4 TexCoord0 : TEXCOORD0,\n" "in float4 TexCoord1 : TEXCOORD1, in float4 TexCoord2 : TEXCOORD2, \n" "in float4 TexCoord3 : TEXCOORD3, out float4 FragColor : COLOR0)\n" "{\n" " float4 data1 = texRECT(tex, TexCoord0.xy);\n" " float4 data2 = texRECT(tex, TexCoord1.xy);\n" " float4 data3 = texRECT(tex, TexCoord2.xy);\n" " float4 data4 = texRECT(tex, TexCoord3.xy);\n" " bool4 helper1 = (abs(data1.r), float4(1.0, 2.0, 3.0, 4.0)); \n" " bool4 helper2 = (abs(data2.r), float4(1.0, 2.0, 3.0, 4.0));\n" " bool4 helper3 = (abs(data3.r), float4(1.0, 2.0, 3.0, 4.0));\n" " bool4 helper4 = (abs(data4.r), float4(1.0, 2.0, 3.0, 4.0));\n" " FragColor.r = any(helper1.xy || helper1.zw); \n" " FragColor.g = any(helper2.xy || helper2.zw); \n" " FragColor.b = any(helper3.xy || helper3.zw); \n" " FragColor.a = any(helper4.xy || helper4.zw); \n" " if(dot(FragColor, float4(1,1,1,1)) == 1) \n" " {\n" " //use a special method if there is only one in the 16, \n" " float4 data, helper; float2 pos, opos; \n" " if(FragColor.r){ \n" " data = data1; helper = helper1; pos = TexCoord0.xy;\n" " }else if(FragColor.g){\n" " data = data2; helper = helper2; pos = TexCoord1.xy;\n" " }else if(FragColor.b){\n" " data = data3; helper = helper3; pos = TexCoord2.xy;\n" " }else{\n" " data = data4; helper = helper4; pos = TexCoord3.xy;\n" " }\n" " opos.x = dot(helper, float4(-0.5, 0.5, -0.5, 0.5));\n" " opos.y = dot(helper, float4(-0.5, -0.5, 0.5, 0.5));\n" " FragColor = float4( pos + pos + opos + data.yz, -1, data.w); \n" " }\n" "}"); s_genlist_init_ex = program = new ProgramCG( "void main (uniform float4 bbox, uniform samplerRECT tex, \n" "in float4 TexCoord0 : TEXCOORD0, in float4 TexCoord1 : TEXCOORD1, \n" "in float4 TexCoord2 : TEXCOORD2, in float4 TexCoord3 : TEXCOORD3,\n" "out float4 FragColor : COLOR0)\n" "{\n" " float4 data1 = texRECT(tex, TexCoord0.xy);\n" " float4 data2 = texRECT(tex, TexCoord1.xy);\n" " float4 data3 = texRECT(tex, TexCoord2.xy);\n" " float4 data4 = texRECT(tex, TexCoord3.xy);\n" " bool4 helper1 = (abs(data1.r), float4(1.0, 2.0, 3.0, 4.0)); \n" " bool4 helper2 = (abs(data2.r), float4(1.0, 2.0, 3.0, 4.0));\n" " bool4 helper3 = (abs(data3.r), float4(1.0, 2.0, 3.0, 4.0));\n" " bool4 helper4 = (abs(data4.r), float4(1.0, 2.0, 3.0, 4.0));\n" " bool4 bx1 = TexCoord0.xxyy < bbox; \n" " bool4 bx4 = TexCoord3.xxyy < bbox; \n" " bool4 bx2 = bool4(bx4.xy, bx1.zw); \n" " bool4 bx3 = bool4(bx1.xy, bx4.zw);\n" " helper1 = bx1.xyxy && bx1.zzww && helper1; \n" " helper2 = bx2.xyxy && bx2.zzww && helper2; \n" " helper3 = bx3.xyxy && bx3.zzww && helper3; \n" " helper4 = bx4.xyxy && bx4.zzww && helper4; \n" " FragColor.r = any(helper1.xy || helper1.zw); \n" " FragColor.g = any(helper2.xy || helper2.zw); \n" " FragColor.b = any(helper3.xy || helper3.zw); \n" " FragColor.a = any(helper4.xy || helper4.zw); \n" " if(dot(FragColor, float4(1,1,1,1)) == 1) \n" " {\n" " //use a special method if there is only one in the 16, \n" " float4 data, helper; bool4 bhelper; float2 pos, opos; \n" " if(FragColor.r){ \n" " data = data1; bhelper = helper1; pos = TexCoord0.xy;\n" " }else if(FragColor.g){\n" " data = data2; bhelper = helper2; pos = TexCoord1.xy;\n" " }else if(FragColor.b){\n" " data = data3; bhelper = helper3; pos = TexCoord2.xy;\n" " }else{\n" " data = data4; bhelper = helper4; pos = TexCoord3.xy;\n" " }\n" " helper = float4(bhelper); \n" " opos.x = dot(helper, float4(-0.5, 0.5, -0.5, 0.5));\n" " opos.y = dot(helper, float4(-0.5, -0.5, 0.5, 0.5));\n" " FragColor = float4(pos + pos + opos + data.yz, -1, data.w); \n" " }\n" "}"); _param_genlist_init_bbox = cgGetNamedParameter( *program, "bbox"); s_genlist_end = program = new ProgramCG( "void main( uniform samplerRECT tex, uniform samplerRECT ktex,\n" " in float4 tpos : TEXCOORD0, out float4 FragColor : COLOR0)\n" "{\n" " float4 tc = texRECT( tex, tpos.xy);\n" " float2 pos = tc.rg; float index = tc.b;\n" " if(index == -1)\n" " {\n" " FragColor = float4(tc.xy, 0, tc.w);\n" " }else\n" " {\n" " float4 tk = texRECT( ktex, pos); \n" " float4 keys = float4(abs(tk.r) == float4(1.0, 2.0, 3.0, 4.0)); \n" " float2 opos; \n" " opos.x = dot(keys, float4(-0.5, 0.5, -0.5, 0.5));\n" " opos.y = dot(keys, float4(-0.5, -0.5, 0.5, 0.5));\n" " FragColor = float4(opos + pos + pos + tk.yz, 0, tk.w);\n" " }\n" "}"); _param_genlist_end_ktex = cgGetNamedParameter(*program, "ktex"); //reduction ... s_genlist_histo = new ProgramCG( "void main (uniform samplerRECT tex, in float2 TexCoord0 : TEXCOORD0,\n" "in float2 TexCoord1 : TEXCOORD1, in float2 TexCoord2 : TEXCOORD2, \n" "in float2 TexCoord3 : TEXCOORD3, out float4 FragColor : COLOR0)\n" "{\n" " float4 helper[4]; float4 helper2; \n" " helper[0] = texRECT(tex, TexCoord0); helper2.xy = helper[0].xy + helper[0].zw; \n" " helper[1] = texRECT(tex, TexCoord1); helper2.zw = helper[1].xy + helper[1].zw; \n" " FragColor.rg = helper2.xz + helper2.yw;\n" " helper[2] = texRECT(tex, TexCoord2); helper2.xy = helper[2].xy + helper[2].zw; \n" " helper[3] = texRECT(tex, TexCoord3); helper2.zw = helper[3].xy + helper[3].zw; \n" " FragColor.ba= helper2.xz+helper2.yw;\n" " bool4 keyt = float4(helper[0].z, helper[1].z, helper[2].z, helper[3].z) == -1.0; \n" " float keyc = dot(float4(keyt), float4(1,1,1,1)); \n" " if(keyc == 1.0 && dot(FragColor, float4(1,1,1,1)) == -1.0) \n" " {\n" " if(keyt.x) FragColor = helper[0];\n" " else if(keyt.y) FragColor = helper[1]; \n" " else if(keyt.z) FragColor = helper[2]; \n" " else FragColor = helper[3]; \n" " }else\n" " {\n" " FragColor = keyt? float4(1,1,1,1) : FragColor;\n" " }\n" "}"); //read of the first part, which generates tex coordinates s_genlist_start= program = ShaderBagCG::LoadGenListStepShaderV2(1, 1); _param_ftex_width= cgGetNamedParameter(*program, "width"); _param_genlist_start_tex0 = cgGetNamedParameter(*program, "tex0"); //stepping s_genlist_step = program = ShaderBagCG::LoadGenListStepShaderV2(0, 1); _param_genlist_step_tex= cgGetNamedParameter(*program, "tex"); _param_genlist_step_tex0= cgGetNamedParameter(*program, "tex0"); } ProgramCG* ShaderBagCG::LoadGenListStepShaderV2(int start, int step) { int i; char buffer[10240]; //char chanels[5] = "rgba"; ostrstream out(buffer, 10240); out<<"void main(out float4 FragColor : COLOR0, \n"; for(i = 0; i < step; i++) out<<"uniform samplerRECT tex"<0) { out<<"float2 cpos = float2(-0.5, 0.5);\t float2 opos;\n"; for(i = 0; i < step; i++) { out<<"cc = texRECT(tex"< max(v1[i], v2[i]), test2 = cc[i] < min(v1[i], v2[i]);\n" " key[i] = cc[i] > THRESHOLD0 && all(test1.xy&&test1.zw)?1.0: 0.0;\n" " key[i] = cc[i] < -THRESHOLD0 && all(test2.xy&&test2.zw)? -1.0: key[i];\n" " }\n" " if(TexCC.x < 1.0) {key.rb = 0;}\n" " if(TexCC.y < 1.0) {key.rg = 0;}\n" " FragData0 = float4(0.0);\n" " if(all(key == 0.0)) return; \n"; //do edge supression first.. //vector v1 is < (-1, 0), (1, 0), (0,-1), (0, 1)> //vector v2 is < (-1,-1), (-1,1), (1,-1), (1, 1)> out<< " float fxx[4], fyy[4], fxy[4], fx[4], fy[4];\n" " for(int i = 0; i < 4; i++) \n" " {\n" " if(key[i] != 0)\n" " {\n" " float4 D2 = v1[i].xyzw - cc[i];\n" " float2 D4 = v2[i].xw - v2[i].yz;\n" " float2 D5 = 0.5*(v1[i].yw-v1[i].xz); \n" " fx[i] = D5.x;\n" " fy[i] = D5.y ;\n" " fxx[i] = D2.x + D2.y;\n" " fyy[i] = D2.z + D2.w;\n" " fxy[i] = 0.25*(D4.x + D4.y);\n" " float fxx_plus_fyy = fxx[i] + fyy[i];\n" " float score_up = fxx_plus_fyy*fxx_plus_fyy; \n" " float score_down = (fxx[i]*fyy[i] - fxy[i]*fxy[i]);\n" " if( score_down <= 0 || score_up > THRESHOLD2 * score_down)key[i] = 0;\n" " }\n" " }\n" " if(all(key == 0.0)) return; \n\n"; //////////////////////////////////////////////// //read 9 pixels of upper/lower level out<< " float4 v4[4], v5[4], v6[4];\n" " ccc = texRECT(texU, TexCC.xy);\n" " clc = texRECT(texU, TexLC.xy);\n" " crc = texRECT(texU, TexRC.xy);\n" " ccd = texRECT(texU, TexCD.xy);\n" " ccu = texRECT(texU, TexCU.xy);\n" " cld = texRECT(texU, TexLD.xy);\n" " clu = texRECT(texU, TexLU.xy);\n" " crd = texRECT(texU, TexRD.xy);\n" " cru = texRECT(texU, TexRU.xy);\n" " float4 cu = ccc;\n" " v4[0] = float4(clc.g, ccc.g, ccd.b, ccc.b);\n" " v4[1] = float4(ccc.r, crc.r, ccd.a, ccc.a);\n" " v4[2] = float4(clc.a, ccc.a, ccc.r, ccu.r);\n" " v4[3] = float4(ccc.b, crc.b, ccc.g, ccu.g);\n" " v6[0] = float4(cld.a, clc.a, ccd.a, ccc.a);\n" " v6[1] = float4(ccd.b, ccc.b, crd.b, crc.b);\n" " v6[2] = float4(clc.g, clu.g, ccc.g, ccu.g);\n" " v6[3] = float4(ccc.r, ccu.r, crc.r, cru.r);\n" << " for(int i = 0; i < 4; i++)\n" " {\n" " if(key[i] == 1.0)\n" " {\n" " bool4 test = cc[i]< max(v4[i], v6[i]); \n" " if(cc[i] < cu[i] || any(test.xy||test.zw))key[i] = 0.0; \n" " }else if(key[i] == -1.0)\n" " {\n" " bool4 test = cc[i]> min( v4[i], v6[i]); \n" " if(cc[i] > cu[i] || any(test.xy||test.zw))key[i] = 0.0; \n" " }\n" " }\n" " if(all(key == 0.0)) return; \n" << " ccc = texRECT(texD, TexCC.xy);\n" " clc = texRECT(texD, TexLC.xy);\n" " crc = texRECT(texD, TexRC.xy);\n" " ccd = texRECT(texD, TexCD.xy);\n" " ccu = texRECT(texD, TexCU.xy);\n" " cld = texRECT(texD, TexLD.xy);\n" " clu = texRECT(texD, TexLU.xy);\n" " crd = texRECT(texD, TexRD.xy);\n" " cru = texRECT(texD, TexRU.xy);\n" " float4 cd = ccc;\n" " v5[0] = float4(clc.g, ccc.g, ccd.b, ccc.b);\n" " v5[1] = float4(ccc.r, crc.r, ccd.a, ccc.a);\n" " v5[2] = float4(clc.a, ccc.a, ccc.r, ccu.r);\n" " v5[3] = float4(ccc.b, crc.b, ccc.g, ccu.g);\n" " v6[0] = float4(cld.a, clc.a, ccd.a, ccc.a);\n" " v6[1] = float4(ccd.b, ccc.b, crd.b, crc.b);\n" " v6[2] = float4(clc.g, clu.g, ccc.g, ccu.g);\n" " v6[3] = float4(ccc.r, ccu.r, crc.r, cru.r);\n" << " for(int i = 0; i < 4; i++)\n" " {\n" " if(key[i] == 1.0)\n" " {\n" " bool4 test = cc[i]< max(v5[i], v6[i]);\n" " if(cc[i] < cd[i] || any(test.xy||test.zw))key[i] = 0.0; \n" " }else if(key[i] == -1.0)\n" " {\n" " bool4 test = cc[i]>min(v5[i],v6[i]);\n" " if(cc[i] > cd[i] || any(test.xy||test.zw))key[i] = 0.0; \n" " }\n" " }\n" " float keysum = dot(abs(key), float4(1, 1, 1, 1)) ;\n" " //assume there is only one keypoint in the four. \n" " if(keysum != 1.0) return; \n"; ////////////////////////////////////////////////////////////////////// if(GlobalUtil::_SubpixelLocalization) out << " float3 offset = float3(0, 0, 0); \n" " /*The unrolled follwing loop is faster than a dynamic indexing version.*/\n" " for(int idx = 1; idx < 4; idx++)\n" " {\n" " if(key[idx] != 0) \n" " {\n" " cu[0] = cu[idx]; cd[0] = cd[idx]; cc[0] = cc[idx]; \n" " v4[0] = v4[idx]; v5[0] = v5[idx]; \n" " fxy[0] = fxy[idx]; fxx[0] = fxx[idx]; fyy[0] = fyy[idx]; \n" " fx[0] = fx[idx]; fy[0] = fy[idx]; \n" " }\n" " }\n" << " float fs = 0.5*( cu[0] - cd[0] ); \n" " float fss = cu[0] + cd[0] - cc[0] - cc[0];\n" " float fxs = 0.25 * (v4[0].y + v5[0].x - v4[0].x - v5[0].y);\n" " float fys = 0.25 * (v4[0].w + v5[0].z - v4[0].z - v5[0].w);\n" " float4 A0, A1, A2 ; \n" " A0 = float4(fxx[0], fxy[0], fxs, -fx[0]); \n" " A1 = float4(fxy[0], fyy[0], fys, -fy[0]); \n" " A2 = float4(fxs, fys, fss, -fs); \n" " float3 x3 = abs(float3(fxx[0], fxy[0], fxs)); \n" " float maxa = max(max(x3.x, x3.y), x3.z); \n" " if(maxa >= 1e-10 ) \n" " { \n" " if(x3.y ==maxa ) \n" " { \n" " float4 TEMP = A1; A1 = A0; A0 = TEMP; \n" " }else if( x3.z == maxa ) \n" " { \n" " float4 TEMP = A2; A2 = A0; A0 = TEMP; \n" " } \n" " A0 /= A0.x; \n" " A1 -= A1.x * A0; \n" " A2 -= A2.x * A0; \n" " float2 x2 = abs(float2(A1.y, A2.y)); \n" " if( x2.y > x2.x ) \n" " { \n" " float3 TEMP = A2.yzw; \n" " A2.yzw = A1.yzw; \n" " A1.yzw = TEMP; \n" " x2.x = x2.y; \n" " } \n" " if(x2.x >= 1e-10) { \n" " A1.yzw /= A1.y; \n" " A2.yzw -= A2.y * A1.yzw; \n" " if(abs(A2.z) >= 1e-10) {\n" " offset.z = A2.w /A2.z; \n" " offset.y = A1.w - offset.z*A1.z; \n" " offset.x = A0.w - offset.z*A0.z - offset.y*A0.y; \n" " bool test = (abs(cc[0] + 0.5*dot(float3(fx[0], fy[0], fs), offset ))>THRESHOLD1) ;\n" " if(!test || any( abs(offset) >= 1.0)) return;\n" " }\n" " }\n" " }\n" <<"\n" " float keyv = dot(key, float4(1.0, 2.0, 3.0, 4.0));\n" " FragData0 = float4(keyv, offset);\n" "}\n" <<'\0'; else out << "\n" " float keyv = dot(key, float4(1.0, 2.0, 3.0, 4.0));\n" " FragData0 = float4(keyv, 0, 0, 0);\n" "}\n" <<'\0'; s_keypoint = program = new ProgramCG(buffer); //parameter _param_dog_texu = cgGetNamedParameter(*program, "texU"); _param_dog_texd = cgGetNamedParameter(*program, "texD"); } void ShaderBagPKCG::LoadOrientationShader() { char buffer[10240]; ostrstream out(buffer,10240); out<<"\n" "#define GAUSSIAN_WF "<1 && GlobalUtil::_OrientationPack2 == 0) out<<", out float4 OrientationData : COLOR1"; //use 9 float4 to store histogram of 36 directions out<<") \n" "{ \n" " float4 bins[10]; \n" " for (int i=0; i<9; i++) bins[i] = float4(0,0,0,0); \n" " float4 sift = texRECT(tex, TexCoord0); \n" " float2 pos = sift.xy; \n" " bool orientation_mode = (size.z != 0); \n" " float sigma = orientation_mode? (abs(size.z) * pow(size.w, sift.w) * sift.z) : (sift.w); \n" " //bool fixed_orientation = (size.z < 0); \n" " if(size.z < 0) {FeatureData = float4(pos, 0, sigma); return;}" " float gsigma = sigma * GAUSSIAN_WF; \n" " float2 win = abs(sigma.xx) * (SAMPLE_WF * GAUSSIAN_WF); \n" " float2 dim = size.xy; \n" " float4 dist_threshold = float4(win.x*win.x+0.5); \n" " float factor = -0.5/(gsigma*gsigma); \n" " float4 sz; float2 spos; \n" " //if(any(pos.xy <= 1)) discard; \n" " sz.xy = max( pos - win, float2(2,2)); \n" " sz.zw = min( pos + win, dim-3); \n" " sz = floor(sz*0.5) + 0.5; "; //loop to get the histogram out<<"\n" " for(spos.y = sz.y; spos.y <= sz.w; spos.y+=1.0) \n" " { \n" " for(spos.x = sz.x; spos.x <= sz.z; spos.x+=1.0) \n" " { \n" " float2 offset = 2* spos - pos - 0.5; \n" " float4 off = float4(offset, offset + 1); \n" " float4 distsq = off.xzxz * off.xzxz + off.yyww * off.yyww; \n" " bool4 inside = distsq < dist_threshold; \n" " if(any(inside.xy||inside.zw)) \n" " { \n" " float4 gg = texRECT(gtex, spos); \n" " float4 oo = texRECT(otex, spos); \n" " float4 weight = gg * exp(distsq * factor); \n" " float4 idxv = floor(degrees(oo)*0.1); \n" " idxv = idxv<0? idxv + 36.0: idxv; \n" " float4 vidx = 4.0* fract(idxv * 0.25);//fmod(idxv, 4.0);\n"; // if(GlobalUtil::_UseDynamicIndexing && strcmp(cgGetProfileString(ProgramCG::_FProfile), "gp4fp")==0) //if(ProgramCG::_FProfile == CG_PROFILE_GPU_FP) this enumerant is not defined in cg1.5 { //gp4fp supports dynamic indexing, but it might be slow on some GPUs out<<"\n" " for(int i = 0 ; i < 4; i++)\n" " {\n" " if(inside[i])\n" " {\n" " float idx = idxv[i]; \n" " float4 inc = weight[i] * float4(vidx[i] == float4(0,1,2,3)); \n" " int iidx = int(floor(idx*0.25)); \n" " bins[iidx]+=inc; \n" " } \n" " } \n" " } \n" " } \n" " }"; }else { //nvfp40 still does not support dynamic array indexing //unrolled binary search //it seems to be faster than the dyanmic indexing version on some GPUs out<<"\n" " for(int i = 0 ; i < 4; i++)\n" " {\n" " if(inside[i])\n" " {\n" " float idx = idxv[i]; \n" " float4 inc = weight[i] * float4(vidx[i] == float4(0,1,2,3)); \n" " if(idx < 16) \n" " { \n" " if(idx < 8) \n" " { \n" " if(idx < 4) { bins[0]+=inc;} \n" " else { bins[1]+=inc;} \n" " }else \n" " { \n" " if(idx < 12){ bins[2]+=inc;} \n" " else { bins[3]+=inc;} \n" " } \n" " }else if(idx < 32) \n" " { \n" " if(idx < 24) \n" " { \n" " if(idx <20) { bins[4]+=inc;} \n" " else { bins[5]+=inc;} \n" " }else \n" " { \n" " if(idx < 28){ bins[6]+=inc;} \n" " else { bins[7]+=inc;} \n" " } \n" " }else \n" " { \n" " bins[8]+=inc; \n" " } \n" " } \n" " } \n" " } \n" " } \n" " }"; } //reuse the code from the unpacked version.. ShaderBagCG::WriteOrientationCodeToStream(out); ProgramCG * program; s_orientation = program = new ProgramCG(buffer); _param_orientation_gtex = cgGetNamedParameter(*program, "gtex"); _param_orientation_otex = cgGetNamedParameter(*program, "otex"); _param_orientation_size = cgGetNamedParameter(*program, "size"); } void ShaderBagPKCG::LoadDescriptorShader() { GlobalUtil::_DescriptorPPT = 16; LoadDescriptorShaderF2(); } void ShaderBagPKCG::LoadDescriptorShaderF2() { //one shader outpout 128/8 = 16 , each fragout encodes 4 //const double twopi = 2.0*3.14159265358979323846; //const double rpi = 8.0/twopi; char buffer[10240]; ostrstream out(buffer, 10240); out<=dim-1)) " " //discard; \n" " { FragData0 = FragData1 = float4(0.0); return; }\n" " float anglef = texRECT(tex, coord).z;\n" " if(anglef > M_PI) anglef -= TWO_PI;\n" " float sigma = texRECT(tex, coord).w; \n" " float spt = abs(sigma * WF); //default to be 3*sigma \n"; //rotation out<< " float4 cscs, rots; \n" " sincos(anglef, cscs.y, cscs.x); \n" " cscs.zw = - cscs.xy; \n" " rots = cscs /spt; \n" " cscs *= spt; \n"; //here cscs is actually (cos, sin, -cos, -sin) * (factor: 3)*sigma //and rots is (cos, sin, -cos, -sin ) /(factor*sigma) //devide the 4x4 sift grid into 16 1x1 block, and each corresponds to a shader thread //To use linear interoplation, 1x1 is increased to 2x2, by adding 0.5 to each side out<< " float4 temp; float2 pt, offsetpt; \n" " /*the fraction part of idx is .5*/ \n" " offsetpt.x = 4.0 * fract(idx * 0.25) - 2.0; \n" " offsetpt.y = floor(idx*0.25) - 1.5; \n" " temp = cscs.xwyx*offsetpt.xyxy; \n" " pt = pos + temp.xz + temp.yw; \n"; //get a horizontal bounding box of the rotated rectangle out<< " float2 bwin = abs(cscs.xy); \n" " float bsz = bwin.x + bwin.y; \n" " float4 sz; float2 spos; \n" " sz.xy = max(pt - bsz, float2(2,2));\n" " sz.zw = min(pt + bsz, dim - 3); \n" " sz = floor(sz * 0.5) + 0.5;"; //move sample point to pixel center //get voting for two box out<<"\n" " float4 DA, DB; \n" " DA = DB = float4(0, 0, 0, 0); \n" " float4 nox = float4(0, rots.xy, rots.x + rots.y); \n" " float4 noy = float4(0, rots.wx, rots.w + rots.x); \n" " for(spos.y = sz.y; spos.y <= sz.w; spos.y+=1.0) \n" " { \n" " for(spos.x = sz.x; spos.x <= sz.z; spos.x+=1.0) \n" " { \n" " float2 tpt = spos * 2.0 - pt - 0.5; \n" " float4 temp = rots.xywx * tpt.xyxy; \n" " float2 temp2 = temp.xz + temp.yw; \n" " float4 nx = temp2.x + nox; \n" " float4 ny = temp2.y + noy; \n" " float4 nxn = abs(nx), nyn = abs(ny); \n" " bool4 inside = (max(nxn, nyn) < 1.0); \n" " if(any(inside.xy || inside.zw))\n" " {\n" " float4 gg = texRECT(gtex, spos);\n" " float4 oo = texRECT(otex, spos);\n" " float4 theta0 = (anglef - oo)*RPI;\n" " float4 theta = theta0 < 0? theta0 + 8.0 : theta0;//8.0 * frac(1.0 + 0.125 * theta0);// \n" " float4 theta1 = floor(theta); \n" " float4 diffx = nx + offsetpt.x, diffy = ny + offsetpt.y; \n" " float4 ww = exp(-0.125 * (diffx * diffx + diffy * diffy )); \n" " float4 weight = (1 - nxn) * (1 - nyn) * gg * ww; \n" " float4 weight2 = (theta - theta1) * weight; \n" " float4 weight1 = weight - weight2; \n" " for(int i = 0;i < 4; i++)\n" " {\n" " if(inside[i])\n" " {\n" " DA += float4(theta1[i] == float4(0, 1, 2, 3))*weight1[i]; \n" " DA += float4(theta1[i] == float4(7, 0, 1, 2))*weight2[i]; \n" " DB += float4(theta1[i] == float4(4, 5, 6, 7))*weight1[i]; \n" " DB += float4(theta1[i] == float4(3, 4, 5, 6))*weight2[i]; \n" " }\n" " }\n" " }\n" " }\n" " }\n"; out<< " FragData0 = DA; FragData1 = DB;\n" "}\n"<<'\0'; ProgramCG * program; s_descriptor_fp = program = new ProgramCG(buffer); _param_descriptor_gtex = cgGetNamedParameter(*program, "gtex"); _param_descriptor_otex = cgGetNamedParameter(*program, "otex"); _param_descriptor_size = cgGetNamedParameter(*program, "size"); _param_descriptor_dsize = cgGetNamedParameter(*program, "dsize"); } void ShaderBagPKCG::SetMarginCopyParam(int xmax, int ymax) { float truncate[4]; truncate[0] = (xmax - 0.5f) * 0.5f; //((xmax + 1) >> 1) - 0.5f; truncate[1] = (ymax - 0.5f) * 0.5f; //((ymax + 1) >> 1) - 0.5f; truncate[2] = (xmax %2 == 1)? 0.0f: 1.0f; truncate[3] = truncate[2] + (((ymax % 2) == 1)? 0.0f : 2.0f); cgGLSetParameter4fv(_param_margin_copy_truncate, truncate); } void ShaderBagPKCG::SetGradPassParam(int texP) { cgGLSetTextureParameter(_param_grad_pass_texp, texP); cgGLEnableTextureParameter(_param_grad_pass_texp); } void ShaderBagPKCG::SetGenListEndParam(int ktex) { cgGLSetTextureParameter(_param_genlist_end_ktex, ktex); cgGLEnableTextureParameter(_param_genlist_end_ktex); } void ShaderBagPKCG::SetDogTexParam(int texU, int texD) { cgGLSetTextureParameter(_param_dog_texu, texU); cgGLEnableTextureParameter(_param_dog_texu); cgGLSetTextureParameter(_param_dog_texd, texD); cgGLEnableTextureParameter(_param_dog_texd); } void ShaderBagPKCG::SetGenListInitParam(int w, int h) { float bbox[4] = {(w -1.0f) * 0.5f +0.25f, (w-1.0f) * 0.5f - 0.25f, (h - 1.0f) * 0.5f + 0.25f, (h-1.0f) * 0.5f - 0.25f}; cgGLSetParameter4fv(_param_genlist_init_bbox, bbox); } void ShaderBagPKCG::SetGenListStartParam(float width, int tex0) { cgGLSetParameter1f(_param_ftex_width, width); if(_param_genlist_start_tex0) { cgGLSetTextureParameter(_param_genlist_start_tex0, tex0); cgGLEnableTextureParameter(_param_genlist_start_tex0); } } void ShaderBagPKCG::SetGenListStepParam(int tex, int tex0) { cgGLSetTextureParameter(_param_genlist_step_tex, tex); cgGLEnableTextureParameter(_param_genlist_step_tex); cgGLSetTextureParameter(_param_genlist_step_tex0, tex0); cgGLEnableTextureParameter(_param_genlist_step_tex0); } void ShaderBagPKCG::SetGenVBOParam(float width, float fwidth, float size) { float sizes[4] = {size*3.0f, fwidth, width, 1.0f/width}; cgGLSetParameter4fv(_param_genvbo_size, sizes); } void ShaderBagPKCG::SetSimpleOrientationInput(int oTex, float sigma, float sigma_step) { cgGLSetTextureParameter(_param_orientation_gtex, oTex); cgGLEnableTextureParameter(_param_orientation_gtex); cgGLSetParameter2f(_param_orientation_size, sigma, sigma_step); } void ShaderBagPKCG::SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int otex, float step) { /// cgGLSetTextureParameter(_param_orientation_gtex, gtex); cgGLEnableTextureParameter(_param_orientation_gtex); cgGLSetTextureParameter(_param_orientation_otex, otex); cgGLEnableTextureParameter(_param_orientation_otex); float size[4]; size[0] = (float)width; size[1] = (float)height; size[2] = sigma; size[3] = step; cgGLSetParameter4fv(_param_orientation_size, size); } void ShaderBagPKCG::SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma) { /// cgGLSetTextureParameter(_param_descriptor_gtex, gtex); cgGLEnableTextureParameter(_param_descriptor_gtex); cgGLSetTextureParameter(_param_descriptor_otex, otex); cgGLEnableTextureParameter(_param_descriptor_otex); float dsize[4] ={dwidth, 1.0f/dwidth, fwidth, 1.0f/fwidth}; cgGLSetParameter4fv(_param_descriptor_dsize, dsize); float size[3]; size[0] = width; size[1] = height; size[2] = GlobalUtil::_DescriptorWindowFactor; cgGLSetParameter3fv(_param_descriptor_size, size); } #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramCG.h000066400000000000000000000135131454702036400212430ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ProgramCG.h // Author: Changchang Wu // Description : interface for the ProgramCG classes. // ProgramCG: Cg programs // ShaderBagCG: All Cg shaders for Sift in a bag // FilterGLCG: Cg Gaussian Filters // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CG_SIFTGPU_ENABLED) #ifndef _PROGRAM_CG_H #define _PROGRAM_CG_H #include "ProgramGPU.h" class FragmentProgram; #include "Cg/cgGL.h" class ProgramCG:public ProgramGPU { CGprogram _programID; CGprofile _profile; int _valid; public: static CGcontext _Context; static CGprofile _FProfile; public: operator CGprogram (){return _programID;} CGprogram GetProgramID(){return _programID;} int UseProgram(); int IsValidProgram(){return _programID && _valid;} static void ErrorCallback(); static void InitContext(); static void DestroyContext(); ProgramCG(const char * code, const char** cg_compile_args= NULL, CGprofile profile = ProgramCG::_FProfile); ProgramCG(); virtual ~ProgramCG(); }; class ShaderBagCG:public ShaderBag { CGparameter _param_dog_texu; CGparameter _param_dog_texd; CGparameter _param_genlist_start_tex0; CGparameter _param_ftex_width; CGparameter _param_genlist_step_tex; CGparameter _param_genlist_step_tex0; CGparameter _param_genvbo_size; CGparameter _param_orientation_gtex; CGparameter _param_orientation_stex; CGparameter _param_orientation_size; CGparameter _param_descriptor_gtex; CGparameter _param_descriptor_size; CGparameter _param_descriptor_dsize; CGparameter _param_margin_copy_truncate; CGparameter _param_genlist_init_bbox; public: virtual void LoadDescriptorShader(); void LoadDescriptorShaderF2(); static void WriteOrientationCodeToStream(ostream& out); virtual void SetGenListInitParam(int w, int h); virtual void SetMarginCopyParam(int xmax, int ymax); virtual void SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex = 0, float step = 1.0f); virtual void SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma); virtual void SetSimpleOrientationInput(int oTex, float sigma, float sigma_step); void LoadOrientationShader(); virtual void SetGenListStartParam(float width, int tex0); static ProgramCG* LoadGenListStepShader(int start, int step); static ProgramCG* LoadGenListStepShaderV2(int start, int step); void LoadGenListShader(int ndoglev, int nlev); virtual void UnloadProgram(); virtual void SetDogTexParam(int texU, int texD); virtual void SetGenListStepParam(int tex, int tex0); virtual void SetGenVBOParam( float width, float fwidth, float size); virtual void LoadFixedShaders(); virtual void LoadDisplayShaders(); virtual void LoadKeypointShader(float threshold, float edgeThreshold); virtual int LoadKeypointShaderMR(float threshold, float edgeThreshold); ShaderBagCG(); virtual ~ShaderBagCG(){} }; class FilterGLCG : public FilterProgram { private: ProgramGPU* CreateFilterH(float kernel[], float offset[], int width); ProgramGPU* CreateFilterV(float kernel[], float offset[], int height); //packed version ProgramGPU* CreateFilterHPK(float kernel[], float offset[], int width); ProgramGPU* CreateFilterVPK(float kernel[], float offset[], int height); }; class ShaderBagPKCG:public ShaderBag { private: CGparameter _param_dog_texu; CGparameter _param_dog_texd; CGparameter _param_margin_copy_truncate; CGparameter _param_grad_pass_texp; CGparameter _param_genlist_init_bbox; CGparameter _param_genlist_start_tex0; CGparameter _param_ftex_width; CGparameter _param_genlist_step_tex; CGparameter _param_genlist_step_tex0; CGparameter _param_genlist_end_ktex; CGparameter _param_genvbo_size; CGparameter _param_orientation_gtex; CGparameter _param_orientation_otex; CGparameter _param_orientation_size; CGparameter _param_descriptor_gtex; CGparameter _param_descriptor_otex; CGparameter _param_descriptor_size; CGparameter _param_descriptor_dsize; public: ShaderBagPKCG(); virtual ~ShaderBagPKCG(){} virtual void LoadDescriptorShader(); virtual void LoadDescriptorShaderF2(); virtual void LoadOrientationShader(); virtual void LoadGenListShader(int ndoglev, int nlev); virtual void LoadGenListShaderV2(int ndoglev, int nlev); virtual void UnloadProgram() ; virtual void LoadKeypointShader(float threshold, float edgeTrheshold); virtual void LoadFixedShaders(); virtual void LoadDisplayShaders(); virtual void SetGradPassParam(int texP); virtual void SetGenListEndParam(int ktex); public: //parameters virtual void SetGenListStartParam(float width, int tex0); virtual void SetGenListInitParam(int w, int h); virtual void SetMarginCopyParam(int xmax, int ymax); virtual void SetDogTexParam(int texU, int texD); virtual void SetGenListStepParam(int tex, int tex0); virtual void SetGenVBOParam( float width, float fwidth, float size); virtual void SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma); virtual void SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex, float step); virtual void SetSimpleOrientationInput(int oTex, float sigma, float sigma_step); }; #endif #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramCL.cpp000066400000000000000000001760071454702036400216130ustar00rootroot00000000000000////////////////////////////////////////////////////////////////////////////// // File: ProgramCL.cpp // Author: Changchang Wu // Description : implementation of CL related class. // class ProgramCL A simple wrapper of Cg programs // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CL_SIFTGPU_ENABLED) #include #include #include #include #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "CLTexImage.h" #include "ProgramCL.h" #include "SiftGPU.h" #if defined(_WIN32) #pragma comment (lib, "OpenCL.lib") #endif #ifndef _INC_WINDOWS #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #endif ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// ProgramCL::ProgramCL() { _program = NULL; _kernel = NULL; _valid = 0; } ProgramCL::~ProgramCL() { if(_kernel) clReleaseKernel(_kernel); if(_program) clReleaseProgram(_program); } ProgramCL::ProgramCL(const char* name, const char * code, cl_context context, cl_device_id device) : _valid(1) { const char * src[1] = {code}; cl_int status; _program = clCreateProgramWithSource(context, 1, src, NULL, &status); if(status != CL_SUCCESS) _valid = 0; status = clBuildProgram(_program, 0, NULL, GlobalUtil::_debug ? "-cl-fast-relaxed-math -cl-single-precision-constant -cl-nv-verbose" : "-cl-fast-relaxed-math -cl-single-precision-constant", NULL, NULL); if(status != CL_SUCCESS) {PrintBuildLog(device, 1); _valid = 0;} else if(GlobalUtil::_debug) PrintBuildLog(device, 0); _kernel = clCreateKernel(_program, name, &status); if(status != CL_SUCCESS) _valid = 0; } void ProgramCL::PrintBuildLog(cl_device_id device, int all) { char buffer[10240] = "\0"; cl_int status = clGetProgramBuildInfo( _program, device, CL_PROGRAM_BUILD_LOG, sizeof(buffer), buffer, NULL); if(all ) { std::cerr << buffer << endl; }else { const char * pos = strstr(buffer, "ptxas"); if(pos) std::cerr << pos << endl; } } /////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////PACKED VERSION?/////////////////////////////////// ProgramBagCL::ProgramBagCL() { //////////////////////////////////// _context = NULL; _queue = NULL; s_gray = s_sampling = NULL; s_packup = s_zero_pass = NULL; s_gray_pack = s_unpack = NULL; s_sampling_u = NULL; s_dog_pass = NULL; s_grad_pass = NULL; s_grad_pass2 = NULL; s_unpack_dog = NULL; s_unpack_grd = NULL; s_unpack_key = NULL; s_keypoint = NULL; f_gaussian_skip0 = NULL; f_gaussian_skip1 = NULL; f_gaussian_step = 0; //////////////////////////////// GlobalUtil::StartTimer("Initialize OpenCL"); if(!InitializeContext()) return; GlobalUtil::StopTimer(); } ProgramBagCL::~ProgramBagCL() { if(s_gray) delete s_gray; if(s_sampling) delete s_sampling; if(s_zero_pass) delete s_zero_pass; if(s_packup) delete s_packup; if(s_unpack) delete s_unpack; if(s_gray_pack) delete s_gray_pack; if(s_sampling_u) delete s_sampling_u; if(s_dog_pass) delete s_dog_pass; if(s_grad_pass) delete s_grad_pass; if(s_grad_pass2) delete s_grad_pass2; if(s_unpack_dog) delete s_unpack_dog; if(s_unpack_grd) delete s_unpack_grd; if(s_unpack_key) delete s_unpack_key; if(s_keypoint) delete s_keypoint; if(f_gaussian_skip1) delete f_gaussian_skip1; for(unsigned int i = 0; i < f_gaussian_skip0_v.size(); i++) { if(f_gaussian_skip0_v[i]) delete f_gaussian_skip0_v[i]; } if(f_gaussian_step && _gaussian_step_num > 0) { for(int i = 0; i< _gaussian_step_num; i++) { delete f_gaussian_step[i]; } delete[] f_gaussian_step; } ////////////////////////////////////// if(_context) clReleaseContext(_context); if(_queue) clReleaseCommandQueue(_queue); } bool ProgramBagCL::InitializeContext() { cl_uint num_platform, num_device; cl_int status; // Get OpenCL platform count status = clGetPlatformIDs (0, NULL, &num_platform); if (status != CL_SUCCESS || num_platform == 0) return false; cl_platform_id platforms[16]; if(num_platform > 16 ) num_platform = 16; status = clGetPlatformIDs (num_platform, platforms, NULL); _platform = platforms[0]; /////////////////////////////// status = clGetDeviceIDs(_platform, CL_DEVICE_TYPE_GPU, 0, NULL, &num_device); if(status != CL_SUCCESS || num_device == 0) return false; // Create the device list cl_device_id* devices = new cl_device_id [num_device]; status = clGetDeviceIDs(_platform, CL_DEVICE_TYPE_GPU, num_device, devices, NULL); _device = (status == CL_SUCCESS? devices[0] : 0); delete[] devices; if(status != CL_SUCCESS) return false; if(GlobalUtil::_verbose) { cl_device_mem_cache_type is_gcache; clGetDeviceInfo(_device, CL_DEVICE_GLOBAL_MEM_CACHE_TYPE, sizeof(is_gcache), &is_gcache, NULL); if(is_gcache == CL_NONE) std::cout << "No cache for global memory\n"; //else if(is_gcache == CL_READ_ONLY_CACHE) std::cout << "Read only cache for global memory\n"; //else std::cout << "Read/Write cache for global memory\n"; } //context; if(GlobalUtil::_UseSiftGPUEX) { cl_context_properties prop[] = { CL_CONTEXT_PLATFORM, (cl_context_properties)_platform, CL_GL_CONTEXT_KHR, (cl_context_properties)wglGetCurrentContext(), CL_WGL_HDC_KHR, (cl_context_properties)wglGetCurrentDC(), 0 }; _context = clCreateContext(prop, 1, &_device, NULL, NULL, &status); if(status != CL_SUCCESS) return false; }else { _context = clCreateContext(0, 1, &_device, NULL, NULL, &status); if(status != CL_SUCCESS) return false; } //command queue _queue = clCreateCommandQueue(_context, _device, 0, &status); return status == CL_SUCCESS; } void ProgramBagCL::InitProgramBag(SiftParam¶m) { GlobalUtil::StartTimer("Load Programs"); LoadFixedShaders(); LoadDynamicShaders(param); if(GlobalUtil::_UseSiftGPUEX) LoadDisplayShaders(); GlobalUtil::StopTimer(); } void ProgramBagCL::UnloadProgram() { } void ProgramBagCL::FinishCL() { clFinish(_queue); } void ProgramBagCL::LoadFixedShaders() { s_gray = new ProgramCL( "gray", "__kernel void gray(__read_only image2d_t input, __write_only image2d_t output) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int2 coord = (int2)(get_global_id(0), get_global_id(1));\n" "float4 weight = (float4)(0.299, 0.587, 0.114, 0.0);\n" "float intensity = dot(weight, read_imagef(input,sampler, coord ));\n" "float4 result= (float4)(intensity, intensity, intensity, 1.0);\n" "write_imagef(output, coord, result); }", _context, _device ); s_sampling = new ProgramCL("sampling", "__kernel void sampling(__read_only image2d_t input, __write_only image2d_t output,\n" " int width, int height) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if( x >= width || y >= height) return;\n" "int xa = x + x, ya = y + y; \n" "int xb = xa + 1, yb = ya + 1; \n" "float v1 = read_imagef(input, sampler, (int2) (xa, ya)).x; \n" "float v2 = read_imagef(input, sampler, (int2) (xb, ya)).x; \n" "float v3 = read_imagef(input, sampler, (int2) (xa, yb)).x; \n" "float v4 = read_imagef(input, sampler, (int2) (xb, yb)).x; \n" "float4 result = (float4) (v1, v2, v3, v4);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_sampling_k = new ProgramCL("sampling_k", "__kernel void sampling_k(__read_only image2d_t input, __write_only image2d_t output, " " int width, int height,\n" " int step, int halfstep) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if( x >= width || y >= height) return;\n" "int xa = x * step, ya = y *step; \n" "int xb = xa + halfstep, yb = ya + halfstep; \n" "float v1 = read_imagef(input, sampler, (int2) (xa, ya)).x; \n" "float v2 = read_imagef(input, sampler, (int2) (xb, ya)).x; \n" "float v3 = read_imagef(input, sampler, (int2) (xa, yb)).x; \n" "float v4 = read_imagef(input, sampler, (int2) (xb, yb)).x; \n" "float4 result = (float4) (v1, v2, v3, v4);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_sampling_u = new ProgramCL("sampling_u", "__kernel void sampling_u(__read_only image2d_t input, \n" " __write_only image2d_t output,\n" " int width, int height,\n" " float step, float halfstep) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_LINEAR;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if( x >= width || y >= height) return;\n" "float xa = x * step, ya = y *step; \n" "float xb = xa + halfstep, yb = ya + halfstep; \n" "float v1 = read_imagef(input, sampler, (float2) (xa, ya)).x; \n" "float v2 = read_imagef(input, sampler, (float2) (xb, ya)).x; \n" "float v3 = read_imagef(input, sampler, (float2) (xa, yb)).x; \n" "float v4 = read_imagef(input, sampler, (float2) (xb, yb)).x; \n" "float4 result = (float4) (v1, v2, v3, v4);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_zero_pass = new ProgramCL("zero_pass", "__kernel void zero_pass(__write_only image2d_t output){\n" "int2 coord = (int2)(get_global_id(0), get_global_id(1));\n" "write_imagef(output, coord, (float4)(0.0));}", _context, _device); s_packup = new ProgramCL("packup", "__kernel void packup(__global float* input, __write_only image2d_t output,\n" " int twidth, int theight, int width){\n" "int2 coord = (int2)(get_global_id(0), get_global_id(1));\n" "if(coord.x >= twidth || coord.y >= theight) return;\n" "int index0 = (coord.y + coord.y) * width; \n" "int index1 = index0 + coord.x;\n" "int x2 = min(width -1, coord.x); \n" "float v1 = input[index1 + coord.x], v2 = input[index1 + x2]; \n" "int index2 = index1 + width; \n" "float v3 = input[index2 + coord.x], v4 = input[index2 + x2]; \n " "write_imagef(output, coord, (float4) (v1, v2, v3, v4));}", _context, _device); s_dog_pass = new ProgramCL("dog_pass", "__kernel void dog_pass(__read_only image2d_t tex, __read_only image2d_t texp,\n" " __write_only image2d_t dog, int width, int height) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE |\n" " CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int2 coord = (int2)(get_global_id(0), get_global_id(1)); \n" "if( coord.x >= width || coord.y >= height) return;\n" "float4 cc = read_imagef(tex , sampler, coord); \n" "float4 cp = read_imagef(texp, sampler, coord);\n" "write_imagef(dog, coord, cc - cp); }\n", _context, _device); s_grad_pass = new ProgramCL("grad_pass", "__kernel void grad_pass(__read_only image2d_t tex, __read_only image2d_t texp,\n" " __write_only image2d_t dog, int width, int height,\n" " __write_only image2d_t grad, __write_only image2d_t rot) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE |\n" " CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if( x >= width || y >= height) return;\n" "int2 coord = (int2) (x, y);\n" "float4 cc = read_imagef(tex , sampler, coord); \n" "float4 cp = read_imagef(texp, sampler, coord);\n" "float2 cl = read_imagef(tex, sampler, (int2)(x - 1, y)).yw;\n" "float2 cr = read_imagef(tex, sampler, (int2)(x + 1, y)).xz;\n" "float2 cd = read_imagef(tex, sampler, (int2)(x, y - 1)).zw;\n" "float2 cu = read_imagef(tex, sampler, (int2)(x, y + 1)).xy;\n" "write_imagef(dog, coord, cc - cp); \n" "float4 dx = (float4)(cc.y - cl.x, cr.x - cc.x, cc.w - cl.y, cr.y - cc.z);\n" "float4 dy = (float4)(cc.zw - cd.xy, cu.xy - cc.xy);\n" "write_imagef(grad, coord, 0.5 * sqrt(dx*dx + dy * dy));\n" "write_imagef(rot, coord, atan2(dy, dx + (float4) (FLT_MIN)));}\n", _context, _device); s_grad_pass2 = new ProgramCL("grad_pass2", "#define BLOCK_DIMX 32\n" "#define BLOCK_DIMY 14\n" "#define BLOCK_SIZE (BLOCK_DIMX * BLOCK_DIMY)\n" "__kernel void grad_pass2(__read_only image2d_t tex, __read_only image2d_t texp,\n" " __write_only image2d_t dog, int width, int height,\n" " __write_only image2d_t grd, __write_only image2d_t rot){\n"//, __local float* block) {\n" "__local float block[BLOCK_SIZE * 4]; \n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE |\n" " CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int2 coord = (int2) ( get_global_id(0) - get_group_id(0) * 2 - 1, \n" " get_global_id(1) - get_group_id(1) * 2- 1); \n" "int idx = mad24(get_local_id(1), BLOCK_DIMX, get_local_id(0));\n" "float4 cc = read_imagef(tex, sampler, coord);\n" "block[idx ] = cc.x;\n" "block[idx + BLOCK_SIZE ] = cc.y;\n" "block[idx + BLOCK_SIZE * 2] = cc.z;\n" "block[idx + BLOCK_SIZE * 3] = cc.w;\n" "barrier(CLK_LOCAL_MEM_FENCE);\n" "if( get_local_id(0) == 0 || get_local_id(0) == BLOCK_DIMX - 1) return;\n" "if( get_local_id(1) == 0 || get_local_id(1) == BLOCK_DIMY - 1) return;\n" "if( coord.x >= width) return; \n" "if( coord.y >= height) return;\n" "float4 cp = read_imagef(texp, sampler, coord);\n" "float4 dx = (float4)( cc.y - block[idx - 1 + BLOCK_SIZE], \n" " block[idx + 1] - cc.x, \n" " cc.w - block[idx - 1 + 3 * BLOCK_SIZE], \n" " block[idx + 1 + 2 * BLOCK_SIZE] - cc.z);\n" "float4 dy = (float4)( cc.z - block[idx - BLOCK_DIMX + 2 * BLOCK_SIZE], \n" " cc.w - block[idx - BLOCK_DIMX + 3 * BLOCK_SIZE]," //" cc.zw - block[idx - BLOCK_DIMX].zw, \n" " block[idx + BLOCK_DIMX] - cc.x,\n " " block[idx + BLOCK_DIMX + BLOCK_SIZE] - cc.y);\n" //" block[idx + BLOCK_DIMX].xy - cc.xy);\n" "write_imagef(dog, coord, cc - cp); \n" "write_imagef(grd, coord, 0.5 * sqrt(dx*dx + dy * dy));\n" "write_imagef(rot, coord, atan2(dy, dx + (float4) (FLT_MIN)));}\n", _context, _device); } void ProgramBagCL::LoadDynamicShaders(SiftParam& param) { LoadKeypointShader(); LoadGenListShader(param._dog_level_num, 0); CreateGaussianFilters(param); } void ProgramBagCL::SelectInitialSmoothingFilter(int octave_min, SiftParam¶m) { float sigma = param.GetInitialSmoothSigma(octave_min); if(sigma == 0) { f_gaussian_skip0 = NULL; }else { for(unsigned int i = 0; i < f_gaussian_skip0_v.size(); i++) { if(f_gaussian_skip0_v[i]->_id == octave_min) { f_gaussian_skip0 = f_gaussian_skip0_v[i]; return ; } } FilterCL * filter = CreateGaussianFilter(sigma); filter->_id = octave_min; f_gaussian_skip0_v.push_back(filter); f_gaussian_skip0 = filter; } } void ProgramBagCL::CreateGaussianFilters(SiftParam¶m) { if(param._sigma_skip0>0.0f) { f_gaussian_skip0 = CreateGaussianFilter(param._sigma_skip0); f_gaussian_skip0->_id = GlobalUtil::_octave_min_default; f_gaussian_skip0_v.push_back(f_gaussian_skip0); } if(param._sigma_skip1>0.0f) { f_gaussian_skip1 = CreateGaussianFilter(param._sigma_skip1); } f_gaussian_step = new FilterCL*[param._sigma_num]; for(int i = 0; i< param._sigma_num; i++) { f_gaussian_step[i] = CreateGaussianFilter(param._sigma[i]); } _gaussian_step_num = param._sigma_num; } FilterCL* ProgramBagCL::CreateGaussianFilter(float sigma) { //pixel inside 3*sigma box int sz = int( ceil( GlobalUtil::_FilterWidthFactor * sigma -0.5) ) ;// int width = 2*sz + 1; //filter size truncation if(GlobalUtil::_MaxFilterWidth >0 && width > GlobalUtil::_MaxFilterWidth) { std::cout<<"Filter size truncated from "<>1; width = 2 * sz + 1; } int i; float * kernel = new float[width]; float rv = 1.0f/(sigma*sigma); float v, ksum =0; // pre-compute filter for( i = -sz ; i <= sz ; ++i) { kernel[i+sz] = v = exp(-0.5f * i * i *rv) ; ksum += v; } //normalize the kernel rv = 1.0f / ksum; for(i = 0; i< width ;i++) kernel[i]*=rv; FilterCL * filter = CreateFilter(kernel, width); delete [] kernel; if(GlobalUtil::_verbose && GlobalUtil::_timingL) std::cout<<"Filter: sigma = "<s_shader_h = CreateFilterH(kernel, width); filter->s_shader_v = CreateFilterV(kernel, width); filter->_size = width; filter->_id = 0; return filter; } ProgramCL* ProgramBagCL::CreateFilterH(float kernel[], int width) { int halfwidth = width >>1; float * pf = kernel + halfwidth; int nhpixel = (halfwidth+1)>>1; //how many neighbour pixels need to be looked up int npixel = (nhpixel<<1)+1;// float weight[3]; //////////////////////////// char buffer[10240]; ostrstream out(buffer, 10240); out< width_ || y > height_) return; \n" "float4 pc; int2 coord; \n" "float4 result = (float4)(0.0);\n"; for(int i = 0 ; i < npixel ; i++) { out<<"coord = (int2)(x + ("<< (i - nhpixel) << "), y);\n"; out<<"pc= read_imagef(input, sampler, coord);\n"; if(GlobalUtil::_PreciseBorder) out<<"if(coord.x < 0) pc = pc.xxzz; else if (coord.x > width_) pc = pc.yyww; \n"; //for each sub-pixel j in center, the weight of sub-pixel k int xw = (i - nhpixel)*2; for(int j = 0; j < 3; j++) { int xwn = xw + j -1; weight[j] = xwn < -halfwidth || xwn > halfwidth? 0 : pf[xwn]; } if(weight[1] == 0.0) { out<<"result += (float4)("<>1; float * pf = kernel + halfwidth; int nhpixel = (halfwidth+1)>>1; //how many neighbour pixels need to be looked up int npixel = (nhpixel<<1)+1;// float weight[3]; //////////////////////////// char buffer[10240]; ostrstream out(buffer, 10240); out< width_ || y >= height_) return; \n" "float4 pc; int2 coord; \n" "float4 result = (float4)(0.0);\n"; for(int i = 0 ; i < npixel ; i++) { out<<"coord = (int2)(x, y + ("<< (i - nhpixel) << "));\n"; out<<"pc= read_imagef(input, sampler, coord);\n"; if(GlobalUtil::_PreciseBorder) out<<"if(coord.y < 0) pc = pc.xyxy; else if (coord.y > height_) pc = pc.zwzw; \n"; //for each sub-pixel j in center, the weight of sub-pixel k int xw = (i - nhpixel)*2; for(int j = 0; j < 3; j++) { int xwn = xw + j -1; weight[j] = xwn < -halfwidth || xwn > halfwidth? 0 : pf[xwn]; } if(weight[1] == 0.0) { out<<"result += (float4)("<s_shader_h->_kernel; cl_kernel kernelv = filter->s_shader_v->_kernel; ////////////////////////////////////////////////////////////////// cl_int status, w = dst->GetImgWidth(), h = dst->GetImgHeight(); cl_int w_ = w - 1, h_ = h - 1; size_t dim0 = 16, dim1 = 16; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; clSetKernelArg(kernelh, 0, sizeof(cl_mem), &src->_clData); clSetKernelArg(kernelh, 1, sizeof(cl_mem), &tmp->_clData); clSetKernelArg(kernelh, 2, sizeof(cl_int), &w_); clSetKernelArg(kernelh, 3, sizeof(cl_int), &h_); status = clEnqueueNDRangeKernel(_queue, kernelh, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::FilterImageH"); if(status != CL_SUCCESS) return; clSetKernelArg(kernelv, 0, sizeof(cl_mem), &tmp->_clData); clSetKernelArg(kernelv, 1, sizeof(cl_mem), &dst->_clData); clSetKernelArg(kernelv, 2, sizeof(cl_int), &w_); clSetKernelArg(kernelv, 3, sizeof(cl_int), &h_); size_t gsz2[2] = {(w + dim1 - 1) / dim1 * dim1, (h + dim0 - 1) / dim0 * dim0}, lsz2[2] = {dim1, dim0}; status = clEnqueueNDRangeKernel(_queue, kernelv, 2, NULL, gsz2, lsz2, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::FilterImageV"); //clReleaseEvent(event); } void ProgramBagCL::SampleImageU(CLTexImage *dst, CLTexImage *src, int log_scale) { cl_kernel kernel= s_sampling_u->_kernel; float scale = 1.0f / (1 << log_scale); float offset = scale * 0.5f; cl_int w = dst->GetImgWidth(), h = dst->GetImgHeight(); clSetKernelArg(kernel, 0, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(h)); clSetKernelArg(kernel, 4, sizeof(cl_float), &(scale)); clSetKernelArg(kernel, 5, sizeof(cl_float), &(offset)); size_t dim0 = 16, dim1 = 16; //while( w * h / dim0 / dim1 < 8 && dim1 > 1) dim1 /= 2; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::SampleImageU"); } void ProgramBagCL::SampleImageD(CLTexImage *dst, CLTexImage *src, int log_scale) { cl_kernel kernel; cl_int w = dst->GetImgWidth(), h = dst->GetImgHeight(); if(log_scale == 1) { kernel = s_sampling->_kernel; clSetKernelArg(kernel, 0, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(h)); }else { cl_int fullstep = (1 << log_scale); cl_int halfstep = fullstep >> 1; kernel = s_sampling_k->_kernel; clSetKernelArg(kernel, 0, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(h)); clSetKernelArg(kernel, 4, sizeof(cl_int), &(fullstep)); clSetKernelArg(kernel, 5, sizeof(cl_int), &(halfstep)); } size_t dim0 = 128, dim1 = 1; //while( w * h / dim0 / dim1 < 8 && dim1 > 1) dim1 /= 2; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::SampleImageD"); } void ProgramBagCL::FilterInitialImage(CLTexImage* tex, CLTexImage* buf) { if(f_gaussian_skip0) FilterImage(f_gaussian_skip0, tex, tex, buf); } void ProgramBagCL::FilterSampledImage(CLTexImage* tex, CLTexImage* buf) { if(f_gaussian_skip1) FilterImage(f_gaussian_skip1, tex, tex, buf); } void ProgramBagCL::ComputeDOG(CLTexImage*tex, CLTexImage* texp, CLTexImage* dog, CLTexImage* grad, CLTexImage* rot) { int margin = 0, use_gm2 = 1; bool both_grad_dog = rot->_clData && grad->_clData; cl_int w = tex->GetImgWidth(), h = tex->GetImgHeight(); cl_kernel kernel ; size_t dim0, dim1; if(!both_grad_dog) {kernel = s_dog_pass->_kernel; dim0 = 16; dim1 = 12; } else if(use_gm2) {kernel = s_grad_pass2->_kernel; dim0 = 32; dim1 = 14; margin = 2; } else {kernel = s_grad_pass->_kernel; dim0 = 16; dim1 = 20; } size_t gsz[2] = { (w + dim0 - 1 - margin) / (dim0 - margin) * dim0, (h + dim1 - 1 - margin) / (dim1 - margin) * dim1}; size_t lsz[2] = {dim0, dim1}; clSetKernelArg(kernel, 0, sizeof(cl_mem), &(tex->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(texp->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_mem), &(dog->_clData)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 4, sizeof(cl_int), &(h)); if(both_grad_dog) { clSetKernelArg(kernel, 5, sizeof(cl_mem), &(grad->_clData)); clSetKernelArg(kernel, 6, sizeof(cl_mem), &(rot->_clData)); } /////////////////////////////////////////////////////// cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::ComputeDOG"); } void ProgramBagCL::ComputeKEY(CLTexImage*dog, CLTexImage* key, float Tdog, float Tedge) { cl_kernel kernel = s_keypoint->_kernel; cl_int w = key->GetImgWidth(), h = key->GetImgHeight(); float threshold0 = Tdog* (GlobalUtil::_SubpixelLocalization?0.8f:1.0f); float threshold1 = Tdog; float threshold2 = (Tedge+1)*(Tedge+1)/Tedge; clSetKernelArg(kernel, 0, sizeof(cl_mem), &(dog->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &((dog + 1)->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_mem), &((dog - 1)->_clData)); clSetKernelArg(kernel, 3, sizeof(cl_mem), &(key->_clData)); clSetKernelArg(kernel, 4, sizeof(cl_float), &(threshold0)); clSetKernelArg(kernel, 5, sizeof(cl_float), &(threshold1)); clSetKernelArg(kernel, 6, sizeof(cl_float), &(threshold2)); clSetKernelArg(kernel, 7, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 8, sizeof(cl_int), &(h)); size_t dim0 = 8, dim1 = 8; //if( w * h / dim0 / dim1 < 16) dim1 /= 2; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::ComputeKEY"); } void ProgramBagCL::UnpackImage(CLTexImage*src, CLTexImage* dst) { cl_kernel kernel = s_unpack->_kernel; cl_int w = dst->GetImgWidth(), h = dst->GetImgHeight(); clSetKernelArg(kernel, 0, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(h)); const size_t dim0 = 16, dim1 = 16; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::UnpackImage"); FinishCL(); } void ProgramBagCL::UnpackImageDOG(CLTexImage*src, CLTexImage* dst) { if(s_unpack_dog == NULL) return; cl_kernel kernel = s_unpack_dog->_kernel; cl_int w = dst->GetImgWidth(), h = dst->GetImgHeight(); clSetKernelArg(kernel, 0, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(h)); const size_t dim0 = 16, dim1 = 16; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::UnpackImage"); FinishCL(); } void ProgramBagCL::UnpackImageGRD(CLTexImage*src, CLTexImage* dst) { if(s_unpack_grd == NULL) return; cl_kernel kernel = s_unpack_grd->_kernel; cl_int w = dst->GetImgWidth(), h = dst->GetImgHeight(); clSetKernelArg(kernel, 0, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(h)); const size_t dim0 = 16, dim1 = 16; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::UnpackImage"); FinishCL(); } void ProgramBagCL::UnpackImageKEY(CLTexImage*src, CLTexImage* dog, CLTexImage* dst) { if(s_unpack_key == NULL) return; cl_kernel kernel = s_unpack_key->_kernel; cl_int w = dst->GetImgWidth(), h = dst->GetImgHeight(); clSetKernelArg(kernel, 0, sizeof(cl_mem), &(dog->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 4, sizeof(cl_int), &(h)); const size_t dim0 = 16, dim1 = 16; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCL::UnpackImageKEY"); FinishCL(); } void ProgramBagCL::LoadDescriptorShader() { GlobalUtil::_DescriptorPPT = 16; LoadDescriptorShaderF2(); } void ProgramBagCL::LoadDescriptorShaderF2() { } void ProgramBagCL::LoadOrientationShader(void) { } void ProgramBagCL::LoadGenListShader(int ndoglev,int nlev) { } void ProgramBagCL::LoadKeypointShader() { int i; char buffer[20240]; ostrstream out(buffer, 20240); streampos pos; //tex(X)(Y) //X: (CLR) (CENTER 0, LEFT -1, RIGHT +1) //Y: (CDU) (CENTER 0, DOWN -1, UP +1) out<< "__kernel void keypoint(__read_only image2d_t tex, __read_only image2d_t texU,\n" " __read_only image2d_t texD, __write_only image2d_t texK,\n" " float THRESHOLD0, float THRESHOLD1, \n" " float THRESHOLD2, int width, int height)\n" "{\n" " sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | \n" " CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;" " int x = get_global_id(0), y = get_global_id(1);\n" " if(x >= width || y >= height) return; \n" " int xp = x - 1, xn = x + 1;\n" " int yp = y - 1, yn = y + 1;\n" " int2 coord0 = (int2) (x, y); \n" " int2 coord1 = (int2) (xp, y); \n" " int2 coord2 = (int2) (xn, y); \n" " int2 coord3 = (int2) (x, yp); \n" " int2 coord4 = (int2) (x, yn); \n" " int2 coord5 = (int2) (xp, yp); \n" " int2 coord6 = (int2) (xp, yn); \n" " int2 coord7 = (int2) (xn, yp); \n" " int2 coord8 = (int2) (xn, yn); \n" " float4 ccc = read_imagef(tex, sampler,coord0);\n" " float4 clc = read_imagef(tex, sampler,coord1);\n" " float4 crc = read_imagef(tex, sampler,coord2);\n" " float4 ccd = read_imagef(tex, sampler,coord3);\n" " float4 ccu = read_imagef(tex, sampler,coord4);\n" " float4 cld = read_imagef(tex, sampler,coord5);\n" " float4 clu = read_imagef(tex, sampler,coord6);\n" " float4 crd = read_imagef(tex, sampler,coord7);\n" " float4 cru = read_imagef(tex, sampler,coord8);\n" " float4 cc = ccc;\n" " float4 v1[4], v2[4];\n" " v1[0] = (float4)(clc.y, ccc.y, ccd.z, ccc.z);\n" " v1[1] = (float4)(ccc.x, crc.x, ccd.w, ccc.w);\n" " v1[2] = (float4)(clc.w, ccc.w, ccc.x, ccu.x);\n" " v1[3] = (float4)(ccc.z, crc.z, ccc.y, ccu.y);\n" " v2[0] = (float4)(cld.w, clc.w, ccd.w, ccc.w);\n" " v2[1] = (float4)(ccd.z, ccc.z, crd.z, crc.z);\n" " v2[2] = (float4)(clc.y, clu.y, ccc.y, ccu.y);\n" " v2[3] = (float4)(ccc.x, ccu.x, crc.x, cru.x);\n" " float4 key4 = (float4)(0); \n"; //test against 8 neighbours //use variable to identify type of extremum //1.0 for local maximum and -1.0 for minimum for(i = 0; i < 4; ++i) out<< " if(cc.s"< THRESHOLD0){ \n" " if(all(isgreater((float4)(cc.s"< //vector v2 is < (-1,-1), (-1,1), (1,-1), (1, 1)> for(i = 0; i < 4; ++i) out << " if(key4.s"< THRESHOLD2 * score_down)keysum = 0;\n" " }\n"; out << " if(keysum == 1) {\n"; //////////////////////////////////////////////// //read 9 pixels of upper/lower level out<< " float4 v4[4], v5[4], v6[4];\n" " ccc = read_imagef(texU, sampler,coord0);\n" " clc = read_imagef(texU, sampler,coord1);\n" " crc = read_imagef(texU, sampler,coord2);\n" " ccd = read_imagef(texU, sampler,coord3);\n" " ccu = read_imagef(texU, sampler,coord4);\n" " cld = read_imagef(texU, sampler,coord5);\n" " clu = read_imagef(texU, sampler,coord6);\n" " crd = read_imagef(texU, sampler,coord7);\n" " cru = read_imagef(texU, sampler,coord8);\n" " float4 cu = ccc;\n" " v4[0] = (float4)(clc.y, ccc.y, ccd.z, ccc.z);\n" " v4[1] = (float4)(ccc.x, crc.x, ccd.w, ccc.w);\n" " v4[2] = (float4)(clc.w, ccc.w, ccc.x, ccu.x);\n" " v4[3] = (float4)(ccc.z, crc.z, ccc.y, ccu.y);\n" " v6[0] = (float4)(cld.w, clc.w, ccd.w, ccc.w);\n" " v6[1] = (float4)(ccd.z, ccc.z, crd.z, crc.z);\n" " v6[2] = (float4)(clc.y, clu.y, ccc.y, ccu.y);\n" " v6[3] = (float4)(ccc.x, ccu.x, crc.x, cru.x);\n"; for(i = 0; i < 4; ++i) out << " if(key4.s"< cu.s"< cd.s"<= 1e-10 ) \n" " { \n" " if(x3.y ==maxa ) \n" " { \n" " float4 TEMP = A1; A1 = A0; A0 = TEMP; \n" " }else if( x3.z == maxa ) \n" " { \n" " float4 TEMP = A2; A2 = A0; A0 = TEMP; \n" " } \n" " A0 /= A0.x; \n" " A1 -= A1.x * A0; \n" " A2 -= A2.x * A0; \n" " float2 x2 = fabs((float2)(A1.y, A2.y)); \n" " if( x2.y > x2.x ) \n" " { \n" " float4 TEMP = A2.yzwx; \n" " A2.yzw = A1.yzw; \n" " A1.yzw = TEMP.xyz; \n" " x2.x = x2.y; \n" " } \n" " if(x2.x >= 1e-10) { \n" " A1.yzw /= A1.y; \n" " A2.yzw -= A2.y * A1.yzw; \n" " if(fabs(A2.z) >= 1e-10) {\n" " offset.z = A2.w /A2.z; \n" " offset.y = A1.w - offset.z*A1.z; \n" " offset.x = A0.w - offset.z*A0.z - offset.y*A0.y; \n" " if(fabs(cc.s0 + 0.5*dot((float4)(fx[0], fy[0], fs, 0), offset ))<=THRESHOLD1\n" " || any( isgreater(fabs(offset), (float4)(1.0)))) key4 = (float4)(0.0);\n" " }\n" " }\n" " }\n" <<"\n" " float keyv = dot(key4, (float4)(1.0, 2.0, 3.0, 4.0));\n" " result = (float4)(keyv, offset.xyz);\n" " }}}}\n" " write_imagef(texK, coord0, result);\n " "}\n" <<'\0'; } else { out << "\n" " float keyv = dot(key4, (float4)(1.0, 2.0, 3.0, 4.0));\n" " result = (float4)(keyv, 0, 0, 0);\n" " }}}}\n" " write_imagef(texK, coord0, result);\n " "}\n" <<'\0'; } s_keypoint = new ProgramCL("keypoint", buffer, _context, _device); } void ProgramBagCL::LoadDisplayShaders() { //"uniform sampler2DRect tex; void main(){\n" //"vec4 pc = texture2DRect(tex, gl_TexCoord[0].xy); bvec2 ff = lessThan(fract(gl_TexCoord[0].xy), vec2(0.5));\n" //"float v = ff.y?(ff.x? pc.r : pc.g):(ff.x?pc.b:pc.a); gl_FragColor = vec4(vec3(v), 1.0);}"); s_unpack = new ProgramCL("main", "__kernel void main(__read_only image2d_t input, __write_only image2d_t output,\n" " int width, int height) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if(x >= width || y >= height) return;\n" "int xx = x / 2, yy = y / 2; \n" "float4 vv = read_imagef(input, sampler, (int2) (xx, yy)); \n" "float v1 = (x & 1 ? vv.w : vv.z); \n" "float v2 = (x & 1 ? vv.y : vv.x);\n" "float v = y & 1 ? v1 : v2;\n" "float4 result = (float4) (v, v, v, 1);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_unpack_dog = new ProgramCL("main", "__kernel void main(__read_only image2d_t input, __write_only image2d_t output,\n" " int width, int height) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if(x >= width || y >= height) return;\n" "int xx = x / 2, yy = y / 2; \n" "float4 vv = read_imagef(input, sampler, (int2) (xx, yy)); \n" "float v1 = (x & 1 ? vv.w : vv.z); \n" "float v2 = (x & 1 ? vv.y : vv.x);\n" "float v0 = y & 1 ? v1 : v2;\n" "float v = 0.5 + 20.0 * v0;\n " "float4 result = (float4) (v, v, v, 1);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_unpack_grd = new ProgramCL("main", "__kernel void main(__read_only image2d_t input, __write_only image2d_t output,\n" " int width, int height) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if(x >= width || y >= height) return;\n" "int xx = x / 2, yy = y / 2; \n" "float4 vv = read_imagef(input, sampler, (int2) (xx, yy)); \n" "float v1 = (x & 1 ? vv.w : vv.z); \n" "float v2 = (x & 1 ? vv.y : vv.x);\n" "float v0 = y & 1 ? v1 : v2;\n" "float v = 5.0 * v0;\n " "float4 result = (float4) (v, v, v, 1);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_unpack_key = new ProgramCL("main", "__kernel void main(__read_only image2d_t dog,\n" " __read_only image2d_t key,\n" " __write_only image2d_t output,\n" " int width, int height) {\n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if(x >= width || y >= height) return;\n" "int xx = x / 2, yy = y / 2; \n" "float4 kk = read_imagef(key, sampler, (int2) (xx, yy));\n" "int4 cc = isequal(fabs(kk.xxxx), (float4)(1.0, 2.0, 3.0, 4.0));\n" "int k1 = (x & 1 ? cc.w : cc.z); \n" "int k2 = (x & 1 ? cc.y : cc.x);\n" "int k0 = y & 1 ? k1 : k2;\n" "float4 result;\n" "if(k0 != 0){\n" " //result = kk.x > 0 ? ((float4)(1.0, 0, 0, 1.0)) : ((float4) (0.0, 1.0, 0.0, 1.0)); \n" " result = kk.x < 0 ? ((float4)(0, 1.0, 0, 1.0)) : ((float4) (1.0, 0.0, 0.0, 1.0)); \n" "}else{" "float4 vv = read_imagef(dog, sampler, (int2) (xx, yy));\n" "float v1 = (x & 1 ? vv.w : vv.z); \n" "float v2 = (x & 1 ? vv.y : vv.x);\n" "float v0 = y & 1 ? v1 : v2;\n" "float v = 0.5 + 20.0 * v0;\n " "result = (float4) (v, v, v, 1);" "}\n" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); } void ProgramBagCL::SetMarginCopyParam(int xmax, int ymax) { } void ProgramBagCL::SetGradPassParam(int texP) { } void ProgramBagCL::SetGenListEndParam(int ktex) { } void ProgramBagCL::SetDogTexParam(int texU, int texD) { } void ProgramBagCL::SetGenListInitParam(int w, int h) { float bbox[4] = {(w -1.0f) * 0.5f +0.25f, (w-1.0f) * 0.5f - 0.25f, (h - 1.0f) * 0.5f + 0.25f, (h-1.0f) * 0.5f - 0.25f}; } void ProgramBagCL::SetGenListStartParam(float width, int tex0) { } void ProgramBagCL::SetGenListStepParam(int tex, int tex0) { } void ProgramBagCL::SetGenVBOParam(float width, float fwidth, float size) { } void ProgramBagCL::SetSimpleOrientationInput(int oTex, float sigma, float sigma_step) { } void ProgramBagCL::SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int otex, float step) { } void ProgramBagCL::SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma) { } const char* ProgramBagCL::GetErrorString(cl_int error) { static const char* errorString[] = { "CL_SUCCESS", "CL_DEVICE_NOT_FOUND", "CL_DEVICE_NOT_AVAILABLE", "CL_COMPILER_NOT_AVAILABLE", "CL_MEM_OBJECT_ALLOCATION_FAILURE", "CL_OUT_OF_RESOURCES", "CL_OUT_OF_HOST_MEMORY", "CL_PROFILING_INFO_NOT_AVAILABLE", "CL_MEM_COPY_OVERLAP", "CL_IMAGE_FORMAT_MISMATCH", "CL_IMAGE_FORMAT_NOT_SUPPORTED", "CL_BUILD_PROGRAM_FAILURE", "CL_MAP_FAILURE", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "CL_INVALID_VALUE", "CL_INVALID_DEVICE_TYPE", "CL_INVALID_PLATFORM", "CL_INVALID_DEVICE", "CL_INVALID_CONTEXT", "CL_INVALID_QUEUE_PROPERTIES", "CL_INVALID_COMMAND_QUEUE", "CL_INVALID_HOST_PTR", "CL_INVALID_MEM_OBJECT", "CL_INVALID_IMAGE_FORMAT_DESCRIPTOR", "CL_INVALID_IMAGE_SIZE", "CL_INVALID_SAMPLER", "CL_INVALID_BINARY", "CL_INVALID_BUILD_OPTIONS", "CL_INVALID_PROGRAM", "CL_INVALID_PROGRAM_EXECUTABLE", "CL_INVALID_KERNEL_NAME", "CL_INVALID_KERNEL_DEFINITION", "CL_INVALID_KERNEL", "CL_INVALID_ARG_INDEX", "CL_INVALID_ARG_VALUE", "CL_INVALID_ARG_SIZE", "CL_INVALID_KERNEL_ARGS", "CL_INVALID_WORK_DIMENSION", "CL_INVALID_WORK_GROUP_SIZE", "CL_INVALID_WORK_ITEM_SIZE", "CL_INVALID_GLOBAL_OFFSET", "CL_INVALID_EVENT_WAIT_LIST", "CL_INVALID_EVENT", "CL_INVALID_OPERATION", "CL_INVALID_GL_OBJECT", "CL_INVALID_BUFFER_SIZE", "CL_INVALID_MIP_LEVEL", "CL_INVALID_GLOBAL_WORK_SIZE", }; const int errorCount = sizeof(errorString) / sizeof(errorString[0]); const int index = -error; return (index >= 0 && index < errorCount) ? errorString[index] : ""; } bool ProgramBagCL::CheckErrorCL(cl_int error, const char* location) { if(error == CL_SUCCESS) return true; const char *errstr = GetErrorString(error); if(errstr && errstr[0]) std::cerr << errstr; else std::cerr << "Error " << error; if(location) std::cerr << " at " << location; std::cerr << "\n"; exit(0); return false; } //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////// void ProgramBagCLN::LoadFixedShaders() { s_sampling = new ProgramCL("sampling", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void sampling(__read_only image2d_t input, __write_only image2d_t output, " " int width, int height) {\n" "int2 coord = (int2)(get_global_id(0), get_global_id(1)); \n" "if( coord.x >= width || coord.y >= height) return;\n" "write_imagef(output, coord, read_imagef(input, sampler, coord << 1)); }" , _context, _device); s_sampling_k = new ProgramCL("sampling_k", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void sampling_k(__read_only image2d_t input, __write_only image2d_t output, " " int width, int height, int step) {\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if( x >= width || y >= height) return;\n" "int xa = x * step, ya = y *step; \n" "float4 v1 = read_imagef(input, sampler, (int2) (xa, ya)); \n" "write_imagef(output, (int2) (x, y), v1); }" , _context, _device); s_sampling_u = new ProgramCL("sampling_u", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_LINEAR;\n" "__kernel void sampling_u(__read_only image2d_t input, \n" " __write_only image2d_t output,\n" " int width, int height, float step) {\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if( x >= width || y >= height) return;\n" "float xa = x * step, ya = y *step; \n" "float v1 = read_imagef(input, sampler, (float2) (xa, ya)).x; \n" "write_imagef(output, (int2) (x, y), (float4)(v1)); }" , _context, _device); s_dog_pass = new ProgramCL("dog_pass", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void dog_pass(__read_only image2d_t tex, __read_only image2d_t texp,\n" " __write_only image2d_t dog, int width, int height) {\n" "int2 coord = (int2)(get_global_id(0), get_global_id(1)); \n" "if( coord.x >= width || coord.y >= height) return;\n" "float cc = read_imagef(tex , sampler, coord).x; \n" "float cp = read_imagef(texp, sampler, coord).x;\n" "write_imagef(dog, coord, (float4)(cc - cp)); }\n", _context, _device); s_grad_pass = new ProgramCL("grad_pass", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void grad_pass(__read_only image2d_t tex, __read_only image2d_t texp,\n" " __write_only image2d_t dog, int width, int height, \n" " __write_only image2d_t grad, __write_only image2d_t rot) {\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if( x >= width || y >= height) return;\n" "int2 coord = (int2) (x, y);\n" "float cl = read_imagef(tex, sampler, (int2)(x - 1, y)).x;\n" "float cc = read_imagef(tex , sampler, coord).x; \n" "float cr = read_imagef(tex, sampler, (int2)(x + 1, y)).x;\n" "float cp = read_imagef(texp, sampler, coord).x;\n" "write_imagef(dog, coord, (float4)(cc - cp)); \n" "float cd = read_imagef(tex, sampler, (int2)(x, y - 1)).x;\n" "float cu = read_imagef(tex, sampler, (int2)(x, y + 1)).x;\n" "float dx = cr - cl, dy = cu - cd; \n" "float gg = 0.5 * sqrt(dx*dx + dy * dy);\n" "write_imagef(grad, coord, (float4)(gg));\n" "float oo = atan2(dy, dx + FLT_MIN);\n" "write_imagef(rot, coord, (float4)(oo));}\n", _context, _device); s_grad_pass2 = new ProgramCL("grad_pass2", "#define BLOCK_DIMX 32\n" "#define BLOCK_DIMY 14\n" "#define BLOCK_SIZE (BLOCK_DIMX * BLOCK_DIMY)\n" "__kernel void grad_pass2(__read_only image2d_t tex, __read_only image2d_t texp,\n" " __write_only image2d_t dog, int width, int height,\n" " __write_only image2d_t grd, __write_only image2d_t rot){\n"//, __local float* block) {\n" "__local float block[BLOCK_SIZE]; \n" "sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE |\n" " CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "int2 coord = (int2) ( get_global_id(0) - get_group_id(0) * 2 - 1, \n" " get_global_id(1) - get_group_id(1) * 2 - 1); \n" "int idx = mad24(get_local_id(1), BLOCK_DIMX, get_local_id(0));\n" "float cc = read_imagef(tex, sampler, coord).x;\n" "block[idx] = cc;\n" "barrier(CLK_LOCAL_MEM_FENCE);\n" "if( get_local_id(0) == 0 || get_local_id(0) == BLOCK_DIMX - 1) return;\n" "if( get_local_id(1) == 0 || get_local_id(1) == BLOCK_DIMY - 1) return;\n" "if( coord.x >= width) return; \n" "if( coord.y >= height) return;\n" "float cp = read_imagef(texp, sampler, coord).x;\n" "float dx = block[idx + 1] - block[idx - 1];\n" "float dy = block[idx + BLOCK_DIMX ] - block[idx - BLOCK_DIMX];\n" "write_imagef(dog, coord, (float4)(cc - cp)); \n" "write_imagef(grd, coord, (float4)(0.5 * sqrt(dx*dx + dy * dy)));\n" "write_imagef(rot, coord, (float4)(atan2(dy, dx + FLT_MIN)));}\n", _context, _device); } void ProgramBagCLN::LoadDisplayShaders() { s_unpack = new ProgramCL("main", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void main(__read_only image2d_t input, __write_only image2d_t output,\n" " int width, int height) {\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if(x >= width || y >= height) return;\n" "float v = read_imagef(input, sampler, (int2) (x, y)).x; \n" "float4 result = (float4) (v, v, v, 1);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_unpack_grd = new ProgramCL("main", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE |\n" " CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void main(__read_only image2d_t input, __write_only image2d_t output,\n" " int width, int height) {\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if(x >= width || y >= height) return;\n" "float v0 = read_imagef(input, sampler, (int2) (x, y)).x; \n" "float v = 5.0 * v0; float4 result = (float4) (v, v, v, 1);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); s_unpack_dog = new ProgramCL("main", "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void main(__read_only image2d_t input, __write_only image2d_t output,\n" " int width, int height) {\n" "int x = get_global_id(0), y = get_global_id(1); \n" "if(x >= width || y >= height) return;\n" "float v0 = read_imagef(input, sampler, (int2) (x, y)).x; \n" "float v = 0.5 + 20.0 * v0; float4 result = (float4) (v, v, v, 1);" "write_imagef(output, (int2) (x, y), result); }" , _context, _device); } ProgramCL* ProgramBagCLN::CreateFilterH(float kernel[], int width) { //////////////////////////// char buffer[10240]; ostrstream out(buffer, 10240); out << "#define KERNEL_WIDTH " << width << "\n" << "#define KERNEL_HALF_WIDTH " << (width / 2) << "\n" "#define BLOCK_WIDTH 128\n" "#define BLOCK_HEIGHT 1\n" "#define CACHE_WIDTH (BLOCK_WIDTH + KERNEL_WIDTH - 1)\n" "#define CACHE_WIDTH_ALIGNED ((CACHE_WIDTH + 15) / 16 * 16)\n" "#define CACHE_COUNT (2 + (CACHE_WIDTH - 2) / BLOCK_WIDTH)\n" "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void filter_h(__read_only image2d_t input, \n" " __write_only image2d_t output, int width_, int height_, \n" " __constant float* weight) {\n" "__local float data[CACHE_WIDTH]; \n" "int x = get_global_id(0), y = get_global_id(1);\n" "#pragma unroll\n" "for(int j = 0; j < CACHE_COUNT; ++j)\n" "{\n" " if(get_local_id(0) + j * BLOCK_WIDTH < CACHE_WIDTH)\n" " {\n" " int fetch_index = min(x + j * BLOCK_WIDTH - KERNEL_HALF_WIDTH, width_);\n" " data[get_local_id(0) + j * BLOCK_WIDTH] = read_imagef(input, sampler, (int2)(fetch_index, y)).x;\n" " }\n" "}\n" "barrier(CLK_LOCAL_MEM_FENCE); \n" "if( x > width_ || y > height_) return; \n" "float result = 0; \n" "#pragma unroll\n" "for(int i = 0; i < KERNEL_WIDTH; ++i)\n" "{\n" " result += data[get_local_id(0) + i] * weight[i];\n" "}\n" << "write_imagef(output, (int2)(x, y), (float4)(result)); }\n" << '\0'; return new ProgramCL("filter_h", buffer, _context, _device); } ProgramCL* ProgramBagCLN::CreateFilterV(float kernel[], int width) { //////////////////////////// char buffer[10240]; ostrstream out(buffer, 10240); out << "#define KERNEL_WIDTH " << width << "\n" << "#define KERNEL_HALF_WIDTH " << (width / 2) << "\n" "#define BLOCK_WIDTH 128\n" "#define CACHE_WIDTH (BLOCK_WIDTH + KERNEL_WIDTH - 1)\n" "#define CACHE_WIDTH_ALIGNED ((CACHE_WIDTH + 15) / 16 * 16)\n" "#define CACHE_COUNT (2 + (CACHE_WIDTH - 2) / BLOCK_WIDTH)\n" "const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | \n" " CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;\n" "__kernel void filter_v(__read_only image2d_t input, \n" " __write_only image2d_t output, int width_, int height_, \n" " __constant float* weight) {\n" "__local float data[CACHE_WIDTH]; \n" "int x = get_global_id(0), y = get_global_id(1);\n" "#pragma unroll\n" "for(int j = 0; j < CACHE_COUNT; ++j)\n" "{\n" " if(get_local_id(1) + j * BLOCK_WIDTH < CACHE_WIDTH)\n" " {\n" " int fetch_index = min(y + j * BLOCK_WIDTH - KERNEL_HALF_WIDTH, height_);\n" " data[get_local_id(1) + j * BLOCK_WIDTH ] = read_imagef(input, sampler, (int2)(x, fetch_index)).x;\n" " }\n" "}\n" "barrier(CLK_LOCAL_MEM_FENCE); \n" "if( x > width_ || y > height_) return; \n" "float result = 0; \n" "#pragma unroll\n" "for(int i = 0; i < KERNEL_WIDTH; ++i)\n" "{\n" " result += data[get_local_id(1) + i] * weight[i];\n" "}\n" << "write_imagef(output, (int2)(x, y), (float4)(result)); }\n" << '\0'; return new ProgramCL("filter_v", buffer, _context, _device); } FilterCL* ProgramBagCLN::CreateFilter(float kernel[], int width) { FilterCL * filter = new FilterCL; filter->s_shader_h = CreateFilterH(kernel, width); filter->s_shader_v = CreateFilterV(kernel, width); filter->_weight = new CLTexImage(_context, _queue); filter->_weight->InitBufferTex(width, 1, 1); filter->_weight->CopyFromHost(kernel); filter->_size = width; return filter; } void ProgramBagCLN::FilterImage(FilterCL* filter, CLTexImage *dst, CLTexImage *src, CLTexImage*tmp) { cl_kernel kernelh = filter->s_shader_h->_kernel; cl_kernel kernelv = filter->s_shader_v->_kernel; ////////////////////////////////////////////////////////////////// cl_int status, w = dst->GetImgWidth(), h = dst->GetImgHeight(); cl_mem weight = (cl_mem) filter->_weight->_clData; cl_int w_ = w - 1, h_ = h - 1; clSetKernelArg(kernelh, 0, sizeof(cl_mem), &src->_clData); clSetKernelArg(kernelh, 1, sizeof(cl_mem), &tmp->_clData); clSetKernelArg(kernelh, 2, sizeof(cl_int), &w_); clSetKernelArg(kernelh, 3, sizeof(cl_int), &h_); clSetKernelArg(kernelh, 4, sizeof(cl_mem), &weight); size_t dim00 = 128, dim01 = 1; size_t gsz1[2] = {(w + dim00 - 1) / dim00 * dim00, (h + dim01 - 1) / dim01 * dim01}, lsz1[2] = {dim00, dim01}; status = clEnqueueNDRangeKernel(_queue, kernelh, 2, NULL, gsz1, lsz1, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCLN::FilterImageH"); if(status != CL_SUCCESS) return; clSetKernelArg(kernelv, 0, sizeof(cl_mem), &tmp->_clData); clSetKernelArg(kernelv, 1, sizeof(cl_mem), &dst->_clData); clSetKernelArg(kernelv, 2, sizeof(cl_int), &w_); clSetKernelArg(kernelv, 3, sizeof(cl_int), &h_); clSetKernelArg(kernelv, 4, sizeof(cl_mem), &weight); size_t dim10 = 1, dim11 = 128; size_t gsz2[2] = {(w + dim10 - 1) / dim10 * dim10, (h + dim11 - 1) / dim11 * dim11}, lsz2[2] = {dim10, dim11}; status = clEnqueueNDRangeKernel(_queue, kernelv, 2, NULL, gsz2, lsz2, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCLN::FilterImageV"); //clReleaseEvent(event); } void ProgramBagCLN::SampleImageD(CLTexImage *dst, CLTexImage *src, int log_scale) { cl_kernel kernel; cl_int w = dst->GetImgWidth(), h = dst->GetImgHeight(); cl_int fullstep = (1 << log_scale); kernel = log_scale == 1? s_sampling->_kernel : s_sampling_k->_kernel; clSetKernelArg(kernel, 0, sizeof(cl_mem), &(src->_clData)); clSetKernelArg(kernel, 1, sizeof(cl_mem), &(dst->_clData)); clSetKernelArg(kernel, 2, sizeof(cl_int), &(w)); clSetKernelArg(kernel, 3, sizeof(cl_int), &(h)); if(log_scale > 1) clSetKernelArg(kernel, 4, sizeof(cl_int), &(fullstep)); size_t dim0 = 128, dim1 = 1; //while( w * h / dim0 / dim1 < 8 && dim1 > 1) dim1 /= 2; size_t gsz[2] = {(w + dim0 - 1) / dim0 * dim0, (h + dim1 - 1) / dim1 * dim1}, lsz[2] = {dim0, dim1}; cl_int status = clEnqueueNDRangeKernel(_queue, kernel, 2, NULL, gsz, lsz, 0, NULL, NULL); CheckErrorCL(status, "ProgramBagCLN::SampleImageD"); } #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramCL.h000066400000000000000000000137361454702036400212570ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ProgramCL.h // Author: Changchang Wu // Description : interface for the ProgramCL classes. // ProgramCL: Cg programs // ShaderBagCG: All Cg shaders for Sift in a bag // FilterCL: Cg Gaussian Filters // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CL_SIFTGPU_ENABLED) #ifndef _PROGRAM_CL_H #define _PROGRAM_CL_H #include "ProgramGPU.h" class ProgramCL: public ProgramGPU { cl_program _program; cl_kernel _kernel; int _valid; public: int IsValidProgram(){return _program && _valid;} ProgramCL(const char* name, const char * code, cl_context contex, cl_device_id device); ProgramCL(); void PrintBuildLog(cl_device_id device, int all); virtual ~ProgramCL(); virtual int UseProgram(){return 1;} virtual void * GetProgramID() {return _kernel;} friend class ProgramBagCL; friend class ProgramBagCLN; }; class CLTexImage; class FilterCL { public: ProgramCL* s_shader_h; ProgramCL* s_shader_v; int _size; int _id; CLTexImage * _weight; public: FilterCL() : s_shader_h(NULL), s_shader_v(NULL), _size(0), _id(0), _weight(NULL) {} ~FilterCL() {if(s_shader_h) delete s_shader_h; if(s_shader_v) delete s_shader_v; if(_weight) delete _weight; } }; class SiftParam; class ProgramBagCL { protected: cl_platform_id _platform; cl_device_id _device; cl_context _context; cl_command_queue _queue; protected: ProgramCL * s_gray; ProgramCL * s_sampling; ProgramCL * s_sampling_k; ProgramCL * s_sampling_u; ProgramCL * s_zero_pass; ProgramCL * s_packup; ProgramCL * s_unpack; ProgramCL * s_unpack_dog; ProgramCL * s_unpack_grd; ProgramCL * s_unpack_key; ProgramCL * s_dog_pass; ProgramCL * s_grad_pass; ProgramCL * s_grad_pass2; ProgramCL * s_gray_pack; ProgramCL * s_keypoint; public: FilterCL * f_gaussian_skip0; vector f_gaussian_skip0_v; FilterCL * f_gaussian_skip1; FilterCL ** f_gaussian_step; int _gaussian_step_num; public: ProgramBagCL(); bool InitializeContext(); virtual ~ProgramBagCL(); void FinishCL(); cl_context GetContextCL() {return _context;} cl_command_queue GetCommandQueue() {return _queue;} static const char* GetErrorString(cl_int error); static bool CheckErrorCL(cl_int error, const char* location = NULL); public: FilterCL * CreateGaussianFilter(float sigma); void CreateGaussianFilters(SiftParam¶m); void SelectInitialSmoothingFilter(int octave_min, SiftParam¶m); void FilterInitialImage(CLTexImage* tex, CLTexImage* buf); void FilterSampledImage(CLTexImage* tex, CLTexImage* buf); void UnpackImage(CLTexImage*src, CLTexImage* dst); void UnpackImageDOG(CLTexImage*src, CLTexImage* dst); void UnpackImageGRD(CLTexImage*src, CLTexImage* dst); void UnpackImageKEY(CLTexImage*src, CLTexImage* dog, CLTexImage* dst); void ComputeDOG(CLTexImage*tex, CLTexImage* texp, CLTexImage* dog, CLTexImage* grad, CLTexImage* rot); void ComputeKEY(CLTexImage*dog, CLTexImage* key, float Tdog, float Tedge); public: virtual void SampleImageU(CLTexImage *dst, CLTexImage *src, int log_scale); virtual void SampleImageD(CLTexImage *dst, CLTexImage *src, int log_scale = 1); virtual void FilterImage(FilterCL* filter, CLTexImage *dst, CLTexImage *src, CLTexImage*tmp); virtual ProgramCL* CreateFilterH(float kernel[], int width); virtual ProgramCL* CreateFilterV(float kernel[], int width); virtual FilterCL* CreateFilter(float kernel[], int width); public: virtual void InitProgramBag(SiftParam¶m); virtual void LoadDescriptorShader(); virtual void LoadDescriptorShaderF2(); virtual void LoadOrientationShader(); virtual void LoadGenListShader(int ndoglev, int nlev); virtual void UnloadProgram() ; virtual void LoadKeypointShader(); virtual void LoadFixedShaders(); virtual void LoadDisplayShaders(); virtual void LoadDynamicShaders(SiftParam& param); public: //parameters virtual void SetGradPassParam(int texP); virtual void SetGenListEndParam(int ktex); virtual void SetGenListStartParam(float width, int tex0); virtual void SetGenListInitParam(int w, int h); virtual void SetMarginCopyParam(int xmax, int ymax); virtual void SetDogTexParam(int texU, int texD); virtual void SetGenListStepParam(int tex, int tex0); virtual void SetGenVBOParam( float width, float fwidth, float size); virtual void SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma); virtual void SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex, float step); virtual void SetSimpleOrientationInput(int oTex, float sigma, float sigma_step); }; class CLTexImage ; class ProgramBagCLN: public ProgramBagCL { public: virtual void SampleImageD(CLTexImage *dst, CLTexImage *src, int log_scale = 1); virtual FilterCL* CreateFilter(float kernel[], int width); virtual ProgramCL* CreateFilterH(float kernel[], int width); virtual ProgramCL* CreateFilterV(float kernel[], int width); virtual void FilterImage(FilterCL* filter, CLTexImage *dst, CLTexImage *src, CLTexImage*tmp); virtual void LoadFixedShaders(); virtual void LoadDisplayShaders(); }; #endif #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramCU.cu000066400000000000000000001716601454702036400214510ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ProgramCU.cu // Author: Changchang Wu // Description : implementation of ProgramCU and all CUDA kernels // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CUDA_SIFTGPU_ENABLED) #include "GL/glew.h" #include "stdio.h" #include "CuTexImage.h" #include "ProgramCU.h" #include "GlobalUtil.h" //---------------------------------------------------------------- //Begin SiftGPU setting section. ////////////////////////////////////////////////////////// #define IMUL(X,Y) __mul24(X,Y) //#define FDIV(X,Y) ((X)/(Y)) #define FDIV(X,Y) __fdividef(X,Y) ///////////////////////////////////////////////////////// //filter kernel width range (don't change this) #define KERNEL_MAX_WIDTH 33 #define KERNEL_MIN_WIDTH 5 ////////////////////////////////////////////////////////// //horizontal filter block size (32, 64, 128, 256, 512) #define FILTERH_TILE_WIDTH 128 //thread block for vertical filter. FILTERV_BLOCK_WIDTH can be (4, 8 or 16) #define FILTERV_BLOCK_WIDTH 16 #define FILTERV_BLOCK_HEIGHT 32 //The corresponding image patch for a thread block #define FILTERV_PIXEL_PER_THREAD 4 #define FILTERV_TILE_WIDTH FILTERV_BLOCK_WIDTH #define FILTERV_TILE_HEIGHT (FILTERV_PIXEL_PER_THREAD * FILTERV_BLOCK_HEIGHT) ////////////////////////////////////////////////////////// //thread block size for computing Difference of Gaussian #define DOG_BLOCK_LOG_DIMX 7 #define DOG_BLOCK_LOG_DIMY 0 #define DOG_BLOCK_DIMX (1 << DOG_BLOCK_LOG_DIMX) #define DOG_BLOCK_DIMY (1 << DOG_BLOCK_LOG_DIMY) ////////////////////////////////////////////////////////// //thread block size for keypoint detection #define KEY_BLOCK_LOG_DIMX 3 #define KEY_BLOCK_LOG_DIMY 3 #define KEY_BLOCK_DIMX (1< __global__ void FilterH(cudaTextureObject_t texData, float* d_result, int width) { const int HALF_WIDTH = FW >> 1; const int CACHE_WIDTH = FILTERH_TILE_WIDTH + FW -1; const int CACHE_COUNT = 2 + (CACHE_WIDTH - 2)/ FILTERH_TILE_WIDTH; __shared__ float data[CACHE_WIDTH]; const int bcol = IMUL(blockIdx.x, FILTERH_TILE_WIDTH); const int col = bcol + threadIdx.x; const int index_min = IMUL(blockIdx.y, width); const int index_max = index_min + width - 1; int src_index = index_min + bcol - HALF_WIDTH + threadIdx.x; int cache_index = threadIdx.x; float value = 0; #pragma unroll for(int j = 0; j < CACHE_COUNT; ++j) { if(cache_index < CACHE_WIDTH) { int fetch_index = src_index < index_min? index_min : (src_index > index_max ? index_max : src_index); data[cache_index] = tex1Dfetch(texData,fetch_index); src_index += FILTERH_TILE_WIDTH; cache_index += FILTERH_TILE_WIDTH; } } __syncthreads(); if(col >= width) return; #pragma unroll for(int i = 0; i < FW; ++i) { value += (data[threadIdx.x + i]* d_kernel[i]); } // value = Conv(data + threadIdx.x); d_result[index_min + col] = value; } //////////////////////////////////////////////////////////////////// template __global__ void FilterV(cudaTextureObject_t texData, float* d_result, int width, int height) { const int HALF_WIDTH = FW >> 1; const int CACHE_WIDTH = FW + FILTERV_TILE_HEIGHT - 1; const int TEMP = CACHE_WIDTH & 0xf; //add some extra space to avoid bank conflict #if FILTERV_TILE_WIDTH == 16 //make the stride 16 * n +/- 1 const int EXTRA = (TEMP == 1 || TEMP == 0) ? 1 - TEMP : 15 - TEMP; #elif FILTERV_TILE_WIDTH == 8 //make the stride 16 * n +/- 2 const int EXTRA = (TEMP == 2 || TEMP == 1 || TEMP == 0) ? 2 - TEMP : (TEMP == 15? 3 : 14 - TEMP); #elif FILTERV_TILE_WIDTH == 4 //make the stride 16 * n +/- 4 const int EXTRA = (TEMP >=0 && TEMP <=4) ? 4 - TEMP : (TEMP > 12? 20 - TEMP : 12 - TEMP); #else #error #endif const int CACHE_TRUE_WIDTH = CACHE_WIDTH + EXTRA; const int CACHE_COUNT = (CACHE_WIDTH + FILTERV_BLOCK_HEIGHT - 1) / FILTERV_BLOCK_HEIGHT; const int WRITE_COUNT = (FILTERV_TILE_HEIGHT + FILTERV_BLOCK_HEIGHT -1) / FILTERV_BLOCK_HEIGHT; __shared__ float data[CACHE_TRUE_WIDTH * FILTERV_TILE_WIDTH]; const int row_block_first = IMUL(blockIdx.y, FILTERV_TILE_HEIGHT); const int col = IMUL(blockIdx.x, FILTERV_TILE_WIDTH) + threadIdx.x; const int row_first = row_block_first - HALF_WIDTH; const int data_index_max = IMUL(height - 1, width) + col; const int cache_col_start = threadIdx.y; const int cache_row_start = IMUL(threadIdx.x, CACHE_TRUE_WIDTH); int cache_index = cache_col_start + cache_row_start; int data_index = IMUL(row_first + cache_col_start, width) + col; if(col < width) { #pragma unroll for(int i = 0; i < CACHE_COUNT; ++i) { if(cache_col_start < CACHE_WIDTH - i * FILTERV_BLOCK_HEIGHT) { int fetch_index = data_index < col ? col : (data_index > data_index_max? data_index_max : data_index); data[cache_index + i * FILTERV_BLOCK_HEIGHT] = tex1Dfetch(texData,fetch_index); data_index += IMUL(FILTERV_BLOCK_HEIGHT, width); } } } __syncthreads(); if(col >= width) return; int row = row_block_first + threadIdx.y; int index_start = cache_row_start + threadIdx.y; #pragma unroll for(int i = 0; i < WRITE_COUNT; ++i, row += FILTERV_BLOCK_HEIGHT, index_start += FILTERV_BLOCK_HEIGHT) { if(row < height) { int index_dest = IMUL(row, width) + col; float value = 0; #pragma unroll for(int i = 0; i < FW; ++i) { value += (data[index_start + i] * d_kernel[i]); } d_result[index_dest] = value; } } } template __global__ void UpsampleKernel(cudaTextureObject_t texData, float* d_result, int width) { const int SCALE = (1 << LOG_SCALE), SCALE_MASK = (SCALE - 1); const float INV_SCALE = 1.0f / (float(SCALE)); int col = IMUL(blockIdx.x, FILTERH_TILE_WIDTH) + threadIdx.x; if(col >= width) return; int row = blockIdx.y >> LOG_SCALE; int index = row * width + col; int dst_row = blockIdx.y; int dst_idx= (width * dst_row + col) * SCALE; int helper = blockIdx.y & SCALE_MASK; if (helper) { float v11 = tex1Dfetch(texData, index); float v12 = tex1Dfetch(texData, index + 1); index += width; float v21 = tex1Dfetch(texData, index); float v22 = tex1Dfetch(texData, index + 1); float w1 = INV_SCALE * helper, w2 = 1.0 - w1; float v1 = (v21 * w1 + w2 * v11); float v2 = (v22 * w1 + w2 * v12); d_result[dst_idx] = v1; #pragma unroll for(int i = 1; i < SCALE; ++i) { const float r2 = i * INV_SCALE; const float r1 = 1.0f - r2; d_result[dst_idx +i] = v1 * r1 + v2 * r2; } }else { float v1 = tex1Dfetch(texData, index); float v2 = tex1Dfetch(texData, index + 1); d_result[dst_idx] = v1; #pragma unroll for(int i = 1; i < SCALE; ++i) { const float r2 = i * INV_SCALE; const float r1 = 1.0f - r2; d_result[dst_idx +i] = v1 * r1 + v2 * r2; } } } //////////////////////////////////////////////////////////////////////////////////////// void ProgramCU::SampleImageU(CuTexImage *dst, CuTexImage *src, int log_scale) { int width = src->GetImgWidth(), height = src->GetImgHeight(); CuTexImage::CuTexObj srcTex = src->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 grid((width + FILTERH_TILE_WIDTH - 1)/ FILTERH_TILE_WIDTH, height << log_scale); dim3 block(FILTERH_TILE_WIDTH); switch(log_scale) { case 1 : UpsampleKernel<1> <<< grid, block>>> (srcTex.handle, (float*) dst->_cuData, width); break; case 2 : UpsampleKernel<2> <<< grid, block>>> (srcTex.handle, (float*) dst->_cuData, width); break; case 3 : UpsampleKernel<3> <<< grid, block>>> (srcTex.handle, (float*) dst->_cuData, width); break; default: break; } } template __global__ void DownsampleKernel(cudaTextureObject_t texData, float* d_result, int src_width, int dst_width) { const int dst_col = IMUL(blockIdx.x, FILTERH_TILE_WIDTH) + threadIdx.x; if(dst_col >= dst_width) return; const int src_col = min((dst_col << LOG_SCALE), (src_width - 1)); const int dst_row = blockIdx.y; const int src_row = blockIdx.y << LOG_SCALE; const int src_idx = IMUL(src_row, src_width) + src_col; const int dst_idx = IMUL(dst_width, dst_row) + dst_col; d_result[dst_idx] = tex1Dfetch(texData, src_idx); } __global__ void DownsampleKernel(cudaTextureObject_t texData, float* d_result, int src_width, int dst_width, const int log_scale) { const int dst_col = IMUL(blockIdx.x, FILTERH_TILE_WIDTH) + threadIdx.x; if(dst_col >= dst_width) return; const int src_col = min((dst_col << log_scale), (src_width - 1)); const int dst_row = blockIdx.y; const int src_row = blockIdx.y << log_scale; const int src_idx = IMUL(src_row, src_width) + src_col; const int dst_idx = IMUL(dst_width, dst_row) + dst_col; d_result[dst_idx] = tex1Dfetch(texData, src_idx); } void ProgramCU::SampleImageD(CuTexImage *dst, CuTexImage *src, int log_scale) { int src_width = src->GetImgWidth(), dst_width = dst->GetImgWidth() ; CuTexImage::CuTexObj srcTex = src->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 grid((dst_width + FILTERH_TILE_WIDTH - 1)/ FILTERH_TILE_WIDTH, dst->GetImgHeight()); dim3 block(FILTERH_TILE_WIDTH); switch(log_scale) { case 1 : DownsampleKernel<1> <<< grid, block>>> (srcTex.handle, (float*) dst->_cuData, src_width, dst_width); break; case 2 : DownsampleKernel<2> <<< grid, block>>> (srcTex.handle, (float*) dst->_cuData, src_width, dst_width); break; case 3 : DownsampleKernel<3> <<< grid, block>>> (srcTex.handle, (float*) dst->_cuData, src_width, dst_width); break; default: DownsampleKernel <<< grid, block>>> (srcTex.handle, (float*) dst->_cuData, src_width, dst_width, log_scale); } } __global__ void ChannelReduce_Kernel(cudaTextureObject_t texData, float* d_result) { int index = IMUL(blockIdx.x, FILTERH_TILE_WIDTH) + threadIdx.x; d_result[index] = tex1Dfetch(texData, index*4); } __global__ void ChannelReduce_Convert_Kernel(cudaTextureObject_t texDataF4, float* d_result) { int index = IMUL(blockIdx.x, FILTERH_TILE_WIDTH) + threadIdx.x; float4 rgba = tex1Dfetch(texDataF4, index); d_result[index] = 0.299f * rgba.x + 0.587f* rgba.y + 0.114f * rgba.z; } void ProgramCU::ReduceToSingleChannel(CuTexImage* dst, CuTexImage* src, int convert_rgb) { int width = src->GetImgWidth(), height = dst->GetImgHeight() ; dim3 grid((width * height + FILTERH_TILE_WIDTH - 1)/ FILTERH_TILE_WIDTH); dim3 block(FILTERH_TILE_WIDTH); if(convert_rgb) { CuTexImage::CuTexObj srcTex = src->BindTexture(texDataDesc, cudaCreateChannelDesc()); ChannelReduce_Convert_Kernel<<>>(srcTex.handle, (float*)dst->_cuData); }else { CuTexImage::CuTexObj srcTex = src->BindTexture(texDataDesc, cudaCreateChannelDesc()); ChannelReduce_Kernel<<>>(srcTex.handle, (float*)dst->_cuData); } } __global__ void ConvertByteToFloat_Kernel(cudaTextureObject_t texDataB, float* d_result) { int index = IMUL(blockIdx.x, FILTERH_TILE_WIDTH) + threadIdx.x; d_result[index] = tex1Dfetch(texDataB, index); } void ProgramCU::ConvertByteToFloat(CuTexImage*src, CuTexImage* dst) { int width = src->GetImgWidth(), height = dst->GetImgHeight() ; dim3 grid((width * height + FILTERH_TILE_WIDTH - 1)/ FILTERH_TILE_WIDTH); dim3 block(FILTERH_TILE_WIDTH); CuTexImage::CuTexObj srcTex = src->BindTexture(texDataBDesc, cudaCreateChannelDesc()); ConvertByteToFloat_Kernel<<>>(srcTex.handle, (float*)dst->_cuData); } void ProgramCU::CreateFilterKernel(float sigma, float* kernel, int& width) { int i, sz = int( ceil( GlobalUtil::_FilterWidthFactor * sigma -0.5) ) ;// width = 2*sz + 1; if(width > KERNEL_MAX_WIDTH) { //filter size truncation sz = KERNEL_MAX_WIDTH >> 1; width =KERNEL_MAX_WIDTH; }else if(width < KERNEL_MIN_WIDTH) { sz = KERNEL_MIN_WIDTH >> 1; width =KERNEL_MIN_WIDTH; } float rv = 1.0f/(sigma*sigma), v, ksum =0; // pre-compute filter for( i = -sz ; i <= sz ; ++i) { kernel[i+sz] = v = exp(-0.5f * i * i *rv) ; ksum += v; } //normalize the kernel rv = 1.0f/ksum; for(i = 0; i< width ;i++) kernel[i]*=rv; } template void ProgramCU::FilterImage(CuTexImage *dst, CuTexImage *src, CuTexImage* buf) { int width = src->GetImgWidth(), height = src->GetImgHeight(); //horizontal filtering CuTexImage::CuTexObj srcTex = src->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 gridh((width + FILTERH_TILE_WIDTH - 1)/ FILTERH_TILE_WIDTH, height); dim3 blockh(FILTERH_TILE_WIDTH); FilterH<<>>(srcTex.handle, (float*)buf->_cuData, width); CheckErrorCUDA("FilterH"); ///vertical filtering CuTexImage::CuTexObj bufTex = buf->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 gridv((width + FILTERV_TILE_WIDTH - 1)/ FILTERV_TILE_WIDTH, (height + FILTERV_TILE_HEIGHT - 1)/FILTERV_TILE_HEIGHT); dim3 blockv(FILTERV_TILE_WIDTH, FILTERV_BLOCK_HEIGHT); FilterV<<>>(bufTex.handle, (float*)dst->_cuData, width, height); CheckErrorCUDA("FilterV"); } ////////////////////////////////////////////////////////////////////// // tested on 2048x1500 image, the time on pyramid construction is // OpenGL version : 18ms // CUDA version: 28 ms void ProgramCU::FilterImage(CuTexImage *dst, CuTexImage *src, CuTexImage* buf, float sigma) { float filter_kernel[KERNEL_MAX_WIDTH]; int width; CreateFilterKernel(sigma, filter_kernel, width); cudaMemcpyToSymbol(d_kernel, filter_kernel, width * sizeof(float), 0, cudaMemcpyHostToDevice); switch(width) { case 5: FilterImage< 5>(dst, src, buf); break; case 7: FilterImage< 7>(dst, src, buf); break; case 9: FilterImage< 9>(dst, src, buf); break; case 11: FilterImage<11>(dst, src, buf); break; case 13: FilterImage<13>(dst, src, buf); break; case 15: FilterImage<15>(dst, src, buf); break; case 17: FilterImage<17>(dst, src, buf); break; case 19: FilterImage<19>(dst, src, buf); break; case 21: FilterImage<21>(dst, src, buf); break; case 23: FilterImage<23>(dst, src, buf); break; case 25: FilterImage<25>(dst, src, buf); break; case 27: FilterImage<27>(dst, src, buf); break; case 29: FilterImage<29>(dst, src, buf); break; case 31: FilterImage<31>(dst, src, buf); break; case 33: FilterImage<33>(dst, src, buf); break; default: break; } } void __global__ ComputeDOG_Kernel(cudaTextureObject_t texC, cudaTextureObject_t texP, float* d_dog, float2* d_got, int width, int height) { int row = (blockIdx.y << DOG_BLOCK_LOG_DIMY) + threadIdx.y; int col = (blockIdx.x << DOG_BLOCK_LOG_DIMX) + threadIdx.x; if(col < width && row < height) { int index = IMUL(row, width) + col; float vp = tex1Dfetch(texP, index); float v = tex1Dfetch(texC, index); d_dog[index] = v - vp; float vxn = tex1Dfetch(texC, index + 1); float vxp = tex1Dfetch(texC, index - 1); float vyp = tex1Dfetch(texC, index - width); float vyn = tex1Dfetch(texC, index + width); float dx = vxn - vxp, dy = vyn - vyp; float grd = 0.5f * sqrt(dx * dx + dy * dy); float rot = (grd == 0.0f? 0.0f : atan2(dy, dx)); d_got[index] = make_float2(grd, rot); } } void __global__ ComputeDOG_Kernel(cudaTextureObject_t texC, cudaTextureObject_t texP, float* d_dog, int width, int height) { int row = (blockIdx.y << DOG_BLOCK_LOG_DIMY) + threadIdx.y; int col = (blockIdx.x << DOG_BLOCK_LOG_DIMX) + threadIdx.x; if(col < width && row < height) { int index = IMUL(row, width) + col; float vp = tex1Dfetch(texP, index); float v = tex1Dfetch(texC, index); d_dog[index] = v - vp; } } void ProgramCU::ComputeDOG(CuTexImage* gus, CuTexImage* dog, CuTexImage* got) { int width = gus->GetImgWidth(), height = gus->GetImgHeight(); dim3 grid((width + DOG_BLOCK_DIMX - 1)/ DOG_BLOCK_DIMX, (height + DOG_BLOCK_DIMY - 1)/DOG_BLOCK_DIMY); dim3 block(DOG_BLOCK_DIMX, DOG_BLOCK_DIMY); CuTexImage::CuTexObj texCObj = gus->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj texPObj = (gus-1)->BindTexture(texDataDesc, cudaCreateChannelDesc()); if(got->_cuData) ComputeDOG_Kernel<<>>(texCObj.handle, texPObj.handle, (float*) dog->_cuData, (float2*) got->_cuData, width, height); else ComputeDOG_Kernel<<>>(texCObj.handle, texPObj.handle, (float*) dog->_cuData, width, height); } #define READ_CMP_DOG_DATA(datai, tex, idx) \ datai[0] = tex1Dfetch(tex, idx - 1);\ datai[1] = tex1Dfetch(tex, idx);\ datai[2] = tex1Dfetch(tex, idx + 1);\ if(v > nmax)\ {\ nmax = max(nmax, datai[0]);\ nmax = max(nmax, datai[1]);\ nmax = max(nmax, datai[2]);\ if(v < nmax) goto key_finish;\ }else\ {\ nmin = min(nmin, datai[0]);\ nmin = min(nmin, datai[1]);\ nmin = min(nmin, datai[2]);\ if(v > nmin) goto key_finish;\ } void __global__ ComputeKEY_Kernel(cudaTextureObject_t texP, cudaTextureObject_t texC, cudaTextureObject_t texN, float4* d_key, int width, int colmax, int rowmax, float dog_threshold0, float dog_threshold, float edge_threshold, int subpixel_localization) { float data[3][3], v; float datap[3][3], datan[3][3]; #ifdef KEY_OFFSET_ONE int row = (blockIdx.y << KEY_BLOCK_LOG_DIMY) + threadIdx.y + 1; int col = (blockIdx.x << KEY_BLOCK_LOG_DIMX) + threadIdx.x + 1; #else int row = (blockIdx.y << KEY_BLOCK_LOG_DIMY) + threadIdx.y; int col = (blockIdx.x << KEY_BLOCK_LOG_DIMX) + threadIdx.x; #endif int index = IMUL(row, width) + col; int idx[3] ={index - width, index, index + width}; int in_image =0; float nmax, nmin, result = 0.0f; float dx = 0, dy = 0, ds = 0; bool offset_test_passed = true; #ifdef KEY_OFFSET_ONE if(row < rowmax && col < colmax) #else if(row > 0 && col > 0 && row < rowmax && col < colmax) #endif { in_image = 1; data[1][1] = v = tex1Dfetch(texC, idx[1]); if(fabs(v) <= dog_threshold0) goto key_finish; data[1][0] = tex1Dfetch(texC, idx[1] - 1); data[1][2] = tex1Dfetch(texC, idx[1] + 1); nmax = max(data[1][0], data[1][2]); nmin = min(data[1][0], data[1][2]); if(v <=nmax && v >= nmin) goto key_finish; //if((v > nmax && v < 0 )|| (v < nmin && v > 0)) goto key_finish; READ_CMP_DOG_DATA(data[0], texC, idx[0]); READ_CMP_DOG_DATA(data[2], texC, idx[2]); //edge supression float vx2 = v * 2.0f; float fxx = data[1][0] + data[1][2] - vx2; float fyy = data[0][1] + data[2][1] - vx2; float fxy = 0.25f * (data[2][2] + data[0][0] - data[2][0] - data[0][2]); float temp1 = fxx * fyy - fxy * fxy; float temp2 = (fxx + fyy) * (fxx + fyy); if(temp1 <=0 || temp2 > edge_threshold * temp1) goto key_finish; //read the previous level READ_CMP_DOG_DATA(datap[0], texP, idx[0]); READ_CMP_DOG_DATA(datap[1], texP, idx[1]); READ_CMP_DOG_DATA(datap[2], texP, idx[2]); //read the next level READ_CMP_DOG_DATA(datan[0], texN, idx[0]); READ_CMP_DOG_DATA(datan[1], texN, idx[1]); READ_CMP_DOG_DATA(datan[2], texN, idx[2]); if(subpixel_localization) { //subpixel localization float fx = 0.5f * (data[1][2] - data[1][0]); float fy = 0.5f * (data[2][1] - data[0][1]); float fs = 0.5f * (datan[1][1] - datap[1][1]); float fss = (datan[1][1] + datap[1][1] - vx2); float fxs = 0.25f* (datan[1][2] + datap[1][0] - datan[1][0] - datap[1][2]); float fys = 0.25f* (datan[2][1] + datap[0][1] - datan[0][1] - datap[2][1]); //need to solve dx, dy, ds; // |-fx| | fxx fxy fxs | |dx| // |-fy| = | fxy fyy fys | * |dy| // |-fs| | fxs fys fss | |ds| float4 A0 = fxx > 0? make_float4(fxx, fxy, fxs, -fx) : make_float4(-fxx, -fxy, -fxs, fx); float4 A1 = fxy > 0? make_float4(fxy, fyy, fys, -fy) : make_float4(-fxy, -fyy, -fys, fy); float4 A2 = fxs > 0? make_float4(fxs, fys, fss, -fs) : make_float4(-fxs, -fys, -fss, fs); float maxa = max(max(A0.x, A1.x), A2.x); if(maxa >= 1e-10) { if(maxa == A1.x) { float4 TEMP = A1; A1 = A0; A0 = TEMP; }else if(maxa == A2.x) { float4 TEMP = A2; A2 = A0; A0 = TEMP; } A0.y /= A0.x; A0.z /= A0.x; A0.w/= A0.x; A1.y -= A1.x * A0.y; A1.z -= A1.x * A0.z; A1.w -= A1.x * A0.w; A2.y -= A2.x * A0.y; A2.z -= A2.x * A0.z; A2.w -= A2.x * A0.w; if(abs(A2.y) > abs(A1.y)) { float4 TEMP = A2; A2 = A1; A1 = TEMP; } if(abs(A1.y) >= 1e-10) { A1.z /= A1.y; A1.w /= A1.y; A2.z -= A2.y * A1.z; A2.w -= A2.y * A1.w; if(abs(A2.z) >= 1e-10) { ds = A2.w / A2.z; dy = A1.w - ds * A1.z; dx = A0.w - ds * A0.z - dy * A0.y; offset_test_passed = fabs(data[1][1] + 0.5f * (dx * fx + dy * fy + ds * fs)) > dog_threshold &&fabs(ds) < 1.0f && fabs(dx) < 1.0f && fabs(dy) < 1.0f; } } } } if(offset_test_passed) result = v > nmax ? 1.0 : -1.0; } key_finish: if(in_image) d_key[index] = make_float4(result, dx, dy, ds); } void ProgramCU::ComputeKEY(CuTexImage* dog, CuTexImage* key, float Tdog, float Tedge) { int width = dog->GetImgWidth(), height = dog->GetImgHeight(); float Tdog1 = (GlobalUtil::_SubpixelLocalization? 0.8f : 1.0f) * Tdog; CuTexImage* dogp = dog - 1; CuTexImage* dogn = dog + 1; #ifdef KEY_OFFSET_ONE dim3 grid((width - 1 + KEY_BLOCK_DIMX - 1)/ KEY_BLOCK_DIMX, (height - 1 + KEY_BLOCK_DIMY - 1)/KEY_BLOCK_DIMY); #else dim3 grid((width + KEY_BLOCK_DIMX - 1)/ KEY_BLOCK_DIMX, (height + KEY_BLOCK_DIMY - 1)/KEY_BLOCK_DIMY); #endif dim3 block(KEY_BLOCK_DIMX, KEY_BLOCK_DIMY); CuTexImage::CuTexObj texPObj = dogp->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj texCObj = dog->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj texNObj = dogn->BindTexture(texDataDesc, cudaCreateChannelDesc()); Tedge = (Tedge+1)*(Tedge+1)/Tedge; ComputeKEY_Kernel<<>>(texPObj.handle, texCObj.handle, texNObj.handle, (float4*) key->_cuData, width, width -1, height -1, Tdog1, Tdog, Tedge, GlobalUtil::_SubpixelLocalization); } void __global__ InitHist_Kernel(cudaTextureObject_t texDataF4, int4* hist, int ws, int wd, int height) { int row = IMUL(blockIdx.y, blockDim.y) + threadIdx.y; int col = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; if(row < height && col < wd) { int hidx = IMUL(row, wd) + col; int scol = col << 2; int sidx = IMUL(row, ws) + scol; int v[4] = {0, 0, 0, 0}; if(row > 0 && row < height -1) { #pragma unroll for(int i = 0; i < 4 ; ++i, ++scol) { float4 temp = tex1Dfetch(texDataF4, sidx +i); v[i] = (scol < ws -1 && scol > 0 && temp.x!=0) ? 1 : 0; } } hist[hidx] = make_int4(v[0], v[1], v[2], v[3]); } } void ProgramCU::InitHistogram(CuTexImage* key, CuTexImage* hist) { int ws = key->GetImgWidth(), hs = key->GetImgHeight(); int wd = hist->GetImgWidth(), hd = hist->GetImgHeight(); dim3 grid((wd + HIST_INIT_WIDTH - 1)/ HIST_INIT_WIDTH, hd); dim3 block(HIST_INIT_WIDTH, 1); CuTexImage::CuTexObj keyTex = key->BindTexture(texDataDesc, cudaCreateChannelDesc()); InitHist_Kernel<<>>(keyTex.handle, (int4*) hist->_cuData, ws, wd, hd); } void __global__ ReduceHist_Kernel(cudaTextureObject_t texDataI4, int4* d_hist, int ws, int wd, int height) { int row = IMUL(blockIdx.y, blockDim.y) + threadIdx.y; int col = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; if(row < height && col < wd) { int hidx = IMUL(row, wd) + col; int scol = col << 2; int sidx = IMUL(row, ws) + scol; int v[4] = {0, 0, 0, 0}; #pragma unroll for(int i = 0; i < 4 && scol < ws; ++i, ++scol) { int4 temp = tex1Dfetch(texDataI4, sidx + i); v[i] = temp.x + temp.y + temp.z + temp.w; } d_hist[hidx] = make_int4(v[0], v[1], v[2], v[3]); } } void ProgramCU::ReduceHistogram(CuTexImage*hist1, CuTexImage* hist2) { int ws = hist1->GetImgWidth(), hs = hist1->GetImgHeight(); int wd = hist2->GetImgWidth(), hd = hist2->GetImgHeight(); int temp = (int)floorf(logf(float(wd * 2/ 3)) / logf(2.0f)); const int wi = min(7, max(temp , 0)); CuTexImage::CuTexObj hist1Tex = hist1->BindTexture(texDataDesc, cudaCreateChannelDesc()); const int BW = 1 << wi, BH = 1 << (7 - wi); dim3 grid((wd + BW - 1)/ BW, (hd + BH -1) / BH); dim3 block(BW, BH); ReduceHist_Kernel<<>>(hist1Tex.handle, (int4*)hist2->_cuData, ws, wd, hd); } void __global__ ListGen_Kernel(cudaTextureObject_t texDataList, cudaTextureObject_t texDataI4, int4* d_list, int list_len, int width) { int idx1 = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; int4 pos = tex1Dfetch(texDataList, idx1); int idx2 = IMUL(pos.y, width) + pos.x; int4 temp = tex1Dfetch(texDataI4, idx2); int sum1 = temp.x + temp.y; int sum2 = sum1 + temp.z; pos.x <<= 2; if(pos.z >= sum2) { pos.x += 3; pos.z -= sum2; }else if(pos.z >= sum1) { pos.x += 2; pos.z -= sum1; }else if(pos.z >= temp.x) { pos.x += 1; pos.z -= temp.x; } if (idx1 < list_len) { d_list[idx1] = pos; } } //input list (x, y) (x, y) .... void ProgramCU::GenerateList(CuTexImage* list, CuTexImage* hist) { int len = list->GetImgWidth(); CuTexImage::CuTexObj listTex = list->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj histTex = hist->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 grid((len + LISTGEN_BLOCK_DIM -1) /LISTGEN_BLOCK_DIM); dim3 block(LISTGEN_BLOCK_DIM); ListGen_Kernel<<>>(listTex.handle, histTex.handle, (int4*) list->_cuData, len, hist->GetImgWidth()); } void __global__ ComputeOrientation_Kernel(cudaTextureObject_t texDataF2, cudaTextureObject_t texDataF4, cudaTextureObject_t texDataList, float4* d_list, int list_len, int width, int height, float sigma, float sigma_step, float gaussian_factor, float sample_factor, int num_orientation, int existing_keypoint, int subpixel, int keepsign) { const float ten_degree_per_radius = 5.7295779513082320876798154814105; const float radius_per_ten_degrees = 1.0 / 5.7295779513082320876798154814105; int idx = IMUL(blockDim.x, blockIdx.x) + threadIdx.x; if(idx >= list_len) return; float4 key; if(existing_keypoint) { key = tex1Dfetch(texDataF4, idx); }else { int4 ikey = tex1Dfetch(texDataList, idx); key.x = ikey.x + 0.5f; key.y = ikey.y + 0.5f; key.z = sigma; if(subpixel || keepsign) { float4 offset = tex1Dfetch(texDataF4, IMUL(width, ikey.y) + ikey.x); if(subpixel) { key.x += offset.y; key.y += offset.z; key.z *= pow(sigma_step, offset.w); } if(keepsign) key.z *= offset.x; } } if(num_orientation == 0) { key.w = 0; d_list[idx] = key; return; } float vote[37]; float gsigma = key.z * gaussian_factor; float win = fabs(key.z) * sample_factor; float dist_threshold = win * win + 0.5; float factor = -0.5f / (gsigma * gsigma); float xmin = max(1.5f, floorf(key.x - win) + 0.5f); float ymin = max(1.5f, floorf(key.y - win) + 0.5f); float xmax = min(width - 1.5f, floorf(key.x + win) + 0.5f); float ymax = min(height -1.5f, floorf(key.y + win) + 0.5f); #pragma unroll for(int i = 0; i < 36; ++i) vote[i] = 0.0f; for(float y = ymin; y <= ymax; y += 1.0f) { for(float x = xmin; x <= xmax; x += 1.0f) { float dx = x - key.x; float dy = y - key.y; float sq_dist = dx * dx + dy * dy; if(sq_dist >= dist_threshold) continue; float2 got = tex2D(texDataF2, x, y); float weight = got.x * exp(sq_dist * factor); float fidx = floorf(got.y * ten_degree_per_radius); int oidx = fidx; if(oidx < 0) oidx += 36; vote[oidx] += weight; } } //filter the vote const float one_third = 1.0 /3.0; #pragma unroll for(int i = 0; i < 6; ++i) { vote[36] = vote[0]; float pre = vote[35]; #pragma unroll for(int j = 0; j < 36; ++j) { float temp = one_third * (pre + vote[j] + vote[j + 1]); pre = vote[j]; vote[j] = temp; } } vote[36] = vote[0]; if(num_orientation == 1 || existing_keypoint) { int index_max = 0; float max_vote = vote[0]; #pragma unroll for(int i = 1; i < 36; ++i) { index_max = vote[i] > max_vote? i : index_max; max_vote = max(max_vote, vote[i]); } float pre = vote[index_max == 0? 35 : index_max -1]; float next = vote[index_max + 1]; float weight = max_vote; float off = 0.5f * FDIV(next - pre, weight + weight - next - pre); key.w = radius_per_ten_degrees * (index_max + 0.5f + off); d_list[idx] = key; }else { float max_vote = vote[0]; #pragma unroll for(int i = 1; i < 36; ++i) max_vote = max(max_vote, vote[i]); float vote_threshold = max_vote * 0.8f; float pre = vote[35]; float max_rot[2], max_vot[2] = {0, 0}; int ocount = 0; #pragma unroll for(int i =0; i < 36; ++i) { float next = vote[i + 1]; if(vote[i] > vote_threshold && vote[i] > pre && vote[i] > next) { float di = 0.5f * FDIV(next - pre, vote[i] + vote[i] - next - pre); float rot = i + di + 0.5f; float weight = vote[i]; /// if(weight > max_vot[1]) { if(weight > max_vot[0]) { max_vot[1] = max_vot[0]; max_rot[1] = max_rot[0]; max_vot[0] = weight; max_rot[0] = rot; } else { max_vot[1] = weight; max_rot[1] = rot; } ocount ++; } } pre = vote[i]; } float fr1 = max_rot[0] / 36.0f; if(fr1 < 0) fr1 += 1.0f; unsigned short us1 = ocount == 0? 65535 : ((unsigned short )floorf(fr1 * 65535.0f)); unsigned short us2 = 65535; if(ocount > 1) { float fr2 = max_rot[1] / 36.0f; if(fr2 < 0) fr2 += 1.0f; us2 = (unsigned short ) floorf(fr2 * 65535.0f); } unsigned int uspack = (us2 << 16) | us1; key.w = __int_as_float(uspack); d_list[idx] = key; } } void ProgramCU::ComputeOrientation(CuTexImage* list, CuTexImage* got, CuTexImage*key, float sigma, float sigma_step, int existing_keypoint) { int len = list->GetImgWidth(); if(len <= 0) return; int width = got->GetImgWidth(), height = got->GetImgHeight(); CuTexImage::CuTexObj texObjF4; CuTexImage::CuTexObj texObjList; if(existing_keypoint) { texObjF4 = list->BindTexture(texDataDesc, cudaCreateChannelDesc()); }else { texObjList = list->BindTexture(texDataDesc, cudaCreateChannelDesc()); if(GlobalUtil::_SubpixelLocalization) { texObjF4 = key->BindTexture(texDataDesc, cudaCreateChannelDesc()); } } CuTexImage::CuTexObj gotTex = got->BindTexture2D(texDataDesc, cudaCreateChannelDesc()); const int block_width = len < ORIENTATION_COMPUTE_PER_BLOCK ? 16 : ORIENTATION_COMPUTE_PER_BLOCK; dim3 grid((len + block_width -1) / block_width); dim3 block(block_width); ComputeOrientation_Kernel<<>>( gotTex.handle, texObjF4.handle, texObjList.handle, (float4*) list->_cuData, len, width, height, sigma, sigma_step, GlobalUtil::_OrientationGaussianFactor, GlobalUtil::_OrientationGaussianFactor * GlobalUtil::_OrientationWindowFactor, GlobalUtil::_FixedOrientation? 0 : GlobalUtil::_MaxOrientation, existing_keypoint, GlobalUtil::_SubpixelLocalization, GlobalUtil::_KeepExtremumSign); ProgramCU::CheckErrorCUDA("ComputeOrientation"); } template void __global__ ComputeDescriptor_Kernel(cudaTextureObject_t texDataF2, cudaTextureObject_t texDataF4, float4* d_des, int num, int width, int height, float window_factor) { const float rpi = 4.0/ 3.14159265358979323846; int idx = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; int fidx = idx >> 4; if(fidx >= num) return; float4 key = tex1Dfetch(texDataF4, fidx); int bidx = idx& 0xf, ix = bidx & 0x3, iy = bidx >> 2; float spt = fabs(key.z * window_factor); float s, c; __sincosf(key.w, &s, &c); float anglef = key.w > 3.14159265358979323846? key.w - (2.0 * 3.14159265358979323846) : key.w ; float cspt = c * spt, sspt = s * spt; float crspt = c / spt, srspt = s / spt; float2 offsetpt, pt; float xmin, ymin, xmax, ymax, bsz; offsetpt.x = ix - 1.5f; offsetpt.y = iy - 1.5f; pt.x = cspt * offsetpt.x - sspt * offsetpt.y + key.x; pt.y = cspt * offsetpt.y + sspt * offsetpt.x + key.y; bsz = fabs(cspt) + fabs(sspt); xmin = max(1.5f, floorf(pt.x - bsz) + 0.5f); ymin = max(1.5f, floorf(pt.y - bsz) + 0.5f); xmax = min(width - 1.5f, floorf(pt.x + bsz) + 0.5f); ymax = min(height - 1.5f, floorf(pt.y + bsz) + 0.5f); float des[9]; #pragma unroll for(int i =0; i < 9; ++i) des[i] = 0.0f; for(float y = ymin; y <= ymax; y += 1.0f) { for(float x = xmin; x <= xmax; x += 1.0f) { float dx = x - pt.x; float dy = y - pt.y; float nx = crspt * dx + srspt * dy; float ny = crspt * dy - srspt * dx; float nxn = fabs(nx); float nyn = fabs(ny); if(nxn < 1.0f && nyn < 1.0f) { float2 cc = tex2D(texDataF2, x, y); float dnx = nx + offsetpt.x; float dny = ny + offsetpt.y; float ww = exp(-0.125f * (dnx * dnx + dny * dny)); float wx = 1.0 - nxn; float wy = 1.0 - nyn; float weight = ww * wx * wy * cc.x; float theta = (anglef - cc.y) * rpi; if(theta < 0) theta += 8.0f; float fo = floorf(theta); int fidx = fo; float weight1 = fo + 1.0f - theta; float weight2 = theta - fo; if(DYNAMIC_INDEXING) { des[fidx] += (weight1 * weight); des[fidx + 1] += (weight2 * weight); //this dynamic indexing part might be slow }else { #pragma unroll for(int k = 0; k < 8; ++k) { if(k == fidx) { des[k] += (weight1 * weight); des[k+1] += (weight2 * weight); } } } } } } des[0] += des[8]; int didx = idx << 1; d_des[didx] = make_float4(des[0], des[1], des[2], des[3]); d_des[didx+1] = make_float4(des[4], des[5], des[6], des[7]); } template void __global__ ComputeDescriptorRECT_Kernel(cudaTextureObject_t texDataF2, cudaTextureObject_t texDataF4, float4* d_des, int num, int width, int height, float window_factor) { const float rpi = 4.0/ 3.14159265358979323846; int idx = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; int fidx = idx >> 4; if(fidx >= num) return; float4 key = tex1Dfetch(texDataF4, fidx); int bidx = idx& 0xf, ix = bidx & 0x3, iy = bidx >> 2; //float aspect_ratio = key.w / key.z; //float aspect_sq = aspect_ratio * aspect_ratio; float sptx = key.z * 0.25, spty = key.w * 0.25; float xmin, ymin, xmax, ymax; float2 pt; pt.x = sptx * (ix + 0.5f) + key.x; pt.y = spty * (iy + 0.5f) + key.y; xmin = max(1.5f, floorf(pt.x - sptx) + 0.5f); ymin = max(1.5f, floorf(pt.y - spty) + 0.5f); xmax = min(width - 1.5f, floorf(pt.x + sptx) + 0.5f); ymax = min(height - 1.5f, floorf(pt.y + spty) + 0.5f); float des[9]; #pragma unroll for(int i =0; i < 9; ++i) des[i] = 0.0f; for(float y = ymin; y <= ymax; y += 1.0f) { for(float x = xmin; x <= xmax; x += 1.0f) { float nx = (x - pt.x) / sptx; float ny = (y - pt.y) / spty; float nxn = fabs(nx); float nyn = fabs(ny); if(nxn < 1.0f && nyn < 1.0f) { float2 cc = tex2D(texDataF2, x, y); float wx = 1.0 - nxn; float wy = 1.0 - nyn; float weight = wx * wy * cc.x; float theta = (- cc.y) * rpi; if(theta < 0) theta += 8.0f; float fo = floorf(theta); int fidx = fo; float weight1 = fo + 1.0f - theta; float weight2 = theta - fo; if(DYNAMIC_INDEXING) { des[fidx] += (weight1 * weight); des[fidx + 1] += (weight2 * weight); //this dynamic indexing part might be slow }else { #pragma unroll for(int k = 0; k < 8; ++k) { if(k == fidx) { des[k] += (weight1 * weight); des[k+1] += (weight2 * weight); } } } } } } des[0] += des[8]; int didx = idx << 1; d_des[didx] = make_float4(des[0], des[1], des[2], des[3]); d_des[didx+1] = make_float4(des[4], des[5], des[6], des[7]); } void __global__ NormalizeDescriptor_Kernel(cudaTextureObject_t texDataF4, float4* d_des, int num) { float4 temp[32]; int idx = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; if(idx >= num) return; int sidx = idx << 5; float norm1 = 0, norm2 = 0; #pragma unroll for(int i = 0; i < 32; ++i) { temp[i] = tex1Dfetch(texDataF4, sidx +i); norm1 += (temp[i].x * temp[i].x + temp[i].y * temp[i].y + temp[i].z * temp[i].z + temp[i].w * temp[i].w); } norm1 = rsqrt(norm1); #pragma unroll for(int i = 0; i < 32; ++i) { temp[i].x = min(0.2f, temp[i].x * norm1); temp[i].y = min(0.2f, temp[i].y * norm1); temp[i].z = min(0.2f, temp[i].z * norm1); temp[i].w = min(0.2f, temp[i].w * norm1); norm2 += (temp[i].x * temp[i].x + temp[i].y * temp[i].y + temp[i].z * temp[i].z + temp[i].w * temp[i].w); } norm2 = rsqrt(norm2); #pragma unroll for(int i = 0; i < 32; ++i) { temp[i].x *= norm2; temp[i].y *= norm2; temp[i].z *= norm2; temp[i].w *= norm2; d_des[sidx + i] = temp[i]; } } void ProgramCU::ComputeDescriptor(CuTexImage*list, CuTexImage* got, CuTexImage* dtex, int rect, int stream) { int num = list->GetImgWidth(); int width = got->GetImgWidth(); int height = got->GetImgHeight(); dtex->InitTexture(num * 128, 1, 1); CuTexImage::CuTexObj gotTex = got->BindTexture2D(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj listTex = list->BindTexture(texDataDesc, cudaCreateChannelDesc()); int block_width = DESCRIPTOR_COMPUTE_BLOCK_SIZE; dim3 grid((num * 16 + block_width -1) / block_width); dim3 block(block_width); if(rect) { if(GlobalUtil::_UseDynamicIndexing) ComputeDescriptorRECT_Kernel<<>>(gotTex.handle, listTex.handle, (float4*) dtex->_cuData, num, width, height, GlobalUtil::_DescriptorWindowFactor); else ComputeDescriptorRECT_Kernel<<>>(gotTex.handle, listTex.handle, (float4*) dtex->_cuData, num, width, height, GlobalUtil::_DescriptorWindowFactor); }else { if(GlobalUtil::_UseDynamicIndexing) ComputeDescriptor_Kernel<<>>(gotTex.handle, listTex.handle, (float4*) dtex->_cuData, num, width, height, GlobalUtil::_DescriptorWindowFactor); else ComputeDescriptor_Kernel<<>>(gotTex.handle, listTex.handle, (float4*) dtex->_cuData, num, width, height, GlobalUtil::_DescriptorWindowFactor); } if(GlobalUtil::_NormalizedSIFT) { CuTexImage::CuTexObj dtexTex = dtex->BindTexture(texDataDesc, cudaCreateChannelDesc()); const int block_width = DESCRIPTOR_NORMALIZ_PER_BLOCK; dim3 grid((num + block_width -1) / block_width); dim3 block(block_width); NormalizeDescriptor_Kernel<<>>(dtexTex.handle, (float4*) dtex->_cuData, num); } CheckErrorCUDA("ComputeDescriptor"); } ////////////////////////////////////////////////////// void ProgramCU::FinishCUDA() { cudaDeviceSynchronize(); } int ProgramCU::CheckErrorCUDA(const char* location) { cudaError_t e = cudaGetLastError(); if(e) { if(location) fprintf(stderr, "%s:\t", location); fprintf(stderr, "%s\n", cudaGetErrorString(e)); //assert(0); return 1; }else { return 0; } } void __global__ ConvertDOG_Kernel(cudaTextureObject_t texData, float* d_result, int width, int height) { int row = (blockIdx.y << BLOCK_LOG_DIM) + threadIdx.y; int col = (blockIdx.x << BLOCK_LOG_DIM) + threadIdx.x; if(col < width && row < height) { int index = row * width + col; float v = tex1Dfetch(texData, index); d_result[index] = (col == 0 || row == 0 || col == width -1 || row == height -1)? 0.5 : __saturatef(0.5+20.0*v); } } /// void ProgramCU::DisplayConvertDOG(CuTexImage* dog, CuTexImage* out) { if(out->_cuData == NULL) return; int width = dog->GetImgWidth(), height = dog ->GetImgHeight(); CuTexImage::CuTexObj dogTex = dog->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 grid((width + BLOCK_DIM - 1)/ BLOCK_DIM, (height + BLOCK_DIM - 1)/BLOCK_DIM); dim3 block(BLOCK_DIM, BLOCK_DIM); ConvertDOG_Kernel<<>>(dogTex.handle, (float*) out->_cuData, width, height); ProgramCU::CheckErrorCUDA("DisplayConvertDOG"); } void __global__ ConvertGRD_Kernel(cudaTextureObject_t texData, float* d_result, int width, int height) { int row = (blockIdx.y << BLOCK_LOG_DIM) + threadIdx.y; int col = (blockIdx.x << BLOCK_LOG_DIM) + threadIdx.x; if(col < width && row < height) { int index = row * width + col; float v = tex1Dfetch(texData, index << 1); d_result[index] = (col == 0 || row == 0 || col == width -1 || row == height -1)? 0 : __saturatef(5 * v); } } void ProgramCU::DisplayConvertGRD(CuTexImage* got, CuTexImage* out) { if(out->_cuData == NULL) return; int width = got->GetImgWidth(), height = got ->GetImgHeight(); CuTexImage::CuTexObj gotTex = got->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 grid((width + BLOCK_DIM - 1)/ BLOCK_DIM, (height + BLOCK_DIM - 1)/BLOCK_DIM); dim3 block(BLOCK_DIM, BLOCK_DIM); ConvertGRD_Kernel<<>>(gotTex.handle, (float*) out->_cuData, width, height); ProgramCU::CheckErrorCUDA("DisplayConvertGRD"); } void __global__ ConvertKEY_Kernel(cudaTextureObject_t texData, cudaTextureObject_t texDataF4, float4* d_result, int width, int height) { int row = (blockIdx.y << BLOCK_LOG_DIM) + threadIdx.y; int col = (blockIdx.x << BLOCK_LOG_DIM) + threadIdx.x; if(col < width && row < height) { int index = row * width + col; float4 keyv = tex1Dfetch(texDataF4, index); int is_key = (keyv.x == 1.0f || keyv.x == -1.0f); int inside = col > 0 && row > 0 && row < height -1 && col < width - 1; float v = inside? __saturatef(0.5 + 20 * tex1Dfetch(texData, index)) : 0.5; d_result[index] = is_key && inside ? (keyv.x > 0? make_float4(1.0f, 0, 0, 1.0f) : make_float4(0.0f, 1.0f, 0.0f, 1.0f)): make_float4(v, v, v, 1.0f) ; } } void ProgramCU::DisplayConvertKEY(CuTexImage* key, CuTexImage* dog, CuTexImage* out) { if(out->_cuData == NULL) return; int width = key->GetImgWidth(), height = key ->GetImgHeight(); CuTexImage::CuTexObj dogTex = dog->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj keyTex = key->BindTexture(texDataDesc, cudaCreateChannelDesc()); dim3 grid((width + BLOCK_DIM - 1)/ BLOCK_DIM, (height + BLOCK_DIM - 1)/BLOCK_DIM); dim3 block(BLOCK_DIM, BLOCK_DIM); ConvertKEY_Kernel<<>>(dogTex.handle, keyTex.handle, (float4*) out->_cuData, width, height); } void __global__ DisplayKeyPoint_Kernel(cudaTextureObject_t texDataF4, float4 * d_result, int num) { int idx = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; if(idx >= num) return; float4 v = tex1Dfetch(texDataF4, idx); d_result[idx] = make_float4(v.x, v.y, 0, 1.0f); } void ProgramCU::DisplayKeyPoint(CuTexImage* ftex, CuTexImage* out) { int num = ftex->GetImgWidth(); int block_width = 64; dim3 grid((num + block_width -1) /block_width); dim3 block(block_width); CuTexImage::CuTexObj ftexTex = ftex->BindTexture(texDataDesc, cudaCreateChannelDesc()); DisplayKeyPoint_Kernel<<>>(ftexTex.handle, (float4*) out->_cuData, num); ProgramCU::CheckErrorCUDA("DisplayKeyPoint"); } void __global__ DisplayKeyBox_Kernel(cudaTextureObject_t texDataF4, float4* d_result, int num) { int idx = IMUL(blockIdx.x, blockDim.x) + threadIdx.x; if(idx >= num) return; int kidx = idx / 10, vidx = idx - IMUL(kidx , 10); float4 v = tex1Dfetch(texDataF4, kidx); float sz = fabs(v.z * 3.0f); /////////////////////// float s, c; __sincosf(v.w, &s, &c); /////////////////////// float dx = vidx == 0? 0 : ((vidx <= 4 || vidx >= 9)? sz : -sz); float dy = vidx <= 1? 0 : ((vidx <= 2 || vidx >= 7)? -sz : sz); float4 pos; pos.x = v.x + c * dx - s * dy; pos.y = v.y + c * dy + s * dx; pos.z = 0; pos.w = 1.0f; d_result[idx] = pos; } void ProgramCU::DisplayKeyBox(CuTexImage* ftex, CuTexImage* out) { int len = ftex->GetImgWidth(); int block_width = 32; dim3 grid((len * 10 + block_width -1) / block_width); dim3 block(block_width); CuTexImage::CuTexObj ftexTex = ftex->BindTexture(texDataDesc, cudaCreateChannelDesc()); DisplayKeyBox_Kernel<<>>(ftexTex.handle, (float4*) out->_cuData, len * 10); } int ProgramCU::CheckCudaDevice(int device) { int count = 0, device_used; if(cudaGetDeviceCount(&count) != cudaSuccess || count <= 0) { ProgramCU::CheckErrorCUDA("CheckCudaDevice"); return 0; }else if(count == 1) { cudaDeviceProp deviceProp; if ( cudaGetDeviceProperties(&deviceProp, 0) != cudaSuccess || (deviceProp.major == 9999 && deviceProp.minor == 9999)) { fprintf(stderr, "CheckCudaDevice: no device supporting CUDA.\n"); return 0; }else { GlobalUtil::_MemCapGPU = deviceProp.totalGlobalMem / 1024; GlobalUtil::_texMaxDimGL = 32768; if(GlobalUtil::_verbose) fprintf(stdout, "NOTE: changing maximum texture dimension to %d\n", GlobalUtil::_texMaxDimGL); } } if(device >0 && device < count) { cudaSetDevice(device); CheckErrorCUDA("cudaSetDevice\n"); } cudaGetDevice(&device_used); if(device != device_used) fprintf(stderr, "\nERROR: Cannot set device to %d\n" "\nWARNING: Use # %d device instead (out of %d)\n", device, device_used, count); return 1; } //////////////////////////////////////////////////////////////////////////////////////// // siftmatch funtions ////////////////////////////////////////////////////////////////////////////////////////// #define MULT_TBLOCK_DIMX 128 #define MULT_TBLOCK_DIMY 1 #define MULT_BLOCK_DIMX (MULT_TBLOCK_DIMX) #define MULT_BLOCK_DIMY (8 * MULT_TBLOCK_DIMY) void __global__ MultiplyDescriptor_Kernel(cudaTextureObject_t texDes1, cudaTextureObject_t texDes2, int* d_result, int num1, int num2, int3* d_temp) { int idx01 = (blockIdx.y * MULT_BLOCK_DIMY), idx02 = (blockIdx.x * MULT_BLOCK_DIMX); int idx1 = idx01 + threadIdx.y, idx2 = idx02 + threadIdx.x; __shared__ int data1[17 * 2 * MULT_BLOCK_DIMY]; int read_idx1 = idx01 * 8 + threadIdx.x, read_idx2 = idx2 * 8; int col4 = threadIdx.x & 0x3, row4 = threadIdx.x >> 2; int cache_idx1 = IMUL(row4, 17) + (col4 << 2); /////////////////////////////////////////////////////////////// //Load feature descriptors /////////////////////////////////////////////////////////////// #if MULT_BLOCK_DIMY == 16 uint4 v = tex1Dfetch(texDes1, read_idx1); data1[cache_idx1] = v.x; data1[cache_idx1+1] = v.y; data1[cache_idx1+2] = v.z; data1[cache_idx1+3] = v.w; #elif MULT_BLOCK_DIMY == 8 if(threadIdx.x < 64) { uint4 v = tex1Dfetch(texDes1, read_idx1); data1[cache_idx1] = v.x; data1[cache_idx1+1] = v.y; data1[cache_idx1+2] = v.z; data1[cache_idx1+3] = v.w; } #else #error #endif __syncthreads(); /// if(idx2 >= num2) return; /////////////////////////////////////////////////////////////////////////// //compare descriptors int results[MULT_BLOCK_DIMY]; #pragma unroll for(int i = 0; i < MULT_BLOCK_DIMY; ++i) results[i] = 0; #pragma unroll for(int i = 0; i < 8; ++i) { uint4 v = tex1Dfetch(texDes2, read_idx2 + i); unsigned char* p2 = (unsigned char*)(&v); #pragma unroll for(int k = 0; k < MULT_BLOCK_DIMY; ++k) { unsigned char* p1 = (unsigned char*) (data1 + k * 34 + i * 4 + (i/4)); results[k] += ( IMUL(p1[0], p2[0]) + IMUL(p1[1], p2[1]) + IMUL(p1[2], p2[2]) + IMUL(p1[3], p2[3]) + IMUL(p1[4], p2[4]) + IMUL(p1[5], p2[5]) + IMUL(p1[6], p2[6]) + IMUL(p1[7], p2[7]) + IMUL(p1[8], p2[8]) + IMUL(p1[9], p2[9]) + IMUL(p1[10], p2[10]) + IMUL(p1[11], p2[11]) + IMUL(p1[12], p2[12]) + IMUL(p1[13], p2[13]) + IMUL(p1[14], p2[14]) + IMUL(p1[15], p2[15])); } } int dst_idx = IMUL(idx1, num2) + idx2; if(d_temp) { int3 cmp_result = make_int3(0, -1, 0); #pragma unroll for(int i = 0; i < MULT_BLOCK_DIMY; ++i) { if(idx1 + i < num1) { cmp_result = results[i] > cmp_result.x? make_int3(results[i], idx1 + i, cmp_result.x) : make_int3(cmp_result.x, cmp_result.y, max(cmp_result.z, results[i])); d_result[dst_idx + IMUL(i, num2)] = results[i]; } } d_temp[ IMUL(blockIdx.y, num2) + idx2] = cmp_result; }else { #pragma unroll for(int i = 0; i < MULT_BLOCK_DIMY; ++i) { if(idx1 + i < num1) d_result[dst_idx + IMUL(i, num2)] = results[i]; } } } void ProgramCU::MultiplyDescriptor(CuTexImage* des1, CuTexImage* des2, CuTexImage* texDot, CuTexImage* texCRT) { int num1 = des1->GetImgWidth() / 8; int num2 = des2->GetImgWidth() / 8; dim3 grid( (num2 + MULT_BLOCK_DIMX - 1)/ MULT_BLOCK_DIMX, (num1 + MULT_BLOCK_DIMY - 1)/MULT_BLOCK_DIMY); dim3 block(MULT_TBLOCK_DIMX, MULT_TBLOCK_DIMY); texDot->InitTexture( num2,num1); if(texCRT) texCRT->InitTexture(num2, (num1 + MULT_BLOCK_DIMY - 1)/MULT_BLOCK_DIMY, 32); CuTexImage::CuTexObj des1Tex = des1->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj des2Tex = des2->BindTexture(texDataDesc, cudaCreateChannelDesc()); MultiplyDescriptor_Kernel<<>>(des1Tex.handle, des2Tex.handle, (int*)texDot->_cuData, num1, num2, (texCRT? (int3*)texCRT->_cuData : NULL)); } struct Matrix33 { float mat[3][3]; }; void __global__ MultiplyDescriptorG_Kernel(cudaTextureObject_t texDes1, cudaTextureObject_t texDes2, cudaTextureObject_t texLoc1, cudaTextureObject_t texLoc2, int* d_result, int num1, int num2, int3* d_temp, Matrix33 H, float hdistmax, Matrix33 F, float fdistmax) { int idx01 = (blockIdx.y * MULT_BLOCK_DIMY); int idx02 = (blockIdx.x * MULT_BLOCK_DIMX); int idx1 = idx01 + threadIdx.y; int idx2 = idx02 + threadIdx.x; __shared__ int data1[17 * 2 * MULT_BLOCK_DIMY]; __shared__ float loc1[MULT_BLOCK_DIMY * 2]; int read_idx1 = idx01 * 8 + threadIdx.x ; int read_idx2 = idx2 * 8; int col4 = threadIdx.x & 0x3, row4 = threadIdx.x >> 2; int cache_idx1 = IMUL(row4, 17) + (col4 << 2); #if MULT_BLOCK_DIMY == 16 uint4 v = tex1Dfetch(texDes1, read_idx1); data1[cache_idx1] = v.x; data1[cache_idx1+1] = v.y; data1[cache_idx1+2] = v.z; data1[cache_idx1+3] = v.w; #elif MULT_BLOCK_DIMY == 8 if(threadIdx.x < 64) { uint4 v = tex1Dfetch(texDes1, read_idx1); data1[cache_idx1] = v.x; data1[cache_idx1+1] = v.y; data1[cache_idx1+2] = v.z; data1[cache_idx1+3] = v.w; } #else #error #endif __syncthreads(); if(threadIdx.x < MULT_BLOCK_DIMY * 2) { loc1[threadIdx.x] = tex1Dfetch(texLoc1, 2 * idx01 + threadIdx.x); } __syncthreads(); if(idx2 >= num2) return; int results[MULT_BLOCK_DIMY]; ///////////////////////////////////////////////////////////////////////////////////////////// //geometric verification ///////////////////////////////////////////////////////////////////////////////////////////// int good_count = 0; float2 loc2 = tex1Dfetch(texLoc2, idx2); #pragma unroll for(int i = 0; i < MULT_BLOCK_DIMY; ++i) { if(idx1 + i < num1) { float* loci = loc1 + i * 2; float locx = loci[0], locy = loci[1]; //homography float x[3], diff[2]; x[0] = H.mat[0][0] * locx + H.mat[0][1] * locy + H.mat[0][2]; x[1] = H.mat[1][0] * locx + H.mat[1][1] * locy + H.mat[1][2]; x[2] = H.mat[2][0] * locx + H.mat[2][1] * locy + H.mat[2][2]; diff[0] = FDIV(x[0], x[2]) - loc2.x; diff[1] = FDIV(x[1], x[2]) - loc2.y; float hdist = diff[0] * diff[0] + diff[1] * diff[1]; if(hdist < hdistmax) { //check fundamental matrix float fx1[3], ftx2[3], x2fx1, se; fx1[0] = F.mat[0][0] * locx + F.mat[0][1] * locy + F.mat[0][2]; fx1[1] = F.mat[1][0] * locx + F.mat[1][1] * locy + F.mat[1][2]; fx1[2] = F.mat[2][0] * locx + F.mat[2][1] * locy + F.mat[2][2]; ftx2[0] = F.mat[0][0] * loc2.x + F.mat[1][0] * loc2.y + F.mat[2][0]; ftx2[1] = F.mat[0][1] * loc2.x + F.mat[1][1] * loc2.y + F.mat[2][1]; //ftx2[2] = F.mat[0][2] * loc2.x + F.mat[1][2] * loc2.y + F.mat[2][2]; x2fx1 = loc2.x * fx1[0] + loc2.y * fx1[1] + fx1[2]; se = FDIV(x2fx1 * x2fx1, fx1[0] * fx1[0] + fx1[1] * fx1[1] + ftx2[0] * ftx2[0] + ftx2[1] * ftx2[1]); results[i] = se < fdistmax? 0: -262144; }else { results[i] = -262144; } }else { results[i] = -262144; } good_count += (results[i] >=0); } ///////////////////////////////////////////////////////////////////////////////////////////// ///compare feature descriptors anyway ///////////////////////////////////////////////////////////////////////////////////////////// if(good_count > 0) { #pragma unroll for(int i = 0; i < 8; ++i) { uint4 v = tex1Dfetch(texDes2, read_idx2 + i); unsigned char* p2 = (unsigned char*)(&v); #pragma unroll for(int k = 0; k < MULT_BLOCK_DIMY; ++k) { unsigned char* p1 = (unsigned char*) (data1 + k * 34 + i * 4 + (i/4)); results[k] += ( IMUL(p1[0], p2[0]) + IMUL(p1[1], p2[1]) + IMUL(p1[2], p2[2]) + IMUL(p1[3], p2[3]) + IMUL(p1[4], p2[4]) + IMUL(p1[5], p2[5]) + IMUL(p1[6], p2[6]) + IMUL(p1[7], p2[7]) + IMUL(p1[8], p2[8]) + IMUL(p1[9], p2[9]) + IMUL(p1[10], p2[10]) + IMUL(p1[11], p2[11]) + IMUL(p1[12], p2[12]) + IMUL(p1[13], p2[13]) + IMUL(p1[14], p2[14]) + IMUL(p1[15], p2[15])); } } } int dst_idx = IMUL(idx1, num2) + idx2; if(d_temp) { int3 cmp_result = make_int3(0, -1, 0); #pragma unroll for(int i= 0; i < MULT_BLOCK_DIMY; ++i) { if(idx1 + i < num1) { cmp_result = results[i] > cmp_result.x? make_int3(results[i], idx1 + i, cmp_result.x) : make_int3(cmp_result.x, cmp_result.y, max(cmp_result.z, results[i])); d_result[dst_idx + IMUL(i, num2)] = max(results[i], 0); }else { break; } } d_temp[ IMUL(blockIdx.y, num2) + idx2] = cmp_result; }else { #pragma unroll for(int i = 0; i < MULT_BLOCK_DIMY; ++i) { if(idx1 + i < num1) d_result[dst_idx + IMUL(i, num2)] = max(results[i], 0); else break; } } } void ProgramCU::MultiplyDescriptorG(CuTexImage* des1, CuTexImage* des2, CuTexImage* loc1, CuTexImage* loc2, CuTexImage* texDot, CuTexImage* texCRT, float* H, float hdistmax, float* F, float fdistmax) { int num1 = des1->GetImgWidth() / 8; int num2 = des2->GetImgWidth() / 8; Matrix33 MatF, MatH; //copy the matrix memcpy(MatF.mat, F, 9 * sizeof(float)); memcpy(MatH.mat, H, 9 * sizeof(float)); //thread blocks dim3 grid( (num2 + MULT_BLOCK_DIMX - 1)/ MULT_BLOCK_DIMX, (num1 + MULT_BLOCK_DIMY - 1)/MULT_BLOCK_DIMY); dim3 block(MULT_TBLOCK_DIMX, MULT_TBLOCK_DIMY); //intermediate results texDot->InitTexture( num2,num1); if(texCRT) texCRT->InitTexture( num2, (num1 + MULT_BLOCK_DIMY - 1)/MULT_BLOCK_DIMY, 3); CuTexImage::CuTexObj loc1Tex = loc1->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj loc2Tex = loc2->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj des1Tex = des1->BindTexture(texDataDesc, cudaCreateChannelDesc()); CuTexImage::CuTexObj des2Tex = des2->BindTexture(texDataDesc, cudaCreateChannelDesc()); MultiplyDescriptorG_Kernel<<>>(des1Tex.handle, des2Tex.handle, loc1Tex.handle, loc2Tex.handle, (int*)texDot->_cuData, num1, num2, (texCRT? (int3*)texCRT->_cuData : NULL), MatH, hdistmax, MatF, fdistmax); } #define ROWMATCH_BLOCK_WIDTH 32 #define ROWMATCH_BLOCK_HEIGHT 1 void __global__ RowMatch_Kernel(int*d_dot, int* d_result, int num2, float distmax, float ratiomax) { #if ROWMATCH_BLOCK_HEIGHT == 1 __shared__ int dotmax[ROWMATCH_BLOCK_WIDTH]; __shared__ int dotnxt[ROWMATCH_BLOCK_WIDTH]; __shared__ int dotidx[ROWMATCH_BLOCK_WIDTH]; int row = blockIdx.y; #else __shared__ int x_dotmax[ROWMATCH_BLOCK_HEIGHT][ROWMATCH_BLOCK_WIDTH]; __shared__ int x_dotnxt[ROWMATCH_BLOCK_HEIGHT][ROWMATCH_BLOCK_WIDTH]; __shared__ int x_dotidx[ROWMATCH_BLOCK_HEIGHT][ROWMATCH_BLOCK_WIDTH]; int* dotmax = x_dotmax[threadIdx.y]; int* dotnxt = x_dotnxt[threadIdx.y]; int* dotidx = x_dotidx[threadIdx.y]; int row = IMUL(blockIdx.y, ROWMATCH_BLOCK_HEIGHT) + threadIdx.y; #endif int base_address = IMUL(row , num2); int t_dotmax = 0, t_dotnxt = 0, t_dotidx = -1; for(int i = 0; i < num2; i += ROWMATCH_BLOCK_WIDTH) { if(threadIdx.x + i < num2) { int v = d_dot[base_address + threadIdx.x + i]; // tex1Dfetch(texDOT, base_address + threadIdx.x + i); bool test = v > t_dotmax; t_dotnxt = test? t_dotmax : max(t_dotnxt, v); t_dotidx = test? (threadIdx.x + i) : t_dotidx; t_dotmax = test? v: t_dotmax; } __syncthreads(); } dotmax[threadIdx.x] = t_dotmax; dotnxt[threadIdx.x] = t_dotnxt; dotidx[threadIdx.x] = t_dotidx; __syncthreads(); #pragma unroll for(int step = ROWMATCH_BLOCK_WIDTH/2; step >0; step /= 2) { if(threadIdx.x < step) { int v1 = dotmax[threadIdx.x], v2 = dotmax[threadIdx.x + step]; bool test = v2 > v1; dotnxt[threadIdx.x] = test? max(v1, dotnxt[threadIdx.x + step]) :max(dotnxt[threadIdx.x], v2); dotidx[threadIdx.x] = test? dotidx[threadIdx.x + step] : dotidx[threadIdx.x]; dotmax[threadIdx.x] = test? v2 : v1; } __syncthreads(); } if(threadIdx.x == 0) { float dist = acos(min(dotmax[0] * 0.000003814697265625f, 1.0)); float distn = acos(min(dotnxt[0] * 0.000003814697265625f, 1.0)); //float ratio = dist / distn; d_result[row] = (dist < distmax) && (dist < distn * ratiomax) ? dotidx[0] : -1;//? : -1; } } void ProgramCU::GetRowMatch(CuTexImage* texDot, CuTexImage* texMatch, float distmax, float ratiomax) { int num1 = texDot->GetImgHeight(); int num2 = texDot->GetImgWidth(); dim3 grid(1, num1/ROWMATCH_BLOCK_HEIGHT); dim3 block(ROWMATCH_BLOCK_WIDTH, ROWMATCH_BLOCK_HEIGHT); RowMatch_Kernel<<>>((int*)texDot->_cuData, (int*)texMatch->_cuData, num2, distmax, ratiomax); } #define COLMATCH_BLOCK_WIDTH 32 void __global__ ColMatch_Kernel(int3*d_crt, int* d_result, int height, int num2, float distmax, float ratiomax) { int col = COLMATCH_BLOCK_WIDTH * blockIdx.x + threadIdx.x; if(col >= num2) return; int3 result = d_crt[col];//tex1Dfetch(texCT, col); int read_idx = col + num2; for(int i = 1; i < height; ++i, read_idx += num2) { int3 temp = d_crt[read_idx];//tex1Dfetch(texCT, read_idx); result = result.x < temp.x? make_int3(temp.x, temp.y, max(result.x, temp.z)) : make_int3(result.x, result.y, max(result.z, temp.x)); } float dist = acos(min(result.x * 0.000003814697265625f, 1.0)); float distn = acos(min(result.z * 0.000003814697265625f, 1.0)); //float ratio = dist / distn; d_result[col] = (dist < distmax) && (dist < distn * ratiomax) ? result.y : -1;//? : -1; } void ProgramCU::GetColMatch(CuTexImage* texCRT, CuTexImage* texMatch, float distmax, float ratiomax) { int height = texCRT->GetImgHeight(); int num2 = texCRT->GetImgWidth(); //texCRT->BindTexture(texCT); dim3 grid((num2 + COLMATCH_BLOCK_WIDTH -1) / COLMATCH_BLOCK_WIDTH); dim3 block(COLMATCH_BLOCK_WIDTH); ColMatch_Kernel<<>>((int3*)texCRT->_cuData, (int*) texMatch->_cuData, height, num2, distmax, ratiomax); } #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramCU.h000066400000000000000000000065051454702036400212640ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ProgramCU.h // Author: Changchang Wu // Description : interface for the ProgramCU classes. // It is basically a wrapper around all the CUDA kernels // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _PROGRAM_CU_H #define _PROGRAM_CU_H #if defined(CUDA_SIFTGPU_ENABLED) class CuTexImage; class ProgramCU { public: //GPU FUNCTIONS static void FinishCUDA(); static int CheckErrorCUDA(const char* location); static int CheckCudaDevice(int device); public: ////SIFTGPU FUNCTIONS static void CreateFilterKernel(float sigma, float* kernel, int& width); template static void FilterImage(CuTexImage *dst, CuTexImage *src, CuTexImage* buf); static void FilterImage(CuTexImage *dst, CuTexImage *src, CuTexImage* buf, float sigma); static void ComputeDOG(CuTexImage* gus, CuTexImage* dog, CuTexImage* got); static void ComputeKEY(CuTexImage* dog, CuTexImage* key, float Tdog, float Tedge); static void InitHistogram(CuTexImage* key, CuTexImage* hist); static void ReduceHistogram(CuTexImage*hist1, CuTexImage* hist2); static void GenerateList(CuTexImage* list, CuTexImage* hist); static void ComputeOrientation(CuTexImage*list, CuTexImage* got, CuTexImage*key, float sigma, float sigma_step, int existing_keypoint); static void ComputeDescriptor(CuTexImage*list, CuTexImage* got, CuTexImage* dtex, int rect = 0, int stream = 0); //data conversion static void SampleImageU(CuTexImage *dst, CuTexImage *src, int log_scale); static void SampleImageD(CuTexImage *dst, CuTexImage *src, int log_scale = 1); static void ReduceToSingleChannel(CuTexImage* dst, CuTexImage* src, int convert_rgb); static void ConvertByteToFloat(CuTexImage*src, CuTexImage* dst); //visualization static void DisplayConvertDOG(CuTexImage* dog, CuTexImage* out); static void DisplayConvertGRD(CuTexImage* got, CuTexImage* out); static void DisplayConvertKEY(CuTexImage* key, CuTexImage* dog, CuTexImage* out); static void DisplayKeyPoint(CuTexImage* ftex, CuTexImage* out); static void DisplayKeyBox(CuTexImage* ftex, CuTexImage* out); //SIFTMATCH FUNCTIONS static void MultiplyDescriptor(CuTexImage* tex1, CuTexImage* tex2, CuTexImage* texDot, CuTexImage* texCRT); static void MultiplyDescriptorG(CuTexImage* texDes1, CuTexImage* texDes2, CuTexImage* texLoc1, CuTexImage* texLoc2, CuTexImage* texDot, CuTexImage* texCRT, float* H, float hdistmax, float* F, float fdistmax); static void GetRowMatch(CuTexImage* texDot, CuTexImage* texMatch, float distmax, float ratiomax); static void GetColMatch(CuTexImage* texCRT, CuTexImage* texMatch, float distmax, float ratiomax); }; #endif #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramGLSL.cpp000066400000000000000000002470251454702036400220550ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ProgramGLSL.cpp // Author: Changchang Wu // Description : GLSL related classes // class ProgramGLSL A simple wrapper of GLSL programs // class ShaderBagGLSL GLSL shaders for SIFT // class FilterGLSL GLSL gaussian filters for SIFT // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "ProgramGLSL.h" #include "GLTexImage.h" #include "ShaderMan.h" #include "SiftGPU.h" ProgramGLSL::ShaderObject::ShaderObject(int shadertype, const char * source, int filesource) { _type = shadertype; _compiled = 0; _shaderID = glCreateShader(shadertype); if(_shaderID == 0) return; if(source) { GLint code_length; if(filesource ==0) { const char* code = source; code_length = (GLint) strlen(code); glShaderSource(_shaderID, 1, (const char **) &code, &code_length); }else { char * code; if((code_length= ReadShaderFile(source, code)) ==0) return; glShaderSource(_shaderID, 1, (const char **) &code, &code_length); delete code; } glCompileShader(_shaderID); CheckCompileLog(); if(!_compiled) std::cout << source; } } int ProgramGLSL::ShaderObject::ReadShaderFile(const char *sourcefile, char*& code ) { code = NULL; FILE * file; int len=0; if(sourcefile == NULL) return 0; file = fopen(sourcefile,"rt"); if(file == NULL) return 0; fseek(file, 0, SEEK_END); len = ftell(file); rewind(file); if(len >1) { code = new char[len+1]; fread(code, sizeof( char), len, file); code[len] = 0; }else { len = 0; } fclose(file); return len; } void ProgramGLSL::ShaderObject::CheckCompileLog() { GLint status; glGetShaderiv(_shaderID, GL_COMPILE_STATUS, &status); _compiled = (status ==GL_TRUE); if(_compiled == 0) PrintCompileLog(std::cout); } ProgramGLSL::ShaderObject::~ShaderObject() { if(_shaderID) glDeleteShader(_shaderID); } int ProgramGLSL::ShaderObject::IsValidFragmentShader() { return _type == GL_FRAGMENT_SHADER && _shaderID && _compiled; } int ProgramGLSL::ShaderObject::IsValidVertexShader() { return _type == GL_VERTEX_SHADER && _shaderID && _compiled; } void ProgramGLSL::ShaderObject::PrintCompileLog(ostream&os) { GLint len = 0; glGetShaderiv(_shaderID, GL_INFO_LOG_LENGTH , &len); if(len <=1) return; char * compileLog = new char[len+1]; if(compileLog == NULL) return; glGetShaderInfoLog(_shaderID, len, &len, compileLog); os<<"Compile Log\n"<= 0) glUniform1i(_TextureParam0, 0); return true; } else { return false; } } ProgramGLSL::ProgramGLSL(const char *frag_source) { _linked = 0; _programID = glCreateProgram(); _TextureParam0 = -1; ShaderObject shader(GL_FRAGMENT_SHADER, frag_source); if(shader.IsValidFragmentShader()) { AttachShaderObject(shader); LinkProgram(); if(!_linked) { //shader.PrintCompileLog(std::cout); PrintLinkLog(std::cout); } else { _TextureParam0 = glGetUniformLocation(_programID, "tex"); } }else { _linked = 0; } } /* ProgramGLSL::ProgramGLSL(char*frag_source, char * vert_source) { _used = 0; _linked = 0; _programID = glCreateProgram(); ShaderObject shader(GL_FRAGMENT_SHADER, frag_source); ShaderObject vertex_shader(GL_VERTEX_SHADER, vert_source); AttachShaderObject(shader); AttachShaderObject(vertex_shader); LinkProgram(); if(!_linked) { shader.PrintCompileLog(std::cout); vertex_shader.PrintCompileLog(std::cout); PrintLinkLog(std::cout); std::cout<0 && width > GlobalUtil::_MaxFilterWidth) { std::cout<<"Filter size truncated from "<>1; width = 2 * sz + 1; } int i; float * kernel = new float[width]; float rv = 1.0f/(sigma*sigma); float v, ksum =0; // pre-compute filter for( i = -sz ; i <= sz ; ++i) { kernel[i+sz] = v = exp(-0.5f * i * i *rv) ; ksum += v; } //normalize the kernel rv = 1.0f / ksum; for(i = 0; i< width ;i++) kernel[i]*=rv; // MakeFilterProgram(kernel, width); _size = sz; delete[] kernel; if(GlobalUtil::_verbose && GlobalUtil::_timingL) std::cout<<"Filter: sigma = "<>1; float * pf = kernel + halfwidth; int nhpixel = (halfwidth+1)>>1; //how many neighbour pixels need to be looked up int npixel = (nhpixel<<1)+1;// float weight[3]; ostringstream out;; out< halfwidth? 0 : pf[xwn]; } if(weight[1] == 0.0) { out<<"result += vec4("<>1; float * pf = kernel + halfh; int nhpixel = (halfh+1)>>1; //how many neighbour pixels need to be looked up int npixel = (nhpixel<<1)+1;// float weight[3]; ostringstream out;; out< halfh? 0 : pf[ywn]; } if(weight[1] == 0.0) { out<<"result += vec4("< 0) { for(int i = 0; i< _gaussian_step_num; i++) { delete f_gaussian_step[i]; } delete[] f_gaussian_step; } } void ShaderBag::SelectInitialSmoothingFilter(int octave_min, SiftParam¶m) { float sigma = param.GetInitialSmoothSigma(octave_min); if(sigma == 0) { f_gaussian_skip0 = NULL; }else { for(unsigned int i = 0; i < f_gaussian_skip0_v.size(); i++) { if(f_gaussian_skip0_v[i]->_id == octave_min) { f_gaussian_skip0 = f_gaussian_skip0_v[i]; return ; } } FilterGLSL * filter = new FilterGLSL(sigma); filter->_id = octave_min; f_gaussian_skip0_v.push_back(filter); f_gaussian_skip0 = filter; } } void ShaderBag::CreateGaussianFilters(SiftParam¶m) { if(param._sigma_skip0>0.0f) { FilterGLSL * filter; f_gaussian_skip0 = filter = new FilterGLSL(param._sigma_skip0); filter->_id = GlobalUtil::_octave_min_default; f_gaussian_skip0_v.push_back(filter); } if(param._sigma_skip1>0.0f) { f_gaussian_skip1 = new FilterGLSL(param._sigma_skip1); } f_gaussian_step = new FilterProgram*[param._sigma_num]; for(int i = 0; i< param._sigma_num; i++) { f_gaussian_step[i] = new FilterGLSL(param._sigma[i]); } _gaussian_step_num = param._sigma_num; } void ShaderBag::LoadDynamicShaders(SiftParam& param) { LoadKeypointShader(param._dog_threshold, param._edge_threshold); LoadGenListShader(param._dog_level_num, 0); CreateGaussianFilters(param); } void ShaderBagGLSL::LoadFixedShaders() { s_gray = new ProgramGLSL( "uniform sampler2DRect tex; void main(void){\n" "float intensity = dot(vec3(0.299, 0.587, 0.114), texture2DRect(tex, gl_TexCoord[0].st ).rgb);\n" "gl_FragColor = vec4(intensity, intensity, intensity, 1.0);}"); s_debug = new ProgramGLSL( "void main(void){gl_FragColor.rg = gl_TexCoord[0].st;}"); s_sampling = new ProgramGLSL( "uniform sampler2DRect tex; void main(void){gl_FragColor.rg= texture2DRect(tex, gl_TexCoord[0].st).rg;}"); // s_grad_pass = new ProgramGLSL( "uniform sampler2DRect tex; void main ()\n" "{\n" " vec4 v1, v2, gg;\n" " vec4 cc = texture2DRect(tex, gl_TexCoord[0].xy);\n" " gg.x = texture2DRect(tex, gl_TexCoord[1].xy).r;\n" " gg.y = texture2DRect(tex, gl_TexCoord[2].xy).r;\n" " gg.z = texture2DRect(tex, gl_TexCoord[3].xy).r;\n" " gg.w = texture2DRect(tex, gl_TexCoord[4].xy).r;\n" " vec2 dxdy = (gg.yw - gg.xz); \n" " float grad = 0.5*length(dxdy);\n" " float theta = grad==0.0? 0.0: atan(dxdy.y, dxdy.x);\n" " gl_FragData[0] = vec4(cc.rg, grad, theta);\n" "}\n\0"); ProgramGLSL * program; s_margin_copy = program = new ProgramGLSL( "uniform sampler2DRect tex; uniform vec2 truncate;\n" "void main(){ gl_FragColor = texture2DRect(tex, min(gl_TexCoord[0].xy, truncate)); }"); _param_margin_copy_truncate = glGetUniformLocation(*program, "truncate"); GlobalUtil::_OrientationPack2 = 0; LoadOrientationShader(); if(s_orientation == NULL) { //Load a simplified version if the right version is not supported s_orientation = program = new ProgramGLSL( "uniform sampler2DRect tex; uniform sampler2DRect oTex;\n" " uniform float size; void main(){\n" " vec4 cc = texture2DRect(tex, gl_TexCoord[0].st);\n" " vec4 oo = texture2DRect(oTex, cc.rg);\n" " gl_FragColor.rg = cc.rg;\n" " gl_FragColor.b = oo.a;\n" " gl_FragColor.a = size;}"); _param_orientation_gtex = glGetUniformLocation(*program, "oTex"); _param_orientation_size = glGetUniformLocation(*program, "size"); GlobalUtil::_MaxOrientation = 0; GlobalUtil::_FullSupported = 0; std::cerr<<"Orientation simplified on this hardware"< 0.9))? size : -size);\n" "dxy.y = type < 0.2 ? 0.0 : (((type < 0.3) || (type > 0.7) )? -size :size); \n" "float s = sin(cc.b); float c = cos(cc.b); \n" "gl_FragColor.x = cc.x + c*dxy.x-s*dxy.y;\n" "gl_FragColor.y = cc.y + c*dxy.y+s*dxy.x;}\n}\n"); _param_genvbo_size = glGetUniformLocation(*program, "sizes"); s_display_gaussian = new ProgramGLSL( "uniform sampler2DRect tex; void main(void){float r = texture2DRect(tex, gl_TexCoord[0].st).r;\n" "gl_FragColor = vec4(r, r, r, 1);}" ); s_display_dog = new ProgramGLSL( "uniform sampler2DRect tex; void main(void){float g = 0.5+(20.0*texture2DRect(tex, gl_TexCoord[0].st).g);\n" "gl_FragColor = vec4(g, g, g, 0.0);}" ); s_display_grad = new ProgramGLSL( "uniform sampler2DRect tex; void main(void){\n" " vec4 cc = texture2DRect(tex, gl_TexCoord[0].st);gl_FragColor = vec4(5.0* cc.bbb, 1.0);}"); s_display_keys= new ProgramGLSL( "uniform sampler2DRect tex; void main(void){\n" " vec4 cc = texture2DRect(tex, gl_TexCoord[0].st);\n" " if(cc.r ==0.0) discard; gl_FragColor = (cc.r==1.0? vec4(1.0, 0.0, 0,1.0):vec4(0.0,1.0,0.0,1.0));}"); } void ShaderBagGLSL::LoadKeypointShader(float threshold, float edge_threshold) { float threshold0 = threshold* (GlobalUtil::_SubpixelLocalization?0.8f:1.0f); float threshold1 = threshold; float threshold2 = (edge_threshold+1)*(edge_threshold+1)/edge_threshold; ostringstream out;; streampos pos; //tex(X)(Y) //X: (CLR) (CENTER 0, LEFT -1, RIGHT +1) //Y: (CDU) (CENTER 0, DOWN -1, UP +1) if(GlobalUtil::_DarknessAdaption) { out << "#define THRESHOLD0 (" << threshold0 << " * min(2.0 * cc.r + 0.1, 1.0))\n" "#define THRESHOLD1 (" << threshold1 << " * min(2.0 * cc.r + 0.1, 1.0))\n" "#define THRESHOLD2 " << threshold2 << "\n"; }else { out << "#define THRESHOLD0 " << threshold0 << "\n" "#define THRESHOLD1 " << threshold1 << "\n" "#define THRESHOLD2 " << threshold2 << "\n"; } out<< "uniform sampler2DRect tex, texU, texD; void main ()\n" "{\n" " vec4 v1, v2, gg, temp;\n" " vec2 TexRU = vec2(gl_TexCoord[2].x, gl_TexCoord[4].y); \n" " vec4 cc = texture2DRect(tex, gl_TexCoord[0].xy);\n" " temp = texture2DRect(tex, gl_TexCoord[1].xy);\n" " v1.x = temp.g; gg.x = temp.r;\n" " temp = texture2DRect(tex, gl_TexCoord[2].xy) ;\n" " v1.y = temp.g; gg.y = temp.r;\n" " temp = texture2DRect(tex, gl_TexCoord[3].xy) ;\n" " v1.z = temp.g; gg.z = temp.r;\n" " temp = texture2DRect(tex, gl_TexCoord[4].xy) ;\n" " v1.w = temp.g; gg.w = temp.r;\n" " v2.x = texture2DRect(tex, gl_TexCoord[5].xy).g;\n" " v2.y = texture2DRect(tex, gl_TexCoord[6].xy).g;\n" " v2.z = texture2DRect(tex, gl_TexCoord[7].xy).g;\n" " v2.w = texture2DRect(tex, TexRU.xy).g;\n" " vec2 dxdy = (gg.yw - gg.xz); \n" " float grad = 0.5*length(dxdy);\n" " float theta = grad==0.0? 0.0: atan(dxdy.y, dxdy.x);\n" " gl_FragData[0] = vec4(cc.rg, grad, theta);\n" //test against 8 neighbours //use variable to identify type of extremum //1.0 for local maximum and 0.5 for minimum << " float dog = 0.0; \n" " gl_FragData[1] = vec4(0, 0, 0, 0); \n" " dog = cc.g > float(THRESHOLD0) && all(greaterThan(cc.gggg, max(v1, v2)))?1.0: 0.0;\n" " dog = cc.g < float(-THRESHOLD0) && all(lessThan(cc.gggg, min(v1, v2)))?0.5: dog;\n" " if(dog == 0.0) return;\n"; pos = out.tellp(); //do edge supression first.. //vector v1 is < (-1, 0), (1, 0), (0,-1), (0, 1)> //vector v2 is < (-1,-1), (-1,1), (1,-1), (1, 1)> out<< " float fxx, fyy, fxy; \n" " vec4 D2 = v1.xyzw - cc.gggg;\n" " vec2 D4 = v2.xw - v2.yz;\n" " fxx = D2.x + D2.y;\n" " fyy = D2.z + D2.w;\n" " fxy = 0.25*(D4.x + D4.y);\n" " float fxx_plus_fyy = fxx + fyy;\n" " float score_up = fxx_plus_fyy*fxx_plus_fyy; \n" " float score_down = (fxx*fyy - fxy*fxy);\n" " if( score_down <= 0.0 || score_up > THRESHOLD2 * score_down)return;\n"; //... out<<" \n" " vec2 D5 = 0.5*(v1.yw-v1.xz); \n" " float fx = D5.x, fy = D5.y ; \n" " float fs, fss , fxs, fys ; \n" " vec2 v3; vec4 v4, v5, v6;\n" //read 9 pixels of upper level << " v3.x = texture2DRect(texU, gl_TexCoord[0].xy).g;\n" " v4.x = texture2DRect(texU, gl_TexCoord[1].xy).g;\n" " v4.y = texture2DRect(texU, gl_TexCoord[2].xy).g;\n" " v4.z = texture2DRect(texU, gl_TexCoord[3].xy).g;\n" " v4.w = texture2DRect(texU, gl_TexCoord[4].xy).g;\n" " v6.x = texture2DRect(texU, gl_TexCoord[5].xy).g;\n" " v6.y = texture2DRect(texU, gl_TexCoord[6].xy).g;\n" " v6.z = texture2DRect(texU, gl_TexCoord[7].xy).g;\n" " v6.w = texture2DRect(texU, TexRU.xy).g;\n" //compare with 9 pixels of upper level //read and compare with 9 pixels of lower level //the maximum case << " if(dog == 1.0)\n" " {\n" " if(cc.g < v3.x || any(lessThan(cc.gggg, v4)) ||any(lessThan(cc.gggg, v6)))return; \n" " v3.y = texture2DRect(texD, gl_TexCoord[0].xy).g;\n" " v5.x = texture2DRect(texD, gl_TexCoord[1].xy).g;\n" " v5.y = texture2DRect(texD, gl_TexCoord[2].xy).g;\n" " v5.z = texture2DRect(texD, gl_TexCoord[3].xy).g;\n" " v5.w = texture2DRect(texD, gl_TexCoord[4].xy).g;\n" " v6.x = texture2DRect(texD, gl_TexCoord[5].xy).g;\n" " v6.y = texture2DRect(texD, gl_TexCoord[6].xy).g;\n" " v6.z = texture2DRect(texD, gl_TexCoord[7].xy).g;\n" " v6.w = texture2DRect(texD, TexRU.xy).g;\n" " if(cc.g < v3.y || any(lessThan(cc.gggg, v5)) ||any(lessThan(cc.gggg, v6)))return; \n" " }\n" //the minimum case << " else{\n" " if(cc.g > v3.x || any(greaterThan(cc.gggg, v4)) ||any(greaterThan(cc.gggg, v6)))return; \n" " v3.y = texture2DRect(texD, gl_TexCoord[0].xy).g;\n" " v5.x = texture2DRect(texD, gl_TexCoord[1].xy).g;\n" " v5.y = texture2DRect(texD, gl_TexCoord[2].xy).g;\n" " v5.z = texture2DRect(texD, gl_TexCoord[3].xy).g;\n" " v5.w = texture2DRect(texD, gl_TexCoord[4].xy).g;\n" " v6.x = texture2DRect(texD, gl_TexCoord[5].xy).g;\n" " v6.y = texture2DRect(texD, gl_TexCoord[6].xy).g;\n" " v6.z = texture2DRect(texD, gl_TexCoord[7].xy).g;\n" " v6.w = texture2DRect(texD, TexRU.xy).g;\n" " if(cc.g > v3.y || any(greaterThan(cc.gggg, v5)) ||any(greaterThan(cc.gggg, v6)))return; \n" " }\n"; if(GlobalUtil::_SubpixelLocalization) // sub-pixel localization FragData1 = vec4(dog, 0, 0, 0); return; out << " fs = 0.5*( v3.x - v3.y ); \n" " fss = v3.x + v3.y - cc.g - cc.g;\n" " fxs = 0.25 * ( v4.y + v5.x - v4.x - v5.y);\n" " fys = 0.25 * ( v4.w + v5.z - v4.z - v5.w);\n" // // let dog difference be quatratic function of dx, dy, ds; // df(dx, dy, ds) = fx * dx + fy*dy + fs * ds + // + 0.5 * ( fxx * dx * dx + fyy * dy * dy + fss * ds * ds) // + (fxy * dx * dy + fxs * dx * ds + fys * dy * ds) // (fx, fy, fs, fxx, fyy, fss, fxy, fxs, fys are the derivatives) //the local extremum satisfies // df/dx = 0, df/dy = 0, df/dz = 0 //that is // |-fx| | fxx fxy fxs | |dx| // |-fy| = | fxy fyy fys | * |dy| // |-fs| | fxs fys fss | |ds| // need to solve dx, dy, ds // Use Gauss elimination to solve the linear system << " vec3 dxys = vec3(0.0); \n" " vec4 A0, A1, A2 ; \n" " A0 = vec4(fxx, fxy, fxs, -fx); \n" " A1 = vec4(fxy, fyy, fys, -fy); \n" " A2 = vec4(fxs, fys, fss, -fs); \n" " vec3 x3 = abs(vec3(fxx, fxy, fxs)); \n" " float maxa = max(max(x3.x, x3.y), x3.z); \n" " if(maxa >= 1e-10 ) { \n" " if(x3.y ==maxa ) \n" " { \n" " vec4 TEMP = A1; A1 = A0; A0 = TEMP; \n" " }else if( x3.z == maxa ) \n" " { \n" " vec4 TEMP = A2; A2 = A0; A0 = TEMP; \n" " } \n" " A0 /= A0.x; \n" " A1 -= A1.x * A0; \n" " A2 -= A2.x * A0; \n" " vec2 x2 = abs(vec2(A1.y, A2.y)); \n" " if( x2.y > x2.x ) \n" " { \n" " vec3 TEMP = A2.yzw; \n" " A2.yzw = A1.yzw; \n" " A1.yzw = TEMP; \n" " x2.x = x2.y; \n" " } \n" " if(x2.x >= 1e-10) { \n" " A1.yzw /= A1.y; \n" " A2.yzw -= A2.y * A1.yzw; \n" " if(abs(A2.z) >= 1e-10) { \n" // compute dx, dy, ds: << " \n" " dxys.z = A2.w /A2.z; \n" " dxys.y = A1.w - dxys.z*A1.z; \n" " dxys.x = A0.w - dxys.z*A0.z - dxys.y*A0.y; \n" //one more threshold which I forgot in versions prior to 286 << " bool dog_test = (abs(cc.g + 0.5*dot(vec3(fx, fy, fs), dxys ))<= float(THRESHOLD1)) ;\n" " if(dog_test || any(greaterThan(abs(dxys), vec3(1.0)))) dog = 0.0;\n" " }\n" " }\n" " }\n" //keep the point when the offset is less than 1 << " gl_FragData[1] = vec4( dog, dxys); \n"; else out<< " gl_FragData[1] = vec4( dog, 0.0, 0.0, 0.0) ; \n"; out<< "}\n" <<'\0'; ProgramGLSL * program = new ProgramGLSL(out.str().c_str()); if(program->IsNative()) { s_keypoint = program ; //parameter }else { delete program; out.seekp(pos); out << " gl_FragData[1] = vec4(dog, 0.0, 0.0, 0.0) ; \n" "}\n" <<'\0'; s_keypoint = program = new ProgramGLSL(out.str().c_str()); GlobalUtil::_SubpixelLocalization = 0; std::cerr<<"Detection simplified on this hardware"<0.0,\n" "all(lessThan(gl_TexCoord[1].xy , bbox)) && helper.y >0.0,\n" "all(lessThan(gl_TexCoord[2].xy , bbox)) && helper.z >0.0,\n" "all(lessThan(gl_TexCoord[3].xy , bbox)) && helper.w >0.0);\n" "gl_FragColor = vec4(helper2);\n" "}"); _param_genlist_init_bbox = glGetUniformLocation( *program, "bbox"); //reduction ... s_genlist_histo = new ProgramGLSL( "uniform sampler2DRect tex; void main (void){\n" "vec4 helper; vec4 helper2; \n" "helper = texture2DRect(tex, gl_TexCoord[0].xy); helper2.xy = helper.xy + helper.zw; \n" "helper = texture2DRect(tex, gl_TexCoord[1].xy); helper2.zw = helper.xy + helper.zw; \n" "gl_FragColor.rg = helper2.xz + helper2.yw;\n" "helper = texture2DRect(tex, gl_TexCoord[2].xy); helper2.xy = helper.xy + helper.zw; \n" "helper = texture2DRect(tex, gl_TexCoord[3].xy); helper2.zw = helper.xy + helper.zw; \n" "gl_FragColor.ba= helper2.xz+helper2.yw;\n" "}"); //read of the first part, which generates tex coordinates s_genlist_start= program = LoadGenListStepShader(1, 1); _param_ftex_width= glGetUniformLocation(*program, "width"); _param_genlist_start_tex0 = glGetUniformLocation(*program, "tex0"); //stepping s_genlist_step = program = LoadGenListStepShader(0, 1); _param_genlist_step_tex0= glGetUniformLocation(*program, "tex0"); } void ShaderBagGLSL::SetMarginCopyParam(int xmax, int ymax) { float truncate[2] = {xmax - 0.5f , ymax - 0.5f}; glUniform2fv(_param_margin_copy_truncate, 1, truncate); } void ShaderBagGLSL::SetGenListInitParam(int w, int h) { float bbox[2] = {w - 1.0f, h - 1.0f}; glUniform2fv(_param_genlist_init_bbox, 1, bbox); } void ShaderBagGLSL::SetGenListStartParam(float width, int tex0) { glUniform1f(_param_ftex_width, width); glUniform1i(_param_genlist_start_tex0, 0); } ProgramGLSL* ShaderBagGLSL::LoadGenListStepShader(int start, int step) { int i; // char chanels[5] = "rgba"; ostringstream out; for(i = 0; i < step; i++) out<<"uniform sampler2DRect tex"<0) { out<<"vec2 cpos = vec2(-0.5, 0.5);\t vec2 opos;\n"; for(i = 0; i < step; i++) { out<<"cc = texture2DRect(tex"<IsNative()) { s_orientation = program ; _param_orientation_gtex = glGetUniformLocation(*program, "gradTex"); _param_orientation_size = glGetUniformLocation(*program, "size"); _param_orientation_stex = glGetUniformLocation(*program, "texS"); }else { delete program; } } void ShaderBagGLSL::WriteOrientationCodeToStream(std::ostream& out) { //smooth histogram and find the largest /* smoothing kernel: (1 3 6 7 6 3 1 )/27 the same as 3 pass of (1 1 1)/3 averaging maybe better to use 4 pass on the vectors... */ //the inner loop on different array numbers is always unrolled in fp40 //bug fixed here:) out<<"\n" " //mat3 m1 = mat3(1, 0, 0, 3, 1, 0, 6, 3, 1)/27.0; \n" " mat3 m1 = mat3(1, 3, 6, 0, 1, 3,0, 0, 1)/27.0; \n" " mat4 m2 = mat4(7, 6, 3, 1, 6, 7, 6, 3, 3, 6, 7, 6, 1, 3, 6, 7)/27.0;\n" " #define FILTER_CODE(i) { \\\n" " vec4 newb = (bins[i]* m2); \\\n" " newb.xyz += ( prev.yzw * m1); \\\n" " prev = bins[i]; \\\n" " newb.wzy += ( bins[i+1].zyx *m1); \\\n" " bins[i] = newb;}\n" " for (int j=0; j<2; j++) \n" " { \n" " vec4 prev = bins[8]; \n" " bins[9] = bins[0]; \n"; if(GlobalUtil::_KeepShaderLoop) { out<< " for (int i=0; i<9; i++) \n" " { \n" " FILTER_CODE(i); \n" " } \n" " }"; }else { //manually unroll the loop for ATI. out << " FILTER_CODE(0);\n" " FILTER_CODE(1);\n" " FILTER_CODE(2);\n" " FILTER_CODE(3);\n" " FILTER_CODE(4);\n" " FILTER_CODE(5);\n" " FILTER_CODE(6);\n" " FILTER_CODE(7);\n" " FILTER_CODE(8);\n" " }\n"; } //find the maximum voting out<<"\n" " vec4 maxh; vec2 maxh2; \n" " vec4 maxh4 = max(max(max(max(max(max(max(max(bins[0], bins[1]), bins[2]), \n" " bins[3]), bins[4]), bins[5]), bins[6]), bins[7]), bins[8]);\n" " maxh2 = max(maxh4.xy, maxh4.zw); maxh = vec4(max(maxh2.x, maxh2.y));"; std::string testpeak_code; std::string savepeak_code; //save two/three/four orientations with the largest votings? if(GlobalUtil::_MaxOrientation>1) { out<<"\n" " vec4 Orientations = vec4(0.0, 0.0, 0.0, 0.0); \n" " vec4 weights = vec4(0.0,0.0,0.0,0.0); "; testpeak_code = "\\\n" " {test = greaterThan(bins[i], hh);"; //save the orientations in weight-decreasing order if(GlobalUtil::_MaxOrientation ==2) { savepeak_code = "\\\n" " if(weight <=weights.g){}\\\n" " else if(weight >weights.r)\\\n" " {weights.rg = vec2(weight, weights.r); Orientations.rg = vec2(th, Orientations.r);}\\\n" " else {weights.g = weight; Orientations.g = th;}"; }else if(GlobalUtil::_MaxOrientation ==3) { savepeak_code = "\\\n" " if(weight <=weights.b){}\\\n" " else if(weight >weights.r)\\\n" " {weights.rgb = vec3(weight, weights.rg); Orientations.rgb = vec3(th, Orientations.rg);}\\\n" " else if(weight >weights.g)\\\n" " {weights.gb = vec2(weight, weights.g); Orientations.gb = vec2(th, Orientations.g);}\\\n" " else {weights.b = weight; Orientations.b = th;}"; }else { savepeak_code = "\\\n" " if(weight <=weights.a){}\\\n" " else if(weight >weights.r)\\\n" " {weights = vec4(weight, weights.rgb); Orientations = vec4(th, Orientations.rgb);}\\\n" " else if(weight >weights.g)\\\n" " {weights.gba = vec3(weight, weights.gb); Orientations.gba = vec3(th, Orientations.gb);}\\\n" " else if(weight >weights.b)\\\n" " {weights.ba = vec2(weight, weights.b); Orientations.ba = vec2(th, Orientations.b);}\\\n" " else {weights.a = weight; Orientations.a = th;}"; } }else { out<<"\n" " float Orientation; "; testpeak_code ="\\\n" " if(npeaks<=0.0){\\\n" " test = equal(bins[i], maxh) ;"; savepeak_code="\\\n" " npeaks++; \\\n" " Orientation = th;"; } //find the peaks out <<"\n" " #define FINDPEAK(i, k)" < prevb && bins[i].x > bins[i].y ) \\\n" " { \\\n" " float di = -0.5 * (bins[i].y-prevb) / (bins[i].y+prevb-bins[i].x - bins[i].x) ; \\\n" " float th = (k+di+0.5); float weight = bins[i].x;" < bins[i].z && bins[i].w > bins[i+1].x ) \\\n" " { \\\n" " float di = -0.5 * (bins[i+1].x-bins[i].z) / (bins[i+1].x+bins[i].z-bins[i].w - bins[i].w) ; \\\n" " float th = (k+di+3.5); float weight = bins[i].w; " <1) { out<<"\n" " if(orientation_mode){\n" " npeaks = dot(vec4(1,1," <<(GlobalUtil::_MaxOrientation>2 ? 1 : 0)<<"," <<(GlobalUtil::_MaxOrientation >3? 1 : 0)<<"), vec4(greaterThan(weights, hh)));\n" " gl_FragData[0] = vec4(pos, npeaks, sigma);\n" " gl_FragData[1] = radians((Orientations )*10.0);\n" " }else{\n" " gl_FragData[0] = vec4(pos, radians((Orientations.x)*10.0), sigma);\n" " }\n"; }else { out<<"\n" " gl_FragData[0] = vec4(pos, radians((Orientation)*10.0), sigma);\n"; } //end out<<"\n" "}\n"<<'\0'; } void ShaderBagGLSL::SetSimpleOrientationInput(int oTex, float sigma, float sigma_step) { glUniform1i(_param_orientation_gtex, 1); glUniform1f(_param_orientation_size, sigma); } void ShaderBagGLSL::SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex, float step) { /// glUniform1i(_param_orientation_gtex, 1); if((GlobalUtil::_SubpixelLocalization || GlobalUtil::_KeepExtremumSign)&& stex) { //specify texutre for subpixel subscale localization glUniform1i(_param_orientation_stex, 2); } float size[4]; size[0] = (float)width; size[1] = (float)height; size[2] = sigma; size[3] = step; glUniform4fv(_param_orientation_size, 1, size); } void ShaderBagGLSL::LoadDescriptorShaderF2() { //one shader outpout 128/8 = 16 , each fragout encodes 4 //const double twopi = 2.0*3.14159265358979323846; //const double rpi = 8.0/twopi; ostringstream out; out< M_PI) anglef -= TWO_PI;\n" " float sigma = texture2DRect(tex, coord).w; \n" " float spt = abs(sigma * WF); //default to be 3*sigma \n"; //rotation out<< " vec4 cscs, rots; \n" " cscs.y = sin(anglef); cscs.x = cos(anglef); \n" " cscs.zw = - cscs.xy; \n" " rots = cscs /spt; \n" " cscs *= spt; \n"; //here cscs is actually (cos, sin, -cos, -sin) * (factor: 3)*sigma //and rots is (cos, sin, -cos, -sin ) /(factor*sigma) //devide the 4x4 sift grid into 16 1x1 block, and each corresponds to a shader thread //To use linear interoplation, 1x1 is increased to 2x2, by adding 0.5 to each side out<< "vec4 temp; vec2 pt, offsetpt; \n" " /*the fraction part of idx is .5*/ \n" " offsetpt.x = 4.0* fract(idx*0.25) - 2.0; \n" " offsetpt.y = floor(idx*0.25) - 1.5; \n" " temp = cscs.xwyx*offsetpt.xyxy; \n" " pt = pos + temp.xz + temp.yw; \n"; //get a horizontal bounding box of the rotated rectangle out<< " vec2 bwin = abs(cscs.xy); \n" " float bsz = bwin.x + bwin.y; \n" " vec4 sz; \n" " sz.xy = max(pt - vec2(bsz), vec2(1,1));\n" " sz.zw = min(pt + vec2(bsz), dim - vec2(2, 2)); \n" " sz = floor(sz)+0.5;"; //move sample point to pixel center //get voting for two box out<<"\n" " vec4 DA, DB; vec2 spos; \n" " DA = DB = vec4(0.0, 0.0, 0.0, 0.0); \n" " for(spos.y = sz.y; spos.y <= sz.w; spos.y+=1.0) \n" " { \n" " for(spos.x = sz.x; spos.x <= sz.z; spos.x+=1.0) \n" " { \n" " vec2 diff = spos - pt; \n" " temp = rots.xywx * diff.xyxy;\n" " vec2 nxy = (temp.xz + temp.yw); \n" " vec2 nxyn = abs(nxy); \n" " if(all( lessThan(nxyn, vec2(1.0)) ))\n" " {\n" " vec4 cc = texture2DRect(gradTex, spos); \n" " float mod = cc.b; float angle = cc.a; \n" " float theta0 = RPI * (anglef - angle); \n" " float theta = theta0 < 0.0? theta0 + 8.0 : theta0;;\n" " diff = nxy + offsetpt.xy; \n" " float ww = exp(-0.125*dot(diff, diff));\n" " vec2 weights = vec2(1) - nxyn;\n" " float weight = weights.x * weights.y *mod*ww; \n" " float theta1 = floor(theta); \n" " float weight2 = (theta - theta1) * weight;\n" " float weight1 = weight - weight2;\n" " DA += vec4(equal(vec4(theta1), vec4(0, 1, 2, 3)))*weight1;\n" " DA += vec4(equal(vec4(theta1), vec4(7, 0, 1, 2)))*weight2; \n" " DB += vec4(equal(vec4(theta1), vec4(4, 5, 6, 7)))*weight1;\n" " DB += vec4(equal(vec4(theta1), vec4(3, 4, 5, 6)))*weight2; \n" " }\n" " }\n" " }\n"; out<< " gl_FragData[0] = DA; gl_FragData[1] = DB;\n" "}\n"<<'\0'; ProgramGLSL * program = new ProgramGLSL(out.str().c_str()); if(program->IsNative()) { s_descriptor_fp = program ; _param_descriptor_gtex = glGetUniformLocation(*program, "gradTex"); _param_descriptor_size = glGetUniformLocation(*program, "size"); _param_descriptor_dsize = glGetUniformLocation(*program, "dsize"); }else { delete program; } } void ShaderBagGLSL::LoadDescriptorShader() { GlobalUtil::_DescriptorPPT = 16; LoadDescriptorShaderF2(); } void ShaderBagGLSL::SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma) { /// glUniform1i(_param_descriptor_gtex, 1); float dsize[4] ={dwidth, 1.0f/dwidth, fwidth, 1.0f/fwidth}; glUniform4fv(_param_descriptor_dsize, 1, dsize); float size[3]; size[0] = width; size[1] = height; size[2] = GlobalUtil::_DescriptorWindowFactor; glUniform3fv(_param_descriptor_size, 1, size); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// void ShaderBagPKSL::LoadFixedShaders() { ProgramGLSL * program; s_gray = new ProgramGLSL( "uniform sampler2DRect tex; void main(){\n" "float intensity = dot(vec3(0.299, 0.587, 0.114), texture2DRect(tex,gl_TexCoord[0].xy ).rgb);\n" "gl_FragColor= vec4(intensity, intensity, intensity, 1.0);}" ); s_sampling = new ProgramGLSL( "uniform sampler2DRect tex; void main(){\n" "gl_FragColor= vec4( texture2DRect(tex,gl_TexCoord[0].st ).r,texture2DRect(tex,gl_TexCoord[1].st ).r,\n" " texture2DRect(tex,gl_TexCoord[2].st ).r,texture2DRect(tex,gl_TexCoord[3].st ).r);}" ); s_margin_copy = program = new ProgramGLSL( "uniform sampler2DRect tex; uniform vec4 truncate; void main(){\n" "vec4 cc = texture2DRect(tex, min(gl_TexCoord[0].xy, truncate.xy)); \n" "bvec2 ob = lessThan(gl_TexCoord[0].xy, truncate.xy);\n" "if(ob.y) { gl_FragColor = (truncate.z ==0.0 ? cc.rrbb : cc.ggaa); } \n" "else if(ob.x) {gl_FragColor = (truncate.w <1.5 ? cc.rgrg : cc.baba);} \n" "else { vec4 weights = vec4(vec4(0.0, 1.0, 2.0, 3.0) == truncate.wwww);\n" "float v = dot(weights, cc); gl_FragColor = vec4(v);}}"); _param_margin_copy_truncate = glGetUniformLocation(*program, "truncate"); s_zero_pass = new ProgramGLSL("void main(){gl_FragColor = vec4(0.0);}"); s_grad_pass = program = new ProgramGLSL( "uniform sampler2DRect tex; uniform sampler2DRect texp; void main ()\n" "{\n" " vec4 v1, v2, gg;\n" " vec4 cc = texture2DRect(tex, gl_TexCoord[0].xy);\n" " vec4 cp = texture2DRect(texp, gl_TexCoord[0].xy);\n" " gl_FragData[0] = cc - cp; \n" " vec4 cl = texture2DRect(tex, gl_TexCoord[1].xy); vec4 cr = texture2DRect(tex, gl_TexCoord[2].xy);\n" " vec4 cd = texture2DRect(tex, gl_TexCoord[3].xy); vec4 cu = texture2DRect(tex, gl_TexCoord[4].xy);\n" " vec4 dx = (vec4(cr.rb, cc.ga) - vec4(cc.rb, cl.ga)).zxwy;\n" " vec4 dy = (vec4(cu.rg, cc.ba) - vec4(cc.rg, cd.ba)).zwxy;\n" " vec4 grad = 0.5 * sqrt(dx*dx + dy * dy);\n" " gl_FragData[1] = grad;\n" " vec4 invalid = vec4(equal(grad, vec4(0.0))); \n" " vec4 ov = atan(dy, dx + invalid); \n" " gl_FragData[2] = ov; \n" "}\n\0"); //when _param_grad_pass_texp = glGetUniformLocation(*program, "texp"); GlobalUtil::_OrientationPack2 = 0; LoadOrientationShader(); if(s_orientation == NULL) { //Load a simplified version if the right version is not supported s_orientation = program = new ProgramGLSL( "uniform sampler2DRect tex; uniform sampler2DRect oTex; uniform vec2 size; void main(){\n" " vec4 cc = texture2DRect(tex, gl_TexCoord[0].xy);\n" " vec2 co = cc.xy * 0.5; \n" " vec4 oo = texture2DRect(oTex, co);\n" " bvec2 bo = lessThan(fract(co), vec2(0.5)); \n" " float o = bo.y? (bo.x? oo.r : oo.g) : (bo.x? oo.b : oo.a); \n" " gl_FragColor = vec4(cc.rg, o, size.x * pow(size.y, cc.a));}"); _param_orientation_gtex= glGetUniformLocation(*program, "oTex"); _param_orientation_size= glGetUniformLocation(*program, "size"); GlobalUtil::_MaxOrientation = 0; GlobalUtil::_FullSupported = 0; std::cerr<<"Orientation simplified on this hardware"< 0.9))? size : -size);\n" " dxy.y = type < 0.2 ? 0.0 : (((type < 0.3) || (type > 0.7) )? -size :size); \n" " s = sin(cc.b); c = cos(cc.b); \n" " gl_FragColor.x = cc.x + c*dxy.x-s*dxy.y;\n" " gl_FragColor.y = cc.y + c*dxy.y+s*dxy.x;}\n" "}\n\0"); /*gl_FragColor = vec4(tpos, 0.0, 1.0);}\n\0");*/ _param_genvbo_size = glGetUniformLocation(*program, "sizes"); s_display_gaussian = new ProgramGLSL( "uniform sampler2DRect tex; void main(){\n" "vec4 pc = texture2DRect(tex, gl_TexCoord[0].xy); bvec2 ff = lessThan(fract(gl_TexCoord[0].xy), vec2(0.5));\n" "float v = ff.y?(ff.x? pc.r : pc.g):(ff.x?pc.b:pc.a); gl_FragColor = vec4(vec3(v), 1.0);}"); s_display_dog = new ProgramGLSL( "uniform sampler2DRect tex; void main(){\n" "vec4 pc = texture2DRect(tex, gl_TexCoord[0].xy); bvec2 ff = lessThan(fract(gl_TexCoord[0].xy), vec2(0.5));\n" "float v = ff.y ?(ff.x ? pc.r : pc.g):(ff.x ? pc.b : pc.a);float g = (0.5+20.0*v);\n" "gl_FragColor = vec4(g, g, g, 1.0);}" ); s_display_grad = new ProgramGLSL( "uniform sampler2DRect tex; void main(){\n" "vec4 pc = texture2DRect(tex, gl_TexCoord[0].xy); bvec2 ff = lessThan(fract(gl_TexCoord[0].xy), vec2(0.5));\n" "float v = ff.y ?(ff.x ? pc.r : pc.g):(ff.x ? pc.b : pc.a); gl_FragColor = vec4(5.0 *vec3(v), 1.0); }"); s_display_keys= new ProgramGLSL( "uniform sampler2DRect tex; void main(){\n" "vec4 oc = texture2DRect(tex, gl_TexCoord[0].xy); \n" "vec4 cc = vec4(equal(abs(oc.rrrr), vec4(1.0, 2.0, 3.0, 4.0))); \n" "bvec2 ff = lessThan(fract(gl_TexCoord[0].xy) , vec2(0.5));\n" "float v = ff.y ?(ff.x ? cc.r : cc.g):(ff.x ? cc.b : cc.a);\n" "if(v == 0.0) discard; \n" "else if(oc.r > 0.0) gl_FragColor = vec4(1.0, 0.0, 0,1.0); \n" "else gl_FragColor = vec4(0.0,1.0,0.0,1.0); }" ); } void ShaderBagPKSL::LoadOrientationShader(void) { ostringstream out; if(GlobalUtil::_IsNvidia) { out << "#pragma optionNV(ifcvt none)\n" "#pragma optionNV(unroll all)\n"; } out<<"\n" "#define GAUSSIAN_WF float("<IsNative()) { s_orientation = program ; _param_orientation_gtex = glGetUniformLocation(*program, "gtex"); _param_orientation_otex = glGetUniformLocation(*program, "otex"); _param_orientation_size = glGetUniformLocation(*program, "size"); }else { delete program; } } void ShaderBagPKSL::SetGenListStartParam(float width, int tex0) { glUniform1f(_param_ftex_width, width); glUniform1i(_param_genlist_start_tex0, 0); } void ShaderBagPKSL::LoadGenListShader(int ndoglev,int nlev) { ProgramGLSL * program; s_genlist_init_tight = new ProgramGLSL( "uniform sampler2DRect tex; void main ()\n" "{\n" " vec4 key = vec4(texture2DRect(tex, gl_TexCoord[0].xy).r, \n" " texture2DRect(tex, gl_TexCoord[1].xy).r, \n" " texture2DRect(tex, gl_TexCoord[2].xy).r, \n" " texture2DRect(tex, gl_TexCoord[3].xy).r); \n" " gl_FragColor = vec4(notEqual(key, vec4(0.0))); \n" "}"); s_genlist_init_ex = program = new ProgramGLSL( "uniform sampler2DRect tex; uniform vec4 bbox; void main ()\n" "{\n" " vec4 helper1 = vec4(equal(vec4(abs(texture2DRect(tex, gl_TexCoord[0].xy).r)), vec4(1.0, 2.0, 3.0, 4.0)));\n" " vec4 helper2 = vec4(equal(vec4(abs(texture2DRect(tex, gl_TexCoord[1].xy).r)), vec4(1.0, 2.0, 3.0, 4.0)));\n" " vec4 helper3 = vec4(equal(vec4(abs(texture2DRect(tex, gl_TexCoord[2].xy).r)), vec4(1.0, 2.0, 3.0, 4.0)));\n" " vec4 helper4 = vec4(equal(vec4(abs(texture2DRect(tex, gl_TexCoord[3].xy).r)), vec4(1.0, 2.0, 3.0, 4.0)));\n" " vec4 bx1 = vec4(lessThan(gl_TexCoord[0].xxyy, bbox)); \n" " vec4 bx4 = vec4(lessThan(gl_TexCoord[3].xxyy, bbox)); \n" " vec4 bx2 = vec4(bx4.xy, bx1.zw); \n" " vec4 bx3 = vec4(bx1.xy, bx4.zw);\n" " helper1 = min(min(bx1.xyxy, bx1.zzww), helper1);\n" " helper2 = min(min(bx2.xyxy, bx2.zzww), helper2);\n" " helper3 = min(min(bx3.xyxy, bx3.zzww), helper3);\n" " helper4 = min(min(bx4.xyxy, bx4.zzww), helper4);\n" " gl_FragColor.r = float(any(greaterThan(max(helper1.xy, helper1.zw), vec2(0.0)))); \n" " gl_FragColor.g = float(any(greaterThan(max(helper2.xy, helper2.zw), vec2(0.0)))); \n" " gl_FragColor.b = float(any(greaterThan(max(helper3.xy, helper3.zw), vec2(0.0)))); \n" " gl_FragColor.a = float(any(greaterThan(max(helper4.xy, helper4.zw), vec2(0.0)))); \n" "}"); _param_genlist_init_bbox = glGetUniformLocation( *program, "bbox"); s_genlist_end = program = new ProgramGLSL( GlobalUtil::_KeepExtremumSign == 0 ? "uniform sampler2DRect tex; uniform sampler2DRect ktex; void main()\n" "{\n" " vec4 tc = texture2DRect( tex, gl_TexCoord[0].xy);\n" " vec2 pos = tc.rg; float index = tc.b;\n" " vec4 tk = texture2DRect( ktex, pos); \n" " vec4 keys = vec4(equal(abs(tk.rrrr), vec4(1.0, 2.0, 3.0, 4.0))); \n" " vec2 opos; \n" " opos.x = dot(keys, vec4(-0.5, 0.5, -0.5, 0.5));\n" " opos.y = dot(keys, vec4(-0.5, -0.5, 0.5, 0.5));\n" " gl_FragColor = vec4(opos + pos * 2.0 + tk.yz, 1.0, tk.w);\n" "}" : "uniform sampler2DRect tex; uniform sampler2DRect ktex; void main()\n" "{\n" " vec4 tc = texture2DRect( tex, gl_TexCoord[0].xy);\n" " vec2 pos = tc.rg; float index = tc.b;\n" " vec4 tk = texture2DRect( ktex, pos); \n" " vec4 keys = vec4(equal(abs(tk.rrrr), vec4(1.0, 2.0, 3.0, 4.0))) \n" " vec2 opos; \n" " opos.x = dot(keys, vec4(-0.5, 0.5, -0.5, 0.5));\n" " opos.y = dot(keys, vec4(-0.5, -0.5, 0.5, 0.5));\n" " gl_FragColor = vec4(opos + pos * 2.0 + tk.yz, sign(tk.r), tk.w);\n" "}" ); _param_genlist_end_ktex = glGetUniformLocation(*program, "ktex"); //reduction ... s_genlist_histo = new ProgramGLSL( "uniform sampler2DRect tex; void main ()\n" "{\n" " vec4 helper; vec4 helper2; \n" " helper = texture2DRect(tex, gl_TexCoord[0].xy); helper2.xy = helper.xy + helper.zw; \n" " helper = texture2DRect(tex, gl_TexCoord[1].xy); helper2.zw = helper.xy + helper.zw; \n" " gl_FragColor.rg = helper2.xz + helper2.yw;\n" " helper = texture2DRect(tex, gl_TexCoord[2].xy); helper2.xy = helper.xy + helper.zw; \n" " helper = texture2DRect(tex, gl_TexCoord[3].xy); helper2.zw = helper.xy + helper.zw; \n" " gl_FragColor.ba= helper2.xz+helper2.yw;\n" "}"); //read of the first part, which generates tex coordinates s_genlist_start= program = ShaderBagGLSL::LoadGenListStepShader(1, 1); _param_ftex_width= glGetUniformLocation(*program, "width"); _param_genlist_start_tex0 = glGetUniformLocation(*program, "tex0"); //stepping s_genlist_step = program = ShaderBagGLSL::LoadGenListStepShader(0, 1); _param_genlist_step_tex0= glGetUniformLocation(*program, "tex0"); } void ShaderBagPKSL::UnloadProgram(void) { glUseProgram(0); } void ShaderBagPKSL::LoadKeypointShader(float dog_threshold, float edge_threshold) { float threshold0 = dog_threshold* (GlobalUtil::_SubpixelLocalization?0.8f:1.0f); float threshold1 = dog_threshold; float threshold2 = (edge_threshold+1)*(edge_threshold+1)/edge_threshold; ostringstream out;; out< float(THRESHOLD0(i)) && all(test1)?1.0: 0.0;\\\n" " key[i] = cc[i] < float(-THRESHOLD0(i)) && all(test2)? -1.0: key[i];\\\n" " }\n" " REPEAT4(KEYTEST_STEP0);\n" " if(gl_TexCoord[0].x < 1.0) {key.rb = vec2(0.0);}\n" " if(gl_TexCoord[0].y < 1.0) {key.rg = vec2(0.0);}\n" " gl_FragColor = vec4(0.0);\n" " if(any(notEqual(key, vec4(0.0)))) {\n"; //do edge supression first.. //vector v1 is < (-1, 0), (1, 0), (0,-1), (0, 1)> //vector v2 is < (-1,-1), (-1,1), (1,-1), (1, 1)> out<< " float fxx[4], fyy[4], fxy[4], fx[4], fy[4];\n" " #define EDGE_SUPPRESION(i) \\\n" " if(key[i] != 0.0)\\\n" " {\\\n" " vec4 D2 = v1[i].xyzw - cc[i];\\\n" " vec2 D4 = v2[i].xw - v2[i].yz;\\\n" " vec2 D5 = 0.5*(v1[i].yw-v1[i].xz); \\\n" " fx[i] = D5.x; fy[i] = D5.y ;\\\n" " fxx[i] = D2.x + D2.y;\\\n" " fyy[i] = D2.z + D2.w;\\\n" " fxy[i] = 0.25*(D4.x + D4.y);\\\n" " float fxx_plus_fyy = fxx[i] + fyy[i];\\\n" " float score_up = fxx_plus_fyy*fxx_plus_fyy; \\\n" " float score_down = (fxx[i]*fyy[i] - fxy[i]*fxy[i]);\\\n" " if( score_down <= 0.0 || score_up > THRESHOLD2 * score_down)key[i] = 0.0;\\\n" " }\n" " REPEAT4(EDGE_SUPPRESION);\n" " if(any(notEqual(key, vec4(0.0)))) {\n"; //////////////////////////////////////////////// //read 9 pixels of upper/lower level out<< " vec4 v4[4], v5[4], v6[4];\n" " ccc = texture2DRect(texU, gl_TexCoord[0].xy);\n" " clc = texture2DRect(texU, gl_TexCoord[1].xy);\n" " crc = texture2DRect(texU, gl_TexCoord[2].xy);\n" " ccd = texture2DRect(texU, gl_TexCoord[3].xy);\n" " ccu = texture2DRect(texU, gl_TexCoord[4].xy);\n" " cld = texture2DRect(texU, gl_TexCoord[5].xy);\n" " clu = texture2DRect(texU, gl_TexCoord[6].xy);\n" " crd = texture2DRect(texU, gl_TexCoord[7].xy);\n" " cru = texture2DRect(texU, TexRU.xy);\n" " vec4 cu = ccc;\n" " v4[0] = vec4(clc.g, ccc.g, ccd.b, ccc.b);\n" " v4[1] = vec4(ccc.r, crc.r, ccd.a, ccc.a);\n" " v4[2] = vec4(clc.a, ccc.a, ccc.r, ccu.r);\n" " v4[3] = vec4(ccc.b, crc.b, ccc.g, ccu.g);\n" " v6[0] = vec4(cld.a, clc.a, ccd.a, ccc.a);\n" " v6[1] = vec4(ccd.b, ccc.b, crd.b, crc.b);\n" " v6[2] = vec4(clc.g, clu.g, ccc.g, ccu.g);\n" " v6[3] = vec4(ccc.r, ccu.r, crc.r, cru.r);\n" << " #define KEYTEST_STEP1(i)\\\n" " if(key[i] == 1.0)\\\n" " {\\\n" " bvec4 test = lessThan(vec4(cc[i]), max(v4[i], v6[i])); \\\n" " if(cc[i] < cu[i] || any(test))key[i] = 0.0; \\\n" " }else if(key[i] == -1.0)\\\n" " {\\\n" " bvec4 test = greaterThan(vec4(cc[i]), min(v4[i], v6[i])); \\\n" " if(cc[i] > cu[i] || any(test) )key[i] = 0.0; \\\n" " }\n" " REPEAT4(KEYTEST_STEP1);\n" " if(any(notEqual(key, vec4(0.0)))) { \n" << " ccc = texture2DRect(texD, gl_TexCoord[0].xy);\n" " clc = texture2DRect(texD, gl_TexCoord[1].xy);\n" " crc = texture2DRect(texD, gl_TexCoord[2].xy);\n" " ccd = texture2DRect(texD, gl_TexCoord[3].xy);\n" " ccu = texture2DRect(texD, gl_TexCoord[4].xy);\n" " cld = texture2DRect(texD, gl_TexCoord[5].xy);\n" " clu = texture2DRect(texD, gl_TexCoord[6].xy);\n" " crd = texture2DRect(texD, gl_TexCoord[7].xy);\n" " cru = texture2DRect(texD, TexRU.xy);\n" " vec4 cd = ccc;\n" " v5[0] = vec4(clc.g, ccc.g, ccd.b, ccc.b);\n" " v5[1] = vec4(ccc.r, crc.r, ccd.a, ccc.a);\n" " v5[2] = vec4(clc.a, ccc.a, ccc.r, ccu.r);\n" " v5[3] = vec4(ccc.b, crc.b, ccc.g, ccu.g);\n" " v6[0] = vec4(cld.a, clc.a, ccd.a, ccc.a);\n" " v6[1] = vec4(ccd.b, ccc.b, crd.b, crc.b);\n" " v6[2] = vec4(clc.g, clu.g, ccc.g, ccu.g);\n" " v6[3] = vec4(ccc.r, ccu.r, crc.r, cru.r);\n" << " #define KEYTEST_STEP2(i)\\\n" " if(key[i] == 1.0)\\\n" " {\\\n" " bvec4 test = lessThan(vec4(cc[i]), max(v5[i], v6[i]));\\\n" " if(cc[i] < cd[i] || any(test))key[i] = 0.0; \\\n" " }else if(key[i] == -1.0)\\\n" " {\\\n" " bvec4 test = greaterThan(vec4(cc[i]), min(v5[i], v6[i]));\\\n" " if(cc[i] > cd[i] || any(test))key[i] = 0.0; \\\n" " }\n" " REPEAT4(KEYTEST_STEP2);\n" " float keysum = dot(abs(key), vec4(1, 1, 1, 1)) ;\n" " //assume there is only one keypoint in the four. \n" " if(keysum==1.0) {\n"; ////////////////////////////////////////////////////////////////////// if(GlobalUtil::_SubpixelLocalization) out << " vec3 offset = vec3(0.0, 0.0, 0.0); \n" " #define TESTMOVE_KEYPOINT(idx) \\\n" " if(key[idx] != 0.0) \\\n" " {\\\n" " cu[0] = cu[idx]; cd[0] = cd[idx]; cc[0] = cc[idx]; \\\n" " v4[0] = v4[idx]; v5[0] = v5[idx]; \\\n" " fxy[0] = fxy[idx]; fxx[0] = fxx[idx]; fyy[0] = fyy[idx]; \\\n" " fx[0] = fx[idx]; fy[0] = fy[idx]; MOVE_EXTRA(idx); \\\n" " }\n" " TESTMOVE_KEYPOINT(1);\n" " TESTMOVE_KEYPOINT(2);\n" " TESTMOVE_KEYPOINT(3);\n" << " float fs = 0.5*( cu[0] - cd[0] ); \n" " float fss = cu[0] + cd[0] - cc[0] - cc[0];\n" " float fxs = 0.25 * (v4[0].y + v5[0].x - v4[0].x - v5[0].y);\n" " float fys = 0.25 * (v4[0].w + v5[0].z - v4[0].z - v5[0].w);\n" " vec4 A0, A1, A2 ; \n" " A0 = vec4(fxx[0], fxy[0], fxs, -fx[0]); \n" " A1 = vec4(fxy[0], fyy[0], fys, -fy[0]); \n" " A2 = vec4(fxs, fys, fss, -fs); \n" " vec3 x3 = abs(vec3(fxx[0], fxy[0], fxs)); \n" " float maxa = max(max(x3.x, x3.y), x3.z); \n" " if(maxa >= 1e-10 ) \n" " { \n" " if(x3.y ==maxa ) \n" " { \n" " vec4 TEMP = A1; A1 = A0; A0 = TEMP; \n" " }else if( x3.z == maxa ) \n" " { \n" " vec4 TEMP = A2; A2 = A0; A0 = TEMP; \n" " } \n" " A0 /= A0.x; \n" " A1 -= A1.x * A0; \n" " A2 -= A2.x * A0; \n" " vec2 x2 = abs(vec2(A1.y, A2.y)); \n" " if( x2.y > x2.x ) \n" " { \n" " vec3 TEMP = A2.yzw; \n" " A2.yzw = A1.yzw; \n" " A1.yzw = TEMP; \n" " x2.x = x2.y; \n" " } \n" " if(x2.x >= 1e-10) { \n" " A1.yzw /= A1.y; \n" " A2.yzw -= A2.y * A1.yzw; \n" " if(abs(A2.z) >= 1e-10) {\n" " offset.z = A2.w /A2.z; \n" " offset.y = A1.w - offset.z*A1.z; \n" " offset.x = A0.w - offset.z*A0.z - offset.y*A0.y; \n" " bool test = (abs(cc[0] + 0.5*dot(vec3(fx[0], fy[0], fs), offset ))>float(THRESHOLD1)) ;\n" " if(!test || any( greaterThan(abs(offset), vec3(1.0)))) key = vec4(0.0);\n" " }\n" " }\n" " }\n" <<"\n" " float keyv = dot(key, vec4(1.0, 2.0, 3.0, 4.0));\n" " gl_FragColor = vec4(keyv, offset);\n" " }}}}\n" "}\n" <<'\0'; else out << "\n" " float keyv = dot(key, vec4(1.0, 2.0, 3.0, 4.0));\n" " gl_FragColor = vec4(keyv, 0.0, 0.0, 0.0);\n" " }}}}\n" "}\n" <<'\0'; ProgramGLSL * program = new ProgramGLSL(out.str().c_str()); s_keypoint = program ; //parameter _param_dog_texu = glGetUniformLocation(*program, "texU"); _param_dog_texd = glGetUniformLocation(*program, "texD"); if(GlobalUtil::_DarknessAdaption) _param_dog_texi = glGetUniformLocation(*program, "texI"); } void ShaderBagPKSL::SetDogTexParam(int texU, int texD) { glUniform1i(_param_dog_texu, 1); glUniform1i(_param_dog_texd, 2); if(GlobalUtil::_DarknessAdaption)glUniform1i(_param_dog_texi, 3); } void ShaderBagPKSL::SetGenListStepParam(int tex, int tex0) { glUniform1i(_param_genlist_step_tex0, 1); } void ShaderBagPKSL::SetGenVBOParam(float width, float fwidth,float size) { float sizes[4] = {size*3.0f, fwidth, width, 1.0f/width}; glUniform4fv(_param_genvbo_size, 1, sizes); } void ShaderBagPKSL::SetGradPassParam(int texP) { glUniform1i(_param_grad_pass_texp, 1); } void ShaderBagPKSL::LoadDescriptorShader() { GlobalUtil::_DescriptorPPT = 16; LoadDescriptorShaderF2(); s_rect_description = LoadDescriptorProgramRECT(); } ProgramGLSL* ShaderBagPKSL::LoadDescriptorProgramRECT() { //one shader outpout 128/8 = 16 , each fragout encodes 4 //const double twopi = 2.0*3.14159265358979323846; //const double rpi = 8.0/twopi; ostringstream out; out<IsNative()) { return program; } else { delete program; return NULL; } } ProgramGLSL* ShaderBagPKSL::LoadDescriptorProgramPKSL() { //one shader outpout 128/8 = 16 , each fragout encodes 4 //const double twopi = 2.0*3.14159265358979323846; //const double rpi = 8.0/twopi; ostringstream out; out< M_PI) anglef -= TWO_PI;\n" " float sigma = texture2DRect(tex, coord).w; \n" " float spt = abs(sigma * WF); //default to be 3*sigma \n"; //rotation out<< " vec4 cscs, rots; \n" " cscs.x = cos(anglef); cscs.y = sin(anglef); \n" " cscs.zw = - cscs.xy; \n" " rots = cscs /spt; \n" " cscs *= spt; \n"; //here cscs is actually (cos, sin, -cos, -sin) * (factor: 3)*sigma //and rots is (cos, sin, -cos, -sin ) /(factor*sigma) //devide the 4x4 sift grid into 16 1x1 block, and each corresponds to a shader thread //To use linear interoplation, 1x1 is increased to 2x2, by adding 0.5 to each side out<< " vec4 temp; vec2 pt, offsetpt; \n" " /*the fraction part of idx is .5*/ \n" " offsetpt.x = 4.0* fract(idx*0.25) - 2.0; \n" " offsetpt.y = floor(idx*0.25) - 1.5; \n" " temp = cscs.xwyx*offsetpt.xyxy; \n" " pt = pos + temp.xz + temp.yw; \n"; //get a horizontal bounding box of the rotated rectangle out<< " vec2 bwin = abs(cscs.xy); \n" " float bsz = bwin.x + bwin.y; \n" " vec4 sz; \n" " sz.xy = max(pt - vec2(bsz), vec2(2,2));\n" " sz.zw = min(pt + vec2(bsz), dim - vec2(3)); \n" " sz = floor(sz * 0.5)+0.5;"; //move sample point to pixel center //get voting for two box out<<"\n" " vec4 DA, DB; vec2 spos; \n" " DA = DB = vec4(0.0, 0.0, 0.0, 0.0); \n" " vec4 nox = vec4(0.0, rots.xy, rots.x + rots.y); \n" " vec4 noy = vec4(0.0, rots.wx, rots.w + rots.x); \n" " for(spos.y = sz.y; spos.y <= sz.w; spos.y+=1.0) \n" " { \n" " for(spos.x = sz.x; spos.x <= sz.z; spos.x+=1.0) \n" " { \n" " vec2 tpt = spos * 2.0 - pt - 0.5; \n" " vec4 temp = rots.xywx * tpt.xyxy; \n" " vec2 temp2 = temp.xz + temp.yw; \n" " vec4 nx = temp2.x + nox; \n" " vec4 ny = temp2.y + noy; \n" " vec4 nxn = abs(nx), nyn = abs(ny); \n" " bvec4 inside = lessThan(max(nxn, nyn) , vec4(1.0)); \n" " if(any(inside))\n" " {\n" " vec4 gg = texture2DRect(gtex, spos);\n" " vec4 oo = texture2DRect(otex, spos);\n" " vec4 theta0 = (anglef - oo)*RPI;\n" " vec4 theta = 8.0 * fract(1.0 + 0.125 * theta0); \n" " vec4 theta1 = floor(theta); \n" " vec4 diffx = nx + offsetpt.x, diffy = ny + offsetpt.y; \n" " vec4 ww = exp(-0.125 * (diffx * diffx + diffy * diffy )); \n" " vec4 weight = (vec4(1) - nxn) * (vec4(1) - nyn) * gg * ww; \n" " vec4 weight2 = (theta - theta1) * weight; \n" " vec4 weight1 = weight - weight2; \n" " #define ADD_DESCRIPTOR(i) \\\n" " if(inside[i])\\\n" " {\\\n" " DA += vec4(equal(vec4(theta1[i]), vec4(0, 1, 2, 3)))*weight1[i]; \\\n" " DA += vec4(equal(vec4(theta1[i]), vec4(7, 0, 1, 2)))*weight2[i]; \\\n" " DB += vec4(equal(vec4(theta1[i]), vec4(4, 5, 6, 7)))*weight1[i]; \\\n" " DB += vec4(equal(vec4(theta1[i]), vec4(3, 4, 5, 6)))*weight2[i]; \\\n" " }\n" " REPEAT4(ADD_DESCRIPTOR);\n" " }\n" " }\n" " }\n"; out<< " gl_FragData[0] = DA; gl_FragData[1] = DB;\n" "}\n"<<'\0'; ProgramGLSL * program = new ProgramGLSL(out.str().c_str()); if(program->IsNative()) { return program; } else { delete program; return NULL; } } void ShaderBagPKSL::LoadDescriptorShaderF2() { ProgramGLSL * program = LoadDescriptorProgramPKSL(); if( program ) { s_descriptor_fp = program; _param_descriptor_gtex = glGetUniformLocation(*program, "gtex"); _param_descriptor_otex = glGetUniformLocation(*program, "otex"); _param_descriptor_size = glGetUniformLocation(*program, "size"); _param_descriptor_dsize = glGetUniformLocation(*program, "dsize"); } } void ShaderBagPKSL::SetSimpleOrientationInput(int oTex, float sigma, float sigma_step) { glUniform1i(_param_orientation_gtex, 1); glUniform2f(_param_orientation_size, sigma, sigma_step); } void ShaderBagPKSL::SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int otex, float step) { /// glUniform1i(_param_orientation_gtex, 1); glUniform1i(_param_orientation_otex, 2); float size[4]; size[0] = (float)width; size[1] = (float)height; size[2] = sigma; size[3] = step; glUniform4fv(_param_orientation_size, 1, size); } void ShaderBagPKSL::SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma) { if(sigma == 0 && s_rect_description) { //rectangle description mode s_rect_description->UseProgram(); GLint param_descriptor_gtex = glGetUniformLocation(*s_rect_description, "gtex"); GLint param_descriptor_otex = glGetUniformLocation(*s_rect_description, "otex"); GLint param_descriptor_size = glGetUniformLocation(*s_rect_description, "size"); GLint param_descriptor_dsize = glGetUniformLocation(*s_rect_description, "dsize"); /// glUniform1i(param_descriptor_gtex, 1); glUniform1i(param_descriptor_otex, 2); float dsize[4] ={dwidth, 1.0f/dwidth, fwidth, 1.0f/fwidth}; glUniform4fv(param_descriptor_dsize, 1, dsize); float size[3]; size[0] = width; size[1] = height; size[2] = GlobalUtil::_DescriptorWindowFactor; glUniform3fv(param_descriptor_size, 1, size); }else { /// glUniform1i(_param_descriptor_gtex, 1); glUniform1i(_param_descriptor_otex, 2); float dsize[4] ={dwidth, 1.0f/dwidth, fwidth, 1.0f/fwidth}; glUniform4fv(_param_descriptor_dsize, 1, dsize); float size[3]; size[0] = width; size[1] = height; size[2] = GlobalUtil::_DescriptorWindowFactor; glUniform3fv(_param_descriptor_size, 1, size); } } void ShaderBagPKSL::SetGenListEndParam(int ktex) { glUniform1i(_param_genlist_end_ktex, 1); } void ShaderBagPKSL::SetGenListInitParam(int w, int h) { float bbox[4] = {(w -1.0f) * 0.5f +0.25f, (w-1.0f) * 0.5f - 0.25f, (h - 1.0f) * 0.5f + 0.25f, (h-1.0f) * 0.5f - 0.25f}; glUniform4fv(_param_genlist_init_bbox, 1, bbox); } void ShaderBagPKSL::SetMarginCopyParam(int xmax, int ymax) { float truncate[4]; truncate[0] = (xmax - 0.5f) * 0.5f; //((xmax + 1) >> 1) - 0.5f; truncate[1] = (ymax - 0.5f) * 0.5f; //((ymax + 1) >> 1) - 0.5f; truncate[2] = (xmax %2 == 1)? 0.0f: 1.0f; truncate[3] = truncate[2] + (((ymax % 2) == 1)? 0.0f : 2.0f); glUniform4fv(_param_margin_copy_truncate, 1, truncate); } colmap-3.9.1/src/thirdparty/SiftGPU/ProgramGLSL.h000066400000000000000000000222741454702036400215170ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ProgramGLSL.h // Author: Changchang Wu // Description : Interface for ProgramGLSL classes // ProgramGLSL: Glsl Program // FilterGLSL: Glsl Gaussian Filters // ShaderBag: base class of ShaderBagPKSL and ShaderBagGLSL // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _PROGRAM_GLSL_H #define _PROGRAM_GLSL_H #include "ProgramGPU.h" class ProgramGLSL:public ProgramGPU { class ShaderObject { GLuint _shaderID; int _type; int _compiled; static int ReadShaderFile(const char * source, char *& code); void CheckCompileLog(); public: void PrintCompileLog(ostream & os ); int inline IsValidShaderObject(){ return _shaderID && _compiled;} int IsValidVertexShader(); int IsValidFragmentShader(); GLuint GetShaderID(){return _shaderID;} ~ShaderObject(); ShaderObject(int shadertype, const char * source, int filesource =0); }; protected: int _linked; GLint _TextureParam0; GLuint _programID; private: void AttachShaderObject(ShaderObject& shader); void DetachShaderObject(ShaderObject& shader); public: void ReLink(); int IsNative(); int UseProgram(); void PrintLinkLog(std::ostream&os); int ValidateProgram(); void CheckLinkLog(); int LinkProgram(); operator GLuint (){return _programID;} virtual void * GetProgramID() { return (void*) _programID; } public: ProgramGLSL(); ~ProgramGLSL(); ProgramGLSL(const char* frag_source); }; class GLTexImage; class FilterGLSL : public FilterProgram { private: ProgramGPU* CreateFilterH(float kernel[], int width); ProgramGPU* CreateFilterV(float kernel[], int height); ProgramGPU* CreateFilterHPK(float kernel[], int width); ProgramGPU* CreateFilterVPK(float kernel[], int height); public: void MakeFilterProgram(float kernel[], int width); public: FilterGLSL(float sigma) ; }; class SiftParam; ///////////////////////////////////////////////////////////////////////////////// //class ShaderBag //desciption: pure virtual class // provides storage and usage interface of all the shaders for SIFT // two implementations are ShaderBagPKSL and ShaderBagGLSL ///////////////////////////////////////////////////////////////////////////////// class ShaderBag { public: //shader: rgb to gray ProgramGPU * s_gray; //shader: copy keypoint to PBO ProgramGPU * s_copy_key; //shader: debug view ProgramGPU * s_debug; //shader: orientation //shader: assign simple orientation to keypoints if hardware is low ProgramGPU * s_orientation; //shader: display gaussian levels ProgramGPU * s_display_gaussian; //shader: display difference of gassian ProgramGPU * s_display_dog; //shader: display gradient ProgramGPU * s_display_grad; //shader: display keypoints as red(maximum) and blue (minimum) ProgramGPU * s_display_keys; //shader: up/down-sample ProgramGPU * s_sampling; //shader: compute gradient/dog ProgramGPU * s_grad_pass; ProgramGPU * s_dog_pass; //shader: keypoint detection in one pass ProgramGPU * s_keypoint; ProgramGPU * s_seperate_sp; //shader: feature list generations.. ProgramGPU * s_genlist_init_tight; ProgramGPU * s_genlist_init_ex; ProgramGPU * s_genlist_histo; ProgramGPU * s_genlist_start; ProgramGPU * s_genlist_step; ProgramGPU * s_genlist_end; ProgramGPU * s_zero_pass; //shader: generate vertex to display SIFT as a square ProgramGPU * s_vertex_list; //shader: descriptor ProgramGPU * s_descriptor_fp; //shader: copy pixels to margin ProgramGPU * s_margin_copy; public: FilterProgram * f_gaussian_skip0; vector f_gaussian_skip0_v; FilterProgram * f_gaussian_skip1; FilterProgram ** f_gaussian_step; int _gaussian_step_num; public: virtual void SetGenListInitParam(int w, int h){}; virtual void SetGenListEndParam(int ktex){}; virtual void SetMarginCopyParam(int xmax, int ymax){}; virtual void LoadDescriptorShader(){}; virtual void SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma){}; virtual void SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex, float step){}; virtual void SetSimpleOrientationInput(int oTex, float sigma, float sigma_step){}; virtual void LoadOrientationShader() =0; virtual void SetGenListStartParam(float width, int tex0) =0; virtual void LoadGenListShader(int ndoglev, int nlev)=0; virtual void UnloadProgram()=0; virtual void LoadKeypointShader(float threshold, float edgeTrheshold) = 0; virtual void LoadFixedShaders()=0; virtual void LoadDisplayShaders() = 0; virtual void SetDogTexParam(int texU, int texD)=0; virtual void SetGradPassParam(int texP=0){} virtual void SetGenListStepParam(int tex, int tex0) = 0; virtual void SetGenVBOParam( float width, float fwidth, float size)=0; public: void CreateGaussianFilters(SiftParam¶m); void SelectInitialSmoothingFilter(int octave_min, SiftParam¶m); void LoadDynamicShaders(SiftParam& param); ShaderBag(); virtual ~ShaderBag(); }; class ShaderBagGLSL:public ShaderBag { GLint _param_dog_texu; GLint _param_dog_texd; GLint _param_ftex_width; GLint _param_genlist_start_tex0; GLint _param_genlist_step_tex0; GLint _param_genvbo_size; GLint _param_orientation_gtex; GLint _param_orientation_size; GLint _param_orientation_stex; GLint _param_margin_copy_truncate; GLint _param_genlist_init_bbox; GLint _param_descriptor_gtex; GLint _param_descriptor_size; GLint _param_descriptor_dsize; public: virtual void SetMarginCopyParam(int xmax, int ymax); void SetSimpleOrientationInput(int oTex, float sigma, float sigma_step); void LoadOrientationShader(); void LoadDescriptorShaderF2(); virtual void LoadDescriptorShader(); virtual void SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex = 0, float step = 1.0f); virtual void SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma); static void WriteOrientationCodeToStream(ostream& out); static ProgramGLSL* LoadGenListStepShader(int start, int step); virtual void SetGenListInitParam(int w, int h); virtual void SetGenListStartParam(float width, int tex0); virtual void LoadGenListShader(int ndoglev, int nlev); virtual void UnloadProgram(); virtual void LoadKeypointShader(float threshold, float edgeTrheshold); virtual void LoadFixedShaders(); virtual void LoadDisplayShaders(); virtual void SetDogTexParam(int texU, int texD); virtual void SetGenListStepParam(int tex, int tex0); virtual void SetGenVBOParam( float width, float fwidth, float size); virtual ~ShaderBagGLSL(){} }; class ShaderBagPKSL:public ShaderBag { private: GLint _param_dog_texu; GLint _param_dog_texd; GLint _param_dog_texi; GLint _param_margin_copy_truncate; GLint _param_grad_pass_texp; GLint _param_genlist_init_bbox; GLint _param_genlist_start_tex0; GLint _param_ftex_width; GLint _param_genlist_step_tex0; GLint _param_genlist_end_ktex; GLint _param_genvbo_size; GLint _param_orientation_gtex; GLint _param_orientation_otex; GLint _param_orientation_size; GLint _param_descriptor_gtex; GLint _param_descriptor_otex; GLint _param_descriptor_size; GLint _param_descriptor_dsize; // ProgramGLSL* s_rect_description; public: ShaderBagPKSL () {s_rect_description = NULL; } virtual ~ShaderBagPKSL() {if(s_rect_description) delete s_rect_description; } virtual void LoadFixedShaders(); virtual void LoadDisplayShaders(); virtual void LoadOrientationShader() ; virtual void SetGenListStartParam(float width, int tex0) ; virtual void LoadGenListShader(int ndoglev, int nlev); virtual void UnloadProgram(); virtual void LoadKeypointShader(float threshold, float edgeTrheshold) ; virtual void LoadDescriptorShader(); virtual void LoadDescriptorShaderF2(); static ProgramGLSL* LoadDescriptorProgramRECT(); static ProgramGLSL* LoadDescriptorProgramPKSL(); ///////////////// virtual void SetDogTexParam(int texU, int texD); virtual void SetGradPassParam(int texP); virtual void SetGenListStepParam(int tex, int tex0); virtual void SetGenVBOParam( float width, float fwidth, float size); virtual void SetFeatureDescirptorParam(int gtex, int otex, float dwidth, float fwidth, float width, float height, float sigma); virtual void SetFeatureOrientationParam(int gtex, int width, int height, float sigma, int stex, float step); virtual void SetSimpleOrientationInput(int oTex, float sigma, float sigma_step); virtual void SetGenListEndParam(int ktex); virtual void SetGenListInitParam(int w, int h); virtual void SetMarginCopyParam(int xmax, int ymax); }; #endif colmap-3.9.1/src/thirdparty/SiftGPU/ProgramGPU.h000066400000000000000000000036531454702036400214110ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ProgramGPU.h // Author: Changchang Wu // Description : Based class for GPU programs // ProgramGPU: base class of ProgramGLSL // FilterProgram: base class of FilterGLSL, FilterPKSL // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _PROGRAM_GPU_H #define _PROGRAM_GPU_H //////////////////////////////////////////////////////////////////////////// //class ProgramGPU //description: pure virtual class // provides a common interface for shader programs /////////////////////////////////////////////////////////////////////////// class ProgramGPU { public: //use a gpu program virtual int UseProgram() = 0; virtual void* GetProgramID() = 0; //not used virtual ~ProgramGPU(){}; }; /////////////////////////////////////////////////////////////////////////// //class FilterProgram /////////////////////////////////////////////////////////////////////////// class FilterProgram { public: ProgramGPU* s_shader_h; ProgramGPU* s_shader_v; int _size; int _id; public: FilterProgram() { s_shader_h = s_shader_v = NULL; _size = _id = 0; } virtual ~FilterProgram() { if(s_shader_h) delete s_shader_h; if(s_shader_v) delete s_shader_v;} }; #endif colmap-3.9.1/src/thirdparty/SiftGPU/PyramidCL.cpp000066400000000000000000000733601454702036400216070ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: PyramidCL.cpp // Author: Changchang Wu // Description : implementation of the PyramidCL class. // OpenCL-based implementation of SiftPyramid // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CL_SIFTGPU_ENABLED) #include "GL/glew.h" #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "GLTexImage.h" #include "CLTexImage.h" #include "SiftGPU.h" #include "SiftPyramid.h" #include "ProgramCL.h" #include "PyramidCL.h" #define USE_TIMING() double t, t0, tt; #define OCTAVE_START() if(GlobalUtil::_timingO){ t = t0 = CLOCK(); cout<<"#"<FinishCL(); tt = CLOCK();cout<<(tt-t)<<"\t"; t = CLOCK();} #define OCTAVE_FINISH() if(GlobalUtil::_timingO)cout<<"|\t"<<(CLOCK()-t0)<InitProgramBag(sp); _inputTex = new CLTexImage( _OpenCL->GetContextCL(), _OpenCL->GetCommandQueue()); ///////////////////////// InitializeContext(); } PyramidCL::~PyramidCL() { DestroyPerLevelData(); DestroySharedData(); DestroyPyramidData(); if(_OpenCL) delete _OpenCL; if(_inputTex) delete _inputTex; if(_bufferTEX) delete _bufferTEX; } void PyramidCL::InitializeContext() { GlobalUtil::InitGLParam(1); } void PyramidCL::InitPyramid(int w, int h, int ds) { int wp, hp, toobig = 0; if(ds == 0) { _down_sample_factor = 0; if(GlobalUtil::_octave_min_default>=0) { wp = w >> _octave_min_default; hp = h >> _octave_min_default; }else { //can't upsample by more than 8 _octave_min_default = max(-3, _octave_min_default); // wp = w << (-_octave_min_default); hp = h << (-_octave_min_default); } _octave_min = _octave_min_default; }else { //must use 0 as _octave_min; _octave_min = 0; _down_sample_factor = ds; w >>= ds; h >>= ds; wp = w; hp = h; } while(wp > GlobalUtil::_texMaxDim || hp > GlobalUtil::_texMaxDim ) { _octave_min ++; wp >>= 1; hp >>= 1; toobig = 1; } if(toobig && GlobalUtil::_verbose && _octave_min > 0) { std::cout<< "**************************************************************\n" "Image larger than allowed dimension, data will be downsampled!\n" "use -maxd to change the settings\n" "***************************************************************\n"; } if( wp == _pyramid_width && hp == _pyramid_height && _allocated ) { FitPyramid(wp, hp); }else if(GlobalUtil::_ForceTightPyramid || _allocated ==0) { ResizePyramid(wp, hp); } else if( wp > _pyramid_width || hp > _pyramid_height ) { ResizePyramid(max(wp, _pyramid_width), max(hp, _pyramid_height)); if(wp < _pyramid_width || hp < _pyramid_height) FitPyramid(wp, hp); } else { //try use the pyramid allocated for large image on small input images FitPyramid(wp, hp); } _OpenCL->SelectInitialSmoothingFilter(_octave_min + _down_sample_factor, param); } void PyramidCL::ResizePyramid(int w, int h) { // unsigned int totalkb = 0; int _octave_num_new, input_sz, i, j; // if(_pyramid_width == w && _pyramid_height == h && _allocated) return; if(w > GlobalUtil::_texMaxDim || h > GlobalUtil::_texMaxDim) return ; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <0) { DestroyPerLevelData(); DestroyPyramidData(); } _pyramid_octave_num = _octave_num_new; } _octave_num = _pyramid_octave_num; int noct = _octave_num; int nlev = param._level_num; int texNum = noct* nlev * DATA_NUM; // //initialize the pyramid if(_allPyramid==NULL) { _allPyramid = new CLTexImage[ texNum]; cl_context context = _OpenCL->GetContextCL(); cl_command_queue queue = _OpenCL->GetCommandQueue(); for(i = 0; i < texNum; ++i) _allPyramid[i].SetContext(context, queue); } CLTexImage * gus = GetBaseLevel(_octave_min, DATA_GAUSSIAN); CLTexImage * dog = GetBaseLevel(_octave_min, DATA_DOG); CLTexImage * grd = GetBaseLevel(_octave_min, DATA_GRAD); CLTexImage * rot = GetBaseLevel(_octave_min, DATA_ROT); CLTexImage * key = GetBaseLevel(_octave_min, DATA_KEYPOINT); ////////////there could be "out of memory" happening during the allocation for(i = 0; i< noct; i++) { for( j = 0; j< nlev; j++, gus++, dog++, grd++, rot++, key++) { gus->InitPackedTex(w, h, GlobalUtil::_usePackedTex); if(j==0)continue; dog->InitPackedTex(w, h, GlobalUtil::_usePackedTex); if(j < 1 + param._dog_level_num) { grd->InitPackedTex(w, h, GlobalUtil::_usePackedTex); rot->InitPackedTex(w, h, GlobalUtil::_usePackedTex); } if(j > 1 && j < nlev -1) key->InitPackedTex(w, h, GlobalUtil::_usePackedTex); } //////////////////////////////////////// int tsz = (gus -1)->GetTexPixelCount() * 16; totalkb += ((nlev *5 -6)* tsz / 1024); //several auxilary textures are not actually required w>>=1; h>>=1; } totalkb += ResizeFeatureStorage(); _allocated = 1; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <<(totalkb/1024)<<"MB\n"; } void PyramidCL::FitPyramid(int w, int h) { _pyramid_octave_first = 0; // _octave_num = GlobalUtil::_octave_num_default; int _octave_num_max = GetRequiredOctaveNum(min(w, h)); if(_octave_num < 1 || _octave_num > _octave_num_max) { _octave_num = _octave_num_max; } int pw = _pyramid_width>>1, ph = _pyramid_height>>1; while(_pyramid_octave_first + _octave_num < _pyramid_octave_num && pw >= w && ph >= h) { _pyramid_octave_first++; pw >>= 1; ph >>= 1; } ////////////////// for(int i = 0; i < _octave_num; i++) { CLTexImage * tex = GetBaseLevel(i + _octave_min); CLTexImage * dog = GetBaseLevel(i + _octave_min, DATA_DOG); CLTexImage * grd = GetBaseLevel(i + _octave_min, DATA_GRAD); CLTexImage * rot = GetBaseLevel(i + _octave_min, DATA_ROT); CLTexImage * key = GetBaseLevel(i + _octave_min, DATA_KEYPOINT); for(int j = param._level_min; j <= param._level_max; j++, tex++, dog++, grd++, rot++, key++) { tex->SetPackedSize(w, h, GlobalUtil::_usePackedTex); if(j == param._level_min) continue; dog->SetPackedSize(w, h, GlobalUtil::_usePackedTex); if(j < param._level_max - 1) { grd->SetPackedSize(w, h, GlobalUtil::_usePackedTex); rot->SetPackedSize(w, h, GlobalUtil::_usePackedTex); } if(j > param._level_min + 1 && j < param._level_max) key->SetPackedSize(w, h, GlobalUtil::_usePackedTex); } w>>=1; h>>=1; } } void PyramidCL::SetLevelFeatureNum(int idx, int fcount) { _featureTex[idx].InitBufferTex(fcount, 1, 4); _levelFeatureNum[idx] = fcount; } int PyramidCL::ResizeFeatureStorage() { int totalkb = 0; if(_levelFeatureNum==NULL) _levelFeatureNum = new int[_octave_num * param._dog_level_num]; std::fill(_levelFeatureNum, _levelFeatureNum+_octave_num * param._dog_level_num, 0); cl_context context = _OpenCL->GetContextCL(); cl_command_queue queue = _OpenCL->GetCommandQueue(); int wmax = GetBaseLevel(_octave_min)->GetImgWidth() * 2; int hmax = GetBaseLevel(_octave_min)->GetImgHeight() * 2; int whmax = max(wmax, hmax); int w, i; // int num = (int)ceil(log(double(whmax))/log(4.0)); if( _hpLevelNum != num) { _hpLevelNum = num; if(_histoPyramidTex ) delete [] _histoPyramidTex; _histoPyramidTex = new CLTexImage[_hpLevelNum]; for(i = 0; i < _hpLevelNum; ++i) _histoPyramidTex[i].SetContext(context, queue); } for(i = 0, w = 1; i < _hpLevelNum; i++) { _histoPyramidTex[i].InitBufferTex(w, whmax, 4); w<<=2; } // (4 ^ (_hpLevelNum) -1 / 3) pixels totalkb += (((1 << (2 * _hpLevelNum)) -1) / 3 * 16 / 1024); //initialize the feature texture int idx = 0, n = _octave_num * param._dog_level_num; if(_featureTex==NULL) { _featureTex = new CLTexImage[n]; for(i = 0; i 1 && GlobalUtil::_OrientationPack2==0 && _orientationTex== NULL) { _orientationTex = new CLTexImage[n]; for(i = 0; i < n; ++i) _orientationTex[i].SetContext(context, queue); } for(i = 0; i < _octave_num; i++) { CLTexImage * tex = GetBaseLevel(i+_octave_min); int fmax = int(4 * tex->GetTexWidth() * tex->GetTexHeight()*GlobalUtil::_MaxFeaturePercent); // if(fmax > GlobalUtil::_MaxLevelFeatureNum) fmax = GlobalUtil::_MaxLevelFeatureNum; else if(fmax < 32) fmax = 32; //give it at least a space of 32 feature for(int j = 0; j < param._dog_level_num; j++, idx++) { _featureTex[idx].InitBufferTex(fmax, 1, 4); totalkb += fmax * 16 /1024; // if(GlobalUtil::_MaxOrientation>1 && GlobalUtil::_OrientationPack2 == 0) { _orientationTex[idx].InitBufferTex(fmax, 1, 4); totalkb += fmax * 16 /1024; } } } //this just need be initialized once if(_descriptorTex==NULL) { //initialize feature texture pyramid int fmax = _featureTex->GetImgWidth(); _descriptorTex = new CLTexImage(context, queue); totalkb += ( fmax /2); _descriptorTex->InitBufferTex(fmax *128, 1, 1); }else { totalkb += _descriptorTex->GetDataSize()/1024; } return totalkb; } void PyramidCL::GetFeatureDescriptors() { //descriptors... /*float* pd = &_descriptor_buffer[0]; vector descriptor_buffer2; //use another buffer if we need to re-order the descriptors if(_keypoint_index.size() > 0) { descriptor_buffer2.resize(_descriptor_buffer.size()); pd = &descriptor_buffer2[0]; } CLTexImage * got, * ftex= _featureTex; for(int i = 0, idx = 0; i < _octave_num; i++) { got = GetBaseLevel(i + _octave_min, DATA_GRAD) + 1; for(int j = 0; j < param._dog_level_num; j++, ftex++, idx++, got++) { if(_levelFeatureNum[idx]==0) continue; ProgramCL::ComputeDescriptor(ftex, got, _descriptorTex);//process _descriptorTex->CopyToHost(pd); //readback descriptor pd += 128*_levelFeatureNum[idx]; } } if(GlobalUtil::_timingS) _OpenCL->FinishCL(); if(_keypoint_index.size() > 0) { //put the descriptor back to the original order for keypoint list. for(int i = 0; i < _featureNum; ++i) { int index = _keypoint_index[i]; memcpy(&_descriptor_buffer[index*128], &descriptor_buffer2[i*128], 128 * sizeof(float)); } }*/ } void PyramidCL::GenerateFeatureListTex() { vector list; int idx = 0; const double twopi = 2.0*3.14159265358979323846; float sigma_half_step = powf(2.0f, 0.5f / param._dog_level_num); float octave_sigma = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; if(_down_sample_factor>0) octave_sigma *= float(1<<_down_sample_factor); _keypoint_index.resize(0); // should already be 0 for(int i = 0; i < _octave_num; i++, octave_sigma*= 2.0f) { for(int j = 0; j < param._dog_level_num; j++, idx++) { list.resize(0); float level_sigma = param.GetLevelSigma(j + param._level_min + 1) * octave_sigma; float sigma_min = level_sigma / sigma_half_step; float sigma_max = level_sigma * sigma_half_step; int fcount = 0 ; for(int k = 0; k < _featureNum; k++) { float * key = &_keypoint_buffer[k*4]; if( (key[2] >= sigma_min && key[2] < sigma_max) ||(key[2] < sigma_min && i ==0 && j == 0) ||(key[2] > sigma_max && i == _octave_num -1 && j == param._dog_level_num - 1)) { //add this keypoint to the list list.push_back((key[0] - offset) / octave_sigma + 0.5f); list.push_back((key[1] - offset) / octave_sigma + 0.5f); list.push_back(key[2] / octave_sigma); list.push_back((float)fmod(twopi-key[3], twopi)); fcount ++; //save the index of keypoints _keypoint_index.push_back(k); } } _levelFeatureNum[idx] = fcount; if(fcount==0)continue; CLTexImage * ftex = _featureTex+idx; SetLevelFeatureNum(idx, fcount); ftex->CopyFromHost(&list[0]); } } if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } } void PyramidCL::ReshapeFeatureListCPU() { int i, szmax =0, sz; int n = param._dog_level_num*_octave_num; for( i = 0; i < n; i++) { sz = _levelFeatureNum[i]; if(sz > szmax ) szmax = sz; } float * buffer = new float[szmax*16]; float * buffer1 = buffer; float * buffer2 = buffer + szmax*4; _featureNum = 0; #ifdef NO_DUPLICATE_DOWNLOAD const double twopi = 2.0*3.14159265358979323846; _keypoint_buffer.resize(0); float os = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); if(_down_sample_factor>0) os *= float(1<<_down_sample_factor); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; #endif for(i = 0; i < n; i++) { if(_levelFeatureNum[i]==0)continue; _featureTex[i].CopyToHost(buffer1); int fcount =0; float * src = buffer1; float * des = buffer2; const static double factor = 2.0*3.14159265358979323846/65535.0; for(int j = 0; j < _levelFeatureNum[i]; j++, src+=4) { unsigned short * orientations = (unsigned short*) (&src[3]); if(orientations[0] != 65535) { des[0] = src[0]; des[1] = src[1]; des[2] = src[2]; des[3] = float( factor* orientations[0]); fcount++; des += 4; if(orientations[1] != 65535 && orientations[1] != orientations[0]) { des[0] = src[0]; des[1] = src[1]; des[2] = src[2]; des[3] = float(factor* orientations[1]); fcount++; des += 4; } } } //texture size SetLevelFeatureNum(i, fcount); _featureTex[i].CopyFromHost(buffer2); if(fcount == 0) continue; #ifdef NO_DUPLICATE_DOWNLOAD float oss = os * (1 << (i / param._dog_level_num)); _keypoint_buffer.resize((_featureNum + fcount) * 4); float* ds = &_keypoint_buffer[_featureNum * 4]; float* fs = buffer2; for(int k = 0; k < fcount; k++, ds+=4, fs+=4) { ds[0] = oss*(fs[0]-0.5f) + offset; //x ds[1] = oss*(fs[1]-0.5f) + offset; //y ds[2] = oss*fs[2]; //scale ds[3] = (float)fmod(twopi-fs[3], twopi); //orientation, mirrored } #endif _featureNum += fcount; } delete[] buffer; if(GlobalUtil::_verbose) { std::cout<<"#Features MO:\t"<<_featureNum<DisplayKeyBox(ftex, &texPBO1); _OpenCL->DisplayKeyPoint(ftex, &texPBO2); }*/ } void PyramidCL::DestroySharedData() { //histogram reduction if(_histoPyramidTex) { delete[] _histoPyramidTex; _hpLevelNum = 0; _histoPyramidTex = NULL; } //descriptor storage shared by all levels if(_descriptorTex) { delete _descriptorTex; _descriptorTex = NULL; } //cpu reduction buffer. if(_histo_buffer) { delete[] _histo_buffer; _histo_buffer = 0; } } void PyramidCL::DestroyPerLevelData() { //integers vector to store the feature numbers. if(_levelFeatureNum) { delete [] _levelFeatureNum; _levelFeatureNum = NULL; } //texture used to store features if( _featureTex) { delete [] _featureTex; _featureTex = NULL; } //texture used for multi-orientation if(_orientationTex) { delete [] _orientationTex; _orientationTex = NULL; } int no = _octave_num* param._dog_level_num; //two sets of vbos used to display the features if(_featureDisplayVBO) { glDeleteBuffers(no, _featureDisplayVBO); delete [] _featureDisplayVBO; _featureDisplayVBO = NULL; } if( _featurePointVBO) { glDeleteBuffers(no, _featurePointVBO); delete [] _featurePointVBO; _featurePointVBO = NULL; } } void PyramidCL::DestroyPyramidData() { if(_allPyramid) { delete [] _allPyramid; _allPyramid = NULL; } } void PyramidCL::DownloadKeypoints() { const double twopi = 2.0*3.14159265358979323846; int idx = 0; float * buffer = &_keypoint_buffer[0]; vector keypoint_buffer2; //use a different keypoint buffer when processing with an exisint features list //without orientation information. if(_keypoint_index.size() > 0) { keypoint_buffer2.resize(_keypoint_buffer.size()); buffer = &keypoint_buffer2[0]; } float * p = buffer, *ps; CLTexImage * ftex = _featureTex; ///////////////////// float os = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); if(_down_sample_factor>0) os *= float(1<<_down_sample_factor); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; ///////////////////// for(int i = 0; i < _octave_num; i++, os *= 2.0f) { for(int j = 0; j < param._dog_level_num; j++, idx++, ftex++) { if(_levelFeatureNum[idx]>0) { ftex->CopyToHost(ps = p); for(int k = 0; k < _levelFeatureNum[idx]; k++, ps+=4) { ps[0] = os*(ps[0]-0.5f) + offset; //x ps[1] = os*(ps[1]-0.5f) + offset; //y ps[2] = os*ps[2]; ps[3] = (float)fmod(twopi-ps[3], twopi); //orientation, mirrored } p+= 4* _levelFeatureNum[idx]; } } } //put the feature into their original order for existing keypoint if(_keypoint_index.size() > 0) { for(int i = 0; i < _featureNum; ++i) { int index = _keypoint_index[i]; memcpy(&_keypoint_buffer[index*4], &keypoint_buffer2[i*4], 4 * sizeof(float)); } } } void PyramidCL::GenerateFeatureListCPU() { //no cpu version provided GenerateFeatureList(); } void PyramidCL::GenerateFeatureList(int i, int j, int reduction_count, vector& hbuffer) { /*int fcount = 0, idx = i * param._dog_level_num + j; int hist_level_num = _hpLevelNum - _pyramid_octave_first /2; int ii, k, len; CLTexImage * htex, * ftex, * tex, *got; ftex = _featureTex + idx; htex = _histoPyramidTex + hist_level_num -1; tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + 2 + j; got = GetBaseLevel(_octave_min + i, DATA_GRAD) + 2 + j; _OpenCL->InitHistogram(tex, htex); for(k = 0; k < reduction_count - 1; k++, htex--) { ProgramCL::ReduceHistogram(htex, htex -1); } //htex has the row reduction result len = htex->GetImgHeight() * 4; hbuffer.resize(len); _OpenCL->FinishCL(); htex->CopyToHost(&hbuffer[0]); // for(ii = 0; ii < len; ++ii) fcount += hbuffer[ii]; SetLevelFeatureNum(idx, fcount); //build the feature list if(fcount > 0) { _featureNum += fcount; _keypoint_buffer.resize(fcount * 4); //vector ikbuf(fcount*4); int* ibuf = (int*) (&_keypoint_buffer[0]); for(ii = 0; ii < len; ++ii) { int x = ii%4, y = ii / 4; for(int jj = 0 ; jj < hbuffer[ii]; ++jj, ibuf+=4) { ibuf[0] = x; ibuf[1] = y; ibuf[2] = jj; ibuf[3] = 0; } } _featureTex[idx].CopyFromHost(&_keypoint_buffer[0]); //////////////////////////////////////////// ProgramCL::GenerateList(_featureTex + idx, ++htex); for(k = 2; k < reduction_count; k++) { ProgramCL::GenerateList(_featureTex + idx, ++htex); } }*/ } void PyramidCL::GenerateFeatureList() { /*double t1, t2; int ocount = 0, reduction_count; int reverse = (GlobalUtil::_TruncateMethod == 1); vector hbuffer; _featureNum = 0; //for(int i = 0, idx = 0; i < _octave_num; i++) FOR_EACH_OCTAVE(i, reverse) { CLTexImage* tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + 2; reduction_count = FitHistogramPyramid(tex); if(GlobalUtil::_timingO) { t1 = CLOCK(); ocount = 0; std::cout<<"#"< 0 && _featureNum > GlobalUtil::_FeatureCountThreshold) continue; GenerateFeatureList(i, j, reduction_count, hbuffer); ///////////////////////////// if(GlobalUtil::_timingO) { int idx = i * param._dog_level_num + j; ocount += _levelFeatureNum[idx]; std::cout<< _levelFeatureNum[idx] <<"\t"; } } if(GlobalUtil::_timingO) { t2 = CLOCK(); std::cout << "| \t" << int(ocount) << " :\t(" << (t2 - t1) << ")\n"; } } ///// CopyGradientTex(); ///// if(GlobalUtil::_timingS)_OpenCL->FinishCL(); if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; }*/ } GLTexImage* PyramidCL::GetLevelTexture(int octave, int level) { return GetLevelTexture(octave, level, DATA_GAUSSIAN); } GLTexImage* PyramidCL::ConvertTexCL2GL(CLTexImage* tex, int dataName) { if(_bufferTEX == NULL) _bufferTEX = new GLTexImage; /////////////////////////////////////////// int ratio = GlobalUtil::_usePackedTex ? 2 : 1; int width = tex->GetImgWidth() * ratio; int height = tex->GetImgHeight() * ratio; int tw = max(width, _bufferTEX->GetTexWidth()); int th = max(height, _bufferTEX->GetTexHeight()); _bufferTEX->InitTexture(tw, th, 1, GL_RGBA); _bufferTEX->SetImageSize(width, height); ////////////////////////////////// CLTexImage texCL(_OpenCL->GetContextCL(), _OpenCL->GetCommandQueue()); texCL.InitTextureGL(*_bufferTEX, width, height, 4); switch(dataName) { case DATA_GAUSSIAN: _OpenCL->UnpackImage(tex, &texCL); break; case DATA_DOG:_OpenCL->UnpackImageDOG(tex, &texCL); break; case DATA_GRAD:_OpenCL->UnpackImageGRD(tex, &texCL); break; case DATA_KEYPOINT:_OpenCL->UnpackImageKEY(tex, tex - param._level_num * _pyramid_octave_num, &texCL);break; default: break; } return _bufferTEX; } GLTexImage* PyramidCL::GetLevelTexture(int octave, int level, int dataName) { CLTexImage* tex = GetBaseLevel(octave, dataName) + (level - param._level_min); return ConvertTexCL2GL(tex, dataName); } void PyramidCL::ConvertInputToCL(GLTexInput* input, CLTexImage* output) { int ws = input->GetImgWidth(), hs = input->GetImgHeight(); //copy the input image to pixel buffer object if(input->_pixel_data) { output->InitTexture(ws, hs, 1); output->CopyFromHost(input->_pixel_data); }else /*if(input->_rgb_converted && input->CopyToPBO(_bufferPBO, ws, hs, GL_LUMINANCE)) { output->InitTexture(ws, hs, 1); output->CopyFromPBO(ws, hs, _bufferPBO); }else if(input->CopyToPBO(_bufferPBO, ws, hs)) { CLTexImage texPBO(ws, hs, 4, _bufferPBO); output->InitTexture(ws, hs, 1); ProgramCL::ReduceToSingleChannel(output, &texPBO, !input->_rgb_converted); }else*/ { std::cerr<< "Unable To Convert Intput\n"; } } void PyramidCL::BuildPyramid(GLTexInput * input) { USE_TIMING(); int i, j; for ( i = _octave_min; i < _octave_min + _octave_num; i++) { CLTexImage *tex = GetBaseLevel(i); CLTexImage *buf = GetBaseLevel(i, DATA_DOG) +2; FilterCL ** filter = _OpenCL->f_gaussian_step; j = param._level_min + 1; OCTAVE_START(); if( i == _octave_min ) { if(GlobalUtil::_usePackedTex) { ConvertInputToCL(input, _inputTex); if(i < 0) _OpenCL->SampleImageU(tex, _inputTex, -i- 1); else _OpenCL->SampleImageD(tex, _inputTex, i + 1); }else { if(i == 0) ConvertInputToCL(input, tex); else { ConvertInputToCL(input, _inputTex); if(i < 0) _OpenCL->SampleImageU(tex, _inputTex, -i); else _OpenCL->SampleImageD(tex, _inputTex, i); } } _OpenCL->FilterInitialImage(tex, buf); }else { _OpenCL->SampleImageD(tex, GetBaseLevel(i - 1) + param._level_ds - param._level_min); _OpenCL->FilterSampledImage(tex, buf); } LEVEL_FINISH(); for( ; j <= param._level_max ; j++, tex++, filter++) { // filtering _OpenCL->FilterImage(*filter, tex + 1, tex, buf); LEVEL_FINISH(); } OCTAVE_FINISH(); } if(GlobalUtil::_timingS) _OpenCL->FinishCL(); } void PyramidCL::DetectKeypointsEX() { int i, j; double t0, t, ts, t1, t2; if(GlobalUtil::_timingS && GlobalUtil::_verbose) ts = CLOCK(); for(i = _octave_min; i < _octave_min + _octave_num; i++) { CLTexImage * gus = GetBaseLevel(i) + 1; CLTexImage * dog = GetBaseLevel(i, DATA_DOG) + 1; CLTexImage * grd = GetBaseLevel(i, DATA_GRAD) + 1; CLTexImage * rot = GetBaseLevel(i, DATA_ROT) + 1; //compute the gradient for(j = param._level_min +1; j <= param._level_max ; j++, gus++, dog++, grd++, rot++) { //input: gus and gus -1 //output: gradient, dog, orientation _OpenCL->ComputeDOG(gus, gus - 1, dog, grd, rot); } } if(GlobalUtil::_timingS && GlobalUtil::_verbose) { _OpenCL->FinishCL(); t1 = CLOCK(); } //if(GlobalUtil::_timingS) _OpenCL->FinishCL(); //if(!GlobalUtil::_usePackedTex) return; //not finished //return; for ( i = _octave_min; i < _octave_min + _octave_num; i++) { if(GlobalUtil::_timingO) { t0 = CLOCK(); std::cout<<"#"<<(i + _down_sample_factor)<<"\t"; } CLTexImage * dog = GetBaseLevel(i, DATA_DOG) + 2; CLTexImage * key = GetBaseLevel(i, DATA_KEYPOINT) +2; for( j = param._level_min +2; j < param._level_max ; j++, dog++, key++) { if(GlobalUtil::_timingL)t = CLOCK(); //input, dog, dog + 1, dog -1 //output, key _OpenCL->ComputeKEY(dog, key, param._dog_threshold, param._edge_threshold); if(GlobalUtil::_timingL) { std::cout<<(CLOCK()-t)<<"\t"; } } if(GlobalUtil::_timingO) { std::cout<<"|\t"<<(CLOCK()-t0)<<"\n"; } } if(GlobalUtil::_timingS) { _OpenCL->FinishCL(); if(GlobalUtil::_verbose) { t2 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n" <<"\t"<<(t2-t1)<<"\n"; } } } void PyramidCL::CopyGradientTex() { /*double ts, t1; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); for(int i = 0, idx = 0; i < _octave_num; i++) { CLTexImage * got = GetBaseLevel(i + _octave_min, DATA_GRAD) + 1; //compute the gradient for(int j = 0; j < param._dog_level_num ; j++, got++, idx++) { if(_levelFeatureNum[idx] > 0) got->CopyToTexture2D(); } } if(GlobalUtil::_timingS) { ProgramCL::FinishCLDA(); if(GlobalUtil::_verbose) { t1 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n"; } }*/ } void PyramidCL::ComputeGradient() { /*int i, j; double ts, t1; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); for(i = _octave_min; i < _octave_min + _octave_num; i++) { CLTexImage * gus = GetBaseLevel(i) + 1; CLTexImage * dog = GetBaseLevel(i, DATA_DOG) + 1; CLTexImage * got = GetBaseLevel(i, DATA_GRAD) + 1; //compute the gradient for(j = 0; j < param._dog_level_num ; j++, gus++, dog++, got++) { ProgramCL::ComputeDOG(gus, dog, got); } } if(GlobalUtil::_timingS) { ProgramCL::FinishCLDA(); if(GlobalUtil::_verbose) { t1 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n"; } }*/ } int PyramidCL::FitHistogramPyramid(CLTexImage* tex) { CLTexImage *htex; int hist_level_num = _hpLevelNum - _pyramid_octave_first / 2; htex = _histoPyramidTex + hist_level_num - 1; int w = (tex->GetImgWidth() + 2) >> 2; int h = tex->GetImgHeight(); int count = 0; for(int k = 0; k < hist_level_num; k++, htex--) { //htex->SetImageSize(w, h); htex->InitTexture(w, h, 4); ++count; if(w == 1) break; w = (w + 3)>>2; } return count; } void PyramidCL::GetFeatureOrientations() { /* CLTexImage * ftex = _featureTex; int * count = _levelFeatureNum; float sigma, sigma_step = powf(2.0f, 1.0f/param._dog_level_num); for(int i = 0; i < _octave_num; i++) { CLTexImage* got = GetBaseLevel(i + _octave_min, DATA_GRAD) + 1; CLTexImage* key = GetBaseLevel(i + _octave_min, DATA_KEYPOINT) + 2; for(int j = 0; j < param._dog_level_num; j++, ftex++, count++, got++, key++) { if(*count<=0)continue; //if(ftex->GetImgWidth() < *count) ftex->InitTexture(*count, 1, 4); sigma = param.GetLevelSigma(j+param._level_min+1); ProgramCL::ComputeOrientation(ftex, got, key, sigma, sigma_step, _existing_keypoints); } } if(GlobalUtil::_timingS)ProgramCL::FinishCL(); */ } void PyramidCL::GetSimplifiedOrientation() { //no simplified orientation GetFeatureOrientations(); } CLTexImage* PyramidCL::GetBaseLevel(int octave, int dataName) { if(octave <_octave_min || octave > _octave_min + _octave_num) return NULL; int offset = (_pyramid_octave_first + octave - _octave_min) * param._level_num; int num = param._level_num * _pyramid_octave_num; return _allPyramid + num * dataName + offset; } #endif colmap-3.9.1/src/thirdparty/SiftGPU/PyramidCL.h000066400000000000000000000052241454702036400212460ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: PyramidCL.h // Author: Changchang Wu // Description : interface for the PyramdCL // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _PYRAMID_CL_H #define _PYRAMID_CL_H #if defined(CL_SIFTGPU_ENABLED) class CLTexImage; class SiftPyramid; class ProgramBagCL; class PyramidCL: public SiftPyramid { CLTexImage* _inputTex; CLTexImage* _allPyramid; CLTexImage* _histoPyramidTex; CLTexImage* _featureTex; CLTexImage* _descriptorTex; CLTexImage* _orientationTex; ProgramBagCL* _OpenCL; GLTexImage* _bufferTEX; public: virtual void GetFeatureDescriptors(); virtual void GenerateFeatureListTex(); virtual void ReshapeFeatureListCPU(); virtual void GenerateFeatureDisplayVBO(); virtual void DestroySharedData(); virtual void DestroyPerLevelData(); virtual void DestroyPyramidData(); virtual void DownloadKeypoints(); virtual void GenerateFeatureListCPU(); virtual void GenerateFeatureList(); virtual GLTexImage* GetLevelTexture(int octave, int level); virtual GLTexImage* GetLevelTexture(int octave, int level, int dataName); virtual void BuildPyramid(GLTexInput * input); virtual void DetectKeypointsEX(); virtual void ComputeGradient(); virtual void GetFeatureOrientations(); virtual void GetSimplifiedOrientation(); virtual void InitPyramid(int w, int h, int ds = 0); virtual void ResizePyramid(int w, int h); ////////// void CopyGradientTex(); void FitPyramid(int w, int h); void InitializeContext(); int ResizeFeatureStorage(); int FitHistogramPyramid(CLTexImage* tex); void SetLevelFeatureNum(int idx, int fcount); void ConvertInputToCL(GLTexInput* input, CLTexImage* output); GLTexImage* ConvertTexCL2GL(CLTexImage* tex, int dataName); CLTexImage* GetBaseLevel(int octave, int dataName = DATA_GAUSSIAN); private: void GenerateFeatureList(int i, int j, int reduction_count, vector& hbuffer); public: PyramidCL(SiftParam& sp); virtual ~PyramidCL(); }; #endif #endif colmap-3.9.1/src/thirdparty/SiftGPU/PyramidCU.cpp000066400000000000000000000750621454702036400216210ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: PyramidCU.cpp // Author: Changchang Wu // Description : implementation of the PyramidCU class. // CUDA-based implementation of SiftPyramid // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CUDA_SIFTGPU_ENABLED) #include "GL/glew.h" #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "GLTexImage.h" #include "CuTexImage.h" #include "SiftGPU.h" #include "SiftPyramid.h" #include "ProgramCU.h" #include "PyramidCU.h" //#include "imdebug/imdebuggl.h" //#pragma comment (lib, "../lib/imdebug.lib") #define USE_TIMING() double t, t0, tt; #define OCTAVE_START() if(GlobalUtil::_timingO){ t = t0 = CLOCK(); cout<<"#"<=0) { wp = w >> _octave_min_default; hp = h >> _octave_min_default; }else { //can't upsample by more than 8 _octave_min_default = max(-3, _octave_min_default); // wp = w << (-_octave_min_default); hp = h << (-_octave_min_default); } _octave_min = _octave_min_default; }else { //must use 0 as _octave_min; _octave_min = 0; _down_sample_factor = ds; w >>= ds; h >>= ds; ///// TruncateWidth(w); wp = w; hp = h; } while(wp > GlobalUtil::_texMaxDim || hp > GlobalUtil::_texMaxDim ) { _octave_min ++; wp >>= 1; hp >>= 1; toobig = 1; } while(GlobalUtil::_MemCapGPU > 0 && GlobalUtil::_FitMemoryCap && (wp >_pyramid_width || hp > _pyramid_height)&& max(max(wp, hp), max(_pyramid_width, _pyramid_height)) > 1024 * sqrt(GlobalUtil::_MemCapGPU / 110.0)) { _octave_min ++; wp >>= 1; hp >>= 1; toobig = 2; } if(toobig && GlobalUtil::_verbose && _octave_min > 0) { std::cout<<(toobig == 2 ? "[**SKIP OCTAVES**]:\tExceeding Memory Cap (-nomc)\n" : "[**SKIP OCTAVES**]:\tReaching the dimension limit(-maxd)!\n"); } //ResizePyramid(wp, hp); if( wp == _pyramid_width && hp == _pyramid_height && _allocated ) { FitPyramid(wp, hp); }else if(GlobalUtil::_ForceTightPyramid || _allocated ==0) { ResizePyramid(wp, hp); } else if( wp > _pyramid_width || hp > _pyramid_height ) { ResizePyramid(max(wp, _pyramid_width), max(hp, _pyramid_height)); if(wp < _pyramid_width || hp < _pyramid_height) FitPyramid(wp, hp); } else { //try use the pyramid allocated for large image on small input images FitPyramid(wp, hp); } } void PyramidCU::ResizePyramid(int w, int h) { // unsigned int totalkb = 0; int _octave_num_new, input_sz, i, j; // if(_pyramid_width == w && _pyramid_height == h && _allocated) return; if(w > GlobalUtil::_texMaxDim || h > GlobalUtil::_texMaxDim) return ; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <0) { DestroyPerLevelData(); DestroyPyramidData(); } _pyramid_octave_num = _octave_num_new; } _octave_num = _pyramid_octave_num; int noct = _octave_num; int nlev = param._level_num; // //initialize the pyramid if(_allPyramid==NULL) _allPyramid = new CuTexImage[ noct* nlev * DATA_NUM]; CuTexImage * gus = GetBaseLevel(_octave_min, DATA_GAUSSIAN); CuTexImage * dog = GetBaseLevel(_octave_min, DATA_DOG); CuTexImage * got = GetBaseLevel(_octave_min, DATA_GRAD); CuTexImage * key = GetBaseLevel(_octave_min, DATA_KEYPOINT); ////////////there could be "out of memory" happening during the allocation for(i = 0; i< noct; i++) { int wa = ((w + 3) / 4) * 4; totalkb += ((nlev *8 -19)* (wa * h) * 4 / 1024); for( j = 0; j< nlev; j++, gus++, dog++, got++, key++) { gus->InitTexture(wa, h); //nlev if(j==0)continue; dog->InitTexture(wa, h); //nlev -1 if( j >= 1 && j < 1 + param._dog_level_num) { got->InitTexture(wa, h, 2); //2 * nlev - 6 } if(j > 1 && j < nlev -1) key->InitTexture(wa, h, 4); // nlev -3 ; 4 * nlev - 12 } w>>=1; h>>=1; } totalkb += ResizeFeatureStorage(); if(ProgramCU::CheckErrorCUDA("ResizePyramid")) SetFailStatus(); _allocated = 1; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <<(totalkb/1024)<<"MB\n"; } void PyramidCU::FitPyramid(int w, int h) { _pyramid_octave_first = 0; // _octave_num = GlobalUtil::_octave_num_default; int _octave_num_max = max(1, (int) floor (log ( double(min(w, h)))/log(2.0)) -3 ); if(_octave_num < 1 || _octave_num > _octave_num_max) { _octave_num = _octave_num_max; } int pw = _pyramid_width>>1, ph = _pyramid_height>>1; while(_pyramid_octave_first + _octave_num < _pyramid_octave_num && pw >= w && ph >= h) { _pyramid_octave_first++; pw >>= 1; ph >>= 1; } ////////////////// int nlev = param._level_num; CuTexImage * gus = GetBaseLevel(_octave_min, DATA_GAUSSIAN); CuTexImage * dog = GetBaseLevel(_octave_min, DATA_DOG); CuTexImage * got = GetBaseLevel(_octave_min, DATA_GRAD); CuTexImage * key = GetBaseLevel(_octave_min, DATA_KEYPOINT); for(int i = 0; i< _octave_num; i++) { int wa = ((w + 3) / 4) * 4; for(int j = 0; j< nlev; j++, gus++, dog++, got++, key++) { gus->InitTexture(wa, h); //nlev if(j==0)continue; dog->InitTexture(wa, h); //nlev -1 if( j >= 1 && j < 1 + param._dog_level_num) { got->InitTexture(wa, h, 2); //2 * nlev - 6 } if(j > 1 && j < nlev -1) key->InitTexture(wa, h, 4); // nlev -3 ; 4 * nlev - 12 } w>>=1; h>>=1; } } int PyramidCU::CheckCudaDevice(int device) { return ProgramCU::CheckCudaDevice(device); } void PyramidCU::SetLevelFeatureNum(int idx, int fcount) { _featureTex[idx].InitTexture(fcount, 1, 4); _levelFeatureNum[idx] = fcount; } int PyramidCU::ResizeFeatureStorage() { int totalkb = 0; if(_levelFeatureNum==NULL) _levelFeatureNum = new int[_octave_num * param._dog_level_num]; std::fill(_levelFeatureNum, _levelFeatureNum+_octave_num * param._dog_level_num, 0); int wmax = GetBaseLevel(_octave_min)->GetImgWidth(); int hmax = GetBaseLevel(_octave_min)->GetImgHeight(); int whmax = max(wmax, hmax); int w, i; // int num = (int)ceil(log(double(whmax))/log(4.0)); if( _hpLevelNum != num) { _hpLevelNum = num; if(_histoPyramidTex ) delete [] _histoPyramidTex; _histoPyramidTex = new CuTexImage[_hpLevelNum]; } for(i = 0, w = 1; i < _hpLevelNum; i++) { _histoPyramidTex[i].InitTexture(w, whmax, 4); w<<=2; } // (4 ^ (_hpLevelNum) -1 / 3) pixels totalkb += (((1 << (2 * _hpLevelNum)) -1) / 3 * 16 / 1024); //initialize the feature texture int idx = 0, n = _octave_num * param._dog_level_num; if(_featureTex==NULL) _featureTex = new CuTexImage[n]; if(GlobalUtil::_MaxOrientation >1 && GlobalUtil::_OrientationPack2==0 && _orientationTex== NULL) _orientationTex = new CuTexImage[n]; for(i = 0; i < _octave_num; i++) { CuTexImage * tex = GetBaseLevel(i+_octave_min); int fmax = int(tex->GetImgWidth() * tex->GetImgHeight()*GlobalUtil::_MaxFeaturePercent); // if(fmax > GlobalUtil::_MaxLevelFeatureNum) fmax = GlobalUtil::_MaxLevelFeatureNum; else if(fmax < 32) fmax = 32; //give it at least a space of 32 feature for(int j = 0; j < param._dog_level_num; j++, idx++) { _featureTex[idx].InitTexture(fmax, 1, 4); totalkb += fmax * 16 /1024; // if(GlobalUtil::_MaxOrientation>1 && GlobalUtil::_OrientationPack2 == 0) { _orientationTex[idx].InitTexture(fmax, 1, 4); totalkb += fmax * 16 /1024; } } } //this just need be initialized once if(_descriptorTex==NULL) { //initialize feature texture pyramid int fmax = _featureTex->GetImgWidth(); _descriptorTex = new CuTexImage; totalkb += ( fmax /2); _descriptorTex->InitTexture(fmax *128, 1, 1); }else { totalkb += _descriptorTex->GetDataSize()/1024; } return totalkb; } void PyramidCU::GetFeatureDescriptors() { //descriptors... float* pd = &_descriptor_buffer[0]; vector descriptor_buffer2; //use another buffer if we need to re-order the descriptors if(_keypoint_index.size() > 0) { descriptor_buffer2.resize(_descriptor_buffer.size()); pd = &descriptor_buffer2[0]; } CuTexImage * got, * ftex= _featureTex; for(int i = 0, idx = 0; i < _octave_num; i++) { got = GetBaseLevel(i + _octave_min, DATA_GRAD) + 1; for(int j = 0; j < param._dog_level_num; j++, ftex++, idx++, got++) { if(_levelFeatureNum[idx]==0) continue; ProgramCU::ComputeDescriptor(ftex, got, _descriptorTex, IsUsingRectDescription());//process _descriptorTex->CopyToHost(pd); //readback descriptor pd += 128*_levelFeatureNum[idx]; } } if(GlobalUtil::_timingS) ProgramCU::FinishCUDA(); if(_keypoint_index.size() > 0) { //put the descriptor back to the original order for keypoint list. for(int i = 0; i < _featureNum; ++i) { int index = _keypoint_index[i]; memcpy(&_descriptor_buffer[index*128], &descriptor_buffer2[i*128], 128 * sizeof(float)); } } if(ProgramCU::CheckErrorCUDA("PyramidCU::GetFeatureDescriptors")) SetFailStatus(); } void PyramidCU::GenerateFeatureListTex() { vector list; int idx = 0; const double twopi = 2.0*3.14159265358979323846; float sigma_half_step = powf(2.0f, 0.5f / param._dog_level_num); float octave_sigma = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; if(_down_sample_factor>0) octave_sigma *= float(1<<_down_sample_factor); _keypoint_index.resize(0); // should already be 0 for(int i = 0; i < _octave_num; i++, octave_sigma*= 2.0f) { for(int j = 0; j < param._dog_level_num; j++, idx++) { list.resize(0); float level_sigma = param.GetLevelSigma(j + param._level_min + 1) * octave_sigma; float sigma_min = level_sigma / sigma_half_step; float sigma_max = level_sigma * sigma_half_step; int fcount = 0 ; for(int k = 0; k < _featureNum; k++) { float * key = &_keypoint_buffer[k*4]; float sigmak = key[2]; ////////////////////////////////////// if(IsUsingRectDescription()) sigmak = min(key[2], key[3]) / 12.0f; if( (sigmak >= sigma_min && sigmak < sigma_max) ||(sigmak < sigma_min && i ==0 && j == 0) ||(sigmak > sigma_max && i == _octave_num -1 && j == param._dog_level_num - 1)) { //add this keypoint to the list list.push_back((key[0] - offset) / octave_sigma + 0.5f); list.push_back((key[1] - offset) / octave_sigma + 0.5f); if(IsUsingRectDescription()) { list.push_back(key[2] / octave_sigma); list.push_back(key[3] / octave_sigma); }else { list.push_back(key[2] / octave_sigma); list.push_back((float)fmod(twopi-key[3], twopi)); } fcount ++; //save the index of keypoints _keypoint_index.push_back(k); } } _levelFeatureNum[idx] = fcount; if(fcount==0)continue; CuTexImage * ftex = _featureTex+idx; SetLevelFeatureNum(idx, fcount); ftex->CopyFromHost(&list[0]); } } if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } } void PyramidCU::ReshapeFeatureListCPU() { int i, szmax =0, sz; int n = param._dog_level_num*_octave_num; for( i = 0; i < n; i++) { sz = _levelFeatureNum[i]; if(sz > szmax ) szmax = sz; } float * buffer = new float[szmax*16]; float * buffer1 = buffer; float * buffer2 = buffer + szmax*4; _featureNum = 0; #ifdef NO_DUPLICATE_DOWNLOAD const double twopi = 2.0*3.14159265358979323846; _keypoint_buffer.resize(0); float os = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); if(_down_sample_factor>0) os *= float(1<<_down_sample_factor); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; #endif for(i = 0; i < n; i++) { if(_levelFeatureNum[i]==0)continue; _featureTex[i].CopyToHost(buffer1); int fcount =0; float * src = buffer1; float * des = buffer2; const static double factor = 2.0*3.14159265358979323846/65535.0; for(int j = 0; j < _levelFeatureNum[i]; j++, src+=4) { unsigned short * orientations = (unsigned short*) (&src[3]); if(orientations[0] != 65535) { des[0] = src[0]; des[1] = src[1]; des[2] = src[2]; des[3] = float( factor* orientations[0]); fcount++; des += 4; if(orientations[1] != 65535 && orientations[1] != orientations[0]) { des[0] = src[0]; des[1] = src[1]; des[2] = src[2]; des[3] = float(factor* orientations[1]); fcount++; des += 4; } } } //texture size SetLevelFeatureNum(i, fcount); _featureTex[i].CopyFromHost(buffer2); if(fcount == 0) continue; #ifdef NO_DUPLICATE_DOWNLOAD float oss = os * (1 << (i / param._dog_level_num)); _keypoint_buffer.resize((_featureNum + fcount) * 4); float* ds = &_keypoint_buffer[_featureNum * 4]; float* fs = buffer2; for(int k = 0; k < fcount; k++, ds+=4, fs+=4) { ds[0] = oss*(fs[0]-0.5f) + offset; //x ds[1] = oss*(fs[1]-0.5f) + offset; //y ds[2] = oss*fs[2]; //scale ds[3] = (float)fmod(twopi-fs[3], twopi); //orientation, mirrored } #endif _featureNum += fcount; } delete[] buffer; if(GlobalUtil::_verbose) { std::cout<<"#Features MO:\t"<<_featureNum< keypoint_buffer2; //use a different keypoint buffer when processing with an exisint features list //without orientation information. if(_keypoint_index.size() > 0) { keypoint_buffer2.resize(_keypoint_buffer.size()); buffer = &keypoint_buffer2[0]; } float * p = buffer, *ps; CuTexImage * ftex = _featureTex; ///////////////////// float os = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); if(_down_sample_factor>0) os *= float(1<<_down_sample_factor); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; ///////////////////// for(int i = 0; i < _octave_num; i++, os *= 2.0f) { for(int j = 0; j < param._dog_level_num; j++, idx++, ftex++) { if(_levelFeatureNum[idx]>0) { ftex->CopyToHost(ps = p); for(int k = 0; k < _levelFeatureNum[idx]; k++, ps+=4) { ps[0] = os*(ps[0]-0.5f) + offset; //x ps[1] = os*(ps[1]-0.5f) + offset; //y ps[2] = os*ps[2]; ps[3] = (float)fmod(twopi-ps[3], twopi); //orientation, mirrored } p+= 4* _levelFeatureNum[idx]; } } } //put the feature into their original order for existing keypoint if(_keypoint_index.size() > 0) { for(int i = 0; i < _featureNum; ++i) { int index = _keypoint_index[i]; memcpy(&_keypoint_buffer[index*4], &keypoint_buffer2[i*4], 4 * sizeof(float)); } } } void PyramidCU::GenerateFeatureListCPU() { //no cpu version provided GenerateFeatureList(); } void PyramidCU::GenerateFeatureList(int i, int j, int reduction_count, vector& hbuffer) { int fcount = 0, idx = i * param._dog_level_num + j; int hist_level_num = _hpLevelNum - _pyramid_octave_first /2; int ii, k, len; CuTexImage * htex, * ftex, * tex, *got; ftex = _featureTex + idx; htex = _histoPyramidTex + hist_level_num -1; tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + 2 + j; got = GetBaseLevel(_octave_min + i, DATA_GRAD) + 2 + j; ProgramCU::InitHistogram(tex, htex); for(k = 0; k < reduction_count - 1; k++, htex--) { ProgramCU::ReduceHistogram(htex, htex -1); } //htex has the row reduction result len = htex->GetImgHeight() * 4; hbuffer.resize(len); ProgramCU::FinishCUDA(); htex->CopyToHost(&hbuffer[0]); ////TO DO: track the error found here.. for(ii = 0; ii < len; ++ii) {if(!(hbuffer[ii]>= 0)) hbuffer[ii] = 0; }//? for(ii = 0; ii < len; ++ii) fcount += hbuffer[ii]; SetLevelFeatureNum(idx, fcount); //build the feature list if(fcount > 0) { _featureNum += fcount; _keypoint_buffer.resize(fcount * 4); //vector ikbuf(fcount*4); int* ibuf = (int*) (&_keypoint_buffer[0]); for(ii = 0; ii < len; ++ii) { int x = ii%4, y = ii / 4; for(int jj = 0 ; jj < hbuffer[ii]; ++jj, ibuf+=4) { ibuf[0] = x; ibuf[1] = y; ibuf[2] = jj; ibuf[3] = 0; } } _featureTex[idx].CopyFromHost(&_keypoint_buffer[0]); //////////////////////////////////////////// ProgramCU::GenerateList(_featureTex + idx, ++htex); for(k = 2; k < reduction_count; k++) { ProgramCU::GenerateList(_featureTex + idx, ++htex); } } } void PyramidCU::GenerateFeatureList() { double t1, t2; int ocount = 0, reduction_count; int reverse = (GlobalUtil::_TruncateMethod == 1); vector hbuffer; _featureNum = 0; //for(int i = 0, idx = 0; i < _octave_num; i++) FOR_EACH_OCTAVE(i, reverse) { CuTexImage* tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + 2; reduction_count = FitHistogramPyramid(tex); if(GlobalUtil::_timingO) { t1 = CLOCK(); ocount = 0; std::cout<<"#"< 0 && _featureNum > GlobalUtil::_FeatureCountThreshold) { int idx = i * param._dog_level_num + j; _levelFeatureNum[idx] = 0; continue; } GenerateFeatureList(i, j, reduction_count, hbuffer); ///////////////////////////// if(GlobalUtil::_timingO) { int idx = i * param._dog_level_num + j; ocount += _levelFeatureNum[idx]; std::cout<< _levelFeatureNum[idx] <<"\t"; } } if(GlobalUtil::_timingO) { t2 = CLOCK(); std::cout << "| \t" << int(ocount) << " :\t(" << (t2 - t1) << ")\n"; } } ///// CopyGradientTex(); ///// if(GlobalUtil::_timingS)ProgramCU::FinishCUDA(); if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } if(ProgramCU::CheckErrorCUDA("PyramidCU::GenerateFeatureList")) SetFailStatus(); } GLTexImage* PyramidCU::GetLevelTexture(int octave, int level) { return GetLevelTexture(octave, level, DATA_GAUSSIAN); } GLTexImage* PyramidCU::ConvertTexCU2GL(CuTexImage* tex, int dataName) { GLenum format = GL_LUMINANCE; int convert_done = 1; if(_bufferPBO == 0) glGenBuffers(1, &_bufferPBO); if(_bufferTEX == NULL) _bufferTEX = new GLTexImage; switch(dataName) { case DATA_GAUSSIAN: { convert_done = tex->CopyToPBO(_bufferPBO); break; } case DATA_DOG: { CuTexImage texPBO(tex->GetImgWidth(), tex->GetImgHeight(), 1, _bufferPBO); if(texPBO._cuData == 0 || tex->_cuData == NULL) convert_done = 0; else ProgramCU::DisplayConvertDOG(tex, &texPBO); break; } case DATA_GRAD: { CuTexImage texPBO(tex->GetImgWidth(), tex->GetImgHeight(), 1, _bufferPBO); if(texPBO._cuData == 0 || tex->_cuData == NULL) convert_done = 0; else ProgramCU::DisplayConvertGRD(tex, &texPBO); break; } case DATA_KEYPOINT: { CuTexImage * dog = tex - param._level_num * _pyramid_octave_num; format = GL_RGBA; CuTexImage texPBO(tex->GetImgWidth(), tex->GetImgHeight(), 4, _bufferPBO); if(texPBO._cuData == 0 || tex->_cuData == NULL) convert_done = 0; else ProgramCU::DisplayConvertKEY(tex, dog, &texPBO); break; } default: convert_done = 0; break; } if(convert_done) { _bufferTEX->InitTexture(max(_bufferTEX->GetTexWidth(), tex->GetImgWidth()), max(_bufferTEX->GetTexHeight(), tex->GetImgHeight())); _bufferTEX->CopyFromPBO(_bufferPBO, tex->GetImgWidth(), tex->GetImgHeight(), format); }else { _bufferTEX->SetImageSize(0, 0); } return _bufferTEX; } GLTexImage* PyramidCU::GetLevelTexture(int octave, int level, int dataName) { CuTexImage* tex = GetBaseLevel(octave, dataName) + (level - param._level_min); //CuTexImage* gus = GetBaseLevel(octave, DATA_GAUSSIAN) + (level - param._level_min); return ConvertTexCU2GL(tex, dataName); } void PyramidCU::ConvertInputToCU(GLTexInput* input) { int ws = input->GetImgWidth(), hs = input->GetImgHeight(); TruncateWidth(ws); //copy the input image to pixel buffer object if(input->_pixel_data) { _inputTex->InitTexture(ws, hs, 1); _inputTex->CopyFromHost(input->_pixel_data); }else { if(_bufferPBO == 0) glGenBuffers(1, &_bufferPBO); if(input->_rgb_converted && input->CopyToPBO(_bufferPBO, ws, hs, GL_LUMINANCE)) { _inputTex->InitTexture(ws, hs, 1); _inputTex->CopyFromPBO(ws, hs, _bufferPBO); }else if(input->CopyToPBO(_bufferPBO, ws, hs)) { CuTexImage texPBO(ws, hs, 4, _bufferPBO); _inputTex->InitTexture(ws, hs, 1); ProgramCU::ReduceToSingleChannel(_inputTex, &texPBO, !input->_rgb_converted); }else { std::cerr<< "Unable To Convert Intput\n"; } } } void PyramidCU::BuildPyramid(GLTexInput * input) { USE_TIMING(); int i, j; for ( i = _octave_min; i < _octave_min + _octave_num; i++) { float* filter_sigma = param._sigma; CuTexImage *tex = GetBaseLevel(i); CuTexImage *buf = GetBaseLevel(i, DATA_KEYPOINT) +2; j = param._level_min + 1; OCTAVE_START(); if( i == _octave_min ) { ConvertInputToCU(input); if(i == 0) { ProgramCU::FilterImage(tex, _inputTex, buf, param.GetInitialSmoothSigma(_octave_min + _down_sample_factor)); }else { if(i < 0) ProgramCU::SampleImageU(tex, _inputTex, -i); else ProgramCU::SampleImageD(tex, _inputTex, i); ProgramCU::FilterImage(tex, tex, buf, param.GetInitialSmoothSigma(_octave_min + _down_sample_factor)); } }else { ProgramCU::SampleImageD(tex, GetBaseLevel(i - 1) + param._level_ds - param._level_min); if(param._sigma_skip1 > 0) { ProgramCU::FilterImage(tex, tex, buf, param._sigma_skip1); } } LEVEL_FINISH(); for( ; j <= param._level_max ; j++, tex++, filter_sigma++) { // filtering ProgramCU::FilterImage(tex + 1, tex, buf, *filter_sigma); LEVEL_FINISH(); } OCTAVE_FINISH(); } if(GlobalUtil::_timingS) ProgramCU::FinishCUDA(); if(ProgramCU::CheckErrorCUDA("PyramidCU::BuildPyramid")) SetFailStatus(); } void PyramidCU::DetectKeypointsEX() { int i, j; double t0, t, ts, t1, t2; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); for(i = _octave_min; i < _octave_min + _octave_num; i++) { CuTexImage * gus = GetBaseLevel(i) + 1; CuTexImage * dog = GetBaseLevel(i, DATA_DOG) + 1; CuTexImage * got = GetBaseLevel(i, DATA_GRAD) + 1; //compute the gradient for(j = param._level_min +1; j <= param._level_max ; j++, gus++, dog++, got++) { //input: gus and gus -1 //output: gradient, dog, orientation ProgramCU::ComputeDOG(gus, dog, got); } } if(GlobalUtil::_timingS && GlobalUtil::_verbose) { ProgramCU::FinishCUDA(); t1 = CLOCK(); } for ( i = _octave_min; i < _octave_min + _octave_num; i++) { if(GlobalUtil::_timingO) { t0 = CLOCK(); std::cout<<"#"<<(i + _down_sample_factor)<<"\t"; } CuTexImage * dog = GetBaseLevel(i, DATA_DOG) + 2; CuTexImage * key = GetBaseLevel(i, DATA_KEYPOINT) +2; for( j = param._level_min +2; j < param._level_max ; j++, dog++, key++) { if(GlobalUtil::_timingL)t = CLOCK(); //input, dog, dog + 1, dog -1 //output, key ProgramCU::ComputeKEY(dog, key, param._dog_threshold, param._edge_threshold); if(GlobalUtil::_timingL) { std::cout<<(CLOCK()-t)<<"\t"; } } if(GlobalUtil::_timingO) { std::cout<<"|\t"<<(CLOCK()-t0)<<"\n"; } } if(GlobalUtil::_timingS) { ProgramCU::FinishCUDA(); if(GlobalUtil::_verbose) { t2 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n" <<"\t"<<(t2-t1)<<"\n"; } } } void PyramidCU::CopyGradientTex() { double ts, t1; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); for(int i = 0, idx = 0; i < _octave_num; i++) { CuTexImage * got = GetBaseLevel(i + _octave_min, DATA_GRAD) + 1; //compute the gradient for(int j = 0; j < param._dog_level_num ; j++, got++, idx++) { // if(_levelFeatureNum[idx] > 0) got->CopyToTexture2D(); } } if(GlobalUtil::_timingS) { ProgramCU::FinishCUDA(); if(GlobalUtil::_verbose) { t1 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n"; } } } void PyramidCU::ComputeGradient() { int i, j; double ts, t1; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); for(i = _octave_min; i < _octave_min + _octave_num; i++) { CuTexImage * gus = GetBaseLevel(i) + 1; CuTexImage * dog = GetBaseLevel(i, DATA_DOG) + 1; CuTexImage * got = GetBaseLevel(i, DATA_GRAD) + 1; //compute the gradient for(j = 0; j < param._dog_level_num ; j++, gus++, dog++, got++) { ProgramCU::ComputeDOG(gus, dog, got); } } if(GlobalUtil::_timingS) { ProgramCU::FinishCUDA(); if(GlobalUtil::_verbose) { t1 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n"; } } } int PyramidCU::FitHistogramPyramid(CuTexImage* tex) { CuTexImage *htex; int hist_level_num = _hpLevelNum - _pyramid_octave_first / 2; htex = _histoPyramidTex + hist_level_num - 1; int w = (tex->GetImgWidth() + 2) >> 2; int h = tex->GetImgHeight(); int count = 0; for(int k = 0; k < hist_level_num; k++, htex--) { //htex->SetImageSize(w, h); htex->InitTexture(w, h, 4); ++count; if(w == 1) break; w = (w + 3)>>2; } return count; } void PyramidCU::GetFeatureOrientations() { CuTexImage * ftex = _featureTex; int * count = _levelFeatureNum; float sigma, sigma_step = powf(2.0f, 1.0f/param._dog_level_num); for(int i = 0; i < _octave_num; i++) { CuTexImage* got = GetBaseLevel(i + _octave_min, DATA_GRAD) + 1; CuTexImage* key = GetBaseLevel(i + _octave_min, DATA_KEYPOINT) + 2; for(int j = 0; j < param._dog_level_num; j++, ftex++, count++, got++, key++) { if(*count<=0)continue; //if(ftex->GetImgWidth() < *count) ftex->InitTexture(*count, 1, 4); sigma = param.GetLevelSigma(j+param._level_min+1); ProgramCU::ComputeOrientation(ftex, got, key, sigma, sigma_step, _existing_keypoints); } } if(GlobalUtil::_timingS)ProgramCU::FinishCUDA(); if(ProgramCU::CheckErrorCUDA("PyramidCU::GetFeatureOrientations")) SetFailStatus(); } void PyramidCU::GetSimplifiedOrientation() { //no simplified orientation GetFeatureOrientations(); } CuTexImage* PyramidCU::GetBaseLevel(int octave, int dataName) { if(octave <_octave_min || octave > _octave_min + _octave_num) return NULL; int offset = (_pyramid_octave_first + octave - _octave_min) * param._level_num; int num = param._level_num * _pyramid_octave_num; if (dataName == DATA_ROT) dataName = DATA_GRAD; return _allPyramid + num * dataName + offset; } #endif colmap-3.9.1/src/thirdparty/SiftGPU/PyramidCU.h000066400000000000000000000055361454702036400212650ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: PyramidCU.h // Author: Changchang Wu // Description : interface for the PyramdCU // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _PYRAMID_CU_H #define _PYRAMID_CU_H #if defined(CUDA_SIFTGPU_ENABLED) class GLTexImage; class CuTexImage; class SiftPyramid; class PyramidCU:public SiftPyramid { CuTexImage* _inputTex; CuTexImage* _allPyramid; CuTexImage* _histoPyramidTex; CuTexImage* _featureTex; CuTexImage* _descriptorTex; CuTexImage* _orientationTex; GLuint _bufferPBO; GLTexImage* _bufferTEX; public: virtual void GetFeatureDescriptors(); virtual void GenerateFeatureListTex(); virtual void ReshapeFeatureListCPU(); virtual void GenerateFeatureDisplayVBO(); virtual void DestroySharedData(); virtual void DestroyPerLevelData(); virtual void DestroyPyramidData(); virtual void DownloadKeypoints(); virtual void GenerateFeatureListCPU(); virtual void GenerateFeatureList(); virtual GLTexImage* GetLevelTexture(int octave, int level); virtual GLTexImage* GetLevelTexture(int octave, int level, int dataName); virtual void BuildPyramid(GLTexInput * input); virtual void DetectKeypointsEX(); virtual void ComputeGradient(); virtual void GetFeatureOrientations(); virtual void GetSimplifiedOrientation(); virtual void InitPyramid(int w, int h, int ds = 0); virtual void ResizePyramid(int w, int h); virtual int IsUsingRectDescription(){return _existing_keypoints & SIFT_RECT_DESCRIPTION; } ////////// void CopyGradientTex(); void FitPyramid(int w, int h); void InitializeContext(); int ResizeFeatureStorage(); int FitHistogramPyramid(CuTexImage* tex); void SetLevelFeatureNum(int idx, int fcount); void ConvertInputToCU(GLTexInput* input); GLTexImage* ConvertTexCU2GL(CuTexImage* tex, int dataName); CuTexImage* GetBaseLevel(int octave, int dataName = DATA_GAUSSIAN); void TruncateWidth(int& w) { w = GLTexInput::TruncateWidthCU(w); } ////////////////////////// static int CheckCudaDevice(int device); private: void GenerateFeatureList(int i, int j, int reduction_count, vector& hbuffer); public: PyramidCU(SiftParam& sp); virtual ~PyramidCU(); }; #endif #endif colmap-3.9.1/src/thirdparty/SiftGPU/PyramidGL.cpp000066400000000000000000002122501454702036400216040ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: PyramidGL.cpp // Author: Changchang Wu // Description : implementation of PyramidGL/PyramidNaive/PyramidPackdc . // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "GLTexImage.h" #include "SiftGPU.h" #include "ShaderMan.h" #include "SiftPyramid.h" #include "ProgramGLSL.h" #include "PyramidGL.h" #include "FrameBufferObject.h" #if defined(__SSE__) || _MSC_VER > 1200 #define USE_SSE_FOR_SIFTGPU #include #else //#pragma message( "SSE optimization off!\n" ) #endif #define USE_TIMING() double t, t0, tt; #define OCTAVE_START() if(GlobalUtil::_timingO){ t = t0 = CLOCK(); cout<<"#"<GetImgWidth() >> 1; int h = tex->GetImgHeight() >> 1; for(int k = 0; k GetImgHeight()!= h || htex->GetImgWidth() != w) { htex->SetImageSize(w, h); htex->ZeroHistoMargin(); } w = (w + 1)>>1; h = (h + 1) >> 1; } } void PyramidNaive::FitPyramid(int w, int h) { //(w, h) <= (_pyramid_width, _pyramid_height); _pyramid_octave_first = 0; // _octave_num = GlobalUtil::_octave_num_default; int _octave_num_max = GetRequiredOctaveNum(min(w, h)); if(_octave_num < 1 || _octave_num > _octave_num_max) { _octave_num = _octave_num_max; } int pw = _pyramid_width>>1, ph = _pyramid_height>>1; while(_pyramid_octave_first + _octave_num < _pyramid_octave_num && pw >= w && ph >= h) { _pyramid_octave_first++; pw >>= 1; ph >>= 1; } for(int i = 0; i < _octave_num; i++) { GLTexImage * tex = GetBaseLevel(i + _octave_min); GLTexImage * aux = GetBaseLevel(i + _octave_min, DATA_KEYPOINT); for(int j = param._level_min; j <= param._level_max; j++, tex++, aux++) { tex->SetImageSize(w, h); aux->SetImageSize(w, h); } w>>=1; h>>=1; } } void PyramidNaive::InitPyramid(int w, int h, int ds) { int wp, hp, toobig = 0; if(ds == 0) { _down_sample_factor = 0; if(GlobalUtil::_octave_min_default>=0) { wp = w >> GlobalUtil::_octave_min_default; hp = h >> GlobalUtil::_octave_min_default; }else { wp = w << (-GlobalUtil::_octave_min_default); hp = h << (-GlobalUtil::_octave_min_default); } _octave_min = _octave_min_default; }else { //must use 0 as _octave_min; _octave_min = 0; _down_sample_factor = ds; w >>= ds; h >>= ds; wp = w; hp = h; } while(wp > GlobalUtil::_texMaxDim || hp > GlobalUtil::_texMaxDim) { _octave_min ++; wp >>= 1; hp >>= 1; toobig = 1; } while(GlobalUtil::_MemCapGPU > 0 && GlobalUtil::_FitMemoryCap && (wp >_pyramid_width || hp > _pyramid_height) && max(max(wp, hp), max(_pyramid_width, _pyramid_height)) > 1024 * sqrt(GlobalUtil::_MemCapGPU / 140.0)) { _octave_min ++; wp >>= 1; hp >>= 1; toobig = 2; } if(toobig && GlobalUtil::_verbose) { std::cout<<(toobig == 2 ? "[**SKIP OCTAVES**]:\tExceeding Memory Cap (-nomc)\n" : "[**SKIP OCTAVES**]:\tReaching the dimension limit (-maxd)!\n"); } if( wp == _pyramid_width && hp == _pyramid_height && _allocated ) { FitPyramid(wp, hp); }else if(GlobalUtil::_ForceTightPyramid || _allocated ==0) { ResizePyramid(wp, hp); } else if( wp > _pyramid_width || hp > _pyramid_height ) { ResizePyramid(max(wp, _pyramid_width), max(hp, _pyramid_height)); if(wp < _pyramid_width || hp < _pyramid_height) FitPyramid(wp, hp); } else { //try use the pyramid allocated for large image on small input images FitPyramid(wp, hp); } //select the initial smoothing filter according to the new _octave_min ShaderMan::SelectInitialSmoothingFilter(_octave_min + _down_sample_factor, param); } void PyramidNaive::ResizePyramid( int w, int h) { // unsigned int totalkb = 0; int _octave_num_new, input_sz; int i, j; GLTexImage * tex, *aux; // if(_pyramid_width == w && _pyramid_height == h && _allocated) return; if(w > GlobalUtil::_texMaxDim || h > GlobalUtil::_texMaxDim) return ; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <0) { DestroyPerLevelData(); DestroyPyramidData(); } _pyramid_octave_num = _octave_num_new; } _octave_num = _pyramid_octave_num; int noct = _octave_num; int nlev = param._level_num; // //initialize the pyramid if(_texPyramid==NULL) _texPyramid = new GLTexImage[ noct* nlev ]; if(_auxPyramid==NULL) _auxPyramid = new GLTexImage[ noct* nlev ]; tex = GetBaseLevel(_octave_min, DATA_GAUSSIAN); aux = GetBaseLevel(_octave_min, DATA_KEYPOINT); for(i = 0; i< noct; i++) { totalkb += (nlev * w * h * 16 / 1024); for( j = 0; j< nlev; j++, tex++) { tex->InitTexture(w, h); //tex->AttachToFBO(0); } //several auxilary textures are not actually required totalkb += ((nlev - 3) * w * h * 16 /1024); for( j = 0; j< nlev ; j++, aux++) { if(j < 2) continue; if(j >= nlev - 1) continue; aux->InitTexture(w, h, 0); //aux->AttachToFBO(0); } w>>=1; h>>=1; } totalkb += ResizeFeatureStorage(); // _allocated = 1; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <<(totalkb/1024)<<"MB\n"; } int PyramidGL::ResizeFeatureStorage() { int totalkb = 0; if(_levelFeatureNum==NULL) _levelFeatureNum = new int[_octave_num * param._dog_level_num]; std::fill(_levelFeatureNum, _levelFeatureNum+_octave_num * param._dog_level_num, 0); int wmax = GetBaseLevel(_octave_min)->GetDrawWidth(); int hmax = GetBaseLevel(_octave_min)->GetDrawHeight(); int w ,h, i; //use a fbo to initialize textures.. FrameBufferObject fbo; // if(_histo_buffer == NULL) _histo_buffer = new float[((size_t)1) << (2 + 2 * GlobalUtil::_ListGenSkipGPU)]; //histogram for feature detection int num = (int)ceil(log(double(max(wmax, hmax)))/log(2.0)); if( _hpLevelNum != num) { _hpLevelNum = num; if(GlobalUtil::_ListGenGPU) { if(_histoPyramidTex ) delete [] _histoPyramidTex; _histoPyramidTex = new GLTexImage[_hpLevelNum]; w = h = 1 ; for(i = 0; i < _hpLevelNum; i++) { _histoPyramidTex[i].InitTexture(w, h, 0); _histoPyramidTex[i].AttachToFBO(0); w<<=1; h<<=1; } } } // (4 ^ (_hpLevelNum) -1 / 3) pixels if(GlobalUtil::_ListGenGPU) totalkb += (((1 << (2 * _hpLevelNum)) -1) / 3 * 16 / 1024); //initialize the feature texture int idx = 0, n = _octave_num * param._dog_level_num; if(_featureTex==NULL) _featureTex = new GLTexImage[n]; if(GlobalUtil::_MaxOrientation >1 && GlobalUtil::_OrientationPack2==0) { if(_orientationTex== NULL) _orientationTex = new GLTexImage[n]; } for(i = 0; i < _octave_num; i++) { GLTexImage * tex = GetBaseLevel(i+_octave_min); int fmax = int(tex->GetImgWidth()*tex->GetImgHeight()*GlobalUtil::_MaxFeaturePercent); int fw, fh; // if(fmax > GlobalUtil::_MaxLevelFeatureNum) fmax = GlobalUtil::_MaxLevelFeatureNum; else if(fmax < 32) fmax = 32; //give it at least a space of 32 feature GetTextureStorageSize(fmax, fw, fh); for(int j = 0; j < param._dog_level_num; j++, idx++) { _featureTex[idx].InitTexture(fw, fh, 0); _featureTex[idx].AttachToFBO(0); // if(_orientationTex) { _orientationTex[idx].InitTexture(fw, fh, 0); _orientationTex[idx].AttachToFBO(0); } } totalkb += fw * fh * 16 * param._dog_level_num * (_orientationTex? 2 : 1) /1024; } //this just need be initialized once if(_descriptorTex==NULL) { //initialize feature texture pyramid wmax = _featureTex->GetImgWidth(); hmax = _featureTex->GetImgHeight(); int nf, ns; if(GlobalUtil::_DescriptorPPT) { //32*4 = 128. nf = 32 / GlobalUtil::_DescriptorPPT; // how many textures we need ns = max(4, GlobalUtil::_DescriptorPPT); // how many point in one texture for one descriptor }else { //at least one, resue for visualization and other work nf = 1; ns = 4; } // _alignment = ns; // _descriptorTex = new GLTexImage[nf]; int fw, fh; GetAlignedStorageSize(hmax*wmax* max(ns, 10), _alignment, fw, fh); if(fh < hmax ) fh = hmax; if(fw < wmax ) fw = wmax; totalkb += ( fw * fh * nf * 16 /1024); for(i =0; i < nf; i++) { _descriptorTex[i].InitTexture(fw, fh); } }else { int nf = GlobalUtil::_DescriptorPPT? 32 / GlobalUtil::_DescriptorPPT: 1; totalkb += nf * _descriptorTex[0].GetTexWidth() * _descriptorTex[0].GetTexHeight() * 16 /1024; } return totalkb; } void PyramidNaive::BuildPyramid(GLTexInput *input) { USE_TIMING(); GLTexPacked * tex; FilterProgram** filter; FrameBufferObject fbo; glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); input->FitTexViewPort(); for (int i = _octave_min; i < _octave_min + _octave_num; i++) { tex = (GLTexPacked*)GetBaseLevel(i); filter = ShaderMan::s_bag->f_gaussian_step; OCTAVE_START(); if( i == _octave_min ) { if(i < 0) TextureUpSample(tex, input, 1<<(-i) ); else TextureDownSample(tex, input, 1< _octave_min + _octave_num) return NULL; switch(dataName) { case DATA_GAUSSIAN: case DATA_DOG: case DATA_GRAD: case DATA_ROT: return _texPyramid+ (_pyramid_octave_first + octave - _octave_min) * param._level_num + (level - param._level_min); case DATA_KEYPOINT: return _auxPyramid + (_pyramid_octave_first + octave - _octave_min) * param._level_num + (level - param._level_min); default: return NULL; } } GLTexImage* PyramidNaive::GetLevelTexture(int octave, int level) { return _texPyramid+ (_pyramid_octave_first + octave - _octave_min) * param._level_num + (level - param._level_min); } //in the packed implementation // DATA_GAUSSIAN, DATA_DOG, DATA_GAD will be stored in different textures. GLTexImage* PyramidNaive::GetBaseLevel(int octave, int dataName) { if(octave <_octave_min || octave > _octave_min + _octave_num) return NULL; switch(dataName) { case DATA_GAUSSIAN: case DATA_DOG: case DATA_GRAD: case DATA_ROT: return _texPyramid+ (_pyramid_octave_first + octave - _octave_min) * param._level_num; case DATA_KEYPOINT: return _auxPyramid + (_pyramid_octave_first + octave - _octave_min) * param._level_num; default: return NULL; } } void PyramidNaive::ComputeGradient() { int i, j; double ts, t1; GLTexImage * tex; FrameBufferObject fbo; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); for ( i = _octave_min; i < _octave_min + _octave_num; i++) { for( j = param._level_min + 1 ; j < param._level_max ; j++) { tex = GetLevelTexture(i, j); tex->FitTexViewPort(); tex->AttachToFBO(0); tex->BindTex(); ShaderMan::UseShaderGradientPass(); tex->DrawQuadMT4(); } } if(GlobalUtil::_timingS && GlobalUtil::_verbose) { glFinish(); t1 = CLOCK(); std::cout<<"\t"<<(t1-ts)<<"\n"; } UnloadProgram(); GLTexImage::UnbindMultiTex(3); fbo.UnattachTex(GL_COLOR_ATTACHMENT1_EXT); } //keypoint detection with subpixel localization void PyramidNaive::DetectKeypointsEX() { int i, j; double t0, t, ts, t1, t2; GLTexImage * tex, *aux; FrameBufferObject fbo; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); //extra gradient data required for visualization int gradient_only_levels[2] = {param._level_min +1, param._level_max}; int n_gradient_only_level = GlobalUtil::_UseSiftGPUEX ? 2 : 1; for ( i = _octave_min; i < _octave_min + _octave_num; i++) { for( j =0; j < n_gradient_only_level ; j++) { tex = GetLevelTexture(i, gradient_only_levels[j]); tex->FitTexViewPort(); tex->AttachToFBO(0); tex->BindTex(); ShaderMan::UseShaderGradientPass(); tex->DrawQuadMT4(); } } if(GlobalUtil::_timingS && GlobalUtil::_verbose) { glFinish(); t1 = CLOCK(); } GLenum buffers[] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT }; glDrawBuffers(2, buffers); for ( i = _octave_min; i < _octave_min + _octave_num; i++) { if(GlobalUtil::_timingO) { t0 = CLOCK(); std::cout<<"#"<<(i + _down_sample_factor)<<"\t"; } tex = GetBaseLevel(i) + 2; aux = GetBaseLevel(i, DATA_KEYPOINT) +2; aux->FitTexViewPort(); for( j = param._level_min + 2; j < param._level_max ; j++, aux++, tex++) { if(GlobalUtil::_timingL)t = CLOCK(); tex->AttachToFBO(0); aux->AttachToFBO(1); glActiveTexture(GL_TEXTURE0); tex->BindTex(); glActiveTexture(GL_TEXTURE1); (tex+1)->BindTex(); glActiveTexture(GL_TEXTURE2); (tex-1)->BindTex(); ShaderMan::UseShaderKeypoint((tex+1)->GetTexID(), (tex-1)->GetTexID()); aux->DrawQuadMT8(); if(GlobalUtil::_timingL) { glFinish(); std::cout<<(CLOCK()-t)<<"\t"; } tex->DetachFBO(0); aux->DetachFBO(1); } if(GlobalUtil::_timingO) { std::cout<<"|\t"<<(CLOCK()-t0)<<"\n"; } } if(GlobalUtil::_timingS) { glFinish(); t2 = CLOCK(); if(GlobalUtil::_verbose) std::cout <<"\t"<<(t2-t1)<<"\n" <<"\t"<<(t1-ts)<<"\n"; } UnloadProgram(); GLTexImage::UnbindMultiTex(3); fbo.UnattachTex(GL_COLOR_ATTACHMENT1_EXT); } void PyramidNaive::GenerateFeatureList(int i, int j) { int hist_level_num = _hpLevelNum - _pyramid_octave_first; int hist_skip_gpu = GlobalUtil::_ListGenSkipGPU; int idx = i * param._dog_level_num + j; GLTexImage* htex, *ftex, *tex; tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + 2 + j; ftex = _featureTex + idx; htex = _histoPyramidTex + hist_level_num - 1 - i; /// glActiveTexture(GL_TEXTURE0); tex->BindTex(); htex->AttachToFBO(0); int tight = ((htex->GetImgWidth() * 2 == tex->GetImgWidth() -1 || tex->GetTexWidth() == tex->GetImgWidth()) && (htex->GetImgHeight() *2 == tex->GetImgHeight()-1 || tex->GetTexHeight() == tex->GetImgHeight())); ShaderMan::UseShaderGenListInit(tex->GetImgWidth(), tex->GetImgHeight(), tight); htex->FitTexViewPort(); //this uses the fact that no feature is on the edge. htex->DrawQuadReduction(); //reduction.. htex--; //this part might have problems on several GPUS //because the output of one pass is the input of the next pass //need to call glFinish to make it right //but too much glFinish makes it slow for(int k = 0; k AttachToFBO(0); htex->FitTexViewPort(); (htex+1)->BindTex(); ShaderMan::UseShaderGenListHisto(); htex->DrawQuadReduction(); } // if(hist_skip_gpu == 0) { //read back one pixel float fn[4], fcount; glReadPixels(0, 0, 1, 1, GL_RGBA , GL_FLOAT, fn); fcount = (fn[0] + fn[1] + fn[2] + fn[3]); if(fcount < 1) fcount = 0; _levelFeatureNum[ idx] = (int)(fcount); SetLevelFeatureNum(idx, (int)fcount); _featureNum += int(fcount); // if(fcount < 1.0) return; ///generate the feature texture htex= _histoPyramidTex; htex->BindTex(); //first pass ftex->AttachToFBO(0); if(GlobalUtil::_MaxOrientation>1) { //this is very important... ftex->FitRealTexViewPort(); glClear(GL_COLOR_BUFFER_BIT); glFinish(); }else { ftex->FitTexViewPort(); //glFinish(); } ShaderMan::UseShaderGenListStart((float)ftex->GetImgWidth(), htex->GetTexID()); ftex->DrawQuad(); //make sure it finishes before the next step ftex->DetachFBO(0); //pass on each pyramid level htex++; }else { int tw = htex[1].GetDrawWidth(), th = htex[1].GetDrawHeight(); int fc = 0; glReadPixels(0, 0, tw, th, GL_RGBA , GL_FLOAT, _histo_buffer); _keypoint_buffer.resize(0); for(int y = 0, pos = 0; y < th; y++) { for(int x= 0; x < tw; x++) { for(int c = 0; c < 4; c++, pos++) { int ss = (int) _histo_buffer[pos]; if(ss == 0) continue; float ft[4] = {2 * x + (c%2? 1.5f: 0.5f), 2 * y + (c>=2? 1.5f: 0.5f), 0, 1 }; for(int t = 0; t < ss; t++) { ft[2] = (float) t; _keypoint_buffer.insert(_keypoint_buffer.end(), ft, ft+4); } fc += (int)ss; } } } _levelFeatureNum[ idx] = fc; SetLevelFeatureNum(idx, fc); if(fc == 0) return; _featureNum += fc; ///////////////////// ftex->AttachToFBO(0); if(GlobalUtil::_MaxOrientation>1) { ftex->FitRealTexViewPort(); glClear(GL_COLOR_BUFFER_BIT); glFlush(); }else { ftex->FitTexViewPort(); glFlush(); } _keypoint_buffer.resize(ftex->GetDrawWidth() * ftex->GetDrawHeight()*4, 0); /////////// glActiveTexture(GL_TEXTURE0); ftex->BindTex(); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, ftex->GetDrawWidth(), ftex->GetDrawHeight(), GL_RGBA, GL_FLOAT, &_keypoint_buffer[0]); htex += 2; } for(int lev = 1 + hist_skip_gpu; lev < hist_level_num - i; lev++, htex++) { glActiveTexture(GL_TEXTURE0); ftex->BindTex(); ftex->AttachToFBO(0); glActiveTexture(GL_TEXTURE1); htex->BindTex(); ShaderMan::UseShaderGenListStep(ftex->GetTexID(), htex->GetTexID()); ftex->DrawQuad(); ftex->DetachFBO(0); } GLTexImage::UnbindMultiTex(2); } //generate feature list on GPU void PyramidNaive::GenerateFeatureList() { //generate the histogram0pyramid FrameBufferObject fbo; glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); double t1, t2; int ocount, reverse = (GlobalUtil::_TruncateMethod == 1); _featureNum = 0; FitHistogramPyramid(); //for(int i = 0, idx = 0; i < _octave_num; i++) FOR_EACH_OCTAVE(i, reverse) { //output if(GlobalUtil::_timingO) { t1= CLOCK(); ocount = 0; std::cout<<"#"< 0 && _featureNum > GlobalUtil::_FeatureCountThreshold) { _levelFeatureNum[i * param._dog_level_num + j] = 0; continue; }else { GenerateFeatureList(i, j); if(GlobalUtil::_timingO) { int idx = i * param._dog_level_num + j; std::cout<< _levelFeatureNum[idx] <<"\t"; ocount += _levelFeatureNum[idx]; } } } if(GlobalUtil::_timingO) { t2 = CLOCK(); std::cout << "| \t" << int(ocount) << " :\t(" << (t2 - t1) << ")\n"; } } if(GlobalUtil::_timingS)glFinish(); if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } } void PyramidGL::GenerateFeatureDisplayVBO() { //use a big VBO to save all the SIFT box vertices int w, h, esize; GLint bsize; int nvbo = _octave_num * param._dog_level_num; //initialize the vbos if(_featureDisplayVBO==NULL) { _featureDisplayVBO = new GLuint[nvbo]; glGenBuffers( nvbo, _featureDisplayVBO ); } if(_featurePointVBO == NULL) { _featurePointVBO = new GLuint[nvbo]; glGenBuffers(nvbo, _featurePointVBO); } FrameBufferObject fbo; glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); glActiveTexture(GL_TEXTURE0); // GLTexImage & tempTex = *_descriptorTex; // for(int i = 0, idx = 0; i < _octave_num; i++) { for(int j = 0; j < param._dog_level_num; j ++, idx++) { GLTexImage * ftex = _featureTex + idx; if(_levelFeatureNum[idx]<=0)continue; //copy the texture into vbo fbo.BindFBO(); tempTex.AttachToFBO(0); ftex->BindTex(); ftex->FitTexViewPort(); ShaderMan::UseShaderCopyKeypoint(); ftex->DrawQuad(); glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, _featurePointVBO[ idx]); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); esize = ftex->GetImgHeight() * ftex->GetImgWidth()*sizeof(float) *4; //increase size when necessary if(bsize < esize) { glBufferData(GL_PIXEL_PACK_BUFFER_ARB, esize*3/2 , NULL, GL_STATIC_DRAW_ARB); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); } //read back if we have enough buffer if(bsize >= esize) glReadPixels(0, 0, ftex->GetImgWidth(), ftex->GetImgHeight(), GL_RGBA, GL_FLOAT, 0); else glBufferData(GL_PIXEL_PACK_BUFFER_ARB, 0, NULL, GL_STATIC_DRAW_ARB); glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, 0); //box display vbo int count = _levelFeatureNum[idx]* 10; GetAlignedStorageSize(count, _alignment, w, h); w = (int)ceil(double(count)/ h); //input fbo.BindFBO(); ftex->BindTex(); //output tempTex.AttachToFBO(0); GlobalUtil::FitViewPort(w, h); //shader ShaderMan::UseShaderGenVBO( (float)ftex->GetImgWidth(), (float) w, param.GetLevelSigma(j + param._level_min + 1)); GLTexImage::DrawQuad(0, (float)w, 0, (float)h); // glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, _featureDisplayVBO[ idx]); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); esize = w*h * sizeof(float)*4; //increase size when necessary if(bsize < esize) { glBufferData(GL_PIXEL_PACK_BUFFER_ARB, esize*3/2, NULL, GL_STATIC_DRAW_ARB); glGetBufferParameteriv(GL_PIXEL_PACK_BUFFER_ARB, GL_BUFFER_SIZE, &bsize); } //read back if we have enough buffer if(bsize >= esize) glReadPixels(0, 0, w, h, GL_RGBA, GL_FLOAT, 0); else glBufferData(GL_PIXEL_PACK_BUFFER_ARB, 0, NULL, GL_STATIC_DRAW_ARB); glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, 0); } } glReadBuffer(GL_NONE); glFinish(); } void PyramidNaive::GetFeatureOrientations() { GLTexImage * gtex; GLTexImage * stex = NULL; GLTexImage * ftex = _featureTex; GLTexImage * otex = _orientationTex; int sid = 0; int * count = _levelFeatureNum; float sigma, sigma_step = powf(2.0f, 1.0f/param._dog_level_num); FrameBufferObject fbo; if(_orientationTex) { GLenum buffers[] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT }; glDrawBuffers(2, buffers); }else { glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); } for(int i = 0; i < _octave_num; i++) { gtex = GetLevelTexture(i+_octave_min, param._level_min + 1); if(GlobalUtil::_SubpixelLocalization || GlobalUtil::_KeepExtremumSign) stex = GetBaseLevel(i+_octave_min, DATA_KEYPOINT) + 2; for(int j = 0; j < param._dog_level_num; j++, ftex++, otex++, count++, gtex++, stex++) { if(*count<=0)continue; sigma = param.GetLevelSigma(j+param._level_min+1); // ftex->FitTexViewPort(); glActiveTexture(GL_TEXTURE0); ftex->BindTex(); glActiveTexture(GL_TEXTURE1); gtex->BindTex(); // ftex->AttachToFBO(0); if(_orientationTex) otex->AttachToFBO(1); if(!_existing_keypoints && (GlobalUtil::_SubpixelLocalization|| GlobalUtil::_KeepExtremumSign)) { glActiveTexture(GL_TEXTURE2); stex->BindTex(); sid = * stex; } ShaderMan::UseShaderOrientation(gtex->GetTexID(), gtex->GetImgWidth(), gtex->GetImgHeight(), sigma, sid, sigma_step, _existing_keypoints); ftex->DrawQuad(); // glFinish(); } } GLTexImage::UnbindMultiTex(3); if(GlobalUtil::_timingS)glFinish(); if(_orientationTex) fbo.UnattachTex(GL_COLOR_ATTACHMENT1_EXT); } //to compare with GPU feature list generation void PyramidNaive::GenerateFeatureListCPU() { FrameBufferObject fbo; _featureNum = 0; GLTexImage * tex = GetBaseLevel(_octave_min); float * mem = new float [tex->GetTexWidth()*tex->GetTexHeight()]; vector list; int idx = 0; for(int i = 0; i < _octave_num; i++) { for(int j = 0; j < param._dog_level_num; j++, idx++) { tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + j + 2; tex->BindTex(); glGetTexImage(GlobalUtil::_texTarget, 0, GL_RED, GL_FLOAT, mem); //tex->AttachToFBO(0); //tex->FitTexViewPort(); //glReadPixels(0, 0, tex->GetTexWidth(), tex->GetTexHeight(), GL_RED, GL_FLOAT, mem); // //make a list of list.resize(0); float * p = mem; int fcount = 0 ; for(int k = 0; k < tex->GetTexHeight(); k++) { for( int m = 0; m < tex->GetTexWidth(); m ++, p++) { if(*p==0)continue; if(m ==0 || k ==0 || k >= tex->GetImgHeight() -1 || m >= tex->GetImgWidth() -1 ) continue; list.push_back(m+0.5f); list.push_back(k+0.5f); list.push_back(0); list.push_back(1); fcount ++; } } if(fcount==0)continue; GLTexImage * ftex = _featureTex+idx; _levelFeatureNum[idx] = (fcount); SetLevelFeatureNum(idx, fcount); _featureNum += (fcount); int fw = ftex->GetImgWidth(); int fh = ftex->GetImgHeight(); list.resize(4*fh*fw); ftex->BindTex(); ftex->AttachToFBO(0); // glTexImage2D(GlobalUtil::_texTarget, 0, GlobalUtil::_iTexFormat, fw, fh, 0, GL_BGRA, GL_FLOAT, &list[0]); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, fw, fh, GL_RGBA, GL_FLOAT, &list[0]); // } } GLTexImage::UnbindTex(); delete[] mem; if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } } #define FEATURELIST_USE_PBO void PyramidGL::ReshapeFeatureListCPU() { //make a compact feature list, each with only one orientation //download orientations and the featue list //reshape it and upload it FrameBufferObject fbo; int i, szmax =0, sz; int n = param._dog_level_num*_octave_num; for( i = 0; i < n; i++) { sz = _featureTex[i].GetImgWidth() * _featureTex[i].GetImgHeight(); if(sz > szmax ) szmax = sz; } float * buffer = new float[szmax*24]; float * buffer1 = buffer; float * buffer2 = buffer + szmax*4; float * buffer3 = buffer + szmax*8; glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); #ifdef FEATURELIST_USE_PBO GLuint ListUploadPBO; glGenBuffers(1, &ListUploadPBO); glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, ListUploadPBO); glBufferData(GL_PIXEL_UNPACK_BUFFER_ARB, szmax * 8 * sizeof(float), NULL, GL_STREAM_DRAW); #endif _featureNum = 0; #ifdef NO_DUPLICATE_DOWNLOAD const double twopi = 2.0*3.14159265358979323846; _keypoint_buffer.resize(0); float os = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); if(_down_sample_factor>0) os *= float(1<<_down_sample_factor); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; #endif for(i = 0; i < n; i++) { if(_levelFeatureNum[i]==0)continue; _featureTex[i].AttachToFBO(0); _featureTex[i].FitTexViewPort(); glReadPixels(0, 0, _featureTex[i].GetImgWidth(), _featureTex[i].GetImgHeight(),GL_RGBA, GL_FLOAT, buffer1); int fcount =0, ocount; float * src = buffer1; float * orientation = buffer2; float * des = buffer3; if(GlobalUtil::_OrientationPack2 == 0) { //read back orientations from another texture _orientationTex[i].AttachToFBO(0); glReadPixels(0, 0, _orientationTex[i].GetImgWidth(), _orientationTex[i].GetImgHeight(),GL_RGBA, GL_FLOAT, buffer2); //make the feature list for(int j = 0; j < _levelFeatureNum[i]; j++, src+=4, orientation+=4) { if(_existing_keypoints) { des[0] = src[0]; des[1] = src[1]; des[2] = orientation[0]; des[3] = src[3]; fcount++; des += 4; }else { ocount = (int)src[2]; for(int k = 0 ; k < ocount; k++, des+=4) { des[0] = src[0]; des[1] = src[1]; des[2] = orientation[k]; des[3] = src[3]; fcount++; } } } }else { _featureTex[i].DetachFBO(0); const static double factor = 2.0*3.14159265358979323846/65535.0; for(int j = 0; j < _levelFeatureNum[i]; j++, src+=4) { unsigned short * orientations = (unsigned short*) (&src[2]); if(_existing_keypoints) { des[0] = src[0]; des[1] = src[1]; des[2] = float( factor* orientations[0]); des[3] = src[3]; fcount++; des += 4; }else { if(orientations[0] != 65535) { des[0] = src[0]; des[1] = src[1]; des[2] = float( factor* orientations[0]); des[3] = src[3]; fcount++; des += 4; if(orientations[1] != 65535) { des[0] = src[0]; des[1] = src[1]; des[2] = float(factor* orientations[1]); des[3] = src[3]; fcount++; des += 4; } } } } } if (fcount == 0){ _levelFeatureNum[i] = 0; continue; } //texture size -------------- SetLevelFeatureNum(i, fcount); int nfw = _featureTex[i].GetImgWidth(); int nfh = _featureTex[i].GetImgHeight(); int sz = nfh * nfw; if(sz > fcount) memset(des, 0, sizeof(float) * (sz - fcount) * 4); #ifndef FEATURELIST_USE_PBO _featureTex[i].BindTex(); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, nfw, nfh, GL_RGBA, GL_FLOAT, buffer3); _featureTex[i].UnbindTex(); #else float* mem = (float*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, GL_WRITE_ONLY); memcpy(mem, buffer3, sz * 4 * sizeof(float) ); glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER_ARB); _featureTex[i].BindTex(); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, nfw, nfh, GL_RGBA, GL_FLOAT, 0); _featureTex[i].UnbindTex(); #endif #ifdef NO_DUPLICATE_DOWNLOAD if(fcount > 0) { float oss = os * (1 << (i / param._dog_level_num)); _keypoint_buffer.resize((_featureNum + fcount) * 4); float* ds = &_keypoint_buffer[_featureNum * 4]; float* fs = buffer3; for(int k = 0; k < fcount; k++, ds+=4, fs+=4) { ds[0] = oss*(fs[0]-0.5f) + offset; //x ds[1] = oss*(fs[1]-0.5f) + offset; //y ds[3] = (float)fmod(twopi-fs[2], twopi); //orientation, mirrored ds[2] = oss*fs[3]; //scale } } #endif _levelFeatureNum[i] = fcount; _featureNum += fcount; } delete[] buffer; if(GlobalUtil::_verbose) { std::cout<<"#Features MO:\t"<<_featureNum< ftex->GetTexWidth()*ftex->GetTexHeight()) { ftex->InitTexture(fw, fh, 0); if(_orientationTex) _orientationTex[idx].InitTexture(fw, fh, 0); } if(GlobalUtil::_NarrowFeatureTex) fh = fcount ==0? 0:(int)ceil(double(fcount)/fw); else fw = fcount ==0? 0:(int)ceil(double(fcount)/fh); ftex->SetImageSize(fw, fh); if(_orientationTex) _orientationTex[idx].SetImageSize(fw, fh); } void PyramidGL::CleanUpAfterSIFT() { GLTexImage::UnbindMultiTex(3); ShaderMan::UnloadProgram(); FrameBufferObject::DeleteGlobalFBO(); GlobalUtil::CleanupOpenGL(); } void PyramidNaive::GetSimplifiedOrientation() { // int idx = 0; // int n = _octave_num * param._dog_level_num; float sigma, sigma_step = powf(2.0f, 1.0f/param._dog_level_num); GLTexImage * ftex = _featureTex; FrameBufferObject fbo; glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); for(int i = 0; i < _octave_num; i++) { GLTexImage *gtex = GetLevelTexture(i+_octave_min, 2+param._level_min); for(int j = 0; j < param._dog_level_num; j++, ftex++, gtex++, idx ++) { if(_levelFeatureNum[idx]<=0)continue; sigma = param.GetLevelSigma(j+param._level_min+1); // ftex->AttachToFBO(0); ftex->FitTexViewPort(); glActiveTexture(GL_TEXTURE0); ftex->BindTex(); glActiveTexture(GL_TEXTURE1); gtex->BindTex(); ShaderMan::UseShaderSimpleOrientation(gtex->GetTexID(), sigma, sigma_step); ftex->DrawQuad(); } } GLTexImage::UnbindMultiTex(2); } #ifdef USE_SSE_FOR_SIFTGPU static inline float dotproduct_128d(float * p) { float z = 0.0f; __m128 sse =_mm_load_ss(&z); float* pf = (float*) (&sse); for( int i = 0; i < 32; i++, p+=4) { __m128 ps = _mm_loadu_ps(p); sse = _mm_add_ps(sse, _mm_mul_ps(ps, ps)); } return pf[0] + pf[1] + pf[2] + pf[3]; } static inline void multiply_and_truncate_128d(float* p, float m) { float z = 0.2f; __m128 t = _mm_load_ps1(&z); __m128 r = _mm_load_ps1(&m); for(int i = 0; i < 32; i++, p+=4) { __m128 ps = _mm_loadu_ps(p); _mm_storeu_ps(p, _mm_min_ps(_mm_mul_ps(ps, r), t)); } } static inline void multiply_128d(float* p, float m) { __m128 r = _mm_load_ps1(&m); for(int i = 0; i < 32; i++, p+=4) { __m128 ps = _mm_loadu_ps(p); _mm_storeu_ps(p, _mm_mul_ps(ps, r)); } } #endif inline void PyramidGL::NormalizeDescriptor(int num, float*pd) { #ifdef USE_SSE_FOR_SIFTGPU for(int k = 0; k < num; k++, pd +=128) { float sq; //normalize and truncate to .2 sq = dotproduct_128d(pd); sq = 1.0f / sqrtf(sq); multiply_and_truncate_128d(pd, sq); //renormalize sq = dotproduct_128d(pd); sq = 1.0f / sqrtf(sq); multiply_128d(pd, sq); } #else //descriptor normalization runs on cpu for OpenGL implemenations for(int k = 0; k < num; k++, pd +=128) { int v; float* ppd, sq = 0; //int v; //normalize ppd = pd; for(v = 0 ; v < 128; v++, ppd++) sq += (*ppd)*(*ppd); sq = 1.0f / sqrtf(sq); //truncate to .2 ppd = pd; for(v = 0; v < 128; v ++, ppd++) *ppd = min(*ppd*sq, 0.2f); //renormalize ppd = pd; sq = 0; for(v = 0; v < 128; v++, ppd++) sq += (*ppd)*(*ppd); sq = 1.0f / sqrtf(sq); ppd = pd; for(v = 0; v < 128; v ++, ppd++) *ppd = *ppd*sq; } #endif } inline void PyramidGL::InterlaceDescriptorF2(int w, int h, float* buf, float* pd, int step) { /* if(GlobalUtil::_DescriptorPPR == 8) { const int dstep = w * 128; float* pp1 = buf; float* pp2 = buf + step; for(int u = 0; u < h ; u++, pd+=dstep) { int v; float* ppd = pd; for(v= 0; v < w; v++) { for(int t = 0; t < 8; t++) { *ppd++ = *pp1++;*ppd++ = *pp1++;*ppd++ = *pp1++;*ppd++ = *pp1++; *ppd++ = *pp2++;*ppd++ = *pp2++;*ppd++ = *pp2++;*ppd++ = *pp2++; } ppd += 64; } ppd = pd + 64; for(v= 0; v < w; v++) { for(int t = 0; t < 8; t++) { *ppd++ = *pp1++;*ppd++ = *pp1++;*ppd++ = *pp1++;*ppd++ = *pp1++; *ppd++ = *pp2++;*ppd++ = *pp2++;*ppd++ = *pp2++;*ppd++ = *pp2++; } ppd += 64; } } }else */ if(GlobalUtil::_DescriptorPPR == 8) { //interlace for(int k = 0; k < 2; k++) { float* pp = buf + k * step; float* ppd = pd + k * 4; for(int u = 0; u < h ; u++) { int v; for(v= 0; v < w; v++) { for(int t = 0; t < 8; t++) { ppd[0] = pp[0]; ppd[1] = pp[1]; ppd[2] = pp[2]; ppd[3] = pp[3]; ppd += 8; pp+= 4; } ppd += 64; } ppd += ( 64 - 128 * w ); for(v= 0; v < w; v++) { for(int t = 0; t < 8; t++) { ppd[0] = pp[0]; ppd[1] = pp[1]; ppd[2] = pp[2]; ppd[3] = pp[3]; ppd += 8; pp+= 4; } ppd += 64; } ppd -=64; } } }else if(GlobalUtil::_DescriptorPPR == 4) { } } void PyramidGL::GetFeatureDescriptors() { //descriptors... float sigma; int idx, i, j, k, w, h; int ndf = 32 / GlobalUtil::_DescriptorPPT; //number of textures int block_width = GlobalUtil::_DescriptorPPR; int block_height = GlobalUtil::_DescriptorPPT/GlobalUtil::_DescriptorPPR; float* pd = &_descriptor_buffer[0], * pbuf = NULL; vectorread_buffer, descriptor_buffer2; //use another buffer, if we need to re-order the descriptors if(_keypoint_index.size() > 0) { descriptor_buffer2.resize(_descriptor_buffer.size()); pd = &descriptor_buffer2[0]; } FrameBufferObject fbo; GLTexImage * gtex, *otex, * ftex; GLenum buffers[8] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT , GL_COLOR_ATTACHMENT2_EXT, GL_COLOR_ATTACHMENT3_EXT , GL_COLOR_ATTACHMENT4_EXT, GL_COLOR_ATTACHMENT5_EXT , GL_COLOR_ATTACHMENT6_EXT, GL_COLOR_ATTACHMENT7_EXT , }; glDrawBuffers(ndf, buffers); glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); for( i = 0, idx = 0, ftex = _featureTex; i < _octave_num; i++) { gtex = GetBaseLevel(i + _octave_min, DATA_GRAD) + 1; otex = GetBaseLevel(i + _octave_min, DATA_ROT) + 1; for( j = 0; j < param._dog_level_num; j++, ftex++, idx++, gtex++, otex++) { if(_levelFeatureNum[idx]==0)continue; sigma = IsUsingRectDescription()? 0 : param.GetLevelSigma(j+param._level_min+1); int count = _levelFeatureNum[idx] * block_width; GetAlignedStorageSize(count, block_width, w, h); h = ((int)ceil(double(count) / w)) * block_height; //not enought space for holding the descriptor data if(w > _descriptorTex[0].GetTexWidth() || h > _descriptorTex[0].GetTexHeight()) { for(k = 0; k < ndf; k++)_descriptorTex[k].InitTexture(w, h); } for(k = 0; k < ndf; k++) _descriptorTex[k].AttachToFBO(k); GlobalUtil::FitViewPort(w, h); glActiveTexture(GL_TEXTURE0); ftex->BindTex(); glActiveTexture(GL_TEXTURE1); gtex->BindTex(); if(otex!=gtex) { glActiveTexture(GL_TEXTURE2); otex->BindTex(); } ShaderMan::UseShaderDescriptor(gtex->GetTexID(), otex->GetTexID(), w, ftex->GetImgWidth(), gtex->GetImgWidth(), gtex->GetImgHeight(), sigma); GLTexImage::DrawQuad(0, (float)w, 0, (float)h); //read back float format descriptors and do normalization on CPU int step = w*h*4; if((unsigned int)step*ndf > read_buffer.size()) { read_buffer.resize(ndf*step); } pbuf = &read_buffer[0]; //read back for(k = 0; k < ndf; k++, pbuf+=step) { glReadBuffer(GL_COLOR_ATTACHMENT0_EXT + k); if(GlobalUtil::_IsNvidia || w * h <= 16384) //were { glReadPixels(0, 0, w, h, GL_RGBA, GL_FLOAT, pbuf); }else { int hstep = 16384 / w; for(int kk = 0; kk < h; kk += hstep) glReadPixels(0, kk, w, min(hstep, h - kk), GL_RGBA, GL_FLOAT, pbuf + w * kk * 4); } } //the following two steps run on cpu, so better cpu better speed //and release version can be a lot faster than debug version //interlace data on the two texture to get the descriptor InterlaceDescriptorF2(w / block_width, h / block_height, &read_buffer[0], pd, step); //need to do normalization //the new version uses SSE to speed up this part if(GlobalUtil::_NormalizedSIFT) NormalizeDescriptor(_levelFeatureNum[idx], pd); pd += 128*_levelFeatureNum[idx]; glReadBuffer(GL_NONE); } } //finally, put the descriptor back to their original order for existing keypoint list. if(_keypoint_index.size() > 0) { for(i = 0; i < _featureNum; ++i) { int index = _keypoint_index[i]; memcpy(&_descriptor_buffer[index*128], &descriptor_buffer2[i*128], 128 * sizeof(float)); } } //////////////////////// GLTexImage::UnbindMultiTex(3); glDrawBuffer(GL_NONE); ShaderMan::UnloadProgram(); if(GlobalUtil::_timingS)glFinish(); for(i = 0; i < ndf; i++) fbo.UnattachTex(GL_COLOR_ATTACHMENT0_EXT +i); } void PyramidGL::DownloadKeypoints() { const double twopi = 2.0*3.14159265358979323846; int idx = 0; float * buffer = &_keypoint_buffer[0]; vector keypoint_buffer2; //use a different keypoint buffer when processing with an exisint features list //without orientation information. if(_keypoint_index.size() > 0) { keypoint_buffer2.resize(_keypoint_buffer.size()); buffer = &keypoint_buffer2[0]; } float * p = buffer, *ps, sigma; GLTexImage * ftex = _featureTex; FrameBufferObject fbo; ftex->FitRealTexViewPort(); ///////////////////// float os = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); if(_down_sample_factor>0) os *= float(1<<_down_sample_factor); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; ///////////////////// for(int i = 0; i < _octave_num; i++, os *= 2.0f) { for(int j = 0; j < param._dog_level_num; j++, idx++, ftex++) { if(_levelFeatureNum[idx]>0) { ftex->AttachToFBO(0); glReadPixels(0, 0, ftex->GetImgWidth(), ftex->GetImgHeight(),GL_RGBA, GL_FLOAT, p); ps = p; for(int k = 0; k < _levelFeatureNum[idx]; k++, ps+=4) { ps[0] = os*(ps[0]-0.5f) + offset; //x ps[1] = os*(ps[1]-0.5f) + offset; //y sigma = os*ps[3]; ps[3] = (float)fmod(twopi-ps[2], twopi); //orientation, mirrored ps[2] = sigma; //scale } p+= 4* _levelFeatureNum[idx]; } } } //put the feature into their original order if(_keypoint_index.size() > 0) { for(int i = 0; i < _featureNum; ++i) { int index = _keypoint_index[i]; memcpy(&_keypoint_buffer[index*4], &keypoint_buffer2[i*4], 4 * sizeof(float)); } } } void PyramidGL::GenerateFeatureListTex() { //generate feature list texture from existing keypoints //do feature sorting in the same time? FrameBufferObject fbo; vector list; int idx = 0; const double twopi = 2.0*3.14159265358979323846; float sigma_half_step = powf(2.0f, 0.5f / param._dog_level_num); float octave_sigma = _octave_min>=0? float(1<<_octave_min): 1.0f/(1<<(-_octave_min)); float offset = GlobalUtil::_LoweOrigin? 0 : 0.5f; if(_down_sample_factor>0) octave_sigma *= float(1<<_down_sample_factor); std::fill(_levelFeatureNum, _levelFeatureNum + _octave_num * param._dog_level_num, 0); _keypoint_index.resize(0); // should already be 0 for(int i = 0; i < _octave_num; i++, octave_sigma*= 2.0f) { for(int j = 0; j < param._dog_level_num; j++, idx++) { list.resize(0); float level_sigma = param.GetLevelSigma(j + param._level_min + 1) * octave_sigma; float sigma_min = level_sigma / sigma_half_step; float sigma_max = level_sigma * sigma_half_step; int fcount = 0 ; for(int k = 0; k < _featureNum; k++) { float * key = &_keypoint_buffer[k*4]; float sigmak = key[2]; ////////////////////////////////////// if(IsUsingRectDescription()) sigmak = min(key[2], key[3]) / 12.0f; if( (sigmak >= sigma_min && sigmak < sigma_max) ||(sigmak < sigma_min && i ==0 && j == 0) ||(sigmak > sigma_max && j == param._dog_level_num - 1&& (i == _octave_num -1 || GlobalUtil::_KeyPointListForceLevel0))) { //add this keypoint to the list list.push_back((key[0] - offset) / octave_sigma + 0.5f); list.push_back((key[1] - offset) / octave_sigma + 0.5f); if(IsUsingRectDescription()) { list.push_back(key[2] / octave_sigma); list.push_back(key[3] / octave_sigma); }else { list.push_back((float)fmod(twopi-key[3], twopi)); list.push_back(key[2] / octave_sigma); } fcount ++; //save the index of keypoints _keypoint_index.push_back(k); } } _levelFeatureNum[idx] = fcount; if(fcount==0)continue; GLTexImage * ftex = _featureTex+idx; SetLevelFeatureNum(idx, fcount); int fw = ftex->GetImgWidth(); int fh = ftex->GetImgHeight(); list.resize(4*fh*fw); ftex->BindTex(); ftex->AttachToFBO(0); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, fw, fh, GL_RGBA, GL_FLOAT, &list[0]); if( fcount == _featureNum) _keypoint_index.resize(0); } if( GlobalUtil::_KeyPointListForceLevel0 ) break; } GLTexImage::UnbindTex(); if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } } PyramidPacked::PyramidPacked(SiftParam& sp): PyramidGL(sp) { _allPyramid = NULL; } PyramidPacked::~PyramidPacked() { DestroyPyramidData(); } //build the gaussian pyrmaid void PyramidPacked::BuildPyramid(GLTexInput * input) { // USE_TIMING(); GLTexImage * tex, *tmp; FilterProgram ** filter; FrameBufferObject fbo; glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); input->FitTexViewPort(); for (int i = _octave_min; i < _octave_min + _octave_num; i++) { tex = GetBaseLevel(i); tmp = GetBaseLevel(i, DATA_DOG) + 2; //use this as a temperory texture filter = ShaderMan::s_bag->f_gaussian_step; OCTAVE_START(); if( i == _octave_min ) { if(i < 0) TextureUpSample(tex, input, 1<<(-i-1)); else TextureDownSample(tex, input, 1<<(i+1)); ShaderMan::FilterInitialImage(tex, tmp); }else { TextureDownSample(tex, GetLevelTexture(i-1, param._level_ds)); ShaderMan::FilterSampledImage(tex, tmp); } LEVEL_FINISH(); for(int j = param._level_min + 1; j <= param._level_max ; j++, tex++, filter++) { // filtering ShaderMan::FilterImage(*filter, tex+1, tex, tmp); LEVEL_FINISH(); } OCTAVE_FINISH(); } if(GlobalUtil::_timingS) glFinish(); UnloadProgram(); } void PyramidPacked::ComputeGradient() { //first pass, compute dog, gradient, orientation GLenum buffers[4] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT , GL_COLOR_ATTACHMENT2_EXT, GL_COLOR_ATTACHMENT3_EXT }; int i, j; double ts, t1; FrameBufferObject fbo; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); for(i = _octave_min; i < _octave_min + _octave_num; i++) { GLTexImage * gus = GetBaseLevel(i) + 1; GLTexImage * dog = GetBaseLevel(i, DATA_DOG) + 1; GLTexImage * grd = GetBaseLevel(i, DATA_GRAD) + 1; GLTexImage * rot = GetBaseLevel(i, DATA_ROT) + 1; glDrawBuffers(3, buffers); gus->FitTexViewPort(); //compute the gradient for(j = 0; j < param._dog_level_num ; j++, gus++, dog++, grd++, rot++) { //gradient, dog, orientation glActiveTexture(GL_TEXTURE0); gus->BindTex(); glActiveTexture(GL_TEXTURE1); (gus-1)->BindTex(); //output dog->AttachToFBO(0); grd->AttachToFBO(1); rot->AttachToFBO(2); ShaderMan::UseShaderGradientPass((gus-1)->GetTexID()); //compute dog->DrawQuadMT4(); } } if(GlobalUtil::_timingS) { glFinish(); if(GlobalUtil::_verbose) { t1 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n"; } } GLTexImage::DetachFBO(1); GLTexImage::DetachFBO(2); UnloadProgram(); GLTexImage::UnbindMultiTex(3); fbo.UnattachTex(GL_COLOR_ATTACHMENT1_EXT); } void PyramidPacked::DetectKeypointsEX() { //first pass, compute dog, gradient, orientation GLenum buffers[4] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT , GL_COLOR_ATTACHMENT2_EXT, GL_COLOR_ATTACHMENT3_EXT }; int i, j; double t0, t, ts, t1, t2; FrameBufferObject fbo; if(GlobalUtil::_timingS && GlobalUtil::_verbose)ts = CLOCK(); for(i = _octave_min; i < _octave_min + _octave_num; i++) { GLTexImage * gus = GetBaseLevel(i) + 1; GLTexImage * dog = GetBaseLevel(i, DATA_DOG) + 1; GLTexImage * grd = GetBaseLevel(i, DATA_GRAD) + 1; GLTexImage * rot = GetBaseLevel(i, DATA_ROT) + 1; glDrawBuffers(3, buffers); gus->FitTexViewPort(); //compute the gradient for(j = param._level_min +1; j <= param._level_max ; j++, gus++, dog++, grd++, rot++) { //gradient, dog, orientation glActiveTexture(GL_TEXTURE0); gus->BindTex(); glActiveTexture(GL_TEXTURE1); (gus-1)->BindTex(); //output dog->AttachToFBO(0); grd->AttachToFBO(1); rot->AttachToFBO(2); ShaderMan::UseShaderGradientPass((gus-1)->GetTexID()); //compute dog->DrawQuadMT4(); } } if(GlobalUtil::_timingS && GlobalUtil::_verbose) { glFinish(); t1 = CLOCK(); } GLTexImage::DetachFBO(1); GLTexImage::DetachFBO(2); //glDrawBuffers(1, buffers); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); GlobalUtil::CheckErrorsGL(); for ( i = _octave_min; i < _octave_min + _octave_num; i++) { if(GlobalUtil::_timingO) { t0 = CLOCK(); std::cout<<"#"<<(i + _down_sample_factor)<<"\t"; } GLTexImage * dog = GetBaseLevel(i, DATA_DOG) + 2; GLTexImage * key = GetBaseLevel(i, DATA_KEYPOINT) +2; GLTexImage * gus = GetBaseLevel(i) + 2; key->FitTexViewPort(); for( j = param._level_min +2; j < param._level_max ; j++, dog++, key++, gus++) { if(GlobalUtil::_timingL)t = CLOCK(); key->AttachToFBO(0); glActiveTexture(GL_TEXTURE0); dog->BindTex(); glActiveTexture(GL_TEXTURE1); (dog+1)->BindTex(); glActiveTexture(GL_TEXTURE2); (dog-1)->BindTex(); if(GlobalUtil::_DarknessAdaption) { glActiveTexture(GL_TEXTURE3); gus->BindTex(); } ShaderMan::UseShaderKeypoint((dog+1)->GetTexID(), (dog-1)->GetTexID()); key->DrawQuadMT8(); if(GlobalUtil::_timingL) { glFinish(); std::cout<<(CLOCK()-t)<<"\t"; } } if(GlobalUtil::_timingO) { glFinish(); std::cout<<"|\t"<<(CLOCK()-t0)<<"\n"; } } if(GlobalUtil::_timingS) { glFinish(); if(GlobalUtil::_verbose) { t2 = CLOCK(); std::cout <<"\t"<<(t1-ts)<<"\n" <<"\t"<<(t2-t1)<<"\n"; } } UnloadProgram(); GLTexImage::UnbindMultiTex(3); fbo.UnattachTex(GL_COLOR_ATTACHMENT1_EXT); } void PyramidPacked::GenerateFeatureList(int i, int j) { float fcount = 0.0f; int hist_skip_gpu = GlobalUtil::_ListGenSkipGPU; int idx = i * param._dog_level_num + j; int hist_level_num = _hpLevelNum - _pyramid_octave_first; GLTexImage * htex, * ftex, * tex; htex = _histoPyramidTex + hist_level_num - 1 - i; ftex = _featureTex + idx; tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + 2 + j; //fill zero to an extra row/col if the height/width is odd glActiveTexture(GL_TEXTURE0); tex->BindTex(); htex->AttachToFBO(0); int tight = (htex->GetImgWidth() * 4 == tex->GetImgWidth() -1 && htex->GetImgHeight() *4 == tex->GetImgHeight()-1 ); ShaderMan::UseShaderGenListInit(tex->GetImgWidth(), tex->GetImgHeight(), tight); htex->FitTexViewPort(); //this uses the fact that no feature is on the edge. htex->DrawQuadReduction(); //reduction.. htex--; //this part might have problems on several GPUS //because the output of one pass is the input of the next pass //may require glFinish to make it right, but too much glFinish makes it slow for(int k = 0; k AttachToFBO(0); htex->FitTexViewPort(); (htex+1)->BindTex(); ShaderMan::UseShaderGenListHisto(); htex->DrawQuadReduction(); } if(hist_skip_gpu == 0) { //read back one pixel float fn[4]; glReadPixels(0, 0, 1, 1, GL_RGBA , GL_FLOAT, fn); fcount = (fn[0] + fn[1] + fn[2] + fn[3]); if(fcount < 1) fcount = 0; _levelFeatureNum[ idx] = (int)(fcount); SetLevelFeatureNum(idx, (int)fcount); //save number of features _featureNum += int(fcount); // if(fcount < 1.0) return;; ///generate the feature texture htex= _histoPyramidTex; htex->BindTex(); //first pass ftex->AttachToFBO(0); if(GlobalUtil::_MaxOrientation>1) { //this is very important... ftex->FitRealTexViewPort(); glClear(GL_COLOR_BUFFER_BIT); glFinish(); }else { ftex->FitTexViewPort(); //glFinish(); } ShaderMan::UseShaderGenListStart((float)ftex->GetImgWidth(), htex->GetTexID()); ftex->DrawQuad(); //make sure it finishes before the next step ftex->DetachFBO(0); //pass on each pyramid level htex++; }else { int tw = htex[1].GetDrawWidth(), th = htex[1].GetDrawHeight(); int fc = 0; glReadPixels(0, 0, tw, th, GL_RGBA , GL_FLOAT, _histo_buffer); _keypoint_buffer.resize(0); for(int y = 0, pos = 0; y < th; y++) { for(int x= 0; x < tw; x++) { for(int c = 0; c < 4; c++, pos++) { int ss = (int) _histo_buffer[pos]; if(ss == 0) continue; float ft[4] = {2 * x + (c%2? 1.5f: 0.5f), 2 * y + (c>=2? 1.5f: 0.5f), 0, 1 }; for(int t = 0; t < ss; t++) { ft[2] = (float) t; _keypoint_buffer.insert(_keypoint_buffer.end(), ft, ft+4); } fc += (int)ss; } } } _levelFeatureNum[ idx] = fc; SetLevelFeatureNum(idx, fc); if(fc == 0) return; fcount = (float) fc; _featureNum += fc; ///////////////////// ftex->AttachToFBO(0); if(GlobalUtil::_MaxOrientation>1) { ftex->FitRealTexViewPort(); glClear(GL_COLOR_BUFFER_BIT); glFlush(); }else { ftex->FitTexViewPort(); glFlush(); } _keypoint_buffer.resize(ftex->GetDrawWidth() * ftex->GetDrawHeight()*4, 0); /////////// glActiveTexture(GL_TEXTURE0); ftex->BindTex(); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, ftex->GetDrawWidth(), ftex->GetDrawHeight(), GL_RGBA, GL_FLOAT, &_keypoint_buffer[0]); htex += 2; } for(int lev = 1 + hist_skip_gpu; lev < hist_level_num - i; lev++, htex++) { glActiveTexture(GL_TEXTURE0); ftex->BindTex(); ftex->AttachToFBO(0); glActiveTexture(GL_TEXTURE1); htex->BindTex(); ShaderMan::UseShaderGenListStep(ftex->GetTexID(), htex->GetTexID()); ftex->DrawQuad(); ftex->DetachFBO(0); } ftex->AttachToFBO(0); glActiveTexture(GL_TEXTURE1); tex->BindTex(); ShaderMan::UseShaderGenListEnd(tex->GetTexID()); ftex->DrawQuad(); GLTexImage::UnbindMultiTex(2); } void PyramidPacked::GenerateFeatureList() { //generate the histogram pyramid FrameBufferObject fbo; glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); double t1, t2; int ocount= 0, reverse = (GlobalUtil::_TruncateMethod == 1); _featureNum = 0; FitHistogramPyramid(); //for(int i = 0, idx = 0; i < _octave_num; i++) FOR_EACH_OCTAVE(i, reverse) { if(GlobalUtil::_timingO) { t1= CLOCK(); ocount = 0; std::cout<<"#"< 0 && _featureNum > GlobalUtil::_FeatureCountThreshold) { _levelFeatureNum[i * param._dog_level_num + j] = 0; continue; } GenerateFeatureList(i, j); if(GlobalUtil::_timingO) { int idx = i * param._dog_level_num + j; ocount += _levelFeatureNum[idx]; std::cout<< _levelFeatureNum[idx] <<"\t"; } } if(GlobalUtil::_timingO) { t2 = CLOCK(); std::cout << "| \t" << int(ocount) << " :\t(" << (t2 - t1) << ")\n"; } } if(GlobalUtil::_timingS)glFinish(); if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } } void PyramidPacked::GenerateFeatureListCPU() { FrameBufferObject fbo; _featureNum = 0; GLTexImage * tex = GetBaseLevel(_octave_min); float * mem = new float [tex->GetTexWidth()*tex->GetTexHeight()*4]; vector list; int idx = 0; for(int i = 0; i < _octave_num; i++) { for(int j = 0; j < param._dog_level_num; j++, idx++) { tex = GetBaseLevel(_octave_min + i, DATA_KEYPOINT) + j + 2; tex->BindTex(); glGetTexImage(GlobalUtil::_texTarget, 0, GL_RGBA, GL_FLOAT, mem); //tex->AttachToFBO(0); //tex->FitTexViewPort(); //glReadPixels(0, 0, tex->GetTexWidth(), tex->GetTexHeight(), GL_RED, GL_FLOAT, mem); // //make a list of list.resize(0); float *pl = mem; int fcount = 0 ; for(int k = 0; k < tex->GetDrawHeight(); k++) { float * p = pl; pl += tex->GetTexWidth() * 4; for( int m = 0; m < tex->GetDrawWidth(); m ++, p+=4) { // if(m ==0 || k ==0 || k == tex->GetDrawHeight() -1 || m == tex->GetDrawWidth() -1) continue; // if(*p == 0) continue; int t = ((int) fabs(p[0])) - 1; if(t < 0) continue; int xx = m + m + ( (t %2)? 1 : 0); int yy = k + k + ( (t <2)? 0 : 1); if(xx ==0 || yy == 0) continue; if(xx >= tex->GetImgWidth() - 1 || yy >= tex->GetImgHeight() - 1)continue; list.push_back(xx + 0.5f + p[1]); list.push_back(yy + 0.5f + p[2]); list.push_back(GlobalUtil::_KeepExtremumSign && p[0] < 0 ? -1.0f : 1.0f); list.push_back(p[3]); fcount ++; } } if(fcount==0)continue; if(GlobalUtil::_timingL) std::cout<GetImgWidth(); int fh = ftex->GetImgHeight(); list.resize(4*fh*fw); ftex->BindTex(); ftex->AttachToFBO(0); // glTexImage2D(GlobalUtil::_texTarget, 0, GlobalUtil::_iTexFormat, fw, fh, 0, GL_BGRA, GL_FLOAT, &list[0]); glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, fw, fh, GL_RGBA, GL_FLOAT, &list[0]); // } } GLTexImage::UnbindTex(); delete[] mem; if(GlobalUtil::_verbose) { std::cout<<"#Features:\t"<<_featureNum<<"\n"; } } void PyramidPacked::GetFeatureOrientations() { GLTexImage * gtex, * otex; GLTexImage * ftex = _featureTex; GLTexImage * fotex = _orientationTex; int * count = _levelFeatureNum; float sigma, sigma_step = powf(2.0f, 1.0f/param._dog_level_num); FrameBufferObject fbo; if(_orientationTex) { GLenum buffers[] = { GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT }; glDrawBuffers(2, buffers); }else { glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); } for(int i = 0; i < _octave_num; i++) { gtex = GetBaseLevel(i+_octave_min, DATA_GRAD) + 1; otex = GetBaseLevel(i+_octave_min, DATA_ROT) + 1; for(int j = 0; j < param._dog_level_num; j++, ftex++, otex++, count++, gtex++, fotex++) { if(*count<=0)continue; sigma = param.GetLevelSigma(j+param._level_min+1); ftex->FitTexViewPort(); glActiveTexture(GL_TEXTURE0); ftex->BindTex(); glActiveTexture(GL_TEXTURE1); gtex->BindTex(); glActiveTexture(GL_TEXTURE2); otex->BindTex(); // ftex->AttachToFBO(0); if(_orientationTex) fotex->AttachToFBO(1); GlobalUtil::CheckFramebufferStatus(); ShaderMan::UseShaderOrientation(gtex->GetTexID(), gtex->GetImgWidth(), gtex->GetImgHeight(), sigma, otex->GetTexID(), sigma_step, _existing_keypoints); ftex->DrawQuad(); } } GLTexImage::UnbindMultiTex(3); if(GlobalUtil::_timingS)glFinish(); if(_orientationTex) fbo.UnattachTex(GL_COLOR_ATTACHMENT1_EXT); } void PyramidPacked::GetSimplifiedOrientation() { // int idx = 0; // int n = _octave_num * param._dog_level_num; float sigma, sigma_step = powf(2.0f, 1.0f/param._dog_level_num); GLTexImage * ftex = _featureTex; FrameBufferObject fbo; glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); for(int i = 0; i < _octave_num; i++) { GLTexImage *otex = GetBaseLevel(i + _octave_min, DATA_ROT) + 2; for(int j = 0; j < param._dog_level_num; j++, ftex++, otex++, idx ++) { if(_levelFeatureNum[idx]<=0)continue; sigma = param.GetLevelSigma(j+param._level_min+1); // ftex->AttachToFBO(0); ftex->FitTexViewPort(); glActiveTexture(GL_TEXTURE0); ftex->BindTex(); glActiveTexture(GL_TEXTURE1); otex->BindTex(); ShaderMan::UseShaderSimpleOrientation(otex->GetTexID(), sigma, sigma_step); ftex->DrawQuad(); } } GLTexImage::UnbindMultiTex(2); } void PyramidPacked::InitPyramid(int w, int h, int ds) { int wp, hp, toobig = 0; if(ds == 0) { _down_sample_factor = 0; if(GlobalUtil::_octave_min_default>=0) { wp = w >> GlobalUtil::_octave_min_default; hp = h >> GlobalUtil::_octave_min_default; }else { wp = w << (-GlobalUtil::_octave_min_default); hp = h << (-GlobalUtil::_octave_min_default); } _octave_min = _octave_min_default; }else { //must use 0 as _octave_min; _octave_min = 0; _down_sample_factor = ds; w >>= ds; h >>= ds; wp = w; hp = h; } while(wp > GlobalUtil::_texMaxDim || hp > GlobalUtil::_texMaxDim ) { _octave_min ++; wp >>= 1; hp >>= 1; toobig = 1; } while(GlobalUtil::_MemCapGPU > 0 && GlobalUtil::_FitMemoryCap && (wp >_pyramid_width || hp > _pyramid_height) && max(max(wp, hp), max(_pyramid_width, _pyramid_height)) > 1024 * sqrt(GlobalUtil::_MemCapGPU / 96.0) ) { _octave_min ++; wp >>= 1; hp >>= 1; toobig = 2; } if(toobig && GlobalUtil::_verbose) { std::cout<<(toobig == 2 ? "[**SKIP OCTAVES**]:\tExceeding Memory Cap (-nomc)\n" : "[**SKIP OCTAVES**]:\tReaching the dimension limit (-maxd)!\n"); } if( wp == _pyramid_width && hp == _pyramid_height && _allocated ) { FitPyramid(wp, hp); }else if(GlobalUtil::_ForceTightPyramid || _allocated ==0) { ResizePyramid(wp, hp); } else if( wp > _pyramid_width || hp > _pyramid_height ) { ResizePyramid(max(wp, _pyramid_width), max(hp, _pyramid_height)); if(wp < _pyramid_width || hp < _pyramid_height) FitPyramid(wp, hp); } else { //try use the pyramid allocated for large image on small input images FitPyramid(wp, hp); } //select the initial smoothing filter according to the new _octave_min ShaderMan::SelectInitialSmoothingFilter(_octave_min + _down_sample_factor, param); } void PyramidPacked::FitPyramid(int w, int h) { //(w, h) <= (_pyramid_width, _pyramid_height); _pyramid_octave_first = 0; // _octave_num = GlobalUtil::_octave_num_default; int _octave_num_max = GetRequiredOctaveNum(min(w, h)); if(_octave_num < 1 || _octave_num > _octave_num_max) { _octave_num = _octave_num_max; } int pw = _pyramid_width>>1, ph = _pyramid_height>>1; while(_pyramid_octave_first + _octave_num < _pyramid_octave_num && pw >= w && ph >= h) { _pyramid_octave_first++; pw >>= 1; ph >>= 1; } for(int i = 0; i < _octave_num; i++) { GLTexImage * tex = GetBaseLevel(i + _octave_min); GLTexImage * dog = GetBaseLevel(i + _octave_min, DATA_DOG); GLTexImage * grd = GetBaseLevel(i + _octave_min, DATA_GRAD); GLTexImage * rot = GetBaseLevel(i + _octave_min, DATA_ROT); GLTexImage * key = GetBaseLevel(i + _octave_min, DATA_KEYPOINT); for(int j = param._level_min; j <= param._level_max; j++, tex++, dog++, grd++, rot++, key++) { tex->SetImageSize(w, h); if(j == param._level_min) continue; dog->SetImageSize(w, h); grd->SetImageSize(w, h); rot->SetImageSize(w, h); if(j == param._level_min + 1 || j == param._level_max) continue; key->SetImageSize(w, h); } w>>=1; h>>=1; } } void PyramidPacked::ResizePyramid( int w, int h) { // unsigned int totalkb = 0; int _octave_num_new, input_sz, i, j; // if(_pyramid_width == w && _pyramid_height == h && _allocated) return; if(w > GlobalUtil::_texMaxDim || h > GlobalUtil::_texMaxDim) return ; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <0) { DestroyPerLevelData(); DestroyPyramidData(); } _pyramid_octave_num = _octave_num_new; } _octave_num = _pyramid_octave_num; int noct = _octave_num; int nlev = param._level_num; // //initialize the pyramid if(_allPyramid==NULL) _allPyramid = new GLTexPacked[ noct* nlev * DATA_NUM]; GLTexPacked * gus = (GLTexPacked *) GetBaseLevel(_octave_min, DATA_GAUSSIAN); GLTexPacked * dog = (GLTexPacked *) GetBaseLevel(_octave_min, DATA_DOG); GLTexPacked * grd = (GLTexPacked *) GetBaseLevel(_octave_min, DATA_GRAD); GLTexPacked * rot = (GLTexPacked *) GetBaseLevel(_octave_min, DATA_ROT); GLTexPacked * key = (GLTexPacked *) GetBaseLevel(_octave_min, DATA_KEYPOINT); ////////////there could be "out of memory" happening during the allocation for(i = 0; i< noct; i++) { for( j = 0; j< nlev; j++, gus++, dog++, grd++, rot++, key++) { gus->InitTexture(w, h); if(j==0)continue; dog->InitTexture(w, h); grd->InitTexture(w, h, 0); rot->InitTexture(w, h); if(j<=1 || j >=nlev -1) continue; key->InitTexture(w, h, 0); } int tsz = (gus -1)->GetTexPixelCount() * 16; totalkb += ((nlev *5 -6)* tsz / 1024); //several auxilary textures are not actually required w>>=1; h>>=1; } totalkb += ResizeFeatureStorage(); _allocated = 1; if(GlobalUtil::_verbose && GlobalUtil::_timingS) std::cout<<"[Allocate Pyramid]:\t" <<(totalkb/1024)<<"MB\n"; } void PyramidPacked::DestroyPyramidData() { if(_allPyramid) { delete [] _allPyramid; _allPyramid = NULL; } } GLTexImage* PyramidPacked::GetLevelTexture(int octave, int level, int dataName) { return _allPyramid+ (_pyramid_octave_first + octave - _octave_min) * param._level_num + param._level_num * _pyramid_octave_num * dataName + (level - param._level_min); } GLTexImage* PyramidPacked::GetLevelTexture(int octave, int level) { return _allPyramid+ (_pyramid_octave_first + octave - _octave_min) * param._level_num + (level - param._level_min); } //in the packed implementation( still in progress) // DATA_GAUSSIAN, DATA_DOG, DATA_GAD will be stored in different textures. GLTexImage* PyramidPacked::GetBaseLevel(int octave, int dataName) { if(octave <_octave_min || octave > _octave_min + _octave_num) return NULL; int offset = (_pyramid_octave_first + octave - _octave_min) * param._level_num; int num = param._level_num * _pyramid_octave_num; return _allPyramid + num *dataName + offset; } void PyramidPacked::FitHistogramPyramid() { GLTexImage * tex, *htex; int hist_level_num = _hpLevelNum - _pyramid_octave_first; tex = GetBaseLevel(_octave_min , DATA_KEYPOINT) + 2; htex = _histoPyramidTex + hist_level_num - 1; int w = (tex->GetImgWidth() + 2) >> 2; int h = (tex->GetImgHeight() + 2)>> 2; //4n+1 -> n; 4n+2,2, 3 -> n+1 for(int k = 0; k GetImgHeight()!= h || htex->GetImgWidth() != w) { htex->SetImageSize(w, h); htex->ZeroHistoMargin(); } w = (w + 1)>>1; h = (h + 1) >> 1; } } colmap-3.9.1/src/thirdparty/SiftGPU/PyramidGL.h000066400000000000000000000074001454702036400212500ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: PyramidGL.h // Author: Changchang Wu // Description : interface for the PyramdGL // class PyramidNaive and PyramidPacked are derived from PyramidGL // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _PYRAMID_GL_H #define _PYRAMID_GL_H class GLTexImage; class SiftParam; class ProgramGPU; class ShaderMan; class GlobalUtil; class SiftPyramid; class PyramidGL:public SiftPyramid { protected: GLTexImage* _histoPyramidTex; GLTexImage* _featureTex; GLTexImage* _descriptorTex; GLTexImage* _orientationTex; public: void InitializeContext(); void SetLevelFeatureNum(int idx, int num); void GetTextureStorageSize(int num, int &fw, int& fh); void GetAlignedStorageSize(int num, int align, int &fw, int &fh); static void InterlaceDescriptorF2(int w, int h, float* buf, float* pd, int step); static void NormalizeDescriptor(int num, float*pd); virtual void DownloadKeypoints(); virtual int ResizeFeatureStorage(); //////////////////////////// virtual void DestroyPerLevelData(); virtual void DestroySharedData(); virtual void GetFeatureDescriptors(); virtual void GenerateFeatureListTex(); virtual void ReshapeFeatureListCPU(); virtual void GenerateFeatureDisplayVBO(); virtual void CleanUpAfterSIFT(); virtual GLTexImage* GetBaseLevel(int octave, int dataName = DATA_GAUSSIAN)=0; public: PyramidGL(SiftParam&sp); virtual ~PyramidGL(); }; class PyramidNaive:public PyramidGL, public ShaderMan { protected: GLTexImage * _texPyramid; GLTexImage * _auxPyramid; public: void DestroyPyramidData(); void GetSimplifiedOrientation(); void GenerateFeatureListCPU(); virtual void GetFeatureOrientations(); virtual void GenerateFeatureList(); void DetectKeypointsEX(); void ComputeGradient(); GLTexImage* GetLevelTexture(int octave, int level); GLTexImage* GetBaseLevel(int octave, int dataName = DATA_GAUSSIAN); GLTexImage* GetLevelTexture(int octave, int level, int dataName); void BuildPyramid(GLTexInput * input); void InitPyramid(int w, int h, int ds); void FitPyramid(int w, int h); void ResizePyramid(int w, int h); void FitHistogramPyramid(); PyramidNaive(SiftParam & sp); ~PyramidNaive(); private: void GenerateFeatureList(int i, int j); }; class PyramidPacked:public PyramidGL, public ShaderMan { GLTexPacked * _allPyramid; public: PyramidPacked(SiftParam& sp); ~PyramidPacked(); void DestroyPyramidData(); void DetectKeypointsEX(); void ComputeGradient(); void BuildPyramid(GLTexInput * input); void InitPyramid(int w, int h, int ds); void FitPyramid(int w, int h); void ResizePyramid(int w, int h); void FitHistogramPyramid(); void GenerateFeatureListCPU(); void GenerateFeatureList(); void GetSimplifiedOrientation(); void GetFeatureOrientations(); GLTexImage* GetBaseLevel(int octave, int dataName = DATA_GAUSSIAN); GLTexImage* GetLevelTexture(int octave, int level); GLTexImage* GetLevelTexture(int octave, int level, int dataName); virtual int IsUsingRectDescription(){return _existing_keypoints & SIFT_RECT_DESCRIPTION; } private: void GenerateFeatureList(int i, int j); }; #endif colmap-3.9.1/src/thirdparty/SiftGPU/ShaderMan.cpp000066400000000000000000000164411454702036400216220ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ShaderMan.cpp // Author: Changchang Wu // Description : implementation of the ShaderMan class. // A Shader Manager that calls different implementation of shaders // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include #include #include using std::vector; using std::ostream; using std::endl; #include "ProgramGLSL.h" #include "GlobalUtil.h" #include "GLTexImage.h" #include "SiftGPU.h" #include "ShaderMan.h" /// ShaderBag * ShaderMan::s_bag = NULL; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// void ShaderMan::InitShaderMan(SiftParam¶m) { if(s_bag) return; if(GlobalUtil::_usePackedTex ) s_bag = new ShaderBagPKSL; else s_bag =new ShaderBagGLSL; GlobalUtil::StartTimer("Load Programs"); s_bag->LoadFixedShaders(); s_bag->LoadDynamicShaders(param); if(GlobalUtil::_UseSiftGPUEX) s_bag->LoadDisplayShaders(); GlobalUtil::StopTimer(); GlobalUtil::CheckErrorsGL("InitShaderMan"); } void ShaderMan::DestroyShaders() { if(s_bag) delete s_bag; s_bag = NULL; } void ShaderMan::UnloadProgram() { if(s_bag) s_bag->UnloadProgram(); } void ShaderMan::FilterImage(FilterProgram* filter, GLTexImage *dst, GLTexImage *src, GLTexImage*tmp) { if(filter == NULL) return; ////////////////////////////// src->FillMargin(filter->_size, 0); //output parameter if(tmp) tmp->AttachToFBO(0); else dst->AttachToFBO(0); //input parameter src->BindTex(); dst->FitTexViewPort(); //horizontal filter filter->s_shader_h->UseProgram(); dst->DrawQuad(); //parameters if(tmp) { // fill margin for out-of-boundary lookup tmp->DetachFBO(0); tmp->AttachToFBO(0); tmp->FillMargin(0, filter->_size); tmp->DetachFBO(0); dst->AttachToFBO(0); tmp->BindTex(); } else { glFinish(); // fill margin for out-of-boundary lookup dst->FillMargin(0, filter->_size); dst->BindTex(); } //vertical filter filter->s_shader_v->UseProgram(); dst->DrawQuad(); //clean up dst->UnbindTex(); dst->DetachFBO(0); // ShaderMan::UnloadProgram(); } void ShaderMan::FilterInitialImage(GLTexImage* tex, GLTexImage* buf) { if(s_bag->f_gaussian_skip0) FilterImage(s_bag->f_gaussian_skip0, tex, tex, buf); } void ShaderMan::FilterSampledImage(GLTexImage* tex, GLTexImage* buf) { if(s_bag->f_gaussian_skip1) FilterImage(s_bag->f_gaussian_skip1, tex, tex, buf); } void ShaderMan::TextureCopy(GLTexImage*dst, GLTexImage*src) { dst->AttachToFBO(0); src->BindTex(); dst->FitTexViewPort(); dst->DrawQuad(); dst->UnbindTex(); // ShaderMan::UnloadProgram(); dst->DetachFBO(0); return; } void ShaderMan::TextureDownSample(GLTexImage *dst, GLTexImage *src, int scale) { //output parameter dst->AttachToFBO(0); //input parameter src->BindTex(); // dst->FitTexViewPort(); s_bag->s_sampling->UseProgram(); dst->DrawQuadDS(scale); src->UnbindTex(); UnloadProgram(); dst->DetachFBO(0); } void ShaderMan::TextureUpSample(GLTexImage *dst, GLTexImage *src, int scale) { //output parameter dst->AttachToFBO(0); //input parameter src->BindTex(); dst->FitTexViewPort(); GlobalUtil::SetTextureParameterUS(); if(GlobalUtil::_usePackedTex) { s_bag->s_sampling->UseProgram(); } dst->DrawQuadUS(scale); src->UnbindTex(); UnloadProgram(); dst->DetachFBO(0); GlobalUtil::SetTextureParameter(); } void ShaderMan::UseShaderDisplayGaussian() { if(s_bag && s_bag->s_display_gaussian) s_bag->s_display_gaussian->UseProgram(); } void ShaderMan::UseShaderDisplayDOG() { if(s_bag && s_bag->s_display_dog) s_bag->s_display_dog->UseProgram(); } void ShaderMan::UseShaderRGB2Gray() { if(s_bag && s_bag->s_gray)s_bag->s_gray->UseProgram(); } void ShaderMan::UseShaderDisplayGrad() { if(s_bag && s_bag->s_display_grad) s_bag->s_display_grad->UseProgram(); } void ShaderMan::UseShaderDisplayKeypoints() { if(s_bag && s_bag->s_display_keys) s_bag->s_display_keys->UseProgram(); } void ShaderMan::UseShaderGradientPass(int texP) { s_bag->s_grad_pass->UseProgram(); s_bag->SetGradPassParam(texP); } void ShaderMan::UseShaderKeypoint(int texU, int texD) { s_bag->s_keypoint->UseProgram(); s_bag->SetDogTexParam(texU, texD); } void ShaderMan::UseShaderGenListInit(int w, int h, int tight) { if(tight) { s_bag->s_genlist_init_tight->UseProgram(); }else { s_bag->s_genlist_init_ex->UseProgram(); s_bag->SetGenListInitParam(w, h); } } void ShaderMan::UseShaderGenListHisto() { s_bag->s_genlist_histo->UseProgram(); } void ShaderMan::UseShaderGenListStart(float fw, int tex0) { s_bag->s_genlist_start->UseProgram(); s_bag->SetGenListStartParam(fw, tex0); } void ShaderMan::UseShaderGenListStep(int tex, int tex0) { s_bag->s_genlist_step->UseProgram(); s_bag->SetGenListStepParam( tex, tex0); } void ShaderMan::UseShaderGenListEnd(int ktex) { s_bag->s_genlist_end->UseProgram(); s_bag->SetGenListEndParam(ktex); } void ShaderMan::UseShaderDebug() { if(s_bag->s_debug) s_bag->s_debug->UseProgram(); } void ShaderMan::UseShaderZeroPass() { if(s_bag->s_zero_pass) s_bag->s_zero_pass->UseProgram(); } void ShaderMan::UseShaderGenVBO( float width, float fwidth, float size) { s_bag->s_vertex_list->UseProgram(); s_bag->SetGenVBOParam(width, fwidth, size); } void ShaderMan::UseShaderMarginCopy(int xmax, int ymax) { s_bag->s_margin_copy->UseProgram(); s_bag->SetMarginCopyParam(xmax, ymax); } void ShaderMan::UseShaderCopyKeypoint() { s_bag->s_copy_key->UseProgram(); } void ShaderMan::UseShaderSimpleOrientation(int oTex, float sigma, float sigma_step) { s_bag->s_orientation->UseProgram(); s_bag->SetSimpleOrientationInput(oTex, sigma, sigma_step); } void ShaderMan::UseShaderOrientation(int gtex, int width, int height, float sigma, int auxtex, float step, int keypoint_list) { s_bag->s_orientation->UseProgram(); //changes in v345. //set sigma to 0 to identify keypoit list mode //set sigma to negative to identify fixed_orientation if(keypoint_list) sigma = 0.0f; else if(GlobalUtil::_FixedOrientation) sigma = - sigma; s_bag->SetFeatureOrientationParam(gtex, width, height, sigma, auxtex, step); } void ShaderMan::UseShaderDescriptor(int gtex, int otex, int dwidth, int fwidth, int width, int height, float sigma) { s_bag->s_descriptor_fp->UseProgram(); s_bag->SetFeatureDescirptorParam(gtex, otex, (float)dwidth, (float)fwidth, (float)width, (float)height, sigma); } void ShaderMan::SelectInitialSmoothingFilter(int octave_min, SiftParam¶m) { s_bag->SelectInitialSmoothingFilter(octave_min, param); } colmap-3.9.1/src/thirdparty/SiftGPU/ShaderMan.h000066400000000000000000000063531454702036400212700ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: ShaderMan.h // Author: Changchang Wu // Description : interface for the ShaderMan class. // This is a class that manages all the shaders for SIFT // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _SIFT_SHADER_MAN_H #define _SIFT_SHADER_MAN_H #include "ProgramGPU.h" #include "ProgramGLSL.h" /////////////////////////////////////////////////////////////////// //class ShaderMan //description: pure static class // wrapper of shaders from different GPU languages /////////////////////////////////////////////////////////////////// class SiftParam; class FilterGLSL; class ShaderMan { public: static ShaderBag* s_bag; public: static void SelectInitialSmoothingFilter(int octave_min, SiftParam¶m); static void UseShaderMarginCopy(int xmax, int ymax); static void UseShaderOrientation(int gtex, int width, int height, float sigma, int auxtex, float step, int keypoint_list); static void UseShaderDescriptor(int gtex, int otex, int dwidth, int fwidth, int width, int height, float sigma); static void UseShaderSimpleOrientation(int oTex, float sigma, float sigma_step); static void UseShaderCopyKeypoint(); static void UseShaderGenVBO( float width, float fwidth, float size); static void UseShaderDebug(); static void UseShaderZeroPass(); static void UseShaderGenListStart(float fw, int tex0); static void UseShaderGenListStep(int tex, int tex0); static void UseShaderGenListEnd(int ktex); static void UseShaderGenListHisto(); static void UseShaderGenListInit(int w, int h, int tight = 1); static void UseShaderKeypoint(int texU, int texD); static void UseShaderGradientPass(int texP = 0); static void UseShaderDisplayKeypoints(); static void UseShaderDisplayGrad(); static void UseShaderRGB2Gray(); static void UseShaderDisplayDOG(); static void UseShaderDisplayGaussian(); /////////////////////////////////////////// static void FilterInitialImage(GLTexImage* tex, GLTexImage* buf); static void FilterSampledImage(GLTexImage* tex, GLTexImage* buf); static void FilterImage(FilterProgram* filter, GLTexImage *dst, GLTexImage *src, GLTexImage*tmp); static void TextureCopy(GLTexImage*dst, GLTexImage*src); static void TextureDownSample(GLTexImage* dst, GLTexImage*src, int scale = 2); static void TextureUpSample(GLTexImage* dst, GLTexImage*src, int scale); /////////////////////////////////////////////// static void InitShaderMan(SiftParam¶m); static void DestroyShaders(); static int HaveShaderMan(){return s_bag != NULL;} static void UnloadProgram(); }; #endif colmap-3.9.1/src/thirdparty/SiftGPU/SiftGPU.cpp000066400000000000000000001135311454702036400212370ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftGPU.cpp // Author: Changchang Wu // Description : Implementation of the SIFTGPU classes. // SiftGPU: The SiftGPU Tool. // SiftGPUEX: SiftGPU + viewer // SiftParam: Sift Parameters // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "SiftGPU.h" #include "GLTexImage.h" #include "ShaderMan.h" #include "FrameBufferObject.h" #include "SiftPyramid.h" #include "PyramidGL.h" //CUDA works only with vc8 or higher #if defined(CUDA_SIFTGPU_ENABLED) #include "PyramidCU.h" #endif #if defined(CL_SIFTGPU_ENABLED) #include "PyramidCL.h" #endif //// #if defined(_WIN32) #include "direct.h" #pragma warning (disable : 4786) #pragma warning (disable : 4996) #else //compatible with linux #define _stricmp strcasecmp #include #include #include #endif #if !defined(_MAX_PATH) #if defined (PATH_MAX) #define _MAX_PATH PATH_MAX #else #define _MAX_PATH 512 #endif #endif ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// // //just want to make this class invisible class ImageList:public std::vector {}; SiftGPU::SiftGPU(int np) { _texImage = new GLTexInput; _imgpath = new char[_MAX_PATH]; _outpath = new char[_MAX_PATH]; _imgpath[0] = _outpath[0] = 0; _initialized = 0; _image_loaded = 0; _current = 0; _list = new ImageList(); _pyramid = NULL; } SiftGPUEX::SiftGPUEX() { _view = _sub_view = 0; _view_debug = 0; GlobalUtil::_UseSiftGPUEX = 1; srand((unsigned int)time(NULL)); RandomizeColor(); } void SiftGPUEX::RandomizeColor() { float hsv[3] = {0, 0.8f, 1.0f}; for(int i = 0; i < COLOR_NUM*3; i+=3) { hsv[0] = (rand()%100)*0.01f; //i/float(COLOR_NUM); HSVtoRGB(hsv, _colors+i); } } SiftGPU::~SiftGPU() { if(_pyramid) delete _pyramid; delete _texImage; delete _list; delete[] _imgpath; delete[] _outpath; } inline void SiftGPU::InitSiftGPU() { if(_initialized || GlobalUtil::_GoodOpenGL ==0) return; //Parse sift parameters ParseSiftParam(); #if !defined(CUDA_SIFTGPU_ENABLED) if(GlobalUtil::_UseCUDA) { GlobalUtil::_UseCUDA = 0; std::cerr << "---------------------------------------------------------------------------\n" << "CUDA not supported in this binary! To enable it, please use SiftGPU_CUDA_Enable\n" << "solution for VS2005+ or set siftgpu_enable_cuda to 1 in makefile\n" << "----------------------------------------------------------------------------\n"; } #else if(GlobalUtil::_UseCUDA == 0 && GlobalUtil::_UseOpenCL == 0) { // GlobalUtil::InitGLParam(0); } if(GlobalUtil::_GoodOpenGL == 0) { GlobalUtil::_UseCUDA = 1; std::cerr << "Switch from OpenGL to CUDA\n"; } if(GlobalUtil::_UseCUDA && !PyramidCU::CheckCudaDevice(GlobalUtil::_DeviceIndex)) { std::cerr << "Switch from CUDA to OpenGL\n"; GlobalUtil::_UseCUDA = 0; } #endif if(GlobalUtil::_verbose) std::cout <<"\n[SiftGPU Language]:\t" << (GlobalUtil::_UseCUDA? "CUDA" : (GlobalUtil::_UseOpenCL? "OpenCL" : "GLSL")) <<"\n"; #if defined(CUDA_SIFTGPU_ENABLED) if(GlobalUtil::_UseCUDA) _pyramid = new PyramidCU(*this); else #endif #if defined(CL_SIFTGPU_ENABLED) if(GlobalUtil::_UseOpenCL) _pyramid = new PyramidCL(*this); else #endif if(GlobalUtil::_usePackedTex) _pyramid = new PyramidPacked(*this); else _pyramid = new PyramidNaive(*this); if(GlobalUtil::_GoodOpenGL && GlobalUtil::_InitPyramidWidth > 0 && GlobalUtil::_InitPyramidHeight > 0) { GlobalUtil::StartTimer("Initialize Pyramids"); _pyramid->InitPyramid(GlobalUtil::_InitPyramidWidth, GlobalUtil::_InitPyramidHeight, 0); GlobalUtil::StopTimer(); } ClockTimer::InitHighResolution(); _initialized = 1; } int SiftGPU::RunSIFT(int index) { if(_list->size()>0 ) { index = index % _list->size(); if(strcmp(_imgpath, _list->at(index).data())) { strcpy(_imgpath, _list->at(index).data()); _image_loaded = 0; _current = index; } return RunSIFT(); }else { return 0; } } int SiftGPU::RunSIFT( int width, int height, const void * data, unsigned int gl_format, unsigned int gl_type) { if(GlobalUtil::_GoodOpenGL ==0 ) return 0; if(!_initialized) InitSiftGPU(); else GlobalUtil::SetGLParam(); if(GlobalUtil::_GoodOpenGL ==0 ) return 0; if(width > 0 && height >0 && data != NULL) { _imgpath[0] = 0; //try downsample the image on CPU GlobalUtil::StartTimer("Upload Image data"); if(_texImage->SetImageData(width, height, data, gl_format, gl_type)) { _image_loaded = 2; //gldata; GlobalUtil::StopTimer(); _timing[0] = GlobalUtil::GetElapsedTime(); //if the size of image is different, the pyramid need to be reallocated. GlobalUtil::StartTimer("Initialize Pyramid"); _pyramid->InitPyramid(width, height, _texImage->_down_sampled); GlobalUtil::StopTimer(); _timing[1] = GlobalUtil::GetElapsedTime(); return RunSIFT(); }else { return 0; } }else { return 0; } } int SiftGPU::RunSIFT(const char * imgpath) { if(imgpath && imgpath[0]) { //set the new image strcpy(_imgpath, imgpath); _image_loaded = 0; return RunSIFT(); }else { return 0; } } int SiftGPU::RunSIFT(int num, const SiftKeypoint * keys, int keys_have_orientation) { if(num <=0) return 0; _pyramid->SetKeypointList(num, (const float*) keys, 1, keys_have_orientation); return RunSIFT(); } int SiftGPU::RunSIFT() { //check image data if(_imgpath[0]==0 && _image_loaded == 0) return 0; //check OpenGL support if(GlobalUtil::_GoodOpenGL ==0 ) return 0; ClockTimer timer; if(!_initialized) { //initialize SIFT GPU for once InitSiftGPU(); if(GlobalUtil::_GoodOpenGL ==0 ) return 0; }else { //in case some OpenGL parameters are changed by users GlobalUtil::SetGLParam(); } timer.StartTimer("RUN SIFT"); //process input image file if( _image_loaded ==0) { int width, height; //load and try down-sample on cpu GlobalUtil::StartTimer("Load Input Image"); if(!_texImage->LoadImageFile(_imgpath, width, height)) return 0; _image_loaded = 1; GlobalUtil::StopTimer(); _timing[0] = GlobalUtil::GetElapsedTime(); //make sure the pyrmid can hold the new image. GlobalUtil::StartTimer("Initialize Pyramid"); _pyramid->InitPyramid(width, height, _texImage->_down_sampled); GlobalUtil::StopTimer(); _timing[1] = GlobalUtil::GetElapsedTime(); }else { //change some global states if(!GlobalUtil::_UseCUDA && !GlobalUtil::_UseOpenCL) { GlobalUtil::FitViewPort(1,1); _texImage->FitTexViewPort(); } if(_image_loaded == 1) { _timing[0] = _timing[1] = 0; }else {//2 _image_loaded = 1; } } if(_pyramid->_allocated ==0 ) return 0; #ifdef DEBUG_SIFTGPU _pyramid->BeginDEBUG(_imgpath); #endif //process the image _pyramid->RunSIFT(_texImage); //read back the timing _pyramid->GetPyramidTiming(_timing + 2); //write output once if there is only one input if(_outpath[0] ){ SaveSIFT(_outpath); _outpath[0] = 0;} //terminate the process when -exit is provided. if(GlobalUtil::_ExitAfterSIFT && GlobalUtil::_UseSiftGPUEX) exit(0); timer.StopTimer(); if(GlobalUtil::_verbose)std::cout<GetSucessStatus(); } void SiftGPU::SetKeypointList(int num, const SiftKeypoint * keys, int keys_have_orientation) { _pyramid->SetKeypointList(num, (const float*)keys, 0, keys_have_orientation); } void SiftGPUEX::DisplayInput() { if(_texImage==NULL) return; _texImage->VerifyTexture(); _texImage->BindTex(); _texImage->DrawImage(); _texImage->UnbindTex(); } void SiftGPU::SetVerbose(int verbose) { GlobalUtil::_timingO = verbose>2; GlobalUtil::_timingL = verbose>3; if(verbose == -1) { //Loop between verbose level 0, 1, 2 if(GlobalUtil::_verbose) { GlobalUtil::_verbose = GlobalUtil::_timingS; GlobalUtil::_timingS = 0; if(GlobalUtil::_verbose ==0 && GlobalUtil::_UseSiftGPUEX) std::cout << "Console output disabled, press Q/V to enable\n\n"; }else { GlobalUtil::_verbose = 1; GlobalUtil::_timingS = 1; } }else if(verbose == -2) { //trick for disabling all output (still keeps the timing level) GlobalUtil::_verbose = 0; GlobalUtil::_timingS = 1; }else { GlobalUtil::_verbose = verbose>0; GlobalUtil::_timingS = verbose>1; } } SiftParam::SiftParam() { _level_min = -1; _dog_level_num = 3; _level_max = 0; _sigma0 = 0; _sigman = 0; _edge_threshold = 0; _dog_threshold = 0; } float SiftParam::GetInitialSmoothSigma(int octave_min) { float sa = _sigma0 * powf(2.0f, float(_level_min)/float(_dog_level_num)) ; float sb = _sigman / powf(2.0f, float(octave_min)) ;// float sigma_skip0 = sa > sb + 0.001?sqrt(sa*sa - sb*sb): 0.0f; return sigma_skip0; } void SiftParam::ParseSiftParam() { if(_dog_level_num ==0) _dog_level_num = 3; if(_level_max ==0) _level_max = _dog_level_num + 1; if(_sigma0 ==0.0f) _sigma0 = 1.6f * powf(2.0f, 1.0f / _dog_level_num) ; if(_sigman == 0.0f) _sigman = 0.5f; _level_num = _level_max -_level_min + 1; _level_ds = _level_min + _dog_level_num; if(_level_ds > _level_max ) _level_ds = _level_max ; /// float _sigmak = powf(2.0f, 1.0f / _dog_level_num) ; float dsigma0 = _sigma0 * sqrt (1.0f - 1.0f / (_sigmak*_sigmak) ) ; float sa, sb; sa = _sigma0 * powf(_sigmak, (float)_level_min) ; sb = _sigman / powf(2.0f, (float)GlobalUtil::_octave_min_default) ;// _sigma_skip0 = sa>sb+ 0.001?sqrt(sa*sa - sb*sb): 0.0f; sa = _sigma0 * powf(_sigmak, float(_level_min )) ; sb = _sigma0 * powf(_sigmak, float(_level_ds - _dog_level_num)) ; _sigma_skip1 = sa>sb + 0.001? sqrt(sa*sa - sb*sb): 0.0f; _sigma_num = _level_max - _level_min; _sigma = new float[_sigma_num]; for(int i = _level_min + 1; i <= _level_max; i++) { _sigma[i-_level_min -1] = dsigma0 * powf(_sigmak, float(i)) ; } if(_dog_threshold ==0) _dog_threshold = 0.02f / _dog_level_num ; if(_edge_threshold==0) _edge_threshold = 10.0f; } void SiftGPUEX::DisplayOctave(void (*UseDisplayShader)(), int i) { if(_pyramid == NULL)return; const int grid_sz = (int)ceil(_level_num/2.0); double scale = 1.0/grid_sz ; int gx=0, gy=0, dx, dy; if(_pyramid->_octave_min >0) scale *= (1<<_pyramid->_octave_min); else if(_pyramid->_octave_min < 0) scale /= (1<<(-_pyramid->_octave_min)); i = i% _pyramid->_octave_num; // if(i<0 ) i+= _pyramid->_octave_num; scale *= ( 1<<(i)); UseDisplayShader(); glPushMatrix(); glScaled(scale, scale, scale); for(int level = _level_min; level<= _level_max; level++) { GLTexImage * tex = _pyramid->GetLevelTexture(i+_pyramid->_octave_min, level); dx = tex->GetImgWidth(); dy = tex->GetImgHeight(); glPushMatrix(); glTranslated(dx*gx, dy*gy, 0); tex->BindTex(); tex->DrawImage(); tex->UnbindTex(); glPopMatrix(); gx++; if(gx>=grid_sz) { gx =0; gy++; } } glPopMatrix(); ShaderMan::UnloadProgram(); } void SiftGPUEX::DisplayPyramid( void (*UseDisplayShader)(), int dataName, int nskip1, int nskip2) { if(_pyramid == NULL)return; int grid_sz = (_level_num -nskip1 - nskip2); if(grid_sz > 4) grid_sz = (int)ceil(grid_sz*0.5); double scale = 1.0/grid_sz; int stepx = 0, stepy = 0, dx, dy=0, nstep; if(_pyramid->_octave_min >0) scale *= (1<<_pyramid->_octave_min); else if(_pyramid->_octave_min < 0) scale /= (1<<(-_pyramid->_octave_min)); glPushMatrix(); glScaled(scale, scale, scale); for(int i = _pyramid->_octave_min; i < _pyramid->_octave_min+_pyramid->_octave_num; i++) { nstep = i==_pyramid->_octave_min? grid_sz: _level_num; dx = 0; UseDisplayShader(); for(int j = _level_min + nskip1; j <= _level_max-nskip2; j++) { GLTexImage * tex = _pyramid->GetLevelTexture(i, j, dataName); if(tex->GetImgWidth() == 0 || tex->GetImgHeight() == 0) continue; stepx = tex->GetImgWidth(); stepy = tex->GetImgHeight(); //// if(j == _level_min + nskip1 + nstep) { dy += stepy; dx = 0; } glPushMatrix(); glTranslated(dx, dy, 0); tex->BindTex(); tex->DrawImage(); tex->UnbindTex(); glPopMatrix(); dx += stepx; } ShaderMan::UnloadProgram(); dy+= stepy; } glPopMatrix(); } void SiftGPUEX::DisplayLevel(void (*UseDisplayShader)(), int i) { if(_pyramid == NULL)return; i = i%(_level_num * _pyramid->_octave_num); if (i<0 ) i+= (_level_num * _pyramid->_octave_num); int octave = _pyramid->_octave_min + i/_level_num; int level = _level_min + i%_level_num; double scale = 1.0; if(octave >0) scale *= (1<GetLevelTexture(octave, level); UseDisplayShader(); glPushMatrix(); glScaled(scale, scale, scale); tex->BindTex(); tex->DrawImage(); tex->UnbindTex(); glPopMatrix(); ShaderMan::UnloadProgram(); } void SiftGPUEX::DisplaySIFT() { if(_pyramid == NULL) return; glEnable(GlobalUtil::_texTarget); switch(_view) { case 0: DisplayInput(); DisplayFeatureBox(_sub_view); break; case 1: DisplayPyramid(ShaderMan::UseShaderDisplayGaussian, SiftPyramid::DATA_GAUSSIAN); break; case 2: DisplayOctave(ShaderMan::UseShaderDisplayGaussian, _sub_view); break; case 3: DisplayLevel(ShaderMan::UseShaderDisplayGaussian, _sub_view); break; case 4: DisplayPyramid(ShaderMan::UseShaderDisplayDOG, SiftPyramid::DATA_DOG, 1); break; case 5: DisplayPyramid(ShaderMan::UseShaderDisplayGrad, SiftPyramid::DATA_GRAD, 1); break; case 6: DisplayPyramid(ShaderMan::UseShaderDisplayDOG, SiftPyramid::DATA_DOG,2, 1); DisplayPyramid(ShaderMan::UseShaderDisplayKeypoints, SiftPyramid::DATA_KEYPOINT, 2,1); } } void SiftGPUEX::SetView(int view, int sub_view, char *title) { const char* view_titles[] = { "Original Image", "Gaussian Pyramid", "Octave Images", "Level Image", "Difference of Gaussian", "Gradient", "Keypoints" }; const int view_num = 7; _view = view % view_num; if(_view <0) _view +=view_num; _sub_view = sub_view; if(_view_debug) strcpy(title, "Debug..."); else strcpy(title, view_titles[_view]); } void SiftGPU::PrintUsage() { std::cout <<"SiftGPU Usage:\n" <<"-h -help : Parameter information\n" <<"-i : Filename(s) of the input image(s)\n" <<"-il : Filename of an image list file\n" <<"-o : Where to save SIFT features\n" <<"-f : Filter width factor; Width will be 2*factor+1 (default : 4.0)\n" <<"-w : Orientation sample window factor (default: 2.0)\n" <<"-dw * : Descriptor grid size factor (default : 3.0)\n" <<"-fo * : First octave to detect DOG keypoints(default : 0)\n" <<"-no : Maximum number of Octaves (default : no limit)\n" <<"-d : Number of DOG levels in an octave (default : 3)\n" <<"-t : DOG threshold (default : 0.02/3)\n" <<"-e : Edge Threshold (default : 10.0)\n" <<"-m : Multi Feature Orientations (default : 1)\n" <<"-m2p : 2 Orientations packed as one float\n" <<"-s : Sub-Pixel, Sub-Scale Localization, Multi-Refinement(num)\n" <<"-lcpu -lc : CPU/GPU mixed Feature List Generation (default: 6)\n" <<" Use GPU first, and use CPU when reduction size <= pow(2,num)\n" <<" When is missing or equals -1, no GPU will be used\n" <<"-noprep : Upload raw data to GPU (default: RGB->LUM and down-sample on CPU)\n" <<"-sd : Skip descriptor computation if specified\n" <<"-unn * : Write unnormalized descriptor if specified\n" <<"-b * : Write binary sift file if specified\n" <<"-fs : Block Size for freature storage \n" <<"-cuda : Use CUDA SiftGPU, and specify the device index\n" <<"-tight : Automatically resize pyramid to fit new images tightly\n" <<"-p x : Inititialize the pyramids to contain image of WxH (eg -p 1024x768)\n" <<"-tc[1|2|3] *: Threshold for limiting the overall number of features (3 methods)\n" <<"-v : Level of timing details. Same as calling Setverbose() function\n" <<"-loweo : (0, 0) at center of top-left pixel (default: corner)\n" <<"-maxd * : Max working dimension (default : 2560 (unpacked) / 3200 (packed))\n" <<"-nomc : Disabling auto-downsamping that try to fit GPU memory cap\n" <<"-exit : Exit program after processing the input image\n" <<"-unpack : Use the old unpacked implementation\n" <<"-di : Use dynamic array indexing if available (default : no)\n" <<" It could make computation faster on cards like GTX 280\n" <<"-ofix * : use 0 as feature orientations.\n" <<"-ofix-not * : disable -ofix.\n" <<"-winpos x * : Screen coordinate used in Win32 to select monitor/GPU.\n" <<"-display *: Display name used in Linux/Mac to select monitor/GPU.\n" <<"\n" <<"NOTE: parameters marked with * can be changed after initialization\n" <<"\n"; } void SiftGPU::ParseParam(const int argc, const char **argv) { #define CHAR1_TO_INT(x) ((x >= 'A' && x <= 'Z') ? x + 32 : x) #define CHAR2_TO_INT(str, i) (str[i] ? CHAR1_TO_INT(str[i]) + (CHAR1_TO_INT(str[i+1]) << 8) : 0) #define CHAR3_TO_INT(str, i) (str[i] ? CHAR1_TO_INT(str[i]) + (CHAR2_TO_INT(str, i + 1) << 8) : 0) #define STRING_TO_INT(str) (CHAR1_TO_INT(str[0]) + (CHAR3_TO_INT(str, 1) << 8)) #ifdef _MSC_VER //charizing is microsoft only #define MAKEINT1(a) (#@a ) #else #define mychar0 '0' #define mychar1 '1' #define mychar2 '2' #define mychar3 '3' #define mychara 'a' #define mycharb 'b' #define mycharc 'c' #define mychard 'd' #define mychare 'e' #define mycharf 'f' #define mycharg 'g' #define mycharh 'h' #define mychari 'i' #define mycharj 'j' #define mychark 'k' #define mycharl 'l' #define mycharm 'm' #define mycharn 'n' #define mycharo 'o' #define mycharp 'p' #define mycharq 'q' #define mycharr 'r' #define mychars 's' #define mychart 't' #define mycharu 'u' #define mycharv 'v' #define mycharw 'w' #define mycharx 'x' #define mychary 'y' #define mycharz 'z' #define MAKEINT1(a) (mychar##a ) #endif #define MAKEINT2(a, b) (MAKEINT1(a) + (MAKEINT1(b) << 8)) #define MAKEINT3(a, b, c) (MAKEINT1(a) + (MAKEINT2(b, c) << 8)) #define MAKEINT4(a, b, c, d) (MAKEINT1(a) + (MAKEINT3(b, c, d) << 8)) const char* arg, *param, * opt; int setMaxD = 0, opti; for(int i = 0; i< argc; i++) { arg = argv[i]; if(arg == NULL || arg[0] != '-' || !arg[1])continue; opt = arg+1; opti = STRING_TO_INT(opt); param = argv[i+1]; //////////////////////////////// switch(opti) { case MAKEINT1(h): case MAKEINT4(h, e, l, p): PrintUsage(); break; case MAKEINT4(c, u, d, a): #if defined(CUDA_SIFTGPU_ENABLED) if(!_initialized) { GlobalUtil::_UseCUDA = 1; int device = -1; if(i+1 =0) { GlobalUtil::_DeviceIndex = device; i++; } } #else std::cerr << "---------------------------------------------------------------------------\n" << "CUDA not supported in this binary! To enable it, please use SiftGPU_CUDA_Enable\n" << "solution for VS2005+ or set siftgpu_enable_cuda to 1 in makefile\n" << "----------------------------------------------------------------------------\n"; #endif break; case MAKEINT2(c, l): #if defined(CL_SIFTGPU_ENABLED) if(!_initialized) GlobalUtil::_UseOpenCL = 1; #else std::cerr << "---------------------------------------------------------------------------\n" << "OpenCL not supported in this binary! Define CL_CUDA_SIFTGPU_ENABLED to..\n" << "----------------------------------------------------------------------------\n"; #endif break; case MAKEINT4(p, a, c, k): if(!_initialized) GlobalUtil::_usePackedTex = 1; break; case MAKEINT4(u, n, p, a): //unpack if(!_initialized) { GlobalUtil::_usePackedTex = 0; if(!setMaxD) GlobalUtil::_texMaxDim = 2560; } break; case MAKEINT4(l, c, p, u): case MAKEINT2(l, c): if(!_initialized) { int gskip = -1; if(i+1 = 0) { GlobalUtil::_ListGenSkipGPU = gskip; }else { GlobalUtil::_ListGenGPU = 0; } } break; case MAKEINT4(p, r, e, p): GlobalUtil::_PreProcessOnCPU = 1; break; case MAKEINT4(n, o, p, r): //noprep GlobalUtil::_PreProcessOnCPU = 0; break; case MAKEINT4(f, b, o, 1): FrameBufferObject::UseSingleFBO =1; break; case MAKEINT4(f, b, o, s): FrameBufferObject::UseSingleFBO = 0; break; case MAKEINT2(s, d): if(!_initialized) GlobalUtil::_DescriptorPPT =0; break; case MAKEINT3(u, n, n): GlobalUtil::_NormalizedSIFT =0; break; case MAKEINT4(n, d, e, s): GlobalUtil::_NormalizedSIFT =1; break; case MAKEINT1(b): GlobalUtil::_BinarySIFT = 1; break; case MAKEINT4(t, i, g, h): //tight GlobalUtil::_ForceTightPyramid = 1; break; case MAKEINT4(e, x, i, t): GlobalUtil::_ExitAfterSIFT = 1; break; case MAKEINT2(d, i): GlobalUtil::_UseDynamicIndexing = 1; break; case MAKEINT4(s, i, g, n): if(!_initialized || GlobalUtil::_UseCUDA) GlobalUtil::_KeepExtremumSign = 1; break; case MAKEINT1(m): case MAKEINT2(m, o): if(!_initialized) { int mo = 2; //default multi-orientation if(i+1 = argc) break; switch(opti) { case MAKEINT1(i): strcpy(_imgpath, param); i++; //get the file list.. _list->push_back(param); while( i+1 < argc && argv[i+1][0] !='-') { _list->push_back(argv[++i]); } break; case MAKEINT2(i, l): LoadImageList(param); i++; break; case MAKEINT1(o): strcpy(_outpath, param); i++; break; case MAKEINT1(f): { float factor = 0.0f; if(sscanf(param, "%f", &factor) && factor > 0 ) { GlobalUtil::_FilterWidthFactor = factor; i++; } } break; case MAKEINT2(o, t): { float factor = 0.0f; if(sscanf(param, "%f", &factor) && factor>0 ) { GlobalUtil::_MulitiOrientationThreshold = factor; i++; } break; } case MAKEINT1(w): { float factor = 0.0f; if(sscanf(param, "%f", &factor) && factor>0 ) { GlobalUtil::_OrientationWindowFactor = factor; i++; } break; } case MAKEINT2(d, w): { float factor = 0.0f; if(sscanf(param, "%f", &factor) && factor > 0 ) { GlobalUtil::_DescriptorWindowFactor = factor; i++; } break; } case MAKEINT2(f, o): { int first_octave = -3; if(sscanf(param, "%d", &first_octave) && first_octave >=-2 ) { GlobalUtil::_octave_min_default = first_octave; i++; } break; } case MAKEINT2(n, o): if(!_initialized) { int octave_num=-1; if(sscanf(param, "%d", &octave_num)) { octave_num = max(-1, octave_num); if(octave_num ==-1 || octave_num >=1) { GlobalUtil::_octave_num_default = octave_num; i++; } } } break; case MAKEINT1(t): { float threshold = 0.0f; if(sscanf(param, "%f", &threshold) && threshold >0 && threshold < 0.5f) { SiftParam::_dog_threshold = threshold; i++; } break; } case MAKEINT1(e): { float threshold = 0.0f; if(sscanf(param, "%f", &threshold) && threshold >0 ) { SiftParam::_edge_threshold = threshold; i++; } break; } case MAKEINT1(d): { int num = 0; if(sscanf(param, "%d", &num) && num >=1 && num <=10) { SiftParam::_dog_level_num = num; i++; } break; } case MAKEINT2(f, s): { int num = 0; if(sscanf(param, "%d", &num) && num >=1) { GlobalParam::_FeatureTexBlock = num; i++; } break; } case MAKEINT1(p): { int w =0, h=0; if(sscanf(param, "%dx%d", &w, &h) == 2 && w >0 && h>0) { GlobalParam::_InitPyramidWidth = w; GlobalParam::_InitPyramidHeight = h; i++; } break; } case MAKEINT4(w, i, n, p): //winpos { int x =0, y=0; if(sscanf(param, "%dx%d", &x, &y) == 2) { GlobalParam::_WindowInitX = x; GlobalParam::_WindowInitY = y; i++; } break; } case MAKEINT4(d, i, s, p): //display { GlobalParam::_WindowDisplay = param; i++; break; } case MAKEINT2(l, m): { int num = 0; if(sscanf(param, "%d", &num) && num >=1000) { GlobalParam::_MaxLevelFeatureNum = num; i++; } break; } case MAKEINT3(l, m, p): { float num = 0.0f; if(sscanf(param, "%f", &num) && num >=0.001) { GlobalParam::_MaxFeaturePercent = num; i++; } break; } case MAKEINT3(t, c, 2): //downward case MAKEINT3(t, c, 3): case MAKEINT2(t, c): //tc case MAKEINT3(t, c, 1): // { switch (opti) { case MAKEINT3(t, c, 2): GlobalUtil::_TruncateMethod = 1; break; case MAKEINT3(t, c, 3): GlobalUtil::_TruncateMethod = 2; break; default: GlobalUtil::_TruncateMethod = 0; break; } int num = -1; if(sscanf(param, "%d", &num) && num > 0) { GlobalParam::_FeatureCountThreshold = num; i++; } break; } case MAKEINT1(v): { int num = 0; if(sscanf(param, "%d", &num) && num >=0 && num <= 4) { SetVerbose(num); } break; } case MAKEINT4(m, a, x, d): { int num = 0; if(sscanf(param, "%d", &num) && num > 0) { GlobalUtil::_texMaxDim = num; setMaxD = 1; } break; } case MAKEINT4(m, i, n, d): { int num = 0; if(sscanf(param, "%d", &num) && num >= 8) { GlobalUtil::_texMinDim = num; } break; } default: break; } break; } } //////////////////////// GlobalUtil::SelectDisplay(); //do not write result if there are more than one input images if(_outpath[0] && _list->size()>1) _outpath[0] = 0; } void SiftGPU::SetImageList(int nimage, const char** filelist) { _list->resize(0); for(int i = 0; i < nimage; i++) { _list->push_back(filelist[i]); } _current = 0; } void SiftGPU:: LoadImageList(const char *imlist) { char filename[_MAX_PATH]; ifstream in(imlist); while(in>>filename) { _list->push_back(filename); } in.close(); if(_list->size()>0) { strcpy(_imgpath, _list->at(0).data()); strcpy(filename, imlist); char * slash = strrchr(filename, '\\'); if(slash == 0) slash = strrchr(filename, '/'); if(slash ) { slash[1] = 0; chdir(filename); } } _image_loaded = 0; } float SiftParam::GetLevelSigma( int lev) { return _sigma0 * powf( 2.0f, float(lev) / float(_dog_level_num )); //bug fix 9/12/2007 } void SiftGPUEX::DisplayFeatureBox(int view ) { view = view%3; if(view<0)view+=3; if(view ==2) return; int idx = 0; const int *fnum = _pyramid->GetLevelFeatureNum(); const GLuint *vbo = _pyramid->GetFeatureDipslayVBO(); const GLuint *vbop = _pyramid->GetPointDisplayVBO(); if(vbo == NULL || vbop == NULL) return; //int nvbo = _dog_level_num * _pyramid->_octave_num; glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glEnableClientState(GL_VERTEX_ARRAY); glPushMatrix(); // glTranslatef(0.0f, 0.0f, -1.0f); glPointSize(2.0f); float scale = 1.0f; if(_pyramid->_octave_min >0) scale *= (1<<_pyramid->_octave_min); else if(_pyramid->_octave_min < 0) scale /= (1<<(-_pyramid->_octave_min)); glScalef(scale, scale, 1.0f); for(int i = 0; i < _pyramid->_octave_num; i++) { for(int j = 0; j < _dog_level_num; j++, idx++) { if(fnum[idx]>0) { if(view ==0) { glColor3f(0.2f, 1.0f, 0.2f); glBindBuffer(GL_ARRAY_BUFFER_ARB, vbop[idx]); glVertexPointer( 4, GL_FLOAT,4*sizeof(float), (char *) 0); glDrawArrays( GL_POINTS, 0, fnum[idx]); glFlush(); }else { //glColor3f(1.0f, 0.0f, 0.0f); glColor3fv(_colors+ (idx%COLOR_NUM)*3); glBindBuffer(GL_ARRAY_BUFFER_ARB, vbo[idx]); glVertexPointer( 4, GL_FLOAT,4*sizeof(float), (char *) 0); glDrawArrays( GL_LINES, 0, fnum[idx]*10 ); glFlush(); } } } glTranslatef(-.5f, -.5f, 0.0f); glScalef(2.0f, 2.0f, 1.0f); } glPopMatrix(); glDisableClientState(GL_VERTEX_ARRAY); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glPointSize(1.0f); } void SiftGPUEX::ToggleDisplayDebug() { _view_debug = !_view_debug; } void SiftGPUEX::DisplayDebug() { glPointSize(1.0f); glColor3f(1.0f, 0.0f, 0.0f); ShaderMan::UseShaderDebug(); glBegin(GL_POINTS); for(int i = 0; i < 100; i++) { glVertex2f(i*4.0f+0.5f, i*4.0f+0.5f); } glEnd(); ShaderMan::UnloadProgram(); } int SiftGPU::CreateContextGL() { if(GlobalUtil::_UseOpenCL || GlobalUtil::_UseCUDA) { //do nothing } else if(!GlobalUtil::CreateWindowEZ()) { #if CUDA_SIFTGPU_ENABLED GlobalUtil::_UseCUDA = 1; #else return 0; #endif } return VerifyContextGL(); } int SiftGPU::VerifyContextGL() { InitSiftGPU(); return (GlobalUtil::_GoodOpenGL > 0) + GlobalUtil::_FullSupported; } int SiftGPU::IsFullSupported() { return GlobalUtil::_GoodOpenGL > 0 && GlobalUtil::_FullSupported; } void SiftGPU::SaveSIFT(const char * szFileName) { _pyramid->SaveSIFT(szFileName); } int SiftGPU::GetFeatureNum() { return _pyramid->GetFeatureNum(); } void SiftGPU::GetFeatureVector(SiftKeypoint * keys, float * descriptors) { // keys.resize(_pyramid->GetFeatureNum()); if(GlobalUtil::_DescriptorPPT) { // descriptors.resize(128*_pyramid->GetFeatureNum()); _pyramid->CopyFeatureVector((float*) (&keys[0]), &descriptors[0]); }else { //descriptors.resize(0); _pyramid->CopyFeatureVector((float*) (&keys[0]), NULL); } } void SiftGPU::SetTightPyramid(int tight) { GlobalUtil::_ForceTightPyramid = tight; } int SiftGPU::AllocatePyramid(int width, int height) { _pyramid->_down_sample_factor = 0; _pyramid->_octave_min = GlobalUtil::_octave_min_default; if(GlobalUtil::_octave_min_default>=0) { width >>= GlobalUtil::_octave_min_default; height >>= GlobalUtil::_octave_min_default; }else { width <<= (-GlobalUtil::_octave_min_default); height <<= (-GlobalUtil::_octave_min_default); } _pyramid->ResizePyramid(width, height); return _pyramid->_pyramid_height == height && width == _pyramid->_pyramid_width ; } void SiftGPU::SetMaxDimension(int sz) { if(sz < GlobalUtil::_texMaxDimGL) { GlobalUtil::_texMaxDim = sz; } } int SiftGPU::GetFeatureCountThreshold() { return GlobalParam::_FeatureCountThreshold; } int SiftGPU::GetMaxOrientation() { return GlobalParam::_MaxOrientation; } int SiftGPU::GetMaxDimension() { return GlobalUtil::_texMaxDim; } int SiftGPU::GetImageCount() { return _list->size(); } void SiftGPUEX::HSVtoRGB(float hsv[3],float rgb[3] ) { int i; float q, t, p; float hh,f, v = hsv[2]; if(hsv[1]==0.0f) { rgb[0]=rgb[1]=rgb[2]=v; } else { ////////////// hh =hsv[0]*6.0f ; // sector 0 to 5 i =(int)hh ; f = hh- i; // factorial part of h ////////// p= v * ( 1 - hsv[1] ); q = v * ( 1 - hsv[1] * f ); t = v * ( 1 - hsv[1] * ( 1 - f ) ); switch( i ) { case 0:rgb[0] = v;rgb[1] = t;rgb[2] = p;break; case 1:rgb[0] = q;rgb[1] = v;rgb[2] = p;break; case 2:rgb[0] = p;rgb[1] = v;rgb[2] = t;break; case 3:rgb[0] = p;rgb[1] = q;rgb[2] = v;break; case 4:rgb[0] = t;rgb[1] = p;rgb[2] = v;break; case 5:rgb[0] = v;rgb[1] = p;rgb[2] = q;break; default:rgb[0]= 0;rgb[1] = 0;rgb[2] = 0; } } } void SiftGPUEX::GetImageDimension( int &w, int &h) { w = _texImage->GetImgWidth(); h = _texImage->GetImgHeight(); } void SiftGPUEX::GetInitWindowPotition(int&x, int&y) { x = GlobalUtil::_WindowInitX; y = GlobalUtil::_WindowInitY; } SiftGPU* CreateNewSiftGPU(int np) { return new SiftGPU(np); } ///////////////////////////////////////////////////// ComboSiftGPU* CreateComboSiftGPU() { return new ComboSiftGPU(); } colmap-3.9.1/src/thirdparty/SiftGPU/SiftGPU.h000066400000000000000000000352661454702036400207140ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftGPU.h // Author: Changchang Wu // Description : interface for the SIFTGPU class. // SiftGPU: The SiftGPU Tool. // SiftGPUEX: SiftGPU + viewer // SiftParam: Sift Parameters // SiftMatchGPU: GPU SIFT Matcher; // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef GPU_SIFT_H #define GPU_SIFT_H #if defined(_WIN32) #ifdef SIFTGPU_DLL #ifdef DLL_EXPORT #define SIFTGPU_EXPORT __declspec(dllexport) #else #define SIFTGPU_EXPORT __declspec(dllimport) #endif #else #define SIFTGPU_EXPORT #endif #define SIFTGPU_EXPORT_EXTERN SIFTGPU_EXPORT #if _MSC_VER > 1000 #pragma once #endif #else #define SIFTGPU_EXPORT #define SIFTGPU_EXPORT_EXTERN extern "C" #endif #ifdef _MSC_VER #if _MSC_VER >= 1600 #include #else typedef __int8 int8_t; typedef __int16 int16_t; typedef __int32 int32_t; typedef __int64 int64_t; typedef unsigned __int8 uint8_t; typedef unsigned __int16 uint16_t; typedef unsigned __int32 uint32_t; typedef unsigned __int64 uint64_t; #endif #elif __GNUC__ >= 3 #include #endif /////////////////////////////////////////////////////////////////// //clss SiftParam //description: SIFT parameters //////////////////////////////////////////////////////////////////// class GlobalUtil; class SiftParam { public: float* _sigma; float _sigma_skip0; // float _sigma_skip1; // //sigma of the first level float _sigma0; float _sigman; int _sigma_num; //how many dog_level in an octave int _dog_level_num; int _level_num; //starting level in an octave int _level_min; int _level_max; int _level_ds; //dog threshold float _dog_threshold; //edge elimination float _edge_threshold; void ParseSiftParam(); public: float GetLevelSigma(int lev); float GetInitialSmoothSigma(int octave_min); SIFTGPU_EXPORT SiftParam(); }; class LiteWindow; class GLTexInput; class ShaderMan; class SiftPyramid; class ImageList; //////////////////////////////////////////////////////////////// //class SIftGPU //description: Interface of SiftGPU lib //////////////////////////////////////////////////////////////// class SiftGPU:public SiftParam { public: enum { SIFTGPU_NOT_SUPPORTED = 0, SIFTGPU_PARTIAL_SUPPORTED = 1, // detction works, but not orientation/descriptor SIFTGPU_FULL_SUPPORTED = 2 }; int gpu_index = 0; typedef struct SiftKeypoint { float x, y, s, o; //x, y, scale, orientation. }SiftKeypoint; protected: //when more than one images are specified //_current indicates the active one int _current; //_initialized indicates if the shaders and OpenGL/SIFT parameters are initialized //they are initialized only once for one SiftGPU inistance //that is, SIFT parameters will not be changed int _initialized; //_image_loaded indicates if the current images are loaded int _image_loaded; //the name of current input image char* _imgpath; //_outpath containes the name of the output file char* _outpath; //the list of image filenames ImageList * _list; //the texture that holds loaded input image GLTexInput * _texImage; //the SiftPyramid SiftPyramid * _pyramid; //print out the command line options static void PrintUsage(); //Initialize OpenGL and SIFT paremeters, and create the shaders accordingly void InitSiftGPU(); //load the image list from a file void LoadImageList(const char *imlist); public: //timing results for 10 steps float _timing[10]; inline const char* GetCurrentImagePath() {return _imgpath; } public: //set the image list for processing SIFTGPU_EXPORT virtual void SetImageList(int nimage, const char** filelist); //get the number of SIFT features in current image SIFTGPU_EXPORT virtual int GetFeatureNum(); //save the SIFT result as a ANSCII/BINARY file SIFTGPU_EXPORT virtual void SaveSIFT(const char * szFileName); //Copy the SIFT result to two vectors SIFTGPU_EXPORT virtual void GetFeatureVector(SiftKeypoint * keys, float * descriptors); //Set keypoint list before running sift to get descriptors SIFTGPU_EXPORT virtual void SetKeypointList(int num, const SiftKeypoint * keys, int keys_have_orientation = 1); //Enable downloading results to CPU. //create a new OpenGL context for processing //call VerifyContextGL instead if you want to crate openGL context yourself, or your are //mixing mixing siftgpu with other openGL code SIFTGPU_EXPORT virtual int CreateContextGL(); //verify the current opengl context.. //(for example, you call wglmakecurrent yourself and verify the current context) SIFTGPU_EXPORT virtual int VerifyContextGL(); //check if all siftgpu functions are supported SIFTGPU_EXPORT virtual int IsFullSupported(); //set verbose mode SIFTGPU_EXPORT virtual void SetVerbose(int verbose = 4); //set SiftGPU to brief display mode, which is faster inline void SetVerboseBrief(){SetVerbose(2);}; //parse SiftGPU parameters SIFTGPU_EXPORT virtual void ParseParam(int argc, const char** argv); //run SIFT on a new image given filename SIFTGPU_EXPORT virtual int RunSIFT(const char * imgpath); //run SIFT on an image in the image list given the file index SIFTGPU_EXPORT virtual int RunSIFT(int index); //run SIFT on a new image given the pixel data and format/type; //gl_format (e.g. GL_LUMINANCE, GL_RGB) is the format of the pixel data //gl_type (e.g. GL_UNSIGNED_BYTE, GL_FLOAT) is the data type of the pixel data; //Check glTexImage2D(...format, type,...) for the accepted values //Using image data of GL_LUMINANCE + GL_UNSIGNED_BYTE can minimize transfer time SIFTGPU_EXPORT virtual int RunSIFT(int width, int height, const void * data, unsigned int gl_format, unsigned int gl_type); //run SIFT on current image (specified by arguments), or processing the current image again SIFTGPU_EXPORT virtual int RunSIFT(); //run SIFT with keypoints on current image again. SIFTGPU_EXPORT virtual int RunSIFT(int num, const SiftKeypoint * keys, int keys_have_orientation = 1); //constructor, the parameter np is ignored.. SIFTGPU_EXPORT explicit SiftGPU(int np = 1); //destructor SIFTGPU_EXPORT virtual ~SiftGPU(); //set the active pyramid...dropped function SIFTGPU_EXPORT virtual void SetActivePyramid(int) {}; //retrieve the number of images in the image list SIFTGPU_EXPORT virtual int GetImageCount(); //set parameter GlobalUtil::_ForceTightPyramid SIFTGPU_EXPORT virtual void SetTightPyramid(int tight = 1); //allocate pyramid for a given size of image SIFTGPU_EXPORT virtual int AllocatePyramid(int width, int height); //none of the texture in processing can be larger //automatic down-sample is used if necessary. SIFTGPU_EXPORT virtual void SetMaxDimension(int sz); SIFTGPU_EXPORT int GetFeatureCountThreshold(); SIFTGPU_EXPORT int GetMaxOrientation(); SIFTGPU_EXPORT int GetMaxDimension(); }; //////////////////////////////////////////////////////////////// //class SIftGPUEX //description: adds some visualization functions to the interface of SiftGPU //////////////////////////////////////////////////////////////// class SiftGPUEX:public SiftGPU { //view mode int _view; //sub view mode int _sub_view; //whether display a debug view int _view_debug; //colors for SIFT feature display enum{COLOR_NUM = 36}; float _colors[COLOR_NUM*3]; //display functions void DisplayInput(); //display gray level image of input image void DisplayDebug(); //display debug view void DisplayFeatureBox(int i); //display SIFT features void DisplayLevel(void (*UseDisplayShader)(), int i); //display one level image void DisplayOctave(void (*UseDisplayShader)(), int i); //display all images in one octave //display different content of Pyramid by specifying different data and display shader //the first nskip1 levels and the last nskip2 levels are skiped in display void DisplayPyramid( void (*UseDisplayShader)(), int dataName, int nskip1 = 0, int nskip2 = 0); //use HSVtoRGB to generate random colors static void HSVtoRGB(float hsv[3],float rgb[3]); public: SIFTGPU_EXPORT SiftGPUEX(); //change view mode SIFTGPU_EXPORT void SetView(int view, int sub_view, char * title); //display current view SIFTGPU_EXPORT void DisplaySIFT(); //toggle debug mode on/off SIFTGPU_EXPORT void ToggleDisplayDebug(); //randomize the display colors SIFTGPU_EXPORT void RandomizeColor(); //retrieve the size of current input image SIFTGPU_EXPORT void GetImageDimension(int &w, int&h); //get the location of the window specified by user SIFTGPU_EXPORT void GetInitWindowPotition(int& x, int& y); }; ///matcher export //This is a gpu-based sift match implementation. class SiftMatchGPU { public: enum SIFTMATCH_LANGUAGE { SIFTMATCH_SAME_AS_SIFTGPU = 0, //when siftgpu already initialized. SIFTMATCH_GLSL = 2, SIFTMATCH_CUDA = 3, SIFTMATCH_CUDA_DEVICE0 = 3 //to use device i, use SIFTMATCH_CUDA_DEVICE0 + i }; int gpu_index = 0; private: int __language; SiftMatchGPU * __matcher; virtual void InitSiftMatch(){} protected: int __max_sift; //move the two functions here for derived class SIFTGPU_EXPORT virtual int _CreateContextGL(); SIFTGPU_EXPORT virtual int _VerifyContextGL(); public: //OpenGL Context creation/verification, initialization is done automatically inside inline int CreateContextGL() {return _CreateContextGL();} inline int VerifyContextGL() {return _VerifyContextGL();} //Consructor, the argument specifies the maximum number of features to match SIFTGPU_EXPORT explicit SiftMatchGPU(int max_sift = 4096); //change gpu_language, check the enumerants in SIFTMATCH_LANGUAGE. SIFTGPU_EXPORT virtual void SetLanguage(int gpu_language); //after calling SetLanguage, you can call SetDeviceParam to select GPU //-winpos, -display, -cuda [device_id] //This is only used when you call CreateContextGL.. //This function doesn't change the language. SIFTGPU_EXPORT virtual void SetDeviceParam(int argc, char**argv); // Allocate all matrices the matrices and return true if successful. virtual bool Allocate(int max_sift, int mbm); //change the maximum of features to match whenever you want SIFTGPU_EXPORT virtual void SetMaxSift(int max_sift); SIFTGPU_EXPORT virtual int GetMaxSift() const { return __max_sift; }; //desctructor SIFTGPU_EXPORT virtual ~SiftMatchGPU(); //Specifiy descriptors to match, index = [0/1] for two features sets respectively //Option1, use float descriptors, and they be already normalized to 1.0 SIFTGPU_EXPORT virtual void SetDescriptors(int index, int num, const float* descriptors, int id = -1); //Option 2 unsigned char descriptors. They must be already normalized to 512 SIFTGPU_EXPORT virtual void SetDescriptors(int index, int num, const unsigned char * descriptors, int id = -1); //match two sets of features, the function RETURNS the number of matches. //Given two normalized descriptor d1,d2, the distance here is acos(d1 *d2); SIFTGPU_EXPORT virtual int GetSiftMatch( int max_match, // the length of the match_buffer. uint32_t match_buffer[][2], //buffer to receive the matched feature indices float distmax = 0.7, //maximum distance of sift descriptor float ratiomax = 0.8, //maximum distance ratio int mutual_best_match = 1); //mutual best match or one way //two functions for guded matching, two constraints can be used //one homography and one fundamental matrix, the use is as follows //1. for each image, first call SetDescriptor then call SetFeatureLocation //2. Call GetGuidedSiftMatch //input feature location is a vector of [float x, float y, float skip[gap]] SIFTGPU_EXPORT virtual void SetFeautreLocation(int index, const float* locations, int gap = 0); inline void SetFeatureLocation(int index, const SiftGPU::SiftKeypoint * keys) { SetFeautreLocation(index, (const float*) keys, 2); } //use a guiding Homography H and a guiding Fundamental Matrix F to compute feature matches //the function returns the number of matches. SIFTGPU_EXPORT virtual int GetGuidedSiftMatch( int max_match, uint32_t match_buffer[][2], //buffer to recieve float* H, //homography matrix, (Set NULL to skip) float* F, //fundamental matrix, (Set NULL to skip) float distmax = 0.7, //maximum distance of sift descriptor float ratiomax = 0.8, //maximum distance ratio float hdistmax = 32, //threshold for |H * x1 - x2|_2 float fdistmax = 16, //threshold for sampson error of x2'FX1 int mutual_best_match = 1); //mutual best or one wayx }; typedef SiftGPU::SiftKeypoint SiftKeypoint; //Two exported global functions used to create SiftGPU and SiftMatchGPU SIFTGPU_EXPORT_EXTERN SiftGPU * CreateNewSiftGPU(int np =1); SIFTGPU_EXPORT_EXTERN SiftMatchGPU* CreateNewSiftMatchGPU(int max_sift = 4096); //////////////////////////////////////////////////////////////////////////// class ComboSiftGPU: public SiftGPU, public SiftMatchGPU { }; SIFTGPU_EXPORT_EXTERN ComboSiftGPU* CreateComboSiftGPU(); ///////////////////////////////////////////////////////////////////////////////////////////// //Multi-process mode and remote mode SIFTGPU_EXPORT_EXTERN ComboSiftGPU* CreateRemoteSiftGPU(int port = 7777, char* remote_server = NULL); //Run SiftGPU computation on a remote computer/process/thread //if( remote_server == NULL) // a local server is created in a different process and connected // multiple-GPU can be used by creating multiple instances // GPU selection done through SiftGPU::ParseParam function //otherwise, // Assumes the existenc of a remote server and connects to it // GPU selection skipped if already done on the server-end // RUN server: server_siftgpu -server port [siftgpu_param] //example: // ComboSiftGPU * combo = CreateRemoteSiftGPU(7777, "my.gpuserver.com"); // SiftGPU* siftgpu = combo, SiftMatchGPU * matcher = combo; // siftgpu->ParseParam... siftgpu->CreateContextGL.. // matcher->SetLanguage...matcher->VerifyContextGL... // // GPU-selection is done throught siftgpu->ParseParam, // // it doesn't really initialize SiftGPU untill you call CreateContextGL/VerifyContextGL // delete combo; //////////////////////////////////////////////////////////////////////// //two internally used function. SIFTGPU_EXPORT int CreateLiteWindow(LiteWindow* window); SIFTGPU_EXPORT void RunServerLoop(int port, int argc, char** argv); #endif colmap-3.9.1/src/thirdparty/SiftGPU/SiftMatch.cpp000066400000000000000000000522141454702036400216400ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftMatch.cpp // Author: Changchang Wu // Description : implementation of SiftMatchGPU and SiftMatchGL // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include #include #include #include using namespace std; #include #include "GlobalUtil.h" #include "ProgramGLSL.h" #include "GLTexImage.h" #include "SiftGPU.h" #include "SiftMatch.h" #include "FrameBufferObject.h" #if defined(CUDA_SIFTGPU_ENABLED) #include "CuTexImage.h" #include "SiftMatchCU.h" #endif SiftMatchGL::SiftMatchGL(int max_sift, int use_glsl): SiftMatchGPU() { s_multiply = s_col_max = s_row_max = s_guided_mult = NULL; _num_sift[0] = _num_sift[1] = 0; _id_sift[0] = _id_sift[1] = 0; _have_loc[0] = _have_loc[1] = 0; __max_sift = max_sift <=0 ? 4096 : ((max_sift + 31)/ 32 * 32) ; _pixel_per_sift = 32; //must be 32 _sift_num_stripe = 1; _sift_per_stripe = 1; _sift_per_row = _sift_per_stripe * _sift_num_stripe; _initialized = 0; } SiftMatchGL::~SiftMatchGL() { if(s_multiply) delete s_multiply; if(s_guided_mult) delete s_guided_mult; if(s_col_max) delete s_col_max; if(s_row_max) delete s_row_max; } bool SiftMatchGL::Allocate(int max_sift, int mbm) { SetMaxSift(max_sift); return glGetError() == GL_NO_ERROR; } void SiftMatchGL::SetMaxSift(int max_sift) { max_sift = ((max_sift + 31)/32)*32; if(max_sift > GlobalUtil::_texMaxDimGL) max_sift = GlobalUtil::_texMaxDimGL; if(max_sift > __max_sift) { __max_sift = max_sift; AllocateSiftMatch(); _have_loc[0] = _have_loc[1] = 0; _id_sift[0] = _id_sift[1] = -1; _num_sift[0] = _num_sift[1] = 1; }else { __max_sift = max_sift; } } void SiftMatchGL::AllocateSiftMatch() { //parameters, number of sift is limited by the texture size if(__max_sift > GlobalUtil::_texMaxDimGL) __max_sift = GlobalUtil::_texMaxDimGL; /// int h = __max_sift / _sift_per_row; int n = (GlobalUtil::_texMaxDimGL + h - 1) / GlobalUtil::_texMaxDimGL; if ( n > 1) {_sift_num_stripe *= n; _sift_per_row *= n; } //initialize _texDes[0].InitTexture(_sift_per_row * _pixel_per_sift, __max_sift / _sift_per_row, 0,GL_RGBA8); _texDes[1].InitTexture(_sift_per_row * _pixel_per_sift, __max_sift / _sift_per_row, 0, GL_RGBA8); _texLoc[0].InitTexture(_sift_per_row , __max_sift / _sift_per_row, 0); _texLoc[1].InitTexture(_sift_per_row , __max_sift / _sift_per_row, 0); if(GlobalUtil::_SupportNVFloat || GlobalUtil::_SupportTextureRG) { //use single-component texture to save memory #ifndef GL_R32F #define GL_R32F 0x822E #endif GLuint format = GlobalUtil::_SupportNVFloat ? GL_FLOAT_R_NV : GL_R32F; _texDot.InitTexture(__max_sift, __max_sift, 0, format); _texMatch[0].InitTexture(16, __max_sift / 16, 0, format); _texMatch[1].InitTexture(16, __max_sift / 16, 0, format); }else { _texDot.InitTexture(__max_sift, __max_sift, 0); _texMatch[0].InitTexture(16, __max_sift / 16, 0); _texMatch[1].InitTexture(16, __max_sift / 16, 0); } } void SiftMatchGL::InitSiftMatch() { if(_initialized) return; GlobalUtil::InitGLParam(0); if(GlobalUtil::_GoodOpenGL == 0) return; AllocateSiftMatch(); LoadSiftMatchShadersGLSL(); _initialized = 1; } void SiftMatchGL::SetDescriptors(int index, int num, const unsigned char* descriptors, int id) { if(_initialized == 0) return; if (index > 1) index = 1; if (index < 0) index = 0; _have_loc[index] = 0; //the same feature is already set if(id !=-1 && id == _id_sift[index]) return ; _id_sift[index] = id; if(num > __max_sift) num = __max_sift; sift_buffer.resize(num * 128 /4); memcpy(&sift_buffer[0], descriptors, 128 * num); _num_sift[index] = num; int w = _sift_per_row * _pixel_per_sift; int h = (num + _sift_per_row - 1)/ _sift_per_row; sift_buffer.resize(w * h * 4, 0); _texDes[index].SetImageSize(w , h); _texDes[index].BindTex(); if(_sift_num_stripe == 1) { glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, &sift_buffer[0]); }else { for(int i = 0; i < _sift_num_stripe; ++i) { int ws = _sift_per_stripe * _pixel_per_sift; int x = i * ws; int pos = i * ws * h * 4; glTexSubImage2D(GlobalUtil::_texTarget, 0, x, 0, ws, h, GL_RGBA, GL_UNSIGNED_BYTE, &sift_buffer[pos]); } } _texDes[index].UnbindTex(); } void SiftMatchGL::SetFeautreLocation(int index, const float* locations, int gap) { if(_num_sift[index] <=0) return; int w = _sift_per_row ; int h = (_num_sift[index] + _sift_per_row - 1)/ _sift_per_row; sift_buffer.resize(_num_sift[index] * 2); if(gap == 0) { memcpy(&sift_buffer[0], locations, _num_sift[index] * 2 * sizeof(float)); }else { for(int i = 0; i < _num_sift[index]; ++i) { sift_buffer[i*2] = *locations++; sift_buffer[i*2+1]= *locations ++; locations += gap; } } sift_buffer.resize(w * h * 2, 0); _texLoc[index].SetImageSize(w , h); _texLoc[index].BindTex(); if(_sift_num_stripe == 1) { glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, w, h, GL_LUMINANCE_ALPHA , GL_FLOAT , &sift_buffer[0]); }else { for(int i = 0; i < _sift_num_stripe; ++i) { int ws = _sift_per_stripe; int x = i * ws; int pos = i * ws * h * 2; glTexSubImage2D(GlobalUtil::_texTarget, 0, x, 0, ws, h, GL_LUMINANCE_ALPHA , GL_FLOAT, &sift_buffer[pos]); } } _texLoc[index].UnbindTex(); _have_loc[index] = 1; } void SiftMatchGL::SetDescriptors(int index, int num, const float* descriptors, int id) { if(_initialized == 0) return; if (index > 1) index = 1; if (index < 0) index = 0; _have_loc[index] = 0; //the same feature is already set if(id !=-1 && id == _id_sift[index]) return ; _id_sift[index] = id; if(num > __max_sift) num = __max_sift; sift_buffer.resize(num * 128 /4); unsigned char * pub = (unsigned char*) &sift_buffer[0]; for(int i = 0; i < 128 * num; ++i) { pub[i] = int(512 * descriptors[i] + 0.5); } _num_sift[index] = num; int w = _sift_per_row * _pixel_per_sift; int h = (num + _sift_per_row - 1)/ _sift_per_row; sift_buffer.resize(w * h * 4, 0); _texDes[index].SetImageSize(w, h); _texDes[index].BindTex(); if(_sift_num_stripe == 1) { glTexSubImage2D(GlobalUtil::_texTarget, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, &sift_buffer[0]); }else { for(int i = 0; i < _sift_num_stripe; ++i) { int ws = _sift_per_stripe * _pixel_per_sift; int x = i * ws; int pos = i * ws * h * 4; glTexSubImage2D(GlobalUtil::_texTarget, 0, x, 0, ws, h, GL_RGBA, GL_UNSIGNED_BYTE, &sift_buffer[pos]); } } _texDes[index].UnbindTex(); } void SiftMatchGL::LoadSiftMatchShadersGLSL() { ProgramGLSL * program; ostringstream out; if(GlobalUtil::_IsNvidia) out << "#pragma optionNV(ifcvt none)\n" "#pragma optionNV(unroll all)\n"; out << "#define SIFT_PER_STRIPE " << _sift_per_stripe << ".0\n" "#define PIXEL_PER_SIFT " << _pixel_per_sift << "\n" "uniform sampler2DRect tex1, tex2; uniform vec2 size;\n" "void main() \n" "{\n" << " vec4 val = vec4(0.0, 0.0, 0.0, 0.0), data1, buf;\n" " vec2 index = gl_FragCoord.yx; \n" " vec2 stripe_size = size.xy * SIFT_PER_STRIPE;\n" " vec2 temp_div1 = index / stripe_size;\n" " vec2 stripe_index = floor(temp_div1);\n" " index = floor(stripe_size * (temp_div1 - stripe_index));\n" " vec2 temp_div2 = index * vec2(1.0 / float(SIFT_PER_STRIPE));\n" " vec2 temp_floor2 = floor(temp_div2);\n" " vec2 index_v = temp_floor2 + vec2(0.5);\n " " vec2 index_h = vec2(SIFT_PER_STRIPE)* (temp_div2 - temp_floor2);\n" " vec2 tx = (index_h + stripe_index * vec2(SIFT_PER_STRIPE))* vec2(PIXEL_PER_SIFT) + 0.5;\n" " vec2 tpos1, tpos2; \n" " vec4 tpos = vec4(tx, index_v);\n" ////////////////////////////////////////////////////// " for(int i = 0; i < PIXEL_PER_SIFT; ++i){\n" " buf = texture2DRect(tex2, tpos.yw);\n" " data1 = texture2DRect(tex1, tpos.xz);\n" " val += (data1 * buf);\n" " tpos.xy = tpos.xy + vec2(1.0, 1.0);\n" " }\n" " const float factor = 0.248050689697265625; \n" " gl_FragColor =vec4(dot(val, vec4(factor)), index, 0);\n" "}" << '\0'; s_multiply = program= new ProgramGLSL(out.str().c_str()); _param_multiply_tex1 = glGetUniformLocation(*program, "tex1"); _param_multiply_tex2 = glGetUniformLocation(*program, "tex2"); _param_multiply_size = glGetUniformLocation(*program, "size"); out.seekp(ios::beg); if(GlobalUtil::_IsNvidia) out << "#pragma optionNV(ifcvt none)\n" "#pragma optionNV(unroll all)\n"; out << "#define SIFT_PER_STRIPE " << _sift_per_stripe << ".0\n" "#define PIXEL_PER_SIFT " << _pixel_per_sift << "\n" "uniform sampler2DRect tex1, tex2;\n" "uniform sampler2DRect texL1;\n" "uniform sampler2DRect texL2; \n" "uniform mat3 H; \n" "uniform mat3 F; \n" "uniform vec4 size; \n" "void main() \n" "{\n" << " vec4 val = vec4(0.0, 0.0, 0.0, 0.0), data1, buf;\n" " vec2 index = gl_FragCoord.yx; \n" " vec2 stripe_size = size.xy * SIFT_PER_STRIPE;\n" " vec2 temp_div1 = index / stripe_size;\n" " vec2 stripe_index = floor(temp_div1);\n" " index = floor(stripe_size * (temp_div1 - stripe_index));\n" " vec2 temp_div2 = index * vec2(1.0/ float(SIFT_PER_STRIPE));\n" " vec2 temp_floor2 = floor(temp_div2);\n" " vec2 index_v = temp_floor2 + vec2(0.5);\n " " vec2 index_h = vec2(SIFT_PER_STRIPE)* (temp_div2 - temp_floor2);\n" //read feature location data " vec4 tlpos = vec4((index_h + stripe_index * vec2(SIFT_PER_STRIPE)) + 0.5, index_v);\n" " vec3 loc1 = vec3(texture2DRect(texL1, tlpos.xz).xw, 1.0);\n" " vec3 loc2 = vec3(texture2DRect(texL2, tlpos.yw).xw, 1.0);\n" //check the guiding homography " vec3 hxloc1 = H* loc1;\n" " vec2 diff = loc2.xy- (hxloc1.xy/hxloc1.z);\n" " float disth = diff.x * diff.x + diff.y * diff.y;\n" " if(disth > size.z ) {gl_FragColor = vec4(0.0, index, 0.0); return;}\n" //check the guiding fundamental " vec3 fx1 = (F * loc1), ftx2 = (loc2 * F);\n" " float x2tfx1 = dot(loc2, fx1);\n" " vec4 temp = vec4(fx1.xy, ftx2.xy); \n" " float sampson_error = (x2tfx1 * x2tfx1) / dot(temp, temp);\n" " if(sampson_error > size.w) {gl_FragColor = vec4(0.0, index, 0.0); return;}\n" //compare feature descriptor " vec2 tx = (index_h + stripe_index * SIFT_PER_STRIPE)* vec2(PIXEL_PER_SIFT) + 0.5;\n" " vec2 tpos1, tpos2; \n" " vec4 tpos = vec4(tx, index_v);\n" " for(int i = 0; i < PIXEL_PER_SIFT; ++i){\n" " buf = texture2DRect(tex2, tpos.yw);\n" " data1 = texture2DRect(tex1, tpos.xz);\n" " val += data1 * buf;\n" " tpos.xy = tpos.xy + vec2(1.0, 1.0);\n" " }\n" " const float factor = 0.248050689697265625; \n" " gl_FragColor =vec4(dot(val, vec4(factor)), index, 0.0);\n" "}" << '\0'; s_guided_mult = program= new ProgramGLSL(out.str().c_str()); _param_guided_mult_tex1 = glGetUniformLocation(*program, "tex1"); _param_guided_mult_tex2= glGetUniformLocation(*program, "tex2"); _param_guided_mult_texl1 = glGetUniformLocation(*program, "texL1"); _param_guided_mult_texl2 = glGetUniformLocation(*program, "texL2"); _param_guided_mult_h = glGetUniformLocation(*program, "H"); _param_guided_mult_f = glGetUniformLocation(*program, "F"); _param_guided_mult_param = glGetUniformLocation(*program, "size"); //row max out.seekp(ios::beg); out << "#define BLOCK_WIDTH 16.0\n" "uniform sampler2DRect tex; uniform vec3 param;\n" "void main ()\n" "{\n" " float index = gl_FragCoord.x + floor(gl_FragCoord.y) * BLOCK_WIDTH; \n" " vec2 bestv = vec2(-1.0); float imax = -1.0;\n" " for(float i = 0.0; i < param.x; i ++){\n " " float v = texture2DRect(tex, vec2(i + 0.5, index)).r; \n" " imax = v > bestv.r ? i : imax; \n " " bestv = v > bestv.r? vec2(v, bestv.r) : max(bestv, vec2(v));\n " " }\n" " bestv = acos(min(bestv, 1.0));\n" " if(bestv.x >= param.y || bestv.x >= param.z * bestv.y) imax = -1.0;\n" " gl_FragColor = vec4(imax, bestv, index);\n" "}" << '\0'; s_row_max = program= new ProgramGLSL(out.str().c_str()); _param_rowmax_param = glGetUniformLocation(*program, "param"); out.seekp(ios::beg); out << "#define BLOCK_WIDTH 16.0\n" "uniform sampler2DRect tex; uniform vec3 param;\n" "void main ()\n" "{\n" " float index = gl_FragCoord.x + floor(gl_FragCoord.y) * BLOCK_WIDTH; \n" " vec2 bestv = vec2(-1.0); float imax = -1.0;\n" " for(float i = 0.0; i < param.x; i ++){\n " " float v = texture2DRect(tex, vec2(index, i + 0.5)).r; \n" " imax = (v > bestv.r)? i : imax; \n " " bestv = v > bestv.r? vec2(v, bestv.r) : max(bestv, vec2(v));\n " " }\n" " bestv = acos(min(bestv, 1.0));\n" " if(bestv.x >= param.y || bestv.x >= param.z * bestv.y) imax = -1.0;\n" " gl_FragColor = vec4(imax, bestv, index);\n" "}" << '\0'; s_col_max = program =new ProgramGLSL(out.str().c_str()); _param_colmax_param = glGetUniformLocation(*program, "param"); } int SiftMatchGL::GetGuidedSiftMatch(int max_match, uint32_t match_buffer[][2], float* H, float* F, float distmax, float ratiomax, float hdistmax, float fdistmax, int mbm) { int dw = _num_sift[1]; int dh = _num_sift[0]; if(_initialized ==0) return 0; if(dw <= 0 || dh <=0) return 0; if(_have_loc[0] == 0 || _have_loc[1] == 0) return 0; FrameBufferObject fbo; glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); _texDot.SetImageSize(dw, dh); //data _texDot.AttachToFBO(0); _texDot.FitTexViewPort(); glActiveTexture(GL_TEXTURE0); _texDes[0].BindTex(); glActiveTexture(GL_TEXTURE1); _texDes[1].BindTex(); glActiveTexture(GL_TEXTURE2); _texLoc[0].BindTex(); glActiveTexture(GL_TEXTURE3); _texLoc[1].BindTex(); //multiply the descriptor matrices s_guided_mult->UseProgram(); //set parameters glsl float dot_param[4] = {(float)_texDes[0].GetDrawHeight(), (float) _texDes[1].GetDrawHeight(), hdistmax, fdistmax}; glUniform1i(_param_guided_mult_tex1, 0); glUniform1i(_param_guided_mult_tex2, 1); glUniform1i(_param_guided_mult_texl1, 2); glUniform1i(_param_guided_mult_texl2, 3); glUniformMatrix3fv(_param_guided_mult_h, 1, GL_TRUE, H); glUniformMatrix3fv(_param_guided_mult_f, 1, GL_TRUE, F); glUniform4fv(_param_guided_mult_param, 1, dot_param); _texDot.DrawQuad(); GLTexImage::UnbindMultiTex(4); return GetBestMatch(max_match, match_buffer, distmax, ratiomax, mbm); } int SiftMatchGL::GetBestMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm) { glActiveTexture(GL_TEXTURE0); _texDot.BindTex(); //readback buffer sift_buffer.resize(_num_sift[0] + _num_sift[1] + 16); float * buffer1 = &sift_buffer[0], * buffer2 = &sift_buffer[_num_sift[0]]; //row max _texMatch[0].AttachToFBO(0); _texMatch[0].SetImageSize(16, ( _num_sift[0] + 15) / 16); _texMatch[0].FitTexViewPort(); ///set parameter glsl s_row_max->UseProgram(); glUniform3f(_param_rowmax_param, (float)_num_sift[1], distmax, ratiomax); _texMatch[0].DrawQuad(); glReadPixels(0, 0, 16, (_num_sift[0] + 15)/16, GL_RED, GL_FLOAT, buffer1); //col max if(mbm) { _texMatch[1].AttachToFBO(0); _texMatch[1].SetImageSize(16, (_num_sift[1] + 15) / 16); _texMatch[1].FitTexViewPort(); //set parameter glsl s_col_max->UseProgram(); glUniform3f(_param_rowmax_param, (float)_num_sift[0], distmax, ratiomax); _texMatch[1].DrawQuad(); glReadPixels(0, 0, 16, (_num_sift[1] + 15) / 16, GL_RED, GL_FLOAT, buffer2); } //unload glUseProgram(0); GLTexImage::UnbindMultiTex(2); GlobalUtil::CleanupOpenGL(); //write back the matches int nmatch = 0, j ; for(int i = 0; i < _num_sift[0] && nmatch < max_match; ++i) { j = int(buffer1[i]); if( j>= 0 && (!mbm ||int(buffer2[j]) == i)) { match_buffer[nmatch][0] = i; match_buffer[nmatch][1] = j; nmatch++; } } const GLenum error_code(glGetError()); if (error_code != GL_NO_ERROR) { return -1; } return nmatch; } int SiftMatchGL::GetSiftMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm) { int dw = _num_sift[1]; int dh = _num_sift[0]; if(_initialized ==0) return 0; if(dw <= 0 || dh <=0) return 0; FrameBufferObject fbo; glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); _texDot.SetImageSize(dw, dh); //data _texDot.AttachToFBO(0); _texDot.FitTexViewPort(); glActiveTexture(GL_TEXTURE0); _texDes[0].BindTex(); glActiveTexture(GL_TEXTURE1); _texDes[1].BindTex(); ////////////////// //multiply the descriptor matrices s_multiply->UseProgram(); //set parameters float heights[2] = {(float)_texDes[0].GetDrawHeight(), (float)_texDes[1].GetDrawHeight()}; glUniform1i(_param_multiply_tex1, 0); glUniform1i(_param_multiply_tex2 , 1); glUniform2fv(_param_multiply_size, 1, heights); _texDot.DrawQuad(); glActiveTexture(GL_TEXTURE1); glBindTexture(GlobalUtil::_texTarget, 0); return GetBestMatch(max_match, match_buffer, distmax, ratiomax, mbm); } int SiftMatchGPU::_CreateContextGL() { //Create an OpenGL Context? if (__language >= SIFTMATCH_CUDA) {} else if(!GlobalUtil::CreateWindowEZ()) { #if CUDA_SIFTGPU_ENABLED __language = SIFTMATCH_CUDA; #else return 0; #endif } return VerifyContextGL(); } int SiftMatchGPU::_VerifyContextGL() { if(__matcher) return GlobalUtil::_GoodOpenGL; #ifdef CUDA_SIFTGPU_ENABLED if(__language >= SIFTMATCH_CUDA) {} else if(__language == SIFTMATCH_SAME_AS_SIFTGPU && GlobalUtil::_UseCUDA){} else GlobalUtil::InitGLParam(0); if(GlobalUtil::_GoodOpenGL == 0) __language = SIFTMATCH_CUDA; if(((__language == SIFTMATCH_SAME_AS_SIFTGPU && GlobalUtil::_UseCUDA) || __language >= SIFTMATCH_CUDA) && SiftMatchCU::CheckCudaDevice (GlobalUtil::_DeviceIndex)) { __language = SIFTMATCH_CUDA; __matcher = ::new SiftMatchCU(__max_sift); }else #else if((__language == SIFTMATCH_SAME_AS_SIFTGPU && GlobalUtil::_UseCUDA) || __language >= SIFTMATCH_CUDA) { std::cerr << "---------------------------------------------------------------------------\n" << "CUDA not supported in this binary! To enable it, please use SiftGPU_CUDA_Enable\n" << "Project for VS2005+ or set siftgpu_enable_cuda to 1 in makefile\n" << "----------------------------------------------------------------------------\n"; } #endif { __language = SIFTMATCH_GLSL; __matcher = ::new SiftMatchGL(__max_sift, 1); } if(GlobalUtil::_verbose) std::cout << "[SiftMatchGPU]: " << (__language == SIFTMATCH_CUDA? "CUDA" : "GLSL") <<"\n\n"; __matcher->InitSiftMatch(); return GlobalUtil::_GoodOpenGL; } SiftMatchGPU::SiftMatchGPU(int max_sift) { __max_sift = max(max_sift, 1024); __language = 0; __matcher = NULL; } void SiftMatchGPU::SetLanguage(int language) { if(__matcher) return; //////////////////////// #ifdef CUDA_SIFTGPU_ENABLED if(language >= SIFTMATCH_CUDA) GlobalUtil::_DeviceIndex = language - SIFTMATCH_CUDA; #endif __language = language > SIFTMATCH_CUDA ? SIFTMATCH_CUDA : language; } void SiftMatchGPU::SetDeviceParam(int argc, char**argv) { if(__matcher) return; GlobalUtil::SetDeviceParam(argc, argv); } bool SiftMatchGPU::Allocate(int max_sift, int mbm) { if(__matcher) { const bool success = __matcher->Allocate(max_sift, mbm); __max_sift = __matcher->__max_sift; return success; } return false; } void SiftMatchGPU::SetMaxSift(int max_sift) { if(__matcher) { __matcher->SetMaxSift(max(128, max_sift)); __max_sift = __matcher->__max_sift; } else { __max_sift = max(128, max_sift); } } SiftMatchGPU::~SiftMatchGPU() { if(__matcher) delete __matcher; } void SiftMatchGPU::SetDescriptors(int index, int num, const unsigned char* descriptors, int id) { __matcher->SetDescriptors(index, num, descriptors, id); } void SiftMatchGPU::SetDescriptors(int index, int num, const float* descriptors, int id) { __matcher->SetDescriptors(index, num, descriptors, id); } void SiftMatchGPU::SetFeautreLocation(int index, const float* locations, int gap) { __matcher->SetFeautreLocation(index, locations, gap); } int SiftMatchGPU::GetGuidedSiftMatch(int max_match, uint32_t match_buffer[][2], float* H, float* F, float distmax, float ratiomax, float hdistmax, float fdistmax, int mutual_best_match) { if(H == NULL && F == NULL) { return __matcher->GetSiftMatch(max_match, match_buffer, distmax, ratiomax, mutual_best_match); }else { float Z[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1}, ti = (1.0e+20F); return __matcher->GetGuidedSiftMatch(max_match, match_buffer, H? H : Z, F? F : Z, distmax, ratiomax, H? hdistmax: ti, F? fdistmax: ti, mutual_best_match); } } int SiftMatchGPU::GetSiftMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mutual_best_match) { return __matcher->GetSiftMatch(max_match, match_buffer, distmax, ratiomax, mutual_best_match); } SiftMatchGPU* CreateNewSiftMatchGPU(int max_sift) { return new SiftMatchGPU(max_sift); } colmap-3.9.1/src/thirdparty/SiftGPU/SiftMatch.h000066400000000000000000000054761454702036400213150ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftMatch.h // Author: Changchang Wu // Description : interface for the SiftMatchGL //// // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef GPU_SIFT_MATCH_H #define GPU_SIFT_MATCH_H class GLTexImage; class ProgramGPU; class SiftMatchGL:public SiftMatchGPU { typedef GLint ParameterGL; private: //tex storage GLTexImage _texLoc[2]; GLTexImage _texDes[2]; GLTexImage _texDot; GLTexImage _texMatch[2]; //programs ProgramGPU * s_multiply; ProgramGPU * s_guided_mult; ProgramGPU * s_col_max; ProgramGPU * s_row_max; //matching parameters ParameterGL _param_multiply_tex1; ParameterGL _param_multiply_tex2; ParameterGL _param_multiply_size; ParameterGL _param_rowmax_param; ParameterGL _param_colmax_param; ///guided matching ParameterGL _param_guided_mult_tex1; ParameterGL _param_guided_mult_tex2; ParameterGL _param_guided_mult_texl1; ParameterGL _param_guided_mult_texl2; ParameterGL _param_guided_mult_h; ParameterGL _param_guided_mult_f; ParameterGL _param_guided_mult_param; // int _num_sift[2]; int _id_sift[2]; int _have_loc[2]; //gpu parameter int _sift_per_stripe; int _sift_num_stripe; int _sift_per_row; int _pixel_per_sift; int _initialized; // vector sift_buffer; private: void AllocateSiftMatch(); void LoadSiftMatchShadersGLSL(); int GetBestMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm); public: SiftMatchGL(int max_sift, int use_glsl); virtual ~SiftMatchGL(); public: bool Allocate(int max_sift, int mbm) override; void InitSiftMatch(); void SetMaxSift(int max_sift) override; void SetDescriptors(int index, int num, const unsigned char * descriptor, int id = -1); void SetDescriptors(int index, int num, const float * descriptor, int id = -1); void SetFeautreLocation(int index, const float* locatoins, int gap); int GetSiftMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm); int GetGuidedSiftMatch(int max_match, uint32_t match_buffer[][2], float* H, float* F, float distmax, float ratiomax, float hdistmax,float fdistmax, int mbm); }; #endif colmap-3.9.1/src/thirdparty/SiftGPU/SiftMatchCU.cpp000066400000000000000000000145171454702036400220740ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftMatchCU.cpp // Author: Changchang Wu // Description : implementation of the SiftMatchCU class. // CUDA-based implementation of SiftMatch // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that // the above copyright notice and the following paragraph appear in all // copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #if defined(CUDA_SIFTGPU_ENABLED) #include "GL/glew.h" #include #include #include #include #include using namespace std; #include #include "CuTexImage.h" #include "GlobalUtil.h" #include "ProgramCU.h" #include "SiftGPU.h" #include "SiftMatchCU.h" #define MULT_TBLOCK_DIMX 128 #define MULT_TBLOCK_DIMY 1 #define MULT_BLOCK_DIMX (MULT_TBLOCK_DIMX) #define MULT_BLOCK_DIMY (8 * MULT_TBLOCK_DIMY) SiftMatchCU::SiftMatchCU(int max_sift) : SiftMatchGPU() { _num_sift[0] = _num_sift[1] = 0; _id_sift[0] = _id_sift[1] = 0; _have_loc[0] = _have_loc[1] = 0; __max_sift = max_sift <= 0 ? 4096 : ((max_sift + 31) / 32 * 32); _initialized = 0; } bool SiftMatchCU::Allocate(int max_sift, int mbm) { SetMaxSift(max_sift); for (int index = 0; index < 2; ++index) { if (!_texDes[index].InitTexture(8 * __max_sift, 1, 4) || !_texLoc[index].InitTexture(__max_sift, 1, 2)) { return false; } } if (!_texDot.InitTexture(__max_sift, __max_sift) || !_texMatch[0].InitTexture(__max_sift, 1)) { return false; } if (mbm) { const int cols = (__max_sift + MULT_BLOCK_DIMY - 1) / MULT_BLOCK_DIMY; if (!_texCRT.InitTexture(__max_sift, cols, 32) || !_texMatch[1].InitTexture(__max_sift, 1)) { return false; } } _num_sift[0] = __max_sift; _num_sift[1] = __max_sift; return true; } void SiftMatchCU::SetMaxSift(int max_sift) { max_sift = ((max_sift + 31) / 32) * 32; __max_sift = max_sift; } int SiftMatchCU::CheckCudaDevice(int device) { return ProgramCU::CheckCudaDevice(device); } void SiftMatchCU::InitSiftMatch() { if (_initialized) return; GlobalUtil::_GoodOpenGL = max(GlobalUtil::_GoodOpenGL, 1); _initialized = 1; } void SiftMatchCU::SetDescriptors(int index, int num, const unsigned char* descriptors, int id) { if (_initialized == 0) return; if (index > 1) index = 1; if (index < 0) index = 0; _have_loc[index] = 0; // the same feature is already set if (id != -1 && id == _id_sift[index]) return; _id_sift[index] = id; if (num > __max_sift) num = __max_sift; _num_sift[index] = num; _texDes[index].InitTexture(8 * num, 1, 4); _texDes[index].CopyFromHost((void*)descriptors); } void SiftMatchCU::SetDescriptors(int index, int num, const float* descriptors, int id) { if (_initialized == 0) return; if (index > 1) index = 1; if (index < 0) index = 0; if (num > __max_sift) num = __max_sift; sift_buffer.resize(num * 128 / 4); unsigned char* pub = (unsigned char*)&sift_buffer[0]; for (int i = 0; i < 128 * num; ++i) { pub[i] = int(512 * descriptors[i] + 0.5); } SetDescriptors(index, num, pub, id); } void SiftMatchCU::SetFeautreLocation(int index, const float* locations, int gap) { if (_num_sift[index] <= 0) return; _texLoc[index].InitTexture(_num_sift[index], 1, 2); if (gap == 0) { _texLoc[index].CopyFromHost(locations); } else { sift_buffer.resize(_num_sift[index] * 2); float* pbuf = (float*)(&sift_buffer[0]); for (int i = 0; i < _num_sift[index]; ++i) { pbuf[i * 2] = *locations++; pbuf[i * 2 + 1] = *locations++; locations += gap; } _texLoc[index].CopyFromHost(pbuf); } _have_loc[index] = 1; } int SiftMatchCU::GetGuidedSiftMatch(int max_match, uint32_t match_buffer[][2], float* H, float* F, float distmax, float ratiomax, float hdistmax, float fdistmax, int mbm) { if (_initialized == 0) return 0; if (_num_sift[0] <= 0 || _num_sift[1] <= 0) return 0; if (_have_loc[0] == 0 || _have_loc[1] == 0) return 0; ProgramCU::MultiplyDescriptorG(_texDes, _texDes + 1, _texLoc, _texLoc + 1, &_texDot, (mbm ? &_texCRT : NULL), H, hdistmax, F, fdistmax); return GetBestMatch(max_match, match_buffer, distmax, ratiomax, mbm); } int SiftMatchCU::GetSiftMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm) { if (_initialized == 0) return 0; if (_num_sift[0] <= 0 || _num_sift[1] <= 0) return 0; ProgramCU::MultiplyDescriptor(_texDes, _texDes + 1, &_texDot, (mbm ? &_texCRT : NULL)); return GetBestMatch(max_match, match_buffer, distmax, ratiomax, mbm); } int SiftMatchCU::GetBestMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm) { sift_buffer.resize(_num_sift[0] + _num_sift[1]); int *buffer1 = (int*)&sift_buffer[0], *buffer2 = (int*)&sift_buffer[_num_sift[0]]; _texMatch[0].InitTexture(_num_sift[0], 1); ProgramCU::GetRowMatch(&_texDot, _texMatch, distmax, ratiomax); _texMatch[0].CopyToHost(buffer1); if (mbm) { _texMatch[1].InitTexture(_num_sift[1], 1); ProgramCU::GetColMatch(&_texCRT, _texMatch + 1, distmax, ratiomax); _texMatch[1].CopyToHost(buffer2); } int nmatch = 0, j; for (int i = 0; i < _num_sift[0] && nmatch < max_match; ++i) { j = int(buffer1[i]); if (j >= 0 && (!mbm || int(buffer2[j]) == i)) { match_buffer[nmatch][0] = i; match_buffer[nmatch][1] = j; nmatch++; } } cudaError_t error = cudaGetLastError(); if (error != cudaSuccess) { return -1; } return nmatch; } #endif colmap-3.9.1/src/thirdparty/SiftGPU/SiftMatchCU.h000066400000000000000000000042561454702036400215400ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftMatchCU.h // Author: Changchang Wu // Description : interface for the SiftMatchCU //// // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef CU_SIFT_MATCH_H #define CU_SIFT_MATCH_H #if defined(CUDA_SIFTGPU_ENABLED) class CuTexImage; class SiftMatchCU:public SiftMatchGPU { private: //tex storage CuTexImage _texLoc[2]; CuTexImage _texDes[2]; CuTexImage _texDot; CuTexImage _texMatch[2]; CuTexImage _texCRT; //programs // int _num_sift[2]; int _id_sift[2]; int _have_loc[2]; //gpu parameter int _initialized; vector sift_buffer; private: int GetBestMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm); public: SiftMatchCU(int max_sift); virtual ~SiftMatchCU(){}; void InitSiftMatch(); bool Allocate(int max_sift, int mbm) override; void SetMaxSift(int max_sift) override; void SetDescriptors(int index, int num, const unsigned char * descriptor, int id = -1); void SetDescriptors(int index, int num, const float * descriptor, int id = -1); void SetFeautreLocation(int index, const float* locatoins, int gap); int GetSiftMatch(int max_match, uint32_t match_buffer[][2], float distmax, float ratiomax, int mbm); int GetGuidedSiftMatch(int max_match, uint32_t match_buffer[][2], float* H, float* F, float distmax, float ratiomax, float hdistmax, float fdistmax, int mbm); ////////////////////////////// static int CheckCudaDevice(int device); }; #endif #endif colmap-3.9.1/src/thirdparty/SiftGPU/SiftPyramid.cpp000066400000000000000000000251661454702036400222170ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftPyramid.cpp // Author: Changchang Wu // Description : Implementation of the SiftPyramid class. // // // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #include "GL/glew.h" #include #include #include #include #include #include #include using namespace std; #include "GlobalUtil.h" #include "SiftPyramid.h" #include "SiftGPU.h" #ifdef DEBUG_SIFTGPU #include "IL/il.h" #include "direct.h" #include "io.h" #include #endif void SiftPyramid::RunSIFT(GLTexInput*input) { CleanupBeforeSIFT(); if(_existing_keypoints & SIFT_SKIP_FILTERING) { }else { GlobalUtil::StartTimer("Build Pyramid"); BuildPyramid(input); GlobalUtil::StopTimer(); _timing[0] = GetElapsedTime(); } if(_existing_keypoints) { //existing keypoint list should at least have the locations and scale GlobalUtil::StartTimer("Upload Feature List"); if(!(_existing_keypoints & SIFT_SKIP_FILTERING)) ComputeGradient(); GenerateFeatureListTex(); GlobalUtil::StopTimer(); _timing[2] = GetElapsedTime(); }else { GlobalUtil::StartTimer("Detect Keypoints"); DetectKeypointsEX(); GlobalUtil::StopTimer(); _timing[1] = GetElapsedTime(); if(GlobalUtil::_ListGenGPU ==1) { GlobalUtil::StartTimer("Get Feature List"); GenerateFeatureList(); GlobalUtil::StopTimer(); }else { GlobalUtil::StartTimer("Transfer Feature List"); GenerateFeatureListCPU(); GlobalUtil::StopTimer(); } LimitFeatureCount(0); _timing[2] = GetElapsedTime(); } if(_existing_keypoints& SIFT_SKIP_ORIENTATION) { //use exisitng feature orientation or }else if(GlobalUtil::_MaxOrientation>0) { //some extra tricks are done to handle existing keypoint list GlobalUtil::StartTimer("Feature Orientations"); GetFeatureOrientations(); GlobalUtil::StopTimer(); _timing[3] = GetElapsedTime(); //for existing keypoint list, only the strongest orientation is kept. if(GlobalUtil::_MaxOrientation >1 && !_existing_keypoints && !GlobalUtil::_FixedOrientation) { GlobalUtil::StartTimer("MultiO Feature List"); ReshapeFeatureListCPU(); LimitFeatureCount(1); GlobalUtil::StopTimer(); _timing[4] = GetElapsedTime(); } }else { GlobalUtil::StartTimer("Feature Orientations"); GetSimplifiedOrientation(); GlobalUtil::StopTimer(); _timing[3] = GetElapsedTime(); } PrepareBuffer(); if(_existing_keypoints & SIFT_SKIP_ORIENTATION) { //no need to read back feature if all fields of keypoints are already specified }else { GlobalUtil::StartTimer("Download Keypoints"); #ifdef NO_DUPLICATE_DOWNLOAD if(GlobalUtil::_MaxOrientation < 2 || GlobalUtil::_FixedOrientation) #endif DownloadKeypoints(); GlobalUtil::StopTimer(); _timing[5] = GetElapsedTime(); } if(GlobalUtil::_DescriptorPPT) { //desciprotrs are downloaded in descriptor computation of each level GlobalUtil::StartTimer("Get Descriptor"); GetFeatureDescriptors(); GlobalUtil::StopTimer(); _timing[6] = GetElapsedTime(); } //reset the existing keypoints _existing_keypoints = 0; _keypoint_index.resize(0); if(GlobalUtil::_UseSiftGPUEX) { GlobalUtil::StartTimer("Gen. Display VBO"); GenerateFeatureDisplayVBO(); GlobalUtil::StopTimer(); _timing[7] = GlobalUtil::GetElapsedTime(); } //clean up CleanUpAfterSIFT(); } void SiftPyramid::LimitFeatureCount(int have_keylist) { if(GlobalUtil::_FeatureCountThreshold <= 0 || _existing_keypoints) return; /////////////////////////////////////////////////////////////// //skip the lowest levels to reduce number of features. if(GlobalUtil::_TruncateMethod == 2) { int i = 0, new_feature_num = 0, level_num = param._dog_level_num * _octave_num; for(; new_feature_num < _FeatureCountThreshold && i < level_num; ++i) new_feature_num += _levelFeatureNum[i]; for(; i < level_num; ++i) _levelFeatureNum[i] = 0; if(new_feature_num < _featureNum) { _featureNum = new_feature_num; if(GlobalUtil::_verbose ) { std::cout<<"#Features Reduced:\t"<<_featureNum< _FeatureCountThreshold) { num_to_erase += _levelFeatureNum[i]; _featureNum -= _levelFeatureNum[i]; _levelFeatureNum[i++] = 0; } if(num_to_erase > 0 && have_keylist) { _keypoint_buffer.erase(_keypoint_buffer.begin(), _keypoint_buffer.begin() + num_to_erase * 4); } if(GlobalUtil::_verbose && num_to_erase > 0) { std::cout<<"#Features Reduced:\t"<<_featureNum< i - 3... //768 in [2^9, 2^10) -> 6 -> smallest will be 768 / 32 = 24 int num = (int) floor (log ( inputsz * 2.0 / GlobalUtil::_texMinDim )/log(2.0)); return num <= 0 ? 1 : num; } void SiftPyramid::CopyFeatureVector(float*keys, float *descriptors) { if(keys) memcpy(keys, &_keypoint_buffer[0], 4*_featureNum*sizeof(float)); if(descriptors) memcpy(descriptors, &_descriptor_buffer[0], 128*_featureNum*sizeof(float)); } void SiftPyramid:: SetKeypointList(int num, const float * keys, int run_on_current, int skip_orientation) { //for each input keypoint //sort the key point list by size, and assign them to corresponding levels if(num <=0) return; _featureNum = num; ///copy the keypoints _keypoint_buffer.resize(num * 4); memcpy(&_keypoint_buffer[0], keys, 4 * num * sizeof(float)); //location and scale can be skipped _existing_keypoints = SIFT_SKIP_DETECTION; //filtering is skipped if it is running on the same image if(run_on_current) _existing_keypoints |= SIFT_SKIP_FILTERING; //orientation can be skipped if specified if(skip_orientation) _existing_keypoints |= SIFT_SKIP_ORIENTATION; //hacking parameter for using rectangle description mode if(skip_orientation == -1) _existing_keypoints |= SIFT_RECT_DESCRIPTION; } void SiftPyramid::SaveSIFT(const char * szFileName) { if (_featureNum <=0) return; float * pk = &_keypoint_buffer[0]; if(GlobalUtil::_BinarySIFT) { std::ofstream out(szFileName, ios::binary); out.write((char* )(&_featureNum), sizeof(int)); if(GlobalUtil::_DescriptorPPT) { int dim = 128; out.write((char* )(&dim), sizeof(int)); float * pd = &_descriptor_buffer[0] ; for(int i = 0; i < _featureNum; i++, pk+=4, pd +=128) { out.write((char* )(pk +1), sizeof(float)); out.write((char* )(pk), sizeof(float)); out.write((char* )(pk+2), 2 * sizeof(float)); out.write((char* )(pd), 128 * sizeof(float)); } }else { int dim = 0; out.write((char* )(&dim), sizeof(int)); for(int i = 0; i < _featureNum; i++, pk+=4) { out.write((char* )(pk +1), sizeof(float)); out.write((char* )(pk), sizeof(float)); out.write((char* )(pk+2), 2 * sizeof(float)); } } }else { std::ofstream out(szFileName); out.flags(ios::fixed); if(GlobalUtil::_DescriptorPPT) { float * pd = &_descriptor_buffer[0] ; out<<_featureNum<<" 128"<GetImgWidth(); int height = tex->GetImgHeight(); float* buffer1 = new float[ width * height * 4]; float* buffer2 = new float[ width * height * 4]; //read data back glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); tex->AttachToFBO(0); tex->FitTexViewPort(); glReadPixels(0, 0, width, height, GL_RGBA , GL_FLOAT, buffer1); //Tiffs saved with IL are flipped for(int i = 0; i < height; i++) { memcpy(buffer2 + i * width * 4, buffer1 + (height - i - 1) * width * 4, width * 4 * sizeof(float)); } //save data as floating point tiff file ilGenImages(1, &imID); ilBindImage(imID); ilEnable(IL_FILE_OVERWRITE); ilTexImage(width, height, 1, 4, IL_RGBA, IL_FLOAT, buffer2); ilSave(IL_TIF, name); ilDeleteImages(1, &imID); delete buffer1; delete buffer2; glReadBuffer(GL_NONE); } #endif colmap-3.9.1/src/thirdparty/SiftGPU/SiftPyramid.h000066400000000000000000000142721454702036400216600ustar00rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // File: SiftPyramid.h // Author: Changchang Wu // Description : interface for the SiftPyramid class. // SiftPyramid: data storage for SIFT // |---PyramidGL: OpenGL based implementation // | |--PyramidNaive: Unpacked version // | |--PyramidPacked: packed version // |--PyramidCU: CUDA-based implementation // // Copyright (c) 2007 University of North Carolina at Chapel Hill // All Rights Reserved // // Permission to use, copy, modify and distribute this software and its // documentation for educational, research and non-profit purposes, without // fee, and without a written agreement is hereby granted, provided that the // above copyright notice and the following paragraph appear in all copies. // // The University of North Carolina at Chapel Hill make no representations // about the suitability of this software for any purpose. It is provided // 'as is' without express or implied warranty. // // Please send BUG REPORTS to ccwu@cs.unc.edu // //////////////////////////////////////////////////////////////////////////// #ifndef _SIFT_PYRAMID_H #define _SIFT_PYRAMID_H class GLTexImage; class GLTexInput; class SiftParam; class GlobalUtil; ///////////////////////////////////////////////////////////////////////////// //class SiftPyramid //description: virutal class of SIFT data pyramid // provides functions for SiftPU to run steps of GPU SIFT // class PyramidNaive is the first implementation // class PyramidPacked is a better OpenGL implementation // class PyramidCU is a CUDA based implementation ///////////////////////////////////////////////////////////////////////////// #define NO_DUPLICATE_DOWNLOAD class SiftPyramid : public GlobalUtil { public: enum{ DATA_GAUSSIAN = 0, DATA_DOG = 1, DATA_KEYPOINT = 2, DATA_GRAD = 3, DATA_ROT = 4, DATA_NUM = 5 }; enum{ SIFT_SKIP_FILTERING = 0x01, SIFT_SKIP_DETECTION = 0x02, SIFT_SKIP_ORIENTATION = 0x04, SIFT_RECT_DESCRIPTION = 0x08 }; protected: SiftParam& param; int _hpLevelNum; int* _levelFeatureNum; int _featureNum; float* _histo_buffer; //keypoint list int _existing_keypoints; vector _keypoint_index; //display vbo GLuint* _featureDisplayVBO; GLuint* _featurePointVBO; public: // float _timing[8]; //image size related //first octave int _octave_min; //how many octaves int _octave_num; //pyramid storage int _pyramid_octave_num; int _pyramid_octave_first; int _pyramid_width; int _pyramid_height; int _down_sample_factor; int _allocated; int _alignment; int _siftgpu_failed; public: vector _keypoint_buffer; vector _descriptor_buffer; private: inline void PrepareBuffer(); inline void LimitFeatureCount(int have_keylist = 0); public: //shared by all implementations virtual void RunSIFT(GLTexInput*input); virtual void SaveSIFT(const char * szFileName); virtual void CopyFeatureVector(float*keys, float *descriptors); virtual void SetKeypointList(int num, const float * keys, int run_on_current, int skip_orientation); //implementation-dependent functions virtual void GetFeatureDescriptors() = 0; virtual void GenerateFeatureListTex() =0; virtual void ReshapeFeatureListCPU() =0; virtual void GenerateFeatureDisplayVBO() =0; virtual void DownloadKeypoints() = 0; virtual void GenerateFeatureListCPU()=0; virtual void GenerateFeatureList()=0; virtual GLTexImage* GetLevelTexture(int octave, int level)=0; virtual GLTexImage* GetLevelTexture(int octave, int level, int dataName) = 0; virtual void BuildPyramid(GLTexInput * input)=0; virtual void ResizePyramid(int w, int h) = 0; virtual void InitPyramid(int w, int h, int ds = 0)=0; virtual void DetectKeypointsEX() = 0; virtual void ComputeGradient() = 0; virtual void GetFeatureOrientations() = 0; virtual void GetSimplifiedOrientation() = 0; //////////////////////////////// virtual void CleanUpAfterSIFT() {} virtual int IsUsingRectDescription() {return 0; } static int GetRequiredOctaveNum(int inputsz); ///inline functions, shared by all implementations inline void SetFailStatus() {_siftgpu_failed = 1; } inline int GetSucessStatus() {return _siftgpu_failed == 0; } inline int GetFeatureNum(){return _featureNum;} inline int GetHistLevelNum(){return _hpLevelNum;} inline const GLuint * GetFeatureDipslayVBO(){return _featureDisplayVBO;} inline const GLuint * GetPointDisplayVBO(){return _featurePointVBO;} inline const int * GetLevelFeatureNum(){return _levelFeatureNum;} inline void GetPyramidTiming(float * timing){ for(int i = 0; i < 8; i++) timing[i] = _timing[i]; } inline void CleanupBeforeSIFT() { _siftgpu_failed = 0; for(int i = 0; i < 8; ++i) _timing[i] = 0; } SiftPyramid(SiftParam&sp):param(sp) { _featureNum = 0; _featureDisplayVBO = 0; _featurePointVBO = 0; _levelFeatureNum = NULL; _histo_buffer = NULL; _hpLevelNum = 0; //image size _octave_num = 0; _octave_min = 0; _alignment = 1; _pyramid_octave_num = _pyramid_octave_first = 0; _pyramid_width = _pyramid_height = 0; _allocated = 0; _down_sample_factor = 0; ///// _existing_keypoints = 0; } virtual ~SiftPyramid() {}; #ifdef DEBUG_SIFTGPU private: void StopDEBUG(); void BeginDEBUG(const char* imagepath); void WriteTextureForDEBUG(GLTexImage * tex, const char * namet, ...); #endif }; #define SIFTGPU_ENABLE_REVERSE_ORDER #ifdef SIFTGPU_ENABLE_REVERSE_ORDER #define FIRST_OCTAVE(R) (R? _octave_num - 1 : 0) #define NOT_LAST_OCTAVE(i, R) (R? (i >= 0) : (i < _octave_num)) #define GOTO_NEXT_OCTAVE(i, R) (R? (--i) : (++i)) #define FIRST_LEVEL(R) (R? param._dog_level_num - 1 : 0) #define GOTO_NEXT_LEVEL(j, R) (R? (--j) : (++j)) #define NOT_LAST_LEVEL(j, R) (R? (j >= 0) : (j < param._dog_level_num)) #define FOR_EACH_OCTAVE(i, R) for(int i = FIRST_OCTAVE(R); NOT_LAST_OCTAVE(i, R); GOTO_NEXT_OCTAVE(i, R)) #define FOR_EACH_LEVEL(j, R) for(int j = FIRST_LEVEL(R); NOT_LAST_LEVEL(j, R); GOTO_NEXT_LEVEL(j, R)) #else #define FOR_EACH_OCTAVE(i, R) for(int i = 0; i < _octave_num; ++i) #define FOR_EACH_LEVEL(j, R) for(int j = 0; j < param._dog_level_num; ++j) #endif #endif colmap-3.9.1/src/thirdparty/VLFeat/000077500000000000000000000000001454702036400171065ustar00rootroot00000000000000colmap-3.9.1/src/thirdparty/VLFeat/CMakeLists.txt000066400000000000000000000041721454702036400216520ustar00rootroot00000000000000set(VLFEAT_SOURCE_FILES aib.c aib.h array.c array.h covdet.c covdet.h dsift.c dsift.h fisher.c fisher.h float.h generic.c generic.h getopt_long.c getopt_long.h gmm.c gmm.h heap-def.h hikmeans.c hikmeans.h hog.c hog.h homkermap.c homkermap.h host.c host.h ikmeans.c ikmeans.h ikmeans_elkan.tc ikmeans_init.tc ikmeans_lloyd.tc imopv.c imopv.h kdtree.c kdtree.h kmeans.c kmeans.h lbp.c lbp.h liop.c liop.h mathop.c mathop.h mser.c mser.h pgm.c pgm.h qsort-def.h quickshift.c quickshift.h random.c random.h rodrigues.c rodrigues.h scalespace.c scalespace.h shuffle-def.h sift.c sift.h slic.c slic.h stringop.c stringop.h svm.c svm.h svmdataset.c svmdataset.h vlad.c vlad.h) if(SIMD_ENABLED AND IS_X86) if (MSVC) # https://github.com/vlfeat/vlfeat/commit/4f0098fd47e9 add_definitions("-DVL_DISABLE_AVX") else() set(AVX_SOURCES mathop_avx.c mathop_avx.h) endif() set(SSE2_SOURCES imopv_sse2.c imopv_sse2.h mathop_sse2.c mathop_sse2.h) list(APPEND VLFEAT_SOURCE_FILES ${AVX_SOURCES} ${SSE2_SOURCES}) if (MSVC) set_source_files_properties(${AVX_SOURCES} PROPERTIES COMPILE_FLAGS "/arch:AVX") set_source_files_properties(${SSE2_SOURCES} PROPERTIES COMPILE_FLAGS "/D__SSE2__") else() set_source_files_properties(${AVX_SOURCES} PROPERTIES COMPILE_FLAGS "-mavx") set_source_files_properties(${SSE2_SOURCES} PROPERTIES COMPILE_FLAGS "-msse2") endif() else() add_definitions("-DVL_DISABLE_AVX") add_definitions("-DVL_DISABLE_SSE2") endif() if(NOT OPENMP_ENABLED OR NOT OPENMP_FOUND) add_definitions("-DVL_DISABLE_OPENMP") endif() COLMAP_ADD_LIBRARY( NAME colmap_vlfeat SRCS ${VLFEAT_SOURCE_FILES} ) if(OPENMP_FOUND) target_link_libraries(colmap_vlfeat PRIVATE OpenMP::OpenMP_C) endif() colmap-3.9.1/src/thirdparty/VLFeat/LICENSE000077500000000000000000000025251454702036400201220ustar00rootroot00000000000000Copyright (C) 2007-11, Andrea Vedaldi and Brian Fulkerson Copyright (C) 2012-13, The VLFeat Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. colmap-3.9.1/src/thirdparty/VLFeat/aib.c000077500000000000000000000517171454702036400200230ustar00rootroot00000000000000/** @file aib.c ** @brief AIB - Definition ** @author Brian Fulkerson ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page aib Agglomerative Information Bottleneck (AIB) @author Brian Fulkerson @author Andrea Vedaldi @ref aib.h implemens the Agglomerative Information Bottleneck (AIB) algorithm as first described in @cite{slonim99agglomerative}. AIB takes a discrete valued feature @f$x@f$ and a label @f$c@f$ and gradually compresses @f$x@f$ by iteratively merging values which minimize the loss in mutual information @f$I(x,c)@f$. While the algorithm is equivalent to the one described in @cite{slonim99agglomerative}, it has some speedups that enable handling much larger datasets. Let N be the number of feature values and C the number of labels. The algorithm of @cite{slonim99agglomerative} is @f$O(N^2)@f$ in space and @f$O(C N^3)@f$ in time. This algorithm is @f$O(N)@f$ space and @f$O(C N^2)@f$ time in common cases (@f$O(C N^3)@f$ in the worst case). @section aib-overview Overview Given a discrete feature @f$x \in \mathcal{X} = \{x_1,\dots,x_N\}@f$ and a category label @f$c = 1,\dots,C@f$ with joint probability @f$p(x,c)@f$, AIB computes a compressed feature @f$[x]_{ij}@f$ by merging two values @f$x_i@f$ and @f$x_j@f$. Among all the pairs @f$ij@f$, AIB chooses the one that yields the smallest loss in the mutual information @f[ D_{ij} = I(x,c) - I([x]_{ij},c) = \sum_c p(x_i) \log \frac{p(x_i,c)}{p(x_i)p(c)} + \sum_c p(x_i) \log \frac{p(x_i,c)}{p(x_i)p(c)} - \sum_c (p(x_i)+p(x_j)) \log \frac {p(x_i,c)+p(x_i,c)}{(p(x_i)+p(x_j))p(c)} @f] AIB iterates this procedure until the desired level of compression is achieved. @section aib-algorithm Algorithm details Computing @f$D_{ij}@f$ requires @f$O(C)@f$ operations. For example, in standard AIB we need to calculate @f[ D_{ij} = I(x,c) - I([x]_{ij},c) = \sum_c p(x_i) \log \frac{p(x_i,c)}{p(x_i)p(c)} + \sum_c p(x_i) \log \frac{p(x_i,c)}{p(x_i)p(c)} - \sum_c (p(x_i)+p(x_j)) \log \frac {p(x_i,c)+p(x_i,c)}{(p(x_i)+p(x_j))p(c)} @f] Thus in a basic implementation of AIB, finding the optimal pair @f$ij@f$ of feature values requires @f$O(CN^2)@f$ operations in total. In order to join all the @f$N@f$ values, we repeat this procedure @f$O(N)@f$ times, yielding @f$O(N^3 C)@f$ time and @f$O(1)@f$ space complexity (this does not account for the space need to store the input). The complexity can be improved by reusing computations. For instance, we can store the matrix @f$D = [ D_{ij} ]@f$ (which requires @f$O(N^2)@f$ space). Then, after joining @f$ij@f$, all of the matrix D except the rows and columns (the matrix is symmetric) of indexes i and j is unchanged. These two rows and columns are deleted and a new row and column, whose computation requires @f$O(NC)@f$ operations, are added for the merged value @f$x_{ij}@f$. Finding the minimal element of the matrix still requires @f$O(N^2)@f$ operations, so the complexity of this algorithm is @f$O(N^2C + N^3)@f$ time and @f$O(N^2)@f$ space. We can obtain a much better expected complexity as follows. First, instead of storing the whole matrix D, we store the smallest element (index and value) of each row as @f$(q_i, D_i)@f$ (notice that this is also the best element of each column since D is symmetric). This requires @f$O(N)@f$ space and finding the minimal element of the matrix requires @f$O(N)@f$ operations. After joining @f$ij@f$, we have to efficiently update this representation. This is done as follows: - The entries @f$(q_i,D_i)@f$ and @f$(q_j,D_j)@f$ are deleted. - A new entry @f$(q_{ij},D_{ij})@f$ for the joint value @f$x_{ij}@f$ is added. This requires @f$O(CN)@f$ operations. - We test which other entries @f$(q_{k},D_{k})@f$ need to be updated. Recall that @f$(q_{k},D_{k})@f$ means that, before the merge, the value closest to @f$x_k@f$ was @f$x_{q_k}@f$ at a distance @f$D_k@f$. Then - If @f$q_k \not = i@f$, @f$q_k \not = j@f$ and @f$D_{k,ij} \geq D_k@f$, then @f$q_k@f$ is still the closest element and we do not do anything. - If @f$q_k \not = i@f$, @f$q_k \not = j@f$ and @f$D_{k,ij} < D_k@f$, then the closest element is @f$ij@f$ and we update the entry in constant time. - If @f$q_k = i@f$ or @f$q_k = j@f$, then we need to re-compute the closest element in @f$O(CN)@f$ operations. This algorithm requires only @f$O(N)@f$ space and @f$O(\gamma(N) C N^2)@f$ time, where @f$\gamma(N)@f$ is the expected number of times we fall in the last case. In common cases one has @f$\gamma(N) \approx \mathrm{const.}@f$, so the time saving is significant. **/ #include "aib.h" #include #include #include #include /* The maximum value which beta may take */ #define BETA_MAX DBL_MAX /** ------------------------------------------------------------------ ** @internal ** @brief Normalizes an array of probabilities to sum to 1 ** ** @param P The array of probabilities ** @param nelem The number of elements in the array ** ** @return Modifies P to contain values which sum to 1 **/ void vl_aib_normalize_P (double * P, vl_uint nelem) { vl_uint i; double sum = 0; for(i=0; ibeta to find the minimum value and fills @a minbeta and ** @a besti and @a bestj with this information. **/ void vl_aib_min_beta (VlAIB * aib, vl_uint * besti, vl_uint * bestj, double * minbeta) { vl_uint i; *minbeta = aib->beta[0]; *besti = 0; *bestj = aib->bidx[0]; for(i=0; inentries; i++) { if(aib->beta[i] < *minbeta) { *minbeta = aib->beta[i]; *besti = i; *bestj = aib->bidx[i]; } } } /** ------------------------------------------------------------------ ** @internal ** @brief Merges two nodes i,j in the internal datastructure ** ** @param aib A pointer to the internal data structure ** @param i The index of one member of the pair to merge ** @param j The index of the other member of the pair to merge ** @param new The index of the new node which corresponds to the union of ** (@a i, @a j). ** ** Nodes are merged by replacing the entry @a i with the union of @c ** ij, moving the node stored in last position (called @c lastnode) ** back to jth position and the entry at the end. ** ** After the nodes have been merged, it updates which nodes should be ** considered on the next iteration based on which beta values could ** potentially change. The merged node will always be part of this ** list. **/ void vl_aib_merge_nodes (VlAIB * aib, vl_uint i, vl_uint j, vl_uint new) { vl_uint last_entry = aib->nentries - 1 ; vl_uint c, n ; /* clear the list of nodes to update */ aib->nwhich = 0; /* make sure that i is smaller than j */ if(i > j) { vl_uint tmp = j; j = i; i = tmp; } /* ----------------------------------------------------------------- * Merge entries i and j, storing the result in i * -------------------------------------------------------------- */ aib-> Px [i] += aib->Px[j] ; aib-> beta [i] = BETA_MAX ; aib-> nodes[i] = new ; for (c = 0; c < aib->nlabels; c++) aib-> Pcx [i*aib->nlabels + c] += aib-> Pcx [j*aib->nlabels + c] ; /* ----------------------------------------------------------------- * Move last entry to j * -------------------------------------------------------------- */ aib-> Px [j] = aib-> Px [last_entry]; aib-> beta [j] = aib-> beta [last_entry]; aib-> bidx [j] = aib-> bidx [last_entry]; aib-> nodes [j] = aib-> nodes [last_entry]; for (c = 0 ; c < aib->nlabels ; c++) aib-> Pcx[j*aib->nlabels + c] = aib-> Pcx [last_entry*aib->nlabels + c] ; /* delete last entry */ aib-> nentries -- ; /* ----------------------------------------------------------------- * Scan for entries to update * -------------------------------------------------------------- */ /* * After mergin entries i and j, we need to update all other entries * that had one of these two as closest match. We also need to * update the renewend entry i. This is added by the loop below * since bidx [i] = j exactly because i was merged. * * Additionaly, since we moved the last entry back to the entry j, * we need to adjust the valeus of bidx to reflect this. */ for (n = 0 ; n < aib->nentries; n++) { if(aib->bidx[n] == i || aib->bidx[n] == j) { aib->bidx [n] = 0; aib->beta [n] = BETA_MAX; aib->which [aib->nwhich++] = n ; } else if(aib->bidx[n] == last_entry) { aib->bidx[n] = j ; } } } /** ------------------------------------------------------------------ ** @internal ** @brief Updates @c aib->beta and @c aib->bidx according to @c aib->which ** ** @param aib AIB data structure. ** ** The function calculates @c beta[i] and @c bidx[i] for the nodes @c ** i listed in @c aib->which. @c beta[i] is the minimal variation of mutual ** information (or other score) caused by merging entry @c i with another entry ** and @c bidx[i] is the index of this best matching entry. ** ** Notice that for each entry @c i that we need to update, a full ** scan of all the other entries must be performed. **/ void vl_aib_update_beta (VlAIB * aib) { #define PLOGP(x) ((x)*log((x))) vl_uint i; double * Px = aib->Px; double * Pcx = aib->Pcx; double * tmp = vl_malloc(sizeof(double)*aib->nentries); vl_uint a, b, c ; /* * T1 = I(x,c) - I([x]_ij) = A + B - C * * A = \sum_c p(xa,c) \log ( p(xa,c) / p(xa) ) * B = \sum_c p(xb,c) \log ( p(xb,c) / p(xb) ) * C = \sum_c (p(xa,c)+p(xb,c)) \log ((p(xa,c)+p(xb,c)) / (p(xa)+p(xb))) * * C = C1 + C2 * C1 = \sum_c (p(xa,c)+p(xb,c)) \log (p(xa,c)+p(xb,c)) * C2 = - (p(xa)+p(xb) \log (p(xa)+p(xb)) */ /* precalculate A and B */ for (a = 0; a < aib->nentries; a++) { tmp[a] = 0; for (c = 0; c < aib->nlabels; c++) { double Pac = Pcx [a*aib->nlabels + c] ; if(Pac != 0) tmp[a] += Pac * log (Pac / Px[a]) ; } } /* for each entry listed in which */ for (i = 0 ; i < aib->nwhich; i++) { a = aib->which[i]; /* for each other entry */ for(b = 0 ; b < aib->nentries ; b++) { double T1 = 0 ; if (a == b || Px [a] == 0 || Px [b] == 0) continue ; T1 = PLOGP ((Px[a] + Px[b])) ; /* - C2 */ T1 += tmp[a] + tmp[b] ; /* + A + B */ for (c = 0 ; c < aib->nlabels; ++ c) { double Pac = Pcx [a*aib->nlabels + c] ; double Pbc = Pcx [b*aib->nlabels + c] ; if (Pac == 0 && Pbc == 0) continue; T1 += - PLOGP ((Pac + Pbc)) ; /* - C1 */ } /* * Now we have beta(a,b). We check wether this is the best beta * for entries a and b. */ { double beta = T1 ; if (beta < aib->beta[a]) { aib->beta[a] = beta; aib->bidx[a] = b; } if (beta < aib->beta[b]) { aib->beta[b] = beta; aib->bidx[b] = a; } } } } vl_free(tmp); } /** ------------------------------------------------------------------ ** @internal @brief Calculates the current information and entropy ** ** @param aib A pointer to the internal data structure ** @param I The current mutual information (out). ** @param H The current entropy (out). ** ** Calculates the current mutual information and entropy of Pcx and sets ** @a I and @a H to these new values. **/ void vl_aib_calculate_information(VlAIB * aib, double * I, double * H) { vl_uint r, c; *H = 0; *I = 0; /* * H(x) = - sum_x p(x) \ log p(x) * I(x,c) = sum_xc p(x,c) \ log (p(x,c) / p(x)p(c)) */ /* for each entry */ for(r = 0 ; r< aib->nentries ; r++) { if (aib->Px[r] == 0) continue ; *H += -log(aib->Px[r]) * aib->Px[r] ; for(c=0; cnlabels; c++) { if (aib->Pcx[r*aib->nlabels+c] == 0) continue; *I += aib->Pcx[r*aib->nlabels+c] * log (aib->Pcx[r*aib->nlabels+c] / (aib->Px[r]*aib->Pc[c])) ; } } } /** ------------------------------------------------------------------ ** @brief Allocates and initializes the internal data structure ** ** @param Pcx A pointer to a 2D array of probabilities ** @param nvalues The number of rows in the array ** @param nlabels The number of columns in the array ** ** Creates a new @a VlAIB struct containing pointers to all the data that ** will be used during the AIB process. ** ** Allocates memory for the following: ** - Px (nvalues*sizeof(double)) ** - Pc (nlabels*sizeof(double)) ** - nodelist (nvalues*sizeof(vl_uint)) ** - which (nvalues*sizeof(vl_uint)) ** - beta (nvalues*sizeof(double)) ** - bidx (nvalues*sizeof(vl_uint)) ** - parents ((2*nvalues-1)*sizeof(vl_uint)) ** - costs (nvalues*sizeof(double)) ** ** Since it simply copies to pointer to Pcx, the total additional memory ** requirement is: ** ** (3*nvalues+nlabels)*sizeof(double) + 4*nvalues*sizeof(vl_uint) ** ** @returns An allocated and initialized @a VlAIB pointer **/ VlAIB * vl_aib_new(double * Pcx, vl_uint nvalues, vl_uint nlabels) { VlAIB * aib = vl_malloc(sizeof(VlAIB)); vl_uint i ; aib->verbosity = 0 ; aib->Pcx = Pcx ; aib->nvalues = nvalues ; aib->nlabels = nlabels ; vl_aib_normalize_P (aib->Pcx, aib->nvalues * aib->nlabels) ; aib->Px = vl_aib_new_Px (aib->Pcx, aib->nvalues, aib->nlabels) ; aib->Pc = vl_aib_new_Pc (aib->Pcx, aib->nvalues, aib->nlabels) ; aib->nentries = aib->nvalues ; aib->nodes = vl_aib_new_nodelist(aib->nentries) ; aib->beta = vl_malloc(sizeof(double) * aib->nentries) ; aib->bidx = vl_malloc(sizeof(vl_uint) * aib->nentries) ; for(i = 0 ; i < aib->nentries ; i++) aib->beta [i] = BETA_MAX ; /* Initially we must consider all nodes */ aib->nwhich = aib->nvalues; aib->which = vl_aib_new_nodelist (aib->nwhich) ; aib->parents = vl_malloc(sizeof(vl_uint)*(aib->nvalues*2-1)); /* Initially, all parents point to a nonexistent node */ for (i = 0 ; i < 2 * aib->nvalues - 1 ; i++) aib->parents [i] = 2 * aib->nvalues ; /* Allocate cost output vector */ aib->costs = vl_malloc (sizeof(double) * (aib->nvalues - 1 + 1)) ; return aib ; } /** ------------------------------------------------------------------ ** @brief Deletes AIB data structure ** @param aib data structure to delete. **/ void vl_aib_delete (VlAIB * aib) { if (aib) { if (aib-> nodes) vl_free (aib-> nodes); if (aib-> beta) vl_free (aib-> beta); if (aib-> bidx) vl_free (aib-> bidx); if (aib-> which) vl_free (aib-> which); if (aib-> Px) vl_free (aib-> Px); if (aib-> Pc) vl_free (aib-> Pc); if (aib-> parents) vl_free (aib-> parents); if (aib-> costs) vl_free (aib-> costs); vl_free (aib) ; } } /** ------------------------------------------------------------------ ** @brief Runs AIB on Pcx ** ** @param aib AIB object to process ** ** The function runs Agglomerative Information Bottleneck (AIB) on ** the joint probability table @a aib->Pcx which has labels along the ** columns and feature values along the rows. AIB iteratively merges ** the two values of the feature @c x that causes the smallest ** decrease in mutual information between the random variables @c x ** and @c c. ** ** Merge operations are arranged in a binary tree. The nodes of the ** tree correspond to the original feature values and any other value ** obtained as a result of a merge operation. The nodes are indexed ** in breadth-first order, starting from the leaves. The first index ** is zero. In this way, the leaves correspond directly to the ** original feature values. In total there are @c 2*nvalues-1 nodes. ** ** The results may be accessed through vl_aib_get_parents which ** returns an array with one element per tree node. Each ** element is the index the parent node. The root parent is equal to ** zero. The array has @c 2*nvalues-1 elements. ** ** Feature values with null probability are ignored by the algorithm ** and their nodes have parents indexing a non-existent tree node (a ** value bigger than @c 2*nvalues-1). ** ** Then the function will also compute the information level after each ** merge. vl_get_costs will return a vector with the information level ** after each merge. @a ** cost has @c nvalues entries: The first is the value of the cost ** functional before any merge, and the others are the cost after the ** @c nvalues-1 merges. ** **/ VL_EXPORT void vl_aib_process(VlAIB *aib) { vl_uint i, besti, bestj, newnode, nodei, nodej; double I, H; double minbeta; /* Calculate initial value of cost function */ vl_aib_calculate_information (aib, &I, &H) ; aib->costs[0] = I; /* Initially which = all */ /* For each merge */ for(i = 0 ; i < aib->nvalues - 1 ; i++) { /* update entries in aib-> which */ vl_aib_update_beta(aib); /* find best pair of nodes to merge */ vl_aib_min_beta (aib, &besti, &bestj, &minbeta); if(minbeta == BETA_MAX) /* only null-probability entries remain */ break; /* Add the parent pointers for the new node */ newnode = aib->nvalues + i ; nodei = aib->nodes[besti]; nodej = aib->nodes[bestj]; aib->parents [nodei] = newnode ; aib->parents [nodej] = newnode ; aib->parents [newnode] = 0 ; /* Merge the nodes which produced the minimum beta */ vl_aib_merge_nodes (aib, besti, bestj, newnode) ; vl_aib_calculate_information (aib, &I, &H) ; aib->costs[i+1] = I; if (aib->verbosity > 0) { VL_PRINTF ("aib: (%5d,%5d)=%5d dE: %10.3g I: %6.4g H: %6.4g updt: %5d\n", nodei, nodej, newnode, minbeta, I, H, aib->nwhich) ; } } /* fill ignored entries with NaNs */ for(; i < aib->nvalues - 1 ; i++) aib->costs[i+1] = VL_NAN_D ; } colmap-3.9.1/src/thirdparty/VLFeat/aib.h000066400000000000000000000067411454702036400200220ustar00rootroot00000000000000/** @file aib.h ** @brief AIB (@ref aib) ** @author Brian Fulkerson ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_AIB_H #define VL_AIB_H #include "generic.h" #include "mathop.h" /** ------------------------------------------------------------------ ** @internal ** @brief AIB algorithm data ** ** The implementation is quite straightforward, but the way feature ** values are handled in order to support efficient joins, ** deletions and re-arrangement needs to be explained. This is ** achieved by adding a layer of indirection: ** - Call each feature value (either original or obtained by a join ** operation) a node. Nodes are identified by numbers. ** - Call each element of the various arrays (such as VlAIB::Px) ** an entry. ** - Entries are dynamically associated to nodes as specified by ** VlAIB::nodes. For example, @c Px[i] refers to the node @c ** nodes[i]. **/ typedef struct _VlAIB { vl_uint *nodes ; /**< Entires to nodes */ vl_uint nentries ; /**< Total number of entries (= # active nodes) */ double *beta ; /**< Minimum distance to an entry */ vl_uint *bidx ; /**< Closest entry */ vl_uint *which ; /**< List of entries to update */ vl_uint nwhich ; /**< Number of entries to update */ double *Pcx; /**< Joint probability table */ double *Px; /**< Marginal. */ double *Pc; /**< Marginal. */ vl_uint nvalues; /**< Number of feature values */ vl_uint nlabels; /**< Number of labels */ vl_uint *parents; /**< Array of parents */ double *costs; /**< Cost of each merge */ vl_uint verbosity ; /** Verbosity level */ } VlAIB; /** @name Create and destroy ** @{ **/ VL_EXPORT VlAIB * vl_aib_new(double * Pcx, vl_uint nvalues, vl_uint nlabels); VL_EXPORT void vl_aib_delete (VlAIB * aib); /** @} */ /** @name Process data ** @{ **/ VL_EXPORT void vl_aib_process(VlAIB * aib); /** @} */ /** @name Retrieve results ** @{ **/ VL_INLINE vl_uint * vl_aib_get_parents(VlAIB const * aib); VL_INLINE double * vl_aib_get_costs(VlAIB const * aib); /** @} */ /* ------------------------------------------------------------------- * Inline functions implementation * ---------------------------------------------------------------- */ /** ------------------------------------------------------------------ ** @brief Get resulting list of parents ** @param aib AIB filter. ** @return An array of parents **/ VL_INLINE vl_uint * vl_aib_get_parents(VlAIB const * aib) { return aib->parents; } /** ------------------------------------------------------------------ ** @brief Get a list of merge costs ** @param aib AIB filter. ** @return An array of costs **/ VL_INLINE double * vl_aib_get_costs(VlAIB const * aib) { return aib->costs; } /* ----------------------------------------------------------------- */ /** @brief Set the verbosity ** @param self AIB object. ** @param verbosity a non-negative integer. **/ VL_INLINE void vl_aib_set_verbosity (VlAIB * self, int verbosity) { self->verbosity = verbosity ; } /** @brief Get the verbosity ** @param self AIB object. ** @return the verbosity level. **/ VL_INLINE int vl_aib_get_verbosity (VlAIB const * self) { return self->verbosity ; } /* VL_AIB_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/array.c000077500000000000000000000130511454702036400203730ustar00rootroot00000000000000/** @file array.h ** @brief Array ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #include "array.h" #include /** @brief Get number of elements in array ** @param self array. ** @return number of elements. **/ VL_EXPORT vl_size vl_array_get_num_elements (VlArray const * self) { vl_size numElements = 1 ; vl_uindex k ; if (self->numDimensions == 0) { return 0 ; } for (k = 0 ; k < self->numDimensions ; ++k) { numElements *= self->dimensions[k] ; } return numElements ; } /* ---------------------------------------------------------------- */ /* init & dealloc */ /* ---------------------------------------------------------------- */ /** @brief New numeric array ** @param self array to initialize. ** @param type data type. ** @param numDimensions number of dimensions. ** @param dimensions dimensions. ** ** The function initializes the specified array and allocates ** the necessary memory for storage. **/ VL_EXPORT VlArray * vl_array_init (VlArray* self, vl_type type, vl_size numDimensions, vl_size const * dimensions) { assert (numDimensions <= VL_ARRAY_MAX_NUM_DIMENSIONS) ; self->type = type ; self->numDimensions = numDimensions ; memcpy(self->dimensions, dimensions, sizeof(vl_size) * numDimensions) ; self->data = vl_malloc(vl_get_type_size(type) * vl_array_get_num_elements (self)) ; self->isEnvelope = VL_FALSE ; self->isSparse = VL_FALSE ; return self ; } /** @brief New numeric array envelope ** @param self array to initialize. ** @param data data to envelople. ** @param type data type. ** @param numDimensions number of dimensions. ** @param dimensions dimensions. ** ** The function initializes the specified array wrapping the ** specified buffer. **/ VL_EXPORT VlArray * vl_array_init_envelope (VlArray * self, void * data, vl_type type, vl_size numDimensions, vl_size const * dimensions) { assert (numDimensions <= VL_ARRAY_MAX_NUM_DIMENSIONS) ; self->type = type ; self->numDimensions = numDimensions ; memcpy(self->dimensions, dimensions, sizeof(vl_size) * numDimensions) ; self->data = data ; self->isEnvelope = VL_TRUE ; self->isSparse = VL_FALSE ; return self ; } /** @brief New numeric array with matrix shape ** @param self array to initialize. ** @param type type. ** @param numRows number of rows. ** @param numColumns number of columns. **/ VL_EXPORT VlArray * vl_array_init_matrix (VlArray * self, vl_type type, vl_size numRows, vl_size numColumns) { vl_size dimensions [2] = {numRows, numColumns} ; return vl_array_init (self, type, 2, dimensions) ; } /** @brief New numeric array envelpe with matrix shape ** @param self array to initialize. ** @param data data to envelope. ** @param type type. ** @param numRows number of rows. ** @param numColumns number of columns. **/ VL_EXPORT VlArray * vl_array_init_matrix_envelope (VlArray * self, void * data, vl_type type, vl_size numRows, vl_size numColumns) { vl_size dimensions [2] = {numRows, numColumns} ; return vl_array_init_envelope (self, data, type, 2, dimensions) ; } /** @brief Delete array ** @param self array. **/ VL_EXPORT void vl_array_dealloc (VlArray * self) { if (! self->isEnvelope) { if (self->data) { vl_free(self->data) ; self->data = NULL ; } } } /* ---------------------------------------------------------------- */ /* new & delete */ /* ---------------------------------------------------------------- */ /** @brief New numeric array ** @param type data type. ** @param numDimensions number of dimensions. ** @param dimensions dimensions. ** ** The function creates a new VLArray instance and allocates ** the necessary memory for storage. **/ VL_EXPORT VlArray * vl_array_new (vl_type type, vl_size numDimensions, vl_size const * dimensions) { VlArray * self = vl_malloc(sizeof(VlArray)) ; return vl_array_init(self, type, numDimensions, dimensions) ; } /** @brief New numeric array with matrix shape ** @param type type. ** @param numRows number of rows. ** @param numColumns number of columns. **/ VL_EXPORT VlArray * vl_array_new_matrix (vl_type type, vl_size numRows, vl_size numColumns) { vl_size dimensions [2] = {numRows, numColumns} ; return vl_array_new (type, 2, dimensions) ; } /** @brief New numeric array envelope ** @param data data to envelople. ** @param type data type. ** @param numDimensions number of dimensions. ** @param dimensions dimensions. **/ VL_EXPORT VlArray * vl_array_new_envelope (void * data, vl_type type, vl_size numDimensions, vl_size const * dimensions) { VlArray * self = vl_malloc(sizeof(VlArray)) ; return vl_array_init_envelope(self, data, type, numDimensions, dimensions) ; } /** @brief New numeric array envelpe with matrix shape ** @param data data to envelope. ** @param type type. ** @param numRows number of rows. ** @param numColumns number of columns. **/ VL_EXPORT VlArray * vl_array_new_matrix_envelope (void * data, vl_type type, vl_size numRows, vl_size numColumns) { vl_size dimensions [2] = {numRows, numColumns} ; return vl_array_new_envelope (data, type, 2, dimensions) ; } /** @brief Delete array ** @param self array. **/ VL_EXPORT void vl_array_delete (VlArray * self) { vl_array_dealloc(self) ; vl_free(self) ; } colmap-3.9.1/src/thirdparty/VLFeat/array.h000066400000000000000000000051361454702036400204020ustar00rootroot00000000000000/** @file array.h ** @brief Array - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_ARRAY_H #define VL_ARRAY_H #include "generic.h" /** @brief Maximum number of array dimensions */ #define VL_ARRAY_MAX_NUM_DIMENSIONS 16 /** @brief Numeric array */ typedef struct _VlArray { vl_type type ; vl_bool isEnvelope ; vl_bool isSparse ; vl_size numDimensions ; vl_size dimensions [VL_ARRAY_MAX_NUM_DIMENSIONS] ; void * data ; void * rowPointers ; void * columnPointers ; } VlArray ; /** @name Get data and parameters ** @{ */ /** @brief Get number of dimensions ** @param self array. ** @return number of dimensions. **/ VL_INLINE vl_size vl_array_get_num_dimensions (VlArray const * self) { return self->numDimensions ; } /** @brief Get dimensions ** @param self array. ** @return dimensions. **/ VL_INLINE vl_size const * vl_array_get_dimensions (VlArray const * self) { return self->dimensions ; } /** @brief Get data ** @param self array. ** @return data. **/ VL_INLINE void * vl_array_get_data (VlArray const * self) { return self->data; } /** @brief Get type ** @param self array. ** @return type. **/ VL_INLINE vl_type vl_array_get_data_type (VlArray const * self) { return self->type ; } VL_EXPORT vl_size vl_array_get_num_elements (VlArray const * self) ; /** @{ */ /** @name Constructing and destroying ** @{ */ VL_EXPORT VlArray * vl_array_init (VlArray * self, vl_type type, vl_size numDimension, vl_size const * dimensions) ; VL_EXPORT VlArray * vl_array_init_envelope (VlArray *self, void * data, vl_type type, vl_size numDimension, vl_size const * dimensions) ; VL_EXPORT VlArray * vl_array_init_matrix (VlArray * self, vl_type type, vl_size numRows, vl_size numColumns) ; VL_EXPORT VlArray * vl_array_init_matrix_envelope (VlArray * self, void * data, vl_type type, vl_size numRows, vl_size numColumns) ; VL_EXPORT VlArray * vl_array_new (vl_type type, vl_size numDimension, vl_size const * dimensions) ; VL_EXPORT VlArray * vl_array_new_envelope (void * data, vl_type type, vl_size numDimension, vl_size const * dimensions) ; VL_EXPORT VlArray * vl_array_new_matrix (vl_type type, vl_size numRows, vl_size numColumns) ; VL_EXPORT VlArray * vl_array_new_matrix_envelope (void * data, vl_type type, vl_size numRows, vl_size numColumns) ; VL_EXPORT void vl_array_dealloc (VlArray * self) ; VL_EXPORT void vl_array_delete (VlArray * self) ; /** @} */ /* VL_ARRAY_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/covdet.c000077500000000000000000003350101454702036400205430ustar00rootroot00000000000000/** @file covdet.c ** @brief Covariant feature detectors - Definition ** @author Karel Lenc ** @author Andrea Vedaldi ** @author Michal Perdoch **/ /* Copyright (C) 2013-14 Andrea Vedaldi. Copyright (C) 2012 Karel Lenc, Andrea Vedaldi and Michal Perdoch. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page covdet Covariant feature detectors @author Karel Lenc @author Andrea Vedaldi @author Michal Perdoch @tableofcontents @ref covdet.h implements a number of covariant feature detectors, based on three cornerness measures (determinant of the Hessian, trace of the Hessian (aka Difference of Gaussians, and Harris). It supprots affine adaptation, orientation estimation, as well as Laplacian scale detection. - @subpage covdet-fundamentals - @subpage covdet-principles - @subpage covdet-differential - @subpage covdet-corner-types @section covdet-starting Getting started The ::VlCovDet object implements a number of covariant feature detectors: Difference of Gaussian, Harris, determinant of Hessian. Variant of the basic detectors support scale selection by maximizing the Laplacian measure as well as affine normalization. @code // create a detector object VlCovDet * covdet = vl_covdet_new(method) ; // set various parameters (optional) vl_covdet_set_first_octave(covdet, -1) ; // start by doubling the image resolution vl_covdet_set_octave_resolution(covdet, octaveResolution) ; vl_covdet_set_peak_threshold(covdet, peakThreshold) ; vl_covdet_set_edge_threshold(covdet, edgeThreshold) ; // process the image and run the detector vl_covdet_put_image(covdet, image, numRows, numCols) ; vl_covdet_detect(covdet) ; // drop features on the margin (optional) vl_covdet_drop_features_outside (covdet, boundaryMargin) ; // compute the affine shape of the features (optional) vl_covdet_extract_affine_shape(covdet) ; // compute the orientation of the features (optional) vl_covdet_extract_orientations(covdet) ; // get feature frames back vl_size numFeatures = vl_covdet_get_num_features(covdet) ; VlCovDetFeature const * feature = vl_covdet_get_features(covdet) ; // get normalized feature appearance patches (optional) vl_size w = 2*patchResolution + 1 ; for (i = 0 ; i < numFeatures ; ++i) { float * patch = malloc(w*w*sizeof(*desc)) ; vl_covdet_extract_patch_for_frame(covdet, patch, patchResolution, patchRelativeExtent, patchRelativeSmoothing, feature[i].frame) ; // do something with patch } @endcode This example code: - Calls ::vl_covdet_new constructs a new detector object. @ref covdet.h supports a variety of different detectors (see ::VlCovDetMethod). - Optionally calls various functions to set the detector parameters if needed (e.g. ::vl_covdet_set_peak_threshold). - Calls ::vl_covdet_put_image to start processing a new image. It causes the detector to compute the scale space representation of the image, but does not compute the features yet. - Calls ::vl_covdet_detect runs the detector. At this point features are ready to be extracted. However, one or all of the following steps may be executed in order to process the features further. - Optionally calls ::vl_covdet_drop_features_outside to drop features outside the image boundary. - Optionally calls ::vl_covdet_extract_affine_shape to compute the affine shape of features using affine adaptation. - Optionally calls ::vl_covdet_extract_orientations to compute the dominant orientation of features looking for the dominant gradient orientation in patches. - Optionally calls ::vl_covdet_extract_patch_for_frame to extract a normalized feature patch, for example to compute an invariant feature descriptor. @page covdet-fundamentals Covariant detectors fundamentals @tableofcontents This page describes the fundamental concepts required to understand a covariant feature detector, the geometry of covariant features, and the process of feature normalization. @section covdet-covariance Covariant detection The purpose of a *covariant detector* is to extract from an image a set of local features in a manner which is consistent with spatial transformations of the image itself. For instance, a covariant detector that extracts interest points $\bx_1,\dots,\bx_n$ from image $\ell$ extracts the translated points $\bx_1+T,\dots,\bx_n+T$ from the translated image $\ell'(\bx) = \ell(\bx-T)$. More in general, consider a image $\ell$ and a transformed version $\ell' = \ell \circ w^{-1}$ of it, as in the following figure: @image html covdet.png "Covariant detection of local features." The transformation or warp $w : \real^2 \mapsto \real^2$ is a deformation of the image domain which may capture a change of camera viewpoint or similar imaging factor. Examples of warps typically considered are translations, scaling, rotations, and general affine transformations; however, in $w$ could be another type of continuous and invertible transformation. Given an image $\ell$, a **detector** selects features $R_1,\dots,R_n$ (one such features is shown in the example as a green circle). The detector is said to be **covariant** with the warps $w$ if it extracts the transformed features $w[R_1],\dots, w[R_n]$ from the transformed image $w[\ell]$. Intuitively, this means that the “same features” are extracted in both cases up to the transformation $w$. This property is described more formally in @ref covdet-principles. Covariance is a key property of local feature detectors as it allows extracting corresponding features from two or more images, making it possible to match them in a meaningful way. The @ref covdet.h module in VLFeat implements an array of feature detection algorithm that have are covariant to different classes of transformations. @section covdet-frame Feature geometry and feature frames As we have seen, local features are subject to image transformations, and they apply a fundamental role in matching and normalizing images. To operates effectively with local features is therefore necessary to understand their geometry. The geometry of a local feature is captured by a feature frame $R$. In VLFeat, depending on the specific detector, the frame can be either a point, a disc, an ellipse, an oriented disc, or an oriented ellipse. A frame captures both the extent of the local features, useful to know which portions of two images are put in correspondence, as well their shape. The latter can be used to associate to diagnose the transformation that affects a feature and remove it through the process of **normalization**. More precisely, in covariant detection feature frames are constructed to be compatible with a certain class of transformations. For example, circles are compatible with similarity transformations as they are closed under them. Likewise, ellipses are compatible with affine transformations. Beyond this closure property, the key idea here is that all feature occurrences can be seen as transformed versions of a base or canonical feature. For example, all discs $R$ can be obtained by applying a similarity transformation to the unit disc $\bar R$ centered at the origin. $\bar R$ is an example of canonical frame as any other disc can be written as $R = w[\bar R]$ for a suitable similarity $w$. @image html frame-canonical.png "The idea of canonical frame and normalization" The equation $R = w[\bar R_0]$ matching the canonical and detected feature frames establishes a constraint on the warp $w$, very similar to the way two reference frames in geometry establish a transformation between spaces. The transformation $w$ can be thought as a the **pose** of the detected feature, a generalization of its location. In the case of discs and similarity transformations, the equation $R = w[\bar R_0]$ fixes $w$ up to a residual rotation. This can be addressed by considering oriented discs instead. An **oriented disc** is a disc with a radius highlighted to represent the feature orientation. While discs are appropriate for similarity transformations, they are not closed under general affine transformations. In this case, one should consider the more general class of (oriented) ellipses. The following image illustrates the five types of feature frames used in VLFeat: @image html frame-types.png "Types of local feature frames: points, discs, oriented discs, ellipses, oriented ellipses." Note that these frames are described respectively by 2, 3, 4, 5 and 6 parameters. The most general type are the oriented ellipses, which can be used to represent all the other frame types as well. @section covdet-frame-transformation Transforming feature frames Consider a warp $w$ mapping image $\ell$ into image $\ell'$ as in the figure below. A feature $R$ in the first image co-variantly transform into a feature $R'=w[R]$ in the second image: @image html covdet-normalization.png "Normalization removes the effect of an image deformation." The poses $u,u'$ of $R=u[R_0]$ and $R' = u'[R_0]$ are then related by the simple expression: \[ u' = w \circ u. \] @section covdet-frame-normalization Normalizing feature frames In the example above, the poses $u$ and $u'$ relate the two occurrences $R$ and $R'$ of the feature to its canonical version $R_0$. If the pose $u$ of the feature in image $\ell$ is known, the canonical feature appearance can be computed by un-warping it: \[ \ell_0 = u^{-1}[\ell] = \ell \circ u. \] This process is known as **normalization** and is the key in the computation of invariant feature descriptors as well as in the construction of most co-variant detectors. @page covdet-principles Principles of covariant detection @tableofcontents The goals of a co-variant detector were discussed in @ref covdet-fundamentals. This page introduces a few general principles that are at the basis of most covariant detection algorithms. Consider an input image $\ell$ and a two dimensional continuous and invertible warp $w$. The *warped image* $w[\ell]$ is defined to be \[ w[\ell] = \ell \circ w^{-1}, \] or, equivalently, \[ w[\ell](x,y) = \ell(w^{-1}(x,y)), \qquad \forall (x,y)\in\real^2. \] Note that, while $w$ pushes pixels forward, from the original to the transformed image domain, defining the transformed image $\ell'$ requires inverting the warp and composing $\ell$ with $w^{-1}$. The goal a covariant detector is to extract the same local features irregardless of image transformations. The detector is said to be covariant or equivariant with a class of warps $w\in\mathcal{W}$ if, when the feature $R$ is detected in image $\ell$, then the transformed feature $w[R]$ is detected in the transformed image $w[\ell]$. The net effect is that a covariant feature detector appears to “track” image transformations; however, it is important to note that a detector *is not a tracker* because it processes images individually rather than jointly as part of a sequence. An intuitive way to construct a covariant feature detector is to extract features in correspondence of images structures that are easily identifiable even after a transformation. Example of specific structures include dots, corners, and blobs. These will be generically indicated as **corners** in the followup. A covariant detector faces two challenges. First, corners have, in practice, an infinite variety of individual appearances and the detector must be able to capture them to be of general applicability. Second, the way corners are identified and detected must remain stable under transformations of the image. These two problems are addressed in @ref covdet-cornerness-localmax and @ref covdet-cornerness-normalization respectively. @section covdet-cornerness Detection using a cornerness measure One way to decide whether an image region $R$ contains a corner is to compare the local appearance to a model or template of the corner; the result of this comparisons produces a *cornerness score* at that location. This page describe general theoretical properties of the cornerness and the detection process. Concrete examples of cornerness are given in @ref covdet-corner-types. A **cornerness measure** associate a score to all possible feature locations in an image $\ell$. As described in @ref covdet-frame, the location or, more in general, pose $u$ of a feature $R$ is the warp $w$ that maps the canonical feature frame $R_0$ to $R$: \[ R = u[R_0]. \] The goal of a cornerness measure is to associate a score $F(u;\ell)$ to all possible feature poses $u$ and use this score to extract a finite number of co-variant features from any image. @subsection covdet-cornerness-localmax Local maxima of a cornerness measure Given the cornerness of each candidate feature, the detector must extract a finite number of them. However, the cornerness of features with nearly identical pose must be similar (otherwise the cornerness measure would be unstable). As such, simply thresholding $F(w;\ell)$ would detect an infinite number of nearly identical features rather than a finite number. The solution is to detect features in correspondence of the local maxima of the score measure: \[ \{w_1,\dots,w_n\} = \operatorname{localmax}_{w\in\mathcal{W}} F(w;\ell). \] This also means that features are never detected in isolation, but by comparing neighborhoods of them. @subsection covdet-cornerness-normalization Covariant detection by normalization The next difficulty is to guarantee that detection is co-variant with image transformations. Hence, if $u$ is the pose of a feature extracted from image $\ell$, then the transformed pose $u' = w[u]$ must be detected in the transformed image $\ell' = w[\ell]$. Since features are extracted in correspondence of the local maxima of the cornerness score, a sufficient condition is that corresponding features attain the same score in the two images: \[ \forall u\in\mathcal{W}: \quad F(u;\ell) = F(w[u];w[\ell]), \qquad\text{or}\qquad F(u;\ell) = F(w \circ u ;\ell \circ w^{-1}). \] One simple way to satisfy this equation is to compute a cornerness score *after normalizing the image* by the inverse of the candidate feature pose warp $u$, as follows: \[ F(u;\ell) = F(1;u^{-1}[\ell]) = F(1;\ell \circ u) = \mathcal{F}(\ell \circ u), \] where $1 = u^{-1} \circ u$ is the identity transformation and $\mathcal{F}$ is an arbitrary functional. Intuitively, co-variant detection is obtained by looking if the appearance of the feature resembles a corner only *after normalization*. Formally: @f{align*} F(w[u];w[\ell]) &= F(w \circ u ;\ell \circ w^{-1}) \\ &= F(1; \ell \circ w^{-1} \circ w \circ u) \\ &= \mathcal{F}(\ell\circ u) \\ &= F(u;\ell). @f} Concrete examples of the functional $\mathcal{F}$ are given in @ref covdet-corner-types. @subsection covdet-locality Locality of the detected features In the definition above, the cornenress functional $\mathcal{F}$ is an arbitrary functional of the entire normalized image $u^{-1}[\ell]$. In practice, one is always interested in detecting **local features** (at the very least because the image extent is finite). This is easily obtained by considering a cornerness $\mathcal{F}$ which only looks in a small region of the normalized image, usually corresponding to the extent of the canonical feature $R_0$ (e.g. a unit disc centered at the origin). In this case the extent of the local feature in the original image is simply given by $R = u[R_0]$. @section covdet-partial Partial and iterated normalization Practical detectors implement variants of the ideas above. Very often, for instance, detection is an iterative process, in which successive parameters of the pose of a feature are determined. For instance, it is typical to first detect the location and scale of a feature using a rotation-invariant cornerness score $\mathcal{F}$. Once these two parameters are known, the rotation can be determined using a different score, sensitive to the orientation of the local image structures. Certain detectors (such as Harris-Laplace and Hessian-Laplace) use even more sophisticated schemes, in which different scores are used to jointly (rather than in succession) different parameters of the pose of a feature, such as its translation and scale. While a formal treatment of these cases is possible as well, we point to the original papers. @page covdet-differential Differential and integral image operations @tableofcontents Dealing with covariant interest point detector requires working a good deal with derivatives, convolutions, and transformations of images. The notation and fundamental properties of interest here are discussed next. @section covdet-derivatives Derivative operations: gradients For the derivatives, we borrow the notation of @cite{kinghorn96integrals}. Let $f: \mathbb{R}^m \rightarrow \mathbb{R}^n, \bx \mapsto f(\bx)$ be a vector function. The derivative of the function with respect to $\bx$ is given by its *Jacobian matrix* denoted by the symbol: \[ \frac{\partial f}{\partial \bx^\top} = \begin{bmatrix} \frac{\partial f_1}{x_1} & \frac{\partial f_1}{x_2} & \dots \\ \frac{\partial f_2}{x_1} & \frac{\partial f_2}{x_2} & \dots \\ \vdots & \vdots & \ddots \\ \end{bmatrix}. \] When the function $ f $ is scalar ($n=1$), the Jacobian is the same as the gradient of the function (or, in fact, its transpose). More precisely, the gradient $\nabla f $ of $ f $ denotes the column vector of partial derivatives: \[ \nabla f = \frac{\partial f}{\partial \bx} = \begin{bmatrix} \frac{\partial f}{\partial x_1} \\ \frac{\partial f}{\partial x_2} \\ \vdots \end{bmatrix}. \] The second derivative $H_f $ of a scalar function $ f $, or Hessian, is denoted as \[ H_f = \frac{\partial f}{\partial \bx \partial \bx^\top} = \frac{\partial \nabla f}{\partial \bx^\top} = \begin{bmatrix} \frac{\partial f}{\partial x_1 \partial x_1} & \frac{\partial f}{\partial x_1 \partial x_2} & \dots \\ \frac{\partial f}{\partial x_2 \partial x_1} & \frac{\partial f}{\partial x_2 \partial x_2} & \dots \\ \vdots & \vdots & \ddots \\ \end{bmatrix}. \] The determinant of the Hessian is also known as Laplacian and denoted as \[ \Delta f = \operatorname{det} H_f = \frac{\partial f}{\partial x_1^2} + \frac{\partial f}{\partial x_2^2} + \dots \] @subsection covdet-derivative-transformations Derivative and image warps In the following, we will often been interested in domain warpings $u: \mathbb{R}^m \rightarrow \mathbb{R}^n, \bx \mapsto u(\bx)$ of a function $f(\bar\bx) $ and its effect on the derivatives of the function. The key transformation is the chain rule: \[ \frac{\partial f \circ u}{\partial \bx^\top} = \left(\frac{\partial f}{\partial \bar\bx^\top} \circ u\right) \frac{\partial u}{\partial \bx^\top} \] In particular, for an affine transformation $u = (A,T) : \bx \mapsto A\bx + T$, one obtains the transformation rules: \[ \begin{align*} \frac{\partial f \circ (A,T)}{\partial \bx^\top} &= \left(\frac{\partial f}{\partial \bar\bx^\top} \circ (A,T)\right)A, \\ \nabla (f \circ (A,T)) &= A^\top (\nabla f) \circ (A,T), \\ H_{f \circ(A,T)} &= A^\top (H_f \circ (A,T)) A, \\ \Delta (f \circ(A,T)) &= \det(A)^2\, (\Delta f) \circ (A,T). \end{align*} \] @section covdet-smoothing Integral operations: smoothing In practice, given an image $\ell$ expressed in digital format, good derivative approximations can be computed only if the bandwidth of the image is limited and, in particular, compatible with the sampling density. Since it is unreasonable to expect real images to be band-limited, the bandwidth is artificially constrained by suitably smoothing the image prior to computing its derivatives. This is also interpreted as a form of regularization or as a way of focusing on the image content at a particular scale. Formally, we will focus on Gaussian smoothing kernels. For the 2D case $\bx\in\real^2$, the Gaussian kernel of covariance $\Sigma$ is given by \[ g_{\Sigma}(\bx) = \frac{1}{2\pi \sqrt{\det\Sigma}} \exp\left( - \frac{1}{2} \bx^\top \Sigma^{-1} \bx \right). \] The symbol $g_{\sigma^2}$ will be used to denote a Gaussian kernel with isotropic standard deviation $\sigma$, i.e. $\Sigma = \sigma^2 I$. Given an image $\ell$, the symbol $\ell_\Sigma$ will be used to denote the image smoothed by the Gaussian kernel of parameter $\Sigma$: \[ \ell_\Sigma(\bx) = (g_\Sigma * \ell)(\bx) = \int_{\real^m} g_\Sigma(\bx - \by) \ell(\by)\,d\by. \] @subsection covdet-smoothing-transformations Smoothing and image warps One advantage of Gaussian kernels is that they are (up to renormalization) closed under a linear warp: \[ |A|\, g_\Sigma \circ A = g_{A^{-1} \Sigma A^{-\top}} \] This also means that smoothing a warped image is the same as warping the result of smoothing the original image by a suitably adjusted Gaussian kernel: \[ g_{\Sigma} * (\ell \circ (A,T)) = (g_{A\Sigma A^\top} * \ell) \circ (A,T). \] @page covdet-corner-types Cornerness measures @tableofcontents The goal of a cornerness measure (@ref covdet-cornerness) is to associate to an image patch a score proportional to how strongly the patch contain a certain strucuture, for example a corner or a blob. This page reviews the most important cornerness measures as implemented in VLFeat: - @ref covdet-harris - @ref covdet-laplacian - @ref covdet-hessian This page makes use of notation introduced in @ref covdet-differential. @section covdet-harris Harris corners This section introduces the fist of the cornerness measure $\mathcal{F}[\ell]$. Recall (@ref covdet-cornerness) that the goal of this functional is to respond strongly to images $\ell$ of corner-like structure. Rather than explicitly encoding the appearance of corners, the idea of the Harris measure is to label as corner *any* image patch whose appearance is sufficiently distinctive to allow accurate localization. In particular, consider an image patch $\ell(\bx), \bx\in\Omega$, where $\Omega$ is a smooth circular window of radius approximately $\sigma_i$; at necessary condition for the patch to allow accurate localization is that even a small translation $\ell(\bx+\delta)$ causes the appearance to vary significantly (if not the origin and location $\delta$ would not be distinguishable from the image alone). This variation is measured by the sum of squared differences \[ E(\delta) = \int g_{\sigma_i^2}(\bx) (\ell_{\sigma_d^2}(\bx+\delta) - \ell_{\sigma_d^2}(\bx))^2 \,d\bx \] Note that images are compared at scale $\sigma_d$, known as *differentiation scale* for reasons that will be clear in a moment, and that the squared differences are summed over a window softly defined by $\sigma_i$, also known as *integration scale*. This function can be approximated as $E(\delta)\approx \delta^\top M[\ell;\sigma_i^2,\sigma_d^2] \delta$ where \[ M[\ell;\sigma_i^2,\sigma_d^2] = \int g_{\sigma_i^2}(\bx) (\nabla \ell_{\sigma_d^2}(\bx)) (\nabla \ell_{\sigma_d^2}(\bx))^\top \, d\bx. \] is the so called **structure tensor**. A corner is identified when the sum of squared differences $E(\delta)$ is large for displacements $\delta$ in all directions. This condition is obtained when both the eignenvalues $\lambda_1,\lambda_2$ of the structure tensor $M$ are large. The **Harris cornerness measure** captures this fact: \[ \operatorname{Harris}[\ell;\sigma_i^2,\sigma_d^2] = \det M - \kappa \operatorname{trace}^2 M = \lambda_1\lambda_2 - \kappa (\lambda_1+\lambda_2)^2 \] @subsection covdet-harris-warped Harris in the warped domain The cornerness measure of a feature a location $u$ (recall that locations $u$ are in general defined as image warps) should be computed after normalizing the image (by applying to it the warp $u^{-1}$). This section shows that, for affine warps, the Harris cornerness measure can be computed directly in the Gaussian affine scale space of the image. In particular, for similarities, it can be computed in the standard Gaussian scale space. To this end, let $u=(A,T)$ be an affine warp identifying a feature location in image $\ell(\bx)$. Let $\bar\ell(\bar\bx) = \ell(A\bar\bx+T)$ be the normalized image and rewrite the structure tensor of the normalized image as follows: \[ M[\bar\ell; \bar\Sigma_i, \bar\Sigma_d] = M[\bar\ell; \bar\Sigma_i, \bar\Sigma_d](\mathbf{0}) = \left[ g_{\bar\Sigma_i} * (\nabla\bar\ell_{\bar\Sigma_d}) (\nabla\bar\ell_{\bar\Sigma_d})^\top \right](\mathbf{0}) \] This notation emphasizes that the structure tensor is obtained by taking derivatives and convolutions of the image. Using the fact that $\nabla g_{\bar\Sigma_d} * \bar\ell = A^\top (\nabla g_{A\bar\Sigma A^\top} * \ell) \circ (A,T)$ and that $g_{\bar\Sigma} * \bar \ell = (g_{A\bar\Sigma A^\top} * \ell) \circ (A,T)$, we get the equivalent expression: \[ M[\bar\ell; \bar\Sigma_i, \bar\Sigma_d](\mathbf{0}) = A^\top \left[ g_{A\bar\Sigma_i A^\top} * (\nabla\ell_{A\bar\Sigma_dA^\top})(\nabla\ell_{A\bar\Sigma_d A^\top})^\top \right](A\mathbf{0}+T) A. \] In other words, the structure tensor of the normalized image can be computed as: \[ M[\bar\ell; \bar\Sigma_i, \bar\Sigma_d](\mathbf{0}) = A^\top M[\ell; \Sigma_i, \Sigma_d](T) A, \quad \Sigma_{i} = A\bar\Sigma_{i}A^\top, \quad \Sigma_{d} = A\bar\Sigma_{d}A^\top. \] This equation allows to compute the structure tensor for feature at all locations directly in the original image. In particular, features at all translations $T$ can be evaluated efficiently by computing convolutions and derivatives of the image $\ell_{A\bar\Sigma_dA^\top}$. A case of particular instance is when $\bar\Sigma_i= \bar\sigma_i^2 I$ and $\bar\Sigma_d = \bar\sigma_d^2$ are both isotropic covariance matrices and the affine transformation is a similarity $A=sR$. Using the fact that $\det\left( s^2 R^\top M R \right)= s^4 \det M$ and $\operatorname{tr}\left(s^2 R^\top M R\right) = s^2 \operatorname{tr} M$, one obtains the relation \[ \operatorname{Harris}[\bar \ell;\bar\sigma_i^2,\bar\sigma_d^2] = s^4 \operatorname{Harris}[\ell;s^2\bar\sigma_i^2,s^2\bar\sigma_d^2](T). \] This equation indicates that, for similarity transformations, not only the structure tensor, but directly the Harris cornerness measure can be computed on the original image and then be transferred back to the normalized domain. Note, however, that this requires rescaling the measure by the factor $s^4$. Another important consequence of this relation is that the Harris measure is invariant to pure image rotations. It cannot, therefore, be used to associate an orientation to the detected features. @section covdet-hessian Hessian blobs The *(determinant of the) Hessian* cornerness measure is given determinant of the Hessian of the image: \[ \operatorname{DetHess}[\ell;\sigma_d^2] = \det H_{g_{\sigma_d^2} * \ell}(\mathbf{0}) \] This number is large and positive if the image is locally curved (peaked), roughly corresponding to blob-like structures in the image. In particular, a large score requires the product of the eigenvalues of the Hessian to be large, which requires both of them to have the same sign and are large in absolute value. @subsection covdet-hessian-warped Hessian in the warped domain Similarly to the Harris measure, it is possible to work with the Hessian measure on the original unnormalized image. As before, let $\bar\ell(\bar\bx) = \ell(A\bar\bx+T)$ be the normalized image and rewrite the Hessian of the normalized image as follows: \[ H_{g_{\bar\Sigma_d} * \bar\ell}(\mathbf{0}) = A^\top \left(H_{g_{\Sigma_d} * \ell}(T)\right) A. \] Then \[ \operatorname{DetHess}[\bar\ell;\bar\Sigma_d] = (\det A)^2 \operatorname{DetHess}[\ell;A\bar\Sigma_d A^\top](T). \] In particular, for isotropic covariance matrices and similarity transformations $A=sR$: \[ \operatorname{DetHess}[\bar\ell;\bar\sigma_d^2] = s^4 \operatorname{DetHess}[\ell;s^2\bar\sigma_d^2](T) \] @section covdet-laplacian Laplacian and Difference of Gaussians blobs The **Laplacian of Gaussian (LoG)** or **trace of the Hessian** cornerness measure is given by the trace of the Hessian of the image: \[ \operatorname{Lap}[\ell;\sigma_d^2] = \operatorname{tr} H_{g_{\sigma_d}^2 * \ell} \] @subsection covdet-laplacian-warped Laplacian in the warped domain Similarly to the Hessian measure, the Laplacian cornenress can often be efficiently computed for features at all locations in the original unnormalized image domain. In particular, if the derivative covariance matrix $\Sigma_d$ is isotropic and one considers as warpings similarity transformations $A=sR$, where $R$ is a rotatin and $s$ a rescaling, one has \[ \operatorname{Lap}[\bar\ell;\bar\sigma_d^2] = s^2 \operatorname{Lap}[\ell;s^2\bar\sigma_d^2](T) \] Note that, comparing to the Harris and determinant of Hessian measures, the scaling for the Laplacian is $s^2$ rather than $s^4$. @subsection covdet-laplacian-matched Laplacian as a matched filter The Laplacian is given by the trace of the Hessian operator. Differently from the determinant of the Hessian, this is a linear operation. This means that computing the Laplacian cornerness measure can be seen as applying a linear filtering operator to the image. This filter can then be interpreted as a *template* of a corner being matched to the image. Hence, the Laplacian cornerness measure can be interpreted as matching this corner template at all possible image locations. To see this formally, compute the Laplacian score in the input image domain: \[ \operatorname{Lap}[\bar\ell;\bar\sigma_d^2] = s^2 \operatorname{Lap}[\ell;s^2\bar\sigma_d^2](T) = s^2 (\Delta g_{s^2\bar\sigma_d^2} * \ell)(T) \] The Laplacian fitler is obtained by moving the Laplacian operator from the image to the Gaussian smoothing kernel: \[ s^2 (\Delta g_{s^2\bar\sigma_d^2} * \ell) = (s^2 \Delta g_{s^2\bar\sigma_d^2}) * \ell \] Note that the filter is rescaled by the $s^2$; sometimes, this factor is incorporated in the Laplacian operator, yielding the so-called normalized Laplacian. The Laplacian of Gaussian is also called *top-hat function* and has the expression: \[ \Delta g_{\sigma^2}(x,y) = \frac{x^2+y^2 - 2 \sigma^2}{\sigma^4} g_{\sigma^2}(x,y). \] This filter, which acts as corner template, resembles a blob (a dark disk surrounded by a bright ring). @subsection covdet-laplacian-dog Difference of Gaussians The **Difference of Gaussian** (DoG) cornerness measure can be interpreted as an approximation of the Laplacian that is easy to obtain once a scalespace of the input image has been computed. As noted above, the Laplacian cornerness of the normalized feature can be computed directly from the input image by convolving the image by the normalized Laplacian of Gaussian filter $s^2 \Delta g_{s^2\bar\sigma_d^2}$. Like the other derivative operators, this filter is simpe to discriteize. However, it is often approximated by computing the the *Difference of Gaussians* (DoG) approximation instead. This approximation is obtained from the easily-proved identity: \[ \frac{\partial}{\partial \sigma} g_{\sigma^2} = \sigma \Delta g_{\sigma^2}. \] This indicates that computing the normalized Laplacian of a Gaussian filter is, in the limit, the same as taking the difference between Gaussian filters of slightly increasing standard deviation $\sigma$ and $\kappa\sigma$, where $\kappa \approx 1$: \[ \sigma^2 \Delta g_{\sigma^2} \approx \sigma \frac{g_{(\kappa\sigma)^2} - g_{\sigma^2}}{\kappa\sigma - \sigma} = \frac{1}{\kappa - 1} (g_{(\kappa\sigma)^2} - g_{\sigma^2}). \] One nice propery of this expression is that the factor $\sigma$ cancels out in the right-hand side. Usually, scales $\sigma$ and $\kappa\sigma$ are pre-computed in the image scale-space and successive scales are sampled with uniform geometric spacing, meaning that the factor $\kappa$ is the same for all scales. Then, up to a overall scaling factor, the LoG cornerness measure can be obtained by taking the difference of successive scale space images $\ell_{(\kappa\sigma)^2}$ and $\ell_{\sigma^2}$. @page covdet-affine-adaptation Affine adaptation @page covdet-dominant-orientation Dominant orientation **/ #include "covdet.h" #include /** @brief Reallocate buffer ** @param buffer ** @param bufferSize ** @param targetSize ** @return error code **/ static int _vl_resize_buffer (void ** buffer, vl_size * bufferSize, vl_size targetSize) { void * newBuffer ; if (*buffer == NULL) { *buffer = vl_malloc(targetSize) ; if (*buffer) { *bufferSize = targetSize ; return VL_ERR_OK ; } else { *bufferSize = 0 ; return VL_ERR_ALLOC ; } } newBuffer = vl_realloc(*buffer, targetSize) ; if (newBuffer) { *buffer = newBuffer ; *bufferSize = targetSize ; return VL_ERR_OK ; } else { return VL_ERR_ALLOC ; } } /** @brief Enlarge buffer ** @param buffer ** @param bufferSize ** @param targetSize ** @return error code **/ static int _vl_enlarge_buffer (void ** buffer, vl_size * bufferSize, vl_size targetSize) { if (*bufferSize >= targetSize) return VL_ERR_OK ; return _vl_resize_buffer(buffer,bufferSize,targetSize) ; } /* ---------------------------------------------------------------- */ /* Finding local extrema */ /* ---------------------------------------------------------------- */ /* Todo: make this generally available in the library */ typedef struct _VlCovDetExtremum2 { vl_index xi ; vl_index yi ; float x ; float y ; float peakScore ; float edgeScore ; } VlCovDetExtremum2 ; typedef struct _VlCovDetExtremum3 { vl_index xi ; vl_index yi ; vl_index zi ; float x ; float y ; float z ; float peakScore ; float edgeScore ; } VlCovDetExtremum3 ; VL_EXPORT vl_size vl_find_local_extrema_3 (vl_index ** extrema, vl_size * bufferSize, float const * map, vl_size width, vl_size height, vl_size depth, double threshold) ; VL_EXPORT vl_size vl_find_local_extrema_2 (vl_index ** extrema, vl_size * bufferSize, float const * map, vl_size width, vl_size height, double threshold) ; VL_EXPORT vl_bool vl_refine_local_extreum_3 (VlCovDetExtremum3 * refined, float const * map, vl_size width, vl_size height, vl_size depth, vl_index x, vl_index y, vl_index z) ; VL_EXPORT vl_bool vl_refine_local_extreum_2 (VlCovDetExtremum2 * refined, float const * map, vl_size width, vl_size height, vl_index x, vl_index y) ; /** @internal ** @brief Find the extrema of a 3D function ** @param extrema buffer containing the extrema found (in/out). ** @param bufferSize size of the @a extrema buffer in bytes (in/out). ** @param map a 3D array representing the map. ** @param width of the map. ** @param height of the map. ** @param depth of the map. ** @param threshold minumum extremum value. ** @return number of extrema found. ** @see @ref ::vl_refine_local_extreum_2. **/ vl_size vl_find_local_extrema_3 (vl_index ** extrema, vl_size * bufferSize, float const * map, vl_size width, vl_size height, vl_size depth, double threshold) { vl_index x, y, z ; vl_size const xo = 1 ; vl_size const yo = width ; vl_size const zo = width * height ; float const *pt = map + xo + yo + zo ; vl_size numExtrema = 0 ; vl_size requiredSize = 0 ; #define CHECK_NEIGHBORS_3(v,CMP,SGN) (\ v CMP ## = SGN threshold && \ v CMP *(pt + xo) && \ v CMP *(pt - xo) && \ v CMP *(pt + zo) && \ v CMP *(pt - zo) && \ v CMP *(pt + yo) && \ v CMP *(pt - yo) && \ \ v CMP *(pt + yo + xo) && \ v CMP *(pt + yo - xo) && \ v CMP *(pt - yo + xo) && \ v CMP *(pt - yo - xo) && \ \ v CMP *(pt + xo + zo) && \ v CMP *(pt - xo + zo) && \ v CMP *(pt + yo + zo) && \ v CMP *(pt - yo + zo) && \ v CMP *(pt + yo + xo + zo) && \ v CMP *(pt + yo - xo + zo) && \ v CMP *(pt - yo + xo + zo) && \ v CMP *(pt - yo - xo + zo) && \ \ v CMP *(pt + xo - zo) && \ v CMP *(pt - xo - zo) && \ v CMP *(pt + yo - zo) && \ v CMP *(pt - yo - zo) && \ v CMP *(pt + yo + xo - zo) && \ v CMP *(pt + yo - xo - zo) && \ v CMP *(pt - yo + xo - zo) && \ v CMP *(pt - yo - xo - zo) ) for (z = 1 ; z < (signed)depth - 1 ; ++z) { for (y = 1 ; y < (signed)height - 1 ; ++y) { for (x = 1 ; x < (signed)width - 1 ; ++x) { float value = *pt ; if (CHECK_NEIGHBORS_3(value,>,+) || CHECK_NEIGHBORS_3(value,<,-)) { numExtrema ++ ; requiredSize += sizeof(vl_index) * 3 ; if (*bufferSize < requiredSize) { int err = _vl_resize_buffer((void**)extrema, bufferSize, requiredSize + 2000 * 3 * sizeof(vl_index)) ; if (err != VL_ERR_OK) abort() ; } (*extrema) [3 * (numExtrema - 1) + 0] = x ; (*extrema) [3 * (numExtrema - 1) + 1] = y ; (*extrema) [3 * (numExtrema - 1) + 2] = z ; } pt += xo ; } pt += 2*xo ; } pt += 2*yo ; } return numExtrema ; } /** @internal ** @brief Find extrema in a 2D function ** @param extrema buffer containing the found extrema (in/out). ** @param bufferSize size of the @a extrema buffer in bytes (in/out). ** @param map a 3D array representing the map. ** @param width of the map. ** @param height of the map. ** @param threshold minumum extremum value. ** @return number of extrema found. ** ** An extremum contains 2 ::vl_index values; they are arranged ** sequentially. ** ** The function can reuse an already allocated buffer if ** @a extrema and @a bufferSize are initialized on input. ** It may have to @a realloc the memory if the buffer is too small. **/ vl_size vl_find_local_extrema_2 (vl_index ** extrema, vl_size * bufferSize, float const* map, vl_size width, vl_size height, double threshold) { vl_index x, y ; vl_size const xo = 1 ; vl_size const yo = width ; float const *pt = map + xo + yo ; vl_size numExtrema = 0 ; vl_size requiredSize = 0 ; #define CHECK_NEIGHBORS_2(v,CMP,SGN) (\ v CMP ## = SGN threshold && \ v CMP *(pt + xo) && \ v CMP *(pt - xo) && \ v CMP *(pt + yo) && \ v CMP *(pt - yo) && \ \ v CMP *(pt + yo + xo) && \ v CMP *(pt + yo - xo) && \ v CMP *(pt - yo + xo) && \ v CMP *(pt - yo - xo) ) for (y = 1 ; y < (signed)height - 1 ; ++y) { for (x = 1 ; x < (signed)width - 1 ; ++x) { float value = *pt ; if (CHECK_NEIGHBORS_2(value,>,+) || CHECK_NEIGHBORS_2(value,<,-)) { numExtrema ++ ; requiredSize += sizeof(vl_index) * 2 ; if (*bufferSize < requiredSize) { int err = _vl_resize_buffer((void**)extrema, bufferSize, requiredSize + 2000 * 2 * sizeof(vl_index)) ; if (err != VL_ERR_OK) abort() ; } (*extrema) [2 * (numExtrema - 1) + 0] = x ; (*extrema) [2 * (numExtrema - 1) + 1] = y ; } pt += xo ; } pt += 2*xo ; } return numExtrema ; } /** @internal ** @brief Refine the location of a local extremum of a 3D map ** @param refined refined extremum (out). ** @param map a 3D array representing the map. ** @param width of the map. ** @param height of the map. ** @param depth of the map. ** @param x initial x position. ** @param y initial y position. ** @param z initial z position. ** @return a flat indicating whether the extrema refinement was stable. **/ VL_EXPORT vl_bool vl_refine_local_extreum_3 (VlCovDetExtremum3 * refined, float const * map, vl_size width, vl_size height, vl_size depth, vl_index x, vl_index y, vl_index z) { vl_size const xo = 1 ; vl_size const yo = width ; vl_size const zo = width * height ; double Dx=0,Dy=0,Dz=0,Dxx=0,Dyy=0,Dzz=0,Dxy=0,Dxz=0,Dyz=0 ; double A [3*3], b [3] ; #define at(dx,dy,dz) (*(pt + (dx)*xo + (dy)*yo + (dz)*zo)) #define Aat(i,j) (A[(i)+(j)*3]) float const * pt ; vl_index dx = 0 ; vl_index dy = 0 ; /*vl_index dz = 0 ;*/ vl_index iter ; int err ; assert (map) ; assert (1 <= x && x <= (signed)width - 2) ; assert (1 <= y && y <= (signed)height - 2) ; assert (1 <= z && z <= (signed)depth - 2) ; for (iter = 0 ; iter < 5 ; ++iter) { x += dx ; y += dy ; pt = map + x*xo + y*yo + z*zo ; /* compute the gradient */ Dx = 0.5 * (at(+1,0,0) - at(-1,0,0)) ; Dy = 0.5 * (at(0,+1,0) - at(0,-1,0)); Dz = 0.5 * (at(0,0,+1) - at(0,0,-1)) ; /* compute the Hessian */ Dxx = (at(+1,0,0) + at(-1,0,0) - 2.0 * at(0,0,0)) ; Dyy = (at(0,+1,0) + at(0,-1,0) - 2.0 * at(0,0,0)) ; Dzz = (at(0,0,+1) + at(0,0,-1) - 2.0 * at(0,0,0)) ; Dxy = 0.25 * (at(+1,+1,0) + at(-1,-1,0) - at(-1,+1,0) - at(+1,-1,0)) ; Dxz = 0.25 * (at(+1,0,+1) + at(-1,0,-1) - at(-1,0,+1) - at(+1,0,-1)) ; Dyz = 0.25 * (at(0,+1,+1) + at(0,-1,-1) - at(0,-1,+1) - at(0,+1,-1)) ; /* solve linear system */ Aat(0,0) = Dxx ; Aat(1,1) = Dyy ; Aat(2,2) = Dzz ; Aat(0,1) = Aat(1,0) = Dxy ; Aat(0,2) = Aat(2,0) = Dxz ; Aat(1,2) = Aat(2,1) = Dyz ; b[0] = - Dx ; b[1] = - Dy ; b[2] = - Dz ; err = vl_solve_linear_system_3(b, A, b) ; if (err != VL_ERR_OK) { b[0] = 0 ; b[1] = 0 ; b[2] = 0 ; break ; } /* Keep going if there is sufficient translation */ dx = (b[0] > 0.6 && x < (signed)width - 2 ? 1 : 0) + (b[0] < -0.6 && x > 1 ? -1 : 0) ; dy = (b[1] > 0.6 && y < (signed)height - 2 ? 1 : 0) + (b[1] < -0.6 && y > 1 ? -1 : 0) ; if (dx == 0 && dy == 0) break ; } /* check threshold and other conditions */ { double peakScore = at(0,0,0) + 0.5 * (Dx * b[0] + Dy * b[1] + Dz * b[2]) ; double alpha = (Dxx+Dyy)*(Dxx+Dyy) / (Dxx*Dyy - Dxy*Dxy) ; double edgeScore ; if (alpha < 0) { /* not an extremum */ edgeScore = VL_INFINITY_D ; } else { edgeScore = (0.5*alpha - 1) + sqrt(VL_MAX(0.25*alpha - 1,0)*alpha) ; } refined->xi = x ; refined->yi = y ; refined->zi = z ; refined->x = x + b[0] ; refined->y = y + b[1] ; refined->z = z + b[2] ; refined->peakScore = peakScore ; refined->edgeScore = edgeScore ; return err == VL_ERR_OK && vl_abs_d(b[0]) < 1.5 && vl_abs_d(b[1]) < 1.5 && vl_abs_d(b[2]) < 1.5 && 0 <= refined->x && refined->x <= (signed)width - 1 && 0 <= refined->y && refined->y <= (signed)height - 1 && 0 <= refined->z && refined->z <= (signed)depth - 1 ; } #undef Aat #undef at } /** @internal ** @brief Refine the location of a local extremum of a 2D map ** @param refined refined extremum (out). ** @param map a 2D array representing the map. ** @param width of the map. ** @param height of the map. ** @param x initial x position. ** @param y initial y position. ** @return a flat indicating whether the extrema refinement was stable. **/ VL_EXPORT vl_bool vl_refine_local_extreum_2 (VlCovDetExtremum2 * refined, float const * map, vl_size width, vl_size height, vl_index x, vl_index y) { vl_size const xo = 1 ; vl_size const yo = width ; double Dx=0,Dy=0,Dxx=0,Dyy=0,Dxy=0; double A [2*2], b [2] ; #define at(dx,dy) (*(pt + (dx)*xo + (dy)*yo )) #define Aat(i,j) (A[(i)+(j)*2]) float const * pt ; vl_index dx = 0 ; vl_index dy = 0 ; vl_index iter ; int err ; assert (map) ; assert (1 <= x && x <= (signed)width - 2) ; assert (1 <= y && y <= (signed)height - 2) ; for (iter = 0 ; iter < 5 ; ++iter) { x += dx ; y += dy ; pt = map + x*xo + y*yo ; /* compute the gradient */ Dx = 0.5 * (at(+1,0) - at(-1,0)) ; Dy = 0.5 * (at(0,+1) - at(0,-1)); /* compute the Hessian */ Dxx = (at(+1,0) + at(-1,0) - 2.0 * at(0,0)) ; Dyy = (at(0,+1) + at(0,-1) - 2.0 * at(0,0)) ; Dxy = 0.25 * (at(+1,+1) + at(-1,-1) - at(-1,+1) - at(+1,-1)) ; /* solve linear system */ Aat(0,0) = Dxx ; Aat(1,1) = Dyy ; Aat(0,1) = Aat(1,0) = Dxy ; b[0] = - Dx ; b[1] = - Dy ; err = vl_solve_linear_system_2(b, A, b) ; if (err != VL_ERR_OK) { b[0] = 0 ; b[1] = 0 ; break ; } /* Keep going if there is sufficient translation */ dx = (b[0] > 0.6 && x < (signed)width - 2 ? 1 : 0) + (b[0] < -0.6 && x > 1 ? -1 : 0) ; dy = (b[1] > 0.6 && y < (signed)height - 2 ? 1 : 0) + (b[1] < -0.6 && y > 1 ? -1 : 0) ; if (dx == 0 && dy == 0) break ; } /* check threshold and other conditions */ { double peakScore = at(0,0) + 0.5 * (Dx * b[0] + Dy * b[1]) ; double alpha = (Dxx+Dyy)*(Dxx+Dyy) / (Dxx*Dyy - Dxy*Dxy) ; double edgeScore ; if (alpha < 0) { /* not an extremum */ edgeScore = VL_INFINITY_D ; } else { edgeScore = (0.5*alpha - 1) + sqrt(VL_MAX(0.25*alpha - 1,0)*alpha) ; } refined->xi = x ; refined->yi = y ; refined->x = x + b[0] ; refined->y = y + b[1] ; refined->peakScore = peakScore ; refined->edgeScore = edgeScore ; return err == VL_ERR_OK && vl_abs_d(b[0]) < 1.5 && vl_abs_d(b[1]) < 1.5 && 0 <= refined->x && refined->x <= (signed)width - 1 && 0 <= refined->y && refined->y <= (signed)height - 1 ; } #undef Aat #undef at } /* ---------------------------------------------------------------- */ /* Covarant detector */ /* ---------------------------------------------------------------- */ #define VL_COVDET_MAX_NUM_ORIENTATIONS 4 #define VL_COVDET_MAX_NUM_LAPLACIAN_SCALES 4 #define VL_COVDET_AA_PATCH_RESOLUTION 20 #define VL_COVDET_AA_MAX_NUM_ITERATIONS 15 #define VL_COVDET_OR_NUM_ORIENTATION_HISTOGAM_BINS 36 #define VL_COVDET_AA_RELATIVE_INTEGRATION_SIGMA 3 #define VL_COVDET_AA_RELATIVE_DERIVATIVE_SIGMA 1 #define VL_COVDET_AA_MAX_ANISOTROPY 5 #define VL_COVDET_AA_CONVERGENCE_THRESHOLD 1.001 #define VL_COVDET_AA_ACCURATE_SMOOTHING VL_FALSE #define VL_COVDET_AA_PATCH_EXTENT (3*VL_COVDET_AA_RELATIVE_INTEGRATION_SIGMA) #define VL_COVDET_OR_ADDITIONAL_PEAKS_RELATIVE_SIZE 0.8 #define VL_COVDET_LAP_NUM_LEVELS 10 #define VL_COVDET_LAP_PATCH_RESOLUTION 16 #define VL_COVDET_LAP_DEF_PEAK_THRESHOLD 0.01 #define VL_COVDET_DOG_DEF_PEAK_THRESHOLD VL_COVDET_LAP_DEF_PEAK_THRESHOLD #define VL_COVDET_DOG_DEF_EDGE_THRESHOLD 10.0 #define VL_COVDET_HARRIS_DEF_PEAK_THRESHOLD 0.000002 #define VL_COVDET_HARRIS_DEF_EDGE_THRESHOLD 10.0 #define VL_COVDET_HESSIAN_DEF_PEAK_THRESHOLD 0.003 #define VL_COVDET_HESSIAN_DEF_EDGE_THRESHOLD 10.0 /** @brief Covariant feature detector */ struct _VlCovDet { VlScaleSpace *gss ; /**< Gaussian scale space. */ VlScaleSpace *css ; /**< Cornerness scale space. */ VlCovDetMethod method ; /**< feature extraction method. */ double peakThreshold ; /**< peak threshold. */ double edgeThreshold ; /**< edge threshold. */ double lapPeakThreshold; /**< peak threshold for Laplacian scale selection. */ vl_size octaveResolution ; /**< resolution of each octave. */ vl_index firstOctave ; /**< index of the first octave. */ double nonExtremaSuppression ; vl_size numNonExtremaSuppressed ; VlCovDetFeature *features ; vl_size numFeatures ; vl_size numFeatureBufferSize ; float * patch ; vl_size patchBufferSize ; vl_bool transposed ; VlCovDetFeatureOrientation orientations [VL_COVDET_MAX_NUM_ORIENTATIONS] ; VlCovDetFeatureLaplacianScale scales [VL_COVDET_MAX_NUM_LAPLACIAN_SCALES] ; vl_bool aaAccurateSmoothing ; float aaPatch [(2*VL_COVDET_AA_PATCH_RESOLUTION+1)*(2*VL_COVDET_AA_PATCH_RESOLUTION+1)] ; float aaPatchX [(2*VL_COVDET_AA_PATCH_RESOLUTION+1)*(2*VL_COVDET_AA_PATCH_RESOLUTION+1)] ; float aaPatchY [(2*VL_COVDET_AA_PATCH_RESOLUTION+1)*(2*VL_COVDET_AA_PATCH_RESOLUTION+1)] ; float aaMask [(2*VL_COVDET_AA_PATCH_RESOLUTION+1)*(2*VL_COVDET_AA_PATCH_RESOLUTION+1)] ; float lapPatch [(2*VL_COVDET_LAP_PATCH_RESOLUTION+1)*(2*VL_COVDET_LAP_PATCH_RESOLUTION+1)] ; float laplacians [(2*VL_COVDET_LAP_PATCH_RESOLUTION+1)*(2*VL_COVDET_LAP_PATCH_RESOLUTION+1)*VL_COVDET_LAP_NUM_LEVELS] ; vl_size numFeaturesWithNumScales [VL_COVDET_MAX_NUM_LAPLACIAN_SCALES + 1] ; } ; VlEnumerator vlCovdetMethods [VL_COVDET_METHOD_NUM] = { {"DoG" , (vl_index)VL_COVDET_METHOD_DOG }, {"Hessian", (vl_index)VL_COVDET_METHOD_HESSIAN }, {"HessianLaplace", (vl_index)VL_COVDET_METHOD_HESSIAN_LAPLACE }, {"HarrisLaplace", (vl_index)VL_COVDET_METHOD_HARRIS_LAPLACE }, {"MultiscaleHessian", (vl_index)VL_COVDET_METHOD_MULTISCALE_HESSIAN}, {"MultiscaleHarris", (vl_index)VL_COVDET_METHOD_MULTISCALE_HARRIS }, {0, 0 } } ; /** @brief Create a new object instance ** @param method method for covariant feature detection. ** @return new covariant detector. **/ VlCovDet * vl_covdet_new (VlCovDetMethod method) { VlCovDet * self = vl_calloc(sizeof(VlCovDet),1) ; self->method = method ; self->octaveResolution = 3 ; self->firstOctave = -1 ; switch (self->method) { case VL_COVDET_METHOD_DOG : self->peakThreshold = VL_COVDET_DOG_DEF_PEAK_THRESHOLD ; self->edgeThreshold = VL_COVDET_DOG_DEF_EDGE_THRESHOLD ; self->lapPeakThreshold = 0 ; /* not used */ break ; case VL_COVDET_METHOD_HARRIS_LAPLACE: case VL_COVDET_METHOD_MULTISCALE_HARRIS: self->peakThreshold = VL_COVDET_HARRIS_DEF_PEAK_THRESHOLD ; self->edgeThreshold = VL_COVDET_HARRIS_DEF_EDGE_THRESHOLD ; self->lapPeakThreshold = VL_COVDET_LAP_DEF_PEAK_THRESHOLD ; break ; case VL_COVDET_METHOD_HESSIAN : case VL_COVDET_METHOD_HESSIAN_LAPLACE: case VL_COVDET_METHOD_MULTISCALE_HESSIAN: self->peakThreshold = VL_COVDET_HESSIAN_DEF_PEAK_THRESHOLD ; self->edgeThreshold = VL_COVDET_HESSIAN_DEF_EDGE_THRESHOLD ; self->lapPeakThreshold = VL_COVDET_LAP_DEF_PEAK_THRESHOLD ; break; default: assert(0) ; } self->nonExtremaSuppression = 0.5 ; self->features = NULL ; self->numFeatures = 0 ; self->numFeatureBufferSize = 0 ; self->patch = NULL ; self->patchBufferSize = 0 ; self->transposed = VL_FALSE ; self->aaAccurateSmoothing = VL_COVDET_AA_ACCURATE_SMOOTHING ; { vl_index const w = VL_COVDET_AA_PATCH_RESOLUTION ; vl_index i,j ; double step = (2.0 * VL_COVDET_AA_PATCH_EXTENT) / (2*w+1) ; double sigma = VL_COVDET_AA_RELATIVE_INTEGRATION_SIGMA ; for (j = -w ; j <= w ; ++j) { for (i = -w ; i <= w ; ++i) { double dx = i*step/sigma ; double dy = j*step/sigma ; self->aaMask[(i+w) + (2*w+1)*(j+w)] = exp(-0.5*(dx*dx+dy*dy)) ; } } } { /* Covers one octave of Laplacian filters, from sigma=1 to sigma=2. The spatial sampling step is 0.5. */ vl_index s ; for (s = 0 ; s < VL_COVDET_LAP_NUM_LEVELS ; ++s) { double sigmaLap = pow(2.0, -0.5 + (double)s / (VL_COVDET_LAP_NUM_LEVELS - 1)) ; double const sigmaImage = 1.0 / sqrt(2.0) ; double const step = 0.5 * sigmaImage ; double const sigmaDelta = sqrt(sigmaLap*sigmaLap - sigmaImage*sigmaImage) ; vl_size const w = VL_COVDET_LAP_PATCH_RESOLUTION ; vl_size const num = 2 * w + 1 ; float * pt = self->laplacians + s * (num * num) ; memset(pt, 0, num * num * sizeof(float)) ; #define at(x,y) pt[(x+w)+(y+w)*(2*w+1)] at(0,0) = - 4.0 ; at(-1,0) = 1.0 ; at(+1,0) = 1.0 ; at(0,1) = 1.0 ; at(0,-1) = 1.0 ; #undef at vl_imsmooth_f(pt, num, pt, num, num, num, sigmaDelta / step, sigmaDelta / step) ; #if 0 { char name [200] ; snprintf(name, 200, "/tmp/%f-lap.pgm", sigmaDelta) ; vl_pgm_write_f(name, pt, num, num) ; } #endif } } return self ; } /** @brief Reset object ** @param self object. ** ** This function removes any buffered features and frees other ** internal buffers. **/ void vl_covdet_reset (VlCovDet * self) { if (self->features) { vl_free(self->features) ; self->features = NULL ; } if (self->css) { vl_scalespace_delete(self->css) ; self->css = NULL ; } if (self->gss) { vl_scalespace_delete(self->gss) ; self->gss = NULL ; } } /** @brief Delete object instance ** @param self object. **/ void vl_covdet_delete (VlCovDet * self) { vl_covdet_reset(self) ; if (self->patch) vl_free (self->patch) ; vl_free(self) ; } /** @brief Append a feature to the internal buffer. ** @param self object. ** @param feature a pointer to the feature to append. ** @return status. ** ** The feature is copied. The function may fail with @c status ** equal to ::VL_ERR_ALLOC if there is insufficient memory. **/ int vl_covdet_append_feature (VlCovDet * self, VlCovDetFeature const * feature) { vl_size requiredSize ; assert(self) ; assert(feature) ; self->numFeatures ++ ; requiredSize = self->numFeatures * sizeof(VlCovDetFeature) ; if (requiredSize > self->numFeatureBufferSize) { int err = _vl_resize_buffer((void**)&self->features, &self->numFeatureBufferSize, (self->numFeatures + 1000) * sizeof(VlCovDetFeature)) ; if (err) { self->numFeatures -- ; return err ; } } self->features[self->numFeatures - 1] = *feature ; return VL_ERR_OK ; } /* ---------------------------------------------------------------- */ /* Process a new image */ /* ---------------------------------------------------------------- */ /** @brief Detect features in an image ** @param self object. ** @param image image to process. ** @param width image width. ** @param height image height. ** @return status. ** ** @a width and @a height must be at least one pixel. The function ** fails by returing ::VL_ERR_ALLOC if the memory is insufficient. **/ int vl_covdet_put_image (VlCovDet * self, float const * image, vl_size width, vl_size height) { vl_size const minOctaveSize = 16 ; vl_index lastOctave ; vl_index octaveFirstSubdivision ; vl_index octaveLastSubdivision ; VlScaleSpaceGeometry geom = vl_scalespace_get_default_geometry(width,height) ; assert (self) ; assert (image) ; assert (width >= 1) ; assert (height >= 1) ; /* (minOctaveSize - 1) 2^lastOctave <= min(width,height) - 1 */ lastOctave = vl_floor_d(vl_log2_d(VL_MIN((double)width-1,(double)height-1) / (minOctaveSize - 1))) ; if (self->method == VL_COVDET_METHOD_DOG) { octaveFirstSubdivision = -1 ; octaveLastSubdivision = self->octaveResolution + 1 ; } else if (self->method == VL_COVDET_METHOD_HESSIAN) { octaveFirstSubdivision = -1 ; octaveLastSubdivision = self->octaveResolution ; } else { octaveFirstSubdivision = 0 ; octaveLastSubdivision = self->octaveResolution - 1 ; } geom.width = width ; geom.height = height ; geom.firstOctave = self->firstOctave ; geom.lastOctave = lastOctave ; geom.octaveResolution = self->octaveResolution ; geom.octaveFirstSubdivision = octaveFirstSubdivision ; geom.octaveLastSubdivision = octaveLastSubdivision ; if (self->gss == NULL || ! vl_scalespacegeometry_is_equal (geom, vl_scalespace_get_geometry(self->gss))) { if (self->gss) vl_scalespace_delete(self->gss) ; self->gss = vl_scalespace_new_with_geometry(geom) ; if (self->gss == NULL) return VL_ERR_ALLOC ; } vl_scalespace_put_image(self->gss, image) ; return VL_ERR_OK ; } /* ---------------------------------------------------------------- */ /* Cornerness measures */ /* ---------------------------------------------------------------- */ /** @brief Scaled derminant of the Hessian filter ** @param hessian output image. ** @param image input image. ** @param width image width. ** @param height image height. ** @param step image sampling step (pixel size). ** @param sigma Gaussian smoothing of the input image. **/ static void _vl_det_hessian_response (float * hessian, float const * image, vl_size width, vl_size height, double step, double sigma) { float factor = (float) pow(sigma/step, 4.0) ; vl_index const xo = 1 ; /* x-stride */ vl_index const yo = width; /* y-stride */ vl_size r, c; float p11, p12, p13, p21, p22, p23, p31, p32, p33; /* setup input pointer to be centered at 0,1 */ float const *in = image + yo ; /* setup output pointer to be centered at 1,1 */ float *out = hessian + xo + yo; /* move 3x3 window and convolve */ for (r = 1; r < height - 1; ++r) { /* fill in shift registers at the beginning of the row */ p11 = in[-yo]; p12 = in[xo - yo]; p21 = in[ 0]; p22 = in[xo ]; p31 = in[+yo]; p32 = in[xo + yo]; /* setup input pointer to (2,1) of the 3x3 square */ in += 2; for (c = 1; c < width - 1; ++c) { float Lxx, Lyy, Lxy; /* fetch remaining values (last column) */ p13 = in[-yo]; p23 = *in; p33 = in[+yo]; /* Compute 3x3 Hessian values from pixel differences. */ Lxx = (-p21 + 2*p22 - p23); Lyy = (-p12 + 2*p22 - p32); Lxy = ((p11 - p31 - p13 + p33)/4.0f); /* normalize and write out */ *out = (Lxx * Lyy - Lxy * Lxy) * factor ; /* move window */ p11=p12; p12=p13; p21=p22; p22=p23; p31=p32; p32=p33; /* move input/output pointers */ in++; out++; } out += 2; } /* Copy the computed values to borders */ in = hessian + yo + xo ; out = hessian + xo ; /* Top row without corners */ memcpy(out, in, (width - 2)*sizeof(float)); out--; in -= yo; /* Left border columns without last row */ for (r = 0; r < height - 1; r++){ *out = *in; *(out + yo - 1) = *(in + yo - 3); in += yo; out += yo; } /* Bottom corners */ in -= yo; *out = *in; *(out + yo - 1) = *(in + yo - 3); /* Bottom row without corners */ out++; memcpy(out, in, (width - 2)*sizeof(float)); } /** @brief Scale-normalised Harris response ** @param harris output image. ** @param image input image. ** @param width image width. ** @param height image height. ** @param step image sampling step (pixel size). ** @param sigma Gaussian smoothing of the input image. ** @param sigmaI integration scale. ** @param alpha factor in the definition of the Harris score. **/ static void _vl_harris_response (float * harris, float const * image, vl_size width, vl_size height, double step, double sigma, double sigmaI, double alpha) { float factor = (float) pow(sigma/step, 4.0) ; vl_index k ; float * LxLx ; float * LyLy ; float * LxLy ; LxLx = vl_malloc(sizeof(float) * width * height) ; LyLy = vl_malloc(sizeof(float) * width * height) ; LxLy = vl_malloc(sizeof(float) * width * height) ; vl_imgradient_f (LxLx, LyLy, 1, width, image, width, height, width) ; for (k = 0 ; k < (signed)(width * height) ; ++k) { float dx = LxLx[k] ; float dy = LyLy[k] ; LxLx[k] = dx*dx ; LyLy[k] = dy*dy ; LxLy[k] = dx*dy ; } vl_imsmooth_f(LxLx, width, LxLx, width, height, width, sigmaI / step, sigmaI / step) ; vl_imsmooth_f(LyLy, width, LyLy, width, height, width, sigmaI / step, sigmaI / step) ; vl_imsmooth_f(LxLy, width, LxLy, width, height, width, sigmaI / step, sigmaI / step) ; for (k = 0 ; k < (signed)(width * height) ; ++k) { float a = LxLx[k] ; float b = LyLy[k] ; float c = LxLy[k] ; float determinant = a * b - c * c ; float trace = a + b ; harris[k] = factor * (determinant - alpha * (trace * trace)) ; } vl_free(LxLy) ; vl_free(LyLy) ; vl_free(LxLx) ; } /** @brief Difference of Gaussian ** @param dog output image. ** @param level1 input image at the smaller Gaussian scale. ** @param level2 input image at the larger Gaussian scale. ** @param width image width. ** @param height image height. **/ static void _vl_dog_response (float * dog, float const * level1, float const * level2, vl_size width, vl_size height) { vl_index k ; for (k = 0 ; k < (signed)(width*height) ; ++k) { dog[k] = level2[k] - level1[k] ; } } /* ---------------------------------------------------------------- */ /* Detect features */ /* ---------------------------------------------------------------- */ /** @brief Detect scale-space features ** @param self object. ** ** This function runs the configured feature detector on the image ** that was passed by using ::vl_covdet_put_image. **/ void vl_covdet_detect (VlCovDet * self, vl_size max_num_features) { VlScaleSpaceGeometry geom = vl_scalespace_get_geometry(self->gss) ; VlScaleSpaceGeometry cgeom ; float * levelxx = NULL ; float * levelyy = NULL ; float * levelxy = NULL ; vl_index o, s ; assert (self) ; assert (self->gss) ; /* clear previous detections if any */ self->numFeatures = 0 ; /* prepare buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ cgeom = geom ; if (self->method == VL_COVDET_METHOD_DOG) { cgeom.octaveLastSubdivision -= 1 ; } if (!self->css || !vl_scalespacegeometry_is_equal(cgeom, vl_scalespace_get_geometry(self->css))) { if (self->css) vl_scalespace_delete(self->css) ; self->css = vl_scalespace_new_with_geometry(cgeom) ; } if (self->method == VL_COVDET_METHOD_HARRIS_LAPLACE || self->method == VL_COVDET_METHOD_MULTISCALE_HARRIS) { VlScaleSpaceOctaveGeometry oct = vl_scalespace_get_octave_geometry(self->gss, geom.firstOctave) ; levelxx = vl_malloc(oct.width * oct.height * sizeof(float)) ; levelyy = vl_malloc(oct.width * oct.height * sizeof(float)) ; levelxy = vl_malloc(oct.width * oct.height * sizeof(float)) ; } /* compute cornerness ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ for (o = cgeom.firstOctave ; o <= cgeom.lastOctave ; ++o) { VlScaleSpaceOctaveGeometry oct = vl_scalespace_get_octave_geometry(self->css, o) ; for (s = cgeom.octaveFirstSubdivision ; s <= cgeom.octaveLastSubdivision ; ++s) { float * level = vl_scalespace_get_level(self->gss, o, s) ; float * clevel = vl_scalespace_get_level(self->css, o, s) ; double sigma = vl_scalespace_get_level_sigma(self->css, o, s) ; switch (self->method) { case VL_COVDET_METHOD_DOG: _vl_dog_response(clevel, vl_scalespace_get_level(self->gss, o, s + 1), level, oct.width, oct.height) ; break ; case VL_COVDET_METHOD_HARRIS_LAPLACE: case VL_COVDET_METHOD_MULTISCALE_HARRIS: _vl_harris_response(clevel, level, oct.width, oct.height, oct.step, sigma, 1.4 * sigma, 0.05) ; break ; case VL_COVDET_METHOD_HESSIAN: case VL_COVDET_METHOD_HESSIAN_LAPLACE: case VL_COVDET_METHOD_MULTISCALE_HESSIAN: _vl_det_hessian_response(clevel, level, oct.width, oct.height, oct.step, sigma) ; break ; default: assert(0) ; } } } /* find and refine local maxima ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ { vl_index * extrema = NULL ; vl_size extremaBufferSize = 0 ; vl_size numExtrema ; vl_size index ; for (o = cgeom.lastOctave; o >= cgeom.firstOctave; --o) { VlScaleSpaceOctaveGeometry octgeom = vl_scalespace_get_octave_geometry(self->css, o) ; double step = octgeom.step ; vl_size width = octgeom.width ; vl_size height = octgeom.height ; vl_size depth = cgeom.octaveLastSubdivision - cgeom.octaveFirstSubdivision + 1 ; switch (self->method) { case VL_COVDET_METHOD_DOG: case VL_COVDET_METHOD_HESSIAN: { /* scale-space extrema */ float const * octave = vl_scalespace_get_level(self->css, o, cgeom.octaveFirstSubdivision) ; numExtrema = vl_find_local_extrema_3(&extrema, &extremaBufferSize, octave, width, height, depth, 0.8 * self->peakThreshold); for (index = 0 ; index < numExtrema ; ++index) { VlCovDetExtremum3 refined ; VlCovDetFeature feature ; vl_bool ok ; memset(&feature, 0, sizeof(feature)) ; ok = vl_refine_local_extreum_3(&refined, octave, width, height, depth, extrema[3*index+0], extrema[3*index+1], extrema[3*index+2]) ; ok &= fabs(refined.peakScore) > self->peakThreshold ; ok &= refined.edgeScore < self->edgeThreshold ; if (ok) { double sigma = cgeom.baseScale * pow(2.0, o + (refined.z + cgeom.octaveFirstSubdivision) / cgeom.octaveResolution) ; feature.frame.x = refined.x * step ; feature.frame.y = refined.y * step ; feature.frame.a11 = sigma ; feature.frame.a12 = 0.0 ; feature.frame.a21 = 0.0 ; feature.frame.a22 = sigma ; feature.o = o ; feature.s = round(refined.z) ; feature.peakScore = refined.peakScore ; feature.edgeScore = refined.edgeScore ; vl_covdet_append_feature(self, &feature) ; } } break ; } default: { for (s = cgeom.octaveFirstSubdivision ; s < cgeom.octaveLastSubdivision ; ++s) { /* space extrema */ float const * level = vl_scalespace_get_level(self->css,o,s) ; numExtrema = vl_find_local_extrema_2(&extrema, &extremaBufferSize, level, width, height, 0.8 * self->peakThreshold); for (index = 0 ; index < numExtrema ; ++index) { VlCovDetExtremum2 refined ; VlCovDetFeature feature ; vl_bool ok ; memset(&feature, 0, sizeof(feature)) ; ok = vl_refine_local_extreum_2(&refined, level, width, height, extrema[2*index+0], extrema[2*index+1]); ok &= fabs(refined.peakScore) > self->peakThreshold ; ok &= refined.edgeScore < self->edgeThreshold ; if (ok) { double sigma = cgeom.baseScale * pow(2.0, o + (double)s / cgeom.octaveResolution) ; feature.frame.x = refined.x * step ; feature.frame.y = refined.y * step ; feature.frame.a11 = sigma ; feature.frame.a12 = 0.0 ; feature.frame.a21 = 0.0 ; feature.frame.a22 = sigma ; feature.o = o ; feature.s = s ; feature.peakScore = refined.peakScore ; feature.edgeScore = refined.edgeScore ; vl_covdet_append_feature(self, &feature) ; } } } break ; } } if (self->numFeatures >= max_num_features) { break; } } /* next octave */ if (extrema) { vl_free(extrema) ; extrema = 0 ; } } /* Laplacian scale selection for certain methods */ switch (self->method) { case VL_COVDET_METHOD_HARRIS_LAPLACE : case VL_COVDET_METHOD_HESSIAN_LAPLACE : vl_covdet_extract_laplacian_scales (self) ; break ; default: break ; } if (self->nonExtremaSuppression) { vl_index i, j ; double tol = self->nonExtremaSuppression ; self->numNonExtremaSuppressed = 0 ; for (i = 0 ; i < (signed)self->numFeatures ; ++i) { double x = self->features[i].frame.x ; double y = self->features[i].frame.y ; double sigma = self->features[i].frame.a11 ; double score = self->features[i].peakScore ; if (score == 0) continue ; for (j = 0 ; j < (signed)self->numFeatures ; ++j) { double dx_ = self->features[j].frame.x - x ; double dy_ = self->features[j].frame.y - y ; double sigma_ = self->features[j].frame.a11 ; double score_ = self->features[j].peakScore ; if (score_ == 0) continue ; if (sigma < (1+tol) * sigma_ && sigma_ < (1+tol) * sigma && vl_abs_d(dx_) < tol * sigma && vl_abs_d(dy_) < tol * sigma && vl_abs_d(score) > vl_abs_d(score_)) { self->features[j].peakScore = 0 ; self->numNonExtremaSuppressed ++ ; } } } j = 0 ; for (i = 0 ; i < (signed)self->numFeatures ; ++i) { VlCovDetFeature feature = self->features[i] ; if (self->features[i].peakScore != 0) { self->features[j++] = feature ; } } self->numFeatures = j ; } if (levelxx) vl_free(levelxx) ; if (levelyy) vl_free(levelyy) ; if (levelxy) vl_free(levelxy) ; } /* ---------------------------------------------------------------- */ /* Extract patches */ /* ---------------------------------------------------------------- */ /** @internal ** @brief Helper for extracting patches ** @param self object. ** @param[out] sigma1 actual patch smoothing along the first axis. ** @param[out] sigma2 actual patch smoothing along the second axis. ** @param patch buffer. ** @param resolution patch resolution. ** @param extent patch extent. ** @param sigma desired smoothing in the patch frame. ** @param A_ linear transfomration from patch to image. ** @param T_ translation from patch to image. ** @param d1 first singular value @a A. ** @param d2 second singular value of @a A. **/ vl_bool vl_covdet_extract_patch_helper (VlCovDet * self, double * sigma1, double * sigma2, float * patch, vl_size resolution, double extent, double sigma, double A_ [4], double T_ [2], double d1, double d2) { vl_index o, s ; double factor ; double sigma_ ; float const * level ; vl_size width, height ; double step ; double A [4] = {A_[0], A_[1], A_[2], A_[3]} ; double T [2] = {T_[0], T_[1]} ; VlScaleSpaceGeometry geom = vl_scalespace_get_geometry(self->gss) ; VlScaleSpaceOctaveGeometry oct ; /* Starting from a pre-smoothed image at scale sigma_ because of the mapping A the resulting smoothing in the warped patch is S, where sigma_^2 I = A S A', S = sigma_^2 inv(A) inv(A)' = sigma_^2 V D^-2 V', A = U D V'. Thus we rotate A by V to obtain an axis-aligned smoothing: A = U*D, S = sigma_^2 D^-2. Then we search the scale-space for the best sigma_ such that the target smoothing is approximated from below: max sigma_(o,s) : simga_(o,s) factor <= sigma, factor = max{abs(D11), abs(D22)}. */ /* Determine the best level (o,s) such that sigma_(o,s) factor <= sigma. This can be obtained by scanning octaves from smallest to largest and stopping when no level in the octave satisfies the relation. Given the range of octave availables, do the best you can. */ factor = 1.0 / VL_MIN(d1, d2) ; for (o = geom.firstOctave + 1 ; o <= geom.lastOctave ; ++o) { s = vl_floor_d(vl_log2_d(sigma / (factor * geom.baseScale)) - o) ; s = VL_MAX(s, geom.octaveFirstSubdivision) ; s = VL_MIN(s, geom.octaveLastSubdivision) ; sigma_ = geom.baseScale * pow(2.0, o + (double)s / geom.octaveResolution) ; /*VL_PRINTF(".. %d D=%g %g; sigma_=%g factor*sigma_=%g\n", o, d1, d2, sigma_, factor* sigma_) ;*/ if (factor * sigma_ > sigma) { o -- ; break ; } } o = VL_MIN(o, geom.lastOctave) ; s = vl_floor_d(vl_log2_d(sigma / (factor * geom.baseScale)) - o) ; s = VL_MAX(s, geom.octaveFirstSubdivision) ; s = VL_MIN(s, geom.octaveLastSubdivision) ; sigma_ = geom.baseScale * pow(2.0, o + (double)s / geom.octaveResolution) ; if (sigma1) *sigma1 = sigma_ / d1 ; if (sigma2) *sigma2 = sigma_ / d2 ; /*VL_PRINTF("%d %d %g %g %g %g\n", o, s, factor, sigma_, factor * sigma_, sigma) ;*/ /* Now the scale space level to be used for this warping has been determined. If the patch is partially or completely out of the image boundary, create a padded copy of the required region first. */ level = vl_scalespace_get_level(self->gss, o, s) ; oct = vl_scalespace_get_octave_geometry(self->gss, o) ; width = oct.width ; height = oct.height ; step = oct.step ; A[0] /= step ; A[1] /= step ; A[2] /= step ; A[3] /= step ; T[0] /= step ; T[1] /= step ; { /* Warp the patch domain [x0hat,y0hat,x1hat,y1hat] to the image domain/ Obtain a box [x0,y0,x1,y1] enclosing that wrapped box, and then an integer vertexes version [x0i, y0i, x1i, y1i], making room for one pixel at the boundary to simplify bilinear interpolation later on. */ vl_index x0i, y0i, x1i, y1i ; double x0 = +VL_INFINITY_D ; double x1 = -VL_INFINITY_D ; double y0 = +VL_INFINITY_D ; double y1 = -VL_INFINITY_D ; double boxx [4] = {extent, extent, -extent, -extent} ; double boxy [4] = {-extent, extent, extent, -extent} ; int i ; for (i = 0 ; i < 4 ; ++i) { double x = A[0] * boxx[i] + A[2] * boxy[i] + T[0] ; double y = A[1] * boxx[i] + A[3] * boxy[i] + T[1] ; x0 = VL_MIN(x0, x) ; x1 = VL_MAX(x1, x) ; y0 = VL_MIN(y0, y) ; y1 = VL_MAX(y1, y) ; } /* Leave one pixel border for bilinear interpolation. */ x0i = floor(x0) - 1 ; y0i = floor(y0) - 1 ; x1i = ceil(x1) + 1 ; y1i = ceil(y1) + 1 ; /* If the box [x0i,y0i,x1i,y1i] is not fully contained in the image domain, then create a copy of this region by padding the image. The image is extended by continuity. */ if (x0i < 0 || x1i > (signed)width-1 || y0i < 0 || y1i > (signed)height-1) { vl_index xi, yi ; /* compute the amount of l,r,t,b padding needed to complete the patch */ vl_index padx0 = VL_MAX(0, - x0i) ; vl_index pady0 = VL_MAX(0, - y0i) ; vl_index padx1 = VL_MAX(0, x1i - ((signed)width - 1)) ; vl_index pady1 = VL_MAX(0, y1i - ((signed)height - 1)) ; /* make enough room for the patch */ vl_index patchWidth = x1i - x0i + 1 ; vl_index patchHeight = y1i - y0i + 1 ; vl_size patchBufferSize = patchWidth * patchHeight * sizeof(float) ; if (patchBufferSize > self->patchBufferSize) { int err = _vl_resize_buffer((void**)&self->patch, &self->patchBufferSize, patchBufferSize) ; if (err) return vl_set_last_error(VL_ERR_ALLOC, NULL) ; } if (pady0 < patchHeight - pady1) { /* start by filling the central horizontal band */ for (yi = y0i + pady0 ; yi < y0i + patchHeight - pady1 ; ++ yi) { float *dst = self->patch + (yi - y0i) * patchWidth ; float const *src = level + yi * width + VL_MIN(VL_MAX(0, x0i),(signed)width-1) ; for (xi = x0i ; xi < x0i + padx0 ; ++xi) *dst++ = *src ; for ( ; xi < x0i + patchWidth - padx1 - 2 ; ++xi) *dst++ = *src++ ; for ( ; xi < x0i + patchWidth ; ++xi) *dst++ = *src ; } /* now extend the central band up and down */ for (yi = 0 ; yi < pady0 ; ++yi) { memcpy(self->patch + yi * patchWidth, self->patch + pady0 * patchWidth, patchWidth * sizeof(float)) ; } for (yi = patchHeight - pady1 ; yi < patchHeight ; ++yi) { memcpy(self->patch + yi * patchWidth, self->patch + (patchHeight - pady1 - 1) * patchWidth, patchWidth * sizeof(float)) ; } } else { /* should be handled better! */ memset(self->patch, 0, self->patchBufferSize) ; } #if 0 { char name [200] ; snprintf(name, 200, "/tmp/%20.0f-ext.pgm", 1e10*vl_get_cpu_time()) ; vl_pgm_write_f(name, patch, patchWidth, patchWidth) ; } #endif level = self->patch ; width = patchWidth ; height = patchHeight ; T[0] -= x0i ; T[1] -= y0i ; } } /* Resample by using bilinear interpolation. */ { float * pt = patch ; double yhat = -extent ; vl_index xxi ; vl_index yyi ; double stephat = extent / resolution ; for (yyi = 0 ; yyi < 2 * (signed)resolution + 1 ; ++yyi) { double xhat = -extent ; double rx = A[2] * yhat + T[0] ; double ry = A[3] * yhat + T[1] ; for (xxi = 0 ; xxi < 2 * (signed)resolution + 1 ; ++xxi) { double x = A[0] * xhat + rx ; double y = A[1] * xhat + ry ; vl_index xi = vl_floor_d(x) ; vl_index yi = vl_floor_d(y) ; double i00 = level[yi * width + xi] ; double i10 = level[yi * width + xi + 1] ; double i01 = level[(yi + 1) * width + xi] ; double i11 = level[(yi + 1) * width + xi + 1] ; double wx = x - xi ; double wy = y - yi ; assert(xi >= 0 && xi <= (signed)width - 1) ; assert(yi >= 0 && yi <= (signed)height - 1) ; *pt++ = (1.0 - wy) * ((1.0 - wx) * i00 + wx * i10) + wy * ((1.0 - wx) * i01 + wx * i11) ; xhat += stephat ; } yhat += stephat ; } } #if 0 { char name [200] ; snprintf(name, 200, "/tmp/%20.0f.pgm", 1e10*vl_get_cpu_time()) ; vl_pgm_write_f(name, patch, 2*resolution+1, 2*resolution+1) ; } #endif return VL_ERR_OK ; } /** @brief Helper for extracting patches ** @param self object. ** @param patch buffer. ** @param resolution patch resolution. ** @param extent patch extent. ** @param sigma desired smoothing in the patch frame. ** @param frame feature frame. ** ** The function considers a patch of extent [-extent,extent] ** on each side, with a side counting 2*resolution+1 pixels. ** In attempts to extract from the scale space a patch ** based on the affine warping specified by @a frame in such a way ** that the resulting smoothing of the image is @a sigma (in the ** patch frame). ** ** The transformation is specified by the matrices @c A and @c T ** embedded in the feature @a frame. Note that this transformation maps ** pixels from the patch frame to the image frame. **/ vl_bool vl_covdet_extract_patch_for_frame (VlCovDet * self, float * patch, vl_size resolution, double extent, double sigma, VlFrameOrientedEllipse frame) { double A[2*2] = {frame.a11, frame.a21, frame.a12, frame.a22} ; double T[2] = {frame.x, frame.y} ; double D[4], U[4], V[4] ; vl_svd2(D, U, V, A) ; return vl_covdet_extract_patch_helper (self, NULL, NULL, patch, resolution, extent, sigma, A, T, D[0], D[3]) ; } /* ---------------------------------------------------------------- */ /* Affine shape */ /* ---------------------------------------------------------------- */ /** @brief Extract the affine shape for a feature frame ** @param self object. ** @param adapted the shape-adapted frame. ** @param frame the input frame. ** @return ::VL_ERR_OK if affine adaptation is successful. ** ** This function may fail if adaptation is unsuccessful or if ** memory is insufficient. **/ int vl_covdet_extract_affine_shape_for_frame (VlCovDet * self, VlFrameOrientedEllipse * adapted, VlFrameOrientedEllipse frame) { vl_index iter = 0 ; double A [2*2] = {frame.a11, frame.a21, frame.a12, frame.a22} ; double T [2] = {frame.x, frame.y} ; double U [2*2] ; double V [2*2] ; double D [2*2] ; double M [2*2] ; double P [2*2] ; double P_ [2*2] ; double Q [2*2] ; double sigma1, sigma2 ; double sigmaD = VL_COVDET_AA_RELATIVE_DERIVATIVE_SIGMA ; double factor ; double anisotropy ; double referenceScale ; vl_size const resolution = VL_COVDET_AA_PATCH_RESOLUTION ; vl_size const side = 2*VL_COVDET_AA_PATCH_RESOLUTION + 1 ; double const extent = VL_COVDET_AA_PATCH_EXTENT ; *adapted = frame ; while (1) { double lxx = 0, lxy = 0, lyy = 0 ; vl_index k ; int err ; /* A = U D V' */ vl_svd2(D, U, V, A) ; anisotropy = VL_MAX(D[0]/D[3], D[3]/D[0]) ; /* VL_PRINTF("anisot: %g\n", anisotropy); */ if (anisotropy > VL_COVDET_AA_MAX_ANISOTROPY) { /* diverged, give up with current solution */ break ; } /* make sure that the smallest singluar value stays fixed after the first iteration */ if (iter == 0) { referenceScale = VL_MIN(D[0], D[3]) ; factor = 1.0 ; } else { factor = referenceScale / VL_MIN(D[0],D[3]) ; } D[0] *= factor ; D[3] *= factor ; A[0] = U[0] * D[0] ; A[1] = U[1] * D[0] ; A[2] = U[2] * D[3] ; A[3] = U[3] * D[3] ; adapted->a11 = A[0] ; adapted->a21 = A[1] ; adapted->a12 = A[2] ; adapted->a22 = A[3] ; if (++iter >= VL_COVDET_AA_MAX_NUM_ITERATIONS) break ; err = vl_covdet_extract_patch_helper(self, &sigma1, &sigma2, self->aaPatch, resolution, extent, sigmaD, A, T, D[0], D[3]) ; if (err) return err ; if (self->aaAccurateSmoothing ) { double deltaSigma1 = sqrt(VL_MAX(sigmaD*sigmaD - sigma1*sigma1,0)) ; double deltaSigma2 = sqrt(VL_MAX(sigmaD*sigmaD - sigma2*sigma2,0)) ; double stephat = extent / resolution ; vl_imsmooth_f(self->aaPatch, side, self->aaPatch, side, side, side, deltaSigma1 / stephat, deltaSigma2 / stephat) ; } /* compute second moment matrix */ vl_imgradient_f (self->aaPatchX, self->aaPatchY, 1, side, self->aaPatch, side, side, side) ; for (k = 0 ; k < (signed)(side*side) ; ++k) { double lx = self->aaPatchX[k] ; double ly = self->aaPatchY[k] ; lxx += lx * lx * self->aaMask[k] ; lyy += ly * ly * self->aaMask[k] ; lxy += lx * ly * self->aaMask[k] ; } M[0] = lxx ; M[1] = lxy ; M[2] = lxy ; M[3] = lyy ; if (lxx == 0 || lyy == 0) { *adapted = frame ; break ; } /* decompose M = P * Q * P' */ vl_svd2 (Q, P, P_, M) ; /* Setting A <- A * dA results in M to change approximatively as M --> dA' M dA = dA' P Q P dA To make this proportional to the identity, we set dA ~= P Q^1/2 we also make it so the smallest singular value of A is unchanged. */ if (Q[3]/Q[0] < VL_COVDET_AA_CONVERGENCE_THRESHOLD && Q[0]/Q[3] < VL_COVDET_AA_CONVERGENCE_THRESHOLD) { break ; } { double Ap [4] ; double q0 = sqrt(Q[0]) ; double q1 = sqrt(Q[3]) ; Ap[0] = (A[0] * P[0] + A[2] * P[1]) / q0 ; Ap[1] = (A[1] * P[0] + A[3] * P[1]) / q0 ; Ap[2] = (A[0] * P[2] + A[2] * P[3]) / q1 ; Ap[3] = (A[1] * P[2] + A[3] * P[3]) / q1 ; memcpy(A,Ap,4*sizeof(double)) ; } } /* next iteration */ /* Make upright. Shape adaptation does not estimate rotation. This is fixed by default so that a selected axis is not rotated at all (usually this is the vertical axis for upright features). To do so, the frame is rotated as follows. */ { double A [2*2] = {adapted->a11, adapted->a21, adapted->a12, adapted->a22} ; double ref [2] ; double ref_ [2] ; double angle ; double angle_ ; double dangle ; double r1, r2 ; if (self->transposed) { /* up is the x axis */ ref[0] = 1 ; ref[1] = 0 ; } else { /* up is the y axis */ ref[0] = 0 ; ref[1] = 1 ; } vl_solve_linear_system_2 (ref_, A, ref) ; angle = atan2(ref[1], ref[0]) ; angle_ = atan2(ref_[1], ref_[0]) ; dangle = angle_ - angle ; r1 = cos(dangle) ; r2 = sin(dangle) ; adapted->a11 = + A[0] * r1 + A[2] * r2 ; adapted->a21 = + A[1] * r1 + A[3] * r2 ; adapted->a12 = - A[0] * r2 + A[2] * r1 ; adapted->a22 = - A[1] * r2 + A[3] * r1 ; } return VL_ERR_OK ; } /** @brief Extract the affine shape for the stored features ** @param self object. ** ** This function may discard features for which no affine ** shape can reliably be detected. **/ void vl_covdet_extract_affine_shape (VlCovDet * self) { vl_index i, j = 0 ; vl_size numFeatures = vl_covdet_get_num_features(self) ; VlCovDetFeature * feature = vl_covdet_get_features(self); for (i = 0 ; i < (signed)numFeatures ; ++i) { int status ; VlFrameOrientedEllipse adapted ; status = vl_covdet_extract_affine_shape_for_frame(self, &adapted, feature[i].frame) ; if (status == VL_ERR_OK) { feature[j] = feature[i] ; feature[j].frame = adapted ; ++ j ; } } self->numFeatures = j ; } /* ---------------------------------------------------------------- */ /* Orientation */ /* ---------------------------------------------------------------- */ static int _vl_covdet_compare_orientations_descending (void const * a_, void const * b_) { VlCovDetFeatureOrientation const * a = a_ ; VlCovDetFeatureOrientation const * b = b_ ; if (a->score > b->score) return -1 ; if (a->score < b->score) return +1 ; return 0 ; } /** @brief Extract the orientation(s) for a feature ** @param self object. ** @param numOrientations the number of detected orientations. ** @param frame pose of the feature. ** @return an array of detected orientations with their scores. ** ** The returned array is a matrix of size @f$ 2 \times n @f$ ** where n is the number of detected orientations. ** ** The function returns @c NULL if memory is insufficient. **/ VlCovDetFeatureOrientation * vl_covdet_extract_orientations_for_frame (VlCovDet * self, vl_size * numOrientations, VlFrameOrientedEllipse frame) { int err ; vl_index k, i ; vl_index iter ; double extent = VL_COVDET_AA_PATCH_EXTENT ; vl_size resolution = VL_COVDET_AA_PATCH_RESOLUTION ; vl_size side = 2 * resolution + 1 ; vl_size const numBins = VL_COVDET_OR_NUM_ORIENTATION_HISTOGAM_BINS ; double hist [VL_COVDET_OR_NUM_ORIENTATION_HISTOGAM_BINS] ; double const binExtent = 2 * VL_PI / VL_COVDET_OR_NUM_ORIENTATION_HISTOGAM_BINS ; double const peakRelativeSize = VL_COVDET_OR_ADDITIONAL_PEAKS_RELATIVE_SIZE ; double maxPeakValue ; double A [2*2] = {frame.a11, frame.a21, frame.a12, frame.a22} ; double T [2] = {frame.x, frame.y} ; double U [2*2] ; double V [2*2] ; double D [2*2] ; double sigma1, sigma2 ; double sigmaD = 1.0 ; double theta0 ; assert(self); assert(numOrientations) ; /* The goal is to estimate a rotation R(theta) such that the patch given by the transformation A R(theta) has the strongest average gradient pointing right (or down for transposed conventions). To compensate for tha anisotropic smoothing due to warping, A is decomposed as A = U D V' and the patch is warped by U D only, meaning that the matrix R_(theta) will be estimated instead, where: A R(theta) = U D V' R(theta) = U D R_(theta) such that R(theta) = V R(theta). That is an extra rotation of theta0 = atan2(V(2,1),V(1,1)). */ /* axis aligned anisotropic smoothing for easier compensation */ vl_svd2(D, U, V, A) ; A[0] = U[0] * D[0] ; A[1] = U[1] * D[0] ; A[2] = U[2] * D[3] ; A[3] = U[3] * D[3] ; theta0 = atan2(V[1],V[0]) ; err = vl_covdet_extract_patch_helper(self, &sigma1, &sigma2, self->aaPatch, resolution, extent, sigmaD, A, T, D[0], D[3]) ; if (err) { *numOrientations = 0 ; return NULL ; } if (1) { double deltaSigma1 = sqrt(VL_MAX(sigmaD*sigmaD - sigma1*sigma1,0)) ; double deltaSigma2 = sqrt(VL_MAX(sigmaD*sigmaD - sigma2*sigma2,0)) ; double stephat = extent / resolution ; vl_imsmooth_f(self->aaPatch, side, self->aaPatch, side, side, side, deltaSigma1 / stephat, deltaSigma2 / stephat) ; } /* histogram of oriented gradients */ vl_imgradient_polar_f (self->aaPatchX, self->aaPatchY, 1, side, self->aaPatch, side, side, side) ; memset (hist, 0, sizeof(double) * numBins) ; for (k = 0 ; k < (signed)(side*side) ; ++k) { double modulus = self->aaPatchX[k] ; double angle = self->aaPatchY[k] ; double weight = self->aaMask[k] ; double x = angle / binExtent ; vl_index bin = vl_floor_d(x) ; double w2 = x - bin ; double w1 = 1.0 - w2 ; hist[(bin + numBins) % numBins] += w1 * (modulus * weight) ; hist[(bin + numBins + 1) % numBins] += w2 * (modulus * weight) ; } /* smooth histogram */ for (iter = 0; iter < 6; iter ++) { double prev = hist [numBins - 1] ; double first = hist [0] ; vl_index i ; for (i = 0; i < (signed)numBins - 1; ++i) { double curr = (prev + hist[i] + hist[(i + 1) % numBins]) / 3.0 ; prev = hist[i] ; hist[i] = curr ; } hist[i] = (prev + hist[i] + first) / 3.0 ; } /* find the histogram maximum */ maxPeakValue = 0 ; for (i = 0 ; i < (signed)numBins ; ++i) { maxPeakValue = VL_MAX (maxPeakValue, hist[i]) ; } /* find peaks within 80% from max */ *numOrientations = 0 ; for(i = 0 ; i < (signed)numBins ; ++i) { double h0 = hist [i] ; double hm = hist [(i - 1 + numBins) % numBins] ; double hp = hist [(i + 1 + numBins) % numBins] ; /* is this a peak? */ if (h0 > peakRelativeSize * maxPeakValue && h0 > hm && h0 > hp) { /* quadratic interpolation */ double di = - 0.5 * (hp - hm) / (hp + hm - 2 * h0) ; double th = binExtent * (i + di) + theta0 ; if (self->transposed) { /* the axis to the right is y, measure orientations from this */ th = th - VL_PI/2 ; } self->orientations[*numOrientations].angle = th ; self->orientations[*numOrientations].score = h0 ; *numOrientations += 1 ; //VL_PRINTF("%d %g\n", *numOrientations, th) ; if (*numOrientations >= VL_COVDET_MAX_NUM_ORIENTATIONS) break ; } } /* sort the orientations by decreasing scores */ qsort(self->orientations, *numOrientations, sizeof(VlCovDetFeatureOrientation), _vl_covdet_compare_orientations_descending) ; return self->orientations ; } /** @brief Extract the orientation(s) for the stored features. ** @param self object. ** ** Note that, since more than one orientation can be detected ** for each feature, this function may create copies of them, ** one for each orientation. **/ void vl_covdet_extract_orientations (VlCovDet * self) { vl_index i, j ; vl_size numFeatures = vl_covdet_get_num_features(self) ; for (i = 0 ; i < (signed)numFeatures ; ++i) { vl_size numOrientations ; VlCovDetFeature feature = self->features[i] ; VlCovDetFeatureOrientation* orientations = vl_covdet_extract_orientations_for_frame(self, &numOrientations, feature.frame) ; for (j = 0 ; j < (signed)numOrientations ; ++j) { double A [2*2] = { feature.frame.a11, feature.frame.a21, feature.frame.a12, feature.frame.a22} ; double r1 = cos(orientations[j].angle) ; double r2 = sin(orientations[j].angle) ; VlCovDetFeature * oriented ; if (j == 0) { oriented = & self->features[i] ; } else { vl_covdet_append_feature(self, &feature) ; oriented = & self->features[self->numFeatures -1] ; } oriented->orientationScore = orientations[j].score ; oriented->frame.a11 = + A[0] * r1 + A[2] * r2 ; oriented->frame.a21 = + A[1] * r1 + A[3] * r2 ; oriented->frame.a12 = - A[0] * r2 + A[2] * r1 ; oriented->frame.a22 = - A[1] * r2 + A[3] * r1 ; } } } /* ---------------------------------------------------------------- */ /* Laplacian scales */ /* ---------------------------------------------------------------- */ /** @brief Extract the Laplacian scale(s) for a feature frame. ** @param self object. ** @param numScales the number of detected scales. ** @param frame pose of the feature. ** @return an array of detected scales. ** ** The function returns @c NULL if memory is insufficient. **/ VlCovDetFeatureLaplacianScale * vl_covdet_extract_laplacian_scales_for_frame (VlCovDet * self, vl_size * numScales, VlFrameOrientedEllipse frame) { /* We try to explore one octave, with the nominal detection scale 1.0 (in the patch reference frame) in the middle. Thus the goal is to sample the response of the tr-Laplacian operator at logarithmically spaced scales in 1/sqrt(2), sqrt(2). To this end, the patch is warped with a smoothing of at most sigmaImage = 1 / sqrt(2) (beginning of the scale), sampled at roughly twice the Nyquist frequency (so step = 1 / (2*sqrt(2))). This maes it possible to approximate the Laplacian operator at that scale by simple finite differences. */ int err ; double const sigmaImage = 1.0 / sqrt(2.0) ; double const step = 0.5 * sigmaImage ; double actualSigmaImage ; vl_size const resolution = VL_COVDET_LAP_PATCH_RESOLUTION ; vl_size const num = 2 * resolution + 1 ; double extent = step * resolution ; double scores [VL_COVDET_LAP_NUM_LEVELS] ; double factor = 1.0 ; float const * pt ; vl_index k ; double A[2*2] = {frame.a11, frame.a21, frame.a12, frame.a22} ; double T[2] = {frame.x, frame.y} ; double D[4], U[4], V[4] ; double sigma1, sigma2 ; assert(self) ; assert(numScales) ; *numScales = 0 ; vl_svd2(D, U, V, A) ; err = vl_covdet_extract_patch_helper (self, &sigma1, &sigma2, self->lapPatch, resolution, extent, sigmaImage, A, T, D[0], D[3]) ; if (err) return NULL ; /* the actual smoothing after warping is never the target one */ if (sigma1 == sigma2) { actualSigmaImage = sigma1 ; } else { /* here we could compensate */ actualSigmaImage = sqrt(sigma1*sigma2) ; } /* now multiply by the bank of Laplacians */ pt = self->laplacians ; for (k = 0 ; k < VL_COVDET_LAP_NUM_LEVELS ; ++k) { vl_index q ; double score = 0 ; double sigmaLap = pow(2.0, -0.5 + (double)k / (VL_COVDET_LAP_NUM_LEVELS - 1)) ; /* note that the sqrt argument cannot be negative since by construction sigmaLap >= sigmaImage */ sigmaLap = sqrt(sigmaLap*sigmaLap - sigmaImage*sigmaImage + actualSigmaImage*actualSigmaImage) ; for (q = 0 ; q < (signed)(num * num) ; ++q) { score += (*pt++) * self->lapPatch[q] ; } scores[k] = score * sigmaLap * sigmaLap ; } /* find and interpolate maxima */ for (k = 1 ; k < VL_COVDET_LAP_NUM_LEVELS - 1 ; ++k) { double a = scores[k-1] ; double b = scores[k] ; double c = scores[k+1] ; double t = self->lapPeakThreshold ; if ((((b > a) && (b > c)) || ((b < a) && (b < c))) && (vl_abs_d(b) >= t)) { double dk = - 0.5 * (c - a) / (c + a - 2 * b) ; double s = k + dk ; double sigmaLap = pow(2.0, -0.5 + s / (VL_COVDET_LAP_NUM_LEVELS - 1)) ; double scale ; sigmaLap = sqrt(sigmaLap*sigmaLap - sigmaImage*sigmaImage + actualSigmaImage*actualSigmaImage) ; scale = sigmaLap / 1.0 ; /* VL_PRINTF("** k:%d, s:%f, sigmaLapFilter:%f, sigmaLap%f, scale:%f (%f %f %f)\n", k,s,sigmaLapFilter,sigmaLap,scale,a,b,c) ; */ if (*numScales < VL_COVDET_MAX_NUM_LAPLACIAN_SCALES) { self->scales[*numScales].scale = scale * factor ; self->scales[*numScales].score = b + 0.5 * (c - a) * dk ; *numScales += 1 ; } } } return self->scales ; } /** @brief Extract the Laplacian scales for the stored features ** @param self object. ** ** Note that, since more than one orientation can be detected ** for each feature, this function may create copies of them, ** one for each orientation. **/ void vl_covdet_extract_laplacian_scales (VlCovDet * self) { vl_index i, j ; vl_bool dropFeaturesWithoutScale = VL_TRUE ; vl_size numFeatures = vl_covdet_get_num_features(self) ; memset(self->numFeaturesWithNumScales, 0, sizeof(self->numFeaturesWithNumScales)) ; for (i = 0 ; i < (signed)numFeatures ; ++i) { vl_size numScales ; VlCovDetFeature feature = self->features[i] ; VlCovDetFeatureLaplacianScale const * scales = vl_covdet_extract_laplacian_scales_for_frame(self, &numScales, feature.frame) ; self->numFeaturesWithNumScales[numScales] ++ ; if (numScales == 0 && dropFeaturesWithoutScale) { self->features[i].peakScore = 0 ; } for (j = 0 ; j < (signed)numScales ; ++j) { VlCovDetFeature * scaled ; if (j == 0) { scaled = & self->features[i] ; } else { vl_covdet_append_feature(self, &feature) ; scaled = & self->features[self->numFeatures -1] ; } scaled->laplacianScaleScore = scales[j].score ; scaled->frame.a11 *= scales[j].scale ; scaled->frame.a21 *= scales[j].scale ; scaled->frame.a12 *= scales[j].scale ; scaled->frame.a22 *= scales[j].scale ; } } if (dropFeaturesWithoutScale) { j = 0 ; for (i = 0 ; i < (signed)self->numFeatures ; ++i) { VlCovDetFeature feature = self->features[i] ; if (feature.peakScore) { self->features[j++] = feature ; } } self->numFeatures = j ; } } /* ---------------------------------------------------------------- */ /* Checking that features are inside an image */ /* ---------------------------------------------------------------- */ vl_bool _vl_covdet_check_frame_inside (VlCovDet * self, VlFrameOrientedEllipse frame, double margin) { double extent = margin ; double A [2*2] = {frame.a11, frame.a21, frame.a12, frame.a22} ; double T[2] = {frame.x, frame.y} ; double x0 = +VL_INFINITY_D ; double x1 = -VL_INFINITY_D ; double y0 = +VL_INFINITY_D ; double y1 = -VL_INFINITY_D ; double boxx [4] = {extent, extent, -extent, -extent} ; double boxy [4] = {-extent, extent, extent, -extent} ; VlScaleSpaceGeometry geom = vl_scalespace_get_geometry(self->gss) ; int i ; for (i = 0 ; i < 4 ; ++i) { double x = A[0] * boxx[i] + A[2] * boxy[i] + T[0] ; double y = A[1] * boxx[i] + A[3] * boxy[i] + T[1] ; x0 = VL_MIN(x0, x) ; x1 = VL_MAX(x1, x) ; y0 = VL_MIN(y0, y) ; y1 = VL_MAX(y1, y) ; } return 0 <= x0 && x1 <= geom.width-1 && 0 <= y0 && y1 <= geom.height-1 ; } /** @brief Drop features (partially) outside the image ** @param self object. ** @param margin geometric marging. ** ** The feature extent is defined by @c maring. A bounding box ** in the normalised feature frame containin a circle of radius ** @a maring is created and mapped to the image by ** the feature frame transformation. Then the feature ** is dropped if the bounding box is not contained in the image. ** ** For example, setting @c margin to zero drops a feature only ** if its center is not contained. ** ** Typically a valua of @c margin equal to 1 or 2 is used. **/ void vl_covdet_drop_features_outside (VlCovDet * self, double margin) { vl_index i, j = 0 ; vl_size numFeatures = vl_covdet_get_num_features(self) ; for (i = 0 ; i < (signed)numFeatures ; ++i) { vl_bool inside = _vl_covdet_check_frame_inside (self, self->features[i].frame, margin) ; if (inside) { self->features[j] = self->features[i] ; ++j ; } } self->numFeatures = j ; } /* ---------------------------------------------------------------- */ /* Setters and getters */ /* ---------------------------------------------------------------- */ /* ---------------------------------------------------------------- */ /** @brief Get wether images are passed in transposed ** @param self object. ** @return whether images are transposed. **/ vl_bool vl_covdet_get_transposed (VlCovDet const * self) { return self->transposed ; } /** @brief Set the index of the first octave ** @param self object. ** @param t whether images are transposed. **/ void vl_covdet_set_transposed (VlCovDet * self, vl_bool t) { self->transposed = t ; } /* ---------------------------------------------------------------- */ /** @brief Get the edge threshold ** @param self object. ** @return the edge threshold. **/ double vl_covdet_get_edge_threshold (VlCovDet const * self) { return self->edgeThreshold ; } /** @brief Set the edge threshold ** @param self object. ** @param edgeThreshold the edge threshold. ** ** The edge threshold must be non-negative. **/ void vl_covdet_set_edge_threshold (VlCovDet * self, double edgeThreshold) { assert(edgeThreshold >= 0) ; self->edgeThreshold = edgeThreshold ; } /* ---------------------------------------------------------------- */ /** @brief Get the peak threshold ** @param self object. ** @return the peak threshold. **/ double vl_covdet_get_peak_threshold (VlCovDet const * self) { return self->peakThreshold ; } /** @brief Set the peak threshold ** @param self object. ** @param peakThreshold the peak threshold. ** ** The peak threshold must be non-negative. **/ void vl_covdet_set_peak_threshold (VlCovDet * self, double peakThreshold) { assert(peakThreshold >= 0) ; self->peakThreshold = peakThreshold ; } /* ---------------------------------------------------------------- */ /** @brief Get the Laplacian peak threshold ** @param self object. ** @return the Laplacian peak threshold. ** ** This parameter affects only the detecors using the Laplacian ** scale selectino method such as Harris-Laplace. **/ double vl_covdet_get_laplacian_peak_threshold (VlCovDet const * self) { return self->lapPeakThreshold ; } /** @brief Set the Laplacian peak threshold ** @param self object. ** @param peakThreshold the Laplacian peak threshold. ** ** The peak threshold must be non-negative. **/ void vl_covdet_set_laplacian_peak_threshold (VlCovDet * self, double peakThreshold) { assert(peakThreshold >= 0) ; self->lapPeakThreshold = peakThreshold ; } /* ---------------------------------------------------------------- */ /** @brief Get the index of the first octave ** @param self object. ** @return index of the first octave. **/ vl_index vl_covdet_get_first_octave (VlCovDet const * self) { return self->firstOctave ; } /** @brief Set the index of the first octave ** @param self object. ** @param o index of the first octave. ** ** Calling this function resets the detector. **/ void vl_covdet_set_first_octave (VlCovDet * self, vl_index o) { self->firstOctave = o ; vl_covdet_reset(self) ; } /* ---------------------------------------------------------------- */ /** @brief Get the octave resolution. ** @param self object. ** @return octave resolution. **/ vl_size vl_covdet_get_octave_resolution (VlCovDet const * self) { return self->octaveResolution ; } /** @brief Set the octave resolutuon. ** @param self object. ** @param r octave resoltuion. ** ** Calling this function resets the detector. **/ void vl_covdet_set_octave_resolution (VlCovDet * self, vl_size r) { self->octaveResolution = r ; vl_covdet_reset(self) ; } /* ---------------------------------------------------------------- */ /** @brief Get whether affine adaptation uses accurate smoothing. ** @param self object. ** @return @c true if accurate smoothing is used. **/ vl_bool vl_covdet_get_aa_accurate_smoothing (VlCovDet const * self) { return self->aaAccurateSmoothing ; } /** @brief Set whether affine adaptation uses accurate smoothing. ** @param self object. ** @param x whether accurate smoothing should be usd. **/ void vl_covdet_set_aa_accurate_smoothing (VlCovDet * self, vl_bool x) { self->aaAccurateSmoothing = x ; } /* ---------------------------------------------------------------- */ /** @brief Get the non-extrema suppression threshold ** @param self object. ** @return threshold. **/ double vl_covdet_get_non_extrema_suppression_threshold (VlCovDet const * self) { return self->nonExtremaSuppression ; } /** @brief Set the non-extrema suppression threshod ** @param self object. ** @param x threshold. **/ void vl_covdet_set_non_extrema_suppression_threshold (VlCovDet * self, double x) { self->nonExtremaSuppression = x ; } /** @brief Get the number of non-extrema suppressed ** @param self object. ** @return number. **/ vl_size vl_covdet_get_num_non_extrema_suppressed (VlCovDet const * self) { return self->numNonExtremaSuppressed ; } /* ---------------------------------------------------------------- */ /** @brief Get number of stored frames ** @return number of frames stored in the detector. **/ vl_size vl_covdet_get_num_features (VlCovDet const * self) { return self->numFeatures ; } /** @brief Get the stored frames ** @return frames stored in the detector. **/ VlCovDetFeature * vl_covdet_get_features (VlCovDet * self) { return self->features ; } /** @brief Get the Gaussian scale space ** @return Gaussian scale space. ** ** A Gaussian scale space exists only after calling ::vl_covdet_put_image. ** Otherwise the function returns @c NULL. **/ VlScaleSpace * vl_covdet_get_gss (VlCovDet const * self) { return self->gss ; } /** @brief Get the cornerness measure scale space ** @return cornerness measure scale space. ** ** A cornerness measure scale space exists only after calling ** ::vl_covdet_detect. Otherwise the function returns @c NULL. **/ VlScaleSpace * vl_covdet_get_css (VlCovDet const * self) { return self->css ; } /** @brief Get the number of features found with a certain number of scales ** @param self object. ** @param numScales length of the histogram (out). ** @return histogram. ** ** Calling this function makes sense only after running a detector ** that uses the Laplacian as a secondary measure for scale ** detection **/ vl_size const * vl_covdet_get_laplacian_scales_statistics (VlCovDet const * self, vl_size * numScales) { *numScales = VL_COVDET_MAX_NUM_LAPLACIAN_SCALES ; return self->numFeaturesWithNumScales ; } colmap-3.9.1/src/thirdparty/VLFeat/covdet.h000066400000000000000000000220221454702036400205410ustar00rootroot00000000000000/** @file covdet.h ** @brief Covariant feature detectors (@ref covdet) ** @author Karel Lenc ** @author Andrea Vedaldi ** @author Michal Perdoch **/ /* Copyright (C) 2013-14 Andrea Vedaldi. Copyright (C) 2012 Karel Lenc, Andrea Vedaldi and Michal Perdoch. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_COVDET_H #define VL_COVDET_H #include "generic.h" #include "stringop.h" #include "scalespace.h" #include /* ---------------------------------------------------------------- */ /* Feature Frames */ /* ---------------------------------------------------------------- */ /** @name Feature frames ** @{ */ /** @brief Types of feature frames */ typedef enum _VlFrameType { VL_FRAMETYPE_DISC = 1, /**< A disc. */ VL_FRAMETYPE_ORIENTED_DISC, /**< An oriented disc. */ VL_FRAMETYPE_ELLIPSE, /**< An ellipse. */ VL_FRAMETYPE_ORIENTED_ELLIPSE, /**< An oriented ellipse. */ VL_FRAMETYPE_NUM } VlFrameType ; /** @brief Names of the frame types */ VL_EXPORT const char* vlFrameNames [VL_FRAMETYPE_NUM] ; /** @brief Mapping between string values and VlFrameType values */ VL_EXPORT VlEnumerator vlFrameTypes [VL_FRAMETYPE_NUM] ; /** @brief Disc feature frame */ typedef struct _VlFrameDisc { float x ; /**< center x-coordinate */ float y ; /**< center y-coordinate */ float sigma ; /**< radius or scale */ } VlFrameDisc ; /** @brief Oriented disc feature frame ** An upright frame has @c angle equal to zero. **/ typedef struct _VlFrameOrientedDisc { float x ; /**< center x-coordinate */ float y ; /**< center y-coordinate */ float sigma ; /**< radius or scale */ float angle ; /**< rotation angle (rad) */ } VlFrameOrientedDisc ; /** @brief Ellipse feature frame */ typedef struct _VlFrameEllipse { float x ; /**< center x-coordinate */ float y ; /**< center y-coordinate */ float e11 ; /**< */ float e12 ; float e22 ; } VlFrameEllipse ; /** @brief Oriented ellipse feature frame ** The affine transformation transforms the ellipse shape into ** a circular region. */ typedef struct _VlFrameOrientedEllipse { float x ; /**< center x-coordinate */ float y ; /**< center y-coordinate */ float a11 ; /**< */ float a12 ; float a21 ; float a22 ; } VlFrameOrientedEllipse; /** @brief Get the size of a frame structure ** @param frameType identifier of the type of frame. ** @return size of the corresponding frame structure in bytes. **/ VL_INLINE vl_size vl_get_frame_size (VlFrameType frameType) { switch (frameType) { case VL_FRAMETYPE_DISC: return sizeof(VlFrameDisc); case VL_FRAMETYPE_ORIENTED_DISC: return sizeof(VlFrameOrientedDisc); case VL_FRAMETYPE_ELLIPSE: return sizeof(VlFrameEllipse); case VL_FRAMETYPE_ORIENTED_ELLIPSE: return sizeof(VlFrameOrientedEllipse); default: assert(0); break; } return 0; } /** @brief Get the size of a frame structure ** @param affineAdaptation whether the detector use affine adaptation. ** @param orientation whether the detector estimates the feature orientation. ** @return the type of extracted frame. ** ** Depedning on whether the detector estimate the affine shape ** and orientation of a feature, different frame types ** are extracted. */ VL_INLINE VlFrameType vl_get_frame_type (vl_bool affineAdaptation, vl_bool orientation) { if (affineAdaptation) { if (orientation) { return VL_FRAMETYPE_ORIENTED_ELLIPSE; } else { return VL_FRAMETYPE_ELLIPSE; } } else { if (orientation) { return VL_FRAMETYPE_ORIENTED_DISC; } else { return VL_FRAMETYPE_DISC; } } } /* ---------------------------------------------------------------- */ /* Covariant Feature Detector */ /* ---------------------------------------------------------------- */ /** @brief A detected feature shape and location */ typedef struct _VlCovDetFeature { VlFrameOrientedEllipse frame ; /**< feature frame. */ int o ; /**< Detected octave. */ int s ; /**< Octave subdivision. */ float peakScore ; /**< peak score. */ float edgeScore ; /**< edge score. */ float orientationScore ; /**< orientation score. */ float laplacianScaleScore ; /**< Laplacian scale score. */ } VlCovDetFeature ; /** @brief A detected feature orientation */ typedef struct _VlCovDetFeatureOrientation { double angle ; double score ; } VlCovDetFeatureOrientation ; /** @brief A detected feature Laplacian scale */ typedef struct _VlCovDetFeatureLaplacianScale { double scale ; double score ; } VlCovDetFeatureLaplacianScale ; /** @brief Covariant feature detection method */ typedef enum _VlCovDetMethod { VL_COVDET_METHOD_DOG = 1, VL_COVDET_METHOD_HESSIAN, VL_COVDET_METHOD_HESSIAN_LAPLACE, VL_COVDET_METHOD_HARRIS_LAPLACE, VL_COVDET_METHOD_MULTISCALE_HESSIAN, VL_COVDET_METHOD_MULTISCALE_HARRIS, VL_COVDET_METHOD_NUM } VlCovDetMethod; /** @brief Mapping between strings and ::VlCovDetMethod values */ VL_EXPORT VlEnumerator vlCovdetMethods [VL_COVDET_METHOD_NUM] ; #ifdef __DOXYGEN__ /** @brief Covariant feature detector ** @see @ref covdet */ struct _VlCovDet { } #endif /** @brief Covariant feature detector ** @see @ref covdet */ typedef struct _VlCovDet VlCovDet ; /** @name Create and destroy ** @{ */ VL_EXPORT VlCovDet * vl_covdet_new (VlCovDetMethod method) ; VL_EXPORT void vl_covdet_delete (VlCovDet * self) ; VL_EXPORT void vl_covdet_reset (VlCovDet * self) ; /** @} */ /** @name Process data ** @{ */ VL_EXPORT int vl_covdet_put_image (VlCovDet * self, float const * image, vl_size width, vl_size height) ; VL_EXPORT void vl_covdet_detect (VlCovDet * self, vl_size max_num_features) ; VL_EXPORT int vl_covdet_append_feature (VlCovDet * self, VlCovDetFeature const * feature) ; VL_EXPORT void vl_covdet_extract_orientations (VlCovDet * self) ; VL_EXPORT void vl_covdet_extract_laplacian_scales (VlCovDet * self) ; VL_EXPORT void vl_covdet_extract_affine_shape (VlCovDet * self) ; VL_EXPORT VlCovDetFeatureOrientation * vl_covdet_extract_orientations_for_frame (VlCovDet * self, vl_size *numOrientations, VlFrameOrientedEllipse frame) ; VL_EXPORT VlCovDetFeatureLaplacianScale * vl_covdet_extract_laplacian_scales_for_frame (VlCovDet * self, vl_size * numScales, VlFrameOrientedEllipse frame) ; VL_EXPORT int vl_covdet_extract_affine_shape_for_frame (VlCovDet * self, VlFrameOrientedEllipse * adapted, VlFrameOrientedEllipse frame) ; VL_EXPORT vl_bool vl_covdet_extract_patch_for_frame (VlCovDet * self, float * patch, vl_size resolution, double extent, double sigma, VlFrameOrientedEllipse frame) ; VL_EXPORT void vl_covdet_drop_features_outside (VlCovDet * self, double margin) ; /** @} */ /** @name Retrieve data and parameters ** @{ */ VL_EXPORT vl_size vl_covdet_get_num_features (VlCovDet const * self) ; VL_EXPORT VlCovDetFeature * vl_covdet_get_features (VlCovDet * self) ; VL_EXPORT vl_index vl_covdet_get_first_octave (VlCovDet const * self) ; VL_EXPORT vl_size vl_covdet_get_octave_resolution (VlCovDet const * self) ; VL_EXPORT double vl_covdet_get_peak_threshold (VlCovDet const * self) ; VL_EXPORT double vl_covdet_get_edge_threshold (VlCovDet const * self) ; VL_EXPORT double vl_covdeg_get_laplacian_peak_threshold (VlCovDet const * self) ; VL_EXPORT vl_bool vl_covdet_get_transposed (VlCovDet const * self) ; VL_EXPORT VlScaleSpace * vl_covdet_get_gss (VlCovDet const * self) ; VL_EXPORT VlScaleSpace * vl_covdet_get_css (VlCovDet const * self) ; VL_EXPORT vl_bool vl_covdet_get_aa_accurate_smoothing (VlCovDet const * self) ; VL_EXPORT vl_size const * vl_covdet_get_laplacian_scales_statistics (VlCovDet const * self, vl_size * numScales) ; VL_EXPORT double vl_covdet_get_non_extrema_suppression_threshold (VlCovDet const * self) ; VL_EXPORT vl_size vl_covdet_get_num_non_extrema_suppressed (VlCovDet const * self) ; /** @} */ /** @name Set parameters ** @{ */ VL_EXPORT void vl_covdet_set_first_octave (VlCovDet * self, vl_index o) ; VL_EXPORT void vl_covdet_set_octave_resolution (VlCovDet * self, vl_size r) ; VL_EXPORT void vl_covdet_set_peak_threshold (VlCovDet * self, double peakThreshold) ; VL_EXPORT void vl_covdet_set_edge_threshold (VlCovDet * self, double edgeThreshold) ; VL_EXPORT void vl_covdet_set_laplacian_peak_threshold (VlCovDet * self, double peakThreshold) ; VL_EXPORT void vl_covdet_set_transposed (VlCovDet * self, vl_bool t) ; VL_EXPORT void vl_covdet_set_aa_accurate_smoothing (VlCovDet * self, vl_bool x) ; VL_EXPORT void vl_covdet_set_non_extrema_suppression_threshold (VlCovDet * self, double x) ; /** @} */ /* VL_COVDET_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/dsift.c000077500000000000000000000602241454702036400203720ustar00rootroot00000000000000/** @file dsift.c ** @brief Dense SIFT - Definition ** @author Andrea Vedaldi ** @author Brian Fulkerson **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #include "dsift.h" #include "pgm.h" #include "mathop.h" #include "imopv.h" #include #include /** @page dsift Dense Scale Invariant Feature Transform (DSIFT) @author Andrea Vedaldi @author Brian Fulkerson @tableofcontents @ref dsift.h implements a dense version of @ref sift.h "SIFT". This is an object that can quickly compute descriptors for densely sampled keypoints with identical size and orientation. It can be reused for multiple images of the same size. @section dsift-intro Overview @sa @ref sift "The SIFT module", @ref dsift-tech "Technical details" This module implements a fast algorithm for the calculation of a large number of SIFT descriptors of densely sampled features of the same scale and orientation. See the @ref sift "SIFT module" for an overview of SIFT. The feature frames (keypoints) are indirectly specified by the sampling steps (::vl_dsift_set_steps) and the sampling bounds (::vl_dsift_set_bounds). The descriptor geometry (number and size of the spatial bins and number of orientation bins) can be customized (::vl_dsift_set_geometry, ::VlDsiftDescriptorGeometry). @image html dsift-geom.png "Dense SIFT descriptor geometry" By default, SIFT uses a Gaussian windowing function that discounts contributions of gradients further away from the descriptor centers. This function can be changed to a flat window by invoking ::vl_dsift_set_flat_window. In this case, gradients are accumulated using only bilinear interpolation, but instad of being reweighted by a Gassuain window, they are all weighted equally. However, after gradients have been accumulated into a spatial bin, the whole bin is reweighted by the average of the Gaussian window over the spatial support of that bin. This “approximation” substantially improves speed with little or no loss of performance in applications. Keypoints are sampled in such a way that the centers of the spatial bins are at integer coordinates within the image boundaries. For instance, the top-left bin of the top-left descriptor is centered on the pixel (0,0). The bin immediately to the right at (binSizeX,0), where binSizeX is a paramtere in the ::VlDsiftDescriptorGeometry structure. ::vl_dsift_set_bounds can be used to further restrict sampling to the keypoints in an image. @section dsift-usage Usage DSIFT is implemented by a ::VlDsiftFilter object that can be used to process a sequence of images of a given geometry. To use the DSIFT filter: - Initialize a new DSIFT filter object by ::vl_dsift_new (or the simplified ::vl_dsift_new_basic). Customize the descriptor parameters by ::vl_dsift_set_steps, ::vl_dsift_set_geometry, etc. - Process an image by ::vl_dsift_process. - Retrieve the number of keypoints (::vl_dsift_get_keypoint_num), the keypoints (::vl_dsift_get_keypoints), and their descriptors (::vl_dsift_get_descriptors). - Optionally repeat for more images. - Delete the DSIFT filter by ::vl_dsift_delete. @section dsift-tech Technical details This section extends the @ref sift-tech-descriptor "SIFT descriptor section" and specialzies it to the case of dense keypoints. @subsection dsift-tech-descriptor-dense Dense descriptors When computing descriptors for many keypoints differing only by their position (and with null rotation), further simplifications are possible. In this case, in fact, @f{eqnarray*} \mathbf{x} &=& m \sigma \hat{\mathbf{x}} + T,\\ h(t,i,j) &=& m \sigma \int g_{\sigma_\mathrm{win}}(\mathbf{x} - T)\, w_\mathrm{ang}(\angle J(\mathbf{x}) - \theta_t)\, w\left(\frac{x - T_x}{m\sigma} - \hat{x}_i\right)\, w\left(\frac{y - T_y}{m\sigma} - \hat{y}_j\right)\, |J(\mathbf{x})|\, d\mathbf{x}. @f} Since many different values of @e T are sampled, this is conveniently expressed as a separable convolution. First, we translate by @f$ \mathbf{x}_{ij} = m\sigma(\hat x_i,\ \hat y_i)^\top @f$ and we use the symmetry of the various binning and windowing functions to write @f{eqnarray*} h(t,i,j) &=& m \sigma \int g_{\sigma_\mathrm{win}}(T' - \mathbf{x} - \mathbf{x}_{ij})\, w_\mathrm{ang}(\angle J(\mathbf{x}) - \theta_t)\, w\left(\frac{T'_x - x}{m\sigma}\right)\, w\left(\frac{T'_y - y}{m\sigma}\right)\, |J(\mathbf{x})|\, d\mathbf{x}, \\ T' &=& T + m\sigma \left[\begin{array}{cc} x_i \\ y_j \end{array}\right]. @f} Then we define kernels @f{eqnarray*} k_i(x) &=& \frac{1}{\sqrt{2\pi} \sigma_{\mathrm{win}}} \exp\left( -\frac{1}{2} \frac{(x-x_i)^2}{\sigma_{\mathrm{win}}^2} \right) w\left(\frac{x}{m\sigma}\right), \\ k_j(y) &=& \frac{1}{\sqrt{2\pi} \sigma_{\mathrm{win}}} \exp\left( -\frac{1}{2} \frac{(y-y_j)^2}{\sigma_{\mathrm{win}}^2} \right) w\left(\frac{y}{m\sigma}\right), @f} and obtain @f{eqnarray*} h(t,i,j) &=& (k_ik_j * \bar J_t)\left( T + m\sigma \left[\begin{array}{cc} x_i \\ y_j \end{array}\right] \right), \\ \bar J_t(\mathbf{x}) &=& w_\mathrm{ang}(\angle J(\mathbf{x}) - \theta_t)\,|J(\mathbf{x})|. @f} Furthermore, if we use a flat rather than Gaussian windowing function, the kernels do not depend on the bin, and we have @f{eqnarray*} k(z) &=& \frac{1}{\sigma_{\mathrm{win}}} w\left(\frac{z}{m\sigma}\right), \\ h(t,i,j) &=& (k(x)k(y) * \bar J_t)\left( T + m\sigma \left[\begin{array}{cc} x_i \\ y_j \end{array}\right] \right), @f} (here @f$ \sigma_\mathrm{win} @f$ is the side of the flat window). @note In this case the binning functions @f$ k(z) @f$ are triangular and the convolution can be computed in time independent on the filter (i.e. descriptor bin) support size by integral signals. @subsection dsift-tech-sampling Sampling To avoid resampling and dealing with special boundary conditions, we impose some mild restrictions on the geometry of the descriptors that can be computed. In particular, we impose that the bin centers @f$ T + m\sigma (x_i,\ y_j) @f$ are always at integer coordinates within the image boundaries. This eliminates the need for costly interpolation. This condition amounts to (expressed in terms of the @e x coordinate, and equally applicable to @e y) @f[ \{0,\dots, W-1\} \ni T_x + m\sigma x_i = T_x + m\sigma i - \frac{N_x-1}{2} = \bar T_x + m\sigma i, \qquad i = 0,\dots,N_x-1. @f] Notice that for this condition to be satisfied, the @em descriptor center @f$ T_x @f$ needs to be either fractional or integer depending on @f$ N_x @f$ being even or odd. To eliminate this complication, it is simpler to use as a reference not the descriptor center @e T, but the coordinates of the upper-left bin @f$ \bar T @f$. Thus we sample the latter on a regular (integer) grid @f[ \left[\begin{array}{cc} 0 \\ 0 \end{array}\right] \leq \bar T = \left[\begin{array}{cc} \bar T_x^{\min} + p \Delta_x \\ \bar T_y^{\min} + q \Delta_y \\ \end{array}\right] \leq \left[\begin{array}{cc} W - 1 - m\sigma N_x \\ H - 1 - m\sigma N_y \end{array}\right], \quad \bar T = \left[\begin{array}{cc} T_x - \frac{N_x - 1}{2} \\ T_y - \frac{N_y - 1}{2} \\ \end{array}\right] @f] and we impose that the bin size @f$ m \sigma @f$ is integer as well. **/ /** ------------------------------------------------------------------ ** @internal @brief Initialize new convolution kernel ** @param binSize ** @param numBins ** @param binIndex negative to use flat window. ** @param windowSize ** @return a pointer to new filter. **/ float * _vl_dsift_new_kernel (int binSize, int numBins, int binIndex, double windowSize) { int filtLen = 2 * binSize - 1 ; float * ker = vl_malloc (sizeof(float) * filtLen) ; float * kerIter = ker ; float delta = binSize * (binIndex - 0.5F * (numBins - 1)) ; /* float sigma = 0.5F * ((numBins - 1) * binSize + 1) ; float sigma = 0.5F * ((numBins) * binSize) ; */ float sigma = (float) binSize * (float) windowSize ; int x ; for (x = - binSize + 1 ; x <= + binSize - 1 ; ++ x) { float z = (x - delta) / sigma ; *kerIter++ = (1.0F - fabsf(x) / binSize) * ((binIndex >= 0) ? expf(- 0.5F * z*z) : 1.0F) ; } return ker ; } static float _vl_dsift_get_bin_window_mean (int binSize, int numBins, int binIndex, double windowSize) { float delta = binSize * (binIndex - 0.5F * (numBins - 1)) ; /*float sigma = 0.5F * ((numBins - 1) * binSize + 1) ;*/ float sigma = (float) binSize * (float) windowSize ; int x ; float acc = 0.0 ; for (x = - binSize + 1 ; x <= + binSize - 1 ; ++ x) { float z = (x - delta) / sigma ; acc += ((binIndex >= 0) ? expf(- 0.5F * z*z) : 1.0F) ; } return acc /= (2 * binSize - 1) ; } /** ------------------------------------------------------------------ ** @internal @brief Normalize histogram ** @param begin first element of the histogram. ** @param end last plus one element of the histogram. ** ** The function divides the specified histogram by its l2 norm. **/ VL_INLINE float _vl_dsift_normalize_histogram (float * begin, float * end) { float * iter ; float norm = 0.0F ; for (iter = begin ; iter < end ; ++ iter) { norm += (*iter) * (*iter) ; } norm = vl_fast_sqrt_f (norm) + VL_EPSILON_F ; for (iter = begin; iter < end ; ++ iter) { *iter /= norm ; } return norm ; } /** ------------------------------------------------------------------ ** @internal @brief Free internal buffers ** @param self DSIFT filter. **/ static void _vl_dsift_free_buffers (VlDsiftFilter* self) { if (self->frames) { vl_free(self->frames) ; self->frames = NULL ; } if (self->descrs) { vl_free(self->descrs) ; self->descrs = NULL ; } if (self->grads) { int t ; for (t = 0 ; t < self->numGradAlloc ; ++t) if (self->grads[t]) vl_free(self->grads[t]) ; vl_free(self->grads) ; self->grads = NULL ; } self->numFrameAlloc = 0 ; self->numBinAlloc = 0 ; self->numGradAlloc = 0 ; } /** ------------------------------------------------------------------ ** @internal @brief Updates internal buffers to current geometry **/ VL_EXPORT void _vl_dsift_update_buffers (VlDsiftFilter * self) { int x1 = self->boundMinX ; int x2 = self->boundMaxX ; int y1 = self->boundMinY ; int y2 = self->boundMaxY ; int rangeX = x2 - x1 - (self->geom.numBinX - 1) * self->geom.binSizeX ; int rangeY = y2 - y1 - (self->geom.numBinY - 1) * self->geom.binSizeY ; int numFramesX = (rangeX >= 0) ? rangeX / self->stepX + 1 : 0 ; int numFramesY = (rangeY >= 0) ? rangeY / self->stepY + 1 : 0 ; self->numFrames = numFramesX * numFramesY ; self->descrSize = self->geom.numBinT * self->geom.numBinX * self->geom.numBinY ; } /** ------------------------------------------------------------------ ** @internal @brief Allocate internal buffers ** @param self DSIFT filter. ** ** The function (re)allocates the internal buffers in accordance with ** the current image and descriptor geometry. **/ static void _vl_dsift_alloc_buffers (VlDsiftFilter* self) { _vl_dsift_update_buffers (self) ; { int numFrameAlloc = vl_dsift_get_keypoint_num (self) ; int numBinAlloc = vl_dsift_get_descriptor_size (self) ; int numGradAlloc = self->geom.numBinT ; /* see if we need to update the buffers */ if (numBinAlloc != self->numBinAlloc || numGradAlloc != self->numGradAlloc || numFrameAlloc != self->numFrameAlloc) { int t ; _vl_dsift_free_buffers(self) ; self->frames = vl_malloc(sizeof(VlDsiftKeypoint) * numFrameAlloc) ; self->descrs = vl_malloc(sizeof(float) * numBinAlloc * numFrameAlloc) ; self->grads = vl_malloc(sizeof(float*) * numGradAlloc) ; for (t = 0 ; t < numGradAlloc ; ++t) { self->grads[t] = vl_malloc(sizeof(float) * self->imWidth * self->imHeight) ; } self->numBinAlloc = numBinAlloc ; self->numGradAlloc = numGradAlloc ; self->numFrameAlloc = numFrameAlloc ; } } } /** ------------------------------------------------------------------ ** @brief Create a new DSIFT filter ** ** @param imWidth width of the image. ** @param imHeight height of the image ** ** @return new filter. **/ VL_EXPORT VlDsiftFilter * vl_dsift_new (int imWidth, int imHeight) { VlDsiftFilter * self = vl_malloc (sizeof(VlDsiftFilter)) ; self->imWidth = imWidth ; self->imHeight = imHeight ; self->stepX = 5 ; self->stepY = 5 ; self->boundMinX = 0 ; self->boundMinY = 0 ; self->boundMaxX = imWidth - 1 ; self->boundMaxY = imHeight - 1 ; self->geom.numBinX = 4 ; self->geom.numBinY = 4 ; self->geom.numBinT = 8 ; self->geom.binSizeX = 5 ; self->geom.binSizeY = 5 ; self->useFlatWindow = VL_FALSE ; self->windowSize = 2.0 ; self->convTmp1 = vl_malloc(sizeof(float) * self->imWidth * self->imHeight) ; self->convTmp2 = vl_malloc(sizeof(float) * self->imWidth * self->imHeight) ; self->numBinAlloc = 0 ; self->numFrameAlloc = 0 ; self->numGradAlloc = 0 ; self->descrSize = 0 ; self->numFrames = 0 ; self->grads = NULL ; self->frames = NULL ; self->descrs = NULL ; _vl_dsift_update_buffers(self) ; return self ; } /** ------------------------------------------------------------------ ** @brief Create a new DSIFT filter (basic interface) ** @param imWidth width of the image. ** @param imHeight height of the image. ** @param step sampling step. ** @param binSize bin size. ** @return new filter. ** ** The descriptor geometry matches the standard SIFT descriptor. **/ VL_EXPORT VlDsiftFilter * vl_dsift_new_basic (int imWidth, int imHeight, int step, int binSize) { VlDsiftFilter* self = vl_dsift_new(imWidth, imHeight) ; VlDsiftDescriptorGeometry geom = *vl_dsift_get_geometry(self) ; geom.binSizeX = binSize ; geom.binSizeY = binSize ; vl_dsift_set_geometry(self, &geom) ; vl_dsift_set_steps(self, step, step) ; return self ; } /** ------------------------------------------------------------------ ** @brief Delete DSIFT filter ** @param self DSIFT filter. **/ VL_EXPORT void vl_dsift_delete (VlDsiftFilter * self) { _vl_dsift_free_buffers (self) ; if (self->convTmp2) vl_free (self->convTmp2) ; if (self->convTmp1) vl_free (self->convTmp1) ; vl_free (self) ; } /** ------------------------------------------------------------------ ** @internal @brief Process with Gaussian window ** @param self DSIFT filter. **/ VL_INLINE void _vl_dsift_with_gaussian_window (VlDsiftFilter * self) { int binx, biny, bint ; int framex, framey ; float *xker, *yker ; int Wx = self->geom.binSizeX - 1 ; int Wy = self->geom.binSizeY - 1 ; for (biny = 0 ; biny < self->geom.numBinY ; ++biny) { yker = _vl_dsift_new_kernel (self->geom.binSizeY, self->geom.numBinY, biny, self->windowSize) ; for (binx = 0 ; binx < self->geom.numBinX ; ++binx) { xker = _vl_dsift_new_kernel(self->geom.binSizeX, self->geom.numBinX, binx, self->windowSize) ; for (bint = 0 ; bint < self->geom.numBinT ; ++bint) { vl_imconvcol_vf (self->convTmp1, self->imHeight, self->grads[bint], self->imWidth, self->imHeight, self->imWidth, yker, -Wy, +Wy, 1, VL_PAD_BY_CONTINUITY|VL_TRANSPOSE) ; vl_imconvcol_vf (self->convTmp2, self->imWidth, self->convTmp1, self->imHeight, self->imWidth, self->imHeight, xker, -Wx, +Wx, 1, VL_PAD_BY_CONTINUITY|VL_TRANSPOSE) ; { float *dst = self->descrs + bint + binx * self->geom.numBinT + biny * (self->geom.numBinX * self->geom.numBinT) ; float *src = self->convTmp2 ; int frameSizeX = self->geom.binSizeX * (self->geom.numBinX - 1) + 1 ; int frameSizeY = self->geom.binSizeY * (self->geom.numBinY - 1) + 1 ; int descrSize = vl_dsift_get_descriptor_size (self) ; for (framey = self->boundMinY ; framey <= self->boundMaxY - frameSizeY + 1 ; framey += self->stepY) { for (framex = self->boundMinX ; framex <= self->boundMaxX - frameSizeX + 1 ; framex += self->stepX) { *dst = src [(framex + binx * self->geom.binSizeX) * 1 + (framey + biny * self->geom.binSizeY) * self->imWidth] ; dst += descrSize ; } /* framex */ } /* framey */ } } /* for bint */ vl_free (xker) ; } /* for binx */ vl_free (yker) ; } /* for biny */ } /** ------------------------------------------------------------------ ** @internal @brief Process with flat window. ** @param self DSIFT filter object. **/ VL_INLINE void _vl_dsift_with_flat_window (VlDsiftFilter* self) { int binx, biny, bint ; int framex, framey ; /* for each orientation bin */ for (bint = 0 ; bint < self->geom.numBinT ; ++bint) { vl_imconvcoltri_f (self->convTmp1, self->imHeight, self->grads [bint], self->imWidth, self->imHeight, self->imWidth, self->geom.binSizeY, /* filt size */ 1, /* subsampling step */ VL_PAD_BY_CONTINUITY|VL_TRANSPOSE) ; vl_imconvcoltri_f (self->convTmp2, self->imWidth, self->convTmp1, self->imHeight, self->imWidth, self->imHeight, self->geom.binSizeX, 1, VL_PAD_BY_CONTINUITY|VL_TRANSPOSE) ; for (biny = 0 ; biny < self->geom.numBinY ; ++biny) { /* This fast version of DSIFT does not use a proper Gaussian weighting scheme for the gradiens that are accumulated on the spatial bins. Instead each spatial bins is accumulated based on the triangular kernel only, equivalent to bilinear interpolation plus a flat, rather than Gaussian, window. Eventually, however, the magnitude of the spatial bins in the SIFT descriptor is reweighted by the average of the Gaussian window on each bin. */ float wy = _vl_dsift_get_bin_window_mean (self->geom.binSizeY, self->geom.numBinY, biny, self->windowSize) ; /* The convolution functions vl_imconvcoltri_* convolve by a * triangular kernel with unit integral. Instead for SIFT the * triangular kernel should have unit height. This is * compensated for by multiplying by the bin size: */ wy *= self->geom.binSizeY ; for (binx = 0 ; binx < self->geom.numBinX ; ++binx) { float w ; float wx = _vl_dsift_get_bin_window_mean (self->geom.binSizeX, self->geom.numBinX, binx, self->windowSize) ; float *dst = self->descrs + bint + binx * self->geom.numBinT + biny * (self->geom.numBinX * self->geom.numBinT) ; float *src = self->convTmp2 ; int frameSizeX = self->geom.binSizeX * (self->geom.numBinX - 1) + 1 ; int frameSizeY = self->geom.binSizeY * (self->geom.numBinY - 1) + 1 ; int descrSize = vl_dsift_get_descriptor_size (self) ; wx *= self->geom.binSizeX ; w = wx * wy ; for (framey = self->boundMinY ; framey <= self->boundMaxY - frameSizeY + 1 ; framey += self->stepY) { for (framex = self->boundMinX ; framex <= self->boundMaxX - frameSizeX + 1 ; framex += self->stepX) { *dst = w * src [(framex + binx * self->geom.binSizeX) * 1 + (framey + biny * self->geom.binSizeY) * self->imWidth] ; dst += descrSize ; } /* framex */ } /* framey */ } /* binx */ } /* biny */ } /* bint */ } /** ------------------------------------------------------------------ ** @brief Compute keypoints and descriptors ** ** @param self DSIFT filter. ** @param im image data. **/ void vl_dsift_process (VlDsiftFilter* self, float const* im) { int t, x, y ; /* update buffers */ _vl_dsift_alloc_buffers (self) ; /* clear integral images */ for (t = 0 ; t < self->geom.numBinT ; ++t) memset (self->grads[t], 0, sizeof(float) * self->imWidth * self->imHeight) ; #undef at #define at(x,y) (im[(y)*self->imWidth+(x)]) /* Compute gradients, their norm, and their angle */ for (y = 0 ; y < self->imHeight ; ++ y) { for (x = 0 ; x < self->imWidth ; ++ x) { float gx, gy ; float angle, mod, nt, rbint ; int bint ; /* y derivative */ if (y == 0) { gy = at(x,y+1) - at(x,y) ; } else if (y == self->imHeight - 1) { gy = at(x,y) - at(x,y-1) ; } else { gy = 0.5F * (at(x,y+1) - at(x,y-1)) ; } /* x derivative */ if (x == 0) { gx = at(x+1,y) - at(x,y) ; } else if (x == self->imWidth - 1) { gx = at(x,y) - at(x-1,y) ; } else { gx = 0.5F * (at(x+1,y) - at(x-1,y)) ; } /* angle and modulus */ angle = vl_fast_atan2_f (gy,gx) ; mod = vl_fast_sqrt_f (gx*gx + gy*gy) ; /* quantize angle */ nt = vl_mod_2pi_f (angle) * (self->geom.numBinT / (2*VL_PI)) ; bint = (int) vl_floor_f (nt) ; rbint = nt - bint ; /* write it back */ self->grads [(bint ) % self->geom.numBinT][x + y * self->imWidth] = (1 - rbint) * mod ; self->grads [(bint + 1) % self->geom.numBinT][x + y * self->imWidth] = ( rbint) * mod ; } } if (self->useFlatWindow) { _vl_dsift_with_flat_window(self) ; } else { _vl_dsift_with_gaussian_window(self) ; } { VlDsiftKeypoint* frameIter = self->frames ; float * descrIter = self->descrs ; int framex, framey, bint ; int frameSizeX = self->geom.binSizeX * (self->geom.numBinX - 1) + 1 ; int frameSizeY = self->geom.binSizeY * (self->geom.numBinY - 1) + 1 ; int descrSize = vl_dsift_get_descriptor_size (self) ; float deltaCenterX = 0.5F * self->geom.binSizeX * (self->geom.numBinX - 1) ; float deltaCenterY = 0.5F * self->geom.binSizeY * (self->geom.numBinY - 1) ; float normConstant = frameSizeX * frameSizeY ; for (framey = self->boundMinY ; framey <= self->boundMaxY - frameSizeY + 1 ; framey += self->stepY) { for (framex = self->boundMinX ; framex <= self->boundMaxX - frameSizeX + 1 ; framex += self->stepX) { frameIter->x = framex + deltaCenterX ; frameIter->y = framey + deltaCenterY ; /* mass */ { float mass = 0 ; for (bint = 0 ; bint < descrSize ; ++ bint) mass += descrIter[bint] ; mass /= normConstant ; frameIter->norm = mass ; } /* L2 normalize */ _vl_dsift_normalize_histogram (descrIter, descrIter + descrSize) ; /* clamp */ for(bint = 0 ; bint < descrSize ; ++ bint) if (descrIter[bint] > 0.2F) descrIter[bint] = 0.2F ; /* L2 normalize */ _vl_dsift_normalize_histogram (descrIter, descrIter + descrSize) ; frameIter ++ ; descrIter += descrSize ; } /* for framex */ } /* for framey */ } } colmap-3.9.1/src/thirdparty/VLFeat/dsift.h000066400000000000000000000254541454702036400204020ustar00rootroot00000000000000/** @file dsift.h ** @brief Dense SIFT (@ref dsift) ** @author Andrea Vedaldi ** @author Brian Fulkerson **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_DSIFT_H #define VL_DSIFT_H #include "generic.h" /** @brief Dense SIFT keypoint */ typedef struct VlDsiftKeypoint_ { double x ; /**< x coordinate */ double y ; /**< y coordinate */ double s ; /**< scale */ double norm ; /**< SIFT descriptor norm */ } VlDsiftKeypoint ; /** @brief Dense SIFT descriptor geometry */ typedef struct VlDsiftDescriptorGeometry_ { int numBinT ; /**< number of orientation bins */ int numBinX ; /**< number of bins along X */ int numBinY ; /**< number of bins along Y */ int binSizeX ; /**< size of bins along X */ int binSizeY ; /**< size of bins along Y */ } VlDsiftDescriptorGeometry ; /** @brief Dense SIFT filter */ typedef struct VlDsiftFilter_ { int imWidth ; /**< @internal @brief image width */ int imHeight ; /**< @internal @brief image height */ int stepX ; /**< frame sampling step X */ int stepY ; /**< frame sampling step Y */ int boundMinX ; /**< frame bounding box min X */ int boundMinY ; /**< frame bounding box min Y */ int boundMaxX ; /**< frame bounding box max X */ int boundMaxY ; /**< frame bounding box max Y */ /** descriptor parameters */ VlDsiftDescriptorGeometry geom ; int useFlatWindow ; /**< flag: whether to approximate the Gaussian window with a flat one */ double windowSize ; /**< size of the Gaussian window */ int numFrames ; /**< number of sampled frames */ int descrSize ; /**< size of a descriptor */ VlDsiftKeypoint *frames ; /**< frame buffer */ float *descrs ; /**< descriptor buffer */ int numBinAlloc ; /**< buffer allocated: descriptor size */ int numFrameAlloc ; /**< buffer allocated: number of frames */ int numGradAlloc ; /**< buffer allocated: number of orientations */ float **grads ; /**< gradient buffer */ float *convTmp1 ; /**< temporary buffer */ float *convTmp2 ; /**< temporary buffer */ } VlDsiftFilter ; VL_EXPORT VlDsiftFilter *vl_dsift_new (int width, int height) ; VL_EXPORT VlDsiftFilter *vl_dsift_new_basic (int width, int height, int step, int binSize) ; VL_EXPORT void vl_dsift_delete (VlDsiftFilter *self) ; VL_EXPORT void vl_dsift_process (VlDsiftFilter *self, float const* im) ; VL_INLINE void vl_dsift_transpose_descriptor (float* dst, float const* src, int numBinT, int numBinX, int numBinY) ; /** @name Setting parameters ** @{ **/ VL_INLINE void vl_dsift_set_steps (VlDsiftFilter *self, int stepX, int stepY) ; VL_INLINE void vl_dsift_set_bounds (VlDsiftFilter *self, int minX, int minY, int maxX, int maxY) ; VL_INLINE void vl_dsift_set_geometry (VlDsiftFilter *self, VlDsiftDescriptorGeometry const* geom) ; VL_INLINE void vl_dsift_set_flat_window (VlDsiftFilter *self, vl_bool useFlatWindow) ; VL_INLINE void vl_dsift_set_window_size (VlDsiftFilter *self, double windowSize) ; /** @} */ /** @name Retrieving data and parameters ** @{ **/ VL_INLINE float const *vl_dsift_get_descriptors (VlDsiftFilter const *self) ; VL_INLINE int vl_dsift_get_descriptor_size (VlDsiftFilter const *self) ; VL_INLINE int vl_dsift_get_keypoint_num (VlDsiftFilter const *self) ; VL_INLINE VlDsiftKeypoint const *vl_dsift_get_keypoints (VlDsiftFilter const *self) ; VL_INLINE void vl_dsift_get_bounds (VlDsiftFilter const *self, int* minX, int* minY, int* maxX, int* maxY) ; VL_INLINE void vl_dsift_get_steps (VlDsiftFilter const* self, int* stepX, int* stepY) ; VL_INLINE VlDsiftDescriptorGeometry const* vl_dsift_get_geometry (VlDsiftFilter const *self) ; VL_INLINE vl_bool vl_dsift_get_flat_window (VlDsiftFilter const *self) ; VL_INLINE double vl_dsift_get_window_size (VlDsiftFilter const *self) ; /** @} */ VL_EXPORT void _vl_dsift_update_buffers (VlDsiftFilter *self) ; /** ------------------------------------------------------------------ ** @brief Get descriptor size. ** @param self DSIFT filter object. ** @return size of a descriptor. **/ int vl_dsift_get_descriptor_size (VlDsiftFilter const *self) { return self->descrSize ; } /** ------------------------------------------------------------------ ** @brief Get descriptors. ** @param self DSIFT filter object. ** @return descriptors. **/ float const * vl_dsift_get_descriptors (VlDsiftFilter const *self) { return self->descrs ; } /** ------------------------------------------------------------------ ** @brief Get keypoints ** @param self DSIFT filter object. **/ VlDsiftKeypoint const * vl_dsift_get_keypoints (VlDsiftFilter const *self) { return self->frames ; } /** ------------------------------------------------------------------ ** @brief Get number of keypoints ** @param self DSIFT filter object. **/ int vl_dsift_get_keypoint_num (VlDsiftFilter const *self) { return self->numFrames ; } /** ------------------------------------------------------------------ ** @brief Get SIFT descriptor geometry ** @param self DSIFT filter object. ** @return DSIFT descriptor geometry. **/ VlDsiftDescriptorGeometry const* vl_dsift_get_geometry (VlDsiftFilter const *self) { return &self->geom ; } /** ------------------------------------------------------------------ ** @brief Get bounds ** @param self DSIFT filter object. ** @param minX bounding box minimum X coordinate. ** @param minY bounding box minimum Y coordinate. ** @param maxX bounding box maximum X coordinate. ** @param maxY bounding box maximum Y coordinate. **/ void vl_dsift_get_bounds (VlDsiftFilter const* self, int *minX, int *minY, int *maxX, int *maxY) { *minX = self->boundMinX ; *minY = self->boundMinY ; *maxX = self->boundMaxX ; *maxY = self->boundMaxY ; } /** ------------------------------------------------------------------ ** @brief Get flat window flag ** @param self DSIFT filter object. ** @return @c TRUE if the DSIFT filter uses a flat window. **/ int vl_dsift_get_flat_window (VlDsiftFilter const* self) { return self->useFlatWindow ; } /** ------------------------------------------------------------------ ** @brief Get steps ** @param self DSIFT filter object. ** @param stepX sampling step along X. ** @param stepY sampling step along Y. **/ void vl_dsift_get_steps (VlDsiftFilter const* self, int* stepX, int* stepY) { *stepX = self->stepX ; *stepY = self->stepY ; } /** ------------------------------------------------------------------ ** @brief Set steps ** @param self DSIFT filter object. ** @param stepX sampling step along X. ** @param stepY sampling step along Y. **/ void vl_dsift_set_steps (VlDsiftFilter* self, int stepX, int stepY) { self->stepX = stepX ; self->stepY = stepY ; _vl_dsift_update_buffers(self) ; } /** ------------------------------------------------------------------ ** @brief Set bounds ** @param self DSIFT filter object. ** @param minX bounding box minimum X coordinate. ** @param minY bounding box minimum Y coordinate. ** @param maxX bounding box maximum X coordinate. ** @param maxY bounding box maximum Y coordinate. **/ void vl_dsift_set_bounds (VlDsiftFilter* self, int minX, int minY, int maxX, int maxY) { self->boundMinX = minX ; self->boundMinY = minY ; self->boundMaxX = maxX ; self->boundMaxY = maxY ; _vl_dsift_update_buffers(self) ; } /** ------------------------------------------------------------------ ** @brief Set SIFT descriptor geometry ** @param self DSIFT filter object. ** @param geom descriptor geometry parameters. **/ void vl_dsift_set_geometry (VlDsiftFilter *self, VlDsiftDescriptorGeometry const *geom) { self->geom = *geom ; _vl_dsift_update_buffers(self) ; } /** ------------------------------------------------------------------ ** @brief Set flat window flag ** @param self DSIFT filter object. ** @param useFlatWindow @c true if the DSIFT filter should use a flat window. **/ void vl_dsift_set_flat_window (VlDsiftFilter* self, vl_bool useFlatWindow) { self->useFlatWindow = useFlatWindow ; } /** ------------------------------------------------------------------ ** @brief Transpose descriptor ** ** @param dst destination buffer. ** @param src source buffer. ** @param numBinT ** @param numBinX ** @param numBinY ** ** The function writes to @a dst the transpose of the SIFT descriptor ** @a src. Let I be an image. The transpose operator ** satisfies the equation transpose(dsift(I,x,y)) = ** dsift(transpose(I),y,x) **/ VL_INLINE void vl_dsift_transpose_descriptor (float* dst, float const* src, int numBinT, int numBinX, int numBinY) { int t, x, y ; for (y = 0 ; y < numBinY ; ++y) { for (x = 0 ; x < numBinX ; ++x) { int offset = numBinT * (x + y * numBinX) ; int offsetT = numBinT * (y + x * numBinY) ; for (t = 0 ; t < numBinT ; ++t) { int tT = numBinT / 4 - t ; dst [offsetT + (tT + numBinT) % numBinT] = src [offset + t] ; } } } } /** ------------------------------------------------------------------ ** @brief Set SIFT descriptor Gaussian window size ** @param self DSIFT filter object. ** @param windowSize window size. **/ void vl_dsift_set_window_size(VlDsiftFilter * self, double windowSize) { assert(windowSize >= 0.0) ; self->windowSize = windowSize ; } /** ------------------------------------------------------------------ ** @brief Get SIFT descriptor Gaussian window size ** @param self DSIFT filter object. ** @return window size. **/ VL_INLINE double vl_dsift_get_window_size(VlDsiftFilter const * self) { return self->windowSize ; } /* VL_DSIFT_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/fisher.c000077500000000000000000000455071454702036400205500ustar00rootroot00000000000000/** @file fisher.c ** @brief Fisher - Declaration ** @author David Novotny **/ /* Copyright (C) 2013 David Novotny and Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page fisher Fisher Vector encoding (FV) @author David Novotny @author Andrea Vedaldi @ref fisher.h implements the Fisher Vectors (FV) image representation @cite{perronnin06fisher} @cite{perronnin10improving}. A FV is a statistics capturing the distribution of a set of vectors, usually a set of local image descriptors. @ref fisher-starting demonstrates how to use the C API to compute the FV representation of an image. For further details refer to: - @subpage fisher-fundamentals - Fisher Vector definition. - @subpage fisher-derivation - Deriving the Fisher Vectors as a Fisher Kernel. - @subpage fisher-kernel - The Fisher Kernel in general. @section fisher-starting Getting started The Fisher Vector encoding of a set of features is obtained by using the function ::vl_fisher_encode. Note that the function requires a @ref gmm "Gaussian Mixture Model" (GMM) of the encoded feature distribution. In the following code, the result of the coding process is stored in the @c enc array and the improved fisher vector normalization is used. @code float * means ; float * covariances ; float * priors ; float * posteriors ; float * enc; // create a GMM object and cluster input data to get means, covariances // and priors of the estimated mixture gmm = vl_gmm_new (VL_TYPE_FLOAT) ; vl_gmm_cluster (gmm, data, dimension, numData, numClusters); // allocate space for the encoding enc = vl_malloc(sizeof(float) * 2 * dimension * numClusters); // run fisher encoding vl_fisher_encode (enc, VL_F_TYPE, vl_gmm_get_means(gmm), dimension, numClusters, vl_gmm_get_covariances(gmm), vl_gmm_get_priors(gmm), dataToEncode, numDataToEncode, VL_FISHER_FLAG_IMPROVED ) ; @endcode The performance of the standard Fisher Vector can be significantly improved @cite{perronnin10improving} by using appropriate @ref fisher-normalization normalizations. These are controlled by the @c flag parameter of ::vl_fisher_encode. @page fisher-fundamentals Fisher vector fundamentals @tableofcontents This page describes the *Fisher Vector* (FV) of @cite{perronnin06fisher} @cite{perronnin10improving}. See @ref fisher for an overview of the C API and @ref fisher-kernel for its relation to the more general notion of Fisher kernel. The FV is an image representation obtained by pooling local image features. It is frequently used as a global image descriptor in visual classification. While the FV can be @ref fisher-kernel "derived" as a special, approximate, and improved case of the general Fisher Kernel framework, it is easy to describe directly. Let $I = (\bx_1,\dots,\bx_N)$ be a set of $D$ dimensional feature vectors (e.g. SIFT descriptors) extracted from an image. Let $\Theta=(\mu_k,\Sigma_k,\pi_k:k=1,\dots,K)$ be the parameters of a @ref gmm "Gaussian Mixture Model" fitting the distribution of descriptors. The GMM associates each vector $\bx_i$ to a mode $k$ in the mixture with a strength given by the posterior probability: \[ q_{ik} = \frac {\exp\left[-\frac{1}{2}(\bx_i - \mu_k)^T \Sigma_k^{-1} (\bx_i - \mu_k)\right]} {\sum_{t=1}^K \exp\left[-\frac{1}{2}(\bx_i - \mu_t)^T \Sigma_k^{-1} (\bx_i - \mu_t)\right]}. \] For each mode $k$, consider the mean and covariance deviation vectors @f{align*} u_{jk} &= {1 \over {N \sqrt{\pi_k}}} \sum_{i=1}^{N} q_{ik} \frac{x_{ji} - \mu_{jk}}{\sigma_{jk}}, \\ v_{jk} &= {1 \over {N \sqrt{2 \pi_k}}} \sum_{i=1}^{N} q_{ik} \left[ \left(\frac{x_{ji} - \mu_{jk}}{\sigma_{jk}}\right)^2 - 1 \right]. @f} where $j=1,2,\dots,D$ spans the vector dimensions. The FV of image $I$ is the stacking of the vectors $\bu_k$ and then of the vectors $\bv_k$ for each of the $K$ modes in the Gaussian mixtures: \[ \Phi(I) = \begin{bmatrix} \vdots \\ \bu_k \\ \vdots \\ \bv_k \\ \vdots \end{bmatrix}. \] @section fisher-normalization Normalization and improved Fisher vectors The *improved* Fisher Vector @cite{perronnin10improving} (IFV) improves the classification performance of the representation by using to ideas: 1. *Non-linear additive kernel.* The Hellinger's kernel (or Bhattacharya coefficient) can be used instead of the linear one at no cost by signed squared rooting. This is obtained by applying the function $|z| \sign z$ to each dimension of the vector $\Phi(I)$. Other @ref homkermap "additive kernels" can also be used at an increased space or time cost. 2. *Normalization.* Before using the representation in a linear model (e.g. a @ref svm "support vector machine"), the vector $\Phi(I)$ is further normalized by the $l^2$ norm (note that the standard Fisher vector is normalized by the number of encoded feature vectors). After square-rooting and normalization, the IFV is often used in a linear classifier such as an @ref svm "SVM". @section fisher-fast Faster computations In practice, several data to cluster assignments $q_{ik}$ are likely to be very small or even negligible. The *fast* version of the FV sets to zero all but the largest assignment for each input feature $\bx_i$. @page fisher-derivation Fisher vector derivation The FV of @cite{perronnin06fisher} is a special case of the @ref fisher-kernel "Fisher kernel" construction. It is designed to encode local image features in a format that is suitable for learning and comparison with simple metrics such as the Euclidean. In this construction, an image is modeled as a collection of $D$-dimensional feature vectors $I=(\bx_1,\dots,\bx_n)$ generated by a GMM with $K$ components $\Theta=(\mu_k,\Sigma_k,\pi_k:k=1,\dots,K)$. The covariance matrices are assumed to be diagonal, i.e. $\Sigma_k = \diag \bsigma_k^2$, $\bsigma_k \in \real^D_+$. The generative model of *one* feature vector $\bx$ is given by the GMM density function: \[ p(\bx|\Theta) = \sum_{k=1}^K \pi_k p(\bx|\Theta_k), \quad p(\bx|\Theta_k) = \frac{1}{(2\pi)^\frac{D}{2} (\det \Sigma_k)^{\frac{1}{2}}} \exp \left[ -\frac{1}{2} (\bx - \mu_k)^\top \Sigma_k^{-1} (\bx - \mu_k) \right] \] where $\Theta_k = (\mu_k,\Sigma_k)$. The Fisher Vector requires computing the derivative of the log-likelihood function with respect to the various model parameters. Consider in particular the parameters $\Theta_k$ of a mode. Due to the exponent in the Gaussian density function, the derivative can be written as \[ \nabla_{\Theta_k} p(\bx|\Theta_k) = p(\bx|\Theta_k) g(\bx|\Theta_k) \] for a simple vector function $g$. The derivative of the log-likelihood function is then \[ \nabla_{\Theta_k} \log p(\bx|\Theta) = \frac{\pi_k p(\bx|\Theta_k)}{\sum_{t=1}^K \pi_k p(\bx|\Theta_k)} g(\bx|\Theta_k) = q_k(\bx) g(\bx|\Theta_k) \] where $q_k(\bx)$ is the soft-assignment of the point $\bx$ to the mode $k$. We make the approximation that $q_k(\bx)\approx 1$ if $\bx$ is sampled from mode $k$ and $\approx 0$ otherwise @cite{perronnin06fisher}. Hence one gets: \[ E_{\bx \sim p(\bx|\Theta)} [ \nabla_{\Theta_k} \log p(\bx|\Theta) \nabla_{\Theta_t} \log p(\bx|\Theta)^\top ] \approx \begin{cases} \pi_k E_{\bx \sim p(\bx|\Theta_k)} [ g(\bx|\Theta_k) g(\bx|\Theta_k)^\top], & t = k, \\ 0, & t\not=k. \end{cases} \] Thus under this approximation there is no correlation between the parameters of the various Gaussian modes. The function $g$ can be further broken down as the stacking of the derivative w.r.t. the mean and the diagonal covariance. \[ g(\bx|\Theta_k) = \begin{bmatrix} g(\bx|\mu_k) \\ g(\bx|\bsigma_k) \end{bmatrix}, \quad [g(\bx|\mu_k)]_j = \frac{x_j - \mu_{jk}}{\sigma_{jk}^2}, \quad [g(\bx|\bsigma_k^2)]_j = \frac{1}{2\sigma_{jk}^2} \left( \left(\frac{x_j - \mu_{jk}}{\sigma_{jk}}\right)^2 - 1 \right) \] Thus the covariance of the model (Fisher information) is diagonal and the diagonal entries are given by \[ H_{\mu_{jk}} = \pi_k E[g(\bx|\mu_{jk})g(\bx|\mu_{jk})] = \frac{\pi_k}{\sigma_{jk}^2}, \quad H_{\sigma_{jk}^2} = \frac{\pi_k}{2 \sigma_{jk}^4}. \] where in the calculation it was used the fact that the fourth moment of the standard Gaussian distribution is 3. Multiplying the inverse square root of the matrix $H$ by the derivative of the log-likelihood function results in the Fisher vector encoding of one image feature $\bx$: \[ \Phi_{\mu_{jk}}(\bx) = H_{\mu_{jk}}^{-\frac{1}{2}} q_k(\bx) g(\bx|\mu_{jk}) = q_k(\bx) \frac{x_j - \mu_{jk}}{\sqrt{\pi_k}\sigma_{jk}}, \qquad \Phi_{\sigma^2_{jk}}(\bx) = \frac{q_k(\bx)}{\sqrt{2 \pi_k}} \left( \left(\frac{x_j - \mu_{jk}}{\sigma_{jk}}\right)^2 - 1 \right) \] Assuming that features are sampled i.i.d. from the GMM results in the formulas given in @ref fisher-fundamentals (note the normalization factor). Note that: * The Fisher components relative to the prior probabilities $\pi_k$ have been ignored. This is because they have little effect on the representation @cite{perronnin10improving}. * Technically, the derivation of the Fisher Vector for multiple image features requires the number of features to be the same in both images. Ultimately, however, the representation can be computed by using any number of features. @page fisher-kernel Fisher kernel This page discusses the Fisher Kernels (FK) of @cite{jaakkola98exploiting} and shows how the FV of @cite{perronnin06fisher} can be derived from it as a special case. The FK induces a similarity measures between data points $\bx$ and $\bx'$ from a parametric generative model $p(\bx|\Theta)$ of the data. The parameter $\Theta$ of the model is selected to fit the a-priori distribution of the data, and is usually the Maximum Likelihood (MLE) estimate obtained from a set of training examples. Once the generative model is learned, each particular datum $\bx$ is represented by looking at how it affects the MLE parameter estimate. This effect is measured by computing the gradient of the log-likelihood term corresponding to $\bx$: \[ \hat\Phi(\bx) = \nabla_\Theta \log p(\bx|\Theta) \] The vectors $\hat\Phi(\bx)$ should be appropriately scaled before they can be meaningfully compared. This is obtained by *whitening* the data by multiplying the vectors by the inverse of the square root of their *covariance matrix*. The covariance matrix can be obtained from the generative model $p(\bx|\Theta)$ itself. Since $\Theta$ is the ML parameter and $\hat\Phi(\bx)$ is the gradient of the log-likelihood function, its expected value $E[\hat\Phi(\bx)]$ is zero. Thus, since the vectors are already centered, their covariance matrix is simply: \[ H = E_{\bx \sim p(\bx|\Theta)} [\hat\Phi(\bx) \hat\Phi(\bx)^\top] \] Note that $H$ is also the *Fisher information matrix* of the model. The final FV encoding $\Phi(\bx)$ is given by the whitened gradient of the log-likelihood function, i.e.: \[ \Phi(\bx) = H^{-\frac{1}{2}} \nabla_\Theta \log p(\bx|\Theta). \] Taking the inner product of two such vectors yields the *Fisher kernel*: \[ K(\bx,\bx') = \langle \Phi(\bx),\Phi(\bx') \rangle = \nabla_\Theta \log p(\bx|\Theta)^\top H^{-1} \nabla_\Theta \log p(\bx'|\Theta). \] **/ #include "fisher.h" #include "gmm.h" #include "mathop.h" #include #include #include #ifdef VL_FISHER_INSTANTIATING static vl_size VL_XCAT(_vl_fisher_encode_, SFX) (TYPE * enc, TYPE const * means, vl_size dimension, vl_size numClusters, TYPE const * covariances, TYPE const * priors, TYPE const * data, vl_size numData, int flags) { vl_size dim; vl_index i_cl, i_d; vl_size numTerms = 0 ; TYPE * posteriors ; TYPE * sqrtInvSigma; assert(numClusters >= 1) ; assert(dimension >= 1) ; posteriors = vl_malloc(sizeof(TYPE) * numClusters * numData); sqrtInvSigma = vl_malloc(sizeof(TYPE) * dimension * numClusters); memset(enc, 0, sizeof(TYPE) * 2 * dimension * numClusters) ; for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { for(dim = 0; dim < dimension; dim++) { sqrtInvSigma[i_cl*dimension + dim] = sqrt(1.0 / covariances[i_cl*dimension + dim]); } } VL_XCAT(vl_get_gmm_data_posteriors_, SFX)(posteriors, numClusters, numData, priors, means, dimension, covariances, data) ; /* sparsify posterior assignments with the FAST option */ if (flags & VL_FISHER_FLAG_FAST) { for(i_d = 0; i_d < (signed)numData; i_d++) { /* find largest posterior assignment for datum i_d */ vl_index best = 0 ; TYPE bestValue = posteriors[i_d * numClusters] ; for (i_cl = 1 ; i_cl < (signed)numClusters; ++ i_cl) { TYPE p = posteriors[i_cl + i_d * numClusters] ; if (p > bestValue) { bestValue = p ; best = i_cl ; } } /* make all posterior assignments zero but the best one */ for (i_cl = 0 ; i_cl < (signed)numClusters; ++ i_cl) { posteriors[i_cl + i_d * numClusters] = (TYPE)(i_cl == best) ; } } } #if defined(_OPENMP) #pragma omp parallel for default(shared) private(i_cl, i_d, dim) num_threads(vl_get_max_threads()) reduction(+:numTerms) #endif for(i_cl = 0; i_cl < (signed)numClusters; ++ i_cl) { TYPE uprefix; TYPE vprefix; TYPE * uk = enc + i_cl*dimension ; TYPE * vk = enc + i_cl*dimension + numClusters * dimension ; /* If the GMM component is degenerate and has a null prior, then it must have null posterior as well. Hence it is safe to skip it. In practice, we skip over it even if the prior is very small; if by any chance a feature is assigned to such a mode, then its weight would be very high due to the division by priors[i_cl] below. */ if (priors[i_cl] < 1e-6) { continue ; } for(i_d = 0; i_d < (signed)numData; i_d++) { TYPE p = posteriors[i_cl + i_d * numClusters] ; if (p < 1e-6) continue ; numTerms += 1; for(dim = 0; dim < dimension; dim++) { TYPE diff = data[i_d*dimension + dim] - means[i_cl*dimension + dim] ; diff *= sqrtInvSigma[i_cl*dimension + dim] ; *(uk + dim) += p * diff ; *(vk + dim) += p * (diff * diff - 1); } } if (numData > 0) { uprefix = 1/(numData*sqrt(priors[i_cl])); vprefix = 1/(numData*sqrt(2*priors[i_cl])); for(dim = 0; dim < dimension; dim++) { *(uk + dim) = *(uk + dim) * uprefix; *(vk + dim) = *(vk + dim) * vprefix; } } } vl_free(posteriors); vl_free(sqrtInvSigma) ; if (flags & VL_FISHER_FLAG_SQUARE_ROOT) { for(dim = 0; dim < 2 * dimension * numClusters ; dim++) { TYPE z = enc [dim] ; if (z >= 0) { enc[dim] = VL_XCAT(vl_sqrt_, SFX)(z) ; } else { enc[dim] = - VL_XCAT(vl_sqrt_, SFX)(- z) ; } } } if (flags & VL_FISHER_FLAG_NORMALIZED) { TYPE n = 0 ; for(dim = 0 ; dim < 2 * dimension * numClusters ; dim++) { TYPE z = enc [dim] ; n += z * z ; } n = VL_XCAT(vl_sqrt_, SFX)(n) ; n = VL_MAX(n, 1e-12) ; for(dim = 0 ; dim < 2 * dimension * numClusters ; dim++) { enc[dim] /= n ; } } return numTerms ; } #else /* not VL_FISHER_INSTANTIATING */ #ifndef __DOXYGEN__ #define FLT VL_TYPE_FLOAT #define TYPE float #define SFX f #define VL_FISHER_INSTANTIATING #include "fisher.c" #define FLT VL_TYPE_DOUBLE #define TYPE double #define SFX d #define VL_FISHER_INSTANTIATING #include "fisher.c" #endif /* not VL_FISHER_INSTANTIATING */ #endif /* ================================================================ */ #ifndef VL_FISHER_INSTANTIATING /** @brief Fisher vector encoding of a set of vectors. ** @param dataType the type of the input data (::VL_TYPE_DOUBLE or ::VL_TYPE_FLOAT). ** @param enc Fisher vector (output). ** @param means Gaussian mixture means. ** @param dimension dimension of the data. ** @param numClusters number of Gaussians mixture components. ** @param covariances Gaussian mixture diagonal covariances. ** @param priors Gaussian mixture prior probabilities. ** @param data vectors to encode. ** @param numData number of vectors to encode. ** @param flags options. ** @return number of averaging operations. ** ** @a means and @a covariances have @a dimension rows and @a ** numCluster columns. @a priors is a vector of size @a ** numCluster. @a data has @a dimension rows and @a numData ** columns. @a enc is a vecotr of size equal to twice the product of ** @a dimension and @a numClusters. All these vectors and matrices ** have the same class, as specified by @a dataType, and must be ** stored in column-major format. ** ** @a flag can be used to control several options: ** ::VL_FISHER_FLAG_SQUARE_ROOT, ::VL_FISHER_FLAG_NORMALIZED, ** ::VL_FISHER_FLAG_IMPROVED, and ::VL_FISHER_FLAG_FAST. ** ** The function returns the number of averaging operations actually ** performed. The upper bound is the number of input features by the ** number of GMM modes; however, assignments are usually failry ** sparse, so this number is often much smaller. In particular, with ** the ::VL_FISHER_FLAG_FAST, is equal to the number of input ** features. This information can be used for diagnostic purposes. ** ** @sa @ref fisher **/ VL_EXPORT vl_size vl_fisher_encode (void * enc, vl_type dataType, void const * means, vl_size dimension, vl_size numClusters, void const * covariances, void const * priors, void const * data, vl_size numData, int flags ) { switch(dataType) { case VL_TYPE_FLOAT: return _vl_fisher_encode_f ((float *) enc, (float const *) means, dimension, numClusters, (float const *) covariances, (float const *) priors, (float const *) data, numData, flags); case VL_TYPE_DOUBLE: return _vl_fisher_encode_d ((double *) enc, (double const *) means, dimension, numClusters, (double const *) covariances, (double const *) priors, (double const *) data, numData, flags); break; default: abort(); } } /* not VL_FISHER_INSTANTIATING */ #endif #undef SFX #undef TYPE #undef FLT #undef VL_FISHER_INSTANTIATING colmap-3.9.1/src/thirdparty/VLFeat/fisher.h000066400000000000000000000030001454702036400205300ustar00rootroot00000000000000/** @file fisher.h ** @brief Fisher encoding (@ref fisher) ** @author David Novotny ** @author Andrea Vedaldi ** @see @ref fisher **/ /* Copyright (C) 2013 David Novotny and Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_FISHER_H #define VL_FISHER_H #include "generic.h" /** @name Fisher vector options ** @{ */ #define VL_FISHER_FLAG_SQUARE_ROOT (0x1 << 0) #define VL_FISHER_FLAG_NORMALIZED (0x1 << 1) #define VL_FISHER_FLAG_IMPROVED (VL_FISHER_FLAG_NORMALIZED|VL_FISHER_FLAG_SQUARE_ROOT) #define VL_FISHER_FLAG_FAST (0x1 << 2) /** @def VL_FISHER_FLAG_SQUARE_ROOT ** @brief Use signed squared-root (@ref fisher-normalization). **/ /** @def VL_FISHER_FLAG_NORMALIZED ** @brief Gobally normalize the Fisher vector in L2 norm (@ref fisher-normalization). **/ /** @def VL_FISHER_FLAG_IMPROVED ** @brief Improved Fisher vector. ** This is the same as @c VL_FISHER_FLAG_SQUARE_ROOT|VL_FISHER_FLAG_NORMALIZED. **/ /** @def VL_FISHER_FLAG_FAST ** @brief Fast but more approximate calculations (@ref fisher-fast). ** Keep only the larges data to cluster assignment (posterior). **/ /** @} */ VL_EXPORT vl_size vl_fisher_encode (void * enc, vl_type dataType, void const * means, vl_size dimension, vl_size numClusters, void const * covariances, void const * priors, void const * data, vl_size numData, int flags) ; /* VL_FISHER_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/float.h000066400000000000000000000062721454702036400203730ustar00rootroot00000000000000/** @file float.h ** @brief Float - Template ** @author Andrea Vedaldi ** @author David Novotny **/ /* Copyright (C) 2014 Andrea Vedaldi. Copyright (C) 2013 David Novotny. Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #include "generic.h" #undef T #undef SFX #undef VSIZE #undef VSFX #undef VTYPE #undef VSIZEavx #undef VSFXavx #undef VTYPEavx #if (FLT == VL_TYPE_FLOAT) # define T float # define SFX f #elif (FLT == VL_TYPE_DOUBLE) # define T double # define SFX d #elif (FLT == VL_TYPE_UINT32) # define T vl_uint32 # define SFX ui32 #elif (FLT == VL_TYPE_INT32) # define T vl_int32 # define SFX i32 #endif /* ---------------------------------------------------------------- */ /* AVX */ /* ---------------------------------------------------------------- */ #ifdef __AVX__ #if (FLT == VL_TYPE_FLOAT) # define VSIZEavx 8 # define VSFXavx s # define VTYPEavx __m256 #elif (FLT == VL_TYPE_DOUBLE) # define VSIZEavx 4 # define VSFXavx d # define VTYPEavx __m256d #endif #define VALIGNEDavx(x) (! (((vl_uintptr)(x)) & 0x1F)) #define VMULavx VL_XCAT(_mm256_mul_p, VSFX) #define VDIVavx VL_XCAT(_mm256_div_p, VSFX) #define VADDavx VL_XCAT(_mm256_add_p, VSFX) #define VHADDavx VL_XCAT(_mm_hadd_p, VSFX) #define VHADD2avx VL_XCAT(_mm256_hadd_p, VSFX) #define VSUBavx VL_XCAT(_mm256_sub_p, VSFX) #define VSTZavx VL_XCAT(_mm256_setzero_p, VSFX) #define VLD1avx VL_XCAT(_mm256_broadcast_s, VSFX) #define VLDUavx VL_XCAT(_mm256_loadu_p, VSFX) #define VST1avx VL_XCAT(_mm256_store_s, VSFX) #define VST2avx VL_XCAT(_mm256_store_p, VSFX) #define VST2Uavx VL_XCAT(_mm256_storeu_p, VSFX) #define VPERMavx VL_XCAT(_mm256_permute2f128_p, VSFX) //#define VCSTavx VL_XCAT( _mm256_castps256_ps128, VSFX) #define VCSTavx VL_XCAT5(_mm256_castp,VSFX,256_p,VSFX,128) /* __AVX__ */ #endif /* ---------------------------------------------------------------- */ /* SSE2 */ /* ---------------------------------------------------------------- */ #ifdef __SSE2__ #if (FLT == VL_TYPE_FLOAT) # define VSIZE 4 # define VSFX s # define VTYPE __m128 #elif (FLT == VL_TYPE_DOUBLE) # define VSIZE 2 # define VSFX d # define VTYPE __m128d #endif #define VALIGNED(x) (! (((vl_uintptr)(x)) & 0xF)) #define VMAX VL_XCAT(_mm_max_p, VSFX) #define VMUL VL_XCAT(_mm_mul_p, VSFX) #define VDIV VL_XCAT(_mm_div_p, VSFX) #define VADD VL_XCAT(_mm_add_p, VSFX) #define VSUB VL_XCAT(_mm_sub_p, VSFX) #define VSTZ VL_XCAT(_mm_setzero_p, VSFX) #define VLD1 VL_XCAT(_mm_load1_p, VSFX) #define VLDU VL_XCAT(_mm_loadu_p, VSFX) #define VST1 VL_XCAT(_mm_store_s, VSFX) #define VSET1 VL_XCAT(_mm_set_s, VSFX) #define VSHU VL_XCAT(_mm_shuffle_p, VSFX) #define VNEQ VL_XCAT(_mm_cmpneq_p, VSFX) #define VAND VL_XCAT(_mm_and_p, VSFX) #define VANDN VL_XCAT(_mm_andnot_p, VSFX) #define VST2 VL_XCAT(_mm_store_p, VSFX) #define VST2U VL_XCAT(_mm_storeu_p, VSFX) /* __SSE2__ */ #endif colmap-3.9.1/src/thirdparty/VLFeat/generic.c000077500000000000000000001565251454702036400207070ustar00rootroot00000000000000/** @file generic.c ** @brief Generic - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. Copyright (C) 2013 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @mainpage Vision Lab Features Library (VLFeat) @version __VLFEAT_VERSION__ @author The VLFeat Team @par Copyright © 2012-14 The VLFeat Authors @par Copyright © 2007-11 Andrea Vedaldi and Brian Fulkerson The VLFeat C library implements common computer vision algorithms, with a special focus on visual features, as used in state-of-the-art object recognition and image matching applications. VLFeat strives to be clutter-free, simple, portable, and well documented. @section main-contents Contents - **Visual feature detectors and descriptors** - @subpage sift - @subpage dsift - @subpage mser - @subpage covdet - @subpage scalespace - @subpage hog - @subpage fisher - @subpage vlad - @subpage liop - @subpage lbp - **Clustering and indexing** - @subpage kmeans - @subpage ikmeans.h "Integer K-means (IKM)" - @subpage hikmeans.h "Hierarchical Integer K-means (HIKM)" - @subpage gmm - @subpage aib - @subpage kdtree - **Segmentation** - @subpage slic - @subpage quickshift - **Statistical methods** - @subpage aib - @subpage homkermap - @subpage svm - **Utilities** - @subpage random - @subpage mathop - @subpage stringop.h "String operations" - @subpage imopv.h "Image operations" - @subpage pgm.h "PGM image format" - @subpage heap-def.h "Generic heap object (priority queue)" - @subpage rodrigues.h "Rodrigues formula" - @subpage mexutils.h "MATLAB MEX helper functions" - @subpage getopt_long.h "Drop-in @c getopt_long replacement" - **General information** - @subpage conventions - @subpage generic - @subpage portability - @ref resources - @subpage objects - @ref threads - @subpage matlab - @subpage metaprogram - @subpage dev - @subpage glossary **/ /** @page resources Memory and resource handling @author Andrea Vedaldi Some VLFeat functions return pointers to memory blocks or objects. Only ::vl_malloc, ::vl_calloc, ::vl_realloc and functions whose name contains either the keywords @c new or @c copy transfer the ownership of the memory block or object to the caller. The caller must dispose explicitly of all the resources it owns (by calling ::vl_free for a memory block, or the appropriate deletion function for an object). The memory allocation functions can be customized by ::vl_set_alloc_func (which sets the implementations of ::vl_malloc, ::vl_realloc, ::vl_calloc and ::vl_free). Remapping the memory allocation functions can be done only if there are no currently allocated VLFeat memory blocks or objects -- thus typically at the very beginning of a program. The memory allocation functions are a global property, shared by all threads. VLFeat uses three rules that simplify handling exceptions when used in combination which certain environment such as MATLAB. - The library allocates local memory only through the re-programmable ::vl_malloc, ::vl_calloc, and ::vl_realloc functions. - The only resource referenced by VLFeat objects is memory (for instance, it is illegal for an object to reference an open file). Other resources such as files or threads may be allocated within a VLFeat function call, but they are all released before the function ends, or their ownership is directly transferred to the caller. - The global library state is an exception. It cannot reference any local object created by the caller and uses the standard C memory allocation functions. In this way, the VLFeat local state can be reset at any time simply by disposing of all the memory allocated by the library so far. The latter can be done easily by mapping the memory allocation functions to implementations that track the memory blocks allocated, and then disposing of all such blocks. Since the global state does not reference any local object nor uses the remapped memory functions, it is unaffected by such an operation; conversely, since no VLFeat object references anything but memory, this guarantees that all allocated resources are properly disposed (avoiding leaking resource). This is used extensively in the design of MATLAB MEX files (see @ref matlab). **/ /** @page conventions Conventions @author Andrea Vedaldi @tableofcontents This page summarizes some of the conventions used by the library. @section conventions-storage Matrix and image storage conventions If not otherwise specified, matrices in VLFeat are stored in memory in column major order. Given a matrix $[A_{ij}] \in \real^{m \times n}$, this amounts of enumerating the elements one column per time: $A_{11}, A_{21}, \dots, A_{m1}, A_{12}, \dots, A_{mn}$. This convention is compatible with Fortran, MATLAB, and popular numerical libraries. Matrices are often used in the library to pack a number data vectors $\bx_1,\dots,\bx_n \in \real^m$ of equal dimension together. These are normally stored as the columns of the matrix: \[ X = \begin{bmatrix} \bx_1, \dots, \bx_n \end{bmatrix}, \qquad X \in \real_{m\times n} \] In this manner, consecutive elements of each data vector $\bx_i$ is stored in consecutive memory locations, improving memory access locality in most algorithms. Images $I(x,y)$ are stored instead in row-major order, i.e. one row after the other. Note that an image can be naturally identified as a matrix $I_{yx}$, where the vertical coordinate $y$ indexes the rows and the horizontal coordinate $x$ the columns. The image convention amounts to storing this matrix in row-major rather than column-major order, which is in conflict with the rule given above. The reason for this choice is that most image processing and graphical libraries assume this convention; it is, however, not the same as MATLAB's. **/ /** @page objects Objects @author Andrea Vedaldi @tableofcontents Many VLFeat algorithms are available in the form of *objects*. The C language, used by VLFeat, does not support objects explicitly. Here an object is intended a C structure along with a number of functions (the object member functions or methods) operating on it. Ideally, the object data structure is kept opaque to the user, for example by defining it in the @c .c implementation files which are not accessible to the library user. Object names are capitalized and start with the Vl prefix (for example @c VlExampleObject). Object methods are lowercase and start with the vl__ suffix (e.g. @c vl_example_object_new). @section objects-lifecycle Object lifecycle Conceptually, an object undergoes four phases during its lifecycle: allocation, initialization, finalization, and deallocation: - **Allocation.** The memory to hold the object structure is allocated. This is usually done by calling a memory allocation function such as ::vl_calloc to reserve an object of the required size @c sizeof(VlExampleObject). Alternatively, the object can simply by allocated on the stack by declaring a local variable of type VlExampleObject. - **Initialization.** The object is initialized by assigning a value to its data members and potentially allocating a number of resources, including other objects or memory buffers. Initialization is done by methods containing the @c init keyword, e.g. @c vl_example_object_init. Several such methods may be provided. - **Finalization.** Initialization is undone by finalization, whose main purpose is to release any resource allocated and still owned by the object. Finalization is done by the @c vl_example_object_finalize method. - **Deallocation.** The memory holding the object structure is disposed of, for example by calling ::vl_free or automatically when the corresponding local variable is popped from the stack. In practice, most VlFeat object are supposed to be created on the heap. To this end, allocation/initialization and finalization/deallocation are combined into two operations: - **Creating a new object.** This allocates a new object on the heap and initializes it, combining allocation and initialization in a single operation. It is done by methods containing the @c new keyword, e.g. @c vl_example_object_new. - **Deleting an object.** This disposes of an object created by a @c new method, combining finalization and deallocation, for example @c vl_example_object_delete. @section objects-getters-setters Getters and setters Most objects contain a number of methods to get (getters) and set (setters) properties. These should contain the @c get and @c set keywords in their name, for example @code double x = vl_example_object_get_property () ; vl_example_object_set_property(x) ; @endcode **/ /** @page matlab MATLAB integration @author Andrea Vedaldi The VLFeat C library is designed to integrate seamlessly with MATLAB. Binary compatibility is simplified by the use of the C language (rather than C++). In addition, the library design follows certain restrictions that make it compatible with the MATLAB MEX interface. The main issue in calling a library function from a MATLAB MEX function is that MATLAB can abort the execution of the MEX function at any point, either due to an error, or directly upon a user request (Ctrl-C) (empirically, however, a MEX function seems to be incorruptible only during the invocation of certain functions of the MEX API such as @c mexErrMsgTxt). When a MEX function is interrupted, resources (memory blocks or objects) whose ownership was transferred from VLFeat to the MEX function may be leaked. Notice that interrupting a MEX function would similarly leak any memory block allocated within the MEX function. To solve this issue, MATLAB provides his own memory manager (@c mxMalloc, @c mxRealloc, ...). When a MEX file is interrupted or ends, all memory blocks allocated by using one of such functions are released, preventing leakage. In order to integrate VLFeat with this model in the most seamless way, VLFeat memory allocation functions (::vl_malloc, ::vl_realloc, ::vl_calloc) are mapped to the corresponding MEX memory allocation functions. Such functions automatically dispose of all the memory allocated by a MEX function when the function ends (even because of an exception). Because of the restrictions of the library design illustrated in @ref resources, this operation is safe and correctly dispose of VLFeat local state. As a consequence, it is possible to call @c mexErrMsgTxt at any point in the MEX function without worrying about leaking resources. This however comes at the price of some limitations. Beyond the restrictions illustrated in @ref resources, here we note that no VLFeat local resource (memory blocks or objects) can persist across MEX file invocations. This implies that any result produced by a VLFeat MEX function must be converted back to a MATLAB object such as a vector or a structure. In particular, there is no direct way of creating an object within a MEX file, returning it to MATLAB, and passing it again to another MEX file. **/ /** @page metaprogram Preprocessor metaprogramming @author Andrea Vedaldi Part of VLFeat code uses a simple form of preprocessor metaprogramming. This technique is used, similarly to C++ templates, to instantiate multiple version of a given algorithm for different data types (e.g. @c float and @c double). In most cases preprocessor metaprogramming is invisible to the library user, as it is used only internally. **/ /** @page glossary Glossary - Column-major. A M x N matrix A is stacked with column-major order as the sequence \f$(A_{11}, A_{21}, \dots, A_{12}, \dots)\f$. More in general, when stacking a multi dimensional array this indicates that the first index is the one varying most quickly, with the other followed in the natural order. - Opaque structure. A structure is opaque if the user is not supposed to access its member directly, but through appropriate interface functions. Opaque structures are commonly used to define objects. - Row-major. A M x N matrix A is stacked with row-major order as the sequence \f$(A_{11}, A_{12}, \dots, A_{21}, \dots)\f$. More in general, when stacking a multi dimensional array this indicates that the last index is the one varying most quickly, with the other followed in reverse order. - Feature frame. A feature frame is the geometrical description of a visual features. For instance, the frame of a @ref sift.h "SIFT feature" is oriented disk and the frame of @ref mser.h "MSER feature" is either a compact and connected set or a disk. - Feature descriptor. A feature descriptor is a quantity (usually a vector) which describes compactly the appearance of an image region (usually corresponding to a feature frame). **/ /** @page dev Developing the library @tableofcontents This page contains information useful to the developer of VLFeat. @section dev-copy Copyright A short copyright notice is added at the beginning of each file. For example:
Copyright (C) 2013 Milan Sulc
Copyright (C) 2012 Daniele Perrone.
Copyright (C) 2011-13 Andrea Vedaldi.
All rights reserved.

This file is part of the VLFeat library and is made available under
the terms of the BSD license (see the COPYING file).
The copyright of each file is assigned to the authors of the file. Every author making a substantial contribution to a file should note its copyright by adding a line to the copyright list with the year of the modification. Year ranges are acceptable. Lines are never deleted, only appended, or potentially modified to list more years. @section dev-style Coding style
  • Look at existing code before you start. The general rule is: try to match the style of the existing code as much as possible.
  • No white spaces at the end of lines. White spaces introduce invisible changes in the code that are however picked up by control version systems such as Git.
  • Descriptive variable names. Most variable names start with a lower case letter and are capitalized, e.g., @c numElements. Only the following abbreviations are considered acceptable: @c num. The @c dimension of a vector is the number of elements it contains (for other objects that could be a @c size, a @c length, or a @c numElements). For multi-dimensional arrays, @c dimensions could indicate the array with each of the @c numDimensions dimensions.
  • Short variable names. For indexes in short for loops it is fine to use short index names such as @c i, @c j, and @c k. For example:
    for (i = 0 ; i < numEntries ; ++i) values[i] ++ ;
    
    is considered acceptable.
  • Function arguments. VLFeat functions that operate on an object (member functions) should be passed the object address as first argument; this argument should be called @c self. For example:
       void vl_object_do_something(VlObject *self) ;
    
    Multi-dimensional arrays should be specified first by their address, and then by their dimensions. For example
      void vl_use_array (float * array, vl_size numColumns, vl_size numRows) ; // good
      void vl_use_array (vl_size numColumns, vl_size numRows, float * array) ; // bad
    
    Arguments that are used as outputs should be specified first (closer to the left-hand side of an expression). For example
     void vl_sum_numbers (float * output, float input1, float input2) ; // good
     void vl_sum_numbers (float input1, float input2, float * output) ; // bad
    
    These rules can be combined. For example
     void vl_object_sum_to_array (VlObject * self, float * outArray,
            vl_size numColumns, vl_size numRows, float * inArray) ; // good
    
    Note that in this case no dimension for @c inArray is specified as it is assumed that @c numColumns and @c numRows are the dimensions of both arrays.
@subsection dev-style-matlab MATLAB coding style
  • Help messages. Each @c .m file should include a standard help comment block (accessible from MATLAB @c help() command). The first line of the block has a space, the name of the function, 4 spaces, and a brief command description. The body of the help message is indented with 4 spaces. For example @code % VL_FUNCTION An example function % VL_FUNCTION() does nothing. @endcode The content HELP message itself should follow MATLAB default style. For example, rather than giving a list of formal input and output arguments as often done, one simply shows how to use the function, explaining along the way the different ways the function can be called and the format of the parameters.
@section dev-doc Documenting the code The VLFeat C library code contains its own in documentation
Doxygen format. The documentation consists in generic pages, such as the @ref index "index" and the page you are reading, and documentations for each library module, usually corresponding to a certain header file. - **Inline comments.** Inline Doxygen comments are discouraged except in the documentation of data members of structures. They start with a capital letter and end with a period. For example: @code struct VlExampleStructure { int aMember ; /\*\*< A useful data member. } @endcode - **Brief comments.** Brief Doxygen comments starts by a capital and end with a period. The documentation of all functions start with a brief comment. @subsection devl-doc-modules Documenting the library modules A library module groups a number of data types and functions that implement a certain functionality of VLFeat. The documentation of a library module is generally organized as follows: 1. A page introducing the module and including a getting started section (3.g. @ref svm-starting) containing a short tutorial to quickly familiarize the user with the module (e.g. @ref svm). 2. One or more pages of detailed technical background discussing the algorithms implemented. These sections are used not just as part of the C API, but also as documentation for other APIs such as MATLAB (e.g. @ref svm-fundamentals). 3. One or more pages with the structure and function documentation (e.g. @ref svm.h). More in detail, consider a module called Example Module. Then one would typically have:
  • A header or declaration file @c example-module.h. Such a file has an heading of the type: @verbinclude example-module-doc.h This comment block contains a file directive, causing the file to be included in the documentation, a brief directive, specifying a short description of what the file is, and a list of authors. A (non-Doxygen) comment block with a short the copyright notice follows. The brief directive should include a @@ref directive to point to the main documentation page describing the module, if there is one.
  • An implementation or definition file @c example-module.c. This file has an heading of the type: @verbinclude example-module-doc.c This is similar to the declaration file, except for the content of the brief comment.
@subsection devl-doc-functions Documenting functions @subsection devl-doc-structures Documenting structures @subsection devl-doc-structures Documenting objects As seen in @ref objects, VLFeat treats certain structures with an object-like semantics. Usually, a module defines exactly one such objects. In this case, the object member functions should be grouped (by using Doxygen grouping functionality) as - **Construct and destroy** for the @c vl_object_new, @c vl_object_delete and similar member functions. - **Set parameters** for setter functions. - **Retrieve parameters and data** for getter functions. - **Process data** for functions processing data. @subsection devl-doc-bib Bibliographic references Since version 0.9.14, the VLFeat C library documentation makes use of a proper bibliographic reference in BibTeX format (see the file @c docsrc/vlfeat.bib). Doxygen uses this file when it sees instances of the @@cite{xyz} command. Here @c xyz is a BibTeX key. For example, @c vlfeat.bib file contains the entry:
@@inproceedings{martin97the-det-curve,
	Author = {A. Martin and G. Doddington and T. Kamm and M. Ordowski and M. Przybocki},
	Booktitle = {Proc. Conf. on Speech Communication and Technology},
	Title = {The {DET} curve in assessment of detection task performance},
	Year = {1997}}
For example, the Doxygen directive @@cite{martin97the-det-curve} generates the output @cite{martin97the-det-curve}, which is a link to the corresponding entry in the bibliography. **/ /** @file generic.h @page generic General support functionalities @author Andrea Vedaldi VLFeat contains several support functionalities addressing the C preprocessors, using multiple threads (including parallel computations), handling errors, allocating memory, etc. These are described in the following pages: - @subpage resources - @subpage threads - @subpage misc **/ /** @page misc Preprocssor, library state, etc. @author Andrea Vedaldi @tableofcontents @section misc-preproc C preprocessor helpers VLFeat provides a few C preprocessor macros of general utility. These include stringification (::VL_STRINGIFY, ::VL_XSTRINGIFY) and concatenation (::VL_CAT, ::VL_XCAT) of symbols. @section misc-state VLFeat state and configuration parameters VLFeat has some global configuration parameters that can changed. Changing the configuration is thread unsave (@ref threads). Use ::vl_set_simd_enabled to toggle the use of a SIMD unit (Intel SSE code), ::vl_set_alloc_func to change the memory allocation functions, and ::vl_set_printf_func to change the logging function. @section misc-error Error handling Some VLFeat functions signal errors in a way similar to the standard C library. In case of error, a VLFeat function may return an error code directly, or an invalid result (for instance a negative file descriptor or a null pointer). Then ::vl_get_last_error and ::vl_get_last_error_message can be used to retrieve further details about the error (these functions should be used right after an error has occurred, before any other VLFeat call). @section misc-memory Memory allocation VLFeat uses the ::vl_malloc, ::vl_realloc, ::vl_calloc and ::vl_free functions to allocate memory. Normally these functions are mapped to the underlying standard C library implementations. However ::vl_set_alloc_func can be used to map them to other implementations. For instance, in MATALB MEX files these functions are mapped to the MATLAB equivalent which has a garbage collection mechanism to cope with interruptions during execution. @section misc-logging Logging VLFeat uses the macros ::VL_PRINT and ::VL_PRINTF to print progress or debug informations. These functions are normally mapped to the @c printf function of the underlying standard C library. However ::vl_set_printf_func can be used to map it to a different implementation. For instance, in MATLAB MEX files this function is mapped to @c mexPrintf. Setting the function to @c NULL disables logging. @section misc-time Measuring time VLFeat provides ::vl_tic and ::vl_toc as an easy way of measuring elapsed time. **/ /** @page threads Threading @tableofcontents VLFeat supports for threaded computations can be used to take advantage of multi-core architectures. Threading support includes: - Supporting using VLFeat functions and objects from multiple threads simultaneously. This is discussed in @ref threads-multiple. - Using multiple cores to accelerate computations. This is discussed in @ref threads-parallel. @section threads-multiple Using VLFeat from multiple threads VLFeat can be used from multiple threads simultaneously if proper rules are followed. - A VLFeat object instance is accessed only from one thread at any given time. Functions operating on objects (member functions) are conditionally thread safe: the same function may be called simultaneously from multiple threads provided that it operates on different, independent objects. However, modifying the same object from multiple threads (using the same or different member functions) is possible only from one thread at any given time, and should therefore be synchronized. Certain VLFeat objects may contain features specific to simplify multi-threaded operations (e.g. ::VlKDForest). - Thread-safe global functions are used. These include thread-specific operations such as retrieving the last error by ::vl_get_last_error and obtaining the thread-specific random number generator instance by ::vl_get_rand. In these cases, the functions operate on thread-specific data that VLFeat creates and maintains. Note in particular that each thread has an independent default random number generator (as returned by ::vl_get_rand). VLFeat objects that involve using random numbers will typically use the random number generator of the thread currently accessing the object (although an object-specific generator can be often be specified instead). - Any other global function is considered non-thread safe and is accessed exclusively by one thread at a time. A small number of operations are non-reentrant and affect all threads simultaneously. These are restricted to changing certain global configuration parameters, such as the memory allocation functions by ::vl_set_alloc_func. These operations are not thread safe and are preferably executed before multiple threads start to operate with the library. @section threads-parallel Parallel computations VLFeat uses OpenMP to implement parallel computations. Generally, this means that multiple cores are uses appropriately and transparently, provided that other multi-threaded parts of the application use OpenMP and that VLFeat and the application link to the same OpenMP library. If finer control is required, read on. VLFeat functions avoids affecting OpenMP global state, including the desired number of computational threads, in order to minimize side effects to the linked application (e.g. MATLAB). Instead, VLFeat duplicates a few OpenMP control parameters when needed (this approach is similar to the method used by other libraries such as Intel MKL). The maximum number of threads available to the application can be obtained by ::vl_get_thread_limit (for OpenMP version 3.0 and greater). This limit is controlled by the OpenMP library (the function is a wrapper around @c omp_get_thread_limit), which in turn may determined that based on the number of computational cores or the value of the @c OMP_THREAD_LIMIT variable when the program is launched. This value is an upper bound on the number of computation threads that can be used at any time. The maximum number of computational thread that VLFeat should use is set by ::vl_set_num_threads() and retrieved by ::vl_get_max_threads(). This number is a target value as well as an upper bound to the number of threads used by VLFeat. This value is stored in the VLFeat private state and is not necessarily equal to the corresponding OpenMP state variable retrieved by calling @c omp_get_max_threads(). @c vl_set_num_threads(1) disables the use of multiple threads and @c vl_set_num_threads(0) uses the value returned by the OpenMP call @c omp_get_max_threads(). The latter value is controlled, for example, by calling @c omp_set_num_threads() in the application. Note that: - @c vl_set_num_threads(0) determines the number of treads using @c omp_get_max_threads() *when it is called*. Subsequent calls to @c omp_set_num_threads() will therefore *not* affect the number of threads used by VLFeat. - @c vl_set_num_threads(vl_get_thread_limit()) causes VLFeat use all the available threads, regardless on the number of threads set within the application by calls to @c omp_set_num_threads(). - OpenMP may still dynamically decide to use a smaller number of threads in any specific parallel computation. @sa http://software.intel.com/sites/products/documentation/doclib/mkl_sa/11/mkl_userguide_win/GUID-C2295BC8-DD22-466B-94C9-5FAA79D4F56D.htm http://software.intel.com/sites/products/documentation/doclib/mkl_sa/11/mkl_userguide_win/index.htm#GUID-DEEF0363-2B34-4BAB-87FA-A75DBE842040.htm http://software.intel.com/sites/products/documentation/hpc/mkl/lin/MKL_UG_managing_performance/Using_Additional_Threading_Control.htm **/ #include "generic.h" #include #include #include #include #include #if defined(VL_OS_WIN) #include #else #include #endif #if ! defined(VL_DISABLE_THREADS) && defined(VL_THREADS_POSIX) #include #endif #if defined(_OPENMP) #include #endif /* ---------------------------------------------------------------- */ /* Global and thread states */ /* ---------------------------------------------------------------- */ /* Thread state */ typedef struct _VlThreadState { /* errors */ int lastError ; char lastErrorMessage [VL_ERR_MSG_LEN] ; /* random number generator */ VlRand rand ; /* time */ #if defined(VL_OS_WIN) LARGE_INTEGER ticFreq ; LARGE_INTEGER ticMark ; #else clock_t ticMark ; #endif } VlThreadState ; /* Gobal state */ typedef struct _VlState { /* The thread state uses either a mutex (POSIX) or a critical section (Win) */ #if defined(VL_DISABLE_THREADS) VlThreadState * threadState ; #else #if defined(VL_THREADS_POSIX) pthread_key_t threadKey ; pthread_mutex_t mutex ; pthread_t mutexOwner ; pthread_cond_t mutexCondition ; size_t mutexCount ; #elif defined(VL_THREADS_WIN) DWORD tlsIndex ; CRITICAL_SECTION mutex ; #endif #endif /* VL_DISABLE_THREADS */ /* Configurable functions */ int (*printf_func) (char const * format, ...) ; void *(*malloc_func) (size_t) ; void *(*realloc_func) (void*,size_t) ; void *(*calloc_func) (size_t, size_t) ; void (*free_func) (void*) ; #if defined(VL_ARCH_IX86) || defined(VL_ARCH_X64) || defined(VL_ARCH_IA64) VlX86CpuInfo cpuInfo ; #endif vl_size numCPUs ; vl_bool simdEnabled ; vl_size numThreads ; } VlState ; /* Global state instance */ VlState _vl_state ; /* ----------------------------------------------------------------- */ VL_INLINE VlState * vl_get_state () ; VL_INLINE VlThreadState * vl_get_thread_specific_state () ; static void vl_lock_state (void) ; static void vl_unlock_state (void) ; static VlThreadState * vl_thread_specific_state_new (void) ; static void vl_thread_specific_state_delete (VlThreadState * self) ; /** @brief Get VLFeat version string ** @return the library version string. **/ char const * vl_get_version_string () { return VL_VERSION_STRING ; } /** @brief Get VLFeat configuration string. ** @return a new configuration string. ** ** The function returns a new string containing a human readable ** description of the library configuration. **/ char * vl_configuration_to_string_copy () { char * string = 0 ; int length = 0 ; char * staticString = vl_static_configuration_to_string_copy() ; char * cpuString = #if defined(VL_ARCH_IX86) || defined(VL_ARCH_X64) || defined(VL_ARCH_IA64) _vl_x86cpu_info_to_string_copy(&vl_get_state()->cpuInfo) ; #else "Generic CPU" ; #endif #if defined(DEBUG) int const debug = 1 ; #else int const debug = 0 ; #endif while (string == 0) { if (length > 0) { string = vl_malloc(sizeof(char) * length) ; if (string == NULL) break ; } length = snprintf(string, length, "VLFeat version %s\n" " Static config: %s\n" " %" VL_FMT_SIZE " CPU(s): %s\n" #if defined(_OPENMP) " OpenMP: max threads: %d (library: %" VL_FMT_SIZE ")\n" #endif " Debug: %s\n", vl_get_version_string (), staticString, vl_get_num_cpus(), cpuString, #if defined(_OPENMP) omp_get_max_threads(), vl_get_max_threads(), #endif VL_YESNO(debug)) ; length += 1 ; } if (staticString) vl_free(staticString) ; if (cpuString) vl_free(cpuString) ; return string ; } /** @internal @brief A printf that does not do anything */ static int do_nothing_printf (char const* format VL_UNUSED, ...) { return 0 ; } /** @internal ** @brief Lock VLFeat state ** ** The function locks VLFeat global state mutex. ** ** The mutex is recursive: locking multiple times from the same thread ** is a valid operations, but requires an equivalent number ** of calls to ::vl_unlock_state. ** ** @sa ::vl_unlock_state **/ static void vl_lock_state (void) { #if ! defined(VL_DISABLE_THREADS) #if defined(VL_THREADS_POSIX) VlState * state = vl_get_state () ; pthread_t thisThread = pthread_self () ; pthread_mutex_lock (&state->mutex) ; if (state->mutexCount >= 1 && pthread_equal (state->mutexOwner, thisThread)) { state->mutexCount ++ ; } else { while (state->mutexCount >= 1) { pthread_cond_wait (&state->mutexCondition, &state->mutex) ; } state->mutexOwner = thisThread ; state->mutexCount = 1 ; } pthread_mutex_unlock (&state->mutex) ; #elif defined(VL_THREADS_WIN) EnterCriticalSection (&vl_get_state()->mutex) ; #endif #endif } /** @internal ** @brief Unlock VLFeat state ** ** The function unlocks VLFeat global state mutex. ** ** @sa ::vl_lock_state **/ static void vl_unlock_state (void) { #if ! defined(VL_DISABLE_THREADS) #if defined(VL_THREADS_POSIX) VlState * state = vl_get_state () ; pthread_mutex_lock (&state->mutex) ; -- state->mutexCount ; if (state->mutexCount == 0) { pthread_cond_signal (&state->mutexCondition) ; } pthread_mutex_unlock (&state->mutex) ; #elif defined(VL_THREADS_WIN) LeaveCriticalSection (&vl_get_state()->mutex) ; #endif #endif } /** @internal ** @brief Return VLFeat global state ** ** The function returns a pointer to VLFeat global state. ** ** @return pointer to the global state structure. **/ VL_INLINE VlState * vl_get_state (void) { return &_vl_state ; } /** @internal@brief Get VLFeat thread state ** @return pointer to the thread state structure. ** ** The function returns a pointer to VLFeat thread state. **/ VL_INLINE VlThreadState * vl_get_thread_specific_state (void) { #ifdef VL_DISABLE_THREADS return vl_get_state()->threadState ; #else VlState * state ; VlThreadState * threadState ; vl_lock_state() ; state = vl_get_state() ; #if defined(VL_THREADS_POSIX) threadState = (VlThreadState *) pthread_getspecific(state->threadKey) ; #elif defined(VL_THREADS_WIN) threadState = (VlThreadState *) TlsGetValue(state->tlsIndex) ; #endif if (! threadState) { threadState = vl_thread_specific_state_new () ; } #if defined(VL_THREADS_POSIX) pthread_setspecific(state->threadKey, threadState) ; #elif defined(VL_THREADS_WIN) TlsSetValue(state->tlsIndex, threadState) ; #endif vl_unlock_state() ; return threadState ; #endif } /* ---------------------------------------------------------------- */ /** @brief Get the number of CPU cores of the host ** @return number of CPU cores. **/ vl_size vl_get_num_cpus (void) { return vl_get_state()->numCPUs ; } /** @fn ::vl_set_simd_enabled(vl_bool) ** @brief Toggle usage of SIMD instructions ** @param x @c true if SIMD instructions are used. ** ** Notice that SIMD instructions are used only if the CPU model ** supports them. Note also that data alignment may restrict the use ** of such instructions. ** ** @see ::vl_cpu_has_sse2(), ::vl_cpu_has_sse3(), etc. **/ void vl_set_simd_enabled (vl_bool x) { vl_get_state()->simdEnabled = x ; } /** @brief Are SIMD instructons enabled? ** @return @c true if SIMD instructions are enabled. **/ vl_bool vl_get_simd_enabled (void) { return vl_get_state()->simdEnabled ; } /** @brief Check for AVX instruction set ** @return @c true if AVX is present. **/ vl_bool vl_cpu_has_avx (void) { #if defined(VL_ARCH_IX86) || defined(VL_ARCH_X64) || defined(VL_ARCH_IA64) return vl_get_state()->cpuInfo.hasAVX ; #else return VL_FALSE ; #endif } /** @brief Check for SSE3 instruction set ** @return @c true if SSE3 is present. **/ vl_bool vl_cpu_has_sse3 (void) { #if defined(VL_ARCH_IX86) || defined(VL_ARCH_X64) || defined(VL_ARCH_IA64) return vl_get_state()->cpuInfo.hasSSE3 ; #else return VL_FALSE ; #endif } /** @brief Check for SSE2 instruction set ** @return @c true if SSE2 is present. **/ vl_bool vl_cpu_has_sse2 (void) { #if defined(VL_ARCH_IX86) || defined(VL_ARCH_X64) || defined(VL_ARCH_IA64) return vl_get_state()->cpuInfo.hasSSE2 ; #else return VL_FALSE ; #endif } /* ---------------------------------------------------------------- */ /** @brief Get the number of computational threads available to the application ** @return number of threads. ** ** This function wraps the OpenMP function @c ** omp_get_thread_limit(). If VLFeat was compiled without OpenMP ** support, this function returns 1. If VLFeat was compiled with ** OpenMP prior to version 3.0 (2008/05), it returns 0. ** ** @sa @ref threads-parallel **/ vl_size vl_get_thread_limit (void) { #if defined(_OPENMP) #if _OPENMP >= 200805 /* OpenMP version >= 3.0 */ return omp_get_thread_limit() ; #else return 0 ; #endif #else return 1 ; #endif } /** @brief Get the maximum number of computational threads used by VLFeat. ** @return number of threads. ** ** This function returns the maximum number of thread used by ** VLFeat. VLFeat will try to use this number of computational ** threads and never exceed it. ** ** This is similar to the OpenMP function @c omp_get_max_threads(); ** however, it reads a parameter private to VLFeat which is ** independent of the value used by the OpenMP library. ** ** If VLFeat was compiled without OpenMP support, this function ** returns 1. ** ** @sa vl_set_num_threads(), @ref threads-parallel **/ vl_size vl_get_max_threads (void) { #if defined(_OPENMP) return vl_get_state()->numThreads ; #else return 1 ; #endif } /** @brief Set the maximum number of threads used by VLFeat. ** @param numThreads number of threads to use. ** ** This function sets the maximum number of computational threads ** that will be used by VLFeat. VLFeat may in practice use fewer ** threads (for example because @a numThreads is larger than the ** number of computational cores in the host, or because the number ** of threads exceeds the limit available to the application). ** ** If @c numThreads is set to 0, then VLFeat sets the number of ** threads to the OpenMP current maximum, obtained by calling @c ** omp_get_max_threads(). ** ** This function is similar to @c omp_set_num_threads() but changes a ** parameter internal to VLFeat rather than affecting OpenMP global ** state. ** ** If VLFeat was compiled without, this function does nothing. ** ** @sa vl_get_max_threads(), @ref threads-parallel **/ #if defined(_OPENMP) void vl_set_num_threads (vl_size numThreads) { if (numThreads == 0) { numThreads = omp_get_max_threads() ; } vl_get_state()->numThreads = numThreads ; } #else void vl_set_num_threads (vl_size numThreads VL_UNUSED) { } #endif /* ---------------------------------------------------------------- */ /** @brief Set last VLFeat error ** @param error error code. ** @param errorMessage error message format string. ** @param ... format string arguments. ** @return error code. ** ** The function sets the code and optionally the error message ** of the last encountered error. @a errorMessage is the message ** format. It uses the @c printf convention and is followed by ** the format arguments. The maximum length of the error message is ** given by ::VL_ERR_MSG_LEN (longer messages are truncated). ** ** Passing @c NULL as @a errorMessage ** sets the error message to the empty string. **/ int vl_set_last_error (int error, char const * errorMessage, ...) { VlThreadState * state = vl_get_thread_specific_state() ; va_list args; va_start(args, errorMessage) ; if (errorMessage) { #ifdef VL_COMPILER_LCC vsprintf(state->lastErrorMessage, errorMessage, args) ; #else vsnprintf(state->lastErrorMessage, sizeof(state->lastErrorMessage)/sizeof(char), errorMessage, args) ; #endif } else { state->lastErrorMessage[0] = 0 ; } state->lastError = error ; va_end(args) ; return error ; } /** @brief Get the code of the last error ** @return error code. ** @sa ::vl_get_last_error_message. **/ int vl_get_last_error (void) { return vl_get_thread_specific_state()->lastError ; } /** @brief Get the last error message ** @return pointer to the error message. ** @sa ::vl_get_last_error. **/ char const * vl_get_last_error_message (void) { return vl_get_thread_specific_state()->lastErrorMessage ; } /* ---------------------------------------------------------------- */ /** @brief Set memory allocation functions ** @param malloc_func pointer to @c malloc. ** @param realloc_func pointer to @c realloc. ** @param calloc_func pointer to @c calloc. ** @param free_func pointer to @c free. **/ void vl_set_alloc_func (void *(*malloc_func) (size_t), void *(*realloc_func) (void*, size_t), void *(*calloc_func) (size_t, size_t), void (*free_func) (void*)) { VlState * state ; vl_lock_state () ; state = vl_get_state() ; state->malloc_func = malloc_func ; state->realloc_func = realloc_func ; state->calloc_func = calloc_func ; state->free_func = free_func ; vl_unlock_state () ; } /** @brief Allocate a memory block ** @param n size in bytes of the new block. ** @return pointer to the allocated block. ** ** This function allocates a memory block of the specified size. ** The synopsis is the same as the POSIX @c malloc function. **/ void * vl_malloc (size_t n) { return (vl_get_state()->malloc_func)(n) ; //return (memalign)(32,n) ; } /** @brief Reallocate a memory block ** @param ptr pointer to a memory block previously allocated. ** @param n size in bytes of the new block. ** @return pointer to the new block. ** ** This function reallocates a memory block to change its size. ** The synopsis is the same as the POSIX @c realloc function. **/ void * vl_realloc (void* ptr, size_t n) { return (vl_get_state()->realloc_func)(ptr, n) ; } /** @brief Free and clear a memory block ** @param n number of items to allocate. ** @param size size in bytes of an item. ** @return pointer to the new block. ** ** This function allocates and clears a memory block. ** The synopsis is the same as the POSIX @c calloc function. **/ void * vl_calloc (size_t n, size_t size) { return (vl_get_state()->calloc_func)(n, size) ; } /** @brief Free a memory block ** @param ptr pointer to the memory block. ** ** This function frees a memory block allocated by ::vl_malloc, ** ::vl_calloc, or ::vl_realloc. The synopsis is the same as the POSIX ** @c malloc function. **/ void vl_free (void *ptr) { (vl_get_state()->free_func)(ptr) ; } /* ---------------------------------------------------------------- */ /** @brief Set the printf function ** @param printf_func pointer to a @c printf implementation. ** Set @c print_func to NULL to disable printf. **/ void vl_set_printf_func (printf_func_t printf_func) { vl_get_state()->printf_func = printf_func ? printf_func : do_nothing_printf ; } /** @brief Get the printf function ** @return printf_func pointer to the @c printf implementation. ** @sa ::vl_set_printf_func. **/ printf_func_t vl_get_printf_func (void) { return vl_get_state()->printf_func ; } /* ---------------------------------------------------------------- */ /** @brief Get processor time ** @return processor time in seconds. ** @sa ::vl_tic, ::vl_toc **/ double vl_get_cpu_time () { #ifdef VL_OS_WIN VlThreadState * threadState = vl_get_thread_specific_state() ; LARGE_INTEGER mark ; QueryPerformanceCounter (&mark) ; return (double)mark.QuadPart / (double)threadState->ticFreq.QuadPart ; #else return (double)clock() / (double)CLOCKS_PER_SEC ; #endif } /** @brief Reset processor time reference ** The function resets VLFeat TIC/TOC time reference. There is one ** such reference per thread. ** @sa ::vl_get_cpu_time, ::vl_toc. **/ void vl_tic (void) { VlThreadState * threadState = vl_get_thread_specific_state() ; #ifdef VL_OS_WIN QueryPerformanceCounter (&threadState->ticMark) ; #else threadState->ticMark = clock() ; #endif } /** @brief Get elapsed time since tic ** @return elapsed time in seconds. ** ** The function ** returns the processor time elapsed since ::vl_tic was called last. ** ** @remark In multi-threaded applications, there is an independent ** timer for each execution thread. ** ** @remark On UNIX, this function uses the @c clock() system call. ** On Windows, it uses the @c QueryPerformanceCounter() system call, ** which is more accurate than @c clock() on this platform. **/ double vl_toc (void) { VlThreadState * threadState = vl_get_thread_specific_state() ; #ifdef VL_OS_WIN LARGE_INTEGER tocMark ; QueryPerformanceCounter(&tocMark) ; return (double) (tocMark.QuadPart - threadState->ticMark.QuadPart) / threadState->ticFreq.QuadPart ; #else return (double) (clock() - threadState->ticMark) / CLOCKS_PER_SEC ; #endif } /* ---------------------------------------------------------------- */ /** @brief Get the default random number generator. ** @return random number generator. ** ** The function returns a pointer to the default ** random number generator. ** There is one such generator per thread. **/ VL_EXPORT VlRand * vl_get_rand (void) { return &vl_get_thread_specific_state()->rand ; } /* ---------------------------------------------------------------- */ /* Library construction and destruction routines */ /* --------------------------------------------------------------- */ /** @internal@brief Construct a new thread state object ** @return new state structure. **/ static VlThreadState * vl_thread_specific_state_new (void) { VlThreadState * self ; #if defined(DEBUG) printf("VLFeat DEBUG: thread constructor begins.\n") ; #endif self = malloc(sizeof(VlThreadState)) ; self->lastError = 0 ; self->lastErrorMessage[0] = 0 ; #if defined(VL_OS_WIN) QueryPerformanceFrequency (&self->ticFreq) ; self->ticMark.QuadPart = 0 ; #else self->ticMark = 0 ; #endif vl_rand_init (&self->rand) ; return self ; } /** @internal@brief Delete a thread state structure ** @param self thread state object. **/ static void vl_thread_specific_state_delete (VlThreadState * self) { #if defined(DEBUG) printf("VLFeat DEBUG: thread destructor begins.\n") ; #endif free (self) ; } /* ---------------------------------------------------------------- */ /* Library constructor and destructor */ /* ---------------------------------------------------------------- */ /** @internal @brief Initialize VLFeat state */ void vl_constructor (void) { VlState * state ; #if defined(DEBUG) printf("VLFeat DEBUG: constructor begins.\n") ; #endif state = vl_get_state() ; #if ! defined(VL_DISABLE_THREADS) #if defined(DEBUG) printf("VLFeat DEBUG: constructing thread specific state.\n") ; #endif #if defined(VL_THREADS_POSIX) { typedef void (*destructorType)(void * ); pthread_key_create (&state->threadKey, (destructorType) vl_thread_specific_state_delete) ; pthread_mutex_init (&state->mutex, NULL) ; pthread_cond_init (&state->mutexCondition, NULL) ; } #elif defined(VL_THREADS_WIN) InitializeCriticalSection (&state->mutex) ; state->tlsIndex = TlsAlloc () ; #endif #else /* threading support disabled */ #if defined(DEBUG) printf("VLFeat DEBUG: constructing the generic thread state instance (threading support disabled).\n") ; #endif vl_get_state()->threadState = vl_thread_specific_state_new() ; #endif state->malloc_func = malloc ; state->realloc_func = realloc ; state->calloc_func = calloc ; state->free_func = free ; state->printf_func = printf ; /* on x86 platforms read the CPUID register */ #if defined(VL_ARCH_IX86) || defined(VL_ARCH_X64) || defined(VL_ARCH_IA64) _vl_x86cpu_info_init (&state->cpuInfo) ; #endif /* get the number of CPUs */ #if defined(VL_OS_WIN) { SYSTEM_INFO info; GetSystemInfo (&info) ; state->numCPUs = info.dwNumberOfProcessors ; } #elif defined(_SC_NPROCESSORS_ONLN) state->numCPUs = sysconf(_SC_NPROCESSORS_ONLN) ; #else state->numCPUs = 1 ; #endif state->simdEnabled = VL_TRUE ; /* get the number of (OpenMP) threads used by the library */ #if defined(_OPENMP) state->numThreads = omp_get_max_threads() ; #else state->numThreads = 1 ; #endif #if defined(DEBUG) printf("VLFeat DEBUG: constructor ends.\n") ; #endif } /** @internal @brief Destruct VLFeat */ void vl_destructor () { VlState * state ; #if defined(DEBUG) printf("VLFeat DEBUG: destructor begins.\n") ; #endif state = vl_get_state() ; #if ! defined(VL_DISABLE_THREADS) #if defined(DEBUG) printf("VLFeat DEBUG: destroying a thread specific state instance.\n") ; #endif #if defined(VL_THREADS_POSIX) { /* Delete the thread state of this thread as the destructor is not called by pthread_key_delete or after the key is deleted. When the library is unloaded, this thread should also be the last one using the library, so this is fine. */ VlThreadState * threadState = pthread_getspecific(state->threadKey) ; if (threadState) { vl_thread_specific_state_delete (threadState) ; pthread_setspecific(state->threadKey, NULL) ; } } pthread_cond_destroy (&state->mutexCondition) ; pthread_mutex_destroy (&state->mutex) ; pthread_key_delete (state->threadKey) ; #elif defined(VL_THREADS_WIN) { /* Delete the thread state of this thread as the destructor is not called by pthread_key_delete or after the key is deleted. When the library is unloaded, this thread should also be the last one using the library, so this is fine. */ VlThreadState * threadState = TlsGetValue(state->tlsIndex) ; if (threadState) { vl_thread_specific_state_delete (threadState) ; TlsSetValue(state->tlsIndex, NULL) ; } } TlsFree (state->tlsIndex) ; DeleteCriticalSection (&state->mutex) ; #endif #else #if defined(DEBUG) printf("VLFeat DEBUG: destroying the generic thread state instance (threading support disabled).\n") ; #endif vl_thread_specific_state_delete(vl_get_state()->threadState) ; #endif #if defined(DEBUG) printf("VLFeat DEBUG: destructor ends.\n") ; #endif } /* ---------------------------------------------------------------- */ /* Cross-platform call to constructor/destructor */ /* ---------------------------------------------------------------- */ #ifdef __cplusplus #define INITIALIZER(f) \ static void f(void); \ struct f##_t_ { f##_t_(void) { f(); } }; static f##_t_ f##_; \ static void f(void) #elif defined(_MSC_VER) #pragma section(".CRT$XCU",read) #define INITIALIZER2_(f,p) \ static void f(void); \ __declspec(allocate(".CRT$XCU")) void (*f##_)(void) = f; \ __pragma(comment(linker,"/include:" p #f "_")) \ static void f(void) #ifdef _WIN64 #define INITIALIZER(f) INITIALIZER2_(f,"") #else #define INITIALIZER(f) INITIALIZER2_(f,"_") #endif #else #define INITIALIZER(f) \ static void f(void) __attribute__((constructor)); \ static void f(void) #endif INITIALIZER(vl_initialize) { vl_constructor(); atexit(vl_destructor); } colmap-3.9.1/src/thirdparty/VLFeat/generic.h000066400000000000000000000151111454702036400206720ustar00rootroot00000000000000/** @file generic.h ** @brief Generic (@ref generic) ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 Andrea Vedaldi. Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_GENERIC_H #define VL_GENERIC_H #include "host.h" #include "random.h" #include #include #include #include /** @brief Library version string */ #define VL_VERSION_STRING "0.9.20" /** @brief Maximum length (in characters) of an error message */ #define VL_ERR_MSG_LEN 1024 /** @name Type identifiers for atomic data types ** @{ */ #define VL_TYPE_FLOAT 1 /**< @c float type */ #define VL_TYPE_DOUBLE 2 /**< @c double type */ #define VL_TYPE_INT8 3 /**< @c ::vl_int8 type */ #define VL_TYPE_UINT8 4 /**< @c ::vl_uint8 type */ #define VL_TYPE_INT16 5 /**< @c ::vl_int16 type */ #define VL_TYPE_UINT16 6 /**< @c ::vl_uint16 type */ #define VL_TYPE_INT32 7 /**< @c ::vl_int32 type */ #define VL_TYPE_UINT32 8 /**< @c ::vl_uint32 type */ #define VL_TYPE_INT64 9 /**< @c ::vl_int64 type */ #define VL_TYPE_UINT64 10 /**< @c ::vl_uint64 type */ typedef vl_uint32 vl_type ; /** @brief Get the name of a data type. ** @param type data type. ** @return data name of the data type. ** ** @c type is one of ::VL_TYPE_FLOAT, ::VL_TYPE_DOUBLE, ** ::VL_TYPE_INT8, ::VL_TYPE_INT16, ::VL_TYPE_INT32, ::VL_TYPE_INT64, ** ::VL_TYPE_UINT8, ::VL_TYPE_UINT16, ::VL_TYPE_UINT32, ::VL_TYPE_UINT64. **/ void vl_constructor(); void vl_destructor(); VL_INLINE char const * vl_get_type_name (vl_type type) { switch (type) { case VL_TYPE_FLOAT : return "float" ; case VL_TYPE_DOUBLE : return "double" ; case VL_TYPE_INT8 : return "int8" ; case VL_TYPE_INT16 : return "int16" ; case VL_TYPE_INT32 : return "int32" ; case VL_TYPE_INT64 : return "int64" ; case VL_TYPE_UINT8 : return "int8" ; case VL_TYPE_UINT16 : return "int16" ; case VL_TYPE_UINT32 : return "int32" ; case VL_TYPE_UINT64 : return "int64" ; default: return NULL ; } } /** @brief Get data type size. ** @param type data type. ** @return size (in byte) ** ** @c type is one of ::VL_TYPE_FLOAT, ::VL_TYPE_DOUBLE, ** ::VL_TYPE_INT8, ::VL_TYPE_INT16, ::VL_TYPE_INT32, ::VL_TYPE_INT64, ** ::VL_TYPE_UINT8, ::VL_TYPE_UINT16, ::VL_TYPE_UINT32, ::VL_TYPE_UINT64. **/ VL_INLINE vl_size vl_get_type_size (vl_type type) { vl_size dataSize = 0 ; switch (type) { case VL_TYPE_DOUBLE : dataSize = sizeof(double) ; break ; case VL_TYPE_FLOAT : dataSize = sizeof(float) ; break ; case VL_TYPE_INT64 : case VL_TYPE_UINT64 : dataSize = sizeof(vl_int64) ; break ; case VL_TYPE_INT32 : case VL_TYPE_UINT32 : dataSize = sizeof(vl_int32) ; break ; case VL_TYPE_INT16 : case VL_TYPE_UINT16 : dataSize = sizeof(vl_int16) ; break ; case VL_TYPE_INT8 : case VL_TYPE_UINT8 : dataSize = sizeof(vl_int8) ; break ; default: abort() ; } return dataSize ; } /** @} */ VL_EXPORT char const * vl_get_version_string (void) ; VL_EXPORT char * vl_configuration_to_string_copy (void) ; VL_EXPORT void vl_set_simd_enabled (vl_bool x) ; VL_EXPORT vl_bool vl_get_simd_enabled (void) ; VL_EXPORT vl_bool vl_cpu_has_avx (void) ; VL_EXPORT vl_bool vl_cpu_has_sse3 (void) ; VL_EXPORT vl_bool vl_cpu_has_sse2 (void) ; VL_EXPORT vl_size vl_get_num_cpus (void) ; VL_EXPORT VlRand * vl_get_rand (void) ; /** @name Multi-thread computations ** @{ */ VL_EXPORT vl_size vl_get_max_threads (void) ; VL_EXPORT void vl_set_num_threads (vl_size n) ; VL_EXPORT vl_size vl_get_thread_limit (void) ; /** @} (*/ /** ------------------------------------------------------------------ ** @name Error handling ** @{ */ #define VL_ERR_OK 0 /**< No error */ #define VL_ERR_OVERFLOW 1 /**< Buffer overflow error */ #define VL_ERR_ALLOC 2 /**< Resource allocation error */ #define VL_ERR_BAD_ARG 3 /**< Bad argument or illegal data error */ #define VL_ERR_IO 4 /**< Input/output error */ #define VL_ERR_EOF 5 /**< End-of-file or end-of-sequence error */ #define VL_ERR_NO_MORE 5 /**< End-of-sequence @deprecated */ VL_EXPORT int vl_get_last_error (void) ; VL_EXPORT char const * vl_get_last_error_message (void) ; VL_EXPORT int vl_set_last_error (int error, char const * errorMessage, ...) ; /** @} */ /** ------------------------------------------------------------------ ** @name Memory allocation ** @{ */ VL_EXPORT void vl_set_alloc_func (void *(*malloc_func) (size_t), void *(*realloc_func) (void*,size_t), void *(*calloc_func) (size_t, size_t), void (*free_func) (void*)) ; VL_EXPORT void *vl_malloc (size_t n) ; VL_EXPORT void *vl_realloc (void *ptr, size_t n) ; VL_EXPORT void *vl_calloc (size_t n, size_t size) ; VL_EXPORT void *vl_memalign (size_t n, size_t size) ; VL_EXPORT void vl_free (void* ptr) ; /** @} */ /** ------------------------------------------------------------------ ** @name Logging ** @{ */ /** @brief Customizable printf function pointer type */ typedef int(*printf_func_t) (char const *format, ...) ; VL_EXPORT void vl_set_printf_func (printf_func_t printf_func) ; VL_EXPORT printf_func_t vl_get_printf_func (void) ; /** @def VL_PRINTF ** @brief Call user-customizable @c printf function ** ** The function calls the user customizable @c printf. **/ /** @def VL_PRINT ** @brief Same as ::VL_PRINTF (legacy code) **/ #define VL_PRINTF (*vl_get_printf_func()) #define VL_PRINT (*vl_get_printf_func()) /** @} */ /** ------------------------------------------------------------------ ** @name Common operations ** @{ */ /** @brief Compute the minimum between two values ** @param x value ** @param y value ** @return the minimum of @a x and @a y. **/ #define VL_MIN(x,y) (((x)<(y))?(x):(y)) /** @brief Compute the maximum between two values ** @param x value. ** @param y value. ** @return the maximum of @a x and @a y. **/ #define VL_MAX(x,y) (((x)>(y))?(x):(y)) /** @brief Signed left shift operation ** @param x value. ** @param n number of shift positions. ** @return @c x << n . ** The macro is equivalent to the builtin @c << operator, but it ** supports negative shifts too. **/ #define VL_SHIFT_LEFT(x,n) (((n)>=0)?((x)<<(n)):((x)>>-(n))) /* @} */ /** ------------------------------------------------------------------ ** @name Measuring time ** @{ **/ VL_EXPORT void vl_tic (void) ; VL_EXPORT double vl_toc (void) ; VL_EXPORT double vl_get_cpu_time (void) ; /** @} */ /* VL_GENERIC_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/getopt_long.c000077500000000000000000000217341454702036400216050ustar00rootroot00000000000000/** @file getopt_long.c ** @brief getopt_long - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file getopt_long.h @brief getopt_long @author Andrea Vedaldi This is a drop-in replacament of GNU getopt_long meant to be used on platforms that do not support such functionality. **/ #include #include #include #include "generic.h" #include "getopt_long.h" int opterr = 1 ; int optind = 1 ; int optopt ; char * optarg ; int optreset ; #define BADCH '?' #define BADARG ':' #define EEND -1 #define EMSG "" /** @brief Parse long options (BSD style) ** @param argc number of arguments. ** @param argv pointer to the vector of arguments. ** @param optstring list of abbreviated options ** @param longopts list of long options. ** @param longindex index of current option in @a longopts. ** @return the code of the next option. ** ** This function extract long and short options from the argument ** list @a argv of @a argc entries. ** ** A short options sequence is introduced by a single dash character ** @c -. Each short option is described by a single character in the ** string @a optstring, possibly followed by a @c : character to ** denote a (mandatory) argument of the short option. A short option ** with an argument cannot appear in the middle of a short option ** sequence, but only at the end. ** ** A long option is introduced by a double dash @c --. Each long ** option is described by an instance of the ::option structure in ** the @a longopts table (the last entry must be filled with zeroes ** to denote the end). ** ** Illegal options and missing arguments cause the function to skip ** the option and return '?'. If ::opterr is @c true (default), the ** function prints an error message to @a stderr. Finally, if @a ** optstring has a leading @c :, then error messages are suppressed ** and a missing argument causes @a : to be returned. ** ** @remark The function is currently not thread safe. **/ VL_EXPORT int getopt_long(int argc, char *const argv[], const char *optstring, const struct option * longopts, int *longindex) { static char *place = EMSG; /* option letter processing */ static int optbegin = 0 ; static int optend = 0 ; char *oli; /* option letter list index */ int has_colon = 0 ; int ret_val = 0 ; /* A semicolon at the beginning of optstring has a special meaning. If we find one, we annote and remove it. */ has_colon = optstring && optstring[0] == ':' ; if (has_colon) ++ optstring ; /* Here we are either processing a short option sequence or we start processing a new option. This is indicated by optreset. */ if (optreset || *place == '\0') { /* --------------------------------------------------------------- * Look for next short/long option * ------------------------------------------------------------ */ optreset = 0 ; /* no more arguments ? */ if (optind >= argc) { place = EMSG ; return -1 ; } /* next argument that may hold an option */ optbegin = optind ; /* --------------------------------------------------------------- * Look for an option to parse * ------------------------------------------------------------ */ parse_option_at_optbegin : /* place points to the candidate option */ place = argv [optbegin] ; /* an option is introduced by '-' */ if (place [0] != '-') { /* this argument is not an option: try next argument */ ++ optbegin ; if (optbegin >= argc) { /* no more arguments to look for options */ place = EMSG ; return -1 ; } goto parse_option_at_optbegin ; } /* consume leading `-' */ ++ place ; /* assume the option is composed of one argument only */ optend = optbegin + 1 ; /* assume no argument */ optarg = 0 ; /* --------------------------------------------------------------- * option `--' * ------------------------------------------------------------ */ /* this special option (void long option) ends the option processing */ if (place[0] && place[0] == '-' && place[1] == '\0') { optind = optend ; place = EMSG ; ret_val = -1 ; goto done_option ; } /* --------------------------------------------------------------- * long option * ------------------------------------------------------------ */ if (place[0] && place[0] == '-' && place[1] ) { size_t namelen ; int i ; /* consume second `-' */ ++ place ; /* count characters before `=' */ namelen = strcspn(place, "=") ; /* scan longopts for this option */ for (i = 0 ; longopts[i].name != NULL ; ++ i) { if (strlen ( longopts[i].name) == namelen && strncmp (place, longopts[i].name, namelen) == 0 ) { /* save back long option index */ if (longindex) *longindex = i ; /* process long option argument */ if (longopts[i].has_arg == required_argument || longopts[i].has_arg == optional_argument) { /* --option=value style */ if (place[namelen] == '=') { optarg = place + namelen + 1 ; } /* --option value style (only required_argument) */ else if (longopts[i].has_arg == required_argument) { /* missing argument ? */ if (optbegin >= argc - 1) { if (! has_colon && opterr) fprintf(stderr, "%s: option requires an argument -- %s\n", argv[0], place); place = EMSG ; ret_val = has_colon ? BADARG : BADCH ; goto done_option ; } optarg = argv [optend] ; ++ optend ; } } /* determine return value */ if (longopts[i].flag == NULL) { ret_val = longopts[i].val ; } else { *longopts[i].flag = longopts[i].val; ret_val = 0 ; } /* mark sequence closed */ place = EMSG ; goto done_option ; } /* if match */ } /* scan longoptions */ /* no matching option found */ if (! has_colon && opterr) fprintf(stderr, "%s: illegal option -- %s\n", argv[0], place) ; place = EMSG ; ret_val = BADCH ; goto done_option ; } } /* end new option */ /* ----------------------------------------------------------------- * Finish short option sequence * -------------------------------------------------------------- */ optopt = (int) *place++ ; /* search charcater in option list */ oli = strchr(optstring, optopt); /* short option not found */ if (!oli) { if (! has_colon && opterr) fprintf(stderr, "%s: illegal option -- %c\n", argv[0], optopt); if (*place) { /* more short options in the list */ return BADCH ; } else { /* error occured as last option in the list */ place = EMSG ; ret_val = BADCH ; goto done_option ; } } /* end short option not found */ if (oli[1] != ':') { /* short option with no argument */ if (*place) { /* more short options in the list */ return optopt ; } else { /* last option in the list */ place = EMSG ; ret_val = optopt ; goto done_option ; } } else { /* short option with argument */ /* -ovalue style */ if (*place) { optarg = place ; place = EMSG ; ret_val = optopt ; goto done_option ; } /* -o value style: missing argument */ else if (optbegin >= argc - 1) { if (! has_colon && opterr) fprintf(stderr, "%s: option requires an argument -- %c\n", argv[0], optopt); place = EMSG ; ret_val = has_colon ? BADARG : BADCH ; goto done_option ; } /* -o value style: process argument */ optarg = argv [optend] ; ++ optend ; place = EMSG ; ret_val = optopt ; goto done_option ; } /* short with argument */ done_option : { int pos = optend - optbegin ; /* n of circular shifts */ int c = pos ; while (c --) { int i ; char *tmp = argv [optend - 1] ; for (i = optend - 1 ; i > optind ; -- i) { ((char**)argv) [i] = argv [i-1] ; } ((char**)argv) [optind] = tmp ; } optind += pos ; } return ret_val ; } colmap-3.9.1/src/thirdparty/VLFeat/getopt_long.h000066400000000000000000000027071454702036400216060ustar00rootroot00000000000000/** @file getopt_long.h ** @brief getopt_long ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_GETOPT_LONG_H #define VL_GETOPT_LONG_H #include "generic.h" VL_EXPORT int opterr ; /**< code of the last error occured while parsing an option */ VL_EXPORT int optind ; /**< index of the next option to process in @c argv */ VL_EXPORT int optopt ; /**< current option */ VL_EXPORT char * optarg ; /**< argument of the current option */ VL_EXPORT int optreset ; /**< reset flag */ /** @brief ::getopt_long option */ struct option { const char *name ; /**< option long name */ int has_arg ; /**< flag indicating whether the option has no, required or optional argument */ int *flag ; /**< pointer to a variable to set (if @c NULL, the value is returned instead) */ int val ; /**< value to set or to return */ } ; #define no_argument 0 /**< ::option with no argument */ #define required_argument 1 /**< ::option with required argument */ #define optional_argument 2 /**< ::option with optional argument */ VL_EXPORT int getopt_long(int argc, char * const argv[], const char * optstring, const struct option * longopts, int * longindex); /* VL_GETOPT_LONG_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/gmm.c000077500000000000000000001504471454702036400200500ustar00rootroot00000000000000/** @file gmm.c ** @brief Gaussian Mixture Models - Implementation ** @author David Novotny ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 David Novotny and Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page gmm Gaussian Mixture Models (GMM) @author David Novotny @author Andrea Vedaldi @tableofcontents @ref gmm.h is an implementation of *Gaussian Mixture Models* (GMMs). The main functionality provided by this module is learning GMMs from data by maximum likelihood. Model optimization uses the Expectation Maximization (EM) algorithm @cite{dempster77maximum}. The implementation supports @c float or @c double data types, is parallelized, and is tuned to work reliably and effectively on datasets of visual features. Stability is obtained in part by regularizing and restricting the parameters of the GMM. @ref gmm-starting demonstreates how to use the C API to compute the FV representation of an image. For further details refer to: - @subpage gmm-fundamentals @section gmm-starting Getting started In order to use @ref gmm.h to learn a GMM from training data, create a new ::VlGMM object instance, set the parameters as desired, and run the training code. The following example learns @c numClusters Gaussian components from @c numData vectors of dimension @c dimension and storage class @c float using at most 100 EM iterations: @code float * means ; float * covariances ; float * priors ; float * posteriors ; double loglikelihood ; // create a new instance of a GMM object for float data gmm = vl_gmm_new (VL_TYPE_FLOAT, dimension, numClusters) ; // set the maximum number of EM iterations to 100 vl_gmm_set_max_num_iterations (gmm, 100) ; // set the initialization to random selection vl_gmm_set_initialization (gmm,VlGMMRand); // cluster the data, i.e. learn the GMM vl_gmm_cluster (gmm, data, numData); // get the means, covariances, and priors of the GMM means = vl_gmm_get_means(gmm); covariances = vl_gmm_get_covariances(gmm); priors = vl_gmm_get_priors(gmm); // get loglikelihood of the estimated GMM loglikelihood = vl_gmm_get_loglikelihood(gmm) ; // get the soft assignments of the data points to each cluster posteriors = vl_gmm_get_posteriors(gmm) ; @endcode @note ::VlGMM assumes that the covariance matrices of the GMM are diagonal. This reduces significantly the number of parameters to learn and is usually an acceptable compromise in vision applications. If the data is significantly correlated, it can be beneficial to de-correlate it by PCA rotation or projection in pre-processing. ::vl_gmm_get_loglikelihood is used to get the final loglikelihood of the estimated mixture, ::vl_gmm_get_means and ::vl_gmm_get_covariances to obtain the means and the diagonals of the covariance matrices of the estimated Gaussian modes, and ::vl_gmm_get_posteriors to get the posterior probabilities that a given point is associated to each of the modes (soft assignments). The learning algorithm, which uses EM, finds a local optimum of the objective function. Therefore the initialization is crucial in obtaining a good model, measured in term of the final loglikelihood. ::VlGMM supports a few methods (use ::vl_gmm_set_initialization to choose one) as follows: Method | ::VlGMMInitialization enumeration | Description ----------------------|-----------------------------------------|----------------------------------------------- Random initialization | ::VlGMMRand | Random initialization of the mixture parameters KMeans | ::VlGMMKMeans | Initialization of the mixture parameters using ::VlKMeans Custom | ::VlGMMCustom | User specified initialization Note that in the case of ::VlGMMKMeans initialization, an object of type ::VlKMeans object must be created and passed to the ::VlGMM instance (see @ref kmeans to see how to correctly set up this object). When a user wants to use the ::VlGMMCustom method, the initial means, covariances and priors have to be specified using the ::vl_gmm_set_means, ::vl_gmm_set_covariances and ::vl_gmm_set_priors methods. **/ /** @page gmm-fundamentals GMM fundamentals @tableofcontents A *Gaussian Mixture Model* (GMM) is a mixture of $K$ multivariate Gaussian distributions. In order to sample from a GMM, one samples first the component index $k \in \{1,\dots,K\}$ with *prior probability* $\pi_k$, and then samples the vector $\bx \in \mathbb{R}^d$ from the $k$-th Gaussian distribution $p(\bx|\mu_k,\Sigma_k)$. Here $\mu_k$ and $\Sigma_k$ are respectively the *mean* and *covariance* of the distribution. The GMM is completely specified by the parameters $\Theta=\{\pi_k,\mu_k,\Sigma_k; k = 1,\dots,K\}$ The density $p(\bx|\Theta)$ induced on the training data is obtained by marginalizing the component selector $k$, obtaining \[ p(\bx|\Theta) = \sum_{k=1}^{K} \pi_k p( \bx_i |\mu_k,\Sigma_k), \qquad p( \bx |\mu_k,\Sigma_k) = \frac{1}{\sqrt{(2\pi)^d\det\Sigma_k}} \exp\left[ -\frac{1}{2} (\bx-\mu_k)^\top\Sigma_k^{-1}(\bx-\mu_k) \right]. \] Learning a GMM to fit a dataset $X=(\bx_1, \dots, \bx_n)$ is usually done by maximizing the log-likelihood of the data: @f[ \ell(\Theta;X) = E_{\bx\sim\hat p} [ \log p(\bx|\Theta) ] = \frac{1}{n}\sum_{i=1}^{n} \log \sum_{k=1}^{K} \pi_k p(\bx_i|\mu_k, \Sigma_k) @f] where $\hat p$ is the empirical distribution of the data. An algorithm to solve this problem is introduced next. @section gmm-em Learning a GMM by expectation maximization The direct maximization of the log-likelihood function of a GMM is difficult due to the fact that the assignments of points to Gaussian mode is not observable and, as such, must be treated as a latent variable. Usually, GMMs are learned by using the *Expectation Maximization* (EM) algorithm @cite{dempster77maximum}. Consider in general the problem of estimating to the maximum likelihood a distribution $p(x|\Theta) = \int p(x,h|\Theta)\,dh$, where $x$ is a measurement, $h$ is a *latent variable*, and $\Theta$ are the model parameters. By introducing an auxiliary distribution $q(h|x)$ on the latent variable, one can use Jensen inequality to obtain the following lower bound on the log-likelihood: @f{align*} \ell(\Theta;X) = E_{x\sim\hat p} \log p(x|\Theta) &= E_{x\sim\hat p} \log \int p(x,h|\Theta) \,dh \\ &= E_{x\sim\hat p} \log \int \frac{p(x,h|\Theta)}{q(h|x)} q(h|x)\,dh \\ &\geq E_{x\sim\hat p} \int q(h) \log \frac{p(x,h|\Theta)}{q(h|x)}\,dh \\ &= E_{(x,q) \sim q(h|x) \hat p(x)} \log p(x,h|\Theta) - E_{(x,q) \sim q(h|x) \hat p(x)} \log q(h|x) @f} The first term of the last expression is the log-likelihood of the model where both the $x$ and $h$ are observed and joinlty distributed as $q(x|h)\hat p(x)$; the second term is the a average entropy of the latent variable, which does not depend on $\Theta$. This lower bound is maximized and becomes tight by setting $q(h|x) = p(h|x,\Theta)$ to be the posterior distribution on the latent variable $h$ (given the current estimate of the parameters $\Theta$). In fact: \[ E_{x \sim \hat p} \log p(x|\Theta) = E_{(x,h) \sim p(h|x,\Theta) \hat p(x)}\left[ \log \frac{p(x,h|\Theta)}{p(h|x,\Theta)} \right] = E_{(x,h) \sim p(h|x,\Theta) \hat p(x)} [ \log p(x|\Theta) ] = \ell(\Theta;X). \] EM alternates between updating the latent variable auxiliary distribution $q(h|x) = p(h|x,\Theta_t)$ (*expectation step*) given the current estimate of the parameters $\Theta_t$, and then updating the model parameters $\Theta_{t+1}$ by maximizing the log-likelihood lower bound derived (*maximization step*). The simplification is that in the maximization step both $x$ and $h$ are now ``observed'' quantities. This procedure converges to a local optimum of the model log-likelihood. @subsection gmm-expectation-step Expectation step In the case of a GMM, the latent variables are the point-to-cluster assignments $k_i, i=1,\dots,n$, one for each of $n$ data points. The auxiliary distribution $q(k_i|\bx_i) = q_{ik}$ is a matrix with $n \times K$ entries. Each row $q_{i,:}$ can be thought of as a vector of soft assignments of the data points $\bx_i$ to each of the Gaussian modes. Setting $q_{ik} = p(k_i | \bx_i, \Theta)$ yields \[ q_{ik} = \frac {\pi_k p(\bx_i|\mu_k,\Sigma_k)} {\sum_{l=1}^K \pi_l p(\bx_i|\mu_l,\Sigma_l)} \] where the Gaussian density $p(\bx_i|\mu_k,\Sigma_k)$ was given above. One important point to keep in mind when these probabilities are computed is the fact that the Gaussian densities may attain very low values and underflow in a vanilla implementation. Furthermore, VLFeat GMM implementation restricts the covariance matrices to be diagonal. In this case, the computation of the determinant of $\Sigma_k$ reduces to computing the trace of the matrix and the inversion of $\Sigma_k$ could be obtained by inverting the elements on the diagonal of the covariance matrix. @subsection gmm-maximization-step Maximization step The M step estimates the parameters of the Gaussian mixture components and the prior probabilities $\pi_k$ given the auxiliary distribution on the point-to-cluster assignments computed in the E step. Since all the variables are now ``observed'', the estimate is quite simple. For example, the mean $\mu_k$ of a Gaussian mode is obtained as the mean of the data points assigned to it (accounting for the strength of the soft assignments). The other quantities are obtained in a similar manner, yielding to: @f{align*} \mu_k &= { { \sum_{i=1}^n q_{ik} \bx_{i} } \over { \sum_{i=1}^n q_{ik} } }, \\ \Sigma_k &= { { \sum_{i=1}^n { q_{ik} (\bx_{i} - \mu_{k}) {(\bx_{i} - \mu_{k})}^T } } \over { \sum_{i=1}^n q_{ik} } }, \\ \pi_k &= { \sum_{i=1}^n { q_{ik} } \over { \sum_{i=1}^n \sum_{l=1}^K q_{il} } }. @f} @section gmm-fundamentals-init Initialization algorithms The EM algorithm is a local optimization method. As such, the quality of the solution strongly depends on the quality of the initial values of the parameters (i.e. of the locations and shapes of the Gaussian modes). @ref gmm.h supports the following cluster initialization algorithms: - Random data points. (::vl_gmm_init_with_rand_data) This method sets the means of the modes by sampling at random a corresponding number of data points, sets the covariance matrices of all the modes are to the covariance of the entire dataset, and sets the prior probabilities of the Gaussian modes to be uniform. This initialization method is the fastest, simplest, as well as the one most likely to end in a bad local minimum. - KMeans initialization (::vl_gmm_init_with_kmeans) This method uses KMeans to pre-cluster the points. It then sets the means and covariances of the Gaussian distributions the sample means and covariances of each KMeans cluster. It also sets the prior probabilities to be proportional to the mass of each cluster. In order to use this initialization method, a user can specify an instance of ::VlKMeans by using the function ::vl_gmm_set_kmeans_init_object, or let ::VlGMM create one automatically. Alternatively, one can manually specify a starting point (::vl_gmm_set_priors, ::vl_gmm_set_means, ::vl_gmm_set_covariances). **/ #include "gmm.h" #include #include #include #ifdef _OPENMP #include #endif #ifndef VL_DISABLE_SSE2 #include "mathop_sse2.h" #endif #ifndef VL_DISABLE_AVX #include "mathop_avx.h" #endif /* ---------------------------------------------------------------- */ #ifndef VL_GMM_INSTANTIATING /* ---------------------------------------------------------------- */ #define VL_GMM_MIN_VARIANCE 1e-6 #define VL_GMM_MIN_POSTERIOR 1e-2 #define VL_GMM_MIN_PRIOR 1e-6 struct _VlGMM { vl_type dataType ; /**< Data type. */ vl_size dimension ; /**< Data dimensionality. */ vl_size numClusters ; /**< Number of clusters */ vl_size numData ; /**< Number of last time clustered data points. */ vl_size maxNumIterations ; /**< Maximum number of refinement iterations. */ vl_size numRepetitions ; /**< Number of clustering repetitions. */ int verbosity ; /**< Verbosity level. */ void * means; /**< Means of Gaussian modes. */ void * covariances; /**< Diagonals of covariance matrices of Gaussian modes. */ void * priors; /**< Weights of Gaussian modes. */ void * posteriors; /**< Probabilities of correspondences of points to clusters. */ double * sigmaLowBound ; /**< Lower bound on the diagonal covariance values. */ VlGMMInitialization initialization; /**< Initialization option */ VlKMeans * kmeansInit; /**< Kmeans object for initialization of gaussians */ double LL ; /**< Current solution loglikelihood */ vl_bool kmeansInitIsOwner; /**< Indicates whether a user provided the kmeans initialization object */ } ; /* ---------------------------------------------------------------- */ /* Life-cycle */ /* ---------------------------------------------------------------- */ static void _vl_gmm_prepare_for_data (VlGMM* self, vl_size numData) { if (self->numData < numData) { vl_free(self->posteriors) ; self->posteriors = vl_malloc(vl_get_type_size(self->dataType) * numData * self->numClusters) ; } self->numData = numData ; } /** @brief Create a new GMM object ** @param dataType type of data (::VL_TYPE_FLOAT or ::VL_TYPE_DOUBLE) ** @param dimension dimension of the data. ** @param numComponents number of Gaussian mixture components. ** @return new GMM object instance. **/ VlGMM * vl_gmm_new (vl_type dataType, vl_size dimension, vl_size numComponents) { vl_index i ; vl_size size = vl_get_type_size(dataType) ; VlGMM * self = vl_calloc(1, sizeof(VlGMM)) ; self->dataType = dataType; self->numClusters = numComponents ; self->numData = 0; self->dimension = dimension ; self->initialization = VlGMMRand; self->verbosity = 0 ; self->maxNumIterations = 50; self->numRepetitions = 1; self->sigmaLowBound = NULL ; self->priors = NULL ; self->covariances = NULL ; self->means = NULL ; self->posteriors = NULL ; self->kmeansInit = NULL ; self->kmeansInitIsOwner = VL_FALSE; self->priors = vl_calloc (numComponents, size) ; self->means = vl_calloc (numComponents * dimension, size) ; self->covariances = vl_calloc (numComponents * dimension, size) ; self->sigmaLowBound = vl_calloc (dimension, sizeof(double)) ; for (i = 0 ; i < (unsigned)self->dimension ; ++i) { self->sigmaLowBound[i] = 1e-4 ; } return self ; } /** @brief Reset state ** @param self object. ** ** The function reset the state of the GMM object. It deletes ** any stored posterior and other internal state variables. **/ void vl_gmm_reset (VlGMM * self) { if (self->posteriors) { vl_free(self->posteriors) ; self->posteriors = NULL ; self->numData = 0 ; } if (self->kmeansInit && self->kmeansInitIsOwner) { vl_kmeans_delete(self->kmeansInit) ; self->kmeansInit = NULL ; self->kmeansInitIsOwner = VL_FALSE ; } } /** @brief Deletes a GMM object ** @param self GMM object instance. ** ** The function deletes the GMM object instance created ** by ::vl_gmm_new. **/ void vl_gmm_delete (VlGMM * self) { if(self->means) vl_free(self->means); if(self->covariances) vl_free(self->covariances); if(self->priors) vl_free(self->priors); if(self->posteriors) vl_free(self->posteriors); if(self->kmeansInit && self->kmeansInitIsOwner) { vl_kmeans_delete(self->kmeansInit); } vl_free(self); } /* ---------------------------------------------------------------- */ /* Getters and setters */ /* ---------------------------------------------------------------- */ /** @brief Get data type ** @param self object ** @return data type. **/ vl_type vl_gmm_get_data_type (VlGMM const * self) { return self->dataType ; } /** @brief Get the number of clusters ** @param self object ** @return number of clusters. **/ vl_size vl_gmm_get_num_clusters (VlGMM const * self) { return self->numClusters ; } /** @brief Get the number of data points ** @param self object ** @return number of data points. **/ vl_size vl_gmm_get_num_data (VlGMM const * self) { return self->numData ; } /** @brief Get the log likelihood of the current mixture ** @param self object ** @return loglikelihood. **/ double vl_gmm_get_loglikelihood (VlGMM const * self) { return self->LL ; } /** @brief Get verbosity level ** @param self object ** @return verbosity level. **/ int vl_gmm_get_verbosity (VlGMM const * self) { return self->verbosity ; } /** @brief Set verbosity level ** @param self object ** @param verbosity verbosity level. **/ void vl_gmm_set_verbosity (VlGMM * self, int verbosity) { self->verbosity = verbosity ; } /** @brief Get means ** @param self object ** @return cluster means. **/ void const * vl_gmm_get_means (VlGMM const * self) { return self->means ; } /** @brief Get covariances ** @param self object ** @return diagonals of cluster covariance matrices. **/ void const * vl_gmm_get_covariances (VlGMM const * self) { return self->covariances ; } /** @brief Get priors ** @param self object ** @return priors of cluster gaussians. **/ void const * vl_gmm_get_priors (VlGMM const * self) { return self->priors ; } /** @brief Get posteriors ** @param self object ** @return posterior probabilities of cluster memberships. **/ void const * vl_gmm_get_posteriors (VlGMM const * self) { return self->posteriors ; } /** @brief Get maximum number of iterations ** @param self object ** @return maximum number of iterations. **/ vl_size vl_gmm_get_max_num_iterations (VlGMM const * self) { return self->maxNumIterations ; } /** @brief Set maximum number of iterations ** @param self VlGMM filter. ** @param maxNumIterations maximum number of iterations. **/ void vl_gmm_set_max_num_iterations (VlGMM * self, vl_size maxNumIterations) { self->maxNumIterations = maxNumIterations ; } /** @brief Get maximum number of repetitions. ** @param self object ** @return current number of repretitions for quantization. **/ vl_size vl_gmm_get_num_repetitions (VlGMM const * self) { return self->numRepetitions ; } /** @brief Set maximum number of repetitions ** @param self object ** @param numRepetitions maximum number of repetitions. ** The number of repetitions cannot be smaller than 1. **/ void vl_gmm_set_num_repetitions (VlGMM * self, vl_size numRepetitions) { assert (numRepetitions >= 1) ; self->numRepetitions = numRepetitions ; } /** @brief Get data dimension ** @param self object ** @return data dimension. **/ vl_size vl_gmm_get_dimension (VlGMM const * self) { return self->dimension ; } /** @brief Get initialization algorithm ** @param self object ** @return initialization algorithm. **/ VlGMMInitialization vl_gmm_get_initialization (VlGMM const * self) { return self->initialization ; } /** @brief Set initialization algorithm. ** @param self object ** @param init initialization algorithm. **/ void vl_gmm_set_initialization (VlGMM * self, VlGMMInitialization init) { self->initialization = init; } /** @brief Get KMeans initialization object. ** @param self object ** @return kmeans initialization object. **/ VlKMeans * vl_gmm_get_kmeans_init_object (VlGMM const * self) { return self->kmeansInit; } /** @brief Set KMeans initialization object. ** @param self object ** @param kmeans initialization KMeans object. **/ void vl_gmm_set_kmeans_init_object (VlGMM * self, VlKMeans * kmeans) { if (self->kmeansInit && self->kmeansInitIsOwner) { vl_kmeans_delete(self->kmeansInit) ; } self->kmeansInit = kmeans; self->kmeansInitIsOwner = VL_FALSE; } /** @brief Get the lower bound on the diagonal covariance values. ** @param self object ** @return lower bound on covariances. **/ double const * vl_gmm_get_covariance_lower_bounds (VlGMM const * self) { return self->sigmaLowBound; } /** @brief Set the lower bounds on diagonal covariance values. ** @param self object. ** @param bounds bounds. ** ** There is one lower bound per dimension. Use ::vl_gmm_set_covariance_lower_bound ** to set all of them to a given scalar. **/ void vl_gmm_set_covariance_lower_bounds (VlGMM * self, double const * bounds) { memcpy(self->sigmaLowBound, bounds, sizeof(double) * self->dimension) ; } /** @brief Set the lower bounds on diagonal covariance values. ** @param self object. ** @param bound bound. ** ** While there is one lower bound per dimension, this function sets ** all of them to the specified scalar. Use ::vl_gmm_set_covariance_lower_bounds ** to set them individually. **/ void vl_gmm_set_covariance_lower_bound (VlGMM * self, double bound) { int i ; for (i = 0 ; i < (signed)self->dimension ; ++i) { self->sigmaLowBound[i] = bound ; } } /* ---------------------------------------------------------------- */ /* Instantiate shuffle algorithm */ #define VL_SHUFFLE_type vl_uindex #define VL_SHUFFLE_prefix _vl_gmm #include "shuffle-def.h" /* #ifdef VL_GMM_INSTANTITATING */ #endif /* ---------------------------------------------------------------- */ #ifdef VL_GMM_INSTANTIATING /* ---------------------------------------------------------------- */ /* ---------------------------------------------------------------- */ /* Posterior assignments */ /* ---------------------------------------------------------------- */ /** @fn vl_get_gmm_data_posterior_f(float*,vl_size,vl_size,float const*,float const*,vl_size,float const*,float const*) ** @brief Get Gaussian modes posterior probabilities ** @param posteriors posterior probabilities (output)/ ** @param numClusters number of modes in the GMM model. ** @param numData number of data elements. ** @param priors prior mode probabilities of the GMM model. ** @param means means of the GMM model. ** @param dimension data dimension. ** @param covariances diagonal covariances of the GMM model. ** @param data data. ** @return data log-likelihood. ** ** This is a helper function that does not require a ::VlGMM object ** instance to operate. **/ double VL_XCAT(vl_get_gmm_data_posteriors_, SFX) (TYPE * posteriors, vl_size numClusters, vl_size numData, TYPE const * priors, TYPE const * means, vl_size dimension, TYPE const * covariances, TYPE const * data) { vl_index i_d, i_cl; vl_size dim; double LL = 0; TYPE halfDimLog2Pi = (dimension / 2.0) * log(2.0*VL_PI); TYPE * logCovariances ; TYPE * logWeights ; TYPE * invCovariances ; #if (FLT == VL_TYPE_FLOAT) VlFloatVector3ComparisonFunction distFn = vl_get_vector_3_comparison_function_f(VlDistanceMahalanobis) ; #else VlDoubleVector3ComparisonFunction distFn = vl_get_vector_3_comparison_function_d(VlDistanceMahalanobis) ; #endif logCovariances = vl_malloc(sizeof(TYPE) * numClusters) ; invCovariances = vl_malloc(sizeof(TYPE) * numClusters * dimension) ; logWeights = vl_malloc(sizeof(TYPE) * numClusters) ; #if defined(_OPENMP) #pragma omp parallel for private(i_cl,dim) num_threads(vl_get_max_threads()) #endif for (i_cl = 0 ; i_cl < (signed)numClusters ; ++ i_cl) { TYPE logSigma = 0 ; if (priors[i_cl] < VL_GMM_MIN_PRIOR) { logWeights[i_cl] = - (TYPE) VL_INFINITY_D ; } else { logWeights[i_cl] = log(priors[i_cl]); } for(dim = 0 ; dim < dimension ; ++ dim) { logSigma += log(covariances[i_cl*dimension + dim]); invCovariances [i_cl*dimension + dim] = (TYPE) 1.0 / covariances[i_cl*dimension + dim]; } logCovariances[i_cl] = logSigma; } /* end of parallel region */ #if defined(_OPENMP) #pragma omp parallel for private(i_cl,i_d) reduction(+:LL) \ num_threads(vl_get_max_threads()) #endif for (i_d = 0 ; i_d < (signed)numData ; ++ i_d) { TYPE clusterPosteriorsSum = 0; TYPE maxPosterior = (TYPE)(-VL_INFINITY_D) ; for (i_cl = 0 ; i_cl < (signed)numClusters ; ++ i_cl) { TYPE p = logWeights[i_cl] - halfDimLog2Pi - 0.5 * logCovariances[i_cl] - 0.5 * distFn (dimension, data + i_d * dimension, means + i_cl * dimension, invCovariances + i_cl * dimension) ; posteriors[i_cl + i_d * numClusters] = p ; if (p > maxPosterior) { maxPosterior = p ; } } for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { TYPE p = posteriors[i_cl + i_d * numClusters] ; p = exp(p - maxPosterior) ; posteriors[i_cl + i_d * numClusters] = p ; clusterPosteriorsSum += p ; } LL += log(clusterPosteriorsSum) + (double) maxPosterior ; for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { posteriors[i_cl + i_d * numClusters] /= clusterPosteriorsSum ; } } /* end of parallel region */ vl_free(logCovariances); vl_free(logWeights); vl_free(invCovariances); return LL; } /* ---------------------------------------------------------------- */ /* Restarts zero-weighted Gaussians */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_gmm_maximization_, SFX) (VlGMM * self, TYPE * posteriors, TYPE * priors, TYPE * covariances, TYPE * means, TYPE const * data, vl_size numData) ; static vl_size VL_XCAT(_vl_gmm_restart_empty_modes_, SFX) (VlGMM * self, TYPE const * data) { vl_size dimension = self->dimension; vl_size numClusters = self->numClusters; vl_index i_cl, j_cl, i_d, d; vl_size zeroWNum = 0; TYPE * priors = (TYPE*)self->priors ; TYPE * means = (TYPE*)self->means ; TYPE * covariances = (TYPE*)self->covariances ; TYPE * posteriors = (TYPE*)self->posteriors ; //VlRand * rand = vl_get_rand() ; TYPE * mass = vl_calloc(sizeof(TYPE), self->numClusters) ; if (numClusters <= 1) { return 0 ; } /* compute statistics */ { vl_uindex i, k ; vl_size numNullAssignments = 0 ; for (i = 0 ; i < self->numData ; ++i) { for (k = 0 ; k < self->numClusters ; ++k) { TYPE p = ((TYPE*)self->posteriors)[k + i * self->numClusters] ; mass[k] += p ; if (p < VL_GMM_MIN_POSTERIOR) { numNullAssignments ++ ; } } } if (self->verbosity) { VL_PRINTF("gmm: sparsity of data posterior: %.1f%%\n", (double)numNullAssignments / (self->numData * self->numClusters) * 100) ; } } #if 0 /* search for cluster with negligible weight and reassign them to fat clusters */ for (i_cl = 0 ; i_cl < numClusters ; ++i_cl) { if (priors[i_cl] < 0.00001/numClusters) { double mass = priors[0] ; vl_index best = 0 ; for (j_cl = 1 ; j_cl < numClusters ; ++j_cl) { if (priors[j_cl] > mass) { mass = priors[j_cl] ; best = j_cl ; } } if (j_cl == i_cl) { /* this should never happen */ continue ; } j_cl = best ; zeroWNum ++ ; VL_PRINTF("gmm: restarting mode %d by splitting mode %d (with prior %f)\n", i_cl,j_cl,mass) ; priors[i_cl] = mass/2 ; priors[j_cl] = mass/2 ; for (d = 0 ; d < dimension ; ++d) { TYPE sigma2 = covariances[j_cl*dimension + d] ; TYPE sigma = VL_XCAT(vl_sqrt_,SFX)(sigma2) ; means[i_cl*dimension + d] = means[j_cl*dimension + d] + 0.001 * (vl_rand_real1(rand) - 0.5) * sigma ; covariances[i_cl*dimension + d] = sigma2 ; } } } #endif /* search for cluster with negligible weight and reassign them to fat clusters */ for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { double size = - VL_INFINITY_D ; vl_index best = -1 ; if (mass[i_cl] >= VL_GMM_MIN_POSTERIOR * VL_MAX(1.0, (double) self->numData / self->numClusters)) { continue ; } if (self->verbosity) { VL_PRINTF("gmm: mode %d is nearly empty (mass %f)\n", i_cl, mass[i_cl]) ; } /* Search for the Gaussian components that (approximately) maximally contribute to make the negative log-likelihood of the data large. Then split the worst offender. To do so, we approximate the exptected log-likelihood of the GMM: E[-log(f(x))] = H(f) = - log \int f(x) log f(x) where the density f(x) = sum_k pk gk(x) is a GMM. This is intractable but it is easy to approximate if we suppose that supp gk is disjoint with supp gq for all components k ~= q. In this canse H(f) ~= sum_k [ - pk log(pk) + pk H(gk) ] where H(gk) is the entropy of component k taken alone. The entropy of the latter is given by: H(gk) = D/2 (1 + log(2pi) + 1/2 sum_{i=0}^D log sigma_i^2 */ for (j_cl = 0 ; j_cl < (signed)numClusters ; ++j_cl) { double size_ ; if (priors[j_cl] < VL_GMM_MIN_PRIOR) { continue ; } size_ = + 0.5 * dimension * (1.0 + log(2*VL_PI)) ; for(d = 0 ; d < (signed)dimension ; d++) { double sigma2 = covariances[j_cl * dimension + d] ; size_ += 0.5 * log(sigma2) ; } size_ = priors[j_cl] * (size_ - log(priors[j_cl])) ; if (self->verbosity > 1) { VL_PRINTF("gmm: mode %d: prior %f, mass %f, entropy contribution %f\n", j_cl, priors[j_cl], mass[j_cl], size_) ; } if (size_ > size) { size = size_ ; best = j_cl ; } } j_cl = best ; if (j_cl == i_cl || j_cl < 0) { if (self->verbosity) { VL_PRINTF("gmm: mode %d is empty, " "but no other mode to split could be found\n", i_cl) ; } continue ; } if (self->verbosity) { VL_PRINTF("gmm: reinitializing empty mode %d with mode %d (prior %f, mass %f, score %f)\n", i_cl, j_cl, priors[j_cl], mass[j_cl], size) ; } /* Search for the dimension with maximum variance. */ size = - VL_INFINITY_D ; best = - 1 ; for(d = 0; d < (signed)dimension; d++) { double sigma2 = covariances[j_cl * dimension + d] ; if (sigma2 > size) { size = sigma2 ; best = d ; } } /* Reassign points j_cl (mode to split) to i_cl (empty mode). */ { TYPE mu = means[best + j_cl * self->dimension] ; for(i_d = 0 ; i_d < (signed)self->numData ; ++ i_d) { TYPE p = posteriors[j_cl + self->numClusters * i_d] ; TYPE q = posteriors[i_cl + self->numClusters * i_d] ; /* ~= 0 */ if (data[best + i_d * self->dimension] < mu) { /* assign this point to i_cl */ posteriors[i_cl + self->numClusters * i_d] = p + q ; posteriors[j_cl + self->numClusters * i_d] = 0 ; } else { /* assign this point to j_cl */ posteriors[i_cl + self->numClusters * i_d] = 0 ; posteriors[j_cl + self->numClusters * i_d] = p + q ; } } } /* Re-estimate. */ VL_XCAT(_vl_gmm_maximization_, SFX) (self,posteriors,priors,covariances,means,data,self->numData) ; } return zeroWNum; } /* ---------------------------------------------------------------- */ /* Helpers */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_gmm_apply_bounds_, SFX)(VlGMM * self) { vl_uindex dim ; vl_uindex k ; vl_size numAdjusted = 0 ; TYPE * cov = (TYPE*)self->covariances ; double const * lbs = self->sigmaLowBound ; for (k = 0 ; k < self->numClusters ; ++k) { vl_bool adjusted = VL_FALSE ; for (dim = 0 ; dim < self->dimension ; ++dim) { if (cov[k * self->dimension + dim] < lbs[dim] ) { cov[k * self->dimension + dim] = lbs[dim] ; adjusted = VL_TRUE ; } } if (adjusted) { numAdjusted ++ ; } } if (numAdjusted > 0 && self->verbosity > 0) { VL_PRINT("gmm: detected %d of %d modes with at least one dimension " "with covariance too small (set to lower bound)\n", numAdjusted, self->numClusters) ; } } /* ---------------------------------------------------------------- */ /* EM - Maximization step */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_gmm_maximization_, SFX) (VlGMM * self, TYPE * posteriors, TYPE * priors, TYPE * covariances, TYPE * means, TYPE const * data, vl_size numData) { vl_size numClusters = self->numClusters; vl_index i_d, i_cl; vl_size dim ; TYPE * oldMeans ; double time = 0 ; if (self->verbosity > 1) { VL_PRINTF("gmm: em: entering maximization step\n") ; time = vl_get_cpu_time() ; } oldMeans = vl_malloc(sizeof(TYPE) * self->dimension * numClusters) ; memcpy(oldMeans, means, sizeof(TYPE) * self->dimension * numClusters) ; memset(priors, 0, sizeof(TYPE) * numClusters) ; memset(means, 0, sizeof(TYPE) * self->dimension * numClusters) ; memset(covariances, 0, sizeof(TYPE) * self->dimension * numClusters) ; #if defined(_OPENMP) #pragma omp parallel default(shared) private(i_d, i_cl, dim) \ num_threads(vl_get_max_threads()) #endif { TYPE * clusterPosteriorSum_, * means_, * covariances_ ; #if defined(_OPENMP) #pragma omp critical #endif { clusterPosteriorSum_ = vl_calloc(sizeof(TYPE), numClusters) ; means_ = vl_calloc(sizeof(TYPE), self->dimension * numClusters) ; covariances_ = vl_calloc(sizeof(TYPE), self->dimension * numClusters) ; } /* Accumulate weighted sums and sum of square differences. Once normalized, these become the means and covariances of each Gaussian mode. The squared differences will be taken w.r.t. the old means however. In this manner, one avoids doing two passes across the data. Eventually, these are corrected to account for the new means properly. In principle, one could set the old means to zero, but this may cause numerical instabilities (by accumulating large squares). */ #if defined(_OPENMP) #pragma omp for #endif for (i_d = 0 ; i_d < (signed)numData ; ++i_d) { for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { TYPE p = posteriors[i_cl + i_d * self->numClusters] ; vl_bool calculated = VL_FALSE ; /* skip very small associations for speed */ if (p < VL_GMM_MIN_POSTERIOR / numClusters) { continue ; } clusterPosteriorSum_ [i_cl] += p ; #ifndef VL_DISABLE_AVX if (vl_get_simd_enabled() && vl_cpu_has_avx()) { VL_XCAT(_vl_weighted_mean_avx_, SFX) (self->dimension, means_+ i_cl * self->dimension, data + i_d * self->dimension, p) ; VL_XCAT(_vl_weighted_sigma_avx_, SFX) (self->dimension, covariances_ + i_cl * self->dimension, data + i_d * self->dimension, oldMeans + i_cl * self->dimension, p) ; calculated = VL_TRUE; } #endif #ifndef VL_DISABLE_SSE2 if (vl_get_simd_enabled() && vl_cpu_has_sse2() && !calculated) { VL_XCAT(_vl_weighted_mean_sse2_, SFX) (self->dimension, means_+ i_cl * self->dimension, data + i_d * self->dimension, p) ; VL_XCAT(_vl_weighted_sigma_sse2_, SFX) (self->dimension, covariances_ + i_cl * self->dimension, data + i_d * self->dimension, oldMeans + i_cl * self->dimension, p) ; calculated = VL_TRUE; } #endif if(!calculated) { for (dim = 0 ; dim < self->dimension ; ++dim) { TYPE x = data[i_d * self->dimension + dim] ; TYPE mu = oldMeans[i_cl * self->dimension + dim] ; TYPE diff = x - mu ; means_ [i_cl * self->dimension + dim] += p * x ; covariances_ [i_cl * self->dimension + dim] += p * (diff*diff) ; } } } } /* accumulate */ #if defined(_OPENMP) #pragma omp critical #endif { for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { priors [i_cl] += clusterPosteriorSum_ [i_cl]; for (dim = 0 ; dim < self->dimension ; ++dim) { means [i_cl * self->dimension + dim] += means_ [i_cl * self->dimension + dim] ; covariances [i_cl * self->dimension + dim] += covariances_ [i_cl * self->dimension + dim] ; } } vl_free(means_); vl_free(covariances_); vl_free(clusterPosteriorSum_); } } /* parallel section */ /* at this stage priors[] contains the total mass of each cluster */ for (i_cl = 0 ; i_cl < (signed)numClusters ; ++ i_cl) { TYPE mass = priors[i_cl] ; /* do not update modes that do not recieve mass */ if (mass >= 1e-6 / numClusters) { for (dim = 0 ; dim < self->dimension ; ++dim) { means[i_cl * self->dimension + dim] /= mass ; covariances[i_cl * self->dimension + dim] /= mass ; } } } /* apply old to new means correction */ for (i_cl = 0 ; i_cl < (signed)numClusters ; ++ i_cl) { TYPE mass = priors[i_cl] ; if (mass >= 1e-6 / numClusters) { for (dim = 0 ; dim < self->dimension ; ++dim) { TYPE mu = means[i_cl * self->dimension + dim] ; TYPE oldMu = oldMeans[i_cl * self->dimension + dim] ; TYPE diff = mu - oldMu ; covariances[i_cl * self->dimension + dim] -= diff * diff ; } } } VL_XCAT(_vl_gmm_apply_bounds_,SFX)(self) ; { TYPE sum = 0; for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { sum += priors[i_cl] ; } sum = VL_MAX(sum, 1e-12) ; for (i_cl = 0 ; i_cl < (signed)numClusters ; ++i_cl) { priors[i_cl] /= sum ; } } if (self->verbosity > 1) { VL_PRINTF("gmm: em: maximization step completed in %.2f s\n", vl_get_cpu_time() - time) ; } vl_free(oldMeans); } /* ---------------------------------------------------------------- */ /* EM iterations */ /* ---------------------------------------------------------------- */ static double VL_XCAT(_vl_gmm_em_, SFX) (VlGMM * self, TYPE const * data, vl_size numData) { vl_size iteration, restarted ; double previousLL = (TYPE)(-VL_INFINITY_D) ; double LL = (TYPE)(-VL_INFINITY_D) ; double time = 0 ; _vl_gmm_prepare_for_data (self, numData) ; VL_XCAT(_vl_gmm_apply_bounds_,SFX)(self) ; for (iteration = 0 ; 1 ; ++ iteration) { double eps ; /* Expectation: assign data to Gaussian modes and compute log-likelihood. */ if (self->verbosity > 1) { VL_PRINTF("gmm: em: entering expectation step\n") ; time = vl_get_cpu_time() ; } LL = VL_XCAT(vl_get_gmm_data_posteriors_,SFX) (self->posteriors, self->numClusters, numData, self->priors, self->means, self->dimension, self->covariances, data) ; if (self->verbosity > 1) { VL_PRINTF("gmm: em: expectation step completed in %.2f s\n", vl_get_cpu_time() - time) ; } /* Check the termination conditions. */ if (self->verbosity) { VL_PRINTF("gmm: em: iteration %d: loglikelihood = %f (variation = %f)\n", iteration, LL, LL - previousLL) ; } if (iteration >= self->maxNumIterations) { if (self->verbosity) { VL_PRINTF("gmm: em: terminating because " "the maximum number of iterations " "(%d) has been reached.\n", self->maxNumIterations) ; } break ; } eps = vl_abs_d ((LL - previousLL) / (LL)); if ((iteration > 0) && (eps < 0.00001)) { if (self->verbosity) { VL_PRINTF("gmm: em: terminating because the algorithm " "fully converged (log-likelihood variation = %f).\n", eps) ; } break ; } previousLL = LL ; /* Restart empty modes. */ if (iteration > 1) { restarted = VL_XCAT(_vl_gmm_restart_empty_modes_, SFX) (self, data); if ((restarted > 0) & (self->verbosity > 0)) { VL_PRINTF("gmm: em: %d Gaussian modes restarted because " "they had become empty.\n", restarted); } } /* Maximization: reestimate the GMM parameters. */ VL_XCAT(_vl_gmm_maximization_, SFX) (self,self->posteriors,self->priors,self->covariances,self->means,data,numData) ; } return LL; } /* ---------------------------------------------------------------- */ /* Kmeans initialization of mixtures */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_gmm_init_with_kmeans_, SFX) (VlGMM * self, TYPE const * data, vl_size numData, VlKMeans * kmeansInit) { vl_size i_d ; vl_uint32 * assignments = vl_malloc(sizeof(vl_uint32) * numData); _vl_gmm_prepare_for_data (self, numData) ; memset(self->means,0,sizeof(TYPE) * self->numClusters * self->dimension) ; memset(self->priors,0,sizeof(TYPE) * self->numClusters) ; memset(self->covariances,0,sizeof(TYPE) * self->numClusters * self->dimension) ; memset(self->posteriors,0,sizeof(TYPE) * self->numClusters * numData) ; /* setup speified KMeans initialization object if any */ if (kmeansInit) { vl_gmm_set_kmeans_init_object (self, kmeansInit) ; } /* if a KMeans initalization object is still unavailable, create one */ if(self->kmeansInit == NULL) { vl_size ncomparisons = VL_MAX(numData / 4, 10) ; vl_size niter = 5 ; vl_size ntrees = 1 ; vl_size nrepetitions = 1 ; VlKMeansAlgorithm algorithm = VlKMeansANN ; VlKMeansInitialization initialization = VlKMeansRandomSelection ; VlKMeans * kmeansInitDefault = vl_kmeans_new(self->dataType,VlDistanceL2) ; vl_kmeans_set_initialization(kmeansInitDefault, initialization); vl_kmeans_set_max_num_iterations (kmeansInitDefault, niter) ; vl_kmeans_set_max_num_comparisons (kmeansInitDefault, ncomparisons) ; vl_kmeans_set_num_trees (kmeansInitDefault, ntrees); vl_kmeans_set_algorithm (kmeansInitDefault, algorithm); vl_kmeans_set_num_repetitions(kmeansInitDefault, nrepetitions); vl_kmeans_set_verbosity (kmeansInitDefault, self->verbosity); self->kmeansInit = kmeansInitDefault; self->kmeansInitIsOwner = VL_TRUE ; } /* Use k-means to assign data to clusters */ vl_kmeans_cluster (self->kmeansInit, data, self->dimension, numData, self->numClusters); vl_kmeans_quantize (self->kmeansInit, assignments, NULL, data, numData) ; /* Transform the k-means assignments in posteriors and estimates the mode parameters */ for(i_d = 0; i_d < numData; i_d++) { ((TYPE*)self->posteriors)[assignments[i_d] + i_d * self->numClusters] = (TYPE) 1.0 ; } /* Update cluster parameters */ VL_XCAT(_vl_gmm_maximization_, SFX) (self,self->posteriors,self->priors,self->covariances,self->means,data,numData); vl_free(assignments) ; } /* ---------------------------------------------------------------- */ /* Random initialization of mixtures */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_gmm_compute_init_sigma_, SFX) (VlGMM * self, TYPE const * data, TYPE * initSigma, vl_size dimension, vl_size numData) { vl_size dim; vl_uindex i; TYPE * dataMean ; memset(initSigma,0,sizeof(TYPE)*dimension) ; if (numData <= 1) return ; dataMean = vl_malloc(sizeof(TYPE)*dimension); memset(dataMean,0,sizeof(TYPE)*dimension) ; /* find mean of the whole dataset */ for(dim = 0 ; dim < dimension ; dim++) { for(i = 0 ; i < numData ; i++) { dataMean[dim] += data[i*dimension + dim]; } dataMean[dim] /= numData; } /* compute variance of the whole dataset */ for(dim = 0; dim < dimension; dim++) { for(i = 0; i < numData; i++) { TYPE diff = (data[i*self->dimension + dim] - dataMean[dim]) ; initSigma[dim] += diff*diff ; } initSigma[dim] /= numData - 1 ; } vl_free(dataMean) ; } static void VL_XCAT(_vl_gmm_init_with_rand_data_, SFX) (VlGMM * self, TYPE const * data, vl_size numData) { vl_uindex i, k, dim ; VlKMeans * kmeans ; _vl_gmm_prepare_for_data(self, numData) ; /* initilaize priors of gaussians so they are equal and sum to one */ for (i = 0 ; i < self->numClusters ; ++i) { ((TYPE*)self->priors)[i] = (TYPE) (1.0 / self->numClusters) ; } /* initialize diagonals of covariance matrices to data covariance */ VL_XCAT(_vl_gmm_compute_init_sigma_, SFX) (self, data, self->covariances, self->dimension, numData); for (k = 1 ; k < self->numClusters ; ++ k) { for(dim = 0; dim < self->dimension; dim++) { *((TYPE*)self->covariances + k * self->dimension + dim) = *((TYPE*)self->covariances + dim) ; } } /* use kmeans++ initialization to pick points at random */ kmeans = vl_kmeans_new(self->dataType,VlDistanceL2) ; vl_kmeans_init_centers_plus_plus(kmeans, data, self->dimension, numData, self->numClusters) ; memcpy(self->means, vl_kmeans_get_centers(kmeans), sizeof(TYPE) * self->dimension * self->numClusters) ; vl_kmeans_delete(kmeans) ; } /* ---------------------------------------------------------------- */ #else /* VL_GMM_INSTANTIATING */ /* ---------------------------------------------------------------- */ #ifndef __DOXYGEN__ #define FLT VL_TYPE_FLOAT #define TYPE float #define SFX f #define VL_GMM_INSTANTIATING #include "gmm.c" #define FLT VL_TYPE_DOUBLE #define TYPE double #define SFX d #define VL_GMM_INSTANTIATING #include "gmm.c" #endif /* VL_GMM_INSTANTIATING */ #endif /* ---------------------------------------------------------------- */ #ifndef VL_GMM_INSTANTIATING /* ---------------------------------------------------------------- */ /** @brief Create a new GMM object by copy ** @param self object. ** @return new copy. ** ** Most parameters, including the cluster priors, means, and ** covariances are copied. Data posteriors (available after ** initalization or EM) are not; nor is the KMeans object used for ** initialization, if any. **/ VlGMM * vl_gmm_new_copy (VlGMM const * self) { vl_size size = vl_get_type_size(self->dataType) ; VlGMM * gmm = vl_gmm_new(self->dataType, self->dimension, self->numClusters); gmm->initialization = self->initialization; gmm->maxNumIterations = self->maxNumIterations; gmm->numRepetitions = self->numRepetitions; gmm->verbosity = self->verbosity; gmm->LL = self->LL; memcpy(gmm->means, self->means, size*self->numClusters*self->dimension); memcpy(gmm->covariances, self->covariances, size*self->numClusters*self->dimension); memcpy(gmm->priors, self->priors, size*self->numClusters); return gmm ; } /** @brief Initialize mixture before EM takes place using random initialization ** @param self GMM object instance. ** @param data data points which should be clustered. ** @param numData number of data points. **/ void vl_gmm_init_with_rand_data (VlGMM * self, void const * data, vl_size numData) { vl_gmm_reset (self) ; switch (self->dataType) { case VL_TYPE_FLOAT : _vl_gmm_init_with_rand_data_f (self, (float const *)data, numData) ; break ; case VL_TYPE_DOUBLE : _vl_gmm_init_with_rand_data_d (self, (double const *)data, numData) ; break ; default: abort() ; } } /** @brief Initializes the GMM using KMeans ** @param self GMM object instance. ** @param data data points which should be clustered. ** @param numData number of data points. ** @param kmeansInit KMeans object to use. **/ void vl_gmm_init_with_kmeans (VlGMM * self, void const * data, vl_size numData, VlKMeans * kmeansInit) { vl_gmm_reset (self) ; switch (self->dataType) { case VL_TYPE_FLOAT : _vl_gmm_init_with_kmeans_f (self, (float const *)data, numData, kmeansInit) ; break ; case VL_TYPE_DOUBLE : _vl_gmm_init_with_kmeans_d (self, (double const *)data, numData, kmeansInit) ; break ; default: abort() ; } } #if 0 #include #endif /** @brief Run GMM clustering - includes initialization and EM ** @param self GMM object instance. ** @param data data points which should be clustered. ** @param numData number of data points. **/ double vl_gmm_cluster (VlGMM * self, void const * data, vl_size numData) { void * bestPriors = NULL ; void * bestMeans = NULL; void * bestCovariances = NULL; void * bestPosteriors = NULL; vl_size size = vl_get_type_size(self->dataType) ; double bestLL = -VL_INFINITY_D; vl_uindex repetition; assert(self->numRepetitions >=1) ; bestPriors = vl_malloc(size * self->numClusters) ; bestMeans = vl_malloc(size * self->dimension * self->numClusters) ; bestCovariances = vl_malloc(size * self->dimension * self->numClusters) ; bestPosteriors = vl_malloc(size * self->numClusters * numData) ; #if 0 feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW); #endif for (repetition = 0 ; repetition < self->numRepetitions ; ++ repetition) { double LL ; double timeRef ; if (self->verbosity) { VL_PRINTF("gmm: clustering: starting repetition %d of %d\n", repetition + 1, self->numRepetitions) ; } /* initialize a new mixture model */ timeRef = vl_get_cpu_time() ; switch (self->initialization) { case VlGMMKMeans : vl_gmm_init_with_kmeans (self, data, numData, NULL) ; break ; case VlGMMRand : vl_gmm_init_with_rand_data (self, data, numData) ; break ; case VlGMMCustom : break ; default: abort() ; } if (self->verbosity) { VL_PRINTF("gmm: model initialized in %.2f s\n", vl_get_cpu_time() - timeRef) ; } /* fit the model to data by running EM */ timeRef = vl_get_cpu_time () ; LL = vl_gmm_em (self, data, numData) ; if (self->verbosity) { VL_PRINTF("gmm: optimization terminated in %.2f s with loglikelihood %f\n", vl_get_cpu_time() - timeRef, LL) ; } if (LL > bestLL || repetition == 0) { void * temp ; temp = bestPriors ; bestPriors = self->priors ; self->priors = temp ; temp = bestMeans ; bestMeans = self->means ; self->means = temp ; temp = bestCovariances ; bestCovariances = self->covariances ; self->covariances = temp ; temp = bestPosteriors ; bestPosteriors = self->posteriors ; self->posteriors = temp ; bestLL = LL; } } vl_free (self->priors) ; vl_free (self->means) ; vl_free (self->covariances) ; vl_free (self->posteriors) ; self->priors = bestPriors ; self->means = bestMeans ; self->covariances = bestCovariances ; self->posteriors = bestPosteriors ; self->LL = bestLL; if (self->verbosity) { VL_PRINTF("gmm: all repetitions terminated with final loglikelihood %f\n", self->LL) ; } return bestLL ; } /** @brief Invoke the EM algorithm. ** @param self GMM object instance. ** @param data data points which should be clustered. ** @param numData number of data points. **/ double vl_gmm_em (VlGMM * self, void const * data, vl_size numData) { switch (self->dataType) { case VL_TYPE_FLOAT: return _vl_gmm_em_f (self, (float const *)data, numData) ; break ; case VL_TYPE_DOUBLE: return _vl_gmm_em_d (self, (double const *)data, numData) ; break ; default: abort() ; } return 0 ; } /** @brief Explicitly set the initial means for EM. ** @param self GMM object instance. ** @param means initial values of means. **/ void vl_gmm_set_means (VlGMM * self, void const * means) { memcpy(self->means,means, self->dimension * self->numClusters * vl_get_type_size(self->dataType)); } /** @brief Explicitly set the initial sigma diagonals for EM. ** @param self GMM object instance. ** @param covariances initial values of covariance matrix diagonals. **/ void vl_gmm_set_covariances (VlGMM * self, void const * covariances) { memcpy(self->covariances,covariances, self->dimension * self->numClusters * vl_get_type_size(self->dataType)); } /** @brief Explicitly set the initial priors of the gaussians. ** @param self GMM object instance. ** @param priors initial values of the gaussian priors. **/ void vl_gmm_set_priors (VlGMM * self, void const * priors) { memcpy(self->priors,priors, self->numClusters * vl_get_type_size(self->dataType)); } /* VL_GMM_INSTANTIATING */ #endif #undef SFX #undef TYPE #undef FLT #undef VL_GMM_INSTANTIATING colmap-3.9.1/src/thirdparty/VLFeat/gmm.h000066400000000000000000000105101454702036400200340ustar00rootroot00000000000000/** @file gmm.h ** @brief GMM (@ref gmm) ** @author David Novotny ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 David Novotny and Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_GMM_H #define VL_GMM_H #include "kmeans.h" /** @brief GMM initialization algorithms */ typedef enum _VlGMMInitialization { VlGMMKMeans, /**< Initialize GMM from KMeans clustering. */ VlGMMRand, /**< Initialize GMM parameters by selecting points at random. */ VlGMMCustom /**< User specifies the initial GMM parameters. */ } VlGMMInitialization ; #ifndef __DOXYGEN__ struct _VlGMM ; typedef struct _VlGMM VlGMM ; #else /** @brief GMM quantizer */ typedef OPAQUE VlGMM ; #endif /** @name Create and destroy ** @{ **/ VL_EXPORT VlGMM * vl_gmm_new (vl_type dataType, vl_size dimension, vl_size numComponents) ; VL_EXPORT VlGMM * vl_gmm_new_copy (VlGMM const * gmm) ; VL_EXPORT void vl_gmm_delete (VlGMM * self) ; VL_EXPORT void vl_gmm_reset (VlGMM * self); /** @} */ /** @name Basic data processing ** @{ **/ VL_EXPORT double vl_gmm_cluster (VlGMM * self, void const * data, vl_size numData); /** @} */ /** @name Fine grained data processing ** @{ */ VL_EXPORT void vl_gmm_init_with_rand_data (VlGMM * self, void const * data, vl_size numData) ; VL_EXPORT void vl_gmm_init_with_kmeans (VlGMM * self, void const * data, vl_size numData, VlKMeans * kmeansInit); VL_EXPORT double vl_gmm_em (VlGMM * self, void const * data, vl_size numData); /** @} */ VL_EXPORT void vl_gmm_set_means (VlGMM * self, void const * means); VL_EXPORT void vl_gmm_set_covariances (VlGMM * self, void const * covariances); VL_EXPORT void vl_gmm_set_priors (VlGMM * self, void const * priors); VL_EXPORT double vl_get_gmm_data_posteriors_f(float * posteriors, vl_size numClusters, vl_size numData, float const * priors, float const * means, vl_size dimension, float const * covariances, float const * data) ; VL_EXPORT double vl_get_gmm_data_posteriors_d(double * posteriors, vl_size numClusters, vl_size numData, double const * priors, double const * means, vl_size dimension, double const * covariances, double const * data) ; /** @} */ /** @name Set parameters ** @{ **/ VL_EXPORT void vl_gmm_set_num_repetitions (VlGMM * self, vl_size numRepetitions) ; VL_EXPORT void vl_gmm_set_max_num_iterations (VlGMM * self, vl_size maxNumIterations) ; VL_EXPORT void vl_gmm_set_verbosity (VlGMM * self, int verbosity) ; VL_EXPORT void vl_gmm_set_initialization (VlGMM * self, VlGMMInitialization init); VL_EXPORT void vl_gmm_set_kmeans_init_object (VlGMM * self, VlKMeans * kmeans); VL_EXPORT void vl_gmm_set_covariance_lower_bounds (VlGMM * self, double const * bounds); VL_EXPORT void vl_gmm_set_covariance_lower_bound (VlGMM * self, double bound) ; /** @} */ /** @name Get parameters ** @{ **/ VL_EXPORT void const * vl_gmm_get_means (VlGMM const * self); VL_EXPORT void const * vl_gmm_get_covariances (VlGMM const * self); VL_EXPORT void const * vl_gmm_get_priors (VlGMM const * self); VL_EXPORT void const * vl_gmm_get_posteriors (VlGMM const * self); VL_EXPORT vl_type vl_gmm_get_data_type (VlGMM const * self); VL_EXPORT vl_size vl_gmm_get_dimension (VlGMM const * self); VL_EXPORT vl_size vl_gmm_get_num_repetitions (VlGMM const * self); VL_EXPORT vl_size vl_gmm_get_num_data (VlGMM const * self); VL_EXPORT vl_size vl_gmm_get_num_clusters (VlGMM const * self); VL_EXPORT double vl_gmm_get_loglikelihood (VlGMM const * self); VL_EXPORT int vl_gmm_get_verbosity (VlGMM const * self); VL_EXPORT vl_size vl_gmm_get_max_num_iterations (VlGMM const * self); VL_EXPORT vl_size vl_gmm_get_num_repetitions (VlGMM const * self); VL_EXPORT VlGMMInitialization vl_gmm_get_initialization (VlGMM const * self); VL_EXPORT VlKMeans * vl_gmm_get_kmeans_init_object (VlGMM const * self); VL_EXPORT double const * vl_gmm_get_covariance_lower_bounds (VlGMM const * self); /** @} */ /* VL_GMM_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/heap-def.h000066400000000000000000000325151454702036400207360ustar00rootroot00000000000000/** @file heap-def.h ** @brief Heap preprocessor metaprogram ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file heap-def.h A heap organizes an array of objects in a priority queue. This module is a template metaprogram that defines heap operations on array of generic objects, or even generic object containers. - @ref heap-def-overview "Overview" - @ref heap-def-overview-general "General usage" - @ref heap-def-tech "Technical details" @section heap-def-overview Overview To use @ref heap-def.h one must specify at least a prefix and the data type for the heap elements: @code #define VL_HEAP_prefix my_heap #define VL_HEAP_type float #include @endcode This code fragment defines a number of functions prefixed by ::VL_HEAP_prefix, such as @c my_heap_push (::VL_HEAP_push) and @c my_heap_pop (::VL_HEAP_pop), that implement the heap operations. These functions operate on an array that has type ::VL_HEAP_array. By default, this is defined to be: @code #define VL_HEAP_array VL_HEAP_type* #define VL_HEAP_array_const VL_HEAP_type const* @endcode The array itself is accessed uniquely by means of two functions: - ::VL_HEAP_cmp, that compares two array elements. The default implementation assumes that ::VL_HEAP_type is numeric. - ::VL_HEAP_swap, that swaps two array elements. The default implementation assumes that ::VL_HEAP_type can be copied by the @c = operator. The heap state is a integer @c numElements (of type ::vl_size) counting the number of elements of the array that are currently part of the heap and the content of the first @c numElements elements of the array. The portion of the array that constitutes the heap satisfies a certain invariant property (heap property, @ref heap-def-tech). From a user viewpoint, the most important consequence is that the first element of the array (the one of index 0) is also the smallest (according to ::VL_HEAP_cmp). Elements are added to the heap by ::VL_HEAP_push and removed from the heap by ::VL_HEAP_pop. A push operation adds to the heap the array element immediately after the last element already in the heap (i.e. the element of index @c numElements) and increases the number of heap elements @c numElements. Elements in the heap are swapped as required in order to maintain the heap consistency. Similarly, a pop operation removes the first (smaller) element from the heap and decreases the number of heap elements @c numElements. The values of nodes currently in the heap can be updated by ::VL_HEAP_update. Notice however that using this function requires knowing the index of the element that needs to be updated up to the swapping operations that the heap performs to maintain consistency. Typically, this requires redefining ::VL_HEAP_swap to keep track of such changes (@ref heap-def-overview-general). @subsection heap-def-overview-general General usage The heap container may be mapped to any type by reimplementing ::VL_HEAP_cmp and ::VL_HEAP_swap explicitly. For instance the following code redefines ::VL_HEAP_cmp to deal with the case in which the heap is an array of structures: @code typedef struct _S { int x ; } S ; int s_cmp (S const * v, vl_uindex a, vl_uindex b) { return v[a].x - v[b].x ; } #define VL_HEAP_prefix s_heap #define VL_HEAP_type S #define VL_HEAP_cmp s_cmp #include @endcode In the following example, the heap itself is an arbitrary structure: @code typedef struct _H { int* array ; } H ; int h_cmp (H const * h, vl_uindex a, vl_uindex b) { return h->array[a] - h->array[b] ; } int h_swap (H * h, vl_uindex a, vl_uindex b) { int t = h->array[a] ; h->array[a] = h->array[b] ; h->array[b] = t ; } #define VL_HEAP_prefix h_heap #define VL_HEAP_swap h_swap #define VL_HEAP_cmp h_cmp #include @endcode @section heap-def-tech Technical details The heap is organised as a binary tree with the property (heap property) that any node is not larger than any of its children. In particular, the root is the smallest node. @ref heap-def.h uses the standard binary tree representation as a linear array. Tree nodes are mapped to array elements as follows: array[0] corresponds to the root, array[1] and array[2] to the root left and right children and so on. In this way, the tree structure is fully specified by the total number of nodes N. Assuming that the heap has N nodes (from array[0] to array[N-1]), adding the node array[N] to the heap is done by a push down operation: if the node array[N] is smaller than its parent (violating the heap property) it is pushed down by swapping it with the parent, and so on recursively. Removing the smallest element array[0] with an heap of N nodes is done by swapping array[0] with array[N-1]. If then array[0] is larger than any of its children, it is swapped with the smallest of the two and so on recursively (push up operation). Restoring the heap property after an element array[i] has been modified can be done by a push up or push down operation on that node. **/ #include "host.h" #include #ifndef VL_HEAP_prefix #error "VL_HEAP_prefix must be defined" #endif #ifndef VL_HEAP_array #ifndef VL_HEAP_type #error "VL_HEAP_type must be defined if VL_HEAP_array is not" #endif #define VL_HEAP_array VL_HEAP_type* #define VL_HEAP_array_const VL_HEAP_type const* #endif #ifndef VL_HEAP_array_const #define VL_HEAP_array_const VL_HEAP_array #endif #ifdef __DOXYGEN__ #define VL_HEAP_prefix HeapObject /**< Prefix of the heap functions */ #define VL_HEAP_type HeapType /**< Data type of the heap elements */ #define VL_HEAP_array HeapType* /**< Data type of the heap container */ #define VL_HEAP_array HeapType const* /**< Const data type of the heap container */ #endif /* ---------------------------------------------------------------- */ #ifndef VL_HEAP_DEF_H #define VL_HEAP_DEF_H /** @internal @brief Get index of parent node ** @param index a node index. ** @return index of the parent node. **/ VL_INLINE vl_uindex vl_heap_parent (vl_uindex index) { if (index == 0) return 0 ; return (index - 1) / 2 ; } /** @internal @brief Get index of left child ** @param index a node index. ** @return index of the left child. **/ VL_INLINE vl_uindex vl_heap_left_child (vl_uindex index) { return 2 * index + 1 ; } /** @internal @brief Get index of right child ** @param index a node index. ** @return index of the right child. **/ VL_INLINE vl_uindex vl_heap_right_child (vl_uindex index) { return vl_heap_left_child (index) + 1 ; } /* VL_HEAP_DEF_H */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_HEAP_cmp) || defined(__DOXYGEN__) #define VL_HEAP_cmp VL_XCAT(VL_HEAP_prefix, _cmp) /** @brief Compare two heap elements ** @param array heap array. ** @param indexA index of the first element @c A to compare. ** @param indexB index of the second element @c B to comapre. ** @return a negative number if @c AB. **/ VL_INLINE VL_HEAP_type VL_HEAP_cmp (VL_HEAP_array_const array, vl_uindex indexA, vl_uindex indexB) { return array[indexA] - array[indexB] ; } /* VL_HEAP_cmp */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_HEAP_swap) || defined(__DOXYGEN__) #define VL_HEAP_swap VL_XCAT(VL_HEAP_prefix, _swap) /** @brief Swap two heap elements ** @param array array of nodes. ** @param array heap array. ** @param indexA index of the first node to swap. ** @param indexB index of the second node to swap. ** ** The function swaps the two heap elements @a a and @ b. The function ** uses a temporary element and the copy operator, which must be ** well defined for the heap elements. **/ VL_INLINE void VL_HEAP_swap (VL_HEAP_array array, vl_uindex indexA, vl_uindex indexB) { VL_HEAP_type t = array [indexA] ; array [indexA] = array [indexB] ; array [indexB] = t ; } /* VL_HEAP_swap */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_HEAP_up) || defined(__DOXYGEN__) #define VL_HEAP_up VL_XCAT(VL_HEAP_prefix, _up) /** @brief Heap up operation ** @param array pointer to the heap array. ** @param heapSize size of the heap. ** @param index index of the node to push up. **/ VL_INLINE void VL_HEAP_up (VL_HEAP_array array, vl_size heapSize, vl_uindex index) { vl_uindex leftIndex = vl_heap_left_child (index) ; vl_uindex rightIndex = vl_heap_right_child (index) ; /* no childer: stop */ if (leftIndex >= heapSize) return ; /* only left childer: easy */ if (rightIndex >= heapSize) { if (VL_HEAP_cmp (array, index, leftIndex) > 0) { VL_HEAP_swap (array, index, leftIndex) ; } return ; } /* both childern */ { if (VL_HEAP_cmp (array, leftIndex, rightIndex) < 0) { /* swap with left */ if (VL_HEAP_cmp (array, index, leftIndex) > 0) { VL_HEAP_swap (array, index, leftIndex) ; VL_HEAP_up (array, heapSize, leftIndex) ; } } else { /* swap with right */ if (VL_HEAP_cmp (array, index, rightIndex) > 0) { VL_HEAP_swap (array, index, rightIndex) ; VL_HEAP_up (array, heapSize, rightIndex) ; } } } } /* VL_HEAP_up */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_HEAP_down) || defined(__DOXYGEN__) #define VL_HEAP_down VL_XCAT(VL_HEAP_prefix, _down) /** @brief Heap down operation ** @param array pointer to the heap node array. ** @param index index of the node to push up. **/ VL_INLINE void VL_HEAP_down (VL_HEAP_array array, vl_uindex index) { vl_uindex parentIndex ; if (index == 0) return ; parentIndex = vl_heap_parent (index) ; if (VL_HEAP_cmp (array, index, parentIndex) < 0) { VL_HEAP_swap (array, index, parentIndex) ; VL_HEAP_down (array, parentIndex) ; } } /* VL_HEAP_down */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_HEAP_push) || defined(__DOXYGEN__) #define VL_HEAP_push VL_XCAT(VL_HEAP_prefix, _push) /** @brief Heap push operation ** @param array pointer to the heap array. ** @param heapSize (in/out) size of the heap. ** ** The function adds to the heap the element of index @c heapSize ** and increments @c heapSize. **/ VL_INLINE void VL_HEAP_push (VL_HEAP_array array, vl_size *heapSize) { VL_HEAP_down (array, *heapSize) ; *heapSize += 1 ; } /* VL_HEAP_push */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_HEAP_pop) || defined(__DOXYGEN__) #define VL_HEAP_pop VL_XCAT(VL_HEAP_prefix, _pop) /** @brief Heap pop operation ** @param array pointer to the heap array. ** @param heapSize (in/out) size of the heap. ** @return index of the popped element. ** ** The function extracts from the heap the element of index 0 ** (the smallest element) and decreases @c heapSize. ** ** The element extracted is moved as the first element after ** the heap end (thus it has index @c heapSize). For convenience, ** this index is returned by the function. ** ** Popping from an empty heap is undefined. **/ VL_INLINE vl_uindex VL_HEAP_pop (VL_HEAP_array array, vl_size *heapSize) { assert (*heapSize) ; *heapSize -= 1 ; VL_HEAP_swap (array, 0, *heapSize) ; if (*heapSize > 1) { VL_HEAP_up (array, *heapSize, 0) ; } return *heapSize ; } /* VL_HEAP_pop */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_HEAP_update) || defined(__DOXYGEN__) #define VL_HEAP_update VL_XCAT(VL_HEAP_prefix, _update) /** @brief Heap update operation ** @param array pointer to the heap array. ** @param heapSize size of the heap. ** @param index index of the node to update. ** ** The function updates the heap to account for a change to the ** element of index @c index in the heap. ** ** Notice that using this ** function requires knowing the index of the heap index of ** element that was changed. Since the heap swaps elements in the ** array, this is in general different from the index that that ** element had originally. **/ VL_INLINE void VL_HEAP_update (VL_HEAP_array array, vl_size heapSize, vl_uindex index) { VL_HEAP_up (array, heapSize, index) ; VL_HEAP_down (array, index) ; } /* VL_HEAP_update */ #endif /* ---------------------------------------------------------------- */ #undef VL_HEAP_cmp #undef VL_HEAP_swap #undef VL_HEAP_up #undef VL_HEAP_down #undef VL_HEAP_push #undef VL_HEAP_pop #undef VL_HEAP_update #undef VL_HEAP_prefix #undef VL_HEAP_type #undef VL_HEAP_array #undef VL_HEAP_array_const colmap-3.9.1/src/thirdparty/VLFeat/hikmeans.c000077500000000000000000000212101454702036400210500ustar00rootroot00000000000000/** @file hikmeans.c ** @brief Hierarchical Integer K-Means Clustering - Declaration ** @author Brian Fulkerson ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file hikmeans.h ** @brief Hierarchical integer K-Means clustering ** ** Hierarchical integer K-Means clustering (HIKM) is a simple ** hierarchical version of integer K-Means (@ref ikmeans.h ** "IKM"). The algorithm recursively applies integer K-means to create ** more refined partitions of the data. ** ** Create a tree with ::vl_hikm_new() and delete it with ** ::vl_hikm_delete(). Use ::vl_hikm_train() to build the tree ** from training data and ::vl_hikm_push() to project new data down ** a HIKM tree. ** ** @section hikm-tree HIKM tree ** ** The HIKM tree is represented by a ::VlHIKMTree structure, which ** contains a tree composed of ::VlHIKMNode. Each node is an ** integer K-means filter which partitions the data into @c K ** clusters. **/ #include #include #include #include #include "hikmeans.h" /** ------------------------------------------------------------------ ** @internal ** @brief Copy a subset of the data to a buffer ** @param data Data ** @param ids Data labels ** @param N Number of indices ** @param M Data dimensionality ** @param id Label of data to copy ** @param N2 Number of data copied (out) ** @return a new buffer with a copy of the selected data. **/ vl_uint8* vl_hikm_copy_subset (vl_uint8 const * data, vl_uint32 *ids, vl_size N, vl_size M, vl_uint32 id, vl_size *N2) { vl_uindex i ; vl_size count = 0; /* count how many data points with this label there are */ for (i = 0 ; i < N ; i++) { if (ids[i] == id) { count ++ ; } } *N2 = count ; /* copy each datum to the buffer */ { vl_uint8 *new_data = vl_malloc (sizeof(*new_data) * M * count); count = 0; for (i = 0 ; i < N ; i ++) { if (ids[i] == id) { memcpy(new_data + count * M, data + i * M, sizeof(*new_data) * M); count ++ ; } } *N2 = count ; return new_data ; } } /** ------------------------------------------------------------------ ** @brief Compute HIKM clustering. ** ** @param tree HIKM tree to initialize. ** @param data Data to cluster. ** @param N Number of data points. ** @param K Number of clusters for this node. ** @param height Tree height. ** ** @remark height cannot be smaller than 1. ** ** @return a new HIKM node representing a sub-clustering. **/ static VlHIKMNode * xmeans (VlHIKMTree *tree, vl_uint8 const *data, vl_size N, vl_size K, vl_size height) { VlHIKMNode *node = vl_malloc (sizeof(*node)) ; vl_uint32 *ids = vl_malloc (sizeof(*ids) * N) ; node->filter = vl_ikm_new (tree -> method) ; node->children = (height == 1) ? 0 : vl_malloc (sizeof(*node->children) * K) ; vl_ikm_set_max_niters (node->filter, tree->max_niters) ; vl_ikm_set_verbosity (node->filter, tree->verb - 1 ) ; vl_ikm_init_rand_data (node->filter, data, tree->M, N, K) ; vl_ikm_train (node->filter, data, N) ; vl_ikm_push (node->filter, ids, data, N) ; /* recursively process each child */ if (height > 1) { vl_uindex k ; for (k = 0 ; k < K ; ++k) { vl_size partition_N ; vl_size partition_K ; vl_uint8 *partition ; partition = vl_hikm_copy_subset (data, ids, N, tree->M, (vl_uint32)k, &partition_N) ; partition_K = VL_MIN (K, partition_N) ; node->children [k] = xmeans (tree, partition, partition_N, partition_K, height - 1) ; vl_free (partition) ; if (tree->verb > (signed)tree->depth - (signed)height) { VL_PRINTF("hikmeans: branch at depth %d: %6.1f %% completed\n", tree->depth - height, (double) (k+1) / K * 100) ; } } } vl_free (ids) ; return node ; } /** ------------------------------------------------------------------ ** @internal ** @brief Delete node ** ** @param node to delete. ** ** The function deletes recursively @a node and all its descendent. **/ static void xdelete (VlHIKMNode *node) { if(node) { vl_uindex k ; if (node->children) { for(k = 0 ; k < vl_ikm_get_K (node->filter) ; ++k) xdelete (node->children[k]) ; vl_free (node->children) ; } if (node->filter) { vl_ikm_delete (node->filter) ; } vl_free(node); } } /** ------------------------------------------------------------------ ** @brief New HIKM tree ** @param method clustering method. ** @return new HIKM tree. **/ VlHIKMTree * vl_hikm_new (int method) { VlHIKMTree *f = vl_calloc (sizeof(VlHIKMTree), 1) ; f->max_niters = 200 ; f->method = method ; return f ; } /** ------------------------------------------------------------------ ** @brief Delete HIKM tree ** @param f HIKM tree. **/ void vl_hikm_delete (VlHIKMTree *f) { if (f) { xdelete (f->root) ; vl_free (f) ; } } /** ------------------------------------------------------------------ ** @brief Initialize HIKM tree ** @param f HIKM tree. ** @param M Data dimensionality. ** @param K Number of clusters per node. ** @param depth Tree depth. ** @return a new HIKM tree representing the clustering. ** ** @remark @a depth cannot be smaller than 1. **/ void vl_hikm_init (VlHIKMTree *f, vl_size M, vl_size K, vl_size depth) { assert(depth > 0) ; assert(M > 0) ; assert(K > 0) ; xdelete (f -> root) ; f->root = 0; f->M = M ; f->K = K ; f->depth = depth ; } /** ------------------------------------------------------------------ ** @brief Train HIKM tree ** @param f HIKM tree. ** @param data Data to cluster. ** @param N Number of data. **/ void vl_hikm_train (VlHIKMTree *f, vl_uint8 const *data, vl_size N) { f->root= xmeans (f, data, N, VL_MIN(f->K, N), f->depth) ; } /** ------------------------------------------------------------------ ** @brief Project data down HIKM tree ** @param f HIKM tree. ** @param asgn Path down the tree (out). ** @param data Data to project. ** @param N Number of data. ** ** The function writes to @a asgn the path of the data @a data ** down the HIKM tree @a f. The parameter @a asgn must point to ** an array of @c M by @c N elements, where @c M is the depth of ** the HIKM tree and @c N is the number of data point to process. **/ void vl_hikm_push (VlHIKMTree *f, vl_uint32 *asgn, vl_uint8 const *data, vl_size N) { vl_uindex i, d ; vl_size M = vl_hikm_get_ndims (f) ; vl_size depth = vl_hikm_get_depth (f) ; /* for each datum */ for(i = 0 ; i < N ; i++) { VlHIKMNode *node = f->root ; d = 0 ; while (node) { vl_uint32 best ; vl_ikm_push (node->filter, &best, data + i * M, 1) ; asgn[i * depth + d] = best ; ++ d ; if (!node->children) break ; node = node->children [best] ; } } } /* ---------------------------------------------------------------- */ /* Setters and getters */ /* ---------------------------------------------------------------- */ /** @brief Get data dimensionality ** @param f HIKM tree. ** @return data dimensionality. **/ vl_size vl_hikm_get_ndims (VlHIKMTree const* f) { return f->M ; } /** @brief Get K ** @param f HIKM tree. ** @return K. **/ vl_size vl_hikm_get_K (VlHIKMTree const *f) { return f->K ; } /** @brief Get depth ** @param f HIKM tree. ** @return depth. **/ vl_size vl_hikm_get_depth (VlHIKMTree const *f) { return f->depth ; } /** @brief Get verbosity level ** @param f HIKM tree. ** @return verbosity level. **/ int vl_hikm_get_verbosity (VlHIKMTree const *f) { return f->verb ; } /** @brief Get maximum number of iterations ** @param f HIKM tree. ** @return maximum number of iterations. **/ vl_size vl_hikm_get_max_niters (VlHIKMTree const *f) { return f-> max_niters ; } /** @brief Get maximum number of iterations ** @param f HIKM tree. ** @return maximum number of iterations. **/ VlHIKMNode const * vl_hikm_get_root (VlHIKMTree const *f) { return f->root ; } /** @brief Set verbosity level ** @param f HIKM tree. ** @param verb verbosity level. **/ void vl_hikm_set_verbosity (VlHIKMTree *f, int verb) { f->verb = verb ; } /** @brief Set maximum number of iterations ** @param f HIKM tree. ** @param max_niters maximum number of iterations. **/ void vl_hikm_set_max_niters (VlHIKMTree *f, int max_niters) { f->max_niters = max_niters ; } colmap-3.9.1/src/thirdparty/VLFeat/hikmeans.h000066400000000000000000000042321454702036400210570ustar00rootroot00000000000000/** @file hikmeans.h ** @brief Hierarchical Integer K-Means Clustering ** @author Brian Fulkerson **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_HIKMEANS_H #define VL_HIKMEANS_H #include "generic.h" #include "ikmeans.h" struct _VLHIKMTree ; struct _VLHIKMNode ; /** @brief HIKM tree node ** ** The number of children @a K is not bigger than the @a K parameter ** of the HIKM tree. **/ typedef struct _VlHIKMNode { VlIKMFilt *filter ; /**< IKM filter for this node*/ struct _VlHIKMNode **children ; /**< Node children (if any) */ } VlHIKMNode ; /** @brief HIKM tree */ typedef struct _VlHIKMTree { vl_size M ; /**< IKM: data dimensionality */ vl_size K ; /**< IKM: K */ vl_size depth ; /**< Depth of the tree */ vl_size max_niters ; /**< IKM: maximum # of iterations */ int method ; /**< IKM: method */ int verb ; /**< Verbosity level */ VlHIKMNode * root; /**< Tree root node */ } VlHIKMTree ; /** @name Create and destroy ** @{ **/ VL_EXPORT VlHIKMTree *vl_hikm_new (int method) ; VL_EXPORT void vl_hikm_delete (VlHIKMTree *f) ; /** @} */ /** @name Retrieve data and parameters ** @{ **/ VL_EXPORT vl_size vl_hikm_get_ndims (VlHIKMTree const *f) ; VL_EXPORT vl_size vl_hikm_get_K (VlHIKMTree const *f) ; VL_EXPORT vl_size vl_hikm_get_depth (VlHIKMTree const *f) ; VL_EXPORT int vl_hikm_get_verbosity (VlHIKMTree const *f) ; VL_EXPORT vl_size vl_hikm_get_max_niters (VlHIKMTree const *f) ; VL_EXPORT VlHIKMNode const * vl_hikm_get_root (VlHIKMTree const *f) ; /** @} */ /** @name Set parameters ** @{ **/ VL_EXPORT void vl_hikm_set_verbosity (VlHIKMTree *f, int verb) ; VL_EXPORT void vl_hikm_set_max_niters (VlHIKMTree *f, int max_niters) ; /** @} */ /** @name Process data ** @{ **/ VL_EXPORT void vl_hikm_init (VlHIKMTree *f, vl_size M, vl_size K, vl_size depth) ; VL_EXPORT void vl_hikm_train (VlHIKMTree *f, vl_uint8 const *data, vl_size N) ; VL_EXPORT void vl_hikm_push (VlHIKMTree *f, vl_uint32 *asgn, vl_uint8 const *data, vl_size N) ; /** @} */ /* VL_HIKMEANS_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/hog.c000077500000000000000000001124641454702036400200420ustar00rootroot00000000000000/** @file hog.c ** @brief Histogram of Oriented Gradients (HOG) - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #include "hog.h" #include "mathop.h" #include /** @page hog Histogram of Oriented Gradients (HOG) features @author Andrea Vedaldi @ref hog.h implements the Histogram of Oriented Gradients (HOG) features in the variants of Dalal Triggs @cite{dalal05histograms} and of UOCTTI @cite{felzenszwalb09object}. Applications include object detection and deformable object detection. - @ref hog-overview - @ref hog-tech @section hog-overview Overview HOG is a standard image feature used, among others, in object detection and deformable object detection. It decomposes the image into square cells of a given size (typically eight pixels), compute a histogram of oriented gradient in each cell (similar to @ref sift), and then renormalizes the cells by looking into adjacent blocks. VLFeat implements two HOG variants: the original one of Dalal-Triggs @cite{dalal05histograms} and the one proposed in Felzenszwalb et al. @cite{felzenszwalb09object}. In order to use HOG, start by creating a new HOG object, set the desired parameters, pass a (color or grayscale) image, and read off the results. @code VlHog * hog = vl_hog_new(VlHogVariantDalalTriggs, numOrientations, VL_FALSE) ; vl_hog_put_image(hog, image, height, width, numChannels, cellSize) ; hogWidth = vl_hog_get_width(hog) ; hogHeight = vl_hog_get_height(hog) ; hogDimenison = vl_hog_get_dimension(hog) ; hogArray = vl_malloc(hogWidth*hogHeight*hogDimension*sizeof(float)) ; vl_hog_extract(hog, hogArray) ; vl_hog_delete(hog) ; @endcode HOG is a feature array of the dimension returned by ::vl_hog_get_width, ::vl_hog_get_height, with each feature (histogram) having dimension ::vl_hog_get_dimension. The array is stored in row major order, with the slowest varying dimension beying the dimension indexing the histogram elements. The number of entreis in the histogram as well as their meaning depends on the HOG variant and is detailed later. However, it is usually unnecessary to know such details. @ref hog.h provides support for creating an inconic representation of a HOG feature array: @code glyphSize = vl_hog_get_glyph_size(hog) ; imageHeight = glyphSize * hogArrayHeight ; imageWidth = glyphSize * hogArrayWidth ; image = vl_malloc(sizeof(float)*imageWidth*imageHeight) ; vl_hog_render(hog, image, hogArray) ; @endcode It is often convenient to mirror HOG features from left to right. This can be obtained by mirroring an array of HOG cells, but the content of each cell must also be rearranged. This can be done by the permutation obtaiend by ::vl_hog_get_permutation. Furthermore, @ref hog.h suppots computing HOG features not from images but from vector fields. @section hog-tech Technical details HOG divdes the input image into square cells of size @c cellSize, fitting as many cells as possible, filling the image domain from the upper-left corner down to the right one. For each row and column, the last cell is at least half contained in the image. More precisely, the number of cells obtained in this manner is: @code hogWidth = (width + cellSize/2) / cellSize ; hogHeight = (height + cellSize/2) / cellSize ; @endcode Then the image gradient @f$ \nabla \ell(x,y) @f$ is computed by using central difference (for colour image the channel with the largest gradient at that pixel is used). The gradient @f$ \nabla \ell(x,y) @f$ is assigned to one of @c 2*numOrientations orientation in the range @f$ [0,2\pi) @f$ (see @ref hog-conventions for details). Contributions are then accumulated by using bilinear interpolation to four neigbhour cells, as in @ref sift. This results in an histogram @f$h_d@f$ of dimension 2*numOrientations, called of @e directed orientations since it accounts for the direction as well as the orientation of the gradient. A second histogram @f$h_u@f$ of undirected orientations of half the size is obtained by folding @f$ h_d @f$ into two. Let a block of cell be a @f$ 2\times 2 @f$ sub-array of cells. Let the norm of a block be the @f$ l^2 @f$ norm of the stacking of the respective unoriented histogram. Given a HOG cell, four normalisation factors are then obtained as the inverse of the norm of the four blocks that contain the cell. For the Dalal-Triggs variant, each histogram @f$ h_d @f$ is copied four times, normalised using the four different normalisation factors, the four vectors are stacked, saturated at 0.2, and finally stored as the descriptor of the cell. This results in a @c numOrientations * 4 dimensional cell descriptor. Blocks are visited from left to right and top to bottom when forming the final descriptor. For the UOCCTI descriptor, the same is done for both the undirected as well as the directed orientation histograms. This would yield a dimension of @c 4*(2+1)*numOrientations elements, but the resulting vector is projected down to @c (2+1)*numOrientations elements by averaging corresponding histogram dimensions. This was shown to be an algebraic approximation of PCA for descriptors computed on natural images. In addition, for the UOCTTI variant the l1 norm of each of the four l2 normalised undirected histograms is computed and stored as additional four dimensions, for a total of @c 4+3*numOrientations dimensions. @subsection hog-conventions Conventions The orientation of a gradient is expressed as the angle it forms with the horizontal axis of the image. Angles are measured clock-wise (as the vertical image axis points downards), and the null angle corresponds to an horizontal vector pointing right. The quantized directed orientations are @f$ \mathrm{k} \pi / \mathrm{numOrientations} @f$, where @c k is an index that varies in the ingeger range @f$ \{0, \dots, 2\mathrm{numOrientations} - 1\} @f$. Note that the orientations capture the orientation of the gradeint; image edges would be oriented at 90 degrees from these. **/ /* ---------------------------------------------------------------- */ /** @brief Create a new HOG object ** @param variant HOG descriptor variant. ** @param numOrientations number of distinguished orientations. ** @param transposed wether images are transposed (column major). ** @return the new HOG object. ** ** The function creates a new HOG object to extract descriptors of ** the prescribed @c variant. The angular resolution is set by ** @a numOrientations, which specifies the number of undirected ** orientations. The object can work with column major images ** by setting @a transposed to true. **/ VlHog * vl_hog_new (VlHogVariant variant, vl_size numOrientations, vl_bool transposed) { vl_index o, k ; VlHog * self = vl_calloc(1, sizeof(VlHog)) ; assert(numOrientations >= 1) ; self->variant = variant ; self->numOrientations = numOrientations ; self->glyphSize = 21 ; self->transposed = transposed ; self->useBilinearOrientationAssigment = VL_FALSE ; self->orientationX = vl_malloc(sizeof(float) * self->numOrientations) ; self->orientationY = vl_malloc(sizeof(float) * self->numOrientations) ; /* Create a vector along the center of each orientation bin. These are used to map gradients to bins. If the image is transposed, then this can be adjusted here by swapping X and Y in these vectors. */ for(o = 0 ; o < (signed)self->numOrientations ; ++o) { double angle = o * VL_PI / self->numOrientations ; if (!self->transposed) { self->orientationX[o] = (float) cos(angle) ; self->orientationY[o] = (float) sin(angle) ; } else { self->orientationX[o] = (float) sin(angle) ; self->orientationY[o] = (float) cos(angle) ; } } /* If the number of orientation is equal to 9, one gets: Uoccti:: 18 directed orientations + 9 undirected orientations + 4 texture DalalTriggs:: 9 undirected orientations x 4 blocks. */ switch (self->variant) { case VlHogVariantUoctti: self->dimension = 3*self->numOrientations + 4 ; break ; case VlHogVariantDalalTriggs: self->dimension = 4*self->numOrientations ; break ; default: assert(0) ; } /* A permutation specifies how to permute elements in a HOG descriptor to flip it horizontally. Since the first orientation of index 0 points to the right, this must be swapped with orientation self->numOrientation that points to the left (for the directed case, and to itself for the undirected one). */ self->permutation = vl_malloc(self->dimension * sizeof(vl_index)) ; switch (self->variant) { case VlHogVariantUoctti: for(o = 0 ; o < (signed)self->numOrientations ; ++o) { vl_index op = self->numOrientations - o ; self->permutation[o] = op ; self->permutation[o + self->numOrientations] = (op + self->numOrientations) % (2*self->numOrientations) ; self->permutation[o + 2*self->numOrientations] = (op % self->numOrientations) + 2*self->numOrientations ; } for (k = 0 ; k < 4 ; ++k) { /* The texture features correspond to four displaced block around a cell. These permute with a lr flip as for DalalTriggs. */ vl_index blockx = k % 2 ; vl_index blocky = k / 2 ; vl_index q = (1 - blockx) + blocky * 2 ; self->permutation[k + self->numOrientations * 3] = q + self->numOrientations * 3 ; } break ; case VlHogVariantDalalTriggs: for(k = 0 ; k < 4 ; ++k) { /* Find the corresponding block. Blocks are listed in order 1,2,3,4,... from left to right and top to bottom */ vl_index blockx = k % 2 ; vl_index blocky = k / 2 ; vl_index q = (1 - blockx) + blocky * 2 ; for(o = 0 ; o < (signed)self->numOrientations ; ++o) { vl_index op = self->numOrientations - o ; self->permutation[o + k*self->numOrientations] = (op % self->numOrientations) + q*self->numOrientations ; } } break ; default: assert(0) ; } /* Create glyphs for representing the HOG features/ filters. The glyphs are simple bars, oriented orthogonally to the gradients to represent image edges. If the object is configured to work on transposed image, the glyphs images are also stored in column-major. */ self->glyphs = vl_calloc(self->glyphSize * self->glyphSize * self->numOrientations, sizeof(float)) ; #define atglyph(x,y,k) self->glyphs[(x) + self->glyphSize * (y) + self->glyphSize * self->glyphSize * (k)] for (o = 0 ; o < (signed)self->numOrientations ; ++o) { double angle = fmod(o * VL_PI / self->numOrientations + VL_PI/2, VL_PI) ; double x2 = self->glyphSize * cos(angle) / 2 ; double y2 = self->glyphSize * sin(angle) / 2 ; if (angle <= VL_PI / 4 || angle >= VL_PI * 3 / 4) { /* along horizontal direction */ double slope = y2 / x2 ; double offset = (1 - slope) * (self->glyphSize - 1) / 2 ; vl_index skip = (1 - fabs(cos(angle))) / 2 * self->glyphSize ; vl_index i, j ; for (i = skip ; i < (signed)self->glyphSize - skip ; ++i) { j = vl_round_d(slope * i + offset) ; if (! self->transposed) { atglyph(i,j,o) = 1 ; } else { atglyph(j,i,o) = 1 ; } } } else { /* along vertical direction */ double slope = x2 / y2 ; double offset = (1 - slope) * (self->glyphSize - 1) / 2 ; vl_index skip = (1 - sin(angle)) / 2 * self->glyphSize ; vl_index i, j ; for (j = skip ; j < (signed)self->glyphSize - skip; ++j) { i = vl_round_d(slope * j + offset) ; if (! self->transposed) { atglyph(i,j,o) = 1 ; } else { atglyph(j,i,o) = 1 ; } } } } return self ; } /* ---------------------------------------------------------------- */ /** @brief Delete a HOG object ** @param self HOG object to delete. **/ void vl_hog_delete (VlHog * self) { if (self->orientationX) { vl_free(self->orientationX) ; self->orientationX = NULL ; } if (self->orientationY) { vl_free(self->orientationY) ; self->orientationY = NULL ; } if (self->glyphs) { vl_free(self->glyphs) ; self->glyphs = NULL ; } if (self->permutation) { vl_free(self->permutation) ; self->permutation = NULL ; } if (self->hog) { vl_free(self->hog) ; self->hog = NULL ; } if (self->hogNorm) { vl_free(self->hogNorm) ; self->hogNorm = NULL ; } vl_free(self) ; } /* ---------------------------------------------------------------- */ /** @brief Get HOG glyph size ** @param self HOG object. ** @return size (height and width) of a glyph. **/ vl_size vl_hog_get_glyph_size (VlHog const * self) { return self->glyphSize ; } /* ---------------------------------------------------------------- */ /** @brief Get HOG left-right flip permutation ** @param self HOG object. ** @return left-right permutation. ** ** The function returns a pointer to an array @c permutation of ::vl_hog_get_dimension ** elements. Given a HOG descriptor (for a cell) @c hog, which is also ** a vector of ::vl_hog_get_dimension elements, the ** descriptor obtained for the same image flipped horizotnally is ** given by flippedHog[i] = hog[permutation[i]]. **/ vl_index const * vl_hog_get_permutation (VlHog const * self) { return self->permutation ; } /* ---------------------------------------------------------------- */ /** @brief Turn bilinear interpolation of assignments on or off ** @param self HOG object. ** @param x @c true if orientations should be assigned with bilinear interpolation. **/ void vl_hog_set_use_bilinear_orientation_assignments (VlHog * self, vl_bool x) { self->useBilinearOrientationAssigment = x ; } /** @brief Tell whether assignments use bilinear interpolation or not ** @param self HOG object. ** @return @c true if orientations are be assigned with bilinear interpolation. **/ vl_bool vl_hog_get_use_bilinear_orientation_assignments (VlHog const * self) { return self->useBilinearOrientationAssigment ; } /* ---------------------------------------------------------------- */ /** @brief Render a HOG descriptor to a glyph image ** @param self HOG object. ** @param image glyph image (output). ** @param descriptor HOG descriptor. ** @param width HOG descriptor width. ** @param height HOG descriptor height. ** ** The function renders the HOG descriptor or filter ** @a descriptor as an image (for visualization) and stores the result in ** the buffer @a image. This buffer ** must be an array of dimensions @c width*glyphSize ** by @c height*glyphSize elements, where @c glyphSize is ** obtained from ::vl_hog_get_glyph_size and is the size in pixels ** of the image element used to represent the descriptor of one ** HOG cell. **/ void vl_hog_render (VlHog const * self, float * image, float const * descriptor, vl_size width, vl_size height) { vl_index x, y, k, cx, cy ; vl_size hogStride = width * height ; assert(self) ; assert(image) ; assert(descriptor) ; assert(width > 0) ; assert(height > 0) ; for (y = 0 ; y < (signed)height ; ++y) { for (x = 0 ; x < (signed)width ; ++x) { float minWeight = 0 ; float maxWeight = 0 ; for (k = 0 ; k < (signed)self->numOrientations ; ++k) { float weight ; float const * glyph = self->glyphs + k * (self->glyphSize*self->glyphSize) ; float * glyphImage = image + self->glyphSize * x + y * width * (self->glyphSize*self->glyphSize) ; switch (self->variant) { case VlHogVariantUoctti: weight = descriptor[k * hogStride] + descriptor[(k + self->numOrientations) * hogStride] + descriptor[(k + 2 * self->numOrientations) * hogStride] ; break ; case VlHogVariantDalalTriggs: weight = descriptor[k * hogStride] + descriptor[(k + self->numOrientations) * hogStride] + descriptor[(k + 2 * self->numOrientations) * hogStride] + descriptor[(k + 3 * self->numOrientations) * hogStride] ; break ; default: abort() ; } maxWeight = VL_MAX(weight, maxWeight) ; minWeight = VL_MIN(weight, minWeight); for (cy = 0 ; cy < (signed)self->glyphSize ; ++cy) { for (cx = 0 ; cx < (signed)self->glyphSize ; ++cx) { *glyphImage++ += weight * (*glyph++) ; } glyphImage += (width - 1) * self->glyphSize ; } } /* next orientation */ { float * glyphImage = image + self->glyphSize * x + y * width * (self->glyphSize*self->glyphSize) ; for (cy = 0 ; cy < (signed)self->glyphSize ; ++cy) { for (cx = 0 ; cx < (signed)self->glyphSize ; ++cx) { float value = *glyphImage ; *glyphImage++ = VL_MAX(minWeight, VL_MIN(maxWeight, value)) ; } glyphImage += (width - 1) * self->glyphSize ; } } ++ descriptor ; } /* next column of cells (x) */ } /* next row of cells (y) */ } /* ---------------------------------------------------------------- */ /** @brief Get the dimension of the HOG features ** @param self HOG object. ** @return imension of a HOG cell descriptors. **/ vl_size vl_hog_get_dimension (VlHog const * self) { return self->dimension ; } /** @brief Get the width of the HOG cell array ** @param self HOG object. ** @return number of HOG cells in the horizontal direction. **/ vl_size vl_hog_get_width (VlHog * self) { return self->hogWidth ; } /** @brief Get the height of the HOG cell array ** @param self HOG object. ** @return number of HOG cells in the vertical direction. **/ vl_size vl_hog_get_height (VlHog * self) { return self->hogHeight ; } /* ---------------------------------------------------------------- */ /** @internal @brief Prepare internal buffers ** @param self HOG object. ** @param width image width. ** @param height image height. ** @param cellSize size of a HOG cell. **/ static void vl_hog_prepare_buffers (VlHog * self, vl_size width, vl_size height, vl_size cellSize) { vl_size hogWidth = (width + cellSize/2) / cellSize ; vl_size hogHeight = (height + cellSize/2) / cellSize ; assert(width > 3) ; assert(height > 3) ; assert(hogWidth > 0) ; assert(hogHeight > 0) ; if (self->hog && self->hogWidth == hogWidth && self->hogHeight == hogHeight) { /* a suitable buffer is already allocated */ memset(self->hog, 0, sizeof(float) * hogWidth * hogHeight * self->numOrientations * 2) ; memset(self->hogNorm, 0, sizeof(float) * hogWidth * hogHeight) ; return ; } if (self->hog) { vl_free(self->hog) ; self->hog = NULL ; } if (self->hogNorm) { vl_free(self->hogNorm) ; self->hogNorm = NULL ; } self->hog = vl_calloc(hogWidth * hogHeight * self->numOrientations * 2, sizeof(float)) ; self->hogNorm = vl_calloc(hogWidth * hogHeight, sizeof(float)) ; self->hogWidth = hogWidth ; self->hogHeight = hogHeight ; } /* ---------------------------------------------------------------- */ /** @brief Process features starting from an image ** @param self HOG object. ** @param image image to process. ** @param width image width. ** @param height image height. ** @param numChannels number of image channles. ** @param cellSize size of a HOG cell. ** ** The buffer @c hog must be a three-dimensional array. ** The first two dimensions are @c (width + cellSize/2)/cellSize and ** @c (height + cellSize/2)/cellSize, where divisions are integer. ** This is approximately @c width/cellSize and @c height/cellSize, ** adjusted so that the last cell is at least half contained in the ** image. ** ** The image @c width and @c height must be not smaller than three ** pixels and not smaller than @c cellSize. **/ void vl_hog_put_image (VlHog * self, float const * image, vl_size width, vl_size height, vl_size numChannels, vl_size cellSize) { vl_size hogStride ; vl_size channelStride = width * height ; vl_index x, y ; vl_uindex k ; assert(self) ; assert(image) ; /* clear features */ vl_hog_prepare_buffers(self, width, height, cellSize) ; hogStride = self->hogWidth * self->hogHeight ; #define at(x,y,k) (self->hog[(x) + (y) * self->hogWidth + (k) * hogStride]) /* compute gradients and map the to HOG cells by bilinear interpolation */ for (y = 1 ; y < (signed)height - 1 ; ++y) { for (x = 1 ; x < (signed)width - 1 ; ++x) { float gradx = 0 ; float grady = 0 ; float gradNorm ; float orientationWeights [2] = {-1, -1} ; vl_index orientationBins [2] = {-1, -1} ; vl_index orientation = 0 ; float hx, hy, wx1, wx2, wy1, wy2 ; vl_index binx, biny, o ; /* Compute the gradient at (x,y). The image channel with the maximum gradient at each location is selected. */ { float const * iter = image + y * width + x ; float gradNorm2 = 0 ; for (k = 0 ; k < numChannels ; ++k) { float gradx_ = *(iter + 1) - *(iter - 1) ; float grady_ = *(iter + width) - *(iter - width) ; float gradNorm2_ = gradx_ * gradx_ + grady_ * grady_ ; if (gradNorm2_ > gradNorm2) { gradx = gradx_ ; grady = grady_ ; gradNorm2 = gradNorm2_ ; } iter += channelStride ; } gradNorm = sqrtf(gradNorm2) ; } /* Map the gradient to the closest and second closets orientation bins. There are numOrientations orientation in the interval [0,pi). The next numOriantations are the symmetric ones, for a total of 2*numOrientation directed orientations. */ for (k = 0 ; k < self->numOrientations ; ++k) { float orientationScore_ = gradx * self->orientationX[k] + grady * self->orientationY[k] ; vl_index orientationBin_ = k ; if (orientationScore_ < 0) { orientationScore_ = - orientationScore_ ; orientationBin_ += self->numOrientations ; } if (orientationScore_ > orientationWeights[0]) { orientationBins[1] = orientationBins[0] ; orientationWeights[1] = orientationWeights[0] ; orientationBins[0] = orientationBin_ ; ; orientationWeights[0] = orientationScore_ ; } else if (orientationScore_ > orientationWeights[1]) { orientationBins[1] = orientationBin_ ; orientationWeights[1] = orientationScore_ ; } } if (self->useBilinearOrientationAssigment) { /* min(1.0,...) guards against small overflows causing NaNs */ float angle0 = acosf(VL_MIN(orientationWeights[0] / VL_MAX(gradNorm, 1e-10),1.0)) ; orientationWeights[1] = angle0 / (VL_PI / self->numOrientations) ; orientationWeights[0] = 1 - orientationWeights[1] ; } else { orientationWeights[0] = 1 ; orientationBins[1] = -1 ; } for (o = 0 ; o < 2 ; ++o) { float ow ; /* Accumulate the gradient. hx is the distance of the pixel x to the cell center at its left, in units of cellSize. With this parametrixation, a pixel on the cell center has hx = 0, which gradually increases to 1 moving to the next center. */ orientation = orientationBins[o] ; if (orientation < 0) continue ; /* (x - (w-1)/2) / w = (x + 0.5)/w - 0.5 */ hx = (x + 0.5) / cellSize - 0.5 ; hy = (y + 0.5) / cellSize - 0.5 ; binx = vl_floor_f(hx) ; biny = vl_floor_f(hy) ; wx2 = hx - binx ; wy2 = hy - biny ; wx1 = 1.0 - wx2 ; wy1 = 1.0 - wy2 ; ow = orientationWeights[o] ; /*VL_PRINTF("%d %d - %d %d %f %f - %f %f %f %f - %d \n ",x,y,binx,biny,hx,hy,wx1,wx2,wy1,wy2,o);*/ if (binx >= 0 && biny >=0) { at(binx,biny,orientation) += gradNorm * ow * wx1 * wy1 ; } if (binx < (signed)self->hogWidth - 1 && biny >=0) { at(binx+1,biny,orientation) += gradNorm * ow * wx2 * wy1 ; } if (binx < (signed)self->hogWidth - 1 && biny < (signed)self->hogHeight - 1) { at(binx+1,biny+1,orientation) += gradNorm * ow * wx2 * wy2 ; } if (binx >= 0 && biny < (signed)self->hogHeight - 1) { at(binx,biny+1,orientation) += gradNorm * ow * wx1 * wy2 ; } } /* next o */ } /* next x */ } /* next y */ } /* ---------------------------------------------------------------- */ /** @brief Process features starting from a field in polar notation ** @param self HOG object. ** @param modulus image gradient modulus. ** @param angle image gradient angle. ** @param directed wrap the gradient angles at 2pi (directed) or pi (undirected). ** @param width image width. ** @param height image height. ** @param cellSize size of a HOG cell. ** ** The function behaves like ::vl_hog_put_image, but foregoes the internal ** computation of the gradient field, allowing the user to specify ** their own. Angles are measure clockwise, the y axis pointing downwards, ** starting from the x axis (pointing to the right). **/ void vl_hog_put_polar_field (VlHog * self, float const * modulus, float const * angle, vl_bool directed, vl_size width, vl_size height, vl_size cellSize) { vl_size hogStride ; vl_index x, y, o ; vl_index period = self->numOrientations * (directed ? 2 : 1) ; double angleStep = VL_PI / self->numOrientations ; assert(self) ; assert(modulus) ; assert(angle) ; /* clear features */ vl_hog_prepare_buffers(self, width, height, cellSize) ; hogStride = self->hogWidth * self->hogHeight ; #define at(x,y,k) (self->hog[(x) + (y) * self->hogWidth + (k) * hogStride]) #define atNorm(x,y) (self->hogNorm[(x) + (y) * self->hogWidth]) /* fill HOG cells from gradient field */ for (y = 0 ; y < (signed)height ; ++y) { for (x = 0 ; x < (signed)width ; ++x) { float ho, hx, hy, wo1, wo2, wx1, wx2, wy1, wy2 ; vl_index bino, binx, biny ; float orientationWeights [2] = {0,0} ; vl_index orientationBins [2] = {-1,-1} ; vl_index orientation = 0 ; float thisAngle = *angle++ ; float thisModulus = *modulus++ ; if (thisModulus <= 0.0f) continue ; /* (x - (w-1)/2) / w = (x + 0.5)/w - 0.5 */ ho = (float)thisAngle / angleStep ; bino = vl_floor_f(ho) ; wo2 = ho - bino ; wo1 = 1.0f - wo2 ; while (bino < 0) { bino += self->numOrientations * 2 ; } if (self->useBilinearOrientationAssigment) { orientationBins[0] = bino % period ; orientationBins[1] = (bino + 1) % period ; orientationWeights[0] = wo1 ; orientationWeights[1] = wo2 ; } else { orientationBins[0] = (bino + ((wo1 > wo2) ? 0 : 1)) % period ; orientationWeights[0] = 1 ; orientationBins[1] = -1 ; } for (o = 0 ; o < 2 ; ++o) { /* Accumulate the gradient. hx is the distance of the pixel x to the cell center at its left, in units of cellSize. With this parametrixation, a pixel on the cell center has hx = 0, which gradually increases to 1 moving to the next center. */ orientation = orientationBins[o] ; if (orientation < 0) continue ; hx = (x + 0.5) / cellSize - 0.5 ; hy = (y + 0.5) / cellSize - 0.5 ; binx = vl_floor_f(hx) ; biny = vl_floor_f(hy) ; wx2 = hx - binx ; wy2 = hy - biny ; wx1 = 1.0 - wx2 ; wy1 = 1.0 - wy2 ; wx1 *= orientationWeights[o] ; wx2 *= orientationWeights[o] ; wy1 *= orientationWeights[o] ; wy2 *= orientationWeights[o] ; /*VL_PRINTF("%d %d - %d %d %f %f - %f %f %f %f - %d \n ",x,y,binx,biny,hx,hy,wx1,wx2,wy1,wy2,o);*/ if (binx >= 0 && biny >=0) { at(binx,biny,orientation) += thisModulus * wx1 * wy1 ; } if (binx < (signed)self->hogWidth - 1 && biny >=0) { at(binx+1,biny,orientation) += thisModulus * wx2 * wy1 ; } if (binx < (signed)self->hogWidth - 1 && biny < (signed)self->hogHeight - 1) { at(binx+1,biny+1,orientation) += thisModulus * wx2 * wy2 ; } if (binx >= 0 && biny < (signed)self->hogHeight - 1) { at(binx,biny+1,orientation) += thisModulus * wx1 * wy2 ; } } /* next o */ } /* next x */ } /* next y */ } /* ---------------------------------------------------------------- */ /** @brief Extract HOG features ** @param self HOG object. ** @param features HOG features (output). ** ** This method is called after ::vl_hog_put_image or ::vl_hog_put_polar_field ** in order to retrieve the computed HOG features. The buffer @c features must have the dimensions returned by ** ::vl_hog_get_width, ::vl_hog_get_height, and ::vl_hog_get_dimension. **/ void vl_hog_extract (VlHog * self, float * features) { vl_index x, y ; vl_uindex k ; vl_size hogStride = self->hogWidth * self->hogHeight ; assert(features) ; #define at(x,y,k) (self->hog[(x) + (y) * self->hogWidth + (k) * hogStride]) #define atNorm(x,y) (self->hogNorm[(x) + (y) * self->hogWidth]) /* Compute the squared L2 norm of the unoriented version of each HOG cell histogram. The unoriented version is obtained by folding the 2*numOrientations compotnent into numOrientations only. */ { float const * iter = self->hog ; for (k = 0 ; k < self->numOrientations ; ++k) { float * niter = self->hogNorm ; float * niterEnd = self->hogNorm + self->hogWidth * self->hogHeight ; vl_size stride = self->hogWidth*self->hogHeight*self->numOrientations ; while (niter != niterEnd) { float h1 = *iter ; float h2 = *(iter + stride) ; float h = h1 + h2 ; *niter += h * h ; niter++ ; iter++ ; } } } /* HOG block-normalisation. The Dalal-Triggs implementation computes a normalized descriptor for each block of 2x2 cells, by stacking the histograms of each cell into a vector and L2-normalizing and truncating the result. Each block-level descriptor is then decomposed back into cells and corresponding parts are stacked into cell-level descritpors. Each HOG cell is contained in exactly four 2x2 cell blocks. For example, the cell number 5 in the following figure is contained in blocks 1245, 2356, 4578, 5689: +---+---+---+ | 1 | 2 | 3 | +---+---+---+ | 4 | 5 | 6 | +---+---+---+ | 7 | 8 | 9 | +---+---+---+ Hence, when block-level descriptors are decomposed back into cells, each cell receives contributions from four blocks. So, if each cell started with a D-dimensional histogram, it ends up with a 4D dimesional descriptor vector. Note however that this is just a convenient way of rewriting the blocks as per-cell contributions, but the block information is unchanged. In particular, barring boundary effects, in an array of H x W cells there are approximately HW blocks; hence the L2 norm of all the blocks stacked is approximately HW (because individual blocks are L2-normalized). Since this does not change in the final HOG descriptor, the L2 norm of the HOG descriptor of an image should be approximately the same as the area of the image divided by the area of a HOG cell. This can be used as a sanity check. The UoCTTI variant differs in some non-negligible ways. First, it includes both oriented and unoriented histograms, as well as four components capturing texture. Second, and most importantly, it merges the four chunks of block-level descirptors landing in each cell into one by taking their average. This makes sense because, ultimately, these four sub-descriptors are identical to the original cell histogram, just with four different normalisations applied. */ { float const * iter = self->hog ; for (y = 0 ; y < (signed)self->hogHeight ; ++y) { for (x = 0 ; x < (signed)self->hogWidth ; ++x) { /* norm of upper-left, upper-right, ... cells */ vl_index xm = VL_MAX(x - 1, 0) ; vl_index xp = VL_MIN(x + 1, (signed)self->hogWidth - 1) ; vl_index ym = VL_MAX(y - 1, 0) ; vl_index yp = VL_MIN(y + 1, (signed)self->hogHeight - 1) ; double norm1 = atNorm(xm,ym) ; double norm2 = atNorm(x,ym) ; double norm3 = atNorm(xp,ym) ; double norm4 = atNorm(xm,y) ; double norm5 = atNorm(x,y) ; double norm6 = atNorm(xp,y) ; double norm7 = atNorm(xm,yp) ; double norm8 = atNorm(x,yp) ; double norm9 = atNorm(xp,yp) ; double factor1, factor2, factor3, factor4 ; double t1 = 0 ; double t2 = 0 ; double t3 = 0 ; double t4 = 0 ; float * oiter = features + x + self->hogWidth * y ; /* each factor is the inverse of the l2 norm of one of the 2x2 blocks surrounding cell x,y */ #if 0 if (self->transposed) { /* if the image is transposed, y and x are swapped */ factor1 = 1.0 / VL_MAX(sqrt(norm1 + norm2 + norm4 + norm5), 1e-10) ; factor3 = 1.0 / VL_MAX(sqrt(norm2 + norm3 + norm5 + norm6), 1e-10) ; factor2 = 1.0 / VL_MAX(sqrt(norm4 + norm5 + norm7 + norm8), 1e-10) ; factor4 = 1.0 / VL_MAX(sqrt(norm5 + norm6 + norm8 + norm9), 1e-10) ; } else { factor1 = 1.0 / VL_MAX(sqrt(norm1 + norm2 + norm4 + norm5), 1e-10) ; factor2 = 1.0 / VL_MAX(sqrt(norm2 + norm3 + norm5 + norm6), 1e-10) ; factor3 = 1.0 / VL_MAX(sqrt(norm4 + norm5 + norm7 + norm8), 1e-10) ; factor4 = 1.0 / VL_MAX(sqrt(norm5 + norm6 + norm8 + norm9), 1e-10) ; } #else /* as implemented in UOCTTI code */ if (self->transposed) { /* if the image is transposed, y and x are swapped */ factor1 = 1.0 / sqrt(norm1 + norm2 + norm4 + norm5 + 1e-4) ; factor3 = 1.0 / sqrt(norm2 + norm3 + norm5 + norm6 + 1e-4) ; factor2 = 1.0 / sqrt(norm4 + norm5 + norm7 + norm8 + 1e-4) ; factor4 = 1.0 / sqrt(norm5 + norm6 + norm8 + norm9 + 1e-4) ; } else { factor1 = 1.0 / sqrt(norm1 + norm2 + norm4 + norm5 + 1e-4) ; factor2 = 1.0 / sqrt(norm2 + norm3 + norm5 + norm6 + 1e-4) ; factor3 = 1.0 / sqrt(norm4 + norm5 + norm7 + norm8 + 1e-4) ; factor4 = 1.0 / sqrt(norm5 + norm6 + norm8 + norm9 + 1e-4) ; } #endif for (k = 0 ; k < self->numOrientations ; ++k) { double ha = iter[hogStride * k] ; double hb = iter[hogStride * (k + self->numOrientations)] ; double hc ; double ha1 = factor1 * ha ; double ha2 = factor2 * ha ; double ha3 = factor3 * ha ; double ha4 = factor4 * ha ; double hb1 = factor1 * hb ; double hb2 = factor2 * hb ; double hb3 = factor3 * hb ; double hb4 = factor4 * hb ; double hc1 = ha1 + hb1 ; double hc2 = ha2 + hb2 ; double hc3 = ha3 + hb3 ; double hc4 = ha4 + hb4 ; ha1 = VL_MIN(0.2, ha1) ; ha2 = VL_MIN(0.2, ha2) ; ha3 = VL_MIN(0.2, ha3) ; ha4 = VL_MIN(0.2, ha4) ; hb1 = VL_MIN(0.2, hb1) ; hb2 = VL_MIN(0.2, hb2) ; hb3 = VL_MIN(0.2, hb3) ; hb4 = VL_MIN(0.2, hb4) ; hc1 = VL_MIN(0.2, hc1) ; hc2 = VL_MIN(0.2, hc2) ; hc3 = VL_MIN(0.2, hc3) ; hc4 = VL_MIN(0.2, hc4) ; t1 += hc1 ; t2 += hc2 ; t3 += hc3 ; t4 += hc4 ; switch (self->variant) { case VlHogVariantUoctti : ha = 0.5 * (ha1 + ha2 + ha3 + ha4) ; hb = 0.5 * (hb1 + hb2 + hb3 + hb4) ; hc = 0.5 * (hc1 + hc2 + hc3 + hc4) ; *oiter = ha ; *(oiter + hogStride * self->numOrientations) = hb ; *(oiter + 2 * hogStride * self->numOrientations) = hc ; break ; case VlHogVariantDalalTriggs : *oiter = hc1 ; *(oiter + hogStride * self->numOrientations) = hc2 ; *(oiter + 2 * hogStride * self->numOrientations) = hc3 ; *(oiter + 3 * hogStride * self->numOrientations) = hc4 ; break ; } oiter += hogStride ; } /* next orientation */ switch (self->variant) { case VlHogVariantUoctti : oiter += 2 * hogStride * self->numOrientations ; *oiter = (1.0f/sqrtf(18.0f)) * t1 ; oiter += hogStride ; *oiter = (1.0f/sqrtf(18.0f)) * t2 ; oiter += hogStride ; *oiter = (1.0f/sqrtf(18.0f)) * t3 ; oiter += hogStride ; *oiter = (1.0f/sqrtf(18.0f)) * t4 ; oiter += hogStride ; break ; case VlHogVariantDalalTriggs : break ; } ++iter ; } /* next x */ } /* next y */ } /* block normalization */ } colmap-3.9.1/src/thirdparty/VLFeat/hog.h000066400000000000000000000053301454702036400200350ustar00rootroot00000000000000/** @file hog.h ** @brief Histogram of Oriented Gradients (@ref hog) ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_HOG_H #define VL_HOG_H #include "generic.h" enum VlHogVariant_ { VlHogVariantDalalTriggs, VlHogVariantUoctti } ; typedef enum VlHogVariant_ VlHogVariant ; struct VlHog_ { VlHogVariant variant ; vl_size dimension ; vl_size numOrientations ; vl_bool transposed ; vl_bool useBilinearOrientationAssigment ; /* left-right flip permutation */ vl_index * permutation ; /* glyphs */ float * glyphs ; vl_size glyphSize ; /* helper vectors */ float * orientationX ; float * orientationY ; /* buffers */ float * hog ; float * hogNorm ; vl_size hogWidth ; vl_size hogHeight ; } ; typedef struct VlHog_ VlHog ; VL_EXPORT VlHog * vl_hog_new (VlHogVariant variant, vl_size numOrientations, vl_bool transposed) ; VL_EXPORT void vl_hog_delete (VlHog * self) ; VL_EXPORT void vl_hog_process (VlHog * self, float * features, float const * image, vl_size width, vl_size height, vl_size numChannels, vl_size cellSize) ; VL_EXPORT void vl_hog_put_image (VlHog * self, float const * image, vl_size width, vl_size height, vl_size numChannels, vl_size cellSize) ; VL_EXPORT void vl_hog_put_polar_field (VlHog * self, float const * modulus, float const * angle, vl_bool directed, vl_size width, vl_size height, vl_size cellSize) ; VL_EXPORT void vl_hog_extract (VlHog * self, float * features) ; VL_EXPORT vl_size vl_hog_get_height (VlHog * self) ; VL_EXPORT vl_size vl_hog_get_width (VlHog * self) ; VL_EXPORT void vl_hog_render (VlHog const * self, float * image, float const * features, vl_size width, vl_size height) ; VL_EXPORT vl_size vl_hog_get_dimension (VlHog const * self) ; VL_EXPORT vl_index const * vl_hog_get_permutation (VlHog const * self) ; VL_EXPORT vl_size vl_hog_get_glyph_size (VlHog const * self) ; VL_EXPORT vl_bool vl_hog_get_use_bilinear_orientation_assignments (VlHog const * self) ; VL_EXPORT void vl_hog_set_use_bilinear_orientation_assignments (VlHog * self, vl_bool x) ; /* VL_HOG_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/homkermap.c000077500000000000000000000460701454702036400212470ustar00rootroot00000000000000/** @file homkermap.c ** @brief Homogeneous kernel map - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. Copyright (C) 2013 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file homkermap.h @page homkermap Homogeneous kernel map @author Andrea Vedaldi @tableofcontents @ref homkermap.h implements the homogeneous kernel maps introduced in @cite{vedaldi10efficient},@cite{vedaldi12efficient}. Such maps are efficient linear representations of popular kernels such as the intersection, $\chi^2$, and Jensen-Shannon ones. @section homkermap-starting Getting started The homogeneous kernel map is implemented as an object of type ::VlHomogeneousKernelMap. To use thois object, first create an instance by using ::vl_homogeneouskernelmap_new, then use ::vl_homogeneouskernelmap_evaluate_d or ::vl_homogeneouskernelmap_evaluate_f (depdening on whether the data is @c double or @c float) to compute the feature map $ \Psi(x) $. When done, dispose of the object by calling ::vl_homogeneouskernelmap_delete. @code double gamma = 1.0 ; int order = 1 ; double period = -1 ; // use default double psi [3] ; vl_size psiStride = 1 ; double x = 0.5 ; VlHomogeneousKernelMap * hom = vl_homogeneouskernelmap_new( VlHomogeneousKernelChi2, gamma, order, period, VlHomogeneousKernelMapWindowRectangular) ; vl_homogeneouskernelmap_evaluate_d(hom, psi, psiStride, x) ; vl_homogeneouskernelmap_delete(x) ; @endcode The constructor ::vl_homogeneouskernelmap_new takes the kernel type @c kernel (see ::VlHomogeneousKernelType), the homogeneity order @c gamma (use one for the standard $1$-homogeneous kernels), the approximation order @c order (usually order one is enough), the period @a period (use a negative value to use the default period), and a window type @c window (use ::VlHomogeneousKernelMapWindowRectangular if unsure). The approximation order trades off the quality and dimensionality of the approximation. The resulting feature map $ \Psi(x) $, computed by ::vl_homogeneouskernelmap_evaluate_d or ::vl_homogeneouskernelmap_evaluate_f , is 2*order+1 dimensional. The code pre-computes the map $ \Psi(x) $ for efficient evaluation. The table spans values of $ x $ in the range $[2^{-20}, 2^{8}) $. In particular, values smaller than $ 2^{-20} $ are treated as zeroes (which results in a null feature). @section homkermap-fundamentals Fundamentals The homogeneous kernel map is a finite dimensional linear approximation of homogeneous kernels, including the intersection, $\chi^2$, and Jensen-Shannon kernels. These kernels are frequently used in computer vision applications because they are particular suited to data in the format of histograms, which includes many common visual descriptors. Let $x,y \in \mathbb{R}_+$ be non-negative scalars and let $k(x,y) \in \mathbb{R}$ be an homogeneous kernel such as the $\chi^2$ and or the intersection ones: @f[ k_{\mathrm{inters}}(x,y) = \min\{x, y\}, \quad k_{\chi^2}(x,y) = 2 \frac{(x - y)^2}{x+y}. @f] For vectorial data $ \mathbf{x},\mathbf{y} \in \mathbb{R}_+^d $, the homogeneous kernels is defined as an additive combination of scalar kernels $K(\mathbf{x},\mathbf{y}) = \sum_{i=1}^d k(x_i,y_i)$. The homogeneous kernel map of order $n$ is a vector function $\Psi(x) \in \mathbb{R}^{2n+1}$ such that, for any choice of $x, y \in \mathbb{R}_+$, the following approximation holds: @f[ k(x,y) \approx \langle \Psi(x), \Psi(y) \rangle. @f] Given the feature map for the scalar case, the corresponding feature map $\Psi(\mathbf{x})$ for the vectorial case is obtained by stacking $[\Psi(x_1), \dots, \Psi(x_n)]$. Note that the stacked feature $\Psi(\mathbf{x})$ has dimension $d(2n+1)$. Using linear analysis tools (e.g. a linear support vector machine) on top of dataset that has been encoded by the homogeneous kernel map is therefore approximately equivalent to using a method based on the corresponding non-linear kernel. @subsection homkermap-overview-negative Extension to the negative reals Any positive (semi-)definite kernel $k(x,y)$ defined on the non-negative reals $x,y \in \mathbb{R}_+$ can be extended to the entire real line by using the definition: @f[ k_\pm(x,y) = \operatorname{sign}(x) \operatorname{sign}(y) k(|x|,|y|). @f] The homogeneous kernel map implements this extension by defining $\Psi_\pm(x) = \operatorname{sign}(x) \Psi(|x|)$. Note that other extensions are possible, such as @f[ k_\pm(x,y) = H(xy) \operatorname{sign}(y) k(|x|,|y|) @f] where $H$ is the Heaviside function, but may result in higher dimensional feature maps. @subsection homkermap-overview-homogeneity Homogeneity degree Any (1-)homogeneous kernel $k_1(x,y)$ can be extended to a so called $\gamma$-homgeneous kernel $k_\gamma(x,y)$ by the definition @f[ k_\gamma(x,y) = (xy)^{\frac{\gamma}{2}} \frac{k_1(x,y)}{\sqrt{xy}} @f] Smaller values of $\gamma$ enhance the kernel non-linearity and are sometimes beneficial in applications (see @cite{vedaldi10efficient},@cite{vedaldi12efficient} for details). @subsection homkermap-overview-window Windowing and period This section discusses aspects of the homogeneous kernel map which are more technical and may be skipped. The homogeneous kernel map approximation is based on periodizing the kernel; given the kernel signature @f[ \mathcal{K}(\lambda) = k(e^{\frac{\lambda}{2}}, e^{-\frac{\lambda}{2}}) @f] the homogeneous kernel map is a feature map for the windowed and periodized kernel whose signature is given by @f[ \hat{\mathcal{K}}(\lambda) = \sum_{i=-\infty}^{+\infty} \mathcal{K}(\lambda + k \Lambda) W(\lambda + k \Lambda) @f] where $W(\lambda)$ is a windowing function and $\Lambda$ is the period. This implementation of the homogeneous kernel map supports the use of a uniform window ($ W(\lambda) = 1 $) or of a rectangular window ($ W(\lambda) = \operatorname{rect}(\lambda/\Lambda) $). Note that $ \lambda = \log(y/x) $ is equal to the logarithmic ratio of the arguments of the kernel. Empirically, the rectangular window seems to have a slight edge in applications. @section homkermap-details Implementation details This implementation uses the expressions given in @cite{vedaldi10efficient},@cite{vedaldi11efficient} to compute in closed form the maps $\Psi(x)$ for the supported kernel types. For efficiency reasons, it precomputes $\Psi(x)$ for a large range of values of the argument when the homogeneous kernel map object is created. The internal table stores $\Psi(x) \in \mathbb{R}^{2n+1}$ by sampling $x\geq 0$. This uses the internal decomposition of IEEE floating point representations (@c float and @c double) in mantissa and exponent:
  x = mantissa * (2**exponent),
  minExponent <= exponent <= maxExponent,
  1 <= matnissa < 2.
Each octave is further sampled in @c numSubdivisions sublevels. When the map $\Psi(x)$ is evaluated, @c x is decomposed again into exponent and mantissa to index the table. The output is obtained by bilinear interpolation from the appropriate table entries. **/ /* ---------------------------------------------------------------- */ #ifndef VL_HOMKERMAP_INSTANTIATING /* ---------------------------------------------------------------- */ #include "homkermap.h" #include "mathop.h" #include struct _VlHomogeneousKernelMap { VlHomogeneousKernelType kernelType ; double gamma ; VlHomogeneousKernelMapWindowType windowType ; vl_size order ; double period ; vl_size numSubdivisions ; double subdivision ; vl_index minExponent ; vl_index maxExponent ; double * table ; } ; /** @internal @brief Sample the kernel specturm ** @param self homogeneous kernel map. ** @param omega sampling frequency. ** @return the spectrum sampled at @a omega. **/ VL_INLINE double vl_homogeneouskernelmap_get_spectrum (VlHomogeneousKernelMap const * self, double omega) { assert (self) ; switch (self->kernelType) { case VlHomogeneousKernelIntersection: return (2.0 / VL_PI) / (1 + 4 * omega*omega) ; case VlHomogeneousKernelChi2: return 2.0 / (exp(VL_PI * omega) + exp(-VL_PI * omega)) ; case VlHomogeneousKernelJS: return (2.0 / log(4.0)) * 2.0 / (exp(VL_PI * omega) + exp(-VL_PI * omega)) / (1 + 4 * omega*omega) ; default: abort() ; } } /* helper */ VL_INLINE double sinc(double x) { if (x == 0.0) return 1.0 ; return sin(x) / x ; } /** @internal @brief Sample the smoothed kernel spectrum ** @param self homogeneous kernel map. ** @param omega sampling frequency. ** @return the spectrum sampled at @a omega after smoothing. **/ VL_INLINE double vl_homogeneouskernelmap_get_smooth_spectrum (VlHomogeneousKernelMap const * self, double omega) { double kappa_hat = 0 ; double omegap ; double epsilon = 1e-2 ; double const omegaRange = 2.0 / (self->period * epsilon) ; double const domega = 2 * omegaRange / (2 * 1024.0 + 1) ; assert (self) ; switch (self->windowType) { case VlHomogeneousKernelMapWindowUniform: kappa_hat = vl_homogeneouskernelmap_get_spectrum(self, omega) ; break ; case VlHomogeneousKernelMapWindowRectangular: for (omegap = - omegaRange ; omegap <= omegaRange ; omegap += domega) { double win = sinc((self->period/2.0) * omegap) ; win *= (self->period/(2.0*VL_PI)) ; kappa_hat += win * vl_homogeneouskernelmap_get_spectrum(self, omegap + omega) ; } kappa_hat *= domega ; /* project on the postivie orthant (see PAMI) */ kappa_hat = VL_MAX(kappa_hat, 0.0) ; break ; default: abort() ; } return kappa_hat ; } /* ---------------------------------------------------------------- */ /* Constructors and destructors */ /* ---------------------------------------------------------------- */ /** @brief Create a new homgeneous kernel map ** @param kernelType type of homogeneous kernel. ** @param gamma kernel homogeneity degree. ** @param order approximation order. ** @param period kernel period. ** @param windowType type of window used to truncate the kernel. ** @return the new homogeneous kernel map. ** ** The function intializes a new homogeneous kernel map for the ** specified kernel type, homogeneity degree, approximation order, ** period, and truncation window. See @ref homkermap-fundamentals for ** details. ** ** The homogeneity degree @c gamma must be positive (the standard ** kernels are obtained by setting @c gamma to 1). When unsure, set ** @c windowType to ::VlHomogeneousKernelMapWindowRectangular. The @c ** period should be non-negative; specifying a negative or null value ** causes the function to switch to a default value. ** ** The function returns @c NULL if there is not enough free memory. **/ VlHomogeneousKernelMap * vl_homogeneouskernelmap_new (VlHomogeneousKernelType kernelType, double gamma, vl_size order, double period, VlHomogeneousKernelMapWindowType windowType) { int tableWidth, tableHeight ; VlHomogeneousKernelMap * self = vl_malloc(sizeof(VlHomogeneousKernelMap)) ; if (! self) return NULL ; assert(gamma > 0) ; assert(kernelType == VlHomogeneousKernelIntersection || kernelType == VlHomogeneousKernelChi2 || kernelType == VlHomogeneousKernelJS) ; assert(windowType == VlHomogeneousKernelMapWindowUniform || windowType == VlHomogeneousKernelMapWindowRectangular) ; if (period < 0) { switch (windowType) { case VlHomogeneousKernelMapWindowUniform: switch (kernelType) { case VlHomogeneousKernelChi2: period = 5.86 * sqrt(order + 0) + 3.65 ; break ; case VlHomogeneousKernelJS: period = 6.64 * sqrt(order + 0) + 7.24 ; break ; case VlHomogeneousKernelIntersection: period = 2.38 * log(order + 0.8) + 5.6 ; break ; } break ; case VlHomogeneousKernelMapWindowRectangular: switch (kernelType) { case VlHomogeneousKernelChi2: period = 8.80 * sqrt(order + 4.44) - 12.6 ; break ; case VlHomogeneousKernelJS: period = 9.63 * sqrt(order + 1.00) - 2.93; break ; case VlHomogeneousKernelIntersection: period = 2.00 * log(order + 0.99) + 3.52 ; break ; } break ; } period = VL_MAX(period, 1.0) ; } self->kernelType = kernelType ; self->windowType = windowType ; self->gamma = gamma ; self->order = order ; self->period = period ; self->numSubdivisions = 8 + 8*order ; self->subdivision = 1.0 / self->numSubdivisions ; self->minExponent = -20 ; self->maxExponent = 8 ; tableHeight = (int) (2*self->order + 1) ; tableWidth = (int) (self->numSubdivisions * (self->maxExponent - self->minExponent + 1)) ; self->table = vl_malloc (sizeof(double) * (tableHeight * tableWidth + 2*(1+self->order))) ; if (! self->table) { vl_free(self) ; return NULL ; } { vl_index exponent ; vl_uindex i, j ; double * tablep = self->table ; double * kappa = self->table + tableHeight * tableWidth ; double * freq = kappa + (1+self->order) ; double L = 2.0 * VL_PI / self->period ; /* precompute the sampled periodicized spectrum */ j = 0 ; i = 0 ; while (i <= self->order) { freq[i] = j ; kappa[i] = vl_homogeneouskernelmap_get_smooth_spectrum(self, j * L) ; ++ j ; if (kappa[i] > 0 || j >= 3*i) ++ i ; } /* fill table */ for (exponent = self->minExponent ; exponent <= self->maxExponent ; ++ exponent) { double x, Lxgamma, Llogx, xgamma ; double sqrt2kappaLxgamma ; double mantissa = 1.0 ; for (i = 0 ; i < self->numSubdivisions ; ++i, mantissa += self->subdivision) { x = ldexp(mantissa, (int)exponent) ; xgamma = pow(x, self->gamma) ; Lxgamma = L * xgamma ; Llogx = L * log(x) ; *tablep++ = sqrt(Lxgamma * kappa[0]) ; for (j = 1 ; j <= self->order ; ++j) { sqrt2kappaLxgamma = sqrt(2.0 * Lxgamma * kappa[j]) ; *tablep++ = sqrt2kappaLxgamma * cos(freq[j] * Llogx) ; *tablep++ = sqrt2kappaLxgamma * sin(freq[j] * Llogx) ; } } /* next mantissa */ } /* next exponent */ } return self ; } /** @brief Delete an object instance. ** @param self object. ** The function deletes the specified map object. **/ void vl_homogeneouskernelmap_delete (VlHomogeneousKernelMap * self) { vl_free(self->table) ; self->table = NULL ; vl_free(self) ; } /* ---------------------------------------------------------------- */ /* Retrieve data and parameters */ /* ---------------------------------------------------------------- */ /** @brief Get the map order. ** @param self object. ** @return the map order. **/ vl_size vl_homogeneouskernelmap_get_order (VlHomogeneousKernelMap const * self) { assert(self) ; return self->order ; } /** @brief Get the map dimension. ** @param self object. ** @return the map dimension (2 @c order +1). **/ vl_size vl_homogeneouskernelmap_get_dimension (VlHomogeneousKernelMap const * self) { assert(self) ; return 2 * self->order + 1 ; } /** @brief Get the kernel type. ** @param self object. ** @return kernel type. **/ VlHomogeneousKernelType vl_homogeneouskernelmap_get_kernel_type (VlHomogeneousKernelMap const * self) { assert(self) ; return self->kernelType ; } /** @brief Get the window type. ** @param self object. ** @return window type. **/ VlHomogeneousKernelMapWindowType vl_homogeneouskernelmap_get_window_type (VlHomogeneousKernelMap const * self) { assert(self) ; return self->windowType ; } /* ---------------------------------------------------------------- */ /* Process data */ /* ---------------------------------------------------------------- */ /** @fn ::vl_homogeneouskernelmap_evaluate_d(VlHomogeneousKernelMap const*,double*,vl_size,double) ** @brief Evaluate map ** @param self map object. ** @param destination output buffer. ** @param stride stride of the output buffer. ** @param x value to expand. ** ** The function evaluates the feature map on @a x and stores the ** resulting 2*order+1 dimensional vector to ** @a destination[0], @a destination[stride], @a destination[2*stride], .... **/ /** @fn ::vl_homogeneouskernelmap_evaluate_f(VlHomogeneousKernelMap const*,float*,vl_size,double) ** @copydetails ::vl_homogeneouskernelmap_evaluate_d(VlHomogeneousKernelMap const*,double*,vl_size,double) **/ #define FLT VL_TYPE_FLOAT #define VL_HOMKERMAP_INSTANTIATING #include "homkermap.c" #define FLT VL_TYPE_DOUBLE #define VL_HOMKERMAP_INSTANTIATING #include "homkermap.c" /* VL_HOMKERMAP_INSTANTIATING */ #endif /* ---------------------------------------------------------------- */ #ifdef VL_HOMKERMAP_INSTANTIATING /* ---------------------------------------------------------------- */ #include "float.h" void VL_XCAT(vl_homogeneouskernelmap_evaluate_,SFX) (VlHomogeneousKernelMap const * self, T * destination, vl_size stride, double x) { /* break value into exponent and mantissa */ int exponent ; int unsigned j ; double mantissa = frexp(x, &exponent) ; double sign = (mantissa >= 0.0) ? +1.0 : -1.0 ; mantissa *= 2*sign ; exponent -- ; if (mantissa == 0 || exponent <= self->minExponent || exponent >= self->maxExponent) { for (j = 0 ; j < 2*self->order+1 ; ++j) { *destination = (T) 0.0 ; destination += stride ; } return ; } { vl_size featureDimension = 2*self->order + 1 ; double const * v1 = self->table + (exponent - self->minExponent) * self->numSubdivisions * featureDimension ; double const * v2 ; double f1, f2 ; mantissa -= 1.0 ; while (mantissa >= self->subdivision) { mantissa -= self->subdivision ; v1 += featureDimension ; } v2 = v1 + featureDimension ; for (j = 0 ; j < featureDimension ; ++j) { f1 = *v1++ ; f2 = *v2++ ; *destination = (T) sign * ((f2 - f1) * (self->numSubdivisions * mantissa) + f1) ; destination += stride ; } } } #undef FLT #undef VL_HOMKERMAP_INSTANTIATING /* VL_HOMKERMAP_INSTANTIATING */ #endif colmap-3.9.1/src/thirdparty/VLFeat/homkermap.h000066400000000000000000000050401454702036400212410ustar00rootroot00000000000000/** @file homkermap.h ** @brief Homogeneous kernel map (@ref homkermap) ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_HOMKERMAP_H #define VL_HOMKERMAP_H #include "generic.h" #include /** @brief Type of kernel */ typedef enum { VlHomogeneousKernelIntersection = 0, /**< intersection kernel */ VlHomogeneousKernelChi2, /**< Chi2 kernel */ VlHomogeneousKernelJS /**< Jensen-Shannon kernel */ } VlHomogeneousKernelType ; /** @brief Type of spectral windowing function */ typedef enum { VlHomogeneousKernelMapWindowUniform = 0, /**< uniform window */ VlHomogeneousKernelMapWindowRectangular = 1, /**< rectangular window */ } VlHomogeneousKernelMapWindowType ; #ifndef __DOXYGEN__ struct _VlHomogeneousKernelMap ; typedef struct _VlHomogeneousKernelMap VlHomogeneousKernelMap ; #else /** @brief Homogeneous kernel map object */ typedef OPAQUE VlHomogeneousKernelMap ; #endif /** @name Create and destroy ** @{ */ VL_EXPORT VlHomogeneousKernelMap * vl_homogeneouskernelmap_new (VlHomogeneousKernelType kernelType, double gamma, vl_size order, double period, VlHomogeneousKernelMapWindowType windowType) ; VL_EXPORT void vl_homogeneouskernelmap_delete (VlHomogeneousKernelMap * self) ; /** @} */ /** @name Process data ** @{ */ VL_EXPORT void vl_homogeneouskernelmap_evaluate_d (VlHomogeneousKernelMap const * self, double * destination, vl_size stride, double x) ; VL_EXPORT void vl_homogeneouskernelmap_evaluate_f (VlHomogeneousKernelMap const * self, float * destination, vl_size stride, double x) ; /** @} */ /** @name Retrieve data and parameters ** @{ */ VL_EXPORT vl_size vl_homogeneouskernelmap_get_order (VlHomogeneousKernelMap const * self) ; VL_EXPORT vl_size vl_homogeneouskernelmap_get_dimension (VlHomogeneousKernelMap const * self) ; VL_EXPORT VlHomogeneousKernelType vl_homogeneouskernelmap_get_kernel_type (VlHomogeneousKernelMap const * self) ; VL_EXPORT VlHomogeneousKernelMapWindowType vl_homogeneouskernelmap_get_window_type (VlHomogeneousKernelMap const * self) ; /** @} */ /* VL_HOMKERMAP_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/host.c000077500000000000000000000371311454702036400202370ustar00rootroot00000000000000/** @file host.c ** @brief Host - Definition ** @author Andrea Vedaldi ** @see @ref portability **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page portability Portability features @author Andrea Vedaldi @tableofcontents Platform dependent details are isolated in the @ref host.h. This module provides functionalities to identify the host operating system, C compiler, and CPU architecture. It also provides a few features to abstract from such details. @see http://predef.sourceforge.net/index.php @see http://en.wikipedia.org/wiki/64-bit_computing @section host-os Host operating system The module defines a symbol to identify the host operating system: ::VL_OS_WIN for Windows, ::VL_OS_LINUX for Linux, ::VL_OS_MACOSX for Mac OS X, and so on. @section host-compiler Host compiler The module defines a symbol to identify the host compiler: ::VL_COMPILER_MSC for Microsoft Visual C++, ::VL_COMPILER_GNUC for GNU C, and so on. The (integer) value of such symbols corresponds the version of the compiler. The module defines a symbol to identify the data model of the compiler: ::VL_COMPILER_ILP32, ::VL_COMPILER_LP64, or ::VL_COMPILER_LLP64 (see Sect. @ref host-compiler-data-model). For convenience, it also defines a number of atomic types of prescribed width (::vl_int8, ::vl_int16, ::vl_int32, etc.). @remark While some of such functionalities are provided by the standard header @c stdint.h, the latter is not supported by all platforms. @subsection host-compiler-data-model Data models The C language defines a number of atomic data types (such as @c char, @c short, @c int and so on). The number of bits (width) used to represent each data type depends on the compiler data model. The different models are *ILP32* (@c int, @c long, and pointer 32 bit), *LP64* (@c int 32 bit, @c long and pointer 64 bit), *ILP64* (@c int, @c long, and pointer 64 bit), and *LLP64* (@c int, @c long 32 bit and pointer 64 -- and `long long` -- 64 bit). Note in particular that `long long` is 64 bit in all models of interest. The following table summarizes them:
Compiler data models.
Data model short int long long long void* Compiler
ILP32 16 32 32 64 32 Most 32 bit architectures.
LP64 16 32 64 64 64 UNIX-64 (Linux, Mac OS X)
ILP64 16 64 64 64 64 Alpha, Cray
SLIP64 64 64 64 64 64
LLP64 16 32 32 64 64 Windows-64
Macros such as ::VL_UINT32_C can be used to generate integer literal with the correct suffix for a type of a given width. @subsection host-compiler-other Other compiler-specific features The module provides the macro ::VL_EXPORT to declare symbols exported from the library and the macro ::VL_INLINE to declare inline functions. Such features are not part of the C89 standard, and change depending on the compiler. @par "Example:" The following header file declares a function @c f that should be visible from outside the library. @code #include VL_EXPORT void f () ; VL_EXPORT int i ; @endcode Notice that the macro ::VL_EXPORT needs not to be included again when the function is defined. @par "Example:" The following header file declares an inline function @c f: @code #include VL_INLINE int f() ; VL_INLINE int f() { return 1 ; } @endcode Here the first instruction defines the function @c f, where the second declares it. Notice that since this is an inline function, its definition must be found in the header file rather than in an implementation file. Notice also that definition and declaration can be merged. These macros translate according to the following tables:
Macros for exporting library symbols
Platform Macro name Value when building the library Value when importing the library
Unix/GCC ::VL_EXPORT empty (assumes -visibility=hidden GCC option) __attribute__((visibility ("default")))
Win/Visual C++ ::VL_EXPORT @c __declspec(dllexport) @c __declspec(dllimport)
Macros for declaring inline functions
Platform Macro name Value
Unix/GCC ::VL_INLINE static inline
Win/Visual C++ ::VL_INLINE static __inline
@section host-arch Host CPU architecture The module defines a symbol to identify the host CPU architecture: ::VL_ARCH_IX86 for Intel x86, ::VL_ARCH_IA64 for Intel 64, and so on. @subsection host-arch-endianness Endianness The module defines a symbol to identify the host CPU endianness: ::VL_ARCH_BIG_ENDIAN for big endian and ::VL_ARCH_LITTLE_ENDIAN for little endian. The functions ::vl_swap_host_big_endianness_8(), ::vl_swap_host_big_endianness_4(), ::vl_swap_host_big_endianness_2() to change the endianness of data (from/to host and network order). Recall that endianness concerns the way multi-byte data types (such as 16, 32 and 64 bits integers) are stored into the addressable memory. All CPUs uses a contiguous address range to store atomic data types (e.g. a 16-bit integer could be assigned to the addresses 0x10001 and 0x10002), but the order may differ. - The convention is big endian, or in network order, if the most significant byte of the multi-byte data types is assigned to the smaller memory address. This is the convention used for instance by the PPC architecture. - The convention is little endian if the least significant byte is assigned to the smaller memory address. This is the convention used for instance by the x86 architecture. @remark The names “big endian” and “little endian” are a little confusing. “Big endian” means “big endian first”, i.e. the address of the most significant byte comes first. Similarly, “little endian” means “little endian first”, in the sense that the address of the least significant byte comes first. Endianness is a concern when data is either exchanged with processors that use different conventions, transmitted over a network, or stored to a file. For the latter two cases, one usually saves data in big endian (network) order regardless of the host CPU. @section host-threads Multi-threading The file defines #VL_THREADS_WIN if multi-threading support is enabled and the host supports Windows threads and #VL_THREADS_POSIX if it supports POSIX threads. **/ /** @def VL_OS_LINUX ** @brief Defined if the host operating system is Linux. **/ /** @def VL_OS_MACOSX ** @brief Defined if the host operating system is Mac OS X. **/ /** @def VL_OS_WIN ** @brief Defined if the host operating system is Windows (32 or 64) **/ /** @def VL_OS_WIN64 ** @brief Defined if the host operating system is Windows-64. **/ /** @def VL_COMPILER_GNUC ** @brief Defined if the host compiler is GNU C. ** ** This macro is defined if the compiler is GNUC. ** Its value is calculated as ** @code ** 10000 * MAJOR + 100 * MINOR + PATCHLEVEL ** @endcode ** @see @ref host-compiler **/ /** @def VL_COMPILER_MSC ** @brief Defined if the host compiler is Microsoft Visual C++. ** @see @ref host-compiler **/ /** @def VL_COMPILER_LCC ** @brief Defined if the host compiler is LCC. ** @deprecated The LCC is not supported anymore. ** @see @ref host-compiler **/ /** @def VL_COMPILER_LLP64 ** @brief Defined if the host compiler data model is LLP64. ** @see @ref host-compiler-data-model **/ /** @def VL_COMPILER_LP64 ** @brief Defined if the host compiler data model is LP64. ** @see @ref host-compiler-data-model **/ /** @def VL_COMPILER_ILP32 ** @brief Defined if the host compiler data model is ILP32. ** @see @ref host-compiler-data-model **/ /** @def VL_INT8_C(x) ** @brief Create an integer constant of the specified width and sign ** @param x integer constant. ** @return @a x with the correct suffix for the given sign and size. ** The suffix used depends on the @ref host-compiler-data-model. ** @par "Example:" ** The macro VL_INT64_C(1234) is expanded as @c 123L in ** a LP64 system and as @c 123LL in a LLP64 system. **/ /** @def VL_INT16_C(x) ** @copydoc VL_INT8_C */ /** @def VL_INT32_C(x) ** @copydoc VL_INT8_C */ /** @def VL_INT64_C(x) ** @copydoc VL_INT8_C */ /** @def VL_UINT8_C(x) ** @copydoc VL_INT8_C */ /** @def VL_UINT16_C(x) ** @copydoc VL_INT8_C */ /** @def VL_UINT32_C(x) ** @copydoc VL_INT8_C */ /** @def VL_UINT64_C(x) ** @copydoc VL_INT8_C */ /** @def VL_ARCH_IX86 ** @brief Defined if the host CPU is of the Intel x86 family. ** @see @ref host-arch **/ /** @def VL_ARCH_IA64 ** @brief Defined if the host CPU is of the Intel Architecture-64 family. ** @see @ref host-arch **/ /** @def VL_ARCH_LITTLE_ENDIAN ** @brief Defined if the host CPU is little endian ** @see @ref host-arch-endianness **/ /** @def VL_ARCH_BIG_ENDIAN ** @brief Defined if the host CPU is big endian ** @see @ref host-arch-endianness **/ /** @def VL_INLINE ** @brief Adds appropriate inline function qualifier ** @see @ref host-compiler-other **/ /** @def VL_EXPORT ** @brief Declares a DLL exported symbol ** @see @ref host-compiler-other **/ /** @def VL_DISABLE_SSE2 ** @brief Defined if SSE2 support if disabled ** ** Define this symbol during compliation of the library and linking ** to another project to disable VLFeat SSE2 support. **/ /** @def VL_DISABLE_THREADS ** @brief Defined if multi-threading support is disabled ** ** Define this symbol during compilation of the library and linking ** to another project to disable VLFeat multi-threading support. **/ /** @def VL_DISABLE_OPENMP ** @brief Defined if OpenMP support is disabled ** ** Define this symbol during compilation of the library and linking ** to another project to disable VLFeat OpenMP support. **/ /** @def VL_THREADS_WIN ** @brief Defined if the host uses Windows threads. ** @see @ref host-threads **/ /** @def VL_THREADS_POSIX ** @brief Defiend if the host uses POISX threads. ** @see @ref host-threads **/ /** --------------------------------------------------------------- */ #include "host.h" #include "generic.h" #include #if defined(VL_ARCH_IX86) || defined(VL_ARCH_IA64) || defined(VL_ARCH_X64) #define HAS_CPUID #else #undef HAS_CPUID #endif #if defined(HAS_CPUID) & defined(VL_COMPILER_MSC) #include VL_INLINE void _vl_cpuid (vl_int32* info, int function) { __cpuid(info, function) ; } #endif #if defined(HAS_CPUID) & defined(VL_COMPILER_GNUC) VL_INLINE void _vl_cpuid (vl_int32* info, int function) { #if defined(VL_ARCH_IX86) && (defined(__PIC__) || defined(__pic__)) /* This version is compatible with -fPIC on x386 targets. This special * case is required becaus * on such platform -fPIC alocates ebx as global offset table pointer. * Note that =r below will be mapped to a register different from ebx, * so the code is sound. */ __asm__ __volatile__ ("pushl %%ebx \n" /* save %ebx */ "cpuid \n" "movl %%ebx, %1 \n" /* save what cpuid just put in %ebx */ "popl %%ebx \n" /* restore the old %ebx */ : "=a"(info[0]), "=r"(info[1]), "=c"(info[2]), "=d"(info[3]) : "a"(function) : "cc") ; /* clobbered (cc=condition codes) */ #else /* no -fPIC or -fPIC with a 64-bit target */ __asm__ __volatile__ ("cpuid" : "=a"(info[0]), "=b"(info[1]), "=c"(info[2]), "=d"(info[3]) : "a"(function) : "cc") ; #endif } #endif #if defined(HAS_CPUID) void _vl_x86cpu_info_init (VlX86CpuInfo *self) { vl_int32 info [4] ; int max_func = 0 ; _vl_cpuid(info, 0) ; max_func = info[0] ; self->vendor.words[0] = info[1] ; self->vendor.words[1] = info[3] ; self->vendor.words[2] = info[2] ; if (max_func >= 1) { _vl_cpuid(info, 1) ; self->hasMMX = info[3] & (1 << 23) ; self->hasSSE = info[3] & (1 << 25) ; self->hasSSE2 = info[3] & (1 << 26) ; self->hasSSE3 = info[2] & (1 << 0) ; self->hasSSE41 = info[2] & (1 << 19) ; self->hasSSE42 = info[2] & (1 << 20) ; self->hasAVX = info[2] & (1 << 28) ; } } #endif char * _vl_x86cpu_info_to_string_copy (VlX86CpuInfo const *self) { char * string = 0 ; int length = 0 ; while (string == 0) { if (length > 0) { string = vl_malloc(sizeof(char) * length) ; if (string == NULL) break ; } length = snprintf(string, length, "%s%s%s%s%s%s%s%s", self->vendor.string, self->hasMMX ? " MMX" : "", self->hasSSE ? " SSE" : "", self->hasSSE2 ? " SSE2" : "", self->hasSSE3 ? " SSE3" : "", self->hasSSE41 ? " SSE41" : "", self->hasSSE42 ? " SSE42" : "", self->hasAVX ? " AVX" : "") ; length += 1 ; } return string ; } /** ------------------------------------------------------------------ ** @brief Human readable static library configuration ** @return a new string with the static configuration. ** ** The string includes information about the compiler, the host, and ** other static configuration parameters. The string must be released ** by ::vl_free. **/ VL_EXPORT char * vl_static_configuration_to_string_copy () { char const * hostString = #ifdef VL_ARCH_X64 "X64" #endif #ifdef VL_ARCH_IA64 "IA64" #endif #ifdef VL_ARCH_IX86 "IX86" #endif #ifdef VL_ARCH_PPC "PPC" #endif ", " #ifdef VL_ARCH_BIG_ENDIAN "big_endian" #endif #ifdef VL_ARCH_LITTLE_ENDIAN "little_endian" #endif ; char compilerString [1024] ; char const * libraryString = #ifndef VL_DISABLE_THREADS #ifdef VL_THREADS_WIN "Windows_threads" #elif VL_THREADS_POSIX "POSIX_threads" #endif #else "No_threads" #endif #ifndef VL_DISABLE_SSE2 ", SSE2" #endif #if defined(_OPENMP) ", OpenMP" #endif ; snprintf(compilerString, 1024, #ifdef VL_COMPILER_MSC "Microsoft Visual C++ %d" #define v VL_COMPILER_MSC #endif #ifdef VL_COMPILER_GNUC "GNU C %d" #define v VL_COMPILER_GNUC #endif " " #ifdef VL_COMPILER_LP64 "LP64" #endif #ifdef VL_COMPILER_LLP64 "LP64" #endif #ifdef VL_COMPILER_ILP32 "ILP32" #endif , v) ; { char * string = 0 ; int length = 0 ; while (string == 0) { if (length > 0) { string = vl_malloc(sizeof(char) * length) ; if (string == NULL) break ; } length = snprintf(string, length, "%s, %s, %s", hostString, compilerString, libraryString) ; length += 1 ; } return string ; } } colmap-3.9.1/src/thirdparty/VLFeat/host.h000066400000000000000000000411741454702036400202430ustar00rootroot00000000000000/** @file host.h ** @brief Host ** @author Andrea Vedaldi ** @sa @ref portability **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_HOST_H #define VL_HOST_H /** ------------------------------------------------------------------ ** @name Configuration options ** @{ */ #if defined(__DOXYGEN__) #define VL_DISABLE_THREADS #define VL_DISABLE_SSE2 #define VL_DISABLE_OPENMP #endif /** @} */ /** ------------------------------------------------------------------ ** @name Defining functions ** @{ */ #if defined(__DOXYGEN__) #define VL_EXPORT #define VL_INLINE #endif /** @} */ /** ------------------------------------------------------------------ ** @name C preprocessor helper macros ** @{ */ /** @brief Convert the argument to a string ** @param x value to be stringified. ** ** This macro stringifies the argument @a x by means of the ** # prerpocessor operator. ** ** The standard C preprocessor does not prescan arguments which are ** stringified, so ** ** @code ** #define A B ** char const * str = VL_STRINGIFY(A) ; ** @endcode ** ** initializes str with a pointer to the string ** "A", which mihgt be unexpected. To fix this issue, ** you can use ::VL_XSTRINGIFY. ** ** @sa ::VL_XSTRINGIFY **/ #define VL_STRINGIFY(x) # x /** @brief Expand and then convert the argument to a string ** @param x value to be macro-expanded and converted. ** ** This macro macro-expands the argument @a x and stringifies the ** result of the expansion. For instance ** ** @code ** #define A B ** char const * str = VL_STRINGIFY(A) ; ** @endcode ** ** initializes str with a pointer to the string ** "B". ** ** @sa ::VL_STRINGIFY **/ #define VL_XSTRINGIFY(x) VL_STRINGIFY(x) /** @brief Concatenate two arguments into a lexical unit ** @param x first argument to be concatenated. ** @param y second argument to be concatenated. ** ** This macro concatenates its arguments into a single lexical unit ** by means of the ## preprocessor operator. Notice that ** arguments concatenated by ## are not pre-expanded by ** the C preprocessor. To macro-expand the arguments and then ** concatenate them,use ::VL_XCAT. ** ** @see ::VL_XCAT **/ #define VL_CAT(x,y) x ## y /** @brief Expand and then concatenate two arguments into a lexical unit ** @param x first argument to be concatenated. ** @param y second argument to be concatenated. ** ** This macro is the same as ::VL_CAT, except that the arguments are ** macro expanded before being concatenated. ** ** @see ::VL_CAT **/ #define VL_XCAT(x,y) VL_CAT(x,y) /** @brief Expand and then concatenate three arguments into a lexical unit ** @param x first argument to be concatenated. ** @param y second argument to be concatenated. ** @param z third argument to be concatenated. ** ** This macro is the same as ::VL_XCAT, except that it has three arguments. ** ** @see ::VL_XCAT **/ #define VL_XCAT3(x,y,z) VL_XCAT(VL_XCAT(x,y),z) /** @brief Expand and then concatenate four arguments into a lexical unit ** @param x first argument to be concatenated. ** @param y second argument to be concatenated. ** @param z third argument to be concatenated. ** @param u fourth argument to be concatenated. ** ** This macro is the same as ::VL_XCAT, except that it has four arguments. ** ** @see ::VL_XCAT **/ #define VL_XCAT4(x,y,z,u) VL_XCAT(VL_XCAT3(x,y,z),u) /** @brief Expand and then concatenate five arguments into a lexical unit ** @param x first argument to be concatenated. ** @param y second argument to be concatenated. ** @param z third argument to be concatenated. ** @param u fourth argument to be concatenated. ** @param v fifth argument to be concatenated. ** ** This macro is the same as ::VL_XCAT, except that it has five arguments. ** ** @see ::VL_XCAT **/ #define VL_XCAT5(x,y,z,u,v) VL_XCAT(VL_XCAT4(x,y,z,u),v) /** @brief Convert a boolean to "yes" or "no" strings ** @param x boolean to convert. ** ** A pointer to either the string "yes" (if @a x is true) ** or the string "no". ** ** @par Example ** @code ** VL_PRINTF("Is x true? %s.", VL_YESNO(x)) ** @endcode **/ #define VL_YESNO(x) ((x)?"yes":"no") /** @} */ /* The following macros identify the host OS, architecture and compiler. They are derived from http://predef.sourceforge.net/ */ /** @name Identifying the host operating system ** @{ */ #if defined(linux) || \ defined(__linux) || \ defined(__linux__) || \ defined(__DOXYGEN__) #define VL_OS_LINUX 1 #endif #if (defined(__APPLE__) & defined(__MACH__)) || \ defined(__DOXYGEN__) #define VL_OS_MACOSX 1 #endif #if defined(__WIN32__) || \ defined(_WIN32) || \ defined(__DOXYGEN__) #define VL_OS_WIN 1 #endif #if defined(_WIN64) || \ defined(__DOXYGEN__) #define VL_OS_WIN64 1 #endif /** @} */ /** @name Identifying the host threading library ** @{ */ #if !defined(VL_OS_WIN) && !defined(VL_OS_WIN64) || \ defined(__DOXYGEN__) #define VL_THREADS_POSIX 1 #endif #if defined(VL_OS_WIN) || defined(VL_OS_WIN64) || \ defined(__DOXYGEN__) #define VL_THREADS_WIN 1 #endif /** @} */ /** @name Identifying the host compiler ** @{ */ #if defined(__GNUC__) || defined(__DOXYGEN__) # if defined(__GNUC_PATCHLEVEL__) # define VL_COMPILER_GNUC (__GNUC__ * 10000 \ + __GNUC_MINOR__ * 100 \ + __GNUC_PATCHLEVEL__) # else # define VL_COMPILER_GNUC (__GNUC__ * 10000 \ + __GNUC_MINOR__ * 100) # endif #endif #if defined(_MSC_VER) || defined(__DOXYGEN__) #define VL_COMPILER_MSC _MSC_VER #endif #if defined(__LCC__) || defined(__DOXYGEN__) #warning "LCC support is experimental!" #define VL_COMPILER_LCC 1 #endif /** @} */ /** @name Identifying the host CPU architecture ** @{ */ #if defined(i386) || \ defined(__i386__) || \ defined(__DOXYGEN__) #define VL_ARCH_IX86 300 #elif defined(__i486__) #define VL_ARCH_IX86 400 #elif defined(__i586__) #define VL_ARCH_IX86 500 #elif defined(__i686__) #define VL_ARCH_IX86 600 #elif defined(_M_IX86) #define VL_ARCH_IX86 _M_IX86 #endif #if defined(_M_X64) || \ defined(__amd64__) || \ defined(__amd64) || \ defined(__x86_64) || \ defined(__x86_64) #define VL_ARCH_X64 #endif #if defined(__ia64__) || \ defined(_IA64) || \ defined(__IA64) || \ defined(__ia64) || \ defined(_M_IA64) || \ defined(__DOXYGEN__) #define VL_ARCH_IA64 #endif /** @} */ /** @name Identifying the host data model ** @{ */ #if defined(__LLP64__) || \ defined(__LLP64) || \ defined(__LLP64) || \ (defined(VL_COMPILER_MSC) & defined(VL_OS_WIN64)) || \ (defined(VL_COMPILER_LCC) & defined(VL_OS_WIN64)) || \ defined(__DOXYGEN__) #define VL_COMPILER_LLP64 #endif #if defined(__LP64__) || \ defined(__LP64) || \ defined(__LP64) || \ (defined(VL_OS_MACOSX) & defined(VL_ARCH_IA64)) || \ defined(__DOXYGEN__) #define VL_COMPILER_LP64 #endif #if (!defined(VL_COMPILER_LLP64) & !defined(VL_COMPILER_LP64)) || \ defined(__DOXYGEN__) #define VL_COMPILER_ILP32 #endif /** @} */ /** @name Identifying the host endianness ** @{ */ #if defined(__LITTLE_ENDIAN__) || \ defined(VL_ARCH_IX86) || \ defined(VL_ARCH_IA64) || \ defined(VL_ARCH_X64) || \ defined(__DOXYGEN__) #define VL_ARCH_LITTLE_ENDIAN #endif #if defined(__DOXYGEN__) || \ !defined(VL_ARCH_LITTLE_ENDIAN) #define VL_ARCH_BIG_ENDIAN #endif /** @} */ #if defined(VL_COMPILER_MSC) & ! defined(__DOXYGEN__) # define VL_UNUSED # define VL_INLINE static __inline #if defined _MSC_VER && _MSC_VER < 1900 # define snprintf _snprintf #endif # define isnan _isnan # ifdef VL_BUILD_DLL # ifdef __cplusplus # define VL_EXPORT extern "C" __declspec(dllexport) # else # define VL_EXPORT extern __declspec(dllexport) # endif # else # ifdef __cplusplus # define VL_EXPORT extern "C" # else # define VL_EXPORT extern # endif # endif #endif #if defined(VL_COMPILER_LCC) & ! defined(__DOXYGEN__) # define VL_UNUSED # define VL_INLINE static __inline # define snprintf _snprintf # define isnan _isnan VL_INLINE float fabsf(float x) { return (float) fabs((double) x) ; } # ifdef VL_BUILD_DLL # define VL_EXPORT extern __declspec(dllexport) # else # define VL_EXPORT extern # endif #endif #if defined(VL_COMPILER_GNUC) & ! defined(__DOXYGEN__) # define VL_UNUSED __attribute__((unused)) # define VL_INLINE static __inline__ # ifdef VL_BUILD_DLL # ifdef __cplusplus # define VL_EXPORT __attribute__((visibility ("default"))) extern "C" # else # define VL_EXPORT __attribute__((visibility ("default"))) extern # endif # else # ifdef __cplusplus # define VL_EXPORT extern "C" # else # define VL_EXPORT extern # endif # endif #endif VL_EXPORT char * vl_static_configuration_to_string_copy () ; /** ------------------------------------------------------------------ ** @name Atomic data types ** @{ **/ #define VL_TRUE 1 /**< @brief @c true (1) constant */ #define VL_FALSE 0 /**< @brief @c false (0) constant */ #if defined(VL_COMPILER_LP64) || defined(VL_COMPILER_LLP64) typedef long long vl_int64 ; /**< @brief Signed 64-bit integer. */ typedef int vl_int32 ; /**< @brief Signed 32-bit integer. */ typedef short vl_int16 ; /**< @brief Signed 16-bit integer. */ typedef char vl_int8 ; /**< @brief Signed 8-bit integer. */ typedef long long unsigned vl_uint64 ; /**< @brief Unsigned 64-bit integer. */ typedef int unsigned vl_uint32 ; /**< @brief Unsigned 32-bit integer. */ typedef short unsigned vl_uint16 ; /**< @brief Unsigned 16-bit integer. */ typedef char unsigned vl_uint8 ; /**< @brief Unsigned 8-bit integer. */ typedef int vl_int ; /**< @brief Same as @c int. */ typedef unsigned int vl_uint ; /**< @brief Same as unsigned int. */ typedef int vl_bool ; /**< @brief Boolean. */ typedef vl_int64 vl_intptr ; /**< @brief Integer holding a pointer. */ typedef vl_uint64 vl_uintptr ; /**< @brief Unsigned integer holding a pointer. */ typedef vl_uint64 vl_size ; /**< @brief Unsigned integer holding the size of a memory block. */ typedef vl_int64 vl_index ; /**< @brief Signed version of ::vl_size and ::vl_uindex */ typedef vl_uint64 vl_uindex ; /**< @brief Same as ::vl_size */ #endif #if defined(VL_COMPILER_ILP32) #ifdef VL_COMPILER_MSC typedef __int64 vl_int64 ; #else typedef long long vl_int64 ; #endif typedef int vl_int32 ; typedef short vl_int16 ; typedef char vl_int8 ; #ifdef VL_COMPILER_MSC typedef __int64 unsigned vl_uint64 ; #else typedef long long unsigned vl_uint64 ; #endif typedef int unsigned vl_uint32 ; typedef short unsigned vl_uint16 ; typedef char unsigned vl_uint8 ; typedef int vl_int ; typedef unsigned int vl_uint ; typedef int vl_bool ; typedef vl_int32 vl_intptr ; typedef vl_uint32 vl_uintptr ; typedef vl_uint32 vl_size ; typedef vl_int32 vl_index ; typedef vl_uint32 vl_uindex ; #endif /** @} */ /** @name Creating integer constants ** @{ */ #if defined(VL_COMPILER_LP64) || defined(__DOXYGEN__) #define VL_INT8_C(x) x #define VL_INT16_C(x) x #define VL_INT32_C(x) x #define VL_INT64_C(x) x ## L #define VL_UINT8_C(x) x #define VL_UINT16_C(x) x #define VL_UINT32_C(x) x ## U #define VL_UINT64_C(x) x ## UL #endif #if (defined(VL_COMPILER_LLP64) || defined(VL_COMPILER_ILP32)) \ & !defined(__DOXYGEN__) #define VL_INT8_C(x) x #define VL_INT16_C(x) x #define VL_INT32_C(x) x #define VL_INT64_C(x) x ## LL #define VL_UINT8_C(x) x #define VL_UINT16_C(x) x #define VL_UINT32_C(x) x ## U #define VL_UINT64_C(x) x ## ULL #endif /** @} */ /** ------------------------------------------------------------------ ** @name Printing the atomic data types ** @{ */ /* Lengths only: */ /** @def VL_FL_INT64 ** @brief @c printf length flag for ::vl_int64 and ::vl_uint64. **/ /** @def VL_FL_INT32 ** @brief @c printf length flag for ::vl_int32 and ::vl_uint32. **/ /** @def VL_FL_INT16 ** @brief @c printf length flag for ::vl_int16 and ::vl_uint16. **/ /** @def VL_FL_INT8 ** @brief @c printf length flag for ::vl_int8 and ::vl_uint8. **/ /** @def VL_FL_INDEX ** @brief @c printf length flag for ::vl_index and ::vl_uindex **/ #ifdef VL_COMPILER_MSC #define VL_FL_INT64 "I64" #else #define VL_FL_INT64 "ll" #endif #define VL_FL_INT32 "" #define VL_FL_INT16 "h" #define VL_FL_INT8 "hh" #if defined(VL_COMPILER_LP64) || defined(VL_COMPILER_LLP64) #define VL_FL_INDEX VL_FL_INT64 #endif #if defined(VL_COMPILER_ILP32) #define VL_FL_INDEX VL_FL_INT32 #endif /* Formats (but not conversions!): */ /** @def VL_FMT_SIZE ** @brief @c printf flag for ::vl_size **/ /** @def VL_FMT_INDEX ** @brief @c printf flag for ::vl_index **/ /** @def VL_FMT_UINDEX ** @brief @c printf flag for ::vl_uindex **/ /** @def VL_FMT_INTPTR ** @brief @c printf flag for ::vl_intptr **/ /** @def VL_FMT_UINTPTR ** @brief @c printf flag for ::vl_uintptr **/ #define VL_FMT_INDEX VL_FL_INDEX "d" #define VL_FMT_INTPTR VL_FMT_INDEX #define VL_FMT_UINDEX VL_FL_INDEX "u" #define VL_FMT_SIZE VL_FMT_UINDEX #define VL_FMT_UINTPTR VL_FMT_UINDEX /** @} */ /** ------------------------------------------------------------------ ** @name Atomic data types limits ** @{ */ /** @brief Largest integer (math constant) */ #define VL_BIG_INT 0x7FFFFFFFL /** @brief Smallest integer (math constant) */ #define VL_SMALL_INT (- VL_BIG_INT - 1) /** @brief Largest unsigned integer (math constant) */ #define VL_BIG_UINT 0xFFFFFFFFUL /** @} */ /** ------------------------------------------------------------------ ** @name Endianness detection and conversion ** @{ **/ VL_INLINE void vl_swap_host_big_endianness_8 (void *dst, void* src) ; VL_INLINE void vl_swap_host_big_endianness_4 (void *dst, void* src) ; VL_INLINE void vl_swap_host_big_endianness_2 (void *dst, void* src) ; /** @} */ /** ------------------------------------------------------------------ ** @name Obtaining host info at run time ** @{ */ typedef struct _VlX86CpuInfo { union { char string [0x20] ; vl_uint32 words [0x20 / 4] ; } vendor ; vl_bool hasAVX ; vl_bool hasSSE42 ; vl_bool hasSSE41 ; vl_bool hasSSE3 ; vl_bool hasSSE2 ; vl_bool hasSSE ; vl_bool hasMMX ; } VlX86CpuInfo ; void _vl_x86cpu_info_init (VlX86CpuInfo *self) ; char * _vl_x86cpu_info_to_string_copy (VlX86CpuInfo const *self) ; /** @} */ /** ------------------------------------------------------------------ ** @brief Host <-> big endian transformation for 8-bytes value ** ** @param dst destination 8-byte buffer. ** @param src source 8-byte bufffer. ** @see @ref host-arch-endianness. **/ VL_INLINE void vl_swap_host_big_endianness_8 (void *dst, void* src) { char *dst_ = (char*) dst ; char *src_ = (char*) src ; #if defined(VL_ARCH_BIG_ENDIAN) dst_ [0] = src_ [0] ; dst_ [1] = src_ [1] ; dst_ [2] = src_ [2] ; dst_ [3] = src_ [3] ; dst_ [4] = src_ [4] ; dst_ [5] = src_ [5] ; dst_ [6] = src_ [6] ; dst_ [7] = src_ [7] ; #else dst_ [0] = src_ [7] ; dst_ [1] = src_ [6] ; dst_ [2] = src_ [5] ; dst_ [3] = src_ [4] ; dst_ [4] = src_ [3] ; dst_ [5] = src_ [2] ; dst_ [6] = src_ [1] ; dst_ [7] = src_ [0] ; #endif } /** ------------------------------------------------------------------ ** @brief Host <-> big endian transformation for 4-bytes value ** ** @param dst destination 4-byte buffer. ** @param src source 4-byte bufffer. ** @sa @ref host-arch-endianness. **/ VL_INLINE void vl_swap_host_big_endianness_4 (void *dst, void* src) { char *dst_ = (char*) dst ; char *src_ = (char*) src ; #if defined(VL_ARCH_BIG_ENDIAN) dst_ [0] = src_ [0] ; dst_ [1] = src_ [1] ; dst_ [2] = src_ [2] ; dst_ [3] = src_ [3] ; #else dst_ [0] = src_ [3] ; dst_ [1] = src_ [2] ; dst_ [2] = src_ [1] ; dst_ [3] = src_ [0] ; #endif } /** ------------------------------------------------------------------ ** @brief Host <-> big endian transformation for 2-bytes value ** ** @param dst destination 2-byte buffer. ** @param src source 2-byte bufffer. ** @see @ref host-arch-endianness. **/ VL_INLINE void vl_swap_host_big_endianness_2 (void *dst, void* src) { char *dst_ = (char*) dst ; char *src_ = (char*) src ; #if defined(VL_ARCH_BIG_ENDIAN) dst_ [0] = src_ [0] ; dst_ [1] = src_ [1] ; #else dst_ [0] = src_ [1] ; dst_ [1] = src_ [0] ; #endif } /* VL_HOST_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/ikmeans.c000077500000000000000000000172421454702036400207120ustar00rootroot00000000000000/** @file ikmeans.c ** @brief Integer K-Means clustering - Definition ** @author Brian Fulkerson ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file ikmeans.h ** ** Integer K-means (IKM) is an implementation of K-means clustering ** (or Vector Quantization, VQ) for integer data. This is ** particularly useful for clustering large collections of visual ** descriptors. ** ** Use the function ::vl_ikm_new() to create a IKM ** quantizer. Initialize the IKM quantizer with @c K clusters by ** ::vl_ikm_init() or similar function. Use ::vl_ikm_train() to train ** the quantizer. Use ::vl_ikm_push() or ::vl_ikm_push_one() to ** quantize new data. ** ** Given data @f$x_1,\dots,x_N\in R^d@f$ and a number of clusters ** @f$K@f$, the goal is to find assignments @f$a_i\in\{1,\dots,K\},@f$ ** and centers @f$c_1,\dots,c_K\in R^d@f$ so that the expected ** distortion ** ** @f[ ** E(\{a_{i}, c_j\}) = \frac{1}{N} \sum_{i=1}^N d(x_i, c_{a_i}) ** @f] ** ** is minimized. Here @f$d(x_i, c_{a_i})@f$ is the ** distortion, i.e. the cost we pay for representing @f$ x_i ** @f$ by @f$ c_{a_i} @f$. IKM uses the squared distortion ** @f$d(x,y)=\|x-y\|^2_2@f$. ** ** @section ikmeans-algo Algorithms ** ** @subsection ikmeans-alg-init Initialization ** ** Most K-means algorithms are iterative and needs an initialization ** in the form of an initial choice of the centers ** @f$c_1,\dots,c_K@f$. We include the following options: ** ** - User specified centers (::vl_ikm_init); ** - Random centers (::vl_ikm_init_rand); ** - Centers from @c K randomly selected data points (::vl_ikm_init_rand_data). ** ** @subsection ikmeans-alg-lloyd Lloyd ** ** The Lloyd (also known as Lloyd-Max and LBG) algorithm iteratively: ** ** - Fixes the centers, optimizing the assignments (minimizing by ** exhaustive search the association of each data point to the ** centers); ** - Fixes the assignments and optimizes the centers (by descending ** the distortion error function). For the squared distortion, this ** step is in closed form. ** ** This algorithm is not particularly efficient because all data ** points need to be compared to all centers, for a complexity ** @f$O(dNKT)@f$, where T is the total number of iterations. ** ** @subsection ikmeans-alg-elkan Elkan ** ** The Elkan algorithm is an optimized variant of Lloyd. By making ** use of the triangle inequality, many comparisons of data points ** and centers are avoided, especially at later iterations. ** Usually 4-5 times less comparisons than Lloyd are preformed, ** providing a dramatic speedup in the execution time. ** **/ #include "ikmeans.h" #include #include #include /* memset */ #include "assert.h" static void vl_ikm_init_lloyd (VlIKMFilt*) ; static void vl_ikm_init_elkan (VlIKMFilt*) ; static int vl_ikm_train_lloyd (VlIKMFilt*, vl_uint8 const*, vl_size) ; static int vl_ikm_train_elkan (VlIKMFilt*, vl_uint8 const*, vl_size) ; static void vl_ikm_push_lloyd (VlIKMFilt*, vl_uint32*, vl_uint8 const*, vl_size) ; static void vl_ikm_push_elkan (VlIKMFilt*, vl_uint32*, vl_uint8 const*, vl_size) ; /** @brief Create a new IKM quantizer ** @param method Clustering algorithm. ** @return new IKM quantizer. ** ** The function allocates initializes a new IKM quantizer to ** operate based algorithm @a method. ** ** @a method has values in the enumerations ::VlIKMAlgorithms. **/ VlIKMFilt * vl_ikm_new (int method) { VlIKMFilt *f = vl_calloc (sizeof(VlIKMFilt), 1) ; f -> method = method ; f -> max_niters = 200 ; return f ; } /** @brief Delete IKM quantizer ** @param f IKM quantizer. **/ void vl_ikm_delete (VlIKMFilt* f) { if (f) { if (f->centers) vl_free(f->centers) ; if (f->inter_dist) vl_free(f->inter_dist) ; vl_free(f) ; } } /** @brief Train clusters ** @param f IKM quantizer. ** @param data data. ** @param N number of data (@a N @c >= 1). ** @return -1 if an overflow may have occurred. **/ int vl_ikm_train (VlIKMFilt *f, vl_uint8 const *data, vl_size N) { int err ; if (f-> verb) { VL_PRINTF ("ikm: training with %d data\n", N) ; VL_PRINTF ("ikm: %d clusters\n", f -> K) ; } switch (f -> method) { case VL_IKM_LLOYD : err = vl_ikm_train_lloyd (f, data, N) ; break ; case VL_IKM_ELKAN : err = vl_ikm_train_elkan (f, data, N) ; break ; default : abort() ; } return err ; } /** @brief Project data to clusters ** @param f IKM quantizer. ** @param asgn Assignments (out). ** @param data data. ** @param N number of data (@a N @c >= 1). ** ** The function projects the data @a data on the integer K-means ** clusters specified by the IKM quantizer @a f. Notice that the ** quantizer must be initialized. **/ void vl_ikm_push (VlIKMFilt *f, vl_uint32 *asgn, vl_uint8 const *data, vl_size N) { switch (f -> method) { case VL_IKM_LLOYD : vl_ikm_push_lloyd (f, asgn, data, N) ; break ; case VL_IKM_ELKAN : vl_ikm_push_elkan (f, asgn, data, N) ; break ; default : abort() ; } } /** @brief Project one datum to clusters ** @param centers centers. ** @param data datum to project. ** @param K number of centers. ** @param M dimensionality of the datum. ** @return the cluster index. ** ** The function projects the specified datum @a data on the clusters ** specified by the centers @a centers. **/ vl_uint32 vl_ikm_push_one (vl_ikmacc_t const *centers, vl_uint8 const *data, vl_size M, vl_size K) { vl_uindex i,k ; /* assign data to centers */ vl_uindex best = (vl_uindex) -1 ; vl_ikmacc_t best_dist = 0 ; for(k = 0 ; k < K ; ++k) { vl_ikmacc_t dist = 0 ; /* compute distance with this center */ for(i = 0 ; i < M ; ++i) { vl_ikmacc_t delta = (vl_ikmacc_t)data[i] - centers[k*M + i] ; dist += delta * delta ; } /* compare with current best */ if (best == (vl_uindex) -1 || dist < best_dist) { best = k ; best_dist = dist ; } } return (vl_uint32)best; } /* ---------------------------------------------------------------- */ /* Getters and setters */ /* ---------------------------------------------------------------- */ /** @brief Get data dimensionality ** @param f IKM filter. ** @return data dimensionality. **/ vl_size vl_ikm_get_ndims (VlIKMFilt const* f) { return f->M ; } /** @brief Get the number of centers K ** @param f IKM filter. ** @return number of centers K. **/ vl_size vl_ikm_get_K (VlIKMFilt const* f) { return f->K ; } /** @brief Get verbosity level ** @param f IKM filter. ** @return verbosity level. **/ int vl_ikm_get_verbosity (VlIKMFilt const* f) { return f->verb ; } /** @brief Get maximum number of iterations ** @param f IKM filter. ** @return maximum number of iterations. **/ vl_size vl_ikm_get_max_niters (VlIKMFilt const* f) { return f->max_niters ; } /** @brief Get maximum number of iterations ** @param f IKM filter. ** @return maximum number of iterations. **/ vl_ikmacc_t const * vl_ikm_get_centers (VlIKMFilt const* f) { return f-> centers ; } /** @brief Set verbosity level ** @param f IKM filter. ** @param verb verbosity level. **/ void vl_ikm_set_verbosity (VlIKMFilt *f, int verb) { f-> verb = VL_MAX(0,verb) ; } /** @brief Set maximum number of iterations ** @param f IKM filter. ** @param max_niters maximum number of iterations. **/ void vl_ikm_set_max_niters (VlIKMFilt *f, vl_size max_niters) { f-> max_niters = max_niters ; } #include "ikmeans_init.tc" #include "ikmeans_lloyd.tc" #include "ikmeans_elkan.tc" colmap-3.9.1/src/thirdparty/VLFeat/ikmeans.h000066400000000000000000000051641454702036400207140ustar00rootroot00000000000000/** @file ikmeans.h ** @brief Integer K-Means clustering ** @author Brian Fulkerson ** @author Andrea Vedaldi **/ /* Copyright (C) 2014 Andrea Vedaldi. Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_IKMEANS_H #define VL_IKMEANS_H #include "generic.h" #include "random.h" #if 0 typedef vl_int64 vl_ikmacc_t ; /**< IKM accumulator data type */ #define VL_IKMACC_MAX 0x7fffffffffffffffULL #else typedef vl_int32 vl_ikmacc_t ; /**< IKM accumulator data type */ #define VL_IKMACC_MAX 0x7fffffffUL #endif /** ------------------------------------------------------------------ ** @brief IKM algorithms **/ enum VlIKMAlgorithms { VL_IKM_LLOYD, /**< Lloyd algorithm */ VL_IKM_ELKAN, /**< Elkan algorithm */ } ; /** ------------------------------------------------------------------ ** @brief IKM quantizer **/ typedef struct _VlIKMFilt { vl_size M ; /**< data dimensionality */ vl_size K ; /**< number of centers */ vl_size max_niters ; /**< Lloyd: maximum number of iterations */ int method ; /**< Learning method */ int verb ; /**< verbosity level */ vl_ikmacc_t *centers ; /**< centers */ vl_ikmacc_t *inter_dist ; /**< centers inter-distances */ } VlIKMFilt ; /** @name Create and destroy ** @{ */ VL_EXPORT VlIKMFilt *vl_ikm_new (int method) ; VL_EXPORT void vl_ikm_delete (VlIKMFilt *f) ; /** @} */ /** @name Process data ** @{ */ VL_EXPORT void vl_ikm_init (VlIKMFilt *f, vl_ikmacc_t const *centers, vl_size M, vl_size K) ; VL_EXPORT void vl_ikm_init_rand (VlIKMFilt *f, vl_size M, vl_size K) ; VL_EXPORT void vl_ikm_init_rand_data (VlIKMFilt *f, vl_uint8 const *data, vl_size M, vl_size N, vl_size K) ; VL_EXPORT int vl_ikm_train (VlIKMFilt *f, vl_uint8 const *data, vl_size N) ; VL_EXPORT void vl_ikm_push (VlIKMFilt *f, vl_uint32 *asgn, vl_uint8 const *data, vl_size N) ; VL_EXPORT vl_uint vl_ikm_push_one (vl_ikmacc_t const *centers, vl_uint8 const *data, vl_size M, vl_size K) ; /** @} */ /** @name Retrieve data and parameters ** @{ */ VL_EXPORT vl_size vl_ikm_get_ndims (VlIKMFilt const *f) ; VL_EXPORT vl_size vl_ikm_get_K (VlIKMFilt const *f) ; VL_EXPORT int vl_ikm_get_verbosity (VlIKMFilt const *f) ; VL_EXPORT vl_size vl_ikm_get_max_niters (VlIKMFilt const *f) ; VL_EXPORT vl_ikmacc_t const *vl_ikm_get_centers (VlIKMFilt const *f) ; /** @} */ /** @name Set parameters ** @{ */ VL_EXPORT void vl_ikm_set_verbosity (VlIKMFilt *f, int verb) ; VL_EXPORT void vl_ikm_set_max_niters (VlIKMFilt *f, vl_size max_niters) ; /** @} */ /* VL_IKMEANS_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/ikmeans_elkan.tc000077500000000000000000000236221454702036400222470ustar00rootroot00000000000000/** @file ikmeans_elkan.tc ** @brief Integer K-Means - Elkan Algorithm - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #include "mathop.h" /** @internal ** Update inter cluster distance table. **/ static void vl_ikm_elkan_update_inter_dist (VlIKMFilt *f) { vl_uindex i, k, kp ; /* inter cluster distances */ for(k = 0 ; k < f->K ; ++ k) { for(kp = 0 ; kp < f->K ; ++ kp) { vl_ikmacc_t dist = 0 ; if (k != kp) { for(i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = f->centers [kp * f->M + i] - f->centers [k * f->M + i] ; dist += delta * delta ; } } f->inter_dist [k * f->K + kp] = f->inter_dist [kp * f->K + k] = dist >> 2 ; } } } /** @internal ** @brief Helper function to initialize filter for Triangle algorithm ** @param f filter. **/ static void vl_ikm_init_elkan (VlIKMFilt *f) { if (f->inter_dist) { vl_free (f-> inter_dist) ; } f->inter_dist = vl_malloc (sizeof(*f->inter_dist) * f->K * f->K) ; vl_ikm_elkan_update_inter_dist (f) ; } /** @internal ** @brief Elkan algorithm ** @param f IKM quantizer. ** @param data Data to quantize. ** @param N Number of data elements. **/ static int vl_ikm_train_elkan (VlIKMFilt* f, vl_uint8 const* data, vl_size N) { /* REMARK !! All distances are squared !! */ vl_uindex i,pass,c,cp,x,cx ; vl_size dist_calc = 0 ; vl_ikmacc_t dist ; vl_ikmacc_t *m_pt = vl_malloc(sizeof(*m_pt) * f->M * f->K) ; /* new centers (temp) */ vl_ikmacc_t *u_pt = vl_malloc(sizeof(*u_pt) * N) ; /* upper bound (may str) */ char *r_pt = vl_malloc(sizeof(*r_pt) * 1 * N) ; /* flag: u is strict */ vl_ikmacc_t *s_pt = vl_malloc(sizeof(*s_pt) * f->K) ; /* min cluster dist. */ vl_ikmacc_t *l_pt = vl_malloc(sizeof(*l_pt) * N * f->K) ; /* lower bound */ vl_ikmacc_t *d_pt = f->inter_dist ; /* half inter clst dist */ vl_uint32 *asgn = vl_malloc (sizeof(*asgn) * N) ; vl_uint32 *counts =vl_malloc (sizeof(*counts) * N) ; int done = 0 ; /* do passes */ vl_ikm_elkan_update_inter_dist (f) ; /* init */ memset(l_pt, 0, sizeof(*l_pt) * N * f->K) ; memset(u_pt, 0, sizeof(*u_pt) * N) ; memset(r_pt, 0, sizeof(*r_pt) * N) ; for(x = 0 ; x < N ; ++x) { vl_ikmacc_t best_dist ; /* do first cluster `by hand' */ dist_calc ++ ; for(dist = 0, i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = (vl_ikmacc_t)data[x * f->M + i] - f->centers[i] ; dist += delta*delta ; } cx = 0 ; best_dist = dist ; l_pt[x] = dist ; /* do other clusters */ for(c = 1 ; c < f->K ; ++c) { if(d_pt[f->K * cx + c] < best_dist) { /* might need to be updated */ dist_calc++ ; for(dist=0, i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = (vl_ikmacc_t)data[x * f->M + i] - f->centers[c * f->M + i] ; dist += delta * delta ; } /* lower bound */ l_pt[N*c + x] = dist ; if(dist < best_dist) { best_dist = dist ; cx = c ; } } } asgn[x] = (vl_uint32)cx ; u_pt[x] = best_dist ; } /* -------------------------------------------------------------------- * Passes * ------------------------------------------------------------------ */ for (pass = 0 ; 1 ; ++ pass) { /* ------------------------------------------------------------------ * Re-calculate means * ---------------------------------------------------------------- */ memset(m_pt, 0, sizeof(*m_pt) * f->M * f->K) ; memset(counts, 0, sizeof(*counts) * f->K) ; /* accumulate */ for(x = 0 ; x < N ; ++x) { int cx = asgn[x] ; ++ counts[ cx ] ; for(i = 0 ; i < f->M ; ++i) { m_pt[cx * f->M + i] += data[x * f->M + i] ; } } /* normalize */ for(c = 0 ; c < f->K ; ++c) { vl_ikmacc_t n = counts[c] ; if(n > 0) { for(i = 0 ; i < f->M ; ++i) { m_pt[c * f->M + i] /= n ; } } else { for(i = 0 ; i < f->M ; ++i) { /*m_pt[c*M + i] = data[pairs_pt[c].j*M + i] ;*/ } } } /* ------------------------------------------------------------------ * Update bounds * --------------------------------------------------------------- */ for(c = 0 ; c < f->K ; ++c) { /* distance d(m(c),c) and update c */ dist_calc++ ; for(dist = 0, i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = (vl_ikmacc_t)m_pt[c * f->M + i] - f->centers[c * f->M + i] ; f->centers[c * f->M + i] = m_pt[c * f->M +i] ; dist += delta * delta ; } for(x = 0 ; x < N ; ++x) { vl_ikmacc_t lxc = l_pt[c * N + x] ; vl_uindex cx = (int) asgn[x] ; /* lower bound */ if(dist < lxc) { lxc = (vl_ikmacc_t) (lxc + dist - 2*(vl_fast_sqrt_ui64(lxc)+1)*(vl_fast_sqrt_ui64(dist)+1)) ; } else { lxc = 0 ; } l_pt[c*N + x] = lxc ; /* upper bound */ if(c == cx) { vl_ikmacc_t ux = u_pt[x] ; u_pt[x] = (vl_ikmacc_t) (ux + dist + 2 * (vl_fast_sqrt_ui64(ux)+1)*(vl_fast_sqrt_ui64(dist)+1)) ; r_pt[x] = 1 ; } } } /* inter cluster distances */ for(c = 0 ; c < f->K ; ++c) { for(cp = 0 ; cp < f->K ; ++cp) { dist = 0 ; if( c != cp ) { dist_calc++; for(i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = f->centers[cp * f->M + i] - f->centers[ c * f->M + i] ; dist += delta*delta ; } } d_pt[c * f->K + cp] = d_pt[cp * f->K + c] = dist>>2 ; } } /* closest cluster distance */ for(c = 0 ; c < f->K ; ++c) { vl_ikmacc_t best_dist = VL_IKMACC_MAX ; for(cp = 0 ; cp < f->K ; ++cp) { dist = d_pt[c * f->K + cp] ; if(c != cp && dist < best_dist) best_dist = dist ; } s_pt[c] = best_dist >> 2 ; } /* ------------------------------------------------------------------ * Assign data to centers * ---------------------------------------------------------------- */ done = 1 ; for(x = 0 ; x < N ; ++x) { vl_uindex cx = (vl_uindex) asgn[x] ; vl_ikmacc_t ux = u_pt[x] ; /* ux is an upper bound of the distance of x to its current center cx. s_pt[cx] is half of the minum distance between the cluster cx and any other cluster center. If ux <= s_pt[cx] then x remains attached to cx. */ if(ux <= s_pt[cx]) continue ; for(c = 0 ; c < f->K ; ++c) { vl_ikmacc_t dist = 0 ; /* so x might need to be re-associated from cx to c. We can exclude c if 1 - cx = c (trivial) or 2 - u(x) <= l(x,c) as this implies d(x,cx) <= d(x,c) or 3 - u(x) <= d(cx,c)/2 as this implies d(x,cx) <= d(x,c). */ if(c == cx || ux <= l_pt[N * c + x] || ux <= d_pt[f->K * c + cx]) continue ; /* we need to make a true comparison */ /* if u_pt[x] is stale (i.e. not strictly equal to d(x,cx)), then re-calcualte it. */ if( r_pt[x] ) { dist_calc++; for(dist = 0, i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = (vl_ikmacc_t)data[x * f->M + i] - f->centers[cx * f->M + i] ; dist += delta*delta ; } ux = u_pt[x] = dist ; r_pt[x] = 0 ; /* now that u_pt[x] is updated, we check the conditions again */ if( ux <= l_pt[N * c + x] || ux <= d_pt[f->K * c + cx] ) continue ; } /* no way... we need to compute the distance d(x,c) */ dist_calc++ ; for(dist = 0, i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = (vl_ikmacc_t)data[ x * f->M + i] - f->centers[c * f->M + i] ; dist += delta * delta ; } l_pt[N * c + x] = dist ; if (dist < ux) { ux = u_pt[x] = dist ; /* r_pt[x] already 0 */ asgn[x] = (vl_uint32)c ; done = 0 ; } } } /* next data point */ /* stopping condition */ if(done || pass == f->max_niters) { break ; } } vl_free (counts) ; vl_free (asgn) ; vl_free (l_pt) ; vl_free (s_pt) ; vl_free (r_pt) ; vl_free (u_pt) ; vl_free (m_pt) ; if (f-> verb) { VL_PRINTF ("ikm: Elkan algorithm: total iterations: %d\n", pass) ; VL_PRINTF ("ikm: Elkan algorithm: distance calculations: %d (speedup: %.2f)\n", dist_calc, (float)N * f->K * (pass+2) / dist_calc - 1) ; } return 0 ; } /** @internal ** @brief Elkan algorithm ** @param f IKM quantizer. ** @param asgn Assignment of data to centers (out). ** @param data Data to quantize. ** @param N Number of data elements. **/ static void vl_ikm_push_elkan (VlIKMFilt *f, vl_uint32 *asgn, vl_uint8 const *data, vl_size N) { vl_uindex i,c,cx,x ; vl_size dist_calc = 0 ; vl_ikmacc_t dist, best_dist ; vl_ikmacc_t *d_pt = f->inter_dist ; /* assign data to centers */ for(x = 0 ; x < N ; ++x) { best_dist = VL_IKMACC_MAX ; cx = 0 ; for(c = 0 ; c < f->K ; ++c) { if(d_pt[f->K * cx + c] < best_dist) { /* might need to be updated */ dist_calc ++ ; for(dist=0, i = 0 ; i < f->M ; ++i) { vl_ikmacc_t delta = data[x * f->M + i] - f->centers[c * f->M + i] ; dist += delta * delta ; } /* u_pt is strict at the beginning */ if(dist < best_dist) { best_dist = dist ; cx = c ; } } } asgn[x] = (vl_uint32)cx ; } } /* * Local Variables: * * mode: C * * End: * */ colmap-3.9.1/src/thirdparty/VLFeat/ikmeans_init.tc000077500000000000000000000074771454702036400221320ustar00rootroot00000000000000/** @file ikmeans_init.tc ** @brief Integer K-Means - Initialization - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #include "random.h" /* pairs are used to generate random permutations of data */ typedef struct { vl_index w; vl_index j; } pair_t; static int cmp_pair (void const *a, void const *b) { pair_t *pa = (pair_t *) a; pair_t *pb = (pair_t *) b; int d = (int)(pa->w - pb->w) ; if (d) return d ; /* break ties based on index (compensates for qsort not being stable) */ return (int)(pa->j - pb->j); } VL_INLINE vl_ikmacc_t calc_dist2 (vl_ikmacc_t const* A, vl_uint8 const* B, vl_size M) { vl_ikmacc_t acc = 0 ; vl_uindex i = 0 ; for (i = 0 ; i < M ; ++i) { vl_ikmacc_t dist = (vl_ikmacc_t)A[i] - (vl_ikmacc_t)B[i] ; acc += (vl_uindex)(dist * dist) ; } return acc ; } /** @internal ** @brief Helper function to allocate memory for an IKM quantizer ** @param f quantizer. ** @param M data dimensionality. ** @param K number of clusters. **/ static void alloc (VlIKMFilt *f, vl_size M, vl_size K) { if (f->centers) vl_free(f->centers) ; f->K = K ; f->M = M ; f->centers = vl_malloc(sizeof(vl_ikmacc_t) * M * K) ; } /** @brief Helper function to initialize the quantizer ** @param f IKM quantizer. **/ static void vl_ikm_init_helper (VlIKMFilt *f) { switch (f-> method) { case VL_IKM_LLOYD: vl_ikm_init_lloyd (f) ; break ; case VL_IKM_ELKAN: vl_ikm_init_elkan (f) ; break ; } } /** @brief Initialize quantizer with centers ** @param f IKM quantizer. ** @param centers centers. ** @param M data dimensionality. ** @param K number of clusters. **/ VL_EXPORT void vl_ikm_init (VlIKMFilt* f, vl_ikmacc_t const * centers, vl_size M, vl_size K) { alloc (f, M, K) ; memcpy (f->centers, centers, sizeof(vl_ikmacc_t) * M * K) ; vl_ikm_init_helper (f) ; } /** @brief Initialize quantizer with random centers ** @param f IKM quantizer. ** @param M data dimensionality. ** @param K number of clusters. **/ VL_EXPORT void vl_ikm_init_rand (VlIKMFilt* f, vl_size M, vl_size K) { vl_uindex k, i ; VlRand * rand = vl_get_rand() ; alloc (f, M, K) ; for (k = 0 ; k < K ; ++ k) { for (i = 0 ; i < M ; ++ i) { f-> centers [k * M + i] = (vl_ikmacc_t) (vl_rand_uint32 (rand)) ; } } vl_ikm_init_helper (f) ; } /** @brief Initialize with centers from random data ** @param f IKM quantizer. ** @param data data. ** @param M data dimensionality. ** @param N number of data. ** @param K number of clusters. **/ VL_EXPORT void vl_ikm_init_rand_data (VlIKMFilt* f, vl_uint8 const* data, vl_size M, vl_size N, vl_size K) { vl_uindex i, j, k ; VlRand *rand = vl_get_rand () ; pair_t *pairs = (pair_t *) vl_malloc (sizeof(pair_t) * N); alloc (f, M, K) ; /* permute the data randomly */ for (j = 0 ; j < N ; ++j) { pairs[j].j = j ; pairs[j].w = ((vl_int32) vl_rand_uint32 (rand)) >> 2 ; } qsort (pairs, N, sizeof(pair_t), cmp_pair); /* initialize centers from random data points */ for (j = 0, k = 0 ; k < K ; ++ k) { /* search for the next candidate which is not a dup */ for ( ; j < N - 1 ; ++j) { vl_uindex prevk = 0 ; for (prevk = 0 ; prevk < k ; ++ prevk) { vl_ikmacc_t dist = calc_dist2(f-> centers + prevk * M, data + pairs[j].j * M, M) ; if (dist == 0) break ; } if (prevk == k) break ; } for (i = 0 ; i < M ; ++ i) { f-> centers [k * M + i] = data [(vl_uint64)pairs[j].j * M + i] ; } if (j < N - 1) ++ j ; } vl_free (pairs) ; vl_ikm_init_helper (f) ; } /* * Local Variables: * * mode: C * * End: * */ colmap-3.9.1/src/thirdparty/VLFeat/ikmeans_lloyd.tc000077500000000000000000000065651454702036400223070ustar00rootroot00000000000000/** @file ikmeans_lloyd.tc ** @brief Integer K-Means - LLoyd Algorithm - Definition ** @author Brian Fulkerson ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @internal ** @brief Helper function to initialize a filter for Lloyd algorithm ** ** @param f filter. **/ static void vl_ikm_init_lloyd (VlIKMFilt * f VL_UNUSED) { } /** @internal ** @brief LLoyd algorithm ** @param f IKM quantizer. ** @param data Training data. ** @param N Number of traning data. ** @return error code. **/ static int vl_ikm_train_lloyd (VlIKMFilt* f, vl_uint8 const* data, vl_size N) { int err = 0 ; vl_uindex iter, i, j, k ; vl_uint32 *asgn = vl_malloc (sizeof(vl_uint32) * N) ; vl_uint32 *counts = vl_malloc (sizeof(vl_uint32) * N) ; for (iter = 0 ; 1 ; ++ iter) { vl_bool done = 1 ; /* --------------------------------------------------------------- * Calc. assignments * ------------------------------------------------------------ */ for (j = 0 ; j < N ; ++j) { vl_ikmacc_t best_dist = 0 ; vl_index best = -1 ; for (k = 0; k < f->K ; ++k) { vl_ikmacc_t dist = 0 ; /* compute distance with this center */ for (i = 0; i < f->M ; ++i) { vl_ikmacc_t delta = data [j * f->M + i] - f->centers [k * f->M + i] ; dist += delta * delta ; } /* compare with current best */ if (best == -1 || dist < best_dist) { best = k ; best_dist = dist ; } } if (asgn [j] != best) { asgn [j] = (vl_uint32) best ; done = 0 ; } } /* stopping condition */ if (done || iter == f->max_niters) break ; /* --------------------------------------------------------------- * Calc. centers * ------------------------------------------------------------ */ /* re-compute centers */ memset (f->centers, 0, sizeof(*f->centers) * f->M * f->K); memset (counts, 0, sizeof(*counts) * f->K); for (j = 0; j < N; ++j) { vl_uindex this_center = asgn [j] ; ++ counts [this_center] ; for (i = 0; i < f->M ; ++i) { f->centers [this_center * f->M + i] += data[j * f->M + i] ; } } for (k = 0; k < f->K; ++k) { vl_index n = counts [k]; if (n > 0xffffff) { err = 1 ; } if (n > 0) { for (i = 0; i < f->M; ++i) { f->centers [k * f->M + i] /= n; } } else { /* If no data are assigned to the center, it is not changed with respect to the previous iteration, so we do not do anything. */ } } } vl_free (counts) ; vl_free (asgn) ; return err ; } /** @internal ** @brief LLoyd algorithm ** @param f IKM quantizer. ** @param asgn Assignments (out). ** @param data Data to quantize. ** @param N Number of data. **/ static void vl_ikm_push_lloyd (VlIKMFilt *f, vl_uint32 *asgn, vl_uint8 const *data, vl_size N) { vl_uindex j ; for(j = 0 ; j < N ; ++j) { asgn[j] = vl_ikm_push_one (f->centers, data + j * f->M, f->M, f->K); } } /* * Local Variables: * * mode: C * * End: * */ colmap-3.9.1/src/thirdparty/VLFeat/imopv.c000077500000000000000000001046221454702036400204140ustar00rootroot00000000000000/** @file imopv.c ** @brief Vectorized image operations - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file imopv.h ** ** This module provides the following image operations: ** ** - Separable convolution. The function ::vl_imconvcol_vf() ** can be used to compute separable convolutions. ** ** - Convolution by a triangular kernel. The function ** vl_imconvcoltri_vf() is an optimized convolution routine for ** triangular kernels. ** ** - Distance transform. ::vl_image_distance_transform_f() is ** a linear algorithm to compute the distance transform of an ** image. ** ** @remark Some operations are optimized to exploit possible SIMD ** instructions. This requires image data to be properly aligned (typically ** to 16 bytes). Similalry, the image stride (the number of bytes to skip to move ** to the next image row), must be aligned. **/ #ifndef VL_IMOPV_INSTANTIATING #include "imopv.h" #include "imopv_sse2.h" #include "mathop.h" #define FLT VL_TYPE_FLOAT #define VL_IMOPV_INSTANTIATING #include "imopv.c" #define FLT VL_TYPE_DOUBLE #define VL_IMOPV_INSTANTIATING #include "imopv.c" #define FLT VL_TYPE_UINT32 #define VL_IMOPV_INSTANTIATING #include "imopv.c" #define FLT VL_TYPE_INT32 #define VL_IMOPV_INSTANTIATING #include "imopv.c" /* VL_IMOPV_INSTANTIATING */ #endif #if defined(VL_IMOPV_INSTANTIATING) || defined(__DOXYGEN__) #include "float.h" /* ---------------------------------------------------------------- */ /* Image Convolution */ /* ---------------------------------------------------------------- */ #if (FLT == VL_TYPE_FLOAT || FLT == VL_TYPE_DOUBLE) /** @fn vl_imconvcol_vd(double*,vl_size,double const*,vl_size,vl_size,vl_size,double const*,vl_index,vl_index,int,unsigned int) ** @brief Convolve image along columns ** ** @param dst destination image. ** @param dst_stride width of the destination image including padding. ** @param src source image. ** @param src_width width of the source image. ** @param src_height height of the source image. ** @param src_stride width of the source image including padding. ** @param filt filter kernel. ** @param filt_begin coordinate of the first filter element. ** @param filt_end coordinate of the last filter element. ** @param step sub-sampling step. ** @param flags operation modes. ** ** The function convolves the column of the image @a src by the ** filter @a filt and saves the result to the image @a dst. The size ** of @a dst must be equal to the size of @a src. Formally, this ** results in the calculation ** ** @f[ ** \mathrm{dst} [x,y] = \sum_{p=y-\mathrm{filt\_end}}^{y-\mathrm{filt\_begin}} ** \mathrm{src}[x,y] \mathrm{filt}[y - p - \mathrm{filt\_begin}] ** @f] ** ** The function subsamples the image along the columns according to ** the parameter @a step. Setting @a step to 1 (one) computes the ** elements @f$\mathrm{dst}[x,y]@f$ for all pairs (x,0), (x,1), (x,2) ** and so on. Setting @a step two 2 (two) computes only (x,0), (x,2) ** and so on (in this case the height of the destination image is ** floor(src_height/step)+1). ** ** Calling twice the function can be used to compute 2-D separable ** convolutions. Use the flag ::VL_TRANSPOSE to transpose the result ** (in this case @a dst has transposed dimension as well). ** ** The function allows the support of the filter to be any range. ** Usually the support is @a filt_end = -@a filt_begin. ** ** The convolution operation may pick up values outside the image ** boundary. To cope with this edge cases, the function either pads ** the image by zero (::VL_PAD_BY_ZERO) or with the values at the ** boundary (::VL_PAD_BY_CONTINUITY). **/ /** @fn vl_imconvcol_vf(float*,vl_size,float const*,vl_size,vl_size,vl_size,float const*,vl_index,vl_index,int,unsigned int) ** @see ::vl_imconvcol_vd **/ VL_EXPORT void VL_XCAT(vl_imconvcol_v, SFX) (T* dst, vl_size dst_stride, T const* src, vl_size src_width, vl_size src_height, vl_size src_stride, T const* filt, vl_index filt_begin, vl_index filt_end, int step, unsigned int flags) { vl_index x = 0 ; vl_index y ; vl_index dheight = (src_height - 1) / step + 1 ; vl_bool transp = flags & VL_TRANSPOSE ; vl_bool zeropad = (flags & VL_PAD_MASK) == VL_PAD_BY_ZERO ; /* dispatch to accelerated version */ #ifndef VL_DISABLE_SSE2 if (vl_cpu_has_sse2() && vl_get_simd_enabled()) { VL_XCAT3(_vl_imconvcol_v,SFX,_sse2) (dst,dst_stride, src,src_width,src_height,src_stride, filt,filt_begin,filt_end, step,flags) ; return ; } #endif /* let filt point to the last sample of the filter */ filt += filt_end - filt_begin ; while (x < (signed)src_width) { /* Calculate dest[x,y] = sum_p image[x,p] filt[y - p] * where supp(filt) = [filt_begin, filt_end] = [fb,fe]. * * CHUNK_A: y - fe <= p < 0 * completes VL_MAX(fe - y, 0) samples * CHUNK_B: VL_MAX(y - fe, 0) <= p < VL_MIN(y - fb, height - 1) * completes fe - VL_MAX(fb, height - y) + 1 samples * CHUNK_C: completes all samples */ T const *filti ; vl_index stop ; for (y = 0 ; y < (signed)src_height ; y += step) { T acc = 0 ; T v = 0, c ; T const* srci ; filti = filt ; stop = filt_end - y ; srci = src + x - stop * src_stride ; if (stop > 0) { if (zeropad) { v = 0 ; } else { v = *(src + x) ; } while (filti > filt - stop) { c = *filti-- ; acc += v * c ; srci += src_stride ; } } stop = filt_end - VL_MAX(filt_begin, y - (signed)src_height + 1) + 1 ; while (filti > filt - stop) { v = *srci ; c = *filti-- ; acc += v * c ; srci += src_stride ; } if (zeropad) v = 0 ; stop = filt_end - filt_begin + 1 ; while (filti > filt - stop) { c = *filti-- ; acc += v * c ; } if (transp) { *dst = acc ; dst += 1 ; } else { *dst = acc ; dst += dst_stride ; } } /* next y */ if (transp) { dst += 1 * dst_stride - dheight * 1 ; } else { dst += 1 * 1 - dheight * dst_stride ; } x += 1 ; } /* next x */ } /* VL_TYPE_FLOAT, VL_TYPE_DOUBLE */ #endif /* ---------------------------------------------------------------- */ /* Image distance transform */ /* ---------------------------------------------------------------- */ #if (FLT == VL_TYPE_FLOAT || FLT == VL_TYPE_DOUBLE) /** @fn ::vl_image_distance_transform_d(double const*,vl_size,vl_size,vl_size,vl_size,double*,vl_uindex*,double,double) ** @brief Compute the distance transform of an image ** @param image image. ** @param numColumns number of columns of the image. ** @param numRows number of rows of the image. ** @param columnStride offset from one column to the next. ** @param rowStride offset from one row to the next. ** @param distanceTransform distance transform (out). ** @param indexes nearest neighbor indexes (in/out). ** @param coeff quadratic cost coefficient (non-negative). ** @param offset quadratic cost offset. ** ** The function computes the distance transform along the first ** dimension of the image @a image. Let @f$ I(u,v) @f$ be @a image. ** Its distance transfrom @f$ D(u,v) @f$ is given by: ** ** @f[ ** u^*(u,v) = \min_{u'} I(u',v) + \mathtt{coeff} (u' - u - \mathtt{offset})^2, ** \quad D(u,v) = I(u^*(u,v),v). ** @f] ** ** Notice that @a coeff must be non negative. ** ** The function fills in the buffer @a distanceTransform with @f$ D ** @f$. This buffer must have the same size as @a image. ** ** If @a indexes is not @c NULL, it must be a matrix of the same size ** o the image. The function interprets the value of this matrix as ** indexes of the pixels, i.e @f$ \mathtt{indexes}(u,v) @f$ is the ** index of pixel @f$ (u,v) @f$. On output, the matrix @a indexes ** contains @f$ \mathtt{indexes}(u^*(u,v),v) @f$. This information ** can be used to determine for each pixel @f$ (u,v) @f$ its ** “nearest neighbor&rdquo. ** ** Notice that by swapping @a numRows and @a numColumns and @a ** columnStride and @a rowStride, the function can be made to operate ** along the other image dimension. Specifically, to compute the ** distance transform along columns and rows, call the functinon ** twice: *** ** @code ** for (i = 0 ; i < numColumns * numRows ; ++i) indexes[i] = i ; ** vl_image_distance_transform_d(image,numColumns,numRows,1,numColumns, ** distanceTransform,indexes,u_coeff,u_offset) ; ** vl_image_distance_transform_d(distanceTransform,numRows,numColumns,numColumns,1, ** distanceTransform,indexes,u_coeff,u_offset) ; ** @endcode ** ** @par Algorithm ** ** The function implements the algorithm described in: ** P. F. Felzenszwalb and D. P. Huttenlocher, Distance Transforms ** of Sampled Functions, Technical Report, Cornell University, ** 2004. ** ** Since the algorithm operates along one dimension per time, ** consider the 1D version of the problem for simplicity: ** ** @f[ ** d(y) = \min_{x} g(y;x), \quad g(y;x) = f(x) + \alpha (y - x - \beta)^2, ** \quad x,y \in \{0,1,\dots,N-1\}. ** @f] ** ** Hence the distance transform @f$ d(y) @f$ is the lower envelope of ** the family of parabolas @f$ g(y;x) @f$ indexed by @f$ x ** @f$. Notice that all parabolas have the same curvature and that ** their centers are located at @f$ x + \beta, @f$ @f$ x=0,\dots,N-1 ** @f$. The algorithm considers one parabola per time, from left to ** right, and finds the interval for which the parabola belongs to ** the lower envelope (if any). ** ** Initially, only the leftmost parabola @f$ g(y;0) @f$ has been ** considered, and its validity interval is @f$(-\infty, \infty) @f$. ** Then the second parabola @f$ g(y;1) @f$ is considered. As long as ** @f$ \alpha > 0 @f$, the two parabolas @f$ g(y;0),\ g(y;1) @f$ ** intersect at a unique point @f$ \bar y @f$. Then the first ** parabola belongs to the envelope in the interval @f$ (-\infty, ** \bar y] @f$ and the second one in the interval @f$ (\bar y, ** +\infty] @f$. When the third parabola @f$ g(y;2) @f$ is ** considered, the intersection point @f$ \hat y @f$ with the ** previously added parabola @f$ g(y;1) @f$ is found. Now two cases ** may arise: ** ** - @f$ \hat y > \bar y @f$, in which case all three parabolas ** belong to the envelope in the intervals @f$ (-\infty,\bar y], ** (\bar y, \hat y], (\hat y, +\infty] @f$. ** ** - @f$ \hat y \leq \bar y @f$, in which case the second parabola ** @f$ g(y;1) @f$ has no point beloning to the envelope, and it is ** removed. One then remains with the two parabolas @f$ g(y;0),\ ** g(y;2) @f$ and the algorithm is re-iterated. ** ** The algorithm proceeds in this fashion. Every time a new parabola ** is considered, its intersection point with the previously added ** parabola on the left is computed, and that parabola is potentially ** removed. The cost of an iteration is 1 plus the number of deleted ** parabolas. Since there are @f$ N @f$ iterations and at most @f$ N ** @f$ parabolas to delete overall, the complexity is linear, ** i.e. @f$ O(N) @f$. **/ /** @fn ::vl_image_distance_transform_f(float const*,vl_size,vl_size,vl_size,vl_size,float*,vl_uindex*,float,float) ** @see ::vl_image_distance_transform_d **/ VL_EXPORT void VL_XCAT(vl_image_distance_transform_,SFX) (T const * image, vl_size numColumns, vl_size numRows, vl_size columnStride, vl_size rowStride, T * distanceTransform, vl_uindex * indexes, T coeff, T offset) { /* Each image pixel corresponds to a parabola. The algorithm scans such parabolas from left to right, keeping track of which parabolas belong to the lower envelope and in which interval. There are NUM active parabolas, FROM stores the beginning of the interval for which a certain parabola is part of the envoelope, and WHICH store the index of the parabola (that is, the pixel x from which the parabola originated). */ vl_uindex x, y ; T * from = vl_malloc (sizeof(T) * (numColumns + 1)) ; T * base = vl_malloc (sizeof(T) * numColumns) ; vl_uindex * baseIndexes = vl_malloc (sizeof(vl_uindex) * numColumns) ; vl_uindex * which = vl_malloc (sizeof(vl_uindex) * numColumns) ; vl_uindex num = 0 ; for (y = 0 ; y < numRows ; ++y) { num = 0 ; for (x = 0 ; x < numColumns ; ++x) { T r = image[x * columnStride + y * rowStride] ; T x2 = x * x ; #if (FLT == VL_TYPE_FLOAT) T from_ = - VL_INFINITY_F ; #else T from_ = - VL_INFINITY_D ; #endif /* Add next parabola (there are NUM so far). The algorithm finds intersection INTERS with the previously added parabola. If the intersection is on the right of the "starting point" of this parabola, then the previous parabola is kept, and the new one is added to its right. Otherwise the new parabola "eats" the old one, which gets deleted and the check is repeated with the parabola added before the deleted one. */ while (num >= 1) { vl_uindex x_ = which[num - 1] ; T x2_ = x_ * x_ ; T r_ = image[x_ * columnStride + y * rowStride] ; T inters ; if (r == r_) { /* handles the case r = r_ = \pm inf */ inters = (x + x_) / 2.0 + offset ; } #if (FLT == VL_TYPE_FLOAT) else if (coeff > VL_EPSILON_F) #else else if (coeff > VL_EPSILON_D) #endif { inters = ((r - r_) + coeff * (x2 - x2_)) / (x - x_) / (2*coeff) + offset ; } else { /* If coeff is very small, the parabolas are flat (= lines). In this case the previous parabola should be deleted if the current pixel has lower score */ #if (FLT == VL_TYPE_FLOAT) inters = (r < r_) ? - VL_INFINITY_F : VL_INFINITY_F ; #else inters = (r < r_) ? - VL_INFINITY_D : VL_INFINITY_D ; #endif } if (inters <= from [num - 1]) { /* delete a previous parabola */ -- num ; } else { /* accept intersection */ from_ = inters ; break ; } } /* add a new parabola */ which[num] = x ; from[num] = from_ ; base[num] = r ; if (indexes) baseIndexes[num] = indexes[x * columnStride + y * rowStride] ; num ++ ; } /* next column */ #if (FLT == VL_TYPE_FLOAT) from[num] = VL_INFINITY_F ; #else from[num] = VL_INFINITY_D ; #endif /* fill in */ num = 0 ; for (x = 0 ; x < numColumns ; ++x) { double delta ; while (x >= from[num + 1]) ++ num ; delta = (double) x - (double) which[num] - offset ; distanceTransform[x * columnStride + y * rowStride] = base[num] + coeff * delta * delta ; if (indexes) { indexes[x * columnStride + y * rowStride] = baseIndexes[num] ; } } } /* next row */ vl_free (from) ; vl_free (which) ; vl_free (base) ; vl_free (baseIndexes) ; } /* VL_TYPE_FLOAT, VL_TYPE_DOUBLE */ #endif /* ---------------------------------------------------------------- */ /* Image convolution by a triangular kernel */ /* ---------------------------------------------------------------- */ #if (FLT == VL_TYPE_FLOAT || FLT == VL_TYPE_DOUBLE) /** @fn vl_imconvcoltri_d(double*,vl_size,double const*,vl_size,vl_size,vl_size,vl_size,vl_size,int unsigned) ** @brief Convolve an image along the columns with a triangular kernel ** @param dest destination image. ** @param destStride destination image stride. ** @param image image to convolve. ** @param imageWidth width of the image. ** @param imageHeight height of the image. ** @param imageStride width of the image including padding. ** @param filterSize size of the triangular filter. ** @param step sub-sampling step. ** @param flags operation modes. ** ** The function convolves the columns of the image @a image with the ** triangular kernel ** ** @f[ ** k(t) = \frac{1}{\Delta^2} \max\{ \Delta - |t|, 0 \}, ** \quad t \in \mathbb{Z} ** @f] ** ** The paramter @f$ \Delta @f$, equal to the function argument @a ** filterSize, controls the width of the kernel. Notice that the ** support of @f$ k(x) @f$ as a continuous function of @f$ x @f$ is ** the open interval @f$ (-\Delta,\Delta) @f$, which has length @f$ ** 2\Delta @f$. However, @f$ k(x) @f$ restricted to the ingeter ** domain @f$ x \in \mathcal{Z} @f$ has support @f$ \{ -\Delta + 1, ** \Delta +2, \dots, \Delta-1 \} @f$, which counts @f$ 2 \Delta - 1 ** @f$ elements only. In particular, the discrete kernel is symmetric ** about the origin for all values of @f$ \Delta @f$. ** ** The normalization factor @f$ 1 / \Delta^2 @f$ guaratnees that the ** filter is normalized to one, i.e.: ** ** @f[ ** \sum_{t=-\infty}^{+\infty} k(t) = 1 ** @f] ** ** @par Algorithm ** ** The function exploits the fact that convolution by a triangular ** kernel can be expressed as the repeated convolution by a ** rectangular kernel, and that the latter can be performed in time ** indepenedent on the fiter width by using an integral-image type ** trick. Overall, the algorithm complexity is independent on the ** parameter @a filterSize and linear in the nubmer of image pixels. ** ** @see ::vl_imconvcol_vd for details on the meaning of the other parameters. **/ /** @fn vl_imconvcoltri_f(float*,vl_size,float const*,vl_size,vl_size,vl_size,vl_size,vl_size,int unsigned) ** @brief Convolve an image along the columns with a triangular kernel ** @see ::vl_imconvcoltri_d() **/ VL_EXPORT void VL_XCAT(vl_imconvcoltri_, SFX) (T * dest, vl_size destStride, T const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride, vl_size filterSize, vl_size step, unsigned int flags) { vl_index x, y, dheight ; vl_bool transp = flags & VL_TRANSPOSE ; vl_bool zeropad = (flags & VL_PAD_MASK) == VL_PAD_BY_ZERO ; T scale = (T) (1.0 / ((double)filterSize * (double)filterSize)) ; T * buffer = vl_malloc (sizeof(T) * (imageHeight + filterSize)) ; buffer += filterSize ; if (imageHeight == 0) { return ; } x = 0 ; dheight = (imageHeight - 1) / step + 1 ; while (x < (signed)imageWidth) { T const * imagei ; imagei = image + x + imageStride * (imageHeight - 1) ; /* We decompose the convolution by a triangluar signal as the convolution * by two rectangular signals. The rectangular convolutions are computed * quickly by computing the integral signals. Each rectangular convolution * introduces a delay, which is compensated by convolving each in opposite * directions. */ /* integrate backward the column */ buffer[imageHeight - 1] = *imagei ; for (y = (signed)imageHeight - 2 ; y >= 0 ; --y) { imagei -= imageStride ; buffer[y] = buffer[y + 1] + *imagei ; } if (zeropad) { for ( ; y >= - (signed)filterSize ; --y) { buffer[y] = buffer[y + 1] ; } } else { for ( ; y >= - (signed)filterSize ; --y) { buffer[y] = buffer[y + 1] + *imagei ; } } /* compute the filter forward */ for (y = - (signed)filterSize ; y < (signed)imageHeight - (signed)filterSize ; ++y) { buffer[y] = buffer[y] - buffer[y + filterSize] ; } if (! zeropad) { for (y = (signed)imageHeight - (signed)filterSize ; y < (signed)imageHeight ; ++y) { buffer[y] = buffer[y] - buffer[imageHeight - 1] * ((signed)imageHeight - (signed)filterSize - y) ; } } /* integrate forward the column */ for (y = - (signed)filterSize + 1 ; y < (signed)imageHeight ; ++y) { buffer[y] += buffer[y - 1] ; } /* compute the filter backward */ { vl_size stride = transp ? 1 : destStride ; dest += dheight * stride ; for (y = step * (dheight - 1) ; y >= 0 ; y -= step) { dest -= stride ; *dest = scale * (buffer[y] - buffer[y - (signed)filterSize]) ; } dest += transp ? destStride : 1 ; } x += 1 ; } /* next x */ vl_free (buffer - filterSize) ; } /* VL_TYPE_FLOAT, VL_TYPE_DOUBLE */ #endif /* ---------------------------------------------------------------- */ /* Gaussian Smoothing */ /* ---------------------------------------------------------------- */ #if (FLT == VL_TYPE_FLOAT || FLT == VL_TYPE_DOUBLE) /** @fn vl_imsmooth_d(double*,vl_size,double const*,vl_size,vl_size,vl_size,double,double) ** @brief Smooth an image with a Gaussian filter ** @param smoothed ** @param smoothedStride ** @param image ** @param width ** @param height ** @param stride ** @param sigmax ** @param sigmay **/ /** @fn vl_imsmooth_f(float*,vl_size,float const*,vl_size,vl_size,vl_size,double,double) ** @brief Smooth an image with a Gaussian filter ** @see ::vl_imsmooth_d **/ static T* VL_XCAT(_vl_new_gaussian_fitler_,SFX)(vl_size *size, double sigma) { T* filter ; T mass = (T)1.0 ; vl_index i ; vl_size width = vl_ceil_d(sigma * 3.0) ; *size = 2 * width + 1 ; assert(size) ; filter = vl_malloc((*size) * sizeof(T)) ; filter[width] = 1.0 ; for (i = 1 ; i <= (signed)width ; ++i) { double x = (double)i / sigma ; double g = exp(-0.5 * x * x) ; mass += g + g ; filter[width-i] = g ; filter[width+i] = g ; } for (i = 0 ; i < (signed)(*size) ; ++i) {filter[i] /= mass ;} return filter ; } VL_EXPORT void VL_XCAT(vl_imsmooth_, SFX) (T * smoothed, vl_size smoothedStride, T const *image, vl_size width, vl_size height, vl_size stride, double sigmax, double sigmay) { T *filterx, *filtery, *buffer ; vl_size sizex, sizey ; filterx = VL_XCAT(_vl_new_gaussian_fitler_,SFX)(&sizex,sigmax) ; if (sigmax == sigmay) { filtery = filterx ; sizey = sizex ; } else { filtery = VL_XCAT(_vl_new_gaussian_fitler_,SFX)(&sizey,sigmay) ; } buffer = vl_malloc(width*height*sizeof(T)) ; VL_XCAT(vl_imconvcol_v,SFX) (buffer, height, image, width, height, stride, filtery, -((signed)sizey-1)/2, ((signed)sizey-1)/2, 1, VL_PAD_BY_CONTINUITY | VL_TRANSPOSE) ; VL_XCAT(vl_imconvcol_v,SFX) (smoothed, smoothedStride, buffer, height, width, height, filterx, -((signed)sizex-1)/2, ((signed)sizex-1)/2, 1, VL_PAD_BY_CONTINUITY | VL_TRANSPOSE) ; vl_free(buffer) ; vl_free(filterx) ; if (sigmax != sigmay) { vl_free(filtery) ; } } /* VL_TYPE_FLOAT, VL_TYPE_DOUBLE */ #endif /* ---------------------------------------------------------------- */ /* Image Gradient */ /* ---------------------------------------------------------------- */ #if (FLT == VL_TYPE_FLOAT || FLT == VL_TYPE_DOUBLE) /** @fn vl_imgradient_d(double*,double*,vl_size,vl_size,double*,vl_size,vl_size,vl_size) ** @brief Compute image gradient ** @param xGradient Pointer to amplitude gradient plane ** @param yGradient Pointer to angle gradient plane ** @param gradWidthStride Width of the gradient plane including padding ** @param gradHeightStride Height of the gradient plane including padding ** @param image Pointer to the source image ** @param imageWidth Source image width ** @param imageHeight Source image height ** @param imageStride Width of the image including padding. ** ** This functions computes the amplitudes and angles of input image gradient. ** ** Gradient is computed simple by gradient kernel \f$ (-1 ~ 1) \f$, ** \f$ (-1 ~ 1)^T \f$ for border pixels and with sobel filter kernel ** \f$ (-0.5 ~ 0 ~ 0.5) \f$, \f$ (-0.5 ~ 0 ~ 0.5)^T \f$ otherwise on the input ** image @a image yielding x-gradient \f$ dx \f$, stored in @a xGradient and ** y-gradient \f$ dy \f$, stored in @a yGradient, respectively. ** ** This function also allows to process only part of the input image ** defining the @a imageStride as original image width and @a width as ** width of the sub-image. ** ** Also it allows to easily align the output data by definition ** of the @a gradWidthStride and @a gradHeightStride . **/ /** @fn vl_imgradient_f(float*,float*,vl_size,vl_size,float*,vl_size,vl_size,vl_size) ** @brief Compute image gradient ** @see ::vl_imgradient_d **/ VL_EXPORT void VL_XCAT(vl_imgradient_, SFX) (T * xGradient, T * yGradient, vl_size gradWidthStride, vl_size gradHeightStride, T const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride) { /* Shortcuts */ vl_index const xo = 1 ; vl_index const yo = imageStride ; vl_size const w = imageWidth; vl_size const h = imageHeight; T const *src, *end ; T *pgrad_x, *pgrad_y; vl_size y; src = image ; pgrad_x = xGradient ; pgrad_y = yGradient ; /* first pixel of the first row */ *pgrad_x = src[+xo] - src[0] ; pgrad_x += gradWidthStride; *pgrad_y = src[+yo] - src[0] ; pgrad_y += gradWidthStride; src++; /* middle pixels of the first row */ end = (src - 1) + w - 1 ; while (src < end) { *pgrad_x = 0.5 * (src[+xo] - src[-xo]) ; pgrad_x += gradWidthStride; *pgrad_y = src[+yo] - src[0] ; pgrad_y += gradWidthStride; src++; } /* last pixel of the first row */ *pgrad_x = src[0] - src[-xo] ; pgrad_x += gradWidthStride; *pgrad_y = src[+yo] - src[0] ; pgrad_y += gradWidthStride; src++; xGradient += gradHeightStride; pgrad_x = xGradient; yGradient += gradHeightStride; pgrad_y = yGradient; image += yo; src = image; for (y = 1 ; y < h -1 ; ++y) { /* first pixel of the middle rows */ *pgrad_x = src[+xo] - src[0] ; pgrad_x += gradWidthStride; *pgrad_y = 0.5 * (src[+yo] - src[-yo]) ; pgrad_y += gradWidthStride; src++; /* middle pixels of the middle rows */ end = (src - 1) + w - 1 ; while (src < end) { *pgrad_x = 0.5 * (src[+xo] - src[-xo]) ; pgrad_x += gradWidthStride; *pgrad_y = 0.5 * (src[+yo] - src[-yo]) ; pgrad_y += gradWidthStride; src++; } /* last pixel of the middle row */ *pgrad_x = src[0] - src[-xo] ; pgrad_x += gradWidthStride; *pgrad_y = 0.5 * (src[+yo] - src[-yo]) ; pgrad_y += gradWidthStride; src++; xGradient += gradHeightStride; pgrad_x = xGradient; yGradient += gradHeightStride; pgrad_y = yGradient; image += yo; src = image; } /* first pixel of the last row */ *pgrad_x = src[+xo] - src[0] ; pgrad_x += gradWidthStride; *pgrad_y = src[ 0] - src[-yo] ; pgrad_y += gradWidthStride; src++; /* middle pixels of the last row */ end = (src - 1) + w - 1 ; while (src < end) { *pgrad_x = 0.5 * (src[+xo] - src[-xo]) ; pgrad_x += gradWidthStride; *pgrad_y = src[0] - src[-yo] ; pgrad_y += gradWidthStride; src++; } /* last pixel of the last row */ *pgrad_x = src[0] - src[-xo] ; *pgrad_y = src[0] - src[-yo] ; } /* VL_TYPE_FLOAT, VL_TYPE_DOUBLE */ #endif /** @fn vl_imgradient_polar_d(double*,double*,vl_size,vl_size,double const*,vl_size,vl_size,vl_size) ** @brief Compute gradient mangitudes and directions of an image. ** @param amplitudeGradient Pointer to amplitude gradient plane ** @param angleGradient Pointer to angle gradient plane ** @param gradWidthStride Width of the gradient plane including padding ** @param gradHeightStride Height of the gradient plane including padding ** @param image Pointer to the source image ** @param imageWidth Source image width ** @param imageHeight Source image height ** @param imageStride Width of the source image including padding. ** ** This functions computes the amplitudes and angles of input image gradient. ** ** Gradient is computed simple by gradient kernel \f$ (-1 ~ 1) \f$, ** \f$ (-1 ~ 1)^T \f$ for border pixels and with sobel filter kernel ** \f$ (-0.5 ~ 0 ~ 0.5) \f$, \f$ (-0.5 ~ 0 ~ 0.5)^T \f$ otherwise on ** the input image @a image yielding x-gradient \f$ dx \f$, stored in ** @a xGradient and y-gradient \f$ dy \f$, stored in @a yGradient, ** respectively. ** ** The amplitude of the gradient, stored in plane @a ** amplitudeGradient, is then calculated as \f$ \sqrt(dx^2+dy^2) \f$ ** and the angle of the gradient, stored in @a angleGradient is \f$ ** atan(\frac{dy}{dx}) \f$ normalised into interval 0 and @f$ 2\pi ** @f$. ** ** This function also allows to process only part of the input image ** defining the @a imageStride as original image width and @a width ** as width of the sub-image. ** ** Also it allows to easily align the output data by definition ** of the @a gradWidthStride and @a gradHeightStride . **/ /** @fn vl_imgradient_polar_f(float*,float*,vl_size,vl_size,float const*,vl_size,vl_size,vl_size) ** @see ::vl_imgradient_polar_d **/ #if (FLT == VL_TYPE_FLOAT || FLT == VL_TYPE_DOUBLE) VL_EXPORT void VL_XCAT(vl_imgradient_polar_, SFX) (T * gradientModulus, T * gradientAngle, vl_size gradientHorizontalStride, vl_size gradHeightStride, T const* image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride) { /* Shortcuts */ vl_index const xo = 1 ; vl_index const yo = imageStride ; vl_size const w = imageWidth; vl_size const h = imageHeight; T const *src, *end; T *pgrad_angl, *pgrad_ampl; T gx, gy ; vl_size y; #define SAVE_BACK \ *pgrad_ampl = vl_fast_sqrt_f (gx*gx + gy*gy) ; \ pgrad_ampl += gradientHorizontalStride ; \ *pgrad_angl = vl_mod_2pi_f (vl_fast_atan2_f (gy, gx) + 2*VL_PI) ; \ pgrad_angl += gradientHorizontalStride ; \ ++src ; \ src = image ; pgrad_angl = gradientAngle ; pgrad_ampl = gradientModulus ; /* first pixel of the first row */ gx = src[+xo] - src[0] ; gy = src[+yo] - src[0] ; SAVE_BACK ; /* middle pixels of the first row */ end = (src - 1) + w - 1 ; while (src < end) { gx = 0.5 * (src[+xo] - src[-xo]) ; gy = src[+yo] - src[0] ; SAVE_BACK ; } /* last pixel of the first row */ gx = src[0] - src[-xo] ; gy = src[+yo] - src[0] ; SAVE_BACK ; gradientModulus += gradHeightStride; pgrad_ampl = gradientModulus; gradientAngle += gradHeightStride; pgrad_angl = gradientAngle; image += imageStride; src = image; for (y = 1 ; y < h -1 ; ++y) { /* first pixel of the middle rows */ gx = src[+xo] - src[0] ; gy = 0.5 * (src[+yo] - src[-yo]) ; SAVE_BACK ; /* middle pixels of the middle rows */ end = (src - 1) + w - 1 ; while (src < end) { gx = 0.5 * (src[+xo] - src[-xo]) ; gy = 0.5 * (src[+yo] - src[-yo]) ; SAVE_BACK ; } /* last pixel of the middle row */ gx = src[0] - src[-xo] ; gy = 0.5 * (src[+yo] - src[-yo]) ; SAVE_BACK ; gradientModulus += gradHeightStride; pgrad_ampl = gradientModulus; gradientAngle += gradHeightStride; pgrad_angl = gradientAngle; image += imageStride; src = image; } /* first pixel of the last row */ gx = src[+xo] - src[0] ; gy = src[ 0] - src[-yo] ; SAVE_BACK ; /* middle pixels of the last row */ end = (src - 1) + w - 1 ; while (src < end) { gx = 0.5 * (src[+xo] - src[-xo]) ; gy = src[0] - src[-yo] ; SAVE_BACK ; } /* last pixel of the last row */ gx = src[0] - src[-xo] ; gy = src[0] - src[-yo] ; SAVE_BACK ; } /* VL_TYPE_FLOAT, VL_TYPE_DOUBLE */ #endif /* ---------------------------------------------------------------- */ /* Integral Image */ /* ---------------------------------------------------------------- */ /** @fn vl_imintegral_d(double*,vl_size,double const*,vl_size,vl_size,vl_size) ** @brief Compute integral image ** ** @param integral integral image. ** @param integralStride integral image stride. ** @param image source image. ** @param imageWidth source image width. ** @param imageHeight source image height. ** @param imageStride source image stride. ** ** Let @f$ I(x,y), (x,y) \in [0, W-1] \times [0, H-1] @f$. The ** function computes the integral image @f$ J(x,y) @f$ of @f$ I(x,g) ** @f$: ** ** @f[ ** J(x,y) = \sum_{x'=0}^{x} \sum_{y'=0}^{y} I(x',y') ** @f] ** ** The integral image @f$ J(x,y) @f$ can be used to compute quickly ** the integral of of @f$ I(x,y) @f$ in a rectangular region @f$ R = ** [x',x'']\times[y',y''] @f$: ** ** @f[ ** \sum_{(x,y)\in[x',x'']\times[y',y'']} I(x,y) = ** (J(x'',y'') - J(x'-1, y'')) - (J(x'',y'-1) - J(x'-1,y'-1)). ** @f] ** ** Note that the order of operations is important when the integral image ** has an unsigned data type (e.g. ::vl_uint32). The formula ** is easily derived as follows: ** ** @f{eqnarray*} ** \sum_{(x,y)\in R} I(x,y) ** &=& \sum_{x=x'}^{x''} \sum_{y=y'}^{y''} I(x,y)\\ ** &=& \sum_{x=0}^{x''} \sum_{y=y'}^{y''} I(x,y) ** - \sum_{x=0}^{x'-1} \sum_{y=y'}^{y''} I(x,y)\\ ** &=& \sum_{x=0}^{x''} \sum_{y=0}^{y''} I(x,y) ** - \sum_{x=0}^{x''} \sum_{y=0}^{y'-1} I(x,y) ** - \sum_{x=0}^{x'-1} \sum_{y=0}^{y''} I(x,y) ** + \sum_{x=0}^{x'-1} \sum_{y=0}^{y'-1} I(x,y)\\ ** &=& J(x'',y'') - J(x'-1,y'') - J(x'',y'-1) + J(x'-1,y'-1). ** @f} **/ /** @fn vl_imintegral_f(float*,vl_size,float const*,vl_size,vl_size,vl_size) ** @brief Compute integral image ** @see ::vl_imintegral_d. **/ /** @fn vl_imintegral_ui32(vl_uint32*,vl_size,vl_uint32 const*,vl_size,vl_size,vl_size) ** @brief Compute integral image ** @see ::vl_imintegral_d. **/ /** @fn vl_imintegral_i32(vl_int32*,vl_size,vl_int32 const*,vl_size,vl_size,vl_size) ** @brief Compute integral image ** @see ::vl_imintegral_d. **/ VL_EXPORT void VL_XCAT(vl_imintegral_, SFX) (T * integral, vl_size integralStride, T const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride) { vl_uindex x, y ; T temp = 0 ; if (imageHeight > 0) { for (x = 0 ; x < imageWidth ; ++ x) { temp += *image++ ; *integral++ = temp ; } } for (y = 1 ; y < imageHeight ; ++ y) { T * integralPrev ; integral += integralStride - imageWidth ; image += imageStride - imageWidth ; integralPrev = integral - integralStride ; temp = 0 ; for (x = 0 ; x < imageWidth ; ++ x) { temp += *image++ ; *integral++ = *integralPrev++ + temp ; } } } /* endif VL_IMOPV_INSTANTIATING */ #undef FLT #undef VL_IMOPV_INSTANTIATING #endif colmap-3.9.1/src/thirdparty/VLFeat/imopv.h000066400000000000000000000136731454702036400204230ustar00rootroot00000000000000/** @file imopv.h ** @brief Vectorized image operations ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_IMOPV_H #define VL_IMOPV_H #include "generic.h" /** @name Image convolution flags ** @{ */ #define VL_PAD_BY_ZERO (0x0 << 0) /**< @brief Pad with zeroes. */ #define VL_PAD_BY_CONTINUITY (0x1 << 0) /**< @brief Pad by continuity. */ #define VL_PAD_MASK (0x3) /**< @brief Padding field selector. */ #define VL_TRANSPOSE (0x1 << 2) /**< @brief Transpose result. */ /** @} */ /** @name Image convolution ** @{ */ VL_EXPORT void vl_imconvcol_vf (float* dst, vl_size dst_stride, float const* src, vl_size src_width, vl_size src_height, vl_size src_stride, float const* filt, vl_index filt_begin, vl_index filt_end, int step, unsigned int flags) ; VL_EXPORT void vl_imconvcol_vd (double* dst, vl_size dst_stride, double const* src, vl_size src_width, vl_size src_height, vl_size src_stride, double const* filt, vl_index filt_begin, vl_index filt_end, int step, unsigned int flags) ; VL_EXPORT void vl_imconvcoltri_f (float * dest, vl_size destStride, float const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride, vl_size filterSize, vl_size step, int unsigned flags) ; VL_EXPORT void vl_imconvcoltri_d (double * dest, vl_size destStride, double const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride, vl_size filterSize, vl_size step, int unsigned flags) ; /** @} */ /** @name Integral image ** @{ */ VL_EXPORT void vl_imintegral_f (float * integral, vl_size integralStride, float const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride) ; VL_EXPORT void vl_imintegral_d (double * integral, vl_size integralStride, double const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride) ; VL_EXPORT void vl_imintegral_i32 (vl_int32 * integral, vl_size integralStride, vl_int32 const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride) ; VL_EXPORT void vl_imintegral_ui32 (vl_uint32 * integral, vl_size integralStride, vl_uint32 const * image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride) ; /** @} */ /** @name Distance transform */ /** @{ */ VL_EXPORT void vl_image_distance_transform_d (double const * image, vl_size numColumns, vl_size numRows, vl_size columnStride, vl_size rowStride, double * distanceTransform, vl_uindex * indexes, double coeff, double offset) ; VL_EXPORT void vl_image_distance_transform_f (float const * image, vl_size numColumns, vl_size numRows, vl_size columnStride, vl_size rowStride, float * distanceTransform, vl_uindex * indexes, float coeff, float offset) ; /** @} */ /* ---------------------------------------------------------------- */ /** @name Image smoothing */ /** @{ */ VL_EXPORT void vl_imsmooth_f (float *smoothed, vl_size smoothedStride, float const *image, vl_size width, vl_size height, vl_size stride, double sigmax, double sigmay) ; VL_EXPORT void vl_imsmooth_d (double *smoothed, vl_size smoothedStride, double const *image, vl_size width, vl_size height, vl_size stride, double sigmax, double sigmay) ; /** @} */ /* ---------------------------------------------------------------- */ /** @name Image gradients */ /** @{ */ VL_EXPORT void vl_imgradient_polar_f (float* amplitudeGradient, float* angleGradient, vl_size gradWidthStride, vl_size gradHeightStride, float const* image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride); VL_EXPORT void vl_imgradient_polar_d (double* amplitudeGradient, double* angleGradient, vl_size gradWidthStride, vl_size gradHeightStride, double const* image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride); VL_EXPORT void vl_imgradient_f (float* xGradient, float* yGradient, vl_size gradWidthStride, vl_size gradHeightStride, float const *image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride); VL_EXPORT void vl_imgradient_d(double* xGradient, double* yGradient, vl_size gradWidthStride, vl_size gradHeightStride, double const *image, vl_size imageWidth, vl_size imageHeight, vl_size imageStride); VL_EXPORT void vl_imgradient_polar_f_callback(float const *sourceImage, int sourceImageWidth, int sourceImageHeight, float *dstImage, int dstWidth, int dstHeight, int octave, int level, void *params); /** @} */ /* VL_IMOPV_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/imopv_sse2.c000077500000000000000000000170331454702036400213470ustar00rootroot00000000000000/** @file imopv_sse2.c ** @brief Vectorized image operations - SSE2 - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #if ! defined(VL_DISABLE_SSE2) & ! defined(__SSE2__) #error "Compiling with SSE2 enabled, but no __SSE2__ defined" #endif #if ! defined(VL_DISABLE_SSE2) #ifndef VL_IMOPV_SSE2_INSTANTIATING #include #include "imopv.h" #include "imopv_sse2.h" #define FLT VL_TYPE_FLOAT #define VL_IMOPV_SSE2_INSTANTIATING #include "imopv_sse2.c" #define FLT VL_TYPE_DOUBLE #define VL_IMOPV_SSE2_INSTANTIATING #include "imopv_sse2.c" /* ---------------------------------------------------------------- */ /* VL_IMOPV_SSE2_INSTANTIATING */ #else #include "float.h" /* ---------------------------------------------------------------- */ void VL_XCAT3(_vl_imconvcol_v, SFX, _sse2) (T* dst, vl_size dst_stride, T const* src, vl_size src_width, vl_size src_height, vl_size src_stride, T const* filt, vl_index filt_begin, vl_index filt_end, int step, unsigned int flags) { vl_index x = 0 ; vl_index y ; vl_index dheight = (src_height - 1) / step + 1 ; vl_bool use_simd = VALIGNED(src_stride) ; vl_bool transp = flags & VL_TRANSPOSE ; vl_bool zeropad = (flags & VL_PAD_MASK) == VL_PAD_BY_ZERO ; double totcol = 0 ; double simdcol = 0 ; /* let filt point to the last sample of the filter */ filt += filt_end - filt_begin ; while (x < (signed)src_width) { /* Calculate dest[x,y] = sum_p image[x,p] filt[y - p] * where supp(filt) = [filt_begin, filt_end] = [fb,fe]. * * CHUNK_A: y - fe <= p < 0 * completes VL_MAX(fe - y, 0) samples * CHUNK_B: VL_MAX(y - fe, 0) <= p < VL_MIN(y - fb, height - 1) * completes fe - VL_MAX(fb, height - y) + 1 samples * CHUNK_C: completes all samples */ T const *filti ; vl_index stop ; if ((x + VSIZE < (signed)src_width) & VALIGNED(src + x) & use_simd) { /* ---------------------------------------------- Vectorized */ for (y = 0 ; y < (signed)src_height ; y += step) { union {VTYPE v ; T x [VSIZE] ; } acc ; VTYPE v, c ; T const *srci ; acc.v = VSTZ () ; v = VSTZ() ; filti = filt ; stop = filt_end - y ; srci = src + x - stop * src_stride ; if (stop > 0) { if (zeropad) { v = VSTZ () ; } else { v = * (VTYPE*) (src + x) ; } while (filti > filt - stop) { c = VLD1 (filti--) ; acc.v = VADD (acc.v, VMUL (v, c)) ; srci += src_stride ; } } stop = filt_end - VL_MAX(filt_begin, y - (signed)src_height + 1) + 1 ; while (filti > filt - stop) { v = * (VTYPE*) srci ; c = VLD1 (filti--) ; acc.v = VADD (acc.v, VMUL (v, c)) ; srci += src_stride ; } if (zeropad) v = VSTZ () ; stop = filt_end - filt_begin + 1; while (filti > filt - stop) { c = VLD1 (filti--) ; acc.v = VADD (acc.v, VMUL (v, c)) ; } if (transp) { *dst = acc.x[0] ; dst += dst_stride ; *dst = acc.x[1] ; dst += dst_stride ; #if(VSIZE == 4) *dst = acc.x[2] ; dst += dst_stride ; *dst = acc.x[3] ; dst += dst_stride ; #endif dst += 1 * 1 - VSIZE * dst_stride ; } else { *dst = acc.x[0] ; dst += 1 ; *dst = acc.x[1] ; dst += 1 ; #if(VSIZE == 4) *dst = acc.x[2] ; dst += 1 ; *dst = acc.x[3] ; dst += 1 ; #endif dst += 1 * dst_stride - VSIZE * 1 ; } } /* next y */ if (transp) { dst += VSIZE * dst_stride - dheight * 1 ; } else { dst += VSIZE * 1 - dheight * dst_stride ; } x += VSIZE ; simdcol += VSIZE ; totcol += VSIZE ; } else { /* ------------------------------------------------- Vanilla */ for (y = 0 ; y < (signed)src_height ; y += step) { T acc = 0 ; T v = 0, c ; T const* srci ; filti = filt ; stop = filt_end - y ; srci = src + x - stop * src_stride ; if (stop > 0) { if (zeropad) { v = 0 ; } else { v = *(src + x) ; } while (filti > filt - stop) { c = *filti-- ; acc += v * c ; srci += src_stride ; } } stop = filt_end - VL_MAX(filt_begin, y - (signed)src_height + 1) + 1 ; while (filti > filt - (signed)stop) { v = *srci ; c = *filti-- ; acc += v * c ; srci += src_stride ; } if (zeropad) v = 0 ; stop = filt_end - filt_begin + 1 ; while (filti > filt - stop) { c = *filti-- ; acc += v * c ; } if (transp) { *dst = acc ; dst += 1 ; } else { *dst = acc ; dst += dst_stride ; } } /* next y */ if (transp) { dst += 1 * dst_stride - dheight * 1 ; } else { dst += 1 * 1 - dheight * dst_stride ; } x += 1 ; totcol += 1 ; } /* next x */ } } /* ---------------------------------------------------------------- */ #if 0 void VL_XCAT(_vl_imconvcoltri_v, SFX, sse2) (T* dst, int dst_stride, T const* src, int src_width, int src_height, int src_stride, int filt_size, int step, unsigned int flags) { int x = 0 ; int y ; int dheight = (src_height - 1) / step + 1 ; vl_bool use_simd = ((src_stride & ALIGNSTRIDE) == 0) && (! (flags & VL_NO_SIMD)) ; vl_bool transp = flags & VL_TRANSPOSE ; vl_bool zeropad = (flags & VL_PAD_MASK) == VL_PAD_BY_ZERO ; T * buff = vl_malloc(sizeof(T) * (src_height + filt_size)) ; #define fa (1.0 / (double) (filt_size + 1)) T scale = fa*fa*fa*fa ; buff += filt_size ; while (x < src_width) { T const *srci ; use_simd = 0 ; if ((x + VSIZE < src_width) & (((vl_ptrint)(src + x) & ALIGNPTR) == 0) & use_simd) { } else { int stridex = transp ? dst_stride : 1 ; int stridey = transp ? 1 : dst_stride ; srci = src + x + src_stride * (src_height - 1) ; /* integrate backward the column */ buff [src_height - 1] = *srci ; for (y = src_height-2 ; y >= 0 ; --y) { srci -= src_stride ; buff [y] = buff [y+1] + *srci ; } if (zeropad) { for ( ; y >= - filt_size ; --y) { buff [y] = buff [y+1] ; } } else { for ( ; y >= - filt_size ; --y) { buff [y] = buff[y+1] + *srci ; } } /* compute the filter forward */ for (y = - filt_size ; y < src_height - filt_size ; ++y) { buff [y] = buff [y] - buff [y + filt_size] ; } if (! zeropad) { for (y = src_height - filt_size ; y < src_height ; ++y) { buff [y] = buff [y] - buff [src_height-1] * (src_height - filt_size - y) ; } } /* integrate forward the column */ for (y = - filt_size + 1 ; y < src_height ; ++y) { buff [y] += buff [y - 1] ; } /* compute the filter backward */ for (y = src_height - 1 ; y >= 0 ; --y) { dst [x*stridex + y*stridey] = scale * (buff [y] - buff [y - filt_size]) ; } } /* next y */ x += 1 ; } vl_free (buff - filt_size) ; } #endif #undef FLT #undef VL_IMOPV_SSE2_INSTANTIATING #endif /* ! VL_DISABLE_SSE2 */ #endif colmap-3.9.1/src/thirdparty/VLFeat/imopv_sse2.h000066400000000000000000000033501454702036400213460ustar00rootroot00000000000000/** @file imopv_sse2.h ** @brief Vectorized image operations - SSE2 ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_IMOPV_SSE2_H #define VL_IMOPV_SSE2_H #include "generic.h" #ifndef VL_DISABLE_SSE2 VL_EXPORT void _vl_imconvcol_vf_sse2 (float* dst, vl_size dst_stride, float const* src, vl_size src_width, vl_size src_height, vl_size src_stride, float const* filt, vl_index filt_begin, vl_index filt_end, int step, unsigned int flags) ; VL_EXPORT void _vl_imconvcol_vd_sse2 (double* dst, vl_size dst_stride, double const* src, vl_size src_width, vl_size src_height, vl_size src_stride, double const* filt, vl_index filt_begin, vl_index filt_end, int step, unsigned int flags) ; /* VL_EXPORT void _vl_imconvcoltri_vf_sse2 (float* dst, int dst_stride, float const* src, int src_width, int src_height, int src_stride, int filt_size, int step, unsigned int flags) ; VL_EXPORT void _vl_imconvcoltri_vd_sse2 (double* dst, int dst_stride, double const* src, int src_width, int src_height, int src_stride, int filt_size, int step, unsigned int flags) ; */ #endif /* VL_IMOPV_SSE2_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/kdtree.c000077500000000000000000001047031454702036400205400ustar00rootroot00000000000000/** @file kdtree.c ** @brief KD-tree - Definition ** @author Andrea Vedaldi, David Novotny **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page kdtree KD-trees and forests @author Andrea Vedaldi @author David Novotny @ref kdtree.h implements a KD-tree object, a data structure that can efficiently index moderately dimensional vector spaces. Both best-bin-first @cite{beis97shape} and randomized KD-tree forests are implemented @cite{silpa-anan08optimised},@cite{muja09fast}. Applications include fast matching of feature descriptors. - @ref kdtree-overview - @ref kdtree-tech @section kdtree-overview Overview To create a ::VlKDForest object use ::vl_kdforest_new specifying the dimensionality of the data and the number of trees in the forest. With one tree only, the algorithm is analogous to @cite{beis97shape} (best-bin KDTree). Multiple trees correspond to the randomized KDTree forest as in @cite{silpa-anan08optimised},@cite{muja09fast}. To let the KD-tree index some data use ::vl_kdforest_build. Note that for efficiency KD-tree does not copy the data but retains a pointer to it. Therefore the data must exist (and not change) until the KD-tree is deleted. To delete the KD-tree object, use ::vl_kdforest_delete. To find the N nearest neighbors to a query point first instantiate a ::VlKDForestSearcher and then start search using a ::vl_kdforest_query with the searcher object as an argument. To set a maximum number of comparisons per query and calculate approximate nearest neighbors use ::vl_kdforest_set_max_num_comparisons. @section kdtree-tech Technical details ::VlKDForest implements the best-bin-first kd-tree of @cite{beis97shape}. Construction. Given a set of points @f$ x_1,\dots,x_n \in \mathbb{R}^d @f$, the algorithm recursively partitions the @e d dimensional Euclidean space @f$ \mathbb{R}^d @f$ into (hyper-) rectangles. Partitions are organized into a binary tree with the root corresponding to the whole space @f$ \mathbb{R}^d @f$. The algorithm refines each partition by dividing it into two halves by thresholding along a given dimension. Both the splitting dimension and the threshold are determined as a statistic of the data points contained in the partition. The splitting dimension is the one which has largest sample variance and the splitting threshold is either the sample mean or the median. Leaves are atomic partitions and they contain a list of zero or more data points (typically one). Querying. Querying amounts to finding the N data points closer to a given query point @f$ x_q \in \mathbb{R}^d @f$. This is done by branch-and-bound. A search state is an active partition (initially the root) and it is weighed by the lower bound on the distance of any point in the partition and the query point. Such a lower bound is trivial to compute because partitions are hyper-rectangles. Querying usage. As said before a user has to create an instance ::VlKDForestSearcher using ::vl_kdforest_new_searcher in order to be able to make queries. When a user wants to delete a KD-Tree all the searchers bound to the given KD-Forest are erased automatically. If a user wants to delete some of the searchers before the KD-Tree erase, he could do it using the vl_kdforest_delete_searcher method. **/ #include "kdtree.h" #include "generic.h" #include "random.h" #include "mathop.h" #include #if defined(_OPENMP) #include #endif #define VL_HEAP_prefix vl_kdforest_search_heap #define VL_HEAP_type VlKDForestSearchState #define VL_HEAP_cmp(v,x,y) (v[x].distanceLowerBound - v[y].distanceLowerBound) #include "heap-def.h" #define VL_HEAP_prefix vl_kdtree_split_heap #define VL_HEAP_type VlKDTreeSplitDimension #define VL_HEAP_cmp(v,x,y) (v[x].variance - v[y].variance) #include "heap-def.h" #define VL_HEAP_prefix vl_kdforest_neighbor_heap #define VL_HEAP_type VlKDForestNeighbor #define VL_HEAP_cmp(v,x,y) (v[y].distance - v[x].distance) #include "heap-def.h" /** ------------------------------------------------------------------ ** @internal ** @brief Allocate a new node from the tree pool **/ static vl_uindex vl_kdtree_node_new (VlKDTree * tree, vl_uindex parentIndex) { VlKDTreeNode * node = NULL ; vl_uindex nodeIndex = tree->numUsedNodes ; tree -> numUsedNodes += 1 ; assert (tree->numUsedNodes <= tree->numAllocatedNodes) ; node = tree->nodes + nodeIndex ; node -> parent = parentIndex ; node -> lowerChild = 0 ; node -> upperChild = 0 ; node -> splitDimension = 0 ; node -> splitThreshold = 0 ; return nodeIndex ; } /** ------------------------------------------------------------------ ** @internal ** @brief Compare KDTree index entries for sorting **/ VL_INLINE int vl_kdtree_compare_index_entries (void const * a, void const * b) { double delta = ((VlKDTreeDataIndexEntry const*)a) -> value - ((VlKDTreeDataIndexEntry const*)b) -> value ; if (delta < 0) return -1 ; if (delta > 0) return +1 ; return 0 ; } /** ------------------------------------------------------------------ ** @internal ** @brief Build KDTree recursively ** @param forest forest to which the tree belongs. ** @param tree tree being built. ** @param nodeIndex node to process. ** @param dataBegin begin of data for this node. ** @param dataEnd end of data for this node. ** @param depth depth of this node. **/ static void vl_kdtree_build_recursively (VlKDForest * forest, VlKDTree * tree, vl_uindex nodeIndex, vl_uindex dataBegin, vl_uindex dataEnd, unsigned int depth) { vl_uindex d, i, medianIndex, splitIndex ; VlKDTreeNode * node = tree->nodes + nodeIndex ; VlKDTreeSplitDimension * splitDimension ; /* base case: there is only one data point */ if (dataEnd - dataBegin <= 1) { if (tree->depth < depth) tree->depth = depth ; node->lowerChild = - dataBegin - 1; node->upperChild = - dataEnd - 1 ; return ; } /* compute the dimension with largest variance > 0 */ forest->splitHeapNumNodes = 0 ; for (d = 0 ; d < forest->dimension ; ++ d) { double mean = 0 ; /* unnormalized */ double secondMoment = 0 ; double variance = 0 ; vl_size numSamples = VL_KDTREE_VARIANCE_EST_NUM_SAMPLES; vl_bool useAllData = VL_FALSE; if(dataEnd - dataBegin <= VL_KDTREE_VARIANCE_EST_NUM_SAMPLES) { useAllData = VL_TRUE; numSamples = dataEnd - dataBegin; } for (i = 0; i < numSamples ; ++ i) { vl_uint32 sampleIndex; vl_index di; double datum ; if(useAllData == VL_TRUE) { sampleIndex = (vl_uint32)i; } else { sampleIndex = (vl_rand_uint32(forest->rand) % VL_KDTREE_VARIANCE_EST_NUM_SAMPLES); } sampleIndex += dataBegin; di = tree->dataIndex[sampleIndex].index ; switch(forest->dataType) { case VL_TYPE_FLOAT: datum = ((float const*)forest->data) [di * forest->dimension + d] ; break ; case VL_TYPE_DOUBLE: datum = ((double const*)forest->data) [di * forest->dimension + d] ; break ; default: abort() ; } mean += datum ; secondMoment += datum * datum ; } mean /= numSamples ; secondMoment /= numSamples ; variance = secondMoment - mean * mean ; if (variance <= 0) continue ; /* keep splitHeapSize most varying dimensions */ if (forest->splitHeapNumNodes < forest->splitHeapSize) { VlKDTreeSplitDimension * splitDimension = forest->splitHeapArray + forest->splitHeapNumNodes ; splitDimension->dimension = (unsigned int)d ; splitDimension->mean = mean ; splitDimension->variance = variance ; vl_kdtree_split_heap_push (forest->splitHeapArray, &forest->splitHeapNumNodes) ; } else { VlKDTreeSplitDimension * splitDimension = forest->splitHeapArray + 0 ; if (splitDimension->variance < variance) { splitDimension->dimension = (unsigned int)d ; splitDimension->mean = mean ; splitDimension->variance = variance ; vl_kdtree_split_heap_update (forest->splitHeapArray, forest->splitHeapNumNodes, 0) ; } } } /* additional base case: the maximum variance is equal to 0 (overlapping points) */ if (forest->splitHeapNumNodes == 0) { node->lowerChild = - dataBegin - 1 ; node->upperChild = - dataEnd - 1 ; return ; } /* toss a dice to decide the splitting dimension (variance > 0) */ splitDimension = forest->splitHeapArray + (vl_rand_uint32(forest->rand) % VL_MIN(forest->splitHeapSize, forest->splitHeapNumNodes)) ; node->splitDimension = splitDimension->dimension ; /* sort data along largest variance dimension */ for (i = dataBegin ; i < dataEnd ; ++ i) { vl_index di = tree->dataIndex[i].index ; double datum ; switch (forest->dataType) { case VL_TYPE_FLOAT: datum = ((float const*)forest->data) [di * forest->dimension + splitDimension->dimension] ; break ; case VL_TYPE_DOUBLE: datum = ((double const*)forest->data) [di * forest->dimension + splitDimension->dimension] ; break ; default: abort() ; } tree->dataIndex [i] .value = datum ; } qsort (tree->dataIndex + dataBegin, dataEnd - dataBegin, sizeof (VlKDTreeDataIndexEntry), vl_kdtree_compare_index_entries) ; /* determine split threshold */ switch (forest->thresholdingMethod) { case VL_KDTREE_MEAN : node->splitThreshold = splitDimension->mean ; for (splitIndex = dataBegin ; splitIndex < dataEnd && tree->dataIndex[splitIndex].value <= node->splitThreshold ; ++ splitIndex) ; splitIndex -= 1 ; /* If the mean does not provide a proper partition, fall back to * median. This usually happens if all points have the same * value and the zero variance test fails for numerical accuracy * reasons. In this case, also due to numerical accuracy, the * mean value can be smaller, equal, or larger than all * points. */ if (dataBegin <= splitIndex && splitIndex + 1 < dataEnd) break ; case VL_KDTREE_MEDIAN : medianIndex = (dataBegin + dataEnd - 1) / 2 ; splitIndex = medianIndex ; node -> splitThreshold = tree->dataIndex[medianIndex].value ; break ; default: abort() ; } /* divide subparts */ node->lowerChild = vl_kdtree_node_new (tree, nodeIndex) ; vl_kdtree_build_recursively (forest, tree, node->lowerChild, dataBegin, splitIndex + 1, depth + 1) ; node->upperChild = vl_kdtree_node_new (tree, nodeIndex) ; vl_kdtree_build_recursively (forest, tree, node->upperChild, splitIndex + 1, dataEnd, depth + 1) ; } /** ------------------------------------------------------------------ ** @brief Create new KDForest object ** @param dataType type of data (::VL_TYPE_FLOAT or ::VL_TYPE_DOUBLE) ** @param dimension data dimensionality. ** @param numTrees number of trees in the forest. ** @param distance type of distance norm (::VlDistanceL1 or ::VlDistanceL2). ** @return new KDForest. ** ** The data dimension @a dimension and the number of trees @a ** numTrees must not be smaller than one. **/ VlKDForest * vl_kdforest_new (vl_type dataType, vl_size dimension, vl_size numTrees, VlVectorComparisonType distance) { VlKDForest * self = vl_calloc (sizeof(VlKDForest), 1) ; assert(dataType == VL_TYPE_FLOAT || dataType == VL_TYPE_DOUBLE) ; assert(dimension >= 1) ; assert(numTrees >= 1) ; self -> rand = vl_get_rand () ; self -> dataType = dataType ; self -> numData = 0 ; self -> data = 0 ; self -> dimension = dimension ; self -> numTrees = numTrees ; self -> trees = 0 ; self -> thresholdingMethod = VL_KDTREE_MEDIAN ; self -> splitHeapSize = VL_MIN(numTrees, VL_KDTREE_SPLIT_HEAP_SIZE) ; self -> splitHeapNumNodes = 0 ; self -> distance = distance; self -> maxNumNodes = 0 ; self -> numSearchers = 0 ; self -> headSearcher = 0 ; switch (self->dataType) { case VL_TYPE_FLOAT: self -> distanceFunction = (void(*)(void)) vl_get_vector_comparison_function_f (distance) ; break; case VL_TYPE_DOUBLE : self -> distanceFunction = (void(*)(void)) vl_get_vector_comparison_function_d (distance) ; break ; default : abort() ; } return self ; } /** ------------------------------------------------------------------ ** @brief Create a KDForest searcher object, used for processing queries ** @param kdforest a forest to which the queries should be pointing. ** @return KDForest searcher object. ** ** A searcher is an object attached to the forest which must be created ** before running the queries. Each query has to be invoked with the ** searcher as its argument. ** ** When using a multi-threaded approach a user should at first instantiate ** a correct number of searchers - each used in one thread. ** Then in each thread a query to the given searcher could be run. ** **/ VlKDForestSearcher * vl_kdforest_new_searcher (VlKDForest * kdforest) { VlKDForestSearcher * self = vl_calloc(sizeof(VlKDForestSearcher), 1); if(kdforest->numSearchers == 0) { kdforest->headSearcher = self; self->previous = NULL; self->next = NULL; } else { VlKDForestSearcher * lastSearcher = kdforest->headSearcher; while (1) { if(lastSearcher->next) { lastSearcher = lastSearcher->next; } else { lastSearcher->next = self; self->previous = lastSearcher; self->next = NULL; break; } } } kdforest->numSearchers++; self->forest = kdforest; self->searchHeapArray = vl_malloc (sizeof(VlKDForestSearchState) * kdforest->maxNumNodes) ; self->searchIdBook = vl_calloc (sizeof(vl_uindex), kdforest->numData) ; return self ; } /** ------------------------------------------------------------------ ** @brief Delete object ** @param self object. **/ void vl_kdforestsearcher_delete (VlKDForestSearcher * self) { if (self->previous && self->next) { self->previous->next = self->next; self->next->previous = self->previous; } else if (self->previous && !self->next) { self->previous->next = NULL; } else if (!self->previous && self->next) { self->next->previous = NULL; self->forest->headSearcher = self->next; } else { self->forest->headSearcher = NULL; } self->forest->numSearchers -- ; vl_free(self->searchHeapArray) ; vl_free(self->searchIdBook) ; vl_free(self) ; } VlKDForestSearcher * vl_kdforest_get_searcher (VlKDForest const * self, vl_uindex pos) { VlKDForestSearcher * lastSearcher = self->headSearcher ; vl_uindex i ; for(i = 0; (i < pos) & (lastSearcher != NULL) ; ++i) { lastSearcher = lastSearcher->next ; } return lastSearcher ; } /** ------------------------------------------------------------------ ** @brief Delete KDForest object ** @param self KDForest object to delete ** @sa ::vl_kdforest_new **/ void vl_kdforest_delete (VlKDForest * self) { vl_uindex ti ; VlKDForestSearcher * searcher ; while ((searcher = vl_kdforest_get_searcher(self, 0))) { vl_kdforestsearcher_delete(searcher) ; } if (self->trees) { for (ti = 0 ; ti < self->numTrees ; ++ ti) { if (self->trees[ti]) { if (self->trees[ti]->nodes) vl_free (self->trees[ti]->nodes) ; if (self->trees[ti]->dataIndex) vl_free (self->trees[ti]->dataIndex) ; vl_free (self->trees[ti]) ; } } vl_free (self->trees) ; } vl_free (self) ; } /** ------------------------------------------------------------------ ** @internal @brief Compute tree bounds recursively ** @param tree KDTree object instance. ** @param nodeIndex node index to start from. ** @param searchBounds 2 x numDimension array of bounds. **/ static void vl_kdtree_calc_bounds_recursively (VlKDTree * tree, vl_uindex nodeIndex, double * searchBounds) { VlKDTreeNode * node = tree->nodes + nodeIndex ; vl_uindex i = node->splitDimension ; double t = node->splitThreshold ; node->lowerBound = searchBounds [2 * i + 0] ; node->upperBound = searchBounds [2 * i + 1] ; //VL_PRINT("%f %f\n",node->lowerBound,node->upperBound); if (node->lowerChild > 0) { searchBounds [2 * i + 1] = t ; vl_kdtree_calc_bounds_recursively (tree, node->lowerChild, searchBounds) ; searchBounds [2 * i + 1] = node->upperBound ; } if (node->upperChild > 0) { searchBounds [2 * i + 0] = t ; vl_kdtree_calc_bounds_recursively (tree, node->upperChild, searchBounds) ; searchBounds [2 * i + 0] = node->lowerBound ; } } /** ------------------------------------------------------------------ ** @brief Build KDTree from data ** @param self KDTree object ** @param numData number of data points. ** @param data pointer to the data. ** ** The function builds the KDTree by processing the data @a data. For ** efficiency, KDTree does not make a copy the data, but retains a ** pointer to it. Therefore the data buffer must be valid and ** unchanged for the lifespan of the object. ** ** The number of data points @c numData must not be smaller than one. **/ void vl_kdforest_build (VlKDForest * self, vl_size numData, void const * data) { vl_uindex di, ti ; vl_size maxNumNodes ; double * searchBounds; assert(data) ; assert(numData >= 1) ; /* need to check: if alredy built, clean first */ self->data = data ; self->numData = numData ; self->trees = vl_malloc (sizeof(VlKDTree*) * self->numTrees) ; maxNumNodes = 0 ; for (ti = 0 ; ti < self->numTrees ; ++ ti) { self->trees[ti] = vl_malloc (sizeof(VlKDTree)) ; self->trees[ti]->dataIndex = vl_malloc (sizeof(VlKDTreeDataIndexEntry) * self->numData) ; for (di = 0 ; di < self->numData ; ++ di) { self->trees[ti]->dataIndex[di].index = di ; } self->trees[ti]->numUsedNodes = 0 ; /* num. nodes of a complete binary tree with numData leaves */ self->trees[ti]->numAllocatedNodes = 2 * self->numData - 1 ; self->trees[ti]->nodes = vl_malloc (sizeof(VlKDTreeNode) * self->trees[ti]->numAllocatedNodes) ; self->trees[ti]->depth = 0 ; vl_kdtree_build_recursively (self, self->trees[ti], vl_kdtree_node_new(self->trees[ti], 0), 0, self->numData, 0) ; maxNumNodes += self->trees[ti]->numUsedNodes ; } searchBounds = vl_malloc(sizeof(double) * 2 * self->dimension); for (ti = 0 ; ti < self->numTrees ; ++ ti) { double * iter = searchBounds ; double * end = iter + 2 * self->dimension ; while (iter < end) { *iter++ = - VL_INFINITY_F ; *iter++ = + VL_INFINITY_F ; } vl_kdtree_calc_bounds_recursively (self->trees[ti], 0, searchBounds) ; } vl_free(searchBounds); self -> maxNumNodes = maxNumNodes; } /** ------------------------------------------------------------------ ** @internal @brief **/ vl_uindex vl_kdforest_query_recursively (VlKDForestSearcher * searcher, VlKDTree * tree, vl_uindex nodeIndex, VlKDForestNeighbor * neighbors, vl_size numNeighbors, vl_size * numAddedNeighbors, double dist, void const * query) { VlKDTreeNode const * node = tree->nodes + nodeIndex ; vl_uindex i = node->splitDimension ; vl_index nextChild, saveChild ; double delta, saveDist ; double x ; double x1 = node->lowerBound ; double x2 = node->splitThreshold ; double x3 = node->upperBound ; VlKDForestSearchState * searchState ; searcher->searchNumRecursions ++ ; switch (searcher->forest->dataType) { case VL_TYPE_FLOAT : x = ((float const*) query)[i] ; break ; case VL_TYPE_DOUBLE : x = ((double const*) query)[i] ; break ; default : abort() ; } /* base case: this is a leaf node */ if (node->lowerChild < 0) { vl_index begin = - node->lowerChild - 1 ; vl_index end = - node->upperChild - 1 ; vl_index iter ; for (iter = begin ; iter < end && (searcher->forest->searchMaxNumComparisons == 0 || searcher->searchNumComparisons < searcher->forest->searchMaxNumComparisons) ; ++ iter) { vl_index di = tree->dataIndex [iter].index ; /* multiple KDTrees share the database points and we must avoid * adding the same point twice */ if (searcher->searchIdBook[di] == searcher->searchId) continue ; searcher->searchIdBook[di] = searcher->searchId ; /* compare the query to this point */ switch (searcher->forest->dataType) { case VL_TYPE_FLOAT: dist = ((VlFloatVectorComparisonFunction)searcher->forest->distanceFunction) (searcher->forest->dimension, ((float const *)query), ((float const*)searcher->forest->data) + di * searcher->forest->dimension) ; break ; case VL_TYPE_DOUBLE: dist = ((VlDoubleVectorComparisonFunction)searcher->forest->distanceFunction) (searcher->forest->dimension, ((double const *)query), ((double const*)searcher->forest->data) + di * searcher->forest->dimension) ; break ; default: abort() ; } searcher->searchNumComparisons += 1 ; /* see if it should be added to the result set */ if (*numAddedNeighbors < numNeighbors) { VlKDForestNeighbor * newNeighbor = neighbors + *numAddedNeighbors ; newNeighbor->index = di ; newNeighbor->distance = dist ; vl_kdforest_neighbor_heap_push (neighbors, numAddedNeighbors) ; } else { VlKDForestNeighbor * largestNeighbor = neighbors + 0 ; if (largestNeighbor->distance > dist) { largestNeighbor->index = di ; largestNeighbor->distance = dist ; vl_kdforest_neighbor_heap_update (neighbors, *numAddedNeighbors, 0) ; } } } /* next data point */ return nodeIndex ; } #if 0 assert (x1 <= x2 && x2 <= x3) ; assert (node->lowerChild >= 0) ; assert (node->upperChild >= 0) ; #endif /* * x1 x2 x3 * x (---|---] * (--x|---] * (---|x--] * (---|---] x */ delta = x - x2 ; saveDist = dist + delta*delta ; if (x <= x2) { nextChild = node->lowerChild ; saveChild = node->upperChild ; if (x <= x1) { delta = x - x1 ; saveDist -= delta*delta ; } } else { nextChild = node->upperChild ; saveChild = node->lowerChild ; if (x > x3) { delta = x - x3 ; saveDist -= delta*delta ; } } if (*numAddedNeighbors < numNeighbors || neighbors[0].distance > saveDist) { searchState = searcher->searchHeapArray + searcher->searchHeapNumNodes ; searchState->tree = tree ; searchState->nodeIndex = saveChild ; searchState->distanceLowerBound = saveDist ; vl_kdforest_search_heap_push (searcher->searchHeapArray , &searcher->searchHeapNumNodes) ; } return vl_kdforest_query_recursively (searcher, tree, nextChild, neighbors, numNeighbors, numAddedNeighbors, dist, query) ; } /** ------------------------------------------------------------------ ** @brief Query the forest ** @param self object. ** @param neighbors list of nearest neighbors found (output). ** @param numNeighbors number of nearest neighbors to find. ** @param query query point. ** @return number of tree leaves visited. ** ** A neighbor is represented by an instance of the structure ** ::VlKDForestNeighbor. Each entry contains the index of the ** neighbor (this is an index into the KDTree data) and its distance ** to the query point. Neighbors are sorted by increasing distance. **/ vl_size vl_kdforest_query (VlKDForest * self, VlKDForestNeighbor * neighbors, vl_size numNeighbors, void const * query) { VlKDForestSearcher * searcher = vl_kdforest_get_searcher(self, 0) ; if (searcher == NULL) { searcher = vl_kdforest_new_searcher(self) ; } return vl_kdforestsearcher_query(searcher, neighbors, numNeighbors, query) ; } /** ------------------------------------------------------------------ ** @brief Query the forest ** @param self object. ** @param neighbors list of nearest neighbors found (output). ** @param numNeighbors number of nearest neighbors to find. ** @param query query point. ** @return number of tree leaves visited. ** ** A neighbor is represented by an instance of the structure ** ::VlKDForestNeighbor. Each entry contains the index of the ** neighbor (this is an index into the KDTree data) and its distance ** to the query point. Neighbors are sorted by increasing distance. **/ vl_size vl_kdforestsearcher_query (VlKDForestSearcher * self, VlKDForestNeighbor * neighbors, vl_size numNeighbors, void const * query) { vl_uindex i, ti ; vl_bool exactSearch = self->forest->searchMaxNumComparisons == 0 ; VlKDForestSearchState * searchState ; vl_size numAddedNeighbors = 0 ; assert (neighbors) ; assert (numNeighbors > 0) ; assert (query) ; /* this number is used to differentiate a query from the next */ self -> searchId += 1 ; self -> searchNumRecursions = 0 ; self->searchNumComparisons = 0 ; self->searchNumSimplifications = 0 ; /* put the root node into the search heap */ self->searchHeapNumNodes = 0 ; for (ti = 0 ; ti < self->forest->numTrees ; ++ ti) { searchState = self->searchHeapArray + self->searchHeapNumNodes ; searchState -> tree = self->forest->trees[ti] ; searchState -> nodeIndex = 0 ; searchState -> distanceLowerBound = 0 ; vl_kdforest_search_heap_push (self->searchHeapArray, &self->searchHeapNumNodes) ; } /* branch and bound */ while (exactSearch || self->searchNumComparisons < self->forest->searchMaxNumComparisons) { /* pop the next optimal search node */ VlKDForestSearchState * searchState ; /* break if search space completed */ if (self->searchHeapNumNodes == 0) { break ; } searchState = self->searchHeapArray + vl_kdforest_search_heap_pop (self->searchHeapArray, &self->searchHeapNumNodes) ; /* break if no better solution may exist */ if (numAddedNeighbors == numNeighbors && neighbors[0].distance < searchState->distanceLowerBound) { self->searchNumSimplifications ++ ; break ; } vl_kdforest_query_recursively (self, searchState->tree, searchState->nodeIndex, neighbors, numNeighbors, &numAddedNeighbors, searchState->distanceLowerBound, query) ; } /* sort neighbors by increasing distance */ for (i = numAddedNeighbors ; i < numNeighbors ; ++ i) { neighbors[i].index = -1 ; neighbors[i].distance = VL_NAN_F ; } while (numAddedNeighbors) { vl_kdforest_neighbor_heap_pop (neighbors, &numAddedNeighbors) ; } return self->searchNumComparisons ; } /** ------------------------------------------------------------------ ** @brief Run multiple queries ** @param self object. ** @param indexes assignments of points. ** @param numNeighbors number of nearest neighbors to be found for each data point ** @param numQueries number of query points. ** @param distances distances of query points. ** @param queries lisf of vectors to use as queries. ** ** @a indexes and @a distances are @a numNeighbors by @a numQueries ** matrices containing the indexes and distances of the nearest neighbours ** for each of the @a numQueries queries @a queries. ** ** This function is similar to ::vl_kdforest_query. The main ** difference is that the function can use multiple cores to query ** large amounts of data. ** ** @sa ::vl_kdforest_query. **/ vl_size vl_kdforest_query_with_array (VlKDForest * self, vl_uint32 * indexes, vl_size numNeighbors, vl_size numQueries, void * distances, void const * queries) { vl_size numComparisons = 0; vl_type dataType = vl_kdforest_get_data_type(self) ; vl_size dimension = vl_kdforest_get_data_dimension(self) ; #ifdef _OPENMP #pragma omp parallel default(shared) num_threads(vl_get_max_threads()) #endif { vl_index qi ; vl_size thisNumComparisons = 0 ; VlKDForestSearcher * searcher ; VlKDForestNeighbor * neighbors ; #ifdef _OPENMP #pragma omp critical #endif { searcher = vl_kdforest_new_searcher(self) ; neighbors = vl_calloc (sizeof(VlKDForestNeighbor), numNeighbors) ; } #ifdef _OPENMP #pragma omp for #endif for(qi = 0 ; qi < (signed)numQueries; ++ qi) { switch (dataType) { case VL_TYPE_FLOAT: { vl_size ni; thisNumComparisons += vl_kdforestsearcher_query (searcher, neighbors, numNeighbors, (float const *) (queries) + qi * dimension) ; for (ni = 0 ; ni < numNeighbors ; ++ni) { indexes [qi*numNeighbors + ni] = (vl_uint32) neighbors[ni].index ; if (distances){ *((float*)distances + qi*numNeighbors + ni) = neighbors[ni].distance ; } } break ; } case VL_TYPE_DOUBLE: { vl_size ni; thisNumComparisons += vl_kdforestsearcher_query (searcher, neighbors, numNeighbors, (double const *) (queries) + qi * dimension) ; for (ni = 0 ; ni < numNeighbors ; ++ni) { indexes [qi*numNeighbors + ni] = (vl_uint32) neighbors[ni].index ; if (distances){ *((double*)distances + qi*numNeighbors + ni) = neighbors[ni].distance ; } } break ; } default: abort() ; } } #ifdef _OPENMP #pragma omp critical #endif { numComparisons += thisNumComparisons ; vl_kdforestsearcher_delete (searcher) ; vl_free (neighbors) ; } } return numComparisons ; } /** ------------------------------------------------------------------ ** @brief Get the number of nodes of a given tree ** @param self KDForest object. ** @param treeIndex index of the tree. ** @return number of trees. **/ vl_size vl_kdforest_get_num_nodes_of_tree (VlKDForest const * self, vl_uindex treeIndex) { assert (treeIndex < self->numTrees) ; return self->trees[treeIndex]->numUsedNodes ; } /** ------------------------------------------------------------------ ** @brief Get the detph of a given tree ** @param self KDForest object. ** @param treeIndex index of the tree. ** @return number of trees. **/ vl_size vl_kdforest_get_depth_of_tree (VlKDForest const * self, vl_uindex treeIndex) { assert (treeIndex < self->numTrees) ; return self->trees[treeIndex]->depth ; } /** ------------------------------------------------------------------ ** @brief Get the number of trees in the forest ** ** @param self KDForest object. ** @return number of trees. **/ vl_size vl_kdforest_get_num_trees (VlKDForest const * self) { return self->numTrees ; } /** ------------------------------------------------------------------ ** @brief Set the maximum number of comparisons for a search ** ** @param self KDForest object. ** @param n maximum number of leaves. ** ** This function sets the maximum number of comparisons for a ** nearest neighbor search. Setting it to 0 means unbounded comparisons. ** ** @sa ::vl_kdforest_query, ::vl_kdforest_get_max_num_comparisons. **/ void vl_kdforest_set_max_num_comparisons (VlKDForest * self, vl_size n) { self->searchMaxNumComparisons = n ; } /** ------------------------------------------------------------------ ** @brief Get the maximum number of comparisons for a search ** ** @param self KDForest object. ** @return maximum number of leaves. ** ** @sa ::vl_kdforest_set_max_num_comparisons. **/ vl_size vl_kdforest_get_max_num_comparisons (VlKDForest * self) { return self->searchMaxNumComparisons ; } /** ------------------------------------------------------------------ ** @brief Set the thresholding method ** @param self KDForest object. ** @param method one of ::VlKDTreeThresholdingMethod. ** ** @sa ::vl_kdforest_get_thresholding_method **/ void vl_kdforest_set_thresholding_method (VlKDForest * self, VlKDTreeThresholdingMethod method) { assert(method == VL_KDTREE_MEDIAN || method == VL_KDTREE_MEAN) ; self->thresholdingMethod = method ; } /** ------------------------------------------------------------------ ** @brief Get the thresholding method ** ** @param self KDForest object. ** @return thresholding method. ** ** @sa ::vl_kdforest_set_thresholding_method **/ VlKDTreeThresholdingMethod vl_kdforest_get_thresholding_method (VlKDForest const * self) { return self->thresholdingMethod ; } /** ------------------------------------------------------------------ ** @brief Get the dimension of the data ** @param self KDForest object. ** @return dimension of the data. **/ vl_size vl_kdforest_get_data_dimension (VlKDForest const * self) { return self->dimension ; } /** ------------------------------------------------------------------ ** @brief Get the data type ** @param self KDForest object. ** @return data type (one of ::VL_TYPE_FLOAT, ::VL_TYPE_DOUBLE). **/ vl_type vl_kdforest_get_data_type (VlKDForest const * self) { return self->dataType ; } /** ------------------------------------------------------------------ ** @brief Get the forest linked to the searcher ** @param self object. ** @return correspoinding KD-Forest. **/ VlKDForest * vl_kdforestsearcher_get_forest (VlKDForestSearcher const * self) { return self->forest; } colmap-3.9.1/src/thirdparty/VLFeat/kdtree.h000066400000000000000000000131121454702036400205330ustar00rootroot00000000000000/** @file kdtree.h ** @brief KD-tree (@ref kdtree) ** @author Andrea Vedaldi, David Novotny **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_KDTREE_H #define VL_KDTREE_H #include "generic.h" #include "mathop.h" #define VL_KDTREE_SPLIT_HEAP_SIZE 5 #define VL_KDTREE_VARIANCE_EST_NUM_SAMPLES 1024 typedef struct _VlKDTreeNode VlKDTreeNode ; typedef struct _VlKDTreeSplitDimension VlKDTreeSplitDimension ; typedef struct _VlKDTreeDataIndexEntry VlKDTreeDataIndexEntry ; typedef struct _VlKDForestSearchState VlKDForestSearchState ; struct _VlKDTreeNode { vl_uindex parent ; vl_index lowerChild ; vl_index upperChild ; unsigned int splitDimension ; double splitThreshold ; double lowerBound ; double upperBound ; } ; struct _VlKDTreeSplitDimension { unsigned int dimension ; double mean ; double variance ; } ; struct _VlKDTreeDataIndexEntry { vl_index index ; double value ; } ; /** @brief Thresholding method */ typedef enum _VlKDTreeThresholdingMethod { VL_KDTREE_MEDIAN, VL_KDTREE_MEAN } VlKDTreeThresholdingMethod ; /** @brief Neighbor of a query point */ typedef struct _VlKDForestNeighbor { double distance ; /**< distance to the query point */ vl_uindex index ; /**< index of the neighbor in the KDTree data */ } VlKDForestNeighbor ; typedef struct _VlKDTree { VlKDTreeNode * nodes ; vl_size numUsedNodes ; vl_size numAllocatedNodes ; VlKDTreeDataIndexEntry * dataIndex ; unsigned int depth ; } VlKDTree ; struct _VlKDForestSearchState { VlKDTree * tree ; vl_uindex nodeIndex ; double distanceLowerBound ; } ; struct _VlKDForestSearcher; /** @brief KDForest object */ typedef struct _VlKDForest { vl_size dimension ; /* random number generator */ VlRand * rand ; /* indexed data */ vl_type dataType ; void const * data ; vl_size numData ; VlVectorComparisonType distance; void (*distanceFunction)(void) ; /* tree structure */ VlKDTree ** trees ; vl_size numTrees ; /* build */ VlKDTreeThresholdingMethod thresholdingMethod ; VlKDTreeSplitDimension splitHeapArray [VL_KDTREE_SPLIT_HEAP_SIZE] ; vl_size splitHeapNumNodes ; vl_size splitHeapSize ; vl_size maxNumNodes; /* query */ vl_size searchMaxNumComparisons ; vl_size numSearchers; struct _VlKDForestSearcher * headSearcher ; /* head of the double linked list with searchers */ } VlKDForest ; /** @brief ::VlKDForest searcher object */ typedef struct _VlKDForestSearcher { /* maintain a linked list of searchers for later disposal*/ struct _VlKDForestSearcher * next; struct _VlKDForestSearcher * previous; vl_uindex * searchIdBook ; VlKDForestSearchState * searchHeapArray ; VlKDForest * forest; vl_size searchNumComparisons; vl_size searchNumRecursions ; vl_size searchNumSimplifications ; vl_size searchHeapNumNodes ; vl_uindex searchId ; } VlKDForestSearcher ; /** @name Creating, copying and disposing ** @{ */ VL_EXPORT VlKDForest * vl_kdforest_new (vl_type dataType, vl_size dimension, vl_size numTrees, VlVectorComparisonType normType) ; VL_EXPORT VlKDForestSearcher * vl_kdforest_new_searcher (VlKDForest * kdforest); VL_EXPORT void vl_kdforest_delete (VlKDForest * self) ; VL_EXPORT void vl_kdforestsearcher_delete (VlKDForestSearcher * searcher) ; /** @} */ /** @name Building and querying ** @{ */ VL_EXPORT void vl_kdforest_build (VlKDForest * self, vl_size numData, void const * data) ; VL_EXPORT vl_size vl_kdforest_query (VlKDForest * self, VlKDForestNeighbor * neighbors, vl_size numNeighbors, void const * query) ; VL_EXPORT vl_size vl_kdforest_query_with_array (VlKDForest * self, vl_uint32 * index, vl_size numNeighbors, vl_size numQueries, void * distance, void const * queries) ; VL_EXPORT vl_size vl_kdforestsearcher_query (VlKDForestSearcher * self, VlKDForestNeighbor * neighbors, vl_size numNeighbors, void const * query) ; /** @} */ /** @name Retrieving and setting parameters ** @{ */ VL_EXPORT vl_size vl_kdforest_get_depth_of_tree (VlKDForest const * self, vl_uindex treeIndex) ; VL_EXPORT vl_size vl_kdforest_get_num_nodes_of_tree (VlKDForest const * self, vl_uindex treeIndex) ; VL_EXPORT vl_size vl_kdforest_get_num_trees (VlKDForest const * self) ; VL_EXPORT vl_size vl_kdforest_get_data_dimension (VlKDForest const * self) ; VL_EXPORT vl_type vl_kdforest_get_data_type (VlKDForest const * self) ; VL_EXPORT void vl_kdforest_set_max_num_comparisons (VlKDForest * self, vl_size n) ; VL_EXPORT vl_size vl_kdforest_get_max_num_comparisons (VlKDForest * self) ; VL_EXPORT void vl_kdforest_set_thresholding_method (VlKDForest * self, VlKDTreeThresholdingMethod method) ; VL_EXPORT VlKDTreeThresholdingMethod vl_kdforest_get_thresholding_method (VlKDForest const * self) ; VL_EXPORT VlKDForest * vl_kdforest_searcher_get_forest (VlKDForestSearcher const * self) ; VL_EXPORT VlKDForestSearcher * vl_kdforest_get_searcher (VlKDForest const * self, vl_uindex pos) ; /** @} */ /* VL_KDTREE_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/kmeans.c000077500000000000000000002151261454702036400205420ustar00rootroot00000000000000/** @file kmeans.c ** @brief K-means - Declaration ** @author Andrea Vedaldi, David Novotny **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. Copyright (C) 2013 Andrea Vedaldi and David Novotny. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page kmeans K-means clustering @author Andrea Vedaldi @author David Novotny @tableofcontents @ref kmeans.h implements a number of algorithm for **K-means quantization**: Lloyd @cite{lloyd82least}, an accelerated version by Elkan @cite{elkan03using}, and a large scale algorithm based on Approximate Nearest Neighbors (ANN). All algorithms support @c float or @c double data and can use the $l^1$ or the $l^2$ distance for clustering. Furthermore, all algorithms can take advantage of multiple CPU cores. Please see @subpage kmeans-fundamentals for a technical description of K-means and of the algorithms implemented here. @section kmeans-starting Getting started The goal of K-means is to partition a dataset into $K$ “compact” clusters. The following example demonstrates using @ref kmeans.h in the C programming language to partition @c numData @c float vectors into compute @c numCenters clusters using Lloyd's algorithm: @code #include double energy ; double * centers ; // Use float data and the L2 distance for clustering KMeans * kmeans = vl_kmeans_new (VLDistanceL2, VL_TYPE_FLOAT) ; // Use Lloyd algorithm vl_kmeans_set_algorithm (kmeans, VlKMeansLloyd) ; // Initialize the cluster centers by randomly sampling the data vl_kmeans_init_centers_with_rand_data (kmeans, data, dimension, numData, numCenters) ; // Run at most 100 iterations of cluster refinement using Lloyd algorithm vl_kmeans_set_max_num_iterations (kmeans, 100) ; vl_kmeans_refine_centers (kmeans, data, numData) ; // Obtain the energy of the solution energy = vl_kmeans_get_energy(kmeans) ; // Obtain the cluster centers centers = vl_kmeans_get_centers(kmeans) ; @endcode Once the centers have been obtained, new data points can be assigned to clusters by using the ::vl_kmeans_quantize function: @code vl_uint32 * assignments = vl_malloc(sizeof(vl_uint32) * numData) ; float * distances = vl_malloc(sizeof(float) * numData) ; vl_kmeans_quantize(kmeans, assignments, distances, data, numData) ; @endcode Alternatively, one can directly assign new pointers to the closest centers, without bothering with a ::VlKMeans object. There are several considerations that may impact the performance of KMeans. First, since K-means is usually based local optimization algorithm, the **initialization method** is important. The following initialization methods are supported: Method | Function | Description ---------------|-----------------------------------------|----------------------------------------------- Random samples | ::vl_kmeans_init_centers_with_rand_data | Random data points K-means++ | ::vl_kmeans_init_centers_plus_plus | Random selection biased towards diversity Custom | ::vl_kmeans_set_centers | Choose centers (useful to run quantization only) See @ref kmeans-init for further details. The initialization methods use a randomized selection of the data points; the random number generator init is controlled by ::vl_rand_init. The second important choice is the **optimization algorithm**. The following optimization algorithms are supported: Algorithm | Symbol | See | Description ------------|------------------|-------------------|----------------------------------------------- Lloyd | ::VlKMeansLloyd | @ref kmeans-lloyd | Alternate EM-style optimization Elkan | ::VlKMeansElkan | @ref kmeans-elkan | A speedup using triangular inequalities ANN | ::VlKMeansANN | @ref kmeans-ann | A speedup using approximated nearest neighbors See the relative sections for further details. These algorithm are iterative, and stop when either a **maximum number of iterations** (::vl_kmeans_set_max_num_iterations) is reached, or when the energy changes sufficiently slowly in one iteration (::vl_kmeans_set_min_energy_variation). All the three algorithms support multithreaded computations. The number of threads used is usually controlled globally by ::vl_set_num_threads. **/ /** @page kmeans-fundamentals K-means fundamentals @tableofcontents Given $n$ points $\bx_1,\dots,\bx_n \in \real^d$, the goal of K-means is find $K$ `centers` $\bc_1,\dots,\bc_m \in \real^d$ and `assignments` $q_1,\dots,q_n \in \{1,\dots,K\}$ of the points to the centers such that the sum of distances \[ E(\bc_1,\dots,\bc_k,q_1,\dots,q_n) = \sum_{i=1}^n \|\bx_i - \bc_{q_i} \|_p^p \] is minimized. $K$-means is obtained for the case $p=2$ ($l^2$ norm), because in this case the optimal centers are the means of the input vectors assigned to them. Here the generalization $p=1$ ($l^1$ norm) will also be considered. Up to normalization, the K-means objective $E$ is also the average reconstruction error if the original points are approximated with the cluster centers. Thus K-means is used not only to group the input points into cluster, but also to `quantize` their values. K-means is widely used in computer vision, for example in the construction of vocabularies of visual features (visual words). In these applications the number $n$ of points to cluster and/or the number $K$ of clusters is often large. Unfortunately, minimizing the objective $E$ is in general a difficult combinatorial problem, so locally optimal or approximated solutions are sought instead. The basic K-means algorithm alternate between re-estimating the centers and the assignments (@ref kmeans-lloyd). Combined with a good initialization strategy (@ref kmeans-init) and, potentially, by re-running the optimization from a number of randomized starting states, this algorithm may attain satisfactory solutions in practice. However, despite its simplicity, Lloyd's algorithm is often too slow. A good replacement is Elkan's algorithm (@ref kmeans-elkan), which uses the triangular inequality to cut down significantly the cost of Lloyd's algorithm. Since this algorithm is otherwise equivalent, it should often be preferred. For very large problems (millions of point to clusters and hundreds, thousands, or more clusters to find), even Elkan's algorithm is not sufficiently fast. In these cases, one can resort to a variant of Lloyd's algorithm that uses an approximated nearest neighbors routine (@ref kmeans-ann). @section kmeans-init Initialization methods All the $K$-means algorithms considered here find locally optimal solutions; as such the way they are initialized is important. @ref kmeans.h supports the following initialization algorithms: @par Random data samples The simplest initialization method is to sample $K$ points at random from the input data and use them as initial values for the cluster centers. @par K-means++ @cite{arthur07k-means} proposes a randomized initialization of the centers which improves upon random selection. The first center $\bc_1$ is selected at random from the data points $\bx_1, \dots, \bx_n $ and the distance from this center to all points $\|\bx_i - \bc_1\|_p^p$ is computed. Then the second center $\bc_2$ is selected at random from the data points with probability proportional to the distance. The procedure is repeated to obtain the other centers by using the minimum distance to the centers collected so far. @section kmeans-lloyd Lloyd's algorithm The most common K-means method is Lloyd's algorithm @cite{lloyd82least}. This algorithm is based on the observation that, while jointly optimizing clusters and assignment is difficult, optimizing one given the other is easy. Lloyd's algorithm alternates the steps: 1. **Quantization.** Each point $\bx_i$ is reassigned to the center $\bc_{q_j}$ closer to it. This requires finding for each point the closest among $K$ other points, which is potentially slow. 2. **Center estimation.** Each center $\bc_q$ is updated to minimize its average distances to the points assigned to it. It is easy to show that the best center is the mean or median of the points, respectively if the $l^2$ or $l^1$ norm is considered. A naive implementation of the assignment step requires $O(dnK)$ operations, where $d$ is the dimensionality of the data, $n$ the number of data points, and $K$ the number of centers. Updating the centers is much cheaper: $O(dn)$ operations suffice to compute the $K$ means and a slightly higher cost is required for the medians. Clearly, the bottleneck is the assignment computation, and this is what the other K-means algorithm try to improve. During the iterations, it can happen that a cluster becomes empty. In this case, K-means automatically **“restarts” the cluster** center by selecting a training point at random. @section kmeans-elkan Elkan's algorithm Elkan's algorithm @cite{elkan03using} is a variation of Lloyd alternate optimization algorithm (@ref kmeans-lloyd) that uses the triangular inequality to avoid many distance calculations when assigning points to clusters. While much faster than Lloyd, Elkan's method uses storage proportional to the umber of clusters by data points, which makes it unpractical for a very large number of clusters. The idea of this algorithm is that, if a center update does not move them much, then most of the point-to-center computations can be avoided when the point-to-center assignments are recomputed. To detect which distances need evaluation, the triangular inequality is used to lower and upper bound distances after a center update. Elkan algorithms uses two key observations. First, one has \[ \|\bx_i - \bc_{q_i}\|_p \leq \|\bc - \bc_{q_i}\|_p / 2 \quad\Rightarrow\quad \|\bx_i - \bc_{q_i}\|_p \leq \|\bx_i - \bc\|_p. \] Thus if the distance between $\bx_i$ and its current center $\bc_{q_i}$ is less than half the distance of the center $\bc_{q_i}$ to another center $\bc$, then $\bc$ can be skipped when the new assignment for $\bx_i$ is searched. Checking this requires keeping track of all the inter-center distances, but centers are typically a small fraction of the training data, so overall this can be a significant saving. In particular, if this condition is satisfied for all the centers $\bc \not= \bc_{q_i}$, the point $\bx_i$ can be skipped completely. Furthermore, the condition can be tested also based on an upper bound $UB_i$ of $\|\bx_i - \bc_{q_i}\|_p$. Second, if a center $\bc$ is updated to $\hat{\bc}$, then the new distance from $\bx$ to $\hat{\bc}$ is bounded from below and above by \[ \|\bx - \bc\|_p - \|bc - \hat\bc\|_p \leq \|\bx - \hat{\bc}\|_p \leq \|\bx - \hat{\bc}\|_p + \|\bc + \hat{\bc}\|_p. \] This allows to maintain an upper bound on the distance of $\bx_i$ to its current center $\bc_{q_i}$ and a lower bound to any other center $\bc$: @f{align*} UB_i & \leftarrow UB_i + \|\bc_{q_i} - \hat{\bc}_{q_i} \|_p \\ LB_i(\bc) & \leftarrow LB_i(\bc) - \|\bc -\hat \bc\|_p. @f} Thus the K-means algorithm becomes: 1. **Initialization.** Compute $LB_i(\bc) = \|\bx_i -\hat \bc\|_p$ for all points and centers. Find the current assignments $q_i$ and bounds $UB_i$ by finding the closest centers to each point: $UB_i = \min_{\bc} LB_i(\bc)$. 2. **Center estimation.** 1. Recompute all the centers based on the new means; call the updated version $\hat{\bc}$. 2. Update all the bounds based on the distance $\|\bc - \hat\bc\|_p$ as explained above. 3. Set $\bc \leftarrow \hat\bc$ for all the centers and go to the next iteration. 3. **Quantization.** 1. Skip any point $\bx_i$ such that $UB_i \leq \frac{1}{2} \|\bc_{q_i} - \bc\|_p$ for all centers $\bc \not= \bc_{q_i}$. 2. For each remaining point $\bx_i$ and center $\bc \not= \bc_{q_i}$: 1. Skip $\bc$ if \[ UB_i \leq \frac{1}{2} \| \bc_{q_i} - \bc \| \quad\text{or}\quad UB_i \leq LB_i(\bc). \] The first condition reflects the first observation above; the second uses the bounds to decide if $\bc$ can be closer than the current center $\bc_{q_i}$ to the point $\bx_i$. If the center cannot be skipped, continue as follows. 3. Skip $\bc$ if the condition above is satisfied after making the upper bound tight: \[ UB_i = LB_i(\bc_{q_i}) = \| \bx_i - \bc_{q_i} \|_p. \] Note that the latter calculation can be done only once for $\bx_i$. If the center cannot be skipped still, continue as follows. 4. Tighten the lower bound too: \[ LB_i(\bc) = \| \bx_i - \bc \|_p. \] At this point both $UB_i$ and $LB_i(\bc)$ are tight. If $LB_i < UB_i$, then the point $\bx_i$ should be reassigned to $\bc$. Update $q_i$ to the index of center $\bc$ and reset $UB_i = LB_i(\bc)$. @section kmeans-ann ANN algorithm The *Approximate Nearest Neighbor* (ANN) K-means algorithm @cite{beis97shape} @cite{silpa-anan08optimised} @cite{muja09fast} is a variant of Lloyd's algorithm (@ref kmeans-lloyd) uses a best-bin-first randomized KD-tree algorithm to approximately (and quickly) find the closest cluster center to each point. The KD-tree implementation is based on @ref kdtree. The algorithm can be summarized as follows: 1. **Quantization.** Each point $\bx_i$ is reassigned to the center $\bc_{q_j}$ closer to it. This starts by indexing the $K$ centers by a KD-tree and then using the latter to quickly find the closest center for every training point. The search is approximated to further improve speed. This opens up the possibility that a data point may receive an assignment that is *worse* than the current one. This is avoided by checking that the new assignment estimated by using ANN is an improvement; otherwise the old assignment is kept. 2. **Center estimation.** Each center $\bc_q$ is updated to minimize its average distances to the points assigned to it. It is easy to show that the best center is the mean or median of the points, respectively if the $l^2$ or $l^1$ norm is considered. The key is to trade-off carefully the speedup obtained by using the ANN algorithm and the loss in accuracy when retrieving neighbors. Due to the curse of dimensionality, KD-trees become less effective for higher dimensional data, so that the search cost, which in the best case is logarithmic with this data structure, may become effectively linear. This is somehow mitigated by the fact that new a new KD-tree is computed at each iteration, reducing the likelihood that points may get stuck with sub-optimal assignments. Experiments with the quantization of 128-dimensional SIFT features show that the ANN algorithm may use one quarter of the comparisons of Elkan's while retaining a similar solution accuracy. */ #include "kmeans.h" #include "generic.h" #include "mathop.h" #include #ifdef _OPENMP #include #endif /* ================================================================ */ #ifndef VL_KMEANS_INSTANTIATING /** ------------------------------------------------------------------ ** @brief Reset state ** ** The function reset the state of the KMeans object. It deletes ** any stored centers, releasing the corresponding memory. This ** cancels the effect of seeding or setting the centers, but ** does not change the other configuration parameters. **/ VL_EXPORT void vl_kmeans_reset (VlKMeans * self) { self->numCenters = 0 ; self->dimension = 0 ; if (self->centers) vl_free(self->centers) ; if (self->centerDistances) vl_free(self->centerDistances) ; self->centers = NULL ; self->centerDistances = NULL ; } /** ------------------------------------------------------------------ ** @brief Create a new KMeans object ** @param dataType type of data (::VL_TYPE_FLOAT or ::VL_TYPE_DOUBLE) ** @param distance distance. ** @return new KMeans object instance. **/ VL_EXPORT VlKMeans * vl_kmeans_new (vl_type dataType, VlVectorComparisonType distance) { VlKMeans * self = vl_calloc(1, sizeof(VlKMeans)) ; self->algorithm = VlKMeansLloyd ; self->distance = distance ; self->dataType = dataType ; self->verbosity = 0 ; self->maxNumIterations = 100 ; self->minEnergyVariation = 1e-4 ; self->numRepetitions = 1 ; self->centers = NULL ; self->centerDistances = NULL ; self->numTrees = 3; self->maxNumComparisons = 100; vl_kmeans_reset (self) ; return self ; } /** ------------------------------------------------------------------ ** @brief Create a new KMeans object by copy ** @param kmeans KMeans object to copy. ** @return new copy. **/ VL_EXPORT VlKMeans * vl_kmeans_new_copy (VlKMeans const * kmeans) { VlKMeans * self = vl_malloc(sizeof(VlKMeans)) ; self->algorithm = kmeans->algorithm ; self->distance = kmeans->distance ; self->dataType = kmeans->dataType ; self->verbosity = kmeans->verbosity ; self->maxNumIterations = kmeans->maxNumIterations ; self->numRepetitions = kmeans->numRepetitions ; self->dimension = kmeans->dimension ; self->numCenters = kmeans->numCenters ; self->centers = NULL ; self->centerDistances = NULL ; self->numTrees = kmeans->numTrees; self->maxNumComparisons = kmeans->maxNumComparisons; if (kmeans->centers) { vl_size dataSize = vl_get_type_size(self->dataType) * self->dimension * self->numCenters ; self->centers = vl_malloc(dataSize) ; memcpy (self->centers, kmeans->centers, dataSize) ; } if (kmeans->centerDistances) { vl_size dataSize = vl_get_type_size(self->dataType) * self->numCenters * self->numCenters ; self->centerDistances = vl_malloc(dataSize) ; memcpy (self->centerDistances, kmeans->centerDistances, dataSize) ; } return self ; } /** ------------------------------------------------------------------ ** @brief Deletes a KMeans object ** @param self KMeans object instance. ** ** The function deletes the KMeans object instance created ** by ::vl_kmeans_new. **/ VL_EXPORT void vl_kmeans_delete (VlKMeans * self) { vl_kmeans_reset (self) ; vl_free (self) ; } /* an helper structure */ typedef struct _VlKMeansSortWrapper { vl_uint32 * permutation ; void const * data ; vl_size stride ; } VlKMeansSortWrapper ; /* ---------------------------------------------------------------- */ /* Instantiate shuffle algorithm */ #define VL_SHUFFLE_type vl_uindex #define VL_SHUFFLE_prefix _vl_kmeans #include "shuffle-def.h" /* #ifdef VL_KMEANS_INSTANTITATING */ #endif /* ================================================================ */ #ifdef VL_KMEANS_INSTANTIATING /* ---------------------------------------------------------------- */ /* Set centers */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_kmeans_set_centers_, SFX) (VlKMeans * self, TYPE const * centers, vl_size dimension, vl_size numCenters) { self->dimension = dimension ; self->numCenters = numCenters ; self->centers = vl_malloc (sizeof(TYPE) * dimension * numCenters) ; memcpy ((TYPE*)self->centers, centers, sizeof(TYPE) * dimension * numCenters) ; } /* ---------------------------------------------------------------- */ /* Random seeding */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_kmeans_init_centers_with_rand_data_, SFX) (VlKMeans * self, TYPE const * data, vl_size dimension, vl_size numData, vl_size numCenters) { vl_uindex i, j, k ; VlRand * rand = vl_get_rand () ; self->dimension = dimension ; self->numCenters = numCenters ; self->centers = vl_malloc (sizeof(TYPE) * dimension * numCenters) ; { vl_uindex * perm = vl_malloc (sizeof(vl_uindex) * numData) ; #if (FLT == VL_TYPE_FLOAT) VlFloatVectorComparisonFunction distFn = vl_get_vector_comparison_function_f(self->distance) ; #else VlDoubleVectorComparisonFunction distFn = vl_get_vector_comparison_function_d(self->distance) ; #endif TYPE * distances = vl_malloc (sizeof(TYPE) * numCenters) ; /* get a random permutation of the data point */ for (i = 0 ; i < numData ; ++i) perm[i] = i ; _vl_kmeans_shuffle (perm, numData, rand) ; for (k = 0, i = 0 ; k < numCenters ; ++ i) { /* compare the next data point to all centers collected so far to detect duplicates (if there are enough left) */ if (numCenters - k < numData - i) { vl_bool duplicateDetected = VL_FALSE ; VL_XCAT(vl_eval_vector_comparison_on_all_pairs_, SFX)(distances, dimension, data + dimension * perm[i], 1, (TYPE*)self->centers, k, distFn) ; for (j = 0 ; j < k ; ++j) { duplicateDetected |= (distances[j] == 0) ; } if (duplicateDetected) continue ; } /* ok, it is not a duplicate so we can accept it! */ memcpy ((TYPE*)self->centers + dimension * k, data + dimension * perm[i], sizeof(TYPE) * dimension) ; k ++ ; } vl_free(distances) ; vl_free(perm) ; } } /* ---------------------------------------------------------------- */ /* kmeans++ seeding */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_kmeans_init_centers_plus_plus_, SFX) (VlKMeans * self, TYPE const * data, vl_size dimension, vl_size numData, vl_size numCenters) { vl_uindex x, c ; VlRand * rand = vl_get_rand () ; TYPE * distances = vl_malloc (sizeof(TYPE) * numData) ; TYPE * minDistances = vl_malloc (sizeof(TYPE) * numData) ; #if (FLT == VL_TYPE_FLOAT) VlFloatVectorComparisonFunction distFn = vl_get_vector_comparison_function_f(self->distance) ; #else VlDoubleVectorComparisonFunction distFn = vl_get_vector_comparison_function_d(self->distance) ; #endif self->dimension = dimension ; self->numCenters = numCenters ; self->centers = vl_malloc (sizeof(TYPE) * dimension * numCenters) ; for (x = 0 ; x < numData ; ++x) { minDistances[x] = (TYPE) VL_INFINITY_D ; } /* select the first point at random */ x = vl_rand_uindex (rand, numData) ; c = 0 ; while (1) { TYPE energy = 0 ; TYPE acc = 0 ; TYPE thresh = (TYPE) vl_rand_real1 (rand) ; memcpy ((TYPE*)self->centers + c * dimension, data + x * dimension, sizeof(TYPE) * dimension) ; c ++ ; if (c == numCenters) break ; VL_XCAT(vl_eval_vector_comparison_on_all_pairs_, SFX) (distances, dimension, (TYPE*)self->centers + (c - 1) * dimension, 1, data, numData, distFn) ; for (x = 0 ; x < numData ; ++x) { minDistances[x] = VL_MIN(minDistances[x], distances[x]) ; energy += minDistances[x] ; } for (x = 0 ; x < numData - 1 ; ++x) { acc += minDistances[x] ; if (acc >= thresh * energy) break ; } } vl_free(distances) ; vl_free(minDistances) ; } /* ---------------------------------------------------------------- */ /* Quantization */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_kmeans_quantize_, SFX) (VlKMeans * self, vl_uint32 * assignments, TYPE * distances, TYPE const * data, vl_size numData) { vl_index i ; #if (FLT == VL_TYPE_FLOAT) VlFloatVectorComparisonFunction distFn = vl_get_vector_comparison_function_f(self->distance) ; #else VlDoubleVectorComparisonFunction distFn = vl_get_vector_comparison_function_d(self->distance) ; #endif #ifdef _OPENMP #pragma omp parallel \ shared(self, distances, assignments, numData, distFn, data) \ num_threads(vl_get_max_threads()) #endif { /* vl_malloc cannot be used here if mapped to MATLAB malloc */ TYPE * distanceToCenters = malloc(sizeof(TYPE) * self->numCenters) ; #ifdef _OPENMP #pragma omp for #endif for (i = 0 ; i < (signed)numData ; ++i) { vl_uindex k ; TYPE bestDistance = (TYPE) VL_INFINITY_D ; VL_XCAT(vl_eval_vector_comparison_on_all_pairs_, SFX)(distanceToCenters, self->dimension, data + self->dimension * i, 1, (TYPE*)self->centers, self->numCenters, distFn) ; for (k = 0 ; k < self->numCenters ; ++k) { if (distanceToCenters[k] < bestDistance) { bestDistance = distanceToCenters[k] ; assignments[i] = (vl_uint32)k ; } } if (distances) distances[i] = bestDistance ; } free(distanceToCenters) ; } } /* ---------------------------------------------------------------- */ /* ANN quantization */ /* ---------------------------------------------------------------- */ static void VL_XCAT(_vl_kmeans_quantize_ann_, SFX) (VlKMeans * self, vl_uint32 * assignments, TYPE * distances, TYPE const * data, vl_size numData, vl_bool update) { #if (FLT == VL_TYPE_FLOAT) VlFloatVectorComparisonFunction distFn = vl_get_vector_comparison_function_f(self->distance) ; #else VlDoubleVectorComparisonFunction distFn = vl_get_vector_comparison_function_d(self->distance) ; #endif VlKDForest * forest = vl_kdforest_new(self->dataType,self->dimension,self->numTrees, self->distance) ; vl_kdforest_set_max_num_comparisons(forest,self->maxNumComparisons); vl_kdforest_set_thresholding_method(forest,VL_KDTREE_MEDIAN); vl_kdforest_build(forest,self->numCenters,self->centers); #ifdef _OPENMP #pragma omp parallel default(none) \ num_threads(vl_get_max_threads()) \ shared(self, forest, update, assignments, distances, data, numData, distFn) #endif { VlKDForestNeighbor neighbor ; VlKDForestSearcher * searcher ; vl_index x; #ifdef _OPENMP #pragma omp critical #endif searcher = vl_kdforest_new_searcher (forest) ; #ifdef _OPENMP #pragma omp for #endif for(x = 0 ; x < (signed)numData ; ++x) { vl_kdforestsearcher_query (searcher, &neighbor, 1, (TYPE const *) (data + x*self->dimension)); if (distances) { if(!update) { distances[x] = (TYPE) neighbor.distance; assignments[x] = (vl_uint32) neighbor.index ; } else { TYPE prevDist = (TYPE) distFn(self->dimension, data + self->dimension * x, (TYPE*)self->centers + self->dimension *assignments[x]); if (prevDist > (TYPE) neighbor.distance) { distances[x] = (TYPE) neighbor.distance ; assignments[x] = (vl_uint32) neighbor.index ; } else { distances[x] = prevDist ; } } } else { assignments[x] = (vl_uint32) neighbor.index ; } } /* end for */ } /* end of parallel region */ vl_kdforest_delete(forest); } /* ---------------------------------------------------------------- */ /* Helper functions */ /* ---------------------------------------------------------------- */ /* The sorting routine is used to find increasing permutation of each * data dimension. This is used to quickly find the median for l1 * distance clustering. */ VL_INLINE TYPE VL_XCAT3(_vl_kmeans_, SFX, _qsort_cmp) (VlKMeansSortWrapper * array, vl_uindex indexA, vl_uindex indexB) { return ((TYPE*)array->data) [array->permutation[indexA] * array->stride] - ((TYPE*)array->data) [array->permutation[indexB] * array->stride] ; } VL_INLINE void VL_XCAT3(_vl_kmeans_, SFX, _qsort_swap) (VlKMeansSortWrapper * array, vl_uindex indexA, vl_uindex indexB) { vl_uint32 tmp = array->permutation[indexA] ; array->permutation[indexA] = array->permutation[indexB] ; array->permutation[indexB] = tmp ; } #define VL_QSORT_prefix VL_XCAT3(_vl_kmeans_, SFX, _qsort) #define VL_QSORT_array VlKMeansSortWrapper* #define VL_QSORT_cmp VL_XCAT3(_vl_kmeans_, SFX, _qsort_cmp) #define VL_QSORT_swap VL_XCAT3(_vl_kmeans_, SFX, _qsort_swap) #include "qsort-def.h" static void VL_XCAT(_vl_kmeans_sort_data_helper_, SFX) (VlKMeans * self, vl_uint32 * permutations, TYPE const * data, vl_size numData) { vl_uindex d, x ; for (d = 0 ; d < self->dimension ; ++d) { VlKMeansSortWrapper array ; array.permutation = permutations + d * numData ; array.data = data + d ; array.stride = self->dimension ; for (x = 0 ; x < numData ; ++x) { array.permutation[x] = (vl_uint32)x ; } VL_XCAT3(_vl_kmeans_, SFX, _qsort_sort)(&array, numData) ; } } /* ---------------------------------------------------------------- */ /* Lloyd refinement */ /* ---------------------------------------------------------------- */ static double VL_XCAT(_vl_kmeans_refine_centers_lloyd_, SFX) (VlKMeans * self, TYPE const * data, vl_size numData) { vl_size c, d, x, iteration ; double previousEnergy = VL_INFINITY_D ; double initialEnergy = VL_INFINITY_D ; double energy ; TYPE * distances = vl_malloc (sizeof(TYPE) * numData) ; vl_uint32 * assignments = vl_malloc (sizeof(vl_uint32) * numData) ; vl_size * clusterMasses = vl_malloc (sizeof(vl_size) * numData) ; vl_uint32 * permutations = NULL ; vl_size * numSeenSoFar = NULL ; VlRand * rand = vl_get_rand () ; vl_size totNumRestartedCenters = 0 ; vl_size numRestartedCenters = 0 ; if (self->distance == VlDistanceL1) { permutations = vl_malloc(sizeof(vl_uint32) * numData * self->dimension) ; numSeenSoFar = vl_malloc(sizeof(vl_size) * self->numCenters) ; VL_XCAT(_vl_kmeans_sort_data_helper_, SFX)(self, permutations, data, numData) ; } for (energy = VL_INFINITY_D, iteration = 0; 1 ; ++ iteration) { /* assign data to cluters */ VL_XCAT(_vl_kmeans_quantize_, SFX)(self, assignments, distances, data, numData) ; /* compute energy */ energy = 0 ; for (x = 0 ; x < numData ; ++x) energy += distances[x] ; if (self->verbosity) { VL_PRINTF("kmeans: Lloyd iter %d: energy = %g\n", iteration, energy) ; } /* check termination conditions */ if (iteration >= self->maxNumIterations) { if (self->verbosity) { VL_PRINTF("kmeans: Lloyd terminating because maximum number of iterations reached\n") ; } break ; } if (energy == previousEnergy) { if (self->verbosity) { VL_PRINTF("kmeans: Lloyd terminating because the algorithm fully converged\n") ; } break ; } if (iteration == 0) { initialEnergy = energy ; } else { double eps = (previousEnergy - energy) / (initialEnergy - energy) ; if (eps < self->minEnergyVariation) { if (self->verbosity) { VL_PRINTF("kmeans: ANN terminating because the energy relative variation was less than %f\n", self->minEnergyVariation) ; } break ; } } /* begin next iteration */ previousEnergy = energy ; /* update clusters */ memset(clusterMasses, 0, sizeof(vl_size) * numData) ; for (x = 0 ; x < numData ; ++x) { clusterMasses[assignments[x]] ++ ; } numRestartedCenters = 0 ; switch (self->distance) { case VlDistanceL2: memset(self->centers, 0, sizeof(TYPE) * self->dimension * self->numCenters) ; for (x = 0 ; x < numData ; ++x) { TYPE * cpt = (TYPE*)self->centers + assignments[x] * self->dimension ; TYPE const * xpt = data + x * self->dimension ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] += xpt[d] ; } } for (c = 0 ; c < self->numCenters ; ++c) { TYPE * cpt = (TYPE*)self->centers + c * self->dimension ; if (clusterMasses[c] > 0) { TYPE mass = clusterMasses[c] ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] /= mass ; } } else { vl_uindex x = vl_rand_uindex(rand, numData) ; numRestartedCenters ++ ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] = data[x * self->dimension + d] ; } } } break ; case VlDistanceL1: for (d = 0 ; d < self->dimension ; ++d) { vl_uint32 * perm = permutations + d * numData ; memset(numSeenSoFar, 0, sizeof(vl_size) * self->numCenters) ; for (x = 0; x < numData ; ++x) { c = assignments[perm[x]] ; if (2 * numSeenSoFar[c] < clusterMasses[c]) { ((TYPE*)self->centers) [d + c * self->dimension] = data [d + perm[x] * self->dimension] ; } numSeenSoFar[c] ++ ; } /* restart the centers as required */ for (c = 0 ; c < self->numCenters ; ++c) { if (clusterMasses[c] == 0) { TYPE * cpt = (TYPE*)self->centers + c * self->dimension ; vl_uindex x = vl_rand_uindex(rand, numData) ; numRestartedCenters ++ ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] = data[x * self->dimension + d] ; } } } } break ; default: abort(); } /* done compute centers */ totNumRestartedCenters += numRestartedCenters ; if (self->verbosity && numRestartedCenters) { VL_PRINTF("kmeans: Lloyd iter %d: restarted %d centers\n", iteration, numRestartedCenters) ; } } /* next Lloyd iteration */ if (permutations) { vl_free(permutations) ; } if (numSeenSoFar) { vl_free(numSeenSoFar) ; } vl_free(distances) ; vl_free(assignments) ; vl_free(clusterMasses) ; return energy ; } static double VL_XCAT(_vl_kmeans_update_center_distances_, SFX) (VlKMeans * self) { #if (FLT == VL_TYPE_FLOAT) VlFloatVectorComparisonFunction distFn = vl_get_vector_comparison_function_f(self->distance) ; #else VlDoubleVectorComparisonFunction distFn = vl_get_vector_comparison_function_d(self->distance) ; #endif if (! self->centerDistances) { self->centerDistances = vl_malloc (sizeof(TYPE) * self->numCenters * self->numCenters) ; } VL_XCAT(vl_eval_vector_comparison_on_all_pairs_, SFX)(self->centerDistances, self->dimension, self->centers, self->numCenters, NULL, 0, distFn) ; return self->numCenters * (self->numCenters - 1) / 2 ; } static double VL_XCAT(_vl_kmeans_refine_centers_ann_, SFX) (VlKMeans * self, TYPE const * data, vl_size numData) { vl_size c, d, x, iteration ; double initialEnergy = VL_INFINITY_D ; double previousEnergy = VL_INFINITY_D ; double energy ; vl_uint32 * permutations = NULL ; vl_size * numSeenSoFar = NULL ; VlRand * rand = vl_get_rand () ; vl_size totNumRestartedCenters = 0 ; vl_size numRestartedCenters = 0 ; vl_uint32 * assignments = vl_malloc (sizeof(vl_uint32) * numData) ; vl_size * clusterMasses = vl_malloc (sizeof(vl_size) * numData) ; TYPE * distances = vl_malloc (sizeof(TYPE) * numData) ; if (self->distance == VlDistanceL1) { permutations = vl_malloc(sizeof(vl_uint32) * numData * self->dimension) ; numSeenSoFar = vl_malloc(sizeof(vl_size) * self->numCenters) ; VL_XCAT(_vl_kmeans_sort_data_helper_, SFX)(self, permutations, data, numData) ; } for (energy = VL_INFINITY_D, iteration = 0; 1 ; ++ iteration) { /* assign data to cluters */ VL_XCAT(_vl_kmeans_quantize_ann_, SFX)(self, assignments, distances, data, numData, iteration > 0) ; /* compute energy */ energy = 0 ; for (x = 0 ; x < numData ; ++x) energy += distances[x] ; if (self->verbosity) { VL_PRINTF("kmeans: ANN iter %d: energy = %g\n", iteration, energy) ; } /* check termination conditions */ if (iteration >= self->maxNumIterations) { if (self->verbosity) { VL_PRINTF("kmeans: ANN terminating because the maximum number of iterations has been reached\n") ; } break ; } if (energy == previousEnergy) { if (self->verbosity) { VL_PRINTF("kmeans: ANN terminating because the algorithm fully converged\n") ; } break ; } if (iteration == 0) { initialEnergy = energy ; } else { double eps = (previousEnergy - energy) / (initialEnergy - energy) ; if (eps < self->minEnergyVariation) { if (self->verbosity) { VL_PRINTF("kmeans: ANN terminating because the energy relative variation was less than %f\n", self->minEnergyVariation) ; } break ; } } /* begin next iteration */ previousEnergy = energy ; /* update clusters */ memset(clusterMasses, 0, sizeof(vl_size) * numData) ; for (x = 0 ; x < numData ; ++x) { clusterMasses[assignments[x]] ++ ; } numRestartedCenters = 0 ; switch (self->distance) { case VlDistanceL2: memset(self->centers, 0, sizeof(TYPE) * self->dimension * self->numCenters) ; for (x = 0 ; x < numData ; ++x) { TYPE * cpt = (TYPE*)self->centers + assignments[x] * self->dimension ; TYPE const * xpt = data + x * self->dimension ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] += xpt[d] ; } } for (c = 0 ; c < self->numCenters ; ++c) { TYPE * cpt = (TYPE*)self->centers + c * self->dimension ; if (clusterMasses[c] > 0) { TYPE mass = clusterMasses[c] ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] /= mass ; } } else { vl_uindex x = vl_rand_uindex(rand, numData) ; numRestartedCenters ++ ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] = data[x * self->dimension + d] ; } } } break ; case VlDistanceL1: for (d = 0 ; d < self->dimension ; ++d) { vl_uint32 * perm = permutations + d * numData ; memset(numSeenSoFar, 0, sizeof(vl_size) * self->numCenters) ; for (x = 0; x < numData ; ++x) { c = assignments[perm[x]] ; if (2 * numSeenSoFar[c] < clusterMasses[c]) { ((TYPE*)self->centers) [d + c * self->dimension] = data [d + perm[x] * self->dimension] ; } numSeenSoFar[c] ++ ; } /* restart the centers as required */ for (c = 0 ; c < self->numCenters ; ++c) { if (clusterMasses[c] == 0) { TYPE * cpt = (TYPE*)self->centers + c * self->dimension ; vl_uindex x = vl_rand_uindex(rand, numData) ; numRestartedCenters ++ ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] = data[x * self->dimension + d] ; } } } } break ; default: VL_PRINT("bad distance set: %d\n",self->distance); abort(); } /* done compute centers */ totNumRestartedCenters += numRestartedCenters ; if (self->verbosity && numRestartedCenters) { VL_PRINTF("kmeans: ANN iter %d: restarted %d centers\n", iteration, numRestartedCenters) ; } } if (permutations) { vl_free(permutations) ; } if (numSeenSoFar) { vl_free(numSeenSoFar) ; } vl_free(distances) ; vl_free(assignments) ; vl_free(clusterMasses) ; return energy ; } /* ---------------------------------------------------------------- */ /* Elkan refinement */ /* ---------------------------------------------------------------- */ static double VL_XCAT(_vl_kmeans_refine_centers_elkan_, SFX) (VlKMeans * self, TYPE const * data, vl_size numData) { vl_size d, iteration ; vl_index x ; vl_uint32 c, j ; vl_bool allDone ; TYPE * distances = vl_malloc (sizeof(TYPE) * numData) ; vl_uint32 * assignments = vl_malloc (sizeof(vl_uint32) * numData) ; vl_size * clusterMasses = vl_malloc (sizeof(vl_size) * numData) ; VlRand * rand = vl_get_rand () ; #if (FLT == VL_TYPE_FLOAT) VlFloatVectorComparisonFunction distFn = vl_get_vector_comparison_function_f(self->distance) ; #else VlDoubleVectorComparisonFunction distFn = vl_get_vector_comparison_function_d(self->distance) ; #endif TYPE * nextCenterDistances = vl_malloc (sizeof(TYPE) * self->numCenters) ; TYPE * pointToClosestCenterUB = vl_malloc (sizeof(TYPE) * numData) ; vl_bool * pointToClosestCenterUBIsStrict = vl_malloc (sizeof(vl_bool) * numData) ; TYPE * pointToCenterLB = vl_malloc (sizeof(TYPE) * numData * self->numCenters) ; TYPE * newCenters = vl_malloc(sizeof(TYPE) * self->dimension * self->numCenters) ; TYPE * centerToNewCenterDistances = vl_malloc (sizeof(TYPE) * self->numCenters) ; vl_uint32 * permutations = NULL ; vl_size * numSeenSoFar = NULL ; double energy ; vl_size totDistanceComputationsToInit = 0 ; vl_size totDistanceComputationsToRefreshUB = 0 ; vl_size totDistanceComputationsToRefreshLB = 0 ; vl_size totDistanceComputationsToRefreshCenterDistances = 0 ; vl_size totDistanceComputationsToNewCenters = 0 ; vl_size totDistanceComputationsToFinalize = 0 ; vl_size totNumRestartedCenters = 0 ; if (self->distance == VlDistanceL1) { permutations = vl_malloc(sizeof(vl_uint32) * numData * self->dimension) ; numSeenSoFar = vl_malloc(sizeof(vl_size) * self->numCenters) ; VL_XCAT(_vl_kmeans_sort_data_helper_, SFX)(self, permutations, data, numData) ; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* Initialization */ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* An iteration is: get_new_centers + reassign + get_energy. This counts as iteration 0, where get_new_centers is assumed to be performed before calling the train function by the initialization function */ /* update distances between centers */ totDistanceComputationsToInit += VL_XCAT(_vl_kmeans_update_center_distances_, SFX)(self) ; /* assigmen points to the initial centers and initialize bounds */ memset(pointToCenterLB, 0, sizeof(TYPE) * self->numCenters * numData) ; for (x = 0 ; x < (signed)numData ; ++x) { TYPE distance ; /* do the first center */ assignments[x] = 0 ; distance = distFn(self->dimension, data + x * self->dimension, (TYPE*)self->centers + 0) ; pointToClosestCenterUB[x] = distance ; pointToClosestCenterUBIsStrict[x] = VL_TRUE ; pointToCenterLB[0 + x * self->numCenters] = distance ; totDistanceComputationsToInit += 1 ; /* do other centers */ for (c = 1 ; c < self->numCenters ; ++c) { /* Can skip if the center assigned so far is twice as close as its distance to the center under consideration */ if (((self->distance == VlDistanceL1) ? 2.0 : 4.0) * pointToClosestCenterUB[x] <= ((TYPE*)self->centerDistances) [c + assignments[x] * self->numCenters]) { continue ; } distance = distFn(self->dimension, data + x * self->dimension, (TYPE*)self->centers + c * self->dimension) ; pointToCenterLB[c + x * self->numCenters] = distance ; totDistanceComputationsToInit += 1 ; if (distance < pointToClosestCenterUB[x]) { pointToClosestCenterUB[x] = distance ; assignments[x] = c ; } } } /* compute UB on energy */ energy = 0 ; for (x = 0 ; x < (signed)numData ; ++x) { energy += pointToClosestCenterUB[x] ; } if (self->verbosity) { VL_PRINTF("kmeans: Elkan iter 0: energy = %g, dist. calc. = %d\n", energy, totDistanceComputationsToInit) ; } /* #define SANITY*/ #ifdef SANITY { int xx ; int cc ; TYPE tol = 1e-5 ; VL_PRINTF("inconsistencies after initial assignments:\n"); for (xx = 0 ; xx < numData ; ++xx) { for (cc = 0 ; cc < self->numCenters ; ++cc) { TYPE a = pointToCenterLB[cc + xx * self->numCenters] ; TYPE b = distFn(self->dimension, data + self->dimension * xx, (TYPE*)self->centers + self->dimension * cc) ; if (cc == assignments[xx]) { TYPE z = pointToClosestCenterUB[xx] ; if (z+tolb+tol) VL_PRINTF("LB %d %d = %f > %f\n", cc, xx, a, b) ; } } } #endif /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* Iterations */ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ for (iteration = 1 ; 1; ++iteration) { vl_size numDistanceComputationsToRefreshUB = 0 ; vl_size numDistanceComputationsToRefreshLB = 0 ; vl_size numDistanceComputationsToRefreshCenterDistances = 0 ; vl_size numDistanceComputationsToNewCenters = 0 ; vl_size numRestartedCenters = 0 ; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* Compute new centers */ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ memset(clusterMasses, 0, sizeof(vl_size) * numData) ; for (x = 0 ; x < (signed)numData ; ++x) { clusterMasses[assignments[x]] ++ ; } switch (self->distance) { case VlDistanceL2: memset(newCenters, 0, sizeof(TYPE) * self->dimension * self->numCenters) ; for (x = 0 ; x < (signed)numData ; ++x) { TYPE * cpt = newCenters + assignments[x] * self->dimension ; TYPE const * xpt = data + x * self->dimension ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] += xpt[d] ; } } for (c = 0 ; c < self->numCenters ; ++c) { TYPE * cpt = newCenters + c * self->dimension ; if (clusterMasses[c] > 0) { TYPE mass = clusterMasses[c] ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] /= mass ; } } else { /* restart the center */ vl_uindex x = vl_rand_uindex(rand, numData) ; numRestartedCenters ++ ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] = data[x * self->dimension + d] ; } } } break ; case VlDistanceL1: for (d = 0 ; d < self->dimension ; ++d) { vl_uint32 * perm = permutations + d * numData ; memset(numSeenSoFar, 0, sizeof(vl_size) * self->numCenters) ; for (x = 0; x < (signed)numData ; ++x) { c = assignments[perm[x]] ; if (2 * numSeenSoFar[c] < clusterMasses[c]) { newCenters [d + c * self->dimension] = data [d + perm[x] * self->dimension] ; } numSeenSoFar[c] ++ ; } } /* restart the centers as required */ for (c = 0 ; c < self->numCenters ; ++c) { if (clusterMasses[c] == 0) { TYPE * cpt = newCenters + c * self->dimension ; vl_uindex x = vl_rand_uindex(rand, numData) ; numRestartedCenters ++ ; for (d = 0 ; d < self->dimension ; ++d) { cpt[d] = data[x * self->dimension + d] ; } } } break ; default: abort(); } /* done compute centers */ /* compute the distance from the old centers to the new centers */ for (c = 0 ; c < self->numCenters ; ++c) { TYPE distance = distFn(self->dimension, newCenters + c * self->dimension, (TYPE*)self->centers + c * self->dimension) ; centerToNewCenterDistances[c] = distance ; numDistanceComputationsToNewCenters += 1 ; } /* make the new centers current */ { TYPE * tmp = self->centers ; self->centers = newCenters ; newCenters = tmp ; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* Reassign points to a centers */ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* Update distances between centers. */ numDistanceComputationsToRefreshCenterDistances += VL_XCAT(_vl_kmeans_update_center_distances_, SFX)(self) ; for (c = 0 ; c < self->numCenters ; ++c) { nextCenterDistances[c] = (TYPE) VL_INFINITY_D ; for (j = 0 ; j < self->numCenters ; ++j) { if (j == c) continue ; nextCenterDistances[c] = VL_MIN(nextCenterDistances[c], ((TYPE*)self->centerDistances) [j + c * self->numCenters]) ; } } /* Update upper bounds on point-to-closest-center distances based on the center variation. */ for (x = 0 ; x < (signed)numData ; ++x) { TYPE a = pointToClosestCenterUB[x] ; TYPE b = centerToNewCenterDistances[assignments[x]] ; if (self->distance == VlDistanceL1) { pointToClosestCenterUB[x] = a + b ; } else { #if (FLT == VL_TYPE_FLOAT) TYPE sqrtab = sqrtf (a * b) ; #else TYPE sqrtab = sqrt (a * b) ; #endif pointToClosestCenterUB[x] = a + b + 2.0 * sqrtab ; } pointToClosestCenterUBIsStrict[x] = VL_FALSE ; } /* Update lower bounds on point-to-center distances based on the center variation. */ #if defined(_OPENMP) #pragma omp parallel for default(shared) private(x,c) num_threads(vl_get_max_threads()) #endif for (x = 0 ; x < (signed)numData ; ++x) { for (c = 0 ; c < self->numCenters ; ++c) { TYPE a = pointToCenterLB[c + x * self->numCenters] ; TYPE b = centerToNewCenterDistances[c] ; if (a < b) { pointToCenterLB[c + x * self->numCenters] = 0 ; } else { if (self->distance == VlDistanceL1) { pointToCenterLB[c + x * self->numCenters] = a - b ; } else { #if (FLT == VL_TYPE_FLOAT) TYPE sqrtab = sqrtf (a * b) ; #else TYPE sqrtab = sqrt (a * b) ; #endif pointToCenterLB[c + x * self->numCenters] = a + b - 2.0 * sqrtab ; } } } } #ifdef SANITY { int xx ; int cc ; TYPE tol = 1e-5 ; VL_PRINTF("inconsistencies before assignments:\n"); for (xx = 0 ; xx < numData ; ++xx) { for (cc = 0 ; cc < self->numCenters ; ++cc) { TYPE a = pointToCenterLB[cc + xx * self->numCenters] ; TYPE b = distFn(self->dimension, data + self->dimension * xx, (TYPE*)self->centers + self->dimension * cc) ; if (cc == assignments[xx]) { TYPE z = pointToClosestCenterUB[xx] ; if (z+tolb+tol) VL_PRINTF("LB %d %d = %f > %f (assign = %d)\n", cc, xx, a, b, assignments[xx]) ; } } } #endif /* Scan the data and do the reassignments. Use the bounds to skip as many point-to-center distance calculations as possible. */ allDone = VL_TRUE ; #if defined(_OPENMP) #pragma omp parallel for \ default(none) \ shared(self,numData, \ pointToClosestCenterUB,pointToCenterLB, \ nextCenterDistances,pointToClosestCenterUBIsStrict, \ assignments,data,distFn,allDone) \ private(c,x) \ reduction(+:numDistanceComputationsToRefreshUB,numDistanceComputationsToRefreshLB) \ num_threads(vl_get_max_threads()) #endif for (x = 0 ; x < (signed)numData ; ++ x) { /* A point x sticks with its current center assignmets[x] the UB to d(x, c[assigmnets[x]]) is not larger than half the distance of c[assigments[x]] to any other center c. */ if (((self->distance == VlDistanceL1) ? 2.0 : 4.0) * pointToClosestCenterUB[x] <= nextCenterDistances[assignments[x]]) { continue ; } for (c = 0 ; c < self->numCenters ; ++c) { vl_uint32 cx = assignments[x] ; TYPE distance ; /* The point is not reassigned to a given center c if either: 0 - c is already the assigned center 1 - The UB of d(x, c[assignments[x]]) is smaller than half the distance of c[assigments[x]] to c, OR 2 - The UB of d(x, c[assignmets[x]]) is smaller than the LB of the distance of x to c. */ if (cx == c) { continue ; } if (((self->distance == VlDistanceL1) ? 2.0 : 4.0) * pointToClosestCenterUB[x] <= ((TYPE*)self->centerDistances) [c + cx * self->numCenters]) { continue ; } if (pointToClosestCenterUB[x] <= pointToCenterLB [c + x * self->numCenters]) { continue ; } /* If the UB is loose, try recomputing it and test again */ if (! pointToClosestCenterUBIsStrict[x]) { distance = distFn(self->dimension, data + self->dimension * x, (TYPE*)self->centers + self->dimension * cx) ; pointToClosestCenterUB[x] = distance ; pointToClosestCenterUBIsStrict[x] = VL_TRUE ; pointToCenterLB[cx + x * self->numCenters] = distance ; numDistanceComputationsToRefreshUB += 1 ; if (((self->distance == VlDistanceL1) ? 2.0 : 4.0) * pointToClosestCenterUB[x] <= ((TYPE*)self->centerDistances) [c + cx * self->numCenters]) { continue ; } if (pointToClosestCenterUB[x] <= pointToCenterLB [c + x * self->numCenters]) { continue ; } } /* Now the UB is strict (equal to d(x, assignments[x])), but we still could not exclude that x should be reassigned to c. We therefore compute the distance, update the LB, and check if a reassigmnet must be made */ distance = distFn(self->dimension, data + x * self->dimension, (TYPE*)self->centers + c * self->dimension) ; numDistanceComputationsToRefreshLB += 1 ; pointToCenterLB[c + x * self->numCenters] = distance ; if (distance < pointToClosestCenterUB[x]) { assignments[x] = c ; pointToClosestCenterUB[x] = distance ; allDone = VL_FALSE ; /* the UB strict flag is already set here */ } } /* assign center */ } /* next data point */ totDistanceComputationsToRefreshUB += numDistanceComputationsToRefreshUB ; totDistanceComputationsToRefreshLB += numDistanceComputationsToRefreshLB ; totDistanceComputationsToRefreshCenterDistances += numDistanceComputationsToRefreshCenterDistances ; totDistanceComputationsToNewCenters += numDistanceComputationsToNewCenters ; totNumRestartedCenters += numRestartedCenters ; #ifdef SANITY { int xx ; int cc ; TYPE tol = 1e-5 ; VL_PRINTF("inconsistencies after assignments:\n"); for (xx = 0 ; xx < numData ; ++xx) { for (cc = 0 ; cc < self->numCenters ; ++cc) { TYPE a = pointToCenterLB[cc + xx * self->numCenters] ; TYPE b = distFn(self->dimension, data + self->dimension * xx, (TYPE*)self->centers + self->dimension * cc) ; if (cc == assignments[xx]) { TYPE z = pointToClosestCenterUB[xx] ; if (z+tolb+tol) VL_PRINTF("LB %d %d = %f > %f (assign = %d)\n", cc, xx, a, b, assignments[xx]) ; } } } #endif /* compute UB on energy */ energy = 0 ; for (x = 0 ; x < (signed)numData ; ++x) { energy += pointToClosestCenterUB[x] ; } if (self->verbosity) { vl_size numDistanceComputations = numDistanceComputationsToRefreshUB + numDistanceComputationsToRefreshLB + numDistanceComputationsToRefreshCenterDistances + numDistanceComputationsToNewCenters ; VL_PRINTF("kmeans: Elkan iter %d: energy <= %g, dist. calc. = %d\n", iteration, energy, numDistanceComputations) ; if (numRestartedCenters) { VL_PRINTF("kmeans: Elkan iter %d: restarted %d centers\n", iteration, energy, numRestartedCenters) ; } if (self->verbosity > 1) { VL_PRINTF("kmeans: Elkan iter %d: total dist. calc. per type: " "UB: %.1f%% (%d), LB: %.1f%% (%d), " "intra_center: %.1f%% (%d), " "new_center: %.1f%% (%d)\n", iteration, 100.0 * numDistanceComputationsToRefreshUB / numDistanceComputations, numDistanceComputationsToRefreshUB, 100.0 *numDistanceComputationsToRefreshLB / numDistanceComputations, numDistanceComputationsToRefreshLB, 100.0 * numDistanceComputationsToRefreshCenterDistances / numDistanceComputations, numDistanceComputationsToRefreshCenterDistances, 100.0 * numDistanceComputationsToNewCenters / numDistanceComputations, numDistanceComputationsToNewCenters) ; } } /* check termination conditions */ if (iteration >= self->maxNumIterations) { if (self->verbosity) { VL_PRINTF("kmeans: Elkan terminating because maximum number of iterations reached\n") ; } break ; } if (allDone) { if (self->verbosity) { VL_PRINTF("kmeans: Elkan terminating because the algorithm fully converged\n") ; } break ; } } /* next Elkan iteration */ /* compute true energy */ energy = 0 ; for (x = 0 ; x < (signed)numData ; ++ x) { vl_uindex cx = assignments [x] ; energy += distFn(self->dimension, data + self->dimension * x, (TYPE*)self->centers + self->dimension * cx) ; totDistanceComputationsToFinalize += 1 ; } { vl_size totDistanceComputations = totDistanceComputationsToInit + totDistanceComputationsToRefreshUB + totDistanceComputationsToRefreshLB + totDistanceComputationsToRefreshCenterDistances + totDistanceComputationsToNewCenters + totDistanceComputationsToFinalize ; double saving = (double)totDistanceComputations / (iteration * self->numCenters * numData) ; if (self->verbosity) { VL_PRINTF("kmeans: Elkan: total dist. calc.: %d (%.2f %% of Lloyd)\n", totDistanceComputations, saving * 100.0) ; if (totNumRestartedCenters) { VL_PRINTF("kmeans: Elkan: there have been %d restarts\n", totNumRestartedCenters) ; } } if (self->verbosity > 1) { VL_PRINTF("kmeans: Elkan: total dist. calc. per type: " "init: %.1f%% (%d), UB: %.1f%% (%d), LB: %.1f%% (%d), " "intra_center: %.1f%% (%d), " "new_center: %.1f%% (%d), " "finalize: %.1f%% (%d)\n", 100.0 * totDistanceComputationsToInit / totDistanceComputations, totDistanceComputationsToInit, 100.0 * totDistanceComputationsToRefreshUB / totDistanceComputations, totDistanceComputationsToRefreshUB, 100.0 *totDistanceComputationsToRefreshLB / totDistanceComputations, totDistanceComputationsToRefreshLB, 100.0 * totDistanceComputationsToRefreshCenterDistances / totDistanceComputations, totDistanceComputationsToRefreshCenterDistances, 100.0 * totDistanceComputationsToNewCenters / totDistanceComputations, totDistanceComputationsToNewCenters, 100.0 * totDistanceComputationsToFinalize / totDistanceComputations, totDistanceComputationsToFinalize) ; } } if (permutations) { vl_free(permutations) ; } if (numSeenSoFar) { vl_free(numSeenSoFar) ; } vl_free(distances) ; vl_free(assignments) ; vl_free(clusterMasses) ; vl_free(nextCenterDistances) ; vl_free(pointToClosestCenterUB) ; vl_free(pointToClosestCenterUBIsStrict) ; vl_free(pointToCenterLB) ; vl_free(newCenters) ; vl_free(centerToNewCenterDistances) ; return energy ; } /* ---------------------------------------------------------------- */ static double VL_XCAT(_vl_kmeans_refine_centers_, SFX) (VlKMeans * self, TYPE const * data, vl_size numData) { switch (self->algorithm) { case VlKMeansLloyd: return VL_XCAT(_vl_kmeans_refine_centers_lloyd_, SFX)(self, data, numData) ; break ; case VlKMeansElkan: return VL_XCAT(_vl_kmeans_refine_centers_elkan_, SFX)(self, data, numData) ; break ; case VlKMeansANN: return VL_XCAT(_vl_kmeans_refine_centers_ann_, SFX)(self, data, numData) ; break ; default: abort() ; } } /* VL_KMEANS_INSTANTIATING */ #else #ifndef __DOXYGEN__ #define FLT VL_TYPE_FLOAT #define TYPE float #define SFX f #define VL_KMEANS_INSTANTIATING #include "kmeans.c" #define FLT VL_TYPE_DOUBLE #define TYPE double #define SFX d #define VL_KMEANS_INSTANTIATING #include "kmeans.c" #endif /* VL_KMEANS_INSTANTIATING */ #endif /* ================================================================ */ #ifndef VL_KMEANS_INSTANTIATING /** ------------------------------------------------------------------ ** @brief Set centers ** @param self KMeans object. ** @param centers centers to copy. ** @param dimension data dimension. ** @param numCenters number of centers. **/ VL_EXPORT void vl_kmeans_set_centers (VlKMeans * self, void const * centers, vl_size dimension, vl_size numCenters) { vl_kmeans_reset (self) ; switch (self->dataType) { case VL_TYPE_FLOAT : _vl_kmeans_set_centers_f (self, (float const *)centers, dimension, numCenters) ; break ; case VL_TYPE_DOUBLE : _vl_kmeans_set_centers_d (self, (double const *)centers, dimension, numCenters) ; break ; default: abort() ; } } /** ------------------------------------------------------------------ ** @brief init centers by randomly sampling data ** @param self KMeans object. ** @param data data to sample from. ** @param dimension data dimension. ** @param numData nmber of data points. ** @param numCenters number of centers. ** ** The function inits the KMeans centers by randomly sampling ** the data @a data. **/ VL_EXPORT void vl_kmeans_init_centers_with_rand_data (VlKMeans * self, void const * data, vl_size dimension, vl_size numData, vl_size numCenters) { vl_kmeans_reset (self) ; switch (self->dataType) { case VL_TYPE_FLOAT : _vl_kmeans_init_centers_with_rand_data_f (self, (float const *)data, dimension, numData, numCenters) ; break ; case VL_TYPE_DOUBLE : _vl_kmeans_init_centers_with_rand_data_d (self, (double const *)data, dimension, numData, numCenters) ; break ; default: abort() ; } } /** ------------------------------------------------------------------ ** @brief Seed centers by the KMeans++ algorithm ** @param self KMeans object. ** @param data data to sample from. ** @param dimension data dimension. ** @param numData nmber of data points. ** @param numCenters number of centers. **/ VL_EXPORT void vl_kmeans_init_centers_plus_plus (VlKMeans * self, void const * data, vl_size dimension, vl_size numData, vl_size numCenters) { vl_kmeans_reset (self) ; switch (self->dataType) { case VL_TYPE_FLOAT : _vl_kmeans_init_centers_plus_plus_f (self, (float const *)data, dimension, numData, numCenters) ; break ; case VL_TYPE_DOUBLE : _vl_kmeans_init_centers_plus_plus_d (self, (double const *)data, dimension, numData, numCenters) ; break ; default: abort() ; } } /** ------------------------------------------------------------------ ** @brief Quantize data ** @param self KMeans object. ** @param assignments data to closest center assignments (output). ** @param distances data to closest center distance (output). ** @param data data to quantize. ** @param numData number of data points to quantize. **/ VL_EXPORT void vl_kmeans_quantize (VlKMeans * self, vl_uint32 * assignments, void * distances, void const * data, vl_size numData) { switch (self->dataType) { case VL_TYPE_FLOAT : _vl_kmeans_quantize_f (self, assignments, distances, (float const *)data, numData) ; break ; case VL_TYPE_DOUBLE : _vl_kmeans_quantize_d (self, assignments, distances, (double const *)data, numData) ; break ; default: abort() ; } } /** ------------------------------------------------------------------ ** @brief Quantize data using approximate nearest neighbours (ANN). ** @param self KMeans object. ** @param assignments data to centers assignments (output). ** @param distances data to closes center distance (output) ** @param data data to quantize. ** @param numData number of data points. ** @param update choose wether to update current assignments. ** ** The function uses an ANN procedure to compute the approximate ** nearest neighbours of the input data point. ** ** Setting @a update to ::VL_TRUE will cause the algorithm ** to *update existing assignments*. This means that each ** element of @a assignments and @a distances is updated ony if the ** ANN procedure can find a better assignment of the existing one. **/ VL_EXPORT void vl_kmeans_quantize_ann (VlKMeans * self, vl_uint32 * assignments, void * distances, void const * data, vl_size numData, vl_bool update) { switch (self->dataType) { case VL_TYPE_FLOAT : _vl_kmeans_quantize_ann_f (self, assignments, distances, (float const *)data, numData, update) ; break ; case VL_TYPE_DOUBLE : _vl_kmeans_quantize_ann_d (self, assignments, distances, (double const *)data, numData, update) ; break ; default: abort() ; } } /** ------------------------------------------------------------------ ** @brief Refine center locations. ** @param self KMeans object. ** @param data data to quantize. ** @param numData number of data points. ** @return K-means energy at the end of optimization. ** ** The function calls the underlying K-means quantization algorithm ** (@ref VlKMeansAlgorithm) to quantize the specified data @a data. ** The function assumes that the cluster centers have already ** been assigned by using one of the seeding functions, or by ** setting them. **/ VL_EXPORT double vl_kmeans_refine_centers (VlKMeans * self, void const * data, vl_size numData) { assert (self->centers) ; switch (self->dataType) { case VL_TYPE_FLOAT : return _vl_kmeans_refine_centers_f (self, (float const *)data, numData) ; case VL_TYPE_DOUBLE : return _vl_kmeans_refine_centers_d (self, (double const *)data, numData) ; default: abort() ; } } /** ------------------------------------------------------------------ ** @brief Cluster data. ** @param self KMeans object. ** @param data data to quantize. ** @param dimension data dimension. ** @param numData number of data points. ** @param numCenters number of clusters. ** @return K-means energy at the end of optimization. ** ** The function initializes the centers by using the initialization ** algorithm set by ::vl_kmeans_set_initialization and refines them ** by the quantization algorithm set by ::vl_kmeans_set_algorithm. ** The process is repeated one or more times (see ** ::vl_kmeans_set_num_repetitions) and the resutl with smaller ** energy is retained. **/ VL_EXPORT double vl_kmeans_cluster (VlKMeans * self, void const * data, vl_size dimension, vl_size numData, vl_size numCenters) { vl_uindex repetition ; double bestEnergy = VL_INFINITY_D ; void * bestCenters = NULL ; for (repetition = 0 ; repetition < self->numRepetitions ; ++ repetition) { double energy ; double timeRef ; if (self->verbosity) { VL_PRINTF("kmeans: repetition %d of %d\n", repetition + 1, self->numRepetitions) ; } timeRef = vl_get_cpu_time() ; switch (self->initialization) { case VlKMeansRandomSelection : vl_kmeans_init_centers_with_rand_data (self, data, dimension, numData, numCenters) ; break ; case VlKMeansPlusPlus : vl_kmeans_init_centers_plus_plus (self, data, dimension, numData, numCenters) ; break ; default: abort() ; } if (self->verbosity) { VL_PRINTF("kmeans: K-means initialized in %.2f s\n", vl_get_cpu_time() - timeRef) ; } timeRef = vl_get_cpu_time () ; energy = vl_kmeans_refine_centers (self, data, numData) ; if (self->verbosity) { VL_PRINTF("kmeans: K-means terminated in %.2f s with energy %g\n", vl_get_cpu_time() - timeRef, energy) ; } /* copy centers to output if current solution is optimal */ /* check repetition == 0 as well in case energy = NaN, which */ /* can happen if the data contain NaNs */ if (energy < bestEnergy || repetition == 0) { void * temp ; bestEnergy = energy ; if (bestCenters == NULL) { bestCenters = vl_malloc(vl_get_type_size(self->dataType) * self->dimension * self->numCenters) ; } /* swap buffers */ temp = bestCenters ; bestCenters = self->centers ; self->centers = temp ; } /* better energy */ } /* next repetition */ vl_free (self->centers) ; self->centers = bestCenters ; return bestEnergy ; } /* VL_KMEANS_INSTANTIATING */ #endif #undef SFX #undef TYPE #undef FLT #undef VL_KMEANS_INSTANTIATING colmap-3.9.1/src/thirdparty/VLFeat/kmeans.h000066400000000000000000000317471454702036400205510ustar00rootroot00000000000000/** @file kmeans.h ** @brief K-means (@ref kmeans) ** @author Andrea Vedaldi ** @author David Novotny **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. Copyright (C) 2013 Andrea Vedaldi and David Novotny. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_KMEANS_H #define VL_KMEANS_H #include "generic.h" #include "random.h" #include "mathop.h" #include "kdtree.h" /* ---------------------------------------------------------------- */ /** @brief K-means algorithms */ typedef enum _VlKMeansAlgorithm { VlKMeansLloyd, /**< Lloyd algorithm */ VlKMeansElkan, /**< Elkan algorithm */ VlKMeansANN /**< Approximate nearest neighbors */ } VlKMeansAlgorithm ; /** @brief K-means initialization algorithms */ typedef enum _VlKMeansInitialization { VlKMeansRandomSelection, /**< Randomized selection */ VlKMeansPlusPlus /**< Plus plus raondomized selection */ } VlKMeansInitialization ; /** ------------------------------------------------------------------ ** @brief K-means quantizer **/ typedef struct _VlKMeans { vl_type dataType ; /**< Data type. */ vl_size dimension ; /**< Data dimensionality. */ vl_size numCenters ; /**< Number of centers. */ vl_size numTrees ; /**< Number of trees in forest when using ANN-kmeans. */ vl_size maxNumComparisons ; /**< Maximum number of comparisons when using ANN-kmeans. */ VlKMeansInitialization initialization ; /**< Initalization algorithm. */ VlKMeansAlgorithm algorithm ; /**< Clustring algorithm. */ VlVectorComparisonType distance ; /**< Distance. */ vl_size maxNumIterations ; /**< Maximum number of refinement iterations. */ double minEnergyVariation ; /**< Minimum energy variation. */ vl_size numRepetitions ; /**< Number of clustering repetitions. */ int verbosity ; /**< Verbosity level. */ void * centers ; /**< Centers */ void * centerDistances ; /**< Centers inter-distances. */ double energy ; /**< Current solution energy. */ VlFloatVectorComparisonFunction floatVectorComparisonFn ; VlDoubleVectorComparisonFunction doubleVectorComparisonFn ; } VlKMeans ; /** @name Create and destroy ** @{ **/ VL_EXPORT VlKMeans * vl_kmeans_new (vl_type dataType, VlVectorComparisonType distance) ; VL_EXPORT VlKMeans * vl_kmeans_new_copy (VlKMeans const * kmeans) ; VL_EXPORT void vl_kmeans_delete (VlKMeans * self) ; /** @} */ /** @name Basic data processing ** @{ **/ VL_EXPORT void vl_kmeans_reset (VlKMeans * self) ; VL_EXPORT double vl_kmeans_cluster (VlKMeans * self, void const * data, vl_size dimension, vl_size numData, vl_size numCenters) ; VL_EXPORT void vl_kmeans_quantize (VlKMeans * self, vl_uint32 * assignments, void * distances, void const * data, vl_size numData) ; VL_EXPORT void vl_kmeans_quantize_ANN (VlKMeans * self, vl_uint32 * assignments, void * distances, void const * data, vl_size numData, vl_size iteration ); /** @} */ /** @name Advanced data processing ** @{ **/ VL_EXPORT void vl_kmeans_set_centers (VlKMeans * self, void const * centers, vl_size dimension, vl_size numCenters) ; VL_EXPORT void vl_kmeans_init_centers_with_rand_data (VlKMeans * self, void const * data, vl_size dimensions, vl_size numData, vl_size numCenters) ; VL_EXPORT void vl_kmeans_init_centers_plus_plus (VlKMeans * self, void const * data, vl_size dimensions, vl_size numData, vl_size numCenters) ; VL_EXPORT double vl_kmeans_refine_centers (VlKMeans * self, void const * data, vl_size numData) ; /** @} */ /** @name Retrieve data and parameters ** @{ **/ VL_INLINE vl_type vl_kmeans_get_data_type (VlKMeans const * self) ; VL_INLINE VlVectorComparisonType vl_kmeans_get_distance (VlKMeans const * self) ; VL_INLINE VlKMeansAlgorithm vl_kmeans_get_algorithm (VlKMeans const * self) ; VL_INLINE VlKMeansInitialization vl_kmeans_get_initialization (VlKMeans const * self) ; VL_INLINE vl_size vl_kmeans_get_num_repetitions (VlKMeans const * self) ; VL_INLINE vl_size vl_kmeans_get_dimension (VlKMeans const * self) ; VL_INLINE vl_size vl_kmeans_get_num_centers (VlKMeans const * self) ; VL_INLINE int vl_kmeans_get_verbosity (VlKMeans const * self) ; VL_INLINE vl_size vl_kmeans_get_max_num_iterations (VlKMeans const * self) ; VL_INLINE double vl_kmeans_get_min_energy_variation (VlKMeans const * self) ; VL_INLINE vl_size vl_kmeans_get_max_num_comparisons (VlKMeans const * self) ; VL_INLINE vl_size vl_kmeans_get_num_trees (VlKMeans const * self) ; VL_INLINE double vl_kmeans_get_energy (VlKMeans const * self) ; VL_INLINE void const * vl_kmeans_get_centers (VlKMeans const * self) ; /** @} */ /** @name Set parameters ** @{ **/ VL_INLINE void vl_kmeans_set_algorithm (VlKMeans * self, VlKMeansAlgorithm algorithm) ; VL_INLINE void vl_kmeans_set_initialization (VlKMeans * self, VlKMeansInitialization initialization) ; VL_INLINE void vl_kmeans_set_num_repetitions (VlKMeans * self, vl_size numRepetitions) ; VL_INLINE void vl_kmeans_set_max_num_iterations (VlKMeans * self, vl_size maxNumIterations) ; VL_INLINE void vl_kmeans_set_min_energy_variation (VlKMeans * self, double minEnergyVariation) ; VL_INLINE void vl_kmeans_set_verbosity (VlKMeans * self, int verbosity) ; VL_INLINE void vl_kmeans_set_max_num_comparisons (VlKMeans * self, vl_size maxNumComparisons) ; VL_INLINE void vl_kmeans_set_num_trees (VlKMeans * self, vl_size numTrees) ; /** @} */ /** ------------------------------------------------------------------ ** @brief Get data type ** @param self KMeans object instance. ** @return data type. **/ VL_INLINE vl_type vl_kmeans_get_data_type (VlKMeans const * self) { return self->dataType ; } /** @brief Get data dimension ** @param self KMeans object instance. ** @return data dimension. **/ VL_INLINE vl_size vl_kmeans_get_dimension (VlKMeans const * self) { return self->dimension ; } /** @brief Get data type ** @param self KMeans object instance. ** @return data type. **/ VL_INLINE VlVectorComparisonType vl_kmeans_get_distance (VlKMeans const * self) { return self->distance ; } /** @brief Get the number of centers (K) ** @param self KMeans object instance. ** @return number of centers. **/ VL_INLINE vl_size vl_kmeans_get_num_centers (VlKMeans const * self) { return self->numCenters ; } /** @brief Get the number energy of the current fit ** @param self KMeans object instance. ** @return energy. **/ VL_INLINE double vl_kmeans_get_energy (VlKMeans const * self) { return self->energy ; } /** ------------------------------------------------------------------ ** @brief Get verbosity level ** @param self KMeans object instance. ** @return verbosity level. **/ VL_INLINE int vl_kmeans_get_verbosity (VlKMeans const * self) { return self->verbosity ; } /** @brief Set verbosity level ** @param self KMeans object instance. ** @param verbosity verbosity level. **/ VL_INLINE void vl_kmeans_set_verbosity (VlKMeans * self, int verbosity) { self->verbosity = verbosity ; } /** ------------------------------------------------------------------ ** @brief Get centers ** @param self KMeans object instance. ** @return cluster centers. **/ VL_INLINE void const * vl_kmeans_get_centers (VlKMeans const * self) { return self->centers ; } /** ------------------------------------------------------------------ ** @brief Get maximum number of iterations ** @param self KMeans object instance. ** @return maximum number of iterations. **/ VL_INLINE vl_size vl_kmeans_get_max_num_iterations (VlKMeans const * self) { return self->maxNumIterations ; } /** @brief Set maximum number of iterations ** @param self KMeans filter. ** @param maxNumIterations maximum number of iterations. **/ VL_INLINE void vl_kmeans_set_max_num_iterations (VlKMeans * self, vl_size maxNumIterations) { self->maxNumIterations = maxNumIterations ; } /** ------------------------------------------------------------------ ** @brief Get maximum number of repetitions. ** @param self KMeans object instance. ** @return current number of repretitions for quantization. **/ VL_INLINE vl_size vl_kmeans_get_num_repetitions (VlKMeans const * self) { return self->numRepetitions ; } /** @brief Set maximum number of repetitions ** @param self KMeans object instance. ** @param numRepetitions maximum number of repetitions. ** The number of repetitions cannot be smaller than 1. **/ VL_INLINE void vl_kmeans_set_num_repetitions (VlKMeans * self, vl_size numRepetitions) { assert (numRepetitions >= 1) ; self->numRepetitions = numRepetitions ; } /** ------------------------------------------------------------------ ** @brief Get the minimum relative energy variation for convergence. ** @param self KMeans object instance. ** @return minimum energy variation. **/ VL_INLINE double vl_kmeans_get_min_energy_variation (VlKMeans const * self) { return self->minEnergyVariation ; } /** @brief Set the maximum relative energy variation for convergence. ** @param self KMeans object instance. ** @param minEnergyVariation maximum number of repetitions. ** The variation cannot be negative. ** ** The relative energy variation is calculated after the $t$-th update ** to the parameters as: ** ** \[ \epsilon_t = \frac{E_{t-1} - E_t}{E_0 - E_t} \] ** ** Note that this quantitiy is non-negative since $E_{t+1} \leq E_t$. ** Hence, $\epsilon_t$ is the improvement to the energy made in the last ** iteration compared to the total improvement so far. The algorithm ** stops if this value is less or equal than @a minEnergyVariation. ** ** This test is applied only to the LLoyd and ANN algorithms. **/ VL_INLINE void vl_kmeans_set_min_energy_variation (VlKMeans * self, double minEnergyVariation) { assert (minEnergyVariation >= 0) ; self->minEnergyVariation = minEnergyVariation ; } /** ------------------------------------------------------------------ ** @brief Get K-means algorithm ** @param self KMeans object. ** @return algorithm. **/ VL_INLINE VlKMeansAlgorithm vl_kmeans_get_algorithm (VlKMeans const * self) { return self->algorithm ; } /** @brief Set K-means algorithm ** @param self KMeans object. ** @param algorithm K-means algorithm. **/ VL_INLINE void vl_kmeans_set_algorithm (VlKMeans * self, VlKMeansAlgorithm algorithm) { self->algorithm = algorithm ; } /** ------------------------------------------------------------------ ** @brief Get K-means initialization algorithm ** @param self KMeans object. ** @return algorithm. **/ VL_INLINE VlKMeansInitialization vl_kmeans_get_initialization (VlKMeans const * self) { return self->initialization ; } /** @brief Set K-means initialization algorithm ** @param self KMeans object. ** @param initialization initialization. **/ VL_INLINE void vl_kmeans_set_initialization (VlKMeans * self, VlKMeansInitialization initialization) { self->initialization = initialization ; } /** ------------------------------------------------------------------ ** @brief Get the maximum number of comparisons in the KD-forest ANN algorithm. ** @param self KMeans object instance. ** @return maximum number of comparisons. **/ VL_INLINE vl_size vl_kmeans_get_max_num_comparisons (VlKMeans const * self) { return self->maxNumComparisons ; } /** @brief Set maximum number of comparisons in ANN-KD-Tree. ** @param self KMeans filter. ** @param maxNumComparisons maximum number of comparisons. **/ VL_INLINE void vl_kmeans_set_max_num_comparisons (VlKMeans * self, vl_size maxNumComparisons) { self->maxNumComparisons = maxNumComparisons; } /** ------------------------------------------------------------------ ** @brief Set the number of trees in the KD-forest ANN algorithm ** @param self KMeans object instance. ** @param numTrees number of trees to use. **/ VL_INLINE void vl_kmeans_set_num_trees (VlKMeans * self, vl_size numTrees) { self->numTrees = numTrees; } VL_INLINE vl_size vl_kmeans_get_num_trees (VlKMeans const * self) { return self->numTrees; } /* VL_IKMEANS_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/lbp.c000077500000000000000000000276431454702036400200460ustar00rootroot00000000000000/** @file lbp.c ** @brief Local Binary Patterns (LBP) - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 Andrea Vedaldi. Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page lbp Local Binary Patterns (LBP) descriptor @author Andrea Vedaldi @ref lbp.h implements the Local Binary Pattern (LBP) feature descriptor. The LBP descriptor @cite{ojala10multiresolution} is a histogram of quantized LBPs pooled in a local image neighborhood. @ref lbp-starting demonstrates how to use the C API to compute the LBP descriptors of an image. For further details refer to: - @subpage lbp-fundamentals - LBP definition and parameters. @section lbp-starting Getting started with LBP To compute the LBP descriptor of an image, start by creating a ::VlLbp object instance by specifying the type of LBP quantization. Given the configure LBP object, then call ::vl_lbp_process to process a grayscale image and obtain the corresponding LBP descriptors. This function expects as input a buffer large enough to contain the computed features. If the image has size @c width x @c height, there are exactly @c floor(width/cellSize) x @c floor(height/cellSize) cells, each of which has a histogram of LBPs of size @c dimension (as returned by ::vl_lbp_get_dimension). Thus the required buffer has size @c floor(width/cellSize) x @c floor(height/cellSize) x @c dimension. ::VlLbp supports computing transposed LPBs as well. A transposed LBP is the LBP obtained by transposing the input image (regarded as a matrix). This functionality can be useful to compute the features when the input image is stored in column major format (e.g. MATLAB) rather than row major. **/ /** @page lbp-fundamentals Local Binary Patterns fundamentals @tableofcontents A *Locally Binary Pattern* (LBP) is a local descriptor that captures the appearance of an image in a small neighborhood around a pixel. An LBP is a string of bits, with one bit for each of the pixels in the neighborhood. Each bit is turned on or off depending on whether the intensity of the corresponding pixel is greater than the intensity of the central pixel. LBP are seldom used directly, however. Instead, the binary string thus produced are further quantized (@ref lbp-quantization) and pooled in local histograms (@ref lbp-histograms). While many variants are possible, ::VlLbp implements only the case of 3 × 3 pixel neighborhoods (this setting was found to be optimal in several applications). In particular, the LBP centered on pixel $(x,y)$ is a string of eight bits. Each bit is equal to one if the corresponding pixel is brighter than the central one. Pixels are scanned starting from the one to the right in anti-clockwise order. For example the first bit is one if, and only if, $I(x+1,y) > I(x,y)$, and the second bit is one if, and only if, $I(x+1,y-1) > I(x,y)$. @section lbp-quantization Quantized LBP For a 3 × 3 neighborhood, an LBP is a string of eight bits and so there are 256 possible LBPs. These are usually too many for a reliable statistics (histogram) to be computed. Therefore the 256 patterns are further quantized into a smaller number of patterns according to one of the following rules: - Uniform (::VlLbpUniform) There is one quantized pattern for each LBP that has exactly a transitions from 0 to 1 and one from 1 to 0 when scanned in anti-clockwise order, plus one quantized pattern comprising the two uniform LBPs, and one quantized pattern comprising all the other LBPs. This yields a total of 58 quantized patterns. @image html lbp.png "LBP quantized patterns." The number of quantized LBPs, which depends on the quantization type, can be obtained by ::vl_lbp_get_dimension. @section lbp-histograms Histograms of LBPs The quantized LBP patterns are further grouped into local histograms. The image is divided into a number of cells of a prescribed size (as specified by the parameter @c cellSize passed to ::vl_lbp_process as described in @ref lbp-starting). Then the quantized LBPs are aggregated into histogram by using bilinear interpolation along the two spatial dimensions (similar to HOG and SIFT). **/ #include "lbp.h" #include "mathop.h" #include "string.h" /* ---------------------------------------------------------------- */ /* Initialization helpers */ /* ---------------------------------------------------------------- */ /* This function creates the LBP quantization table for the uniform LBP patterns. The purpose of this lookup table is to map a 8-bit LBP strings to one of 58 uniform pattern codes. Pixels in the 8-neighbourhoods are read in counterclockwise order starting from the east direction, as follows: NW(5) N(6) NE(7) W(4) E(0) -> b0 b1 b2 b3 b4 b5 b6 b7 SW(3) S(2) SE(1) There are 256 such strings, indexing the lookup table. The table contains the corresponding code, effectively quantizing the 256 patterns into 58. There is one bin for constant patterns (all zeros or ones), 8*7 for the uniform ones, and one for all other. A uniform pattern is a circular sequence of bit b0b1...b7 such that there is exactly one switch from 0 to 1 and one from 1 to 0. These uniform patterns are enumerated as follows. The slowest varying index i (0...7) points to the first bit that is on and the slowest varying index j (1...7) to the length of the run of bits equal to one, resulting in the sequence: 0: 1000 0000 1: 1100 0000 ... 7: 1111 1110 8: 0100 0000 9: 0110 0000 ... 56: 1111 1101 The function also accounts for when the image is stored in transposed format. The sampling function is unchanged, so that the first bit to be read is not the one to the east, but the one to the south, and overall the following sequence is read: NW(5) W(4) SW(3) N(6) S(2) -> b2 b1 b0 b7 b6 b5 b4 b3 NE(7) E(0) SE(1) In enumerating the uniform patterns, the index j is unchanged as it encodes the runlenght. On the contrary, the index i changes to account for the transposition and for the fact that the beginning and ending of the run are swapped. With modular arithmetic, the i must be transformed as ip = - i + 2 - (j - 1) */ static void _vl_lbp_init_uniform(VlLbp * self) { int i, j ; /* overall number of quantized LBPs */ self->dimension = 58 ; /* all but selected patterns map to bin 57 (the first bin has index 0) */ for (i = 0 ; i < 256 ; ++i) { self->mapping[i] = 57 ; } /* the uniform (all zeros or ones) patterns map to bin 56 */ self->mapping[0x00] = 56 ; self->mapping[0xff] = 56 ; /* 56 uniform patterns */ for (i = 0 ; i < 8 ; ++i) { for (j = 1 ; j <= 7 ; ++j) { int ip ; int unsigned string ; if (self->transposed) { ip = (- i + 2 - (j - 1) + 16) % 8 ; } else { ip = i ; } /* string starting with j ones */ string = (1 << j) - 1 ; string <<= ip ; string = (string | (string >> 8)) & 0xff ; self->mapping[string] = i * 7 + (j-1) ; } } } /* ---------------------------------------------------------------- */ /** @brief Create a new LBP object ** @param type type of LBP features. ** @param transposed if @c true, then transpose each LBP pattern. ** @return new VlLbp object instance. **/ VlLbp * vl_lbp_new(VlLbpMappingType type, vl_bool transposed) { VlLbp * self = vl_malloc(sizeof(VlLbp)) ; if (self == NULL) { vl_set_last_error(VL_ERR_ALLOC, NULL) ; return NULL ; } self->transposed = transposed ; switch (type) { case VlLbpUniform: _vl_lbp_init_uniform(self) ; break ; default: exit(1) ; } return self ; } /** @brief Delete VlLbp object ** @param self object to delete. **/ void vl_lbp_delete(VlLbp * self) { vl_free(self) ; } /** @brief Get the dimension of the LBP histograms ** @return dimension of the LBP histograms. ** The dimension depends on the type of quantization used. ** @see ::vl_lbp_new(). **/ VL_EXPORT vl_size vl_lbp_get_dimension(VlLbp * self) { return self->dimension ; } /* ---------------------------------------------------------------- */ /** @brief Extract LBP features ** @param self LBP object. ** @param features buffer to write the features to. ** @param image image. ** @param width image width. ** @param height image height. ** @param cellSize size of the LBP cells. ** ** @a features is a @c numColumns x @c numRows x @c dimension where ** @c dimension is the dimension of a LBP feature obtained from ::vl_lbp_get_dimension, ** @c numColumns is equal to @c floor(width / cellSize), and similarly ** for @c numRows. **/ VL_EXPORT void vl_lbp_process (VlLbp * self, float * features, float * image, vl_size width, vl_size height, vl_size cellSize) { vl_size cwidth = width / cellSize; vl_size cheight = height / cellSize ; vl_size cstride = cwidth * cheight ; vl_size cdimension = vl_lbp_get_dimension(self) ; vl_index x,y,cx,cy,k,bin ; #define at(u,v) (*(image + width * (v) + (u))) #define to(u,v,w) (*(features + cstride * (w) + cwidth * (v) + (u))) /* clear the output buffer */ memset(features, 0, sizeof(float)*cdimension*cstride) ; /* accumulate pixel-level measurements into cells */ for (y = 1 ; y < (signed)height - 1 ; ++y) { float wy1 = (y + 0.5f) / (float)cellSize - 0.5f ; int cy1 = (int) vl_floor_f(wy1) ; int cy2 = cy1 + 1 ; float wy2 = wy1 - (float)cy1 ; wy1 = 1.0f - wy2 ; if (cy1 >= (signed)cheight) continue ; for (x = 1 ; x < (signed)width - 1; ++x) { float wx1 = (x + 0.5f) / (float)cellSize - 0.5f ; int cx1 = (int) vl_floor_f(wx1) ; int cx2 = cx1 + 1 ; float wx2 = wx1 - (float)cx1 ; wx1 = 1.0f - wx2 ; if (cx1 >= (signed)cwidth) continue ; { int unsigned bitString = 0 ; float center = at(x,y) ; if(at(x+1,y+0) > center) bitString |= 0x1 << 0; /* E */ if(at(x+1,y+1) > center) bitString |= 0x1 << 1; /* SE */ if(at(x+0,y+1) > center) bitString |= 0x1 << 2; /* S */ if(at(x-1,y+1) > center) bitString |= 0x1 << 3; /* SW */ if(at(x-1,y+0) > center) bitString |= 0x1 << 4; /* W */ if(at(x-1,y-1) > center) bitString |= 0x1 << 5; /* NW */ if(at(x+0,y-1) > center) bitString |= 0x1 << 6; /* N */ if(at(x+1,y-1) > center) bitString |= 0x1 << 7; /* NE */ bin = self->mapping[bitString] ; } if ((cx1 >= 0) & (cy1 >=0)) { to(cx1,cy1,bin) += wx1 * wy1; } if ((cx2 < (signed)cwidth) & (cy1 >=0)) { to(cx2,cy1,bin) += wx2 * wy1 ; } if ((cx1 >= 0) & (cy2 < (signed)cheight)) { to(cx1,cy2,bin) += wx1 * wy2 ; } if ((cx2 < (signed)cwidth) & (cy2 < (signed)cheight)) { to(cx2,cy2,bin) += wx2 * wy2 ; } } /* x */ } /* y */ /* normalize cells */ for (cy = 0 ; cy < (signed)cheight ; ++cy) { for (cx = 0 ; cx < (signed)cwidth ; ++ cx) { float norm = 0 ; for (k = 0 ; k < (signed)cdimension ; ++k) { norm += features[k * cstride] ; } norm = sqrtf(norm) + 1e-10f; ; for (k = 0 ; k < (signed)cdimension ; ++k) { features[k * cstride] = sqrtf(features[k * cstride]) / norm ; } features += 1 ; } } /* next cell to normalize */ } colmap-3.9.1/src/thirdparty/VLFeat/lbp.h000066400000000000000000000021731454702036400200370ustar00rootroot00000000000000/** @file lbp.h ** @brief Local Binary Patterns (LBP) descriptor (@ref lbp) ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_LBP_H #define VL_LBP_H #include "generic.h" /** @brief Type of quantization for the LBP descriptors ** @see @ref lbp-quantization **/ typedef enum _VlLbpMappingType { VlLbpUniform /**< Uniform local binary patterns. */ } VlLbpMappingType ; /** @brief Local Binary Pattern extractor */ typedef struct VlLbp_ { vl_size dimension ; vl_uint8 mapping [256] ; vl_bool transposed ; } VlLbp ; VL_EXPORT VlLbp * vl_lbp_new(VlLbpMappingType type, vl_bool transposed) ; VL_EXPORT void vl_lbp_delete(VlLbp * self) ; VL_EXPORT void vl_lbp_process(VlLbp * self, float * features, float * image, vl_size width, vl_size height, vl_size cellSize) ; VL_EXPORT vl_size vl_lbp_get_dimension(VlLbp * self) ; /* VL_LBP_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/liop.c000077500000000000000000000510301454702036400202170ustar00rootroot00000000000000/** @file liop.c ** @brief Local Intensity Order Pattern (LIOP) descriptor - Definition ** @author Hana Sarbortova ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 Hana Sarbortova and Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page liop Local Intensity Order Pattern (LIOP) descriptor @author Hana Sarbortova @author Andrea Vedaldi @ref liop.h implements *Local Intensity Order Pattern descriptor* (LIOP) of @cite{wang11local}. LIOP is a local image descriptor, similarly to the @ref sift "SIFT descriptor". @ref liop-starting demonstrates how to use the C API to compute the LIOP descriptor of a patch. For further details refer to: - @subpage liop-fundamentals - LIOP definition and parameters. @section liop-starting Getting started with LIOP The following code fragment demonstrates how tow to use @ref liop.h in a C program in order to compute the LIOP descriptor of an image patch. @code #include // Create a new object instance (these numbers corresponds to parameter // values proposed by authors of the paper, except for 41) vl_size sideLength = 41 ; VlLiopDesc * liop = vl_liopdesc_new_basic (sideLength); // allocate the descriptor array vl_size dimension = vl_liopdesc_get_dimension(liop) ; float * desc = vl_malloc(sizeof(float) * dimension) ; // compute descriptor from a patch (an array of length sideLegnth * // sideLength) vl_liopdesc_process(liop, desc, patch) ; // delete the object vl_liopdesc_delete(liop) ; @endcode The image patch must be of odd side length and in single precision. There are several parameters affecting the LIOP descriptor. An example is the @ref liop-weighing "threshold" used to discard low-contrast oder pattern in the computation of the statistics. This is changed by using ::vl_liopdesc_set_intensity_threshold. **/ /** @page liop-fundamentals LIOP fundamentals @tableofcontents The *Local Invariant Order Pattern* (LIOP) descriptor @cite{wang11local} is a local image descriptor based on the concept of *local order pattern*. An order pattern is simply the order obtained by sorting selected image samples by increasing intensity. Consider in particular a pixel $\bx$ and $n$ neighbors $\bx_1,\bx_2,\dots,\bx_n$. The local order pattern at $\bx$ is the permutation $\sigma$ that sorts the neighbours by increasing intensity $I(\bx_{\sigma(1)}) \leq I(\bx_{\sigma(2)}) \leq \dots \leq I(\bx_{\sigma(2)})$. An advantage of order patterns is that they are invariant to monotonic changes of the image intensity. However, an order pattern describes only a small portion of a patch and is not very distinctive. LIOP assembles local order patterns computed at all image locations to obtain a descriptor that at the same time distinctive and invariant to monotonic intensity changes as well as image rotations. In order to make order patterns rotation invariant, the neighborhood of samples around $\bx$ is taken in a rotation-covariant manner. In particular, the points $\bx_1,\dots,\bx_n$ are sampled anticlockwise on a circle of radius $r$ around $\bx$, as shown in the following figure: @image html liop.png "LIOP descriptor layout: square input patch (shaded area), circular measurement region (white area), local neighborhood of a point (blue)." Since the sample points do not necessarily have integer coordinates, $I(\bx_i)$ is computed using bilinear interpolation. @section liop-spatial-binning Intensity rank spatial binning Once local order patterns are computed for all pixels $\bx$ in the image, they can be pooled into a histogram to form an image descriptor. Pooling discards spatial information resulting in a warp-invariant statistics. In practice, there are two restriction on which pixels can be used for this purpose: - A margin of $r$ pixels from the image boundary must be maintained so that neighborhoods fall within the image boundaries. - Rotation invariance requires the pooling regions to be rotation co-variant. A way to do so is to make the shape of the pooling region rotation invariant. For this reason, the histogram pooling region is restricted to the circular region shown with a light color in the figure above. In order to increase distinctiveness of the descriptor, LIOP pools multiple histograms from a number of regions $R_1,\dots,R_m$ (spatial pooling). These regions are selected in an illumination-invariant and rotation-covariant manner by looking at level sets: \[ R_t = \{\bx :\tau_{t} \leq I(\bx) < \tau_{t+1} \}. \] In order to be invariant to monotonic changes of the intensity, the thresholds $\tau_t$ are selected so that all regions contain the same number of pixels. This can be done efficiently by sorting pixels by increasing intensity and then partitioning the resulting list into $m$ equal parts (when $m$ does not divide the number of pixels exactly, the remaining pixels are incorporated into the last partition). @section liop-weighing Weighted pooling In order to compute a histogram of order pattern occurrences, one needs to map permutations to histogram bins. This is obtained by sorting permutation in lexycogrpahical order. For example, for $n=4$ neighbors one has the following $n!=24$ permutations: Permutation | Lexycographical rank --------------|---------------------- 1 2 3 4 | 1 1 2 4 3 | 2 1 3 2 4 | 3 1 3 4 2 | 4 ... | ... 4 3 1 2 | 23 4 3 2 1 | 24 In the following, $q(\bx) \in [1, n!]$ will denote the index of the local order pattern $\sigma$ centered at pixel $\bx$. The local order patterns $q(\bx)$ in a region $R_t$ are then pooled to form a histogram of size $!n$. In this process, patterns are weighted based on their stability. The latter is assumed to be proportional to the number of pairs of pixels in the neighborhood that have a sufficiently large intensity difference: @f[ w(\bx) = \sum_{i=1}^n \sum_{j=1}^n [ |I(\bx_{i}) - I(\bx_{j})| > \Theta) ] @f] where $[\cdot]$ is the indicator function. In VLFeat LIOP implementation, the threshold $\Theta$ is either set as an absolute value, or as a faction of the difference between the maximum and minimum intensity in the image (restricted to the pixels in the light area in the figure above). Overall, LIOP consists of $m$ histograms of size $n!$ obtained as \[ h_{qt} = \sum_{\bx : q(\bx) = q \ \wedge\ \bx \in R_t} w(\bx). \] @section liop-normalization Normalization After computing the weighted counts $h_{qt}$, the LIOP descriptor is obtained by stacking the values $\{h_{qt}\}$ into a vector $\mathbf{h}$ and then normalising it: \[ \Phi = \frac{\mathbf{h}}{\|\mathbf{h}\|_2} \] The dimensionality is therefore $m n!$, where $m$ is the @c numSpatialBins number of spatial bins and $n$ is the @c numNeighbours number of neighbours (see ::vl_liopdesc_new). By default, this descriptor is stored in @c single format. It can be stored as a sequence of bytes by premultiplying the values by the constant 255 and then rounding: \[ \operatorname{round}\left[ 255\, \times \Phi\right]. \] */ #include "liop.h" #include "mathop.h" #include "imopv.h" #include #define DEFAULT_INTENSITY_THRESHOLD -(5.0/255) #define DEFAULT_RADIUS 6.0 #define DEFAULT_NUM_SPATIAL_BINS 6 #define DEFAULT_NUM_NEIGHBOURS 4 /* ---------------------------------------------------------------- */ /* Helper functions */ /* ---------------------------------------------------------------- */ static vl_int factorial(vl_int num) { vl_int result = 1; while(num > 1){ result = num*result; num--; } return result ; } /** @internal @brief Compute permutation index. ** @param permutation array containing all values from 0 to (size - 1) (input/output). ** @param size size of the permutation array. ** @return permutation index. ** ** Compute the position of @a permutation in the lexycographcial ** sorting of permutations of the given @a size. ** ** For example, in the lexicographical ordering, permutations of four elements ** are listed as [1 2 3 4], [1 2 4 3], [1 3 2 4], [1 3 4 2], [1 4 2 3], ** [1 4 3 2], [2 1 3 4], ..., [4 3 2 1]. ** ** The index can be computed as follows. First pick the first digit ** perm[1]. This is either 1,2,...,n. For each ** choice of the first digits, there are (n-1)! other permutations, separated ** therefore by (n-1)! elements in lexicographical order. ** ** Process then the second digit perm[2]. This can be though as finding ** the lexycotraphical index of perm[2], ..., perm[n], a permutation of ** n-1 elements. This can be explicitly obtained by taking out 1 from ** all elements perm[i] > perm[1]. */ VL_INLINE vl_index get_permutation_index(vl_uindex *permutation, vl_size size){ vl_index index = 0 ; vl_index i ; vl_index j ; for (i = 0 ; i < (signed)size ; ++i) { index = index * ((signed)size - i) + permutation[i] ; for (j = i + 1 ; j < (signed)size ; ++j) { if (permutation[j] > permutation[i]) { permutation[j] -- ; } } } return index ; } /* instantiate two quick sort algorithms */ VL_INLINE float patch_cmp (VlLiopDesc * liop, vl_index i, vl_index j) { vl_index ii = liop->patchPermutation[i] ; vl_index jj = liop->patchPermutation[j] ; return liop->patchIntensities[ii] - liop->patchIntensities[jj] ; } VL_INLINE void patch_swap (VlLiopDesc * liop, vl_index i, vl_index j) { vl_index tmp = liop->patchPermutation[i] ; liop->patchPermutation[i] = liop->patchPermutation[j] ; liop->patchPermutation[j] = tmp ; } #define VL_QSORT_prefix patch #define VL_QSORT_array VlLiopDesc* #define VL_QSORT_cmp patch_cmp #define VL_QSORT_swap patch_swap #include "qsort-def.h" VL_INLINE float neigh_cmp (VlLiopDesc * liop, vl_index i, vl_index j) { vl_index ii = liop->neighPermutation[i] ; vl_index jj = liop->neighPermutation[j] ; return liop->neighIntensities[ii] - liop->neighIntensities[jj] ; } VL_INLINE void neigh_swap (VlLiopDesc * liop, vl_index i, vl_index j) { vl_index tmp = liop->neighPermutation[i] ; liop->neighPermutation[i] = liop->neighPermutation[j] ; liop->neighPermutation[j] = tmp ; } #define VL_QSORT_prefix neigh #define VL_QSORT_array VlLiopDesc* #define VL_QSORT_cmp neigh_cmp #define VL_QSORT_swap neigh_swap #include "qsort-def.h" /* ---------------------------------------------------------------- */ /* Construct and destroy */ /* ---------------------------------------------------------------- */ /** @brief Create a new LIOP object instance. ** @param numNeighbours number of neighbours. ** @param numSpatialBins number of bins. ** @param radius radius of the cirucal sample neighbourhoods. ** @param sideLength width of the input image patch (the patch is square). ** @return new object instance. ** ** The value of @a radius should be at least less than half the @a ** sideLength of the patch. **/ VlLiopDesc * vl_liopdesc_new (vl_int numNeighbours, vl_int numSpatialBins, float radius, vl_size sideLength) { vl_index i, t ; VlLiopDesc * self = vl_calloc(sizeof(VlLiopDesc), 1); assert(radius <= sideLength/2) ; self->numNeighbours = numNeighbours ; self->numSpatialBins = numSpatialBins ; self->neighRadius = radius ; self->intensityThreshold = DEFAULT_INTENSITY_THRESHOLD ; self->dimension = factorial(numNeighbours) * numSpatialBins ; /* Precompute a list of pixels within a circular patch inside the square image. Leave a suitable marging for sampling around these pixels. */ self->patchSize = 0 ; self->patchPixels = vl_malloc(sizeof(vl_uindex)*sideLength*sideLength) ; self->patchSideLength = sideLength ; { vl_index x, y ; vl_index center = (sideLength - 1) / 2 ; double t = center - radius + 0.6 ; vl_index t2 = (vl_index) (t * t) ; for (y = 0 ; y < (signed)sideLength ; ++y) { for (x = 0 ; x < (signed)sideLength ; ++x) { vl_index dx = x - center ; vl_index dy = y - center ; if (x == 0 && y == 0) continue ; if (dx*dx + dy*dy <= t2) { self->patchPixels[self->patchSize++] = x + y * sideLength ; } } } } self->patchIntensities = vl_malloc(sizeof(vl_uindex)*self->patchSize) ; self->patchPermutation = vl_malloc(sizeof(vl_uindex)*self->patchSize) ; /* Precompute the samples in the circular neighbourhood of each measurement point. */ self->neighPermutation = vl_malloc(sizeof(vl_uindex) * self->numNeighbours) ; self->neighIntensities = vl_malloc(sizeof(float) * self->numNeighbours) ; self->neighSamplesX = vl_calloc(sizeof(double), self->numNeighbours * self->patchSize) ; self->neighSamplesY = vl_calloc(sizeof(double), self->numNeighbours * self->patchSize) ; for (i = 0 ; i < (signed)self->patchSize ; ++i) { vl_index pixel ; double x, y ; double dangle = 2*VL_PI / (double)self->numNeighbours ; double angle0 ; vl_index center = (sideLength - 1) / 2 ; pixel = self->patchPixels[i] ; x = (pixel % (signed)self->patchSideLength) - center ; y = (pixel / (signed)self->patchSideLength) - center ; angle0 = atan2(y,x) ; for (t = 0 ; t < (signed)self->numNeighbours ; ++t) { double x1 = x + radius * cos(angle0 + dangle * t) + center ; double y1 = y + radius * sin(angle0 + dangle * t) + center ; self->neighSamplesX[t + (signed)self->numNeighbours * i] = x1 ; self->neighSamplesY[t + (signed)self->numNeighbours * i] = y1 ; } } return self ; } /** @brief Create a new object with default parameters ** @param sideLength size of the patches to be processed. ** @return new object. ** ** @see ::vl_liopdesc_new. */ VlLiopDesc * vl_liopdesc_new_basic (vl_size sideLength) { return vl_liopdesc_new(DEFAULT_NUM_NEIGHBOURS, DEFAULT_NUM_SPATIAL_BINS, DEFAULT_RADIUS, sideLength) ; } /** @brief Delete object instance. ** @param self object instance. */ void vl_liopdesc_delete (VlLiopDesc * self) { vl_free (self->patchPixels) ; vl_free (self->patchIntensities) ; vl_free (self->patchPermutation) ; vl_free (self->neighPermutation) ; vl_free (self->neighIntensities) ; vl_free (self->neighSamplesX) ; vl_free (self->neighSamplesY) ; vl_free (self) ; } /* ---------------------------------------------------------------- */ /* Compute LIOP descriptor */ /* ---------------------------------------------------------------- */ /** @brief Compute liop descriptor for a patch ** @param self object instance ** @param desc descriptor to be computed (output). ** @param patch patch to process ** ** Use ::vl_liopdesc_get_dimension to get the size of the descriptor ** @a desc. */ void vl_liopdesc_process (VlLiopDesc * self, float * desc, float const * patch) { vl_index i,t ; vl_index offset,numPermutations ; vl_index spatialBinArea, spatialBinEnd, spatialBinIndex ; float threshold ; memset(desc, 0, sizeof(float) * self->dimension) ; /* * Sort pixels in the patch by increasing intensity. */ for (i = 0 ; i < (signed)self->patchSize ; ++i) { vl_index pixel = self->patchPixels[i] ; self->patchIntensities[i] = patch[pixel] ; self->patchPermutation[i] = i ; } patch_sort(self, self->patchSize) ; /* * Tune the threshold if needed. */ if (self->intensityThreshold < 0) { i = self->patchPermutation[0] ; t = self->patchPermutation[self->patchSize-1] ; threshold = - self->intensityThreshold * (self->patchIntensities[t] - self->patchIntensities[i]); } else { threshold = self->intensityThreshold ; } /* * Process pixels in order of increasing intenisity, dividing them into * spatial bins on the fly. */ numPermutations = factorial(self->numNeighbours) ; spatialBinArea = self->patchSize / self->numSpatialBins ; spatialBinEnd = spatialBinArea ; spatialBinIndex = 0 ; offset = 0 ; for (i = 0 ; i < (signed)self->patchSize ; ++i) { vl_index permIndex ; double *sx, *sy ; /* advance to the next spatial bin if needed */ if (i >= (signed)spatialBinEnd && spatialBinIndex < (signed)self->numSpatialBins - 1) { spatialBinEnd += spatialBinArea ; spatialBinIndex ++ ; offset += numPermutations ; } /* get intensities of neighbours of the current patch element and sort them */ sx = self->neighSamplesX + self->numNeighbours * self->patchPermutation[i] ; sy = self->neighSamplesY + self->numNeighbours * self->patchPermutation[i] ; for (t = 0 ; t < self->numNeighbours ; ++t) { double x = *sx++ ; double y = *sy++ ; /* bilinear interpolation */ vl_index ix = vl_floor_d(x) ; vl_index iy = vl_floor_d(y) ; double wx = x - ix ; double wy = y - iy ; double a = 0, b = 0, c = 0, d = 0 ; int L = (int) self->patchSideLength ; if (ix >= 0 && iy >= 0 ) { a = patch[ix + iy * L] ; } if (ix < L-1 && iy >= 0 ) { b = patch[ix+1 + iy * L] ; } if (ix >= 0 && iy < L-1) { c = patch[ix + (iy+1) * L] ; } if (ix < L-1 && iy < L-1) { d = patch[ix+1 + (iy+1) * L] ; } self->neighPermutation[t] = t; self->neighIntensities[t] = (1 - wy) * (a + (b - a) * wx) + wy * (c + (d - c) * wx) ; } neigh_sort (self, self->numNeighbours) ; /* get permutation index */ permIndex = get_permutation_index(self->neighPermutation, self->numNeighbours); /* * Compute weight according to difference in intensity values and * accumulate. */ { int k, t ; float weight = 0 ; for(k = 0; k < self->numNeighbours ; ++k) { for(t = k + 1; t < self->numNeighbours; ++t){ double a = self->neighIntensities[k] ; double b = self->neighIntensities[t] ; weight += (a > b + threshold || b > a + threshold) ; } } desc[permIndex + offset] += weight ; } } /* normalization */ { float norm = 0; for(i = 0; i < (signed)self->dimension; i++) { norm += desc[i]*desc[i]; } norm = VL_MAX(sqrt(norm), 1e-12) ; for(i = 0; i < (signed)self->dimension; i++){ desc[i] /= norm ; } } } /* ---------------------------------------------------------------- */ /* Getters and setters */ /* ---------------------------------------------------------------- */ /** @brief Get the dimension of a LIOP descriptor. ** @param self object. ** @return dimension. */ vl_size vl_liopdesc_get_dimension (VlLiopDesc const * self) { return self->dimension ; } /** @brief Get the number of neighbours. ** @param self object. ** @return number of neighbours. **/ vl_size vl_liopdesc_get_num_neighbours (VlLiopDesc const * self) { assert(self) ; return self->numNeighbours ; } /** @brief Get the intensity threshold ** @param self object. ** @return intensity threshold. ** @see liop-weighing **/ float vl_liopdesc_get_intensity_threshold (VlLiopDesc const * self) { assert(self) ; return self->intensityThreshold ; } /** @brief Set the intensity threshold ** @param self object. ** @param x intensity threshold. ** ** If non-negative, the threshold as is is used when comparing ** intensities. If negative, the absolute value of the specified ** number is multipled by the maximum intensity difference inside a ** patch to obtain the threshold. ** ** @see liop-weighing **/ void vl_liopdesc_set_intensity_threshold (VlLiopDesc * self, float x) { assert(self) ; self->intensityThreshold = x ; } /** @brief Get the neighbourhood radius. ** @param self object. ** @return neighbourhood radius. **/ double vl_liopdesc_get_neighbourhood_radius (VlLiopDesc const * self) { assert(self) ; return self->neighRadius ; } /** @brief Get the number of spatial bins. ** @param self object. ** @return number of spatial bins. **/ vl_size vl_liopdesc_get_num_spatial_bins (VlLiopDesc const * self) { assert(self) ; return self->numSpatialBins ; } colmap-3.9.1/src/thirdparty/VLFeat/liop.h000066400000000000000000000043501454702036400202240ustar00rootroot00000000000000/** @file liop.h ** @brief Local Intensity Order Pattern (LIOP) descriptor (@ref liop) ** @author Hana Sarbortova ** @author Andrea Vedaldi ** @see @ref liop **/ /* Copyright (C) 2013 Hana Sarbortova and Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_LIOP_H #define VL_LIOP_H #include "generic.h" /** @brief LIOP descriptor extractor object */ typedef struct _VlLiopDesc { vl_int numNeighbours; /**< Number of neighbours. */ vl_int numSpatialBins; /**< Number of bins. */ float intensityThreshold; /**< Weight threshold. */ vl_size dimension; /**< LIOP descriptor size. */ /* Pixels in the circular patch */ vl_size patchSideLength ; vl_size patchSize ; /* only circular neighbourhood */ vl_uindex * patchPixels ; float * patchIntensities ; vl_uindex * patchPermutation ; /* Neighbourhoods of each pixel (samples in a circle) */ float neighRadius; /**< Point to neighbour radius (distance). */ float * neighIntensities ; vl_uindex * neighPermutation ; double * neighSamplesX ; double * neighSamplesY ; } VlLiopDesc ; /** @name Construct and destroy ** @{ */ VL_EXPORT VlLiopDesc * vl_liopdesc_new (vl_int numNeighbours, vl_int numSpatialBins, float radius, vl_size sideLength) ; VL_EXPORT VlLiopDesc * vl_liopdesc_new_basic (vl_size sideLength) ; VL_EXPORT void vl_liopdesc_delete (VlLiopDesc * self) ; /** @} */ /** @name Get data and parameters ** @{ */ VL_EXPORT vl_size vl_liopdesc_get_dimension (VlLiopDesc const * self) ; VL_EXPORT vl_size vl_liopdesc_get_num_neighbours (VlLiopDesc const * self) ; VL_EXPORT float vl_liopdesc_get_intensity_threshold (VlLiopDesc const * self) ; VL_EXPORT vl_size vl_liopdesc_get_num_spatial_bins (VlLiopDesc const * self) ; VL_EXPORT double vl_liopdesc_get_neighbourhood_radius (VlLiopDesc const * self) ; VL_EXPORT void vl_liopdesc_set_intensity_threshold (VlLiopDesc * self, float x) ; /** @} */ /** @name Compute LIOP descriptor ** @{ */ VL_EXPORT void vl_liopdesc_process (VlLiopDesc * liop, float * desc, float const * patch) ; /** @} */ /* VL_LIOP_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/mathop.c000077500000000000000000000675041454702036400205610ustar00rootroot00000000000000/** @file mathop.c ** @brief Math operations - Definition ** @author Andrea Vedaldi, David Novotny **/ /* Copyright (C) 2014 Andrea Vedaldi. Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page mathop Mathematical operations @author Andrea Vedaldi @author Brian Fulkerson @tableofcontents VLFeat include several low-level routines to speedup common mathematical operations used throughout the library. Most are collected in the @ref mathop.h module. @section mathop-usage-vector-comparison Comparing vectors @ref mathop.h includes a number of functions to quickly compute distances or similarity of pairs of vector. Applications include clustering and evaluation of SVM-like classifiers. Use ::vl_get_vector_comparison_function_f or ::vl_get_vector_comparison_function_d obtain an approprite function to comprare vectors of floats or doubles, respectively. Such functions are usually optimized (for instance, on X86 platforms they use the SSE vector extension) and are several times faster than a naive implementation. ::vl_eval_vector_comparison_on_all_pairs_f and ::vl_eval_vector_comparison_on_all_pairs_d can be used to evaluate the comparison function on all pairs of one or two sequences of vectors. Let @f$ \mathbf{x} = (x_1,\dots,x_d) @f$ and @f$ \mathbf{y} = (y_1,\dots,y_d) @f$ be two vectors. The following comparison functions are supported:
@f$ l^1 @f$ ::VlDistanceL1 @f$ \sum_{i=1}^d |x_i - y_i| @f$ l1 distance (squared intersection metric)
@f$ l^2 @f$ ::VlDistanceL2 @f$\sum_{i=1}^d (x_i - y_i)^2@f$ Squared Euclidean disance
@f$ \chi^2 @f$ ::VlDistanceChi2 @f$\sum_{i=1}^d \frac{(x_i - y_i)^2}{x_i + y_i}@f$ Squared chi-square distance
- ::VlDistanceHellinger @f$\sum_{i=1}^d (\sqrt{x_i} - \sqrt{y_i})^2@f$ Squared Hellinger's distance
- ::VlDistanceJS @f$ \sum_{i=1}^d \left( x_i \log\frac{2x_i}{x_i+y_i} + y_i \log\frac{2y_i}{x_i+y_i} \right) @f$ Squared Jensen-Shannon distance
@f$ l^1 @f$ ::VlKernelL1 @f$ \sum_{i=1}^d \min\{ x_i, y_i \} @f$ intersection kernel
@f$ l^2 @f$ ::VlKernelL2 @f$\sum_{i=1}^d x_iy_i @f$ linear kernel
@f$ \chi^2 @f$ ::VlKernelChi2 @f$\sum_{i=1}^d 2 \frac{x_iy_i}{x_i + y_i}@f$ chi-square kernel
- ::VlKernelHellinger @f$\sum_{i=1}^d 2 \sqrt{x_i y_i}@f$ Hellinger's kernel (Bhattacharya coefficient)
- ::VlKernelJS @f$ \sum_{i=1}^d \left( \frac{x_i}{2} \log_2\frac{x_i+y_i}{x_i} + \frac{y_i}{2} \log_2\frac{x_i+y_i}{y_i} \right) @f$ Jensen-Shannon kernel
@remark The definitions have been choosen so that corresponding kernels and distances are related by the equation: @f[ d^2(\mathbf{x},\mathbf{y}) = k(\mathbf{x},\mathbf{x}) +k(\mathbf{y},\mathbf{y}) -k(\mathbf{x},\mathbf{y}) -k(\mathbf{y},\mathbf{x}) @f] This means that each of these distances can be interpreted as a squared distance or metric in the corresponding reproducing kernel Hilbert space. Notice in particular that the @f$ l^1 @f$ or Manhattan distance is also a squared distance in this sense. @section mathop-integer-ops Fast basic functions operations In certain algorithm it is useful to quickly compute integer approximation of certain mathematical operations. Presently, VLFeat includes and implementations of: - Fast single precision atan2: ::vl_fast_sqrt_f. - Fast inverse square root: ::vl_fast_resqrt_f, ::vl_fast_resqrt_d. - Fast square root: ::vl_fast_sqrt_f, ::vl_fast_sqrt_d. - Fast integer square root: ::vl_fast_sqrt_ui16, ::vl_fast_sqrt_ui32, ::vl_fast_sqrt_ui64 (see also @subpage mathop-sqrti). **/ /** @fn vl_get_vector_comparison_function_f(VlVectorComparisonType) ** ** @brief Get vector comparison function from comparison type ** @param type vector comparison type. ** @return comparison function. **/ /** @fn vl_get_vector_comparison_function_d(VlVectorComparisonType) ** @brief Get vector comparison function from comparison type ** @sa vl_get_vector_comparison_function_f **/ /** @fn vl_eval_vector_comparison_on_all_pairs_f(float*,vl_size, ** float const*,vl_size,float const*,vl_size,VlFloatVectorComparisonFunction) ** ** @brief Evaluate vector comparison function on all vector pairs ** @param result comparison matrix (output). ** @param dimension number of vector components (rows of @a X and @a Y). ** @param X data matrix X. ** @param Y data matrix Y. ** @param numDataX number of vectors in @a X (columns of @a X) ** @param numDataY number of vectros in @a Y (columns of @a Y) ** @param function vector comparison function. ** ** The function evaluates @a function on all pairs of columns ** from matrices @a X and @a Y, filling a @a numDataX by @a numDataY ** matrix. ** ** If @a Y is a null pointer the function compares all columns from ** @a X with themselves. **/ /** @fn vl_eval_vector_comparison_on_all_pairs_d(double*,vl_size, ** double const*,vl_size,double const*,vl_size,VlDoubleVectorComparisonFunction) ** @brief Evaluate vector comparison function on all vector pairs ** @sa vl_eval_vector_comparison_on_all_pairs_f **/ /** @page mathop-sqrti Fast integer square root algorithm @tableofcontents This section describes the fast integer square root algorithm used by vl_fast_sqrt_ui8, ::vl_fast_sqrt_ui16, ::vl_fast_sqrt_ui32, ::vl_fast_sqrt_ui64. Given a non-negative integer $x \in \mathbb{Z}_+$, the goal of this algorithm is to quickly compute the integer approximation of the square root of an integer number: \[ y = \max_{\bar y\in\mathbb{Z}} \bar y, \qquad \text{such that}\ \bar y^2 \leq x. \] Consider determining the k-th bit of $y$. To this end, decompose $y$ in three parts: \[ y = y_{k+1} + q 2^k + r, \qquad \text{where}\ y_{k+1} \geq 2^{k+1}, r < 2^k, \] and $q\in\{0,1\}$ is the bit to be determined. Here $y_{k+1}$ is a part of the result $y$ that has already been determined, while the bit $q$ and the remainder $r$ are still unknown. Recall that the goal is to find the largest $y^2$ such that $y^2 \leq x$. Expanding $y^2$ this condition becomes \[ q (2^{2k} + 2 y_{k+1} 2^k) + r(r + 2q 2^k + 2 y_{k+1}) \leq x - y_{k+1}^2. \] We can now determine if $q=1$ or $q=0$ based on the value of the residual $x - y_{k+1}^2$. Specifically, $q=1$ requires that: \[ \boxed{ 2^{2k} + 2a2^k \leq x - y_{k+1}^2. } \] On the other hand, if this equation is satisfied, then setting $r=0$ shows that there exists at least one $y$ such that $q=1$ and $y^2 \leq x$. In particular, greedily choosing $q=1$ in $x=y_{k+1} + 2^k q + r$ is optimal because $2^k > r$. This yields the algorithm: 1. Note that if $x$ is stored in $n$ bits and $n$ is even, then the integer square root $y$ does not require more than $m = n / 2$ bit to be stored. Thus the first bit to be determined is $k \leftarrow m - 1 = n/2 - 1$ and $y_{n/2}=0$. 2. The algorithm stores and updates $y_k/2^{k}$ and $x - y_{k}^2$ for convenience. 3. During iteration $k$, $y_k$ is determined. On entering the iteration, the first step is to compute $y_{k+1}/2^k = 2 y_{k+1}/2^{k+1}$. 4. Then the bound $t = (2^{2k} + 2 y_{k+1})2^k = 2^{2k}(1 + 2 y_{k+1}/2^k)$. 5. If $t \geq x - y_{k+1}$, the $k$-th bit of $y_k$ is set to one. This means applying the update $\hat y_{k}/2^k \leftarrow y_{k+1}/2^k + 1$. This also requires computing $x - y_{k}^2 \leftarrow x - y_{k+1}^2 - t$. 6. Decrement $k \leftarrow k -1$ and, if $k\geq 0$, continue from 3. **/ /* ---------------------------------------------------------------- */ #ifndef VL_MATHOP_INSTANTIATING #include "mathop.h" #include "mathop_sse2.h" #include "mathop_avx.h" #include #undef FLT #define FLT VL_TYPE_FLOAT #define VL_MATHOP_INSTANTIATING #include "mathop.c" #undef FLT #define FLT VL_TYPE_DOUBLE #define VL_MATHOP_INSTANTIATING #include "mathop.c" #endif /* ---------------------------------------------------------------- */ #ifdef VL_MATHOP_INSTANTIATING #include "float.h" #undef COMPARISONFUNCTION_TYPE #undef COMPARISONFUNCTION3_TYPE #if (FLT == VL_TYPE_FLOAT) # define COMPARISONFUNCTION_TYPE VlFloatVectorComparisonFunction # define COMPARISONFUNCTION3_TYPE VlFloatVector3ComparisonFunction #else # define COMPARISONFUNCTION_TYPE VlDoubleVectorComparisonFunction # define COMPARISONFUNCTION3_TYPE VlDoubleVector3ComparisonFunction #endif /* ---------------------------------------------------------------- */ VL_EXPORT T VL_XCAT(_vl_distance_l2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T d = *X++ - *Y++ ; acc += d * d ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_l1_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T d = *X++ - *Y++ ; acc += VL_MAX(d, -d) ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_chi2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T delta = a - b ; T denom = (a + b) ; T numer = delta * delta ; if (denom) { T ratio = numer / denom ; acc += ratio ; } } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_hellinger_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; #if (FLT == VL_TYPE_FLOAT) acc += a + b - 2.0 * sqrtf (a*b) ; #else acc += a + b - 2.0 * sqrt (a*b) ; #endif } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_js_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T x = *X++ ; T y = *Y++ ; if (x) acc += x - x * VL_XCAT(vl_log2_,SFX)(1 + y/x) ; if (y) acc += y - y * VL_XCAT(vl_log2_,SFX)(1 + x/y) ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_kernel_l2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; acc += a * b ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_kernel_l1_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T a_ = VL_XCAT(vl_abs_, SFX) (a) ; T b_ = VL_XCAT(vl_abs_, SFX) (b) ; acc += a_ + b_ - VL_XCAT(vl_abs_, SFX) (a - b) ; } return acc / ((T)2) ; } VL_EXPORT T VL_XCAT(_vl_kernel_chi2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T denom = (a + b) ; if (denom) { T numer = 2 * a * b ; T ratio = numer / denom ; acc += ratio ; } } return acc ; } VL_EXPORT T VL_XCAT(_vl_kernel_hellinger_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; #if (FLT == VL_TYPE_FLOAT) acc += sqrtf (a*b) ; #else acc += sqrt (a*b) ; #endif } return acc ; } VL_EXPORT T VL_XCAT(_vl_kernel_js_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T x = *X++ ; T y = *Y++ ; if (x) acc += x * VL_XCAT(vl_log2_,SFX)(1 + y/x) ; if (y) acc += y * VL_XCAT(vl_log2_,SFX)(1 + x/y) ; } return (T)0.5 * acc ; } VL_EXPORT T VL_XCAT(_vl_distance_mahalanobis_sq_, SFX) (vl_size dimension, T const * X, T const * MU, T const * S) { T const * X_end = X + dimension ; T acc = 0.0 ; while (X < X_end) { T d = *X++ - *MU++ ; acc += d * d * (*S++) ; } return acc ; } /* ---------------------------------------------------------------- */ VL_EXPORT COMPARISONFUNCTION_TYPE VL_XCAT(vl_get_vector_comparison_function_, SFX)(VlVectorComparisonType type) { COMPARISONFUNCTION_TYPE function = 0 ; switch (type) { case VlDistanceL2 : function = VL_XCAT(_vl_distance_l2_, SFX) ; break ; case VlDistanceL1 : function = VL_XCAT(_vl_distance_l1_, SFX) ; break ; case VlDistanceChi2 : function = VL_XCAT(_vl_distance_chi2_, SFX) ; break ; case VlDistanceHellinger : function = VL_XCAT(_vl_distance_hellinger_, SFX) ; break ; case VlDistanceJS : function = VL_XCAT(_vl_distance_js_, SFX) ; break ; case VlKernelL2 : function = VL_XCAT(_vl_kernel_l2_, SFX) ; break ; case VlKernelL1 : function = VL_XCAT(_vl_kernel_l1_, SFX) ; break ; case VlKernelChi2 : function = VL_XCAT(_vl_kernel_chi2_, SFX) ; break ; case VlKernelHellinger : function = VL_XCAT(_vl_kernel_hellinger_, SFX) ; break ; case VlKernelJS : function = VL_XCAT(_vl_kernel_js_, SFX) ; break ; default: abort() ; } #ifndef VL_DISABLE_SSE2 /* if a SSE2 implementation is available, use it */ if (vl_cpu_has_sse2() && vl_get_simd_enabled()) { switch (type) { case VlDistanceL2 : function = VL_XCAT(_vl_distance_l2_sse2_, SFX) ; break ; case VlDistanceL1 : function = VL_XCAT(_vl_distance_l1_sse2_, SFX) ; break ; case VlDistanceChi2 : function = VL_XCAT(_vl_distance_chi2_sse2_, SFX) ; break ; case VlKernelL2 : function = VL_XCAT(_vl_kernel_l2_sse2_, SFX) ; break ; case VlKernelL1 : function = VL_XCAT(_vl_kernel_l1_sse2_, SFX) ; break ; case VlKernelChi2 : function = VL_XCAT(_vl_kernel_chi2_sse2_, SFX) ; break ; default: break ; } } #endif #ifndef VL_DISABLE_AVX /* if an AVX implementation is available, use it */ if (vl_cpu_has_avx() && vl_get_simd_enabled()) { switch (type) { case VlDistanceL2 : function = VL_XCAT(_vl_distance_l2_avx_, SFX) ; break ; default: break ; } } #endif return function ; } /* ---------------------------------------------------------------- */ VL_EXPORT COMPARISONFUNCTION3_TYPE VL_XCAT(vl_get_vector_3_comparison_function_, SFX)(VlVectorComparisonType type) { COMPARISONFUNCTION3_TYPE function = 0 ; switch (type) { case VlDistanceMahalanobis : function = VL_XCAT(_vl_distance_mahalanobis_sq_, SFX) ; break ; default: abort() ; } #ifndef VL_DISABLE_SSE2 /* if a SSE2 implementation is available, use it */ if (vl_cpu_has_sse2() && vl_get_simd_enabled()) { switch (type) { case VlDistanceMahalanobis : function = VL_XCAT(_vl_distance_mahalanobis_sq_sse2_, SFX) ; break ; default: break ; } } #endif #ifndef VL_DISABLE_AVX /* if an AVX implementation is available, use it */ if (vl_cpu_has_avx() && vl_get_simd_enabled()) { switch (type) { case VlDistanceMahalanobis : function = VL_XCAT(_vl_distance_mahalanobis_sq_avx_, SFX) ; break ; default: break ; } } #endif return function ; } /* ---------------------------------------------------------------- */ VL_EXPORT void VL_XCAT(vl_eval_vector_comparison_on_all_pairs_, SFX) (T * result, vl_size dimension, T const * X, vl_size numDataX, T const * Y, vl_size numDataY, COMPARISONFUNCTION_TYPE function) { vl_uindex xi ; vl_uindex yi ; if (dimension == 0) return ; if (numDataX == 0) return ; assert (X) ; if (Y) { if (numDataY == 0) return ; for (yi = 0 ; yi < numDataY ; ++ yi) { for (xi = 0 ; xi < numDataX ; ++ xi) { *result++ = (*function)(dimension, X, Y) ; X += dimension ; } X -= dimension * numDataX ; Y += dimension ; } } else { T * resultTransp = result ; Y = X ; for (yi = 0 ; yi < numDataX ; ++ yi) { for (xi = 0 ; xi <= yi ; ++ xi) { T z = (*function)(dimension, X, Y) ; X += dimension ; *result = z ; *resultTransp = z ; result += 1 ; resultTransp += numDataX ; } X -= dimension * (yi + 1) ; Y += dimension ; result += numDataX - (yi + 1) ; resultTransp += 1 - (yi + 1) * numDataX ; } } } /* VL_MATHOP_INSTANTIATING */ #endif /* ---------------------------------------------------------------- */ /* Numerical analysis */ /* ---------------------------------------------------------------- */ #ifndef VL_MATHOP_INSTANTIATING /** @brief SVD of a 2x2 real matrix ** @param S 2x2 real diagonal matrix of the singular values (out). ** @param U first 2x2 real orthonormal matrix (out). ** @param V second 2x2 real orthonormal matrix (out). ** @param M 2x2 matrix. ** ** The function comptues the SVD decomposition of the 2x2 ** real matrix @f$ M @f$: ** @f[ ** M = U \operatorname S V^\top ** @f] ** where @f$ U @f$ and @f$ V @f$ are real orthonormal matrices ** and @f$ S @f$ is the diagonal matrix of the singular values ** in decreasing order. ** ** @par Algorithm ** ** The fist step is to find rotation matrices @f$ U_1 @f$ and ** @f$ V_1 @f$ such taht ** @f[ ** M = U_1 R V_1^\top, \quad ** U_1 = \begin{barray} c_{u1} & - s_{u1} \\ s_{u1} & c_{u1} \end{barray}, \quad ** V_1 = \begin{barray} c_{v1} & - s_{v1} \\ s_{v1} & c_{v1} \end{barray}, \quad ** R = \begin{barray} f & g \\ 0 & h \end{barray}. ** @f] ** Gives a 2x2 triangular matrix. The second step is to call ** ::vl_lapack_dlasv2 on the matrix @f$ R @f$ obtaining ** @f[ ** M = U_1 (U_2 S V_2^\top) V_2. ** @f] **/ void vl_svd2 (double* S, double *U, double *V, double const *M) { double m11 = M[0] ; double m21 = M[1] ; double m12 = M[2] ; double m22 = M[3] ; double cu1 = m11 ; double su1 = m21 ; double norm = sqrt(cu1*cu1 + su1*su1) ; double cu2, su2, cv2, sv2 ; double f, g, h ; double smin, smax ; cu1 /= norm ; su1 /= norm ; f = cu1 * m11 + su1 * m21 ; g = cu1 * m12 + su1 * m22 ; h = - su1 * m12 + cu1 * m22 ; vl_lapack_dlasv2 (&smin, &smax, &sv2, &cv2, &su2, &cu2, f, g, h) ; assert(S) ; S[0] = smax ; S[1] = 0 ; S[2] = 0 ; S[3] = smin ; if (U) { U[0] = cu2*cu1 - su2*su1 ; U[1] = su2*cu1 + cu2*su1 ; U[2] = - cu2*su1 - su2*cu1 ; U[3] = - su2*su1 + cu2*cu1 ; } if (V) { V[0] = cv2 ; V[1] = sv2 ; V[2] = - sv2 ; V[3] = cv2 ; } } /** @brief SVD of a 2x2 upper triangular matrix (LAPACK @c dlasv2 equivalent) ** @param smin smallest (in modulus) singular value (out). ** @param smax largest (in modulus) singuarl value (out). ** @param sv second component of the right singular vector of @c smax (out). ** @param cv first component of the right singular vector of @c smax (out). ** @param su second component of the left singular vector of @c smax (out). ** @param cu first component of the left singular vector of @c smax (out). ** @param f first entry of the upper triangular matrix. ** @param g second entry of the upper triangular matrix. ** @param h third entry of the upper triangular matrix. ** ** @f[ ** \begin{bmatrix} f & g \\ 0 & h \end{bmatrix} ** = ** \begin{bmatrix} cv & - sv \\ sv & cv \end{bmatrix} ** \begon{bmatrix} smax & 0 \\ 0 & smin \end{bmatrix} ** \begin{bmatrix} cv & - sv \\ sv & cv \end{bmatrix} ** @f] ** ** Z.Bai and J.Demmel, ** "Computing the Generalized Singular Value Decomposition", ** SIAM J. Sci. Comput., Vol. 14, No. 6, pp. 1464-1486, November 1993 **/ #define isign(i) ((i)<0 ? (-1) : (+1)) /* integer sign function */ #define sign(x) ((x)<0.0 ? (-1) : (+1)) /* double sign function */ void vl_lapack_dlasv2 (double *smin, double *smax, double *sv, double *cv, double *su, double *cu, double f, double g, double h) { double svt, cvt, sut, cut; /* temporary sv, cv, su, and cu */ double ft = f, gt = g, ht = h; /* temporary f, g, h */ double fa = fabs(f), ga = fabs(g), ha = fabs(h); /* |f|, |g|, and |h| */ int pmax = 1 ; /* pointer to max abs entry */ int swap = 0 ; /* is swapped */ int glarge = 0 ; /* is g very large */ int tsign ; /* tmp sign */ double fmh ; /* |f| -|h| */ double d ; /* (|f| -|h|)/|f| */ double dd ; /* d*d */ double q ; /* g/f */ double qq ; /* q*q */ double s ; /* (|f| + |h|)/|f| */ double ss ; /* s*s */ double spq ; /* sqrt(ss + qq) */ double dpq ; /* sqrt(dd + qq) */ double a ; /* (spq + dpq)/2 */ double tmp ; /* temporaries */ double tt; /* make fa >= ha */ if (fa < ha) { pmax = 3 ; tmp =ft ; ft = ht ; ht = tmp ; /* swap ft and ht */ tmp =fa ; fa = ha ; ha = tmp ; /* swap fa and ha */ swap = 1 ; } if (ga == 0.0) { /* diagonal */ *smin = ha ; *smax = fa ; /* identity matrix */ cut = 1.0 ; sut = 0.0 ; cvt = 1.0 ; svt = 0.0 ; } else { /* not diagonal */ if (ga > fa) { /* g is the largest entry */ pmax = 2 ; if ((fa / ga) < VL_EPSILON_D) { /* g is very large */ glarge = 1 ; *smax = ga ; /* 1 ulp */ if (ha > 1.0) { *smin = fa / (ga / ha) ; /* 2 ulps */ } else { *smin = (fa / ga) * ha ; /* 2 ulps */ } cut = 1.0 ; sut = ht / gt ; cvt = 1.0 ; svt = ft / gt ; } } if (glarge == 0) { /* normal case */ fmh = fa - ha ; /* 1ulp */ if (fmh == fa) { /* cope with infinite f or h */ d = 1.0 ; } else { d = fmh / fa ; /* note 0<=d<=1.0, 2 ulps */ } q = gt / ft ; /* note |q|<1/EPS, 1 ulp */ s = 2.0 - d ; /* note s>=1.0, 3 ulps */ dd = d*d ; qq = q*q ; ss = s*s ; spq = sqrt(ss + qq) ; /* note 1<=spq<=1+1/EPS, 5 ulps */ if (d == 0.0) { dpq = fabs(q) ; /* 0 ulp */ } else { dpq = sqrt(dd + qq) ; /* note 0<=dpq<=1+1/EPS, 3.5 ulps */ } a = 0.5 * (spq + dpq) ; /* note 1<=a<=1 + |q|, 6 ulps */ *smin = ha / a; /* 7 ulps */ *smax = fa * a; /* 7 ulps */ if (qq==0.0) { /* qq underflow */ if (d==0.0) { tmp = sign(ft)*2*sign(gt); /* 0ulp */ } else { tmp = gt/(sign(ft)*fmh) + q/s; /* 6 ulps */ } } else { tmp = (q/(spq + s) + q/(dpq + d))*(1.0 + a); /* 17 ulps */ } /* if qq */ tt = sqrt(tmp*tmp + 4.0) ; /* 18.5 ulps */ cvt = 2.0 / tt ; /* 19.5 ulps */ svt = tmp / tt ; /* 36.5 ulps */ cut = (cvt + svt*q) / a ; /* 46.5 ulps */ sut = (ht / ft) * svt / a ; /* 45.5 ulps */ } /* if g not large */ } /* if ga */ if (swap == 1) { *cu = svt ; *su = cvt ; *cv = sut ; *sv = cut ; } else { *cu = cut ; *su = sut ; *cv = cvt ; *sv = svt ; } /* correct the signs of smax and smin */ if (pmax==1) { tsign = sign(*cv) * sign(*cu) * sign(f) ; } if (pmax==2) { tsign = sign(*sv) * sign(*cu) * sign(g) ; } if (pmax==3) { tsign = sign(*sv) * sign(*su) * sign(h) ; } *smax = isign(tsign) * (*smax); *smin = isign(tsign * sign(f) * sign(h)) * (*smin) ; } /** @brief Solve a 3x3 linear system ** @param x result. ** @param A system matrix. ** @param b coefficients. ** ** The function computes a solution to @f$ Ax =b @f$ for a 3x3 ** matrix. **/ VL_EXPORT int vl_solve_linear_system_3 (double * x, double const * A, double const *b) { int err ; double M[3*4] ; M[0] = A[0] ; M[1] = A[1] ; M[2] = A[2] ; M[3] = A[3] ; M[4] = A[4] ; M[5] = A[5] ; M[6] = A[6] ; M[7] = A[7] ; M[8] = A[8] ; M[9] = b[0] ; M[10] = b[1] ; M[11] = b[2] ; err = vl_gaussian_elimination(M,3,4) ; x[0] = M[9] ; x[1] = M[10] ; x[2] = M[11] ; return err ; } /** @brief Solve a 2x2 linear system ** @param x result. ** @param A system matrix. ** @param b coefficients. ** ** The function computes a solution to @f$ Ax =b @f$ for a 2x2 ** matrix. **/ VL_EXPORT int vl_solve_linear_system_2 (double * x, double const * A, double const *b) { int err ; double M[2*3] ; M[0] = A[0] ; M[1] = A[1] ; M[2] = A[2] ; M[3] = A[3] ; M[4] = b[0]; M[5] = b[1] ; err = vl_gaussian_elimination(M,2,3) ; x[0] = M[4] ; x[1] = M[5] ; return err ; } /** @brief Gaussian elimination ** @param M matrix. ** @param numRows number of rows of @c M. ** @param numColumns number of columns of @c M. ** ** The function runs Gaussian elimination with pivoting ** on the matrix @a M in place. ** @c numRows must be not larger than @c numColumns. ** ** Let @f$ M = [A, b] @f$ to obtain the solution to the linear ** system @f$ Ax=b @f$ (as the last column of @c M after ** elimination). ** ** Let @f$ M = [A, I] @f$ to compute the inverse of @c A in ** a similar manner. **/ VL_EXPORT vl_bool vl_gaussian_elimination (double * A, vl_size numRows, vl_size numColumns) { vl_index i, j, ii, jj ; assert(A) ; assert(numRows <= numColumns) ; #define Aat(i,j) A[(i) + (j)*numRows] /* Gauss elimination */ for(j = 0 ; j < (signed)numRows ; ++j) { double maxa = 0 ; double maxabsa = 0 ; vl_index maxi = -1 ; double tmp ; #if 0 { vl_index iii, jjj ; for (iii = 0 ; iii < 2 ; ++iii) { for (jjj = 0 ; jjj < 3 ; ++jjj) { VL_PRINTF("%5.2g ", Aat(iii,jjj)) ; } VL_PRINTF("\n") ; } VL_PRINTF("\n") ; } #endif /* look for the maximally stable pivot */ for (i = j ; i < (signed)numRows ; ++i) { double a = Aat(i,j) ; double absa = vl_abs_d (a) ; if (absa > maxabsa) { maxa = a ; maxabsa = absa ; maxi = i ; } } i = maxi ; /* if singular give up */ if (maxabsa < 1e-10) return VL_ERR_OVERFLOW ; /* swap j-th row with i-th row and normalize j-th row */ for(jj = j ; jj < (signed)numColumns ; ++jj) { tmp = Aat(i,jj) ; Aat(i,jj) = Aat(j,jj) ; Aat(j,jj) = tmp ; Aat(j,jj) /= maxa ; } #if 0 { vl_index iii, jjj ; VL_PRINTF("after swap %d %d\n", j, i); for (iii = 0 ; iii < 2 ; ++iii) { for (jjj = 0 ; jjj < 3 ; ++jjj) { VL_PRINTF("%5.2g ", Aat(iii,jjj)) ; } VL_PRINTF("\n") ; } VL_PRINTF("\n") ; } #endif /* elimination */ for (ii = j+1 ; ii < (signed)numRows ; ++ii) { double x = Aat(ii,j) ; for (jj = j ; jj < (signed)numColumns ; ++jj) { Aat(ii,jj) -= x * Aat(j,jj) ; } } #if 0 { VL_PRINTF("after elimination\n"); vl_index iii, jjj ; for (iii = 0 ; iii < 2 ; ++iii) { for (jjj = 0 ; jjj < 3 ; ++jjj) { VL_PRINTF("%5.2g ", Aat(iii,jjj)) ; } VL_PRINTF("\n") ; } VL_PRINTF("\n") ; } #endif } /* backward substitution */ for (i = numRows - 1 ; i > 0 ; --i) { /* substitute in all rows above */ for (ii = i - 1 ; ii >= 0 ; --ii) { double x = Aat(ii,i) ; /* j = numRows */ for (j = numRows ; j < (signed)numColumns ; ++j) { Aat(ii,j) -= x * Aat(i,j) ; } } } #if 0 { VL_PRINTF("after substitution\n"); vl_index iii, jjj ; for (iii = 0 ; iii < 2 ; ++iii) { for (jjj = 0 ; jjj < 3 ; ++jjj) { VL_PRINTF("%5.2g ", Aat(iii,jjj)) ; } VL_PRINTF("\n") ; } VL_PRINTF("\n") ; } #endif return VL_ERR_OK ; } /* VL_MATHOP_INSTANTIATING */ #endif #undef VL_MATHOP_INSTANTIATING colmap-3.9.1/src/thirdparty/VLFeat/mathop.h000066400000000000000000000437031454702036400205560ustar00rootroot00000000000000/** @file mathop.h ** @brief Math operations (@ref mathop) ** @author Andrea Vedaldi, David Novotny **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_MATHOP_H #define VL_MATHOP_H #include "generic.h" #include #include /** @brief Euler constant*/ #define VL_E 2.718281828459045 /** @brief Logarithm of 2 (math constant)*/ #define VL_LOG_OF_2 0.693147180559945 /** @brief Pi (math constant) */ #define VL_PI 3.141592653589793 /** @brief IEEE single precision epsilon (math constant) ** ** 1.0F + VL_EPSILON_F is the smallest representable ** single precision number greater than @c 1.0F. Numerically, ** ::VL_EPSILON_F is equal to @f$ 2^{-23} @f$. ** **/ #define VL_EPSILON_F 1.19209290E-07F /** @brief IEEE double precision epsilon (math constant) ** ** 1.0 + VL_EPSILON_D is the smallest representable ** double precision number greater than @c 1.0. Numerically, ** ::VL_EPSILON_D is equal to @f$ 2^{-52} @f$. **/ #define VL_EPSILON_D 2.220446049250313e-16 /* For the code below: An ANSI C compiler takes the two expressions, LONG_VAR and CHAR_VAR, and implicitly casts them to the type of the first member of the union. Refer to K&R Second Edition Page 148, last paragraph. */ /** @internal @brief IEEE single precision quiet NaN constant */ static union { vl_uint32 raw ; float value ; } const vl_nan_f = { 0x7FC00000UL } ; /** @internal @brief IEEE single precision infinity constant */ static union { vl_uint32 raw ; float value ; } const vl_infinity_f = { 0x7F800000UL } ; /** @internal @brief IEEE double precision quiet NaN constant */ static union { vl_uint64 raw ; double value ; } const vl_nan_d = #ifdef VL_COMPILER_MSC { 0x7FF8000000000000ui64 } ; #else { 0x7FF8000000000000ULL } ; #endif /** @internal @brief IEEE double precision infinity constant */ static union { vl_uint64 raw ; double value ; } const vl_infinity_d = #ifdef VL_COMPILER_MSC { 0x7FF0000000000000ui64 } ; #else { 0x7FF0000000000000ULL } ; #endif /** @brief IEEE single precision NaN (not signaling) */ #define VL_NAN_F (vl_nan_f.value) /** @brief IEEE single precision positive infinity (not signaling) */ #define VL_INFINITY_F (vl_infinity_f.value) /** @brief IEEE double precision NaN (not signaling) */ #define VL_NAN_D (vl_nan_d.value) /** @brief IEEE double precision positive infinity (not signaling) */ #define VL_INFINITY_D (vl_infinity_d.value) /* ---------------------------------------------------------------- */ /** @brief Fast mod(x, 2 * VL_PI) ** ** @param x input value. ** @return mod(x, 2 * VL_PI) ** ** The function is optimized for small absolute values of @a x. ** ** The result is guaranteed to be not smaller than 0. However, due to ** finite numerical precision and rounding errors, the result can be ** equal to 2 * VL_PI (for instance, if @c x is a very small negative ** number). **/ VL_INLINE float vl_mod_2pi_f (float x) { while (x > (float)(2 * VL_PI)) x -= (float) (2 * VL_PI) ; while (x < 0.0F) x += (float) (2 * VL_PI); return x ; } /** @brief Fast mod(x, 2 * VL_PI) ** @see vl_mod_2pi_f **/ VL_INLINE double vl_mod_2pi_d (double x) { while (x > 2.0 * VL_PI) x -= 2 * VL_PI ; while (x < 0.0) x += 2 * VL_PI ; return x ; } /** @brief Floor and convert to integer ** @param x argument. ** @return Similar to @c (int) floor(x) **/ VL_INLINE long int vl_floor_f (float x) { long int xi = (long int) x ; if (x >= 0 || (float) xi == x) return xi ; else return xi - 1 ; } /** @brief Floor and convert to integer ** @see vl_floor_f **/ VL_INLINE long int vl_floor_d (double x) { long int xi = (long int) x ; if (x >= 0 || (double) xi == x) return xi ; else return xi - 1 ; } /** @brief Ceil and convert to integer ** @param x argument. ** @return @c lceilf(x) **/ VL_INLINE long int vl_ceil_f (float x) { #ifdef VL_COMPILER_GNUC return (long int) __builtin_ceilf(x) ; #else return (long int) ceilf(x) ; #endif } /** @brief Ceil and convert to integer ** @see vl_ceil_f **/ VL_INLINE long int vl_ceil_d (double x) { #ifdef VL_COMPILER_GNUC return __builtin_ceil(x) ; #else return (long int) ceil(x) ; #endif } /** @brief Round ** @param x argument. ** @return @c lroundf(x) ** This function is either the same or similar to C99 @c lroundf(). **/ VL_INLINE long int vl_round_f (float x) { #ifdef VL_COMPILER_GNUC return __builtin_lroundf(x) ; #elif VL_COMPILER_MSC if (x >= 0.0F) { return vl_floor_f(x + 0.5F) ; } else { return vl_ceil_f(x - 0.5F) ; } #else return lroundf(x) ; #endif } /** @brief Round ** @param x argument. ** @return @c lround(x) ** This function is either the same or similar to C99 @c lround(). **/ VL_INLINE long int vl_round_d (double x) { #ifdef VL_COMPILER_GNUC return __builtin_lround(x) ; #elif VL_COMPILER_MSC if (x >= 0.0) { return vl_floor_d(x + 0.5) ; } else { return vl_ceil_d(x - 0.5) ; } #else return lround(x) ; #endif } /** @brief Fast @c abs(x) ** @param x argument. ** @return @c abs(x) **/ VL_INLINE float vl_abs_f (float x) { #ifdef VL_COMPILER_GNUC return __builtin_fabsf (x) ; #else return fabsf(x) ; #endif } /** @brief Fast @c abs(x) ** @sa vl_abs_f **/ VL_INLINE double vl_abs_d (double x) { #ifdef VL_COMPILER_GNUC return __builtin_fabs (x) ; #else return fabs(x) ; #endif } /** @brief Base-2 logaritghm ** @param x argument. ** @return @c log(x). **/ VL_INLINE double vl_log2_d (double x) { #ifdef VL_COMPILER_GNUC return __builtin_log2(x) ; #elif VL_COMPILER_MSC return log(x) / 0.693147180559945 ; #else return log2(x) ; #endif } /** @copydoc vl_log2_d */ VL_INLINE float vl_log2_f (float x) { #ifdef VL_COMPILER_GNUC return __builtin_log2f (x) ; #elif VL_COMPILER_MSC return logf(x) / 0.6931472F ; #else return log2(x) ; #endif } /** @brief Square root. ** @param x argument. ** @return @c sqrt(x). **/ VL_INLINE double vl_sqrt_d (double x) { #ifdef VL_COMPILER_GNUC return __builtin_sqrt(x) ; #else return sqrt(x) ; #endif } /** @copydoc vl_sqrt_d */ VL_INLINE float vl_sqrt_f (float x) { #ifdef VL_COMPILER_GNUC return __builtin_sqrtf(x) ; #else return sqrtf(x) ; #endif } /** @brief Check whether a floating point value is NaN ** @param x argument. ** @return true if @a x is NaN. **/ VL_INLINE vl_bool vl_is_nan_f (float x) { #ifdef VL_COMPILER_GNUC return __builtin_isnan (x) ; #elif VL_COMPILER_MSC return _isnan(x) ; #else return isnan(x) ; #endif } /** @copydoc vl_is_nan_f */ VL_INLINE vl_bool vl_is_nan_d (double x) { #ifdef VL_COMPILER_GNUC return __builtin_isnan (x) ; #elif VL_COMPILER_MSC return _isnan(x) ; #else return isnan(x) ; #endif } /** @brief Check whether a floating point value is infinity ** @param x argument. ** @return true if @a x is infinity. **/ VL_INLINE vl_bool vl_is_inf_f (float x) { #ifdef VL_COMPILER_GNUC return __builtin_isinf (x) ; #elif VL_COMPILER_MSC return ! _finite(x) ; #else return isinf(x) ; #endif } /** @copydoc vl_is_inf_f */ VL_INLINE vl_bool vl_is_inf_d (double x) { #ifdef VL_COMPILER_GNUC return __builtin_isinf (x) ; #elif VL_COMPILER_MSC return ! _finite(x) ; #else return isinf(x) ; #endif } /** ------------------------------------------------------------------ ** @brief Fast @c atan2 approximation ** @param y argument. ** @param x argument. ** ** The function computes a relatively rough but fast approximation of ** @c atan2(y,x). ** ** @par Algorithm ** ** The algorithm approximates the function @f$ f(r)=atan((1-r)/(1+r)) ** @f$, @f$ r \in [-1,1] @f$ with a third order polynomial @f$ ** f(r)=c_0 + c_1 r + c_2 r^2 + c_3 r^3 @f$. To fit the polynomial ** we impose the constraints ** ** @f{eqnarray*} ** f(+1) &=& c_0 + c_1 + c_2 + c_3 = atan(0) = 0,\\ ** f(-1) &=& c_0 - c_1 + c_2 - c_3 = atan(\infty) = \pi/2,\\ ** f(0) &=& c_0 = atan(1) = \pi/4. ** @f} ** ** The last degree of freedom is fixed by minimizing the @f$ ** l^{\infty} @f$ error, which yields ** ** @f[ ** c_0=\pi/4, \quad ** c_1=-0.9675, \quad ** c_2=0, \quad ** c_3=0.1821, ** @f] ** ** with maximum error of 0.0061 radians at 0.35 degrees. ** ** @return Approximation of @c atan2(y,x). **/ VL_INLINE float vl_fast_atan2_f (float y, float x) { float angle, r ; float const c3 = 0.1821F ; float const c1 = 0.9675F ; float abs_y = vl_abs_f (y) + VL_EPSILON_F ; if (x >= 0) { r = (x - abs_y) / (x + abs_y) ; angle = (float) (VL_PI / 4) ; } else { r = (x + abs_y) / (abs_y - x) ; angle = (float) (3 * VL_PI / 4) ; } angle += (c3*r*r - c1) * r ; return (y < 0) ? - angle : angle ; } /** @brief Fast @c atan2 approximation ** @sa vl_fast_atan2_f **/ VL_INLINE double vl_fast_atan2_d (double y, double x) { double angle, r ; double const c3 = 0.1821 ; double const c1 = 0.9675 ; double abs_y = vl_abs_d (y) + VL_EPSILON_D ; if (x >= 0) { r = (x - abs_y) / (x + abs_y) ; angle = VL_PI / 4 ; } else { r = (x + abs_y) / (abs_y - x) ; angle = 3 * VL_PI / 4 ; } angle += (c3*r*r - c1) * r ; return (y < 0) ? - angle : angle ; } /** ------------------------------------------------------------------ ** @brief Fast @c resqrt approximation ** @param x argument. ** @return approximation of @c resqrt(x). ** ** The function quickly computes an approximation of @f$ x^{-1/2} ** @f$. ** ** @par Algorithm ** ** The goal is to compute @f$ y = x^{-1/2} @f$, which we do by ** finding the solution of @f$ 0 = f(y) = y^{-2} - x @f$ by two Newton ** steps. Each Newton iteration is given by ** ** @f[ ** y \leftarrow ** y - \frac{f(y)}{\frac{df(y)}{dy}} = ** y + \frac{1}{2} (y-xy^3) = ** \frac{y}{2} \left( 3 - xy^2 \right) ** @f] ** ** which yields a simple polynomial update rule. ** ** The clever bit (attributed to either J. Carmack or G. Tarolli) is ** the way an initial guess @f$ y \approx x^{-1/2} @f$ is chosen. ** ** @see Inverse Sqare Root. ** **/ VL_INLINE float vl_fast_resqrt_f (float x) { /* 32-bit version */ union { float x ; vl_int32 i ; } u ; float xhalf = (float) 0.5 * x ; /* convert floating point value in RAW integer */ u.x = x ; /* gives initial guess y0 */ u.i = 0x5f3759df - (u.i >> 1); /*u.i = 0xdf59375f - (u.i>>1);*/ /* two Newton steps */ u.x = u.x * ( (float) 1.5 - xhalf*u.x*u.x) ; u.x = u.x * ( (float) 1.5 - xhalf*u.x*u.x) ; return u.x ; } /** @brief Fast @c resqrt approximation ** @sa vl_fast_resqrt_d **/ VL_INLINE double vl_fast_resqrt_d (double x) { /* 64-bit version */ union { double x ; vl_int64 i ; } u ; double xhalf = (double) 0.5 * x ; /* convert floating point value in RAW integer */ u.x = x ; /* gives initial guess y0 */ #ifdef VL_COMPILER_MSC u.i = 0x5fe6ec85e7de30dai64 - (u.i >> 1) ; #else u.i = 0x5fe6ec85e7de30daLL - (u.i >> 1) ; #endif /* two Newton steps */ u.x = u.x * ( (double) 1.5 - xhalf*u.x*u.x) ; u.x = u.x * ( (double) 1.5 - xhalf*u.x*u.x) ; return u.x ; } /** ------------------------------------------------------------------ ** @brief Fast @c sqrt approximation ** @param x argument. ** @return approximation of @c sqrt(x). ** ** The function uses ::vl_fast_resqrt_f ** (or ::vl_fast_resqrt_d) to compute x * ** vl_fast_resqrt_f(x). **/ VL_INLINE float vl_fast_sqrt_f (float x) { return (x < 1e-8) ? 0 : x * vl_fast_resqrt_f (x) ; } /** @brief Fast @c sqrt approximation ** @copydoc vl_fast_sqrt_f **/ VL_INLINE double vl_fast_sqrt_d (float x) { return (x < 1e-8) ? 0 : x * vl_fast_resqrt_d (x) ; } /** @brief Fast integer @c sqrt approximation ** @param x non-negative integer. ** @return largest integer $y$ such that $y^2 \leq x$. ** @sa @ref mathop-sqrti "Algorithm" **/ VL_INLINE vl_uint64 vl_fast_sqrt_ui64 (vl_uint64 x) ; /** @brief Fast @c sqrt approximation ** @copydoc vl_fast_sqrt_ui64 */ VL_INLINE vl_uint32 vl_fast_sqrt_ui32 (vl_uint32 x) ; /** @brief Fast @c sqrt approximation ** @copydoc vl_fast_sqrt_ui64 */ VL_INLINE vl_uint16 vl_fast_sqrt_ui16 (vl_uint16 x) ; /** @brief Fast @c sqrt approximation ** @copydoc vl_fast_sqrt_ui64 */ VL_INLINE vl_uint8 vl_fast_sqrt_ui8 (vl_uint8 x) ; #define VL_FAST_SQRT_UI(T,SFX) \ VL_INLINE T \ vl_fast_sqrt_ ## SFX (T x) \ { \ T y = 0 ; \ T tmp = 0 ; \ int twice_k ; \ for (twice_k = 8 * sizeof(T) - 2 ; \ twice_k >= 0 ; twice_k -= 2) { \ y <<= 1 ; /* y = 2 * y */ \ tmp = (2*y + 1) << twice_k ; \ if (x >= tmp) { \ x -= tmp ; \ y += 1 ; \ } \ } \ return y ; \ } VL_FAST_SQRT_UI(vl_uint64,ui64) VL_FAST_SQRT_UI(vl_uint32,ui32) VL_FAST_SQRT_UI(vl_uint16,ui16) VL_FAST_SQRT_UI(vl_uint8,ui8) /* ---------------------------------------------------------------- */ /* Vector distances and similarities */ /* ---------------------------------------------------------------- */ /** @typedef VlFloatVectorComparisonFunction ** @brief Pointer to a function to compare vectors of floats **/ typedef float (*VlFloatVectorComparisonFunction)(vl_size dimension, float const * X, float const * Y) ; /** @typedef VlDoubleVectorComparisonFunction ** @brief Pointer to a function to compare vectors of doubles **/ typedef double (*VlDoubleVectorComparisonFunction)(vl_size dimension, double const * X, double const * Y) ; /** @typedef VlFloatVector3ComparisonFunction ** @brief Pointer to a function to compare 3 vectors of doubles **/ typedef float (*VlFloatVector3ComparisonFunction)(vl_size dimension, float const * X, float const * Y, float const * Z) ; /** @typedef VlDoubleVector3ComparisonFunction ** @brief Pointer to a function to compare 3 vectors of doubles **/ typedef double (*VlDoubleVector3ComparisonFunction)(vl_size dimension, double const * X, double const * Y, double const * Z) ; /** @brief Vector comparison types */ enum _VlVectorComparisonType { VlDistanceL1, /**< l1 distance (squared intersection metric) */ VlDistanceL2, /**< squared l2 distance */ VlDistanceChi2, /**< squared Chi2 distance */ VlDistanceHellinger, /**< squared Hellinger's distance */ VlDistanceJS, /**< squared Jensen-Shannon distance */ VlDistanceMahalanobis, /**< squared mahalanobis distance */ VlKernelL1, /**< intersection kernel */ VlKernelL2, /**< l2 kernel */ VlKernelChi2, /**< Chi2 kernel */ VlKernelHellinger, /**< Hellinger's kernel */ VlKernelJS /**< Jensen-Shannon kernel */ } ; /** @brief Vector comparison types */ typedef enum _VlVectorComparisonType VlVectorComparisonType ; /** @brief Get the symbolic name of a vector comparison type ** @param type vector comparison type. ** @return data symbolic name. **/ VL_INLINE char const * vl_get_vector_comparison_type_name (int type) { switch (type) { case VlDistanceL1 : return "l1" ; case VlDistanceL2 : return "l2" ; case VlDistanceChi2 : return "chi2" ; case VlDistanceMahalanobis : return "mahalanobis" ; case VlKernelL1 : return "kl1" ; case VlKernelL2 : return "kl2" ; case VlKernelChi2 : return "kchi2" ; default: return NULL ; } } VL_EXPORT VlFloatVectorComparisonFunction vl_get_vector_comparison_function_f (VlVectorComparisonType type) ; VL_EXPORT VlDoubleVectorComparisonFunction vl_get_vector_comparison_function_d (VlVectorComparisonType type) ; VL_EXPORT VlFloatVector3ComparisonFunction vl_get_vector_3_comparison_function_f (VlVectorComparisonType type) ; VL_EXPORT VlDoubleVector3ComparisonFunction vl_get_vector_3_comparison_function_d (VlVectorComparisonType type) ; VL_EXPORT void vl_eval_vector_comparison_on_all_pairs_f (float * result, vl_size dimension, float const * X, vl_size numDataX, float const * Y, vl_size numDataY, VlFloatVectorComparisonFunction function) ; VL_EXPORT void vl_eval_vector_comparison_on_all_pairs_d (double * result, vl_size dimension, double const * X, vl_size numDataX, double const * Y, vl_size numDataY, VlDoubleVectorComparisonFunction function) ; /* ---------------------------------------------------------------- */ /* Numerical analysis */ /* ---------------------------------------------------------------- */ VL_EXPORT void vl_svd2 (double* S, double *U, double *V, double const *M) ; VL_EXPORT void vl_lapack_dlasv2 (double *smin, double *smax, double *sv, double *cv, double *su, double *cu, double f, double g, double h) ; VL_EXPORT int vl_solve_linear_system_3 (double * x, double const * A, double const *b) ; VL_EXPORT int vl_solve_linear_system_2 (double * x, double const * A, double const *b) ; VL_EXPORT int vl_gaussian_elimination (double * A, vl_size numRows, vl_size numColumns) ; /* VL_MATHOP_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/mathop_avx.c000077500000000000000000000144171454702036400214320ustar00rootroot00000000000000/** @file mathop_avx.c ** @brief mathop for AVX - Definition ** @author Andrea Vedaldi, David Novotny **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /* ---------------------------------------------------------------- */ #if ! defined(VL_MATHOP_AVX_INSTANTIATING) #include "mathop_avx.h" #undef FLT #define FLT VL_TYPE_DOUBLE #define VL_MATHOP_AVX_INSTANTIATING #include "mathop_avx.c" #undef FLT #define FLT VL_TYPE_FLOAT #define VL_MATHOP_AVX_INSTANTIATING #include "mathop_avx.c" /* ---------------------------------------------------------------- */ /* VL_MATHOP_AVX_INSTANTIATING */ #else #ifndef VL_DISABLE_AVX #ifndef __AVX__ #error Compiling AVX functions but AVX does not seem to be supported by the compiler. #endif #include #include "generic.h" #include "mathop.h" #include "float.h" VL_INLINE T VL_XCAT(_vl_vhsum_avx_, SFX)(VTYPEavx x) { T acc ; #if (VSIZEavx == 8) { //VTYPEavx hsum = _mm256_hadd_ps(x, x); //hsum = _mm256_add_ps(hsum, _mm256_permute2f128_ps(hsum, hsum, 0x1)); //_mm_store_ss(&acc, _mm_hadd_ps( _mm256_castps256_ps128(hsum), _mm256_castps256_ps128(hsum) ) ); VTYPEavx hsum = VHADD2avx(x, x); hsum = VADDavx(hsum, VPERMavx(hsum, hsum, 0x1)); VST1(&acc, VHADDavx( VCSTavx(hsum), VCSTavx(hsum) ) ); } #else { //VTYPEavx hsum = _mm256_add_pd(x, _mm256_permute2f128_pd(x, x, 0x1)); VTYPEavx hsum = VADDavx(x, VPERMavx(x, x, 0x1)); //_mm_store_sd(&acc, _mm_hadd_pd( _mm256_castpd256_pd128(hsum), _mm256_castpd256_pd128(hsum) ) ); VST1(&acc, VHADDavx( VCSTavx(hsum), VCSTavx(hsum) ) ); } #endif return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_l2_avx_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZEavx + 1 ; T acc ; VTYPEavx vacc = VSTZavx() ; vl_bool dataAligned = VALIGNEDavx(X) & VALIGNEDavx(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPEavx a = *(VTYPEavx*)X ; VTYPEavx b = *(VTYPEavx*)Y ; VTYPEavx delta = VSUBavx(a, b) ; VTYPEavx delta2 = VMULavx(delta, delta) ; vacc = VADDavx(vacc, delta2) ; X += VSIZEavx ; Y += VSIZEavx ; } } else { while (X < X_vec_end) { VTYPEavx a = VLDUavx(X) ; VTYPEavx b = VLDUavx(Y) ; VTYPEavx delta = VSUBavx(a, b) ; VTYPEavx delta2 = VMULavx(delta, delta) ; vacc = VADDavx(vacc, delta2) ; X += VSIZEavx ; Y += VSIZEavx ; } } acc = VL_XCAT(_vl_vhsum_avx_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T delta = a - b ; acc += delta * delta ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_mahalanobis_sq_avx_, SFX) (vl_size dimension, T const * X, T const * MU, T const * S) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZEavx + 1 ; T acc ; VTYPEavx vacc = VSTZavx() ; vl_bool dataAligned = VALIGNEDavx(X) & VALIGNEDavx(MU) & VALIGNEDavx(S); if (dataAligned) { while (X < X_vec_end) { VTYPEavx a = *(VTYPEavx*)X ; VTYPEavx b = *(VTYPEavx*)MU ; VTYPEavx c = *(VTYPEavx*)S ; VTYPEavx delta = VSUBavx(a, b) ; VTYPEavx delta2 = VMULavx(delta, delta) ; VTYPEavx delta2div = VMULavx(delta2,c); vacc = VADDavx(vacc, delta2div) ; X += VSIZEavx ; MU += VSIZEavx ; S += VSIZEavx ; } } else { while (X < X_vec_end) { VTYPEavx a = VLDUavx(X) ; VTYPEavx b = VLDUavx(MU) ; VTYPEavx c = VLDUavx(S) ; VTYPEavx delta = VSUBavx(a, b) ; VTYPEavx delta2 = VMULavx(delta, delta) ; VTYPEavx delta2div = VMULavx(delta2,c); vacc = VADDavx(vacc, delta2div) ; X += VSIZEavx ; MU += VSIZEavx ; S += VSIZEavx ; } } acc = VL_XCAT(_vl_vhsum_avx_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *MU++ ; T c = *S++ ; T delta = a - b ; acc += (delta * delta) * c; } return acc ; } VL_EXPORT void VL_XCAT(_vl_weighted_mean_avx_, SFX) (vl_size dimension, T * MU, T const * X, T const W) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZEavx + 1 ; vl_bool dataAligned = VALIGNEDavx(X) & VALIGNEDavx(MU); VTYPEavx w = VLD1avx (&W) ; if (dataAligned) { while (X < X_vec_end) { VTYPEavx a = *(VTYPEavx*)X ; VTYPEavx mu = *(VTYPEavx*)MU ; VTYPEavx aw = VMULavx(a, w) ; VTYPEavx meanStore = VADDavx(aw, mu); *(VTYPEavx *)MU = meanStore; X += VSIZEavx ; MU += VSIZEavx ; } } else { while (X < X_vec_end) { VTYPEavx a = VLDUavx(X) ; VTYPEavx mu = VLDUavx(MU) ; VTYPEavx aw = VMULavx(a, w) ; VTYPEavx meanStore = VADDavx(aw, mu); VST2Uavx(MU,meanStore); X += VSIZEavx ; MU += VSIZEavx ; } } while (X < X_end) { T a = *X++ ; *MU += a * W ; MU++; } } VL_EXPORT void VL_XCAT(_vl_weighted_sigma_avx_, SFX) (vl_size dimension, T * S, T const * X, T const * Y, T const W) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZEavx + 1 ; vl_bool dataAligned = VALIGNEDavx(X) & VALIGNEDavx(Y) & VALIGNEDavx(S); VTYPEavx w = VLD1avx (&W) ; if (dataAligned) { while (X < X_vec_end) { VTYPEavx a = *(VTYPEavx*)X ; VTYPEavx b = *(VTYPEavx*)Y ; VTYPEavx s = *(VTYPEavx*)S ; VTYPEavx delta = VSUBavx(a, b) ; VTYPEavx delta2 = VMULavx(delta, delta) ; VTYPEavx delta2w = VMULavx(delta2, w) ; VTYPEavx sigmaStore = VADDavx(s,delta2w); *(VTYPEavx *)S = sigmaStore; X += VSIZEavx ; Y += VSIZEavx ; S += VSIZEavx ; } } else { while (X < X_vec_end) { VTYPEavx a = VLDUavx(X) ; VTYPEavx b = VLDUavx(Y) ; VTYPEavx s = VLDUavx(S) ; VTYPEavx delta = VSUBavx(a, b) ; VTYPEavx delta2 = VMULavx(delta, delta) ; VTYPEavx delta2w = VMULavx(delta2, w) ; VTYPEavx sigmaStore = VADDavx(s,delta2w); VST2Uavx(S,sigmaStore); X += VSIZEavx ; Y += VSIZEavx ; S += VSIZEavx ; } } while (X < X_end) { T a = *X++ ; T b = *Y++ ; T delta = a - b ; *S += ((delta * delta)*W) ; S++; } } /* VL_DISABLE_AVX */ #endif #undef VL_MATHOP_AVX_INSTANTIATING #endif colmap-3.9.1/src/thirdparty/VLFeat/mathop_avx.h000066400000000000000000000025361454702036400214330ustar00rootroot00000000000000/** @file mathop_avx.h ** @brief mathop for avx ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /* ---------------------------------------------------------------- */ #ifndef VL_MATHOP_AVX_H_INSTANTIATING #ifndef VL_MATHOP_AVX_H #define VL_MATHOP_AVX_H #undef FLT #define FLT VL_TYPE_DOUBLE #define VL_MATHOP_AVX_H_INSTANTIATING #include "mathop_avx.h" #undef FLT #define FLT VL_TYPE_FLOAT #define VL_MATHOP_AVX_H_INSTANTIATING #include "mathop_avx.h" /* VL_MATHOP_AVX_H */ #endif /* ---------------------------------------------------------------- */ /* VL_MATHOP_AVX_H_INSTANTIATING */ #else #ifndef VL_DISABLE_AVX #include "generic.h" #include "float.h" VL_EXPORT T VL_XCAT(_vl_distance_mahalanobis_sq_avx_, SFX) (vl_size dimension, T const * X, T const * MU, T const * S); VL_EXPORT T VL_XCAT(_vl_distance_l2_avx_, SFX) (vl_size dimension, T const * X, T const * Y); VL_EXPORT void VL_XCAT(_vl_weighted_sigma_avx_, SFX) (vl_size dimension, T * S, T const * X, T const * Y, T const W); VL_EXPORT void VL_XCAT(_vl_weighted_mean_avx_, SFX) (vl_size dimension, T * MU, T const * X, T const W); /* ! VL_DISABLE_AVX */ #endif #undef VL_MATHOP_AVX_H_INSTANTIATING #endif colmap-3.9.1/src/thirdparty/VLFeat/mathop_sse2.c000077500000000000000000000277671454702036400215240ustar00rootroot00000000000000/** @file mathop_sse2.c ** @brief mathop for SSE2 - Definition ** @author Andrea Vedaldi, David Novotny **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /* ---------------------------------------------------------------- */ #ifndef VL_MATHOP_SSE2_INSTANTIATING #include "mathop_sse2.h" #undef FLT #define FLT VL_TYPE_DOUBLE #define VL_MATHOP_SSE2_INSTANTIATING #include "mathop_sse2.c" #undef FLT #define FLT VL_TYPE_FLOAT #define VL_MATHOP_SSE2_INSTANTIATING #include "mathop_sse2.c" /* ---------------------------------------------------------------- */ /* VL_MATHOP_SSE2_INSTANTIATING */ #else #ifndef VL_DISABLE_SSE2 #ifndef __SSE2__ #error Compiling SSE2 functions but SSE2 does not to be supported by the compiler. #endif #include #include "mathop.h" #include "generic.h" #include "float.h" VL_INLINE T VL_XCAT(_vl_vhsum_sse2_, SFX)(VTYPE x) { T acc ; #if (VSIZE == 4) { VTYPE sum ; VTYPE shuffle ; /* shuffle = [1 0 3 2] */ /* sum = [3+1 2+0 1+3 0+2] */ /* shuffle = [2+0 3+1 0+2 1+3] */ /* vacc = [3+1+2+0 3+1+2+0 1+3+0+2 0+2+1+3] */ shuffle = VSHU (x, x, _MM_SHUFFLE(1, 0, 3, 2)) ; sum = VADD (x, shuffle) ; shuffle = VSHU (sum, sum, _MM_SHUFFLE(2, 3, 0, 1)) ; x = VADD (sum, shuffle) ; } #else { VTYPE shuffle ; /* acc = [1 0 ] */ /* shuffle = [0 1 ] */ /* sum = [1+0 0+1] */ shuffle = VSHU (x, x, _MM_SHUFFLE2(0, 1)) ; x = VADD (x, shuffle) ; } #endif VST1(&acc, x); return acc ; } VL_EXPORT T VL_XCAT(_vl_dot_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZE + 1 ; T acc ; VTYPE vacc = VSTZ() ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; VTYPE d = VMUL(a, b) ; vacc = VADD(vacc, d) ; X += VSIZE ; Y += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; VTYPE d = VMUL(a, b) ; vacc = VADD(vacc, d) ; X += VSIZE ; Y += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; acc += a * b ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_l2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZE + 1 ; T acc ; VTYPE vacc = VSTZ() ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; VTYPE delta = VSUB(a, b) ; VTYPE delta2 = VMUL(delta, delta) ; vacc = VADD(vacc, delta2) ; X += VSIZE ; Y += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; VTYPE delta = VSUB(a, b) ; VTYPE delta2 = VMUL(delta, delta) ; vacc = VADD(vacc, delta2) ; X += VSIZE ; Y += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T delta = a - b ; acc += delta * delta ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_mahalanobis_sq_sse2_, SFX) (vl_size dimension, T const * X, T const * MU, T const * S) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZE + 1 ; T acc ; VTYPE vacc = VSTZ() ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(MU) & VALIGNED(S); if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)MU ; VTYPE c = *(VTYPE*)S ; VTYPE delta = VSUB(a, b) ; VTYPE delta2 = VMUL(delta, delta) ; VTYPE delta2div = VMUL(delta2,c); vacc = VADD(vacc, delta2div) ; X += VSIZE ; MU += VSIZE ; S += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(MU) ; VTYPE c = VLDU(S) ; VTYPE delta = VSUB(a, b) ; VTYPE delta2 = VMUL(delta, delta) ; VTYPE delta2div = VMUL(delta2,c); vacc = VADD(vacc, delta2div) ; X += VSIZE ; MU += VSIZE ; S += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *MU++ ; T c = *S++ ; T delta = a - b ; acc += (delta * delta) * c; } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_l1_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X + dimension - VSIZE ; T acc ; VTYPE vacc = VSTZ() ; VTYPE vminus = VL_XCAT(_mm_set1_p, VSFX) ((T) -0.0) ; /* sign bit */ vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; VTYPE delta = VSUB(a, b) ; vacc = VADD(vacc, VANDN(vminus, delta)) ; X += VSIZE ; Y += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; VTYPE delta = VSUB(a, b) ; vacc = VADD(vacc, VANDN(vminus, delta)) ; X += VSIZE ; Y += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T delta = a - b ; acc += VL_MAX(delta, - delta) ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_distance_chi2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X + dimension - VSIZE ; T acc ; VTYPE vacc = VSTZ() ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; VTYPE delta = VSUB(a, b) ; VTYPE denom = VADD(a, b) ; VTYPE numer = VMUL(delta, delta) ; VTYPE ratio = VDIV(numer, denom) ; ratio = VAND(ratio, VNEQ(denom, VSTZ())) ; vacc = VADD(vacc, ratio) ; X += VSIZE ; Y += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; VTYPE delta = VSUB(a, b) ; VTYPE denom = VADD(a, b) ; VTYPE numer = VMUL(delta, delta) ; VTYPE ratio = VDIV(numer, denom) ; ratio = VAND(ratio, VNEQ(denom, VSTZ())) ; vacc = VADD(vacc, ratio) ; X += VSIZE ; Y += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T delta = a - b ; T denom = a + b ; T numer = delta * delta ; if (denom) { T ratio = numer / denom ; acc += ratio ; } } return acc ; } VL_EXPORT T VL_XCAT(_vl_kernel_l2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZE + 1 ; T acc ; VTYPE vacc = VSTZ() ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; vacc = VADD(vacc, VMUL(a,b)) ; X += VSIZE ; Y += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; vacc = VADD(vacc, VMUL(a,b)) ; X += VSIZE ; Y += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; acc += a * b ; } return acc ; } VL_EXPORT T VL_XCAT(_vl_kernel_l1_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZE + 1 ; T acc ; VTYPE vacc = VSTZ() ; VTYPE vminus = VL_XCAT(_mm_set1_p, VSFX) ((T) -0.0) ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; VTYPE a_ = VANDN(vminus, a) ; VTYPE b_ = VANDN(vminus, b) ; VTYPE sum = VADD(a_,b_) ; VTYPE diff = VSUB(a, b) ; VTYPE diff_ = VANDN(vminus, diff) ; vacc = VADD(vacc, VSUB(sum, diff_)) ; X += VSIZE ; Y += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; VTYPE a_ = VANDN(vminus, a) ; VTYPE b_ = VANDN(vminus, b) ; VTYPE sum = VADD(a_,b_) ; VTYPE diff = VSUB(a, b) ; VTYPE diff_ = VANDN(vminus, diff) ; vacc = VADD(vacc, VSUB(sum, diff_)) ; X += VSIZE ; Y += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T a_ = VL_XCAT(vl_abs_, SFX) (a) ; T b_ = VL_XCAT(vl_abs_, SFX) (b) ; acc += a_ + b_ - VL_XCAT(vl_abs_, SFX) (a - b) ; } return acc / ((T)2) ; } VL_EXPORT T VL_XCAT(_vl_kernel_chi2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) { T const * X_end = X + dimension ; T const * X_vec_end = X + dimension - VSIZE ; T acc ; VTYPE vacc = VSTZ() ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; VTYPE denom = VADD(a, b) ; VTYPE numer = VMUL(a,b) ; VTYPE ratio = VDIV(numer, denom) ; ratio = VAND(ratio, VNEQ(denom, VSTZ())) ; vacc = VADD(vacc, ratio) ; X += VSIZE ; Y += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; VTYPE denom = VADD(a, b) ; VTYPE numer = VMUL(a,b) ; VTYPE ratio = VDIV(numer, denom) ; ratio = VAND(ratio, VNEQ(denom, VSTZ())) ; vacc = VADD(vacc, ratio) ; X += VSIZE ; Y += VSIZE ; } } acc = VL_XCAT(_vl_vhsum_sse2_, SFX)(vacc) ; while (X < X_end) { T a = *X++ ; T b = *Y++ ; T denom = a + b ; if (denom) { T ratio = a * b / denom ; acc += ratio ; } } return ((T)2) * acc ; } // VL_EXPORT void VL_XCAT(_vl_weighted_sigma_sse2_, SFX) (vl_size dimension, T * S, T const * X, T const * Y, T const W) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZE + 1 ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(Y) & VALIGNED(S); VTYPE w = VLD1 (&W) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE b = *(VTYPE*)Y ; VTYPE s = *(VTYPE*)S ; VTYPE delta = VSUB(a, b) ; VTYPE delta2 = VMUL(delta, delta) ; VTYPE delta2w = VMUL(delta2, w) ; VTYPE sigmaStore = VADD(s,delta2w); *(VTYPE *)S = sigmaStore; X += VSIZE ; Y += VSIZE ; S += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE b = VLDU(Y) ; VTYPE s = VLDU(S) ; VTYPE delta = VSUB(a, b) ; VTYPE delta2 = VMUL(delta, delta) ; VTYPE delta2w = VMUL(delta2, w) ; VTYPE sigmaStore = VADD(s,delta2w); VST2U(S,sigmaStore); X += VSIZE ; Y += VSIZE ; S += VSIZE ; } } while (X < X_end) { T a = *X++ ; T b = *Y++ ; T delta = a - b ; *S += ((delta * delta)*W) ; S++; } } VL_EXPORT void VL_XCAT(_vl_weighted_mean_sse2_, SFX) (vl_size dimension, T * MU, T const * X, T const W) { T const * X_end = X + dimension ; T const * X_vec_end = X_end - VSIZE + 1 ; vl_bool dataAligned = VALIGNED(X) & VALIGNED(MU); VTYPE w = VLD1 (&W) ; if (dataAligned) { while (X < X_vec_end) { VTYPE a = *(VTYPE*)X ; VTYPE mu = *(VTYPE*)MU ; VTYPE aw = VMUL(a, w) ; VTYPE meanStore = VADD(aw, mu); *(VTYPE *)MU = meanStore; X += VSIZE ; MU += VSIZE ; } } else { while (X < X_vec_end) { VTYPE a = VLDU(X) ; VTYPE mu = VLDU(MU) ; VTYPE aw = VMUL(a, w) ; VTYPE meanStore = VADD(aw, mu); VST2U(MU,meanStore); X += VSIZE ; MU += VSIZE ; } } while (X < X_end) { T a = *X++ ; *MU += a * W ; MU++; } } /* VL_DISABLE_SSE2 */ #endif #undef VL_MATHOP_SSE2_INSTANTIATING #endif colmap-3.9.1/src/thirdparty/VLFeat/mathop_sse2.h000066400000000000000000000036531454702036400215120ustar00rootroot00000000000000/** @file mathop_sse2.h ** @brief mathop for sse2 ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /* ---------------------------------------------------------------- */ #ifndef VL_MATHOP_SSE2_H_INSTANTIATING #ifndef VL_MATHOP_SSE2_H #define VL_MATHOP_SSE2_H #undef FLT #define FLT VL_TYPE_DOUBLE #define VL_MATHOP_SSE2_H_INSTANTIATING #include "mathop_sse2.h" #undef FLT #define FLT VL_TYPE_FLOAT #define VL_MATHOP_SSE2_H_INSTANTIATING #include "mathop_sse2.h" /* VL_MATHOP_SSE2_H */ #endif /* ---------------------------------------------------------------- */ /* VL_MATHOP_SSE2_H_INSTANTIATING */ #else #ifndef VL_DISABLE_SSE2 #include "generic.h" #include "float.h" VL_EXPORT T VL_XCAT(_vl_dot_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) ; VL_EXPORT T VL_XCAT(_vl_distance_l2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) ; VL_EXPORT T VL_XCAT(_vl_distance_l1_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) ; VL_EXPORT T VL_XCAT(_vl_distance_chi2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) ; VL_EXPORT T VL_XCAT(_vl_kernel_l2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) ; VL_EXPORT T VL_XCAT(_vl_kernel_l1_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) ; VL_EXPORT T VL_XCAT(_vl_kernel_chi2_sse2_, SFX) (vl_size dimension, T const * X, T const * Y) ; VL_EXPORT T VL_XCAT(_vl_distance_mahalanobis_sq_sse2_, SFX) (vl_size dimension, T const * X, T const * MU, T const * S); VL_EXPORT void VL_XCAT(_vl_weighted_sigma_sse2_, SFX) (vl_size dimension, T * S, T const * X, T const * Y, T const W); VL_EXPORT void VL_XCAT(_vl_weighted_mean_sse2_, SFX) (vl_size dimension, T * MU, T const * X, T const W); /* ! VL_DISABLE_SSE2 */ #endif #undef VL_MATHOP_SSE2_INSTANTIATING #endif colmap-3.9.1/src/thirdparty/VLFeat/mser.c000077500000000000000000000731271454702036400202350ustar00rootroot00000000000000/** @file mser.c ** @brief MSER - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-13 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page mser Maximally Stable Extremal Regions (MSER) @author Andrea Vedaldi @tableofcontents @ref mser.h implements the *Maximally Stable Extremal Regions* (MSER) local feature detector of @cite{matas03robust}. This detector extracts as features the the connected components of the level sets of the input intensity image. Among all such regions, the ones that are locally maximally stable are selected. MSERs are affine co-variant, as well as largely co-variant to generic diffeomorphic transformations. See @ref mser-starting for an introduction on how to use the detector from the C API. For further details refer to: - @subpage mser-fundamentals - MSER definition and parameters. @section mser-starting Getting started with the MSER detector Running the MSER filter usually involves the following steps: - Initialize the MSER filter by ::vl_mser_new(). The filter can be reused for images of the same size. - Compute the MSERs by ::vl_mser_process(). - Optionally fit ellipsoids to the MSERs by ::vl_mser_ell_fit(). - Retrieve the results by ::vl_mser_get_regions() (and optionally ::vl_mser_get_ell()). - Optionally retrieve filter statistics by ::vl_mser_get_stats(). - Delete the MSER filter by ::vl_mser_delete(). @page mser-fundamentals MSER fundamentals @tableofcontents The *extermal regions* of an image are the connected components of the level sets $S_l = \{ x : I(x) \leq l \}, l \in \real$ of the image $I(x)$. Consider a discretization of the intensity levels $l$ consisting of $M$ samples $\mathcal{L}=\{0,\dots,M-1\}$. The extremal regions $R_l \subset S_l$ of the level sets $S_l, l \in \mathcal{L}$ can be arranged in a tree, where a region $R_l$ is a children of a region $R_{l+1}$ if $R_l \subset R_{l+1}$. The following figures shows a 1D example where the regions are denoted by dark thick lines: @image html mser-tree.png "Connected components of the image level sets arranged in a tree." Note that, depending on the image, regions at different levels can be identical as sets: @image html mser-er-step.png "Connected components when the image contains step changes." A *stable extremal region* is an extremal region that does not change much as the index $l$ is varied. Here we use a criterion which is similar but not identical to the original paper. This definition is somewhat simpler both to understand and code. Let $B(R_l)=(R_l,R_{l+1},\dots,R_{l+\Delta})$ be the branch of the tree $R_l \subset R_{l+1} \subset \dots \subset R_{l + \Delta}$ rooted at $R_l$. We associate to the branch the (in)stability score @f[ v(R_l) = \frac{|R_{l+\Delta} - R_l|}{|R_l|}. @f] This score is a relative measure of how much $R_l$ changes as the index is increased from $l$ to $l+\Delta$, as illustrated in the following figure. @image html mser-er.png "Stability is measured by looking at how much a region changes with the intensity level." The score is low if the regions along the branch have similar area (and thus similar shape). We aim to select maximally stable branches; then a maximally stable region is just a representative region selected from a maximally stable branch (for simplicity we select $R_l$, but one could choose for example $R_{l+\Delta/2}$). Roughly speaking, a branch is maximally stable if it is a local minimum of the (in)stability score. More accurately, we start by assuming that all branches are maximally stable. Then we consider each branch $B(R_{l})$ and its parent branch $B(R_{l+1}):R_{l+1}\supset R_l$ (notice that, due to the discrete nature of the calculations, they might be geometrically identical) and we mark as unstable the less stable one, i.e.: - if $v(R_l)v(R_{l+1})$, mark $R_{l}$ as unstable; - otherwise, do nothing. This criterion selects among nearby regions the ones that are more stable. We optionally refine the selection by running (starting from the bigger and going to the smaller regions) the following tests: - $a_- \leq |R_{l}|/|R_{\infty}| \leq a_+$: exclude MSERs too small or too big ($|R_{\infty}|$ is the area of the image). - $v(R_{l}) < v_+$: exclude MSERs too unstable. - For any MSER $R_l$, find the parent MSER $R_{l'}$ and check if $|R_{l'} - R_l|/|R_l'| < d_+$: remove duplicated MSERs.
parameter alt. name standard value set by
$\Delta$ @c delta 5 ::vl_mser_set_delta()
$a_+$ @c max_area 0.75 ::vl_mser_set_max_area()
$a_-$ @c min_area 3.0/$|R_\infty|$ ::vl_mser_set_min_area()
$v_+$ @c max_var 0.25 ::vl_mser_set_max_variation()
$d_+$ @c min_diversity 0.2 ::vl_mser_set_min_diversity()
@section mser-vol Volumetric images The code supports images of arbitrary dimension. For instance, it is possible to find the MSER regions of volumetric images or time sequences. See ::vl_mser_new() for further details @section mser-ell Ellipsoids Usually extremal regions are returned as a set of ellipsoids fitted to the actual regions (which have arbitrary shape). The fit is done by calculating the mean and variance of the pixels composing the region: @f[ \mu_l = \frac{1}{|R_l|}\sum_{x\in R_l}x, \qquad \Sigma_l = \frac{1}{|R_l|}\sum_{x\in R_l} (x-\mu_l)^\top(x-\mu_l) @f] Ellipsoids are fitted by ::vl_mser_ell_fit(). Notice that for a n dimensional image, the mean has n components and the variance has n(n+1)/2 independent components. The total number of components is obtained by ::vl_mser_get_ell_dof() and the total number of fitted ellipsoids by ::vl_mser_get_ell_num(). A matrix with an ellipsoid per column is returned by ::vl_mser_get_ell(). The column is the stacking of the mean and of the independent components of the variance, in the order (1,1),(1,2),..,(1,n), (2,2),(2,3).... In the calculations, the pixel coordinate $x=(x_1,...,x_n)$ use the standard index order and ranges. @section mser-algo Algorithm The algorithm is quite efficient. While some details may be tricky, the overall idea is easy to grasp. - Pixels are sorted by increasing intensity. - Pixels are added to a forest by increasing intensity. The forest has the following properties: - All the descendent of a certain pixels are subset of an extremal region. - All the extremal regions are the descendants of some pixels. - Extremal regions are extracted from the region tree and the extremal regions tree is calculated. - Stable regions are marked. - Duplicates and other bad regions are removed. @remark The extremal region tree which is calculated is a subset of the actual extremal region tree. In particular, it does not contain redundant entries extremal regions that coincide as sets. So, for example, in the calculated extremal region tree, the parent $R_q$ of an extremal region $R_{l}$ may or may not correspond to $R_{l+1}$, depending whether $q\leq l+1$ or not. These subtleties are important when calculating the stability tests. **/ #include "mser.h" #include #include #include /** ------------------------------------------------------------------- ** @brief Advance N-dimensional subscript ** ** The function increments by one the subscript @a subs indexing an ** array the @a ndims dimensions @a dims. ** ** @param ndims number of dimensions. ** @param dims dimensions. ** @param subs subscript to advance. **/ VL_INLINE void adv(int ndims, int const *dims, int *subs) { int d = 0 ; while(d < ndims) { if( ++subs[d] < dims[d] ) return ; subs[d++] = 0 ; } } /** ------------------------------------------------------------------- ** @brief Climb the region forest to reach aa root ** ** The function climbs the regions forest @a r starting from the node ** @a idx to the corresponding root. ** ** To speed-up the operation, the function uses the ** VlMserReg::shortcut field to quickly jump to the root. After the ** root is reached, all the used shortcut are updated. ** ** @param r regions' forest. ** @param idx stating node. ** @return index of the reached root. **/ VL_INLINE vl_uint climb (VlMserReg* r, vl_uint idx) { vl_uint prev_idx = idx ; vl_uint next_idx ; vl_uint root_idx ; /* move towards root to find it */ while (1) { /* next jump to the root */ next_idx = r [idx] .shortcut ; /* recycle shortcut to remember how we came here */ r [idx] .shortcut = prev_idx ; /* stop if the root is found */ if( next_idx == idx ) break ; /* next guy */ prev_idx = idx ; idx = next_idx ; } root_idx = idx ; /* move backward to update shortcuts */ while (1) { /* get previously visited one */ prev_idx = r [idx] .shortcut ; /* update shortcut to point to the new root */ r [idx] .shortcut = root_idx ; /* stop if the first visited node is reached */ if( prev_idx == idx ) break ; /* next guy */ idx = prev_idx ; } return root_idx ; } /** ------------------------------------------------------------------- ** @brief Create a new MSER filter ** ** Initializes a new MSER filter for images of the specified ** dimensions. Images are @a ndims -dimensional arrays of dimensions ** @a dims. ** ** @param ndims number of dimensions. ** @param dims dimensions. **/ VL_EXPORT VlMserFilt* vl_mser_new (int ndims, int const* dims) { VlMserFilt* f ; int *strides, k ; f = vl_calloc (sizeof(VlMserFilt), 1) ; f-> ndims = ndims ; f-> dims = vl_malloc (sizeof(int) * ndims) ; f-> subs = vl_malloc (sizeof(int) * ndims) ; f-> dsubs = vl_malloc (sizeof(int) * ndims) ; f-> strides = vl_malloc (sizeof(int) * ndims) ; /* shortcuts */ strides = f-> strides ; /* copy dims to f->dims */ for(k = 0 ; k < ndims ; ++k) { f-> dims [k] = dims [k] ; } /* compute strides to move into the N-dimensional image array */ strides [0] = 1 ; for(k = 1 ; k < ndims ; ++k) { strides [k] = strides [k-1] * dims [k-1] ; } /* total number of pixels */ f-> nel = strides [ndims-1] * dims [ndims-1] ; /* dof of ellipsoids */ f-> dof = ndims * (ndims + 1) / 2 + ndims ; /* more buffers */ f-> perm = vl_malloc (sizeof(vl_uint) * f-> nel) ; f-> joins = vl_malloc (sizeof(vl_uint) * f-> nel) ; f-> r = vl_malloc (sizeof(VlMserReg) * f-> nel) ; f-> er = 0 ; f-> rer = 0 ; f-> mer = 0 ; f-> rmer = 0 ; f-> ell = 0 ; f-> rell = 0 ; /* other parameters */ f-> delta = 5 ; f-> max_area = 0.75 ; f-> min_area = 3.0 / f-> nel ; f-> max_variation = 0.25 ; f-> min_diversity = 0.2 ; return f ; } /** ------------------------------------------------------------------- ** @brief Delete MSER filter ** ** The function releases the MSER filter @a f and all its resources. ** ** @param f MSER filter to be deleted. **/ VL_EXPORT void vl_mser_delete (VlMserFilt* f) { if(f) { if(f-> acc ) vl_free( f-> acc ) ; if(f-> ell ) vl_free( f-> ell ) ; if(f-> er ) vl_free( f-> er ) ; if(f-> r ) vl_free( f-> r ) ; if(f-> joins ) vl_free( f-> joins ) ; if(f-> perm ) vl_free( f-> perm ) ; if(f-> strides) vl_free( f-> strides) ; if(f-> dsubs ) vl_free( f-> dsubs ) ; if(f-> subs ) vl_free( f-> subs ) ; if(f-> dims ) vl_free( f-> dims ) ; if(f-> mer ) vl_free( f-> mer ) ; vl_free (f) ; } } /** ------------------------------------------------------------------- ** @brief Process image ** ** The functions calculates the Maximally Stable Extremal Regions ** (MSERs) of image @a im using the MSER filter @a f. ** ** The filter @a f must have been initialized to be compatible with ** the dimensions of @a im. ** ** @param f MSER filter. ** @param im image data. **/ VL_EXPORT void vl_mser_process (VlMserFilt* f, vl_mser_pix const* im) { /* shortcuts */ vl_uint nel = f-> nel ; vl_uint *perm = f-> perm ; vl_uint *joins = f-> joins ; int ndims = f-> ndims ; int *dims = f-> dims ; int *subs = f-> subs ; int *dsubs = f-> dsubs ; int *strides = f-> strides ; VlMserReg *r = f-> r ; VlMserExtrReg *er = f-> er ; vl_uint *mer = f-> mer ; int delta = f-> delta ; int njoins = 0 ; int ner = 0 ; int nmer = 0 ; int nbig = 0 ; int nsmall = 0 ; int nbad = 0 ; int ndup = 0 ; int i, j, k ; /* delete any previosuly computed ellipsoid */ f-> nell = 0 ; /* ----------------------------------------------------------------- * Sort pixels by intensity * -------------------------------------------------------------- */ { vl_uint buckets [ VL_MSER_PIX_MAXVAL ] ; /* clear buckets */ memset (buckets, 0, sizeof(vl_uint) * VL_MSER_PIX_MAXVAL ) ; /* compute bucket size (how many pixels for each intensity value) */ for(i = 0 ; i < (int) nel ; ++i) { vl_mser_pix v = im [i] ; ++ buckets [v] ; } /* cumulatively add bucket sizes */ for(i = 1 ; i < VL_MSER_PIX_MAXVAL ; ++i) { buckets [i] += buckets [i-1] ; } /* empty buckets computing pixel ordering */ for(i = nel ; i >= 1 ; ) { vl_mser_pix v = im [ --i ] ; vl_uint j = -- buckets [v] ; perm [j] = i ; } } /* initialize the forest with all void nodes */ for(i = 0 ; i < (int) nel ; ++i) { r [i] .parent = VL_MSER_VOID_NODE ; } /* ----------------------------------------------------------------- * Compute regions and count extremal regions * -------------------------------------------------------------- */ /* In the following: idx : index of the current pixel val : intensity of the current pixel r_idx : index of the root of the current pixel n_idx : index of the neighbors of the current pixel nr_idx : index of the root of the neighbor of the current pixel */ /* process each pixel by increasing intensity */ for(i = 0 ; i < (int) nel ; ++i) { /* pop next node xi */ vl_uint idx = perm [i] ; vl_mser_pix val = im [idx] ; vl_uint r_idx ; /* add the pixel to the forest as a root for now */ r [idx] .parent = idx ; r [idx] .shortcut = idx ; r [idx] .area = 1 ; r [idx] .height = 1 ; r_idx = idx ; /* convert the index IDX into the subscript SUBS; also initialize DSUBS to (-1,-1,...,-1) */ { vl_uint temp = idx ; for(k = ndims - 1 ; k >= 0 ; --k) { dsubs [k] = -1 ; subs [k] = temp / strides [k] ; temp = temp % strides [k] ; } } /* examine the neighbors of the current pixel */ while (1) { vl_uint n_idx = 0 ; vl_bool good = 1 ; /* Compute the neighbor subscript as NSUBS+SUB, the corresponding neighbor index NINDEX and check that the neighbor is within the image domain. */ for(k = 0 ; k < ndims && good ; ++k) { int temp = dsubs [k] + subs [k] ; good &= (0 <= temp) && (temp < dims [k]) ; n_idx += temp * strides [k] ; } /* The neighbor should be processed if the following conditions are met: 1. The neighbor is within image boundaries. 2. The neighbor is indeed different from the current node (the opposite happens when DSUB=(0,0,...,0)). 3. The neighbor is already in the forest, meaning that it has already been processed. */ if (good && n_idx != idx && r [n_idx] .parent != VL_MSER_VOID_NODE ) { vl_mser_pix nr_val = 0 ; vl_uint nr_idx = 0 ; int hgt = r [ r_idx] .height ; int n_hgt = r [nr_idx] .height ; /* Now we join the two subtrees rooted at R_IDX = ROOT( IDX) NR_IDX = ROOT(N_IDX). Note that R_IDX = ROOT(IDX) might change as we process more neighbors, so we need keep updating it. */ r_idx = climb(r, idx) ; nr_idx = climb(r, n_idx) ; /* At this point we have three possibilities: (A) ROOT(IDX) == ROOT(NR_IDX). In this case the two trees have already been joined and we do not do anything. (B) I(ROOT(IDX)) == I(ROOT(NR_IDX)). In this case the pixel IDX is extending an extremal region with the same intensity value. Since ROOT(NR_IDX) will NOT be an extremal region of the full image, ROOT(IDX) can be safely added as children of ROOT(NR_IDX) if this reduces the height according to the union rank heuristic. (C) I(ROOT(IDX)) > I(ROOT(NR_IDX)). In this case the pixel IDX is starting a new extremal region. Thus ROOT(NR_IDX) WILL be an extremal region of the final image and the only possibility is to add ROOT(NR_IDX) as children of ROOT(IDX), which becomes parent. */ if( r_idx != nr_idx ) { /* skip if (A) */ nr_val = im [nr_idx] ; if( nr_val == val && hgt < n_hgt ) { /* ROOT(IDX) becomes the child */ r [r_idx] .parent = nr_idx ; r [r_idx] .shortcut = nr_idx ; r [nr_idx] .area += r [r_idx] .area ; r [nr_idx] .height = VL_MAX(n_hgt, hgt+1) ; joins [njoins++] = r_idx ; } else { /* cases ROOT(IDX) becomes the parent */ r [nr_idx] .parent = r_idx ; r [nr_idx] .shortcut = r_idx ; r [r_idx] .area += r [nr_idx] .area ; r [r_idx] .height = VL_MAX(hgt, n_hgt + 1) ; joins [njoins++] = nr_idx ; /* count if extremal */ if (nr_val != val) ++ ner ; } /* check b vs c */ } /* check a vs b or c */ } /* neighbor done */ /* move to next neighbor */ k = 0 ; while(++ dsubs [k] > 1) { dsubs [k++] = -1 ; if(k == ndims) goto done_all_neighbors ; } } /* next neighbor */ done_all_neighbors : ; } /* next pixel */ /* the last root is extremal too */ ++ ner ; /* save back */ f-> njoins = njoins ; f-> stats. num_extremal = ner ; /* ----------------------------------------------------------------- * Extract extremal regions * -------------------------------------------------------------- */ /* Extremal regions are extracted and stored into the array ER. The structure R is also updated so that .SHORTCUT indexes the corresponding extremal region if any (otherwise it is set to VOID). */ /* make room */ if (f-> rer < ner) { if (er) vl_free (er) ; f->er = er = vl_malloc (sizeof(VlMserExtrReg) * ner) ; f->rer = ner ; } ; /* save back */ f-> nmer = ner ; /* count again */ ner = 0 ; /* scan all regions Xi */ for(i = 0 ; i < (int) nel ; ++i) { /* pop next node xi */ vl_uint idx = perm [i] ; vl_mser_pix val = im [idx] ; vl_uint p_idx = r [idx] .parent ; vl_mser_pix p_val = im [p_idx] ; /* is extremal ? */ vl_bool is_extr = (p_val > val) || idx == p_idx ; if( is_extr ) { /* if so, add it */ er [ner] .index = idx ; er [ner] .parent = ner ; er [ner] .value = im [idx] ; er [ner] .area = r [idx] .area ; /* link this region to this extremal region */ r [idx] .shortcut = ner ; /* increase count */ ++ ner ; } else { /* link this region to void */ r [idx] .shortcut = VL_MSER_VOID_NODE ; } } /* ----------------------------------------------------------------- * Link extremal regions in a tree * -------------------------------------------------------------- */ for(i = 0 ; i < ner ; ++i) { vl_uint idx = er [i] .index ; do { idx = r[idx] .parent ; } while (r[idx] .shortcut == VL_MSER_VOID_NODE) ; er[i] .parent = r[idx] .shortcut ; er[i] .shortcut = i ; } /* ----------------------------------------------------------------- * Compute variability of +DELTA branches * -------------------------------------------------------------- */ /* For each extremal region Xi of value VAL we look for the biggest * parent that has value not greater than VAL+DELTA. This is dubbed * `top parent'. */ for(i = 0 ; i < ner ; ++i) { /* Xj is the current region the region and Xj are the parents */ int top_val = er [i] .value + delta ; int top = er [i] .shortcut ; /* examine all parents */ while (1) { int next = er [top] .parent ; int next_val = er [next] .value ; /* Break if: * - there is no node above the top or * - the next node is above the top value. */ if (next == top || next_val > top_val) break ; /* so next could be the top */ top = next ; } /* calculate branch variation */ { int area = er [i ] .area ; int area_top = er [top] .area ; er [i] .variation = (float) (area_top - area) / area ; er [i] .max_stable = 1 ; } /* Optimization: since extremal regions are processed by * increasing intensity, all next extremal regions being processed * have value at least equal to the one of Xi. If any of them has * parent the parent of Xi (this comprises the parent itself), we * can safely skip most intermediate node along the branch and * skip directly to the top to start our search. */ { int parent = er [i] .parent ; int curr = er [parent] .shortcut ; er [parent] .shortcut = VL_MAX (top, curr) ; } } /* ----------------------------------------------------------------- * Select maximally stable branches * -------------------------------------------------------------- */ nmer = ner ; for(i = 0 ; i < ner ; ++i) { vl_uint parent = er [i ] .parent ; vl_mser_pix val = er [i ] .value ; float var = er [i ] .variation ; vl_mser_pix p_val = er [parent] .value ; float p_var = er [parent] .variation ; vl_uint loser ; /* Notice that R_parent = R_{l+1} only if p_val = val + 1. If not, this and the parent region coincide and there is nothing to do. */ if(p_val > val + 1) continue ; /* decide which one to keep and put that in loser */ if(var < p_var) loser = parent ; else loser = i ; /* make loser NON maximally stable */ if(er [loser] .max_stable) { -- nmer ; er [loser] .max_stable = 0 ; } } f-> stats. num_unstable = ner - nmer ; /* ----------------------------------------------------------------- * Further filtering * -------------------------------------------------------------- */ /* It is critical for correct duplicate detection to remove regions * from the bottom (smallest one first). */ { float max_area = (float) f-> max_area * nel ; float min_area = (float) f-> min_area * nel ; float max_var = (float) f-> max_variation ; float min_div = (float) f-> min_diversity ; /* scan all extremal regions (intensity value order) */ for(i = ner-1 ; i >= 0L ; --i) { /* process only maximally stable extremal regions */ if (! er [i] .max_stable) continue ; if (er [i] .variation >= max_var ) { ++ nbad ; goto remove ; } if (er [i] .area > max_area) { ++ nbig ; goto remove ; } if (er [i] .area < min_area) { ++ nsmall ; goto remove ; } /* * Remove duplicates */ if (min_div < 1.0) { vl_uint parent = er [i] .parent ; int area, p_area ; float div ; /* check all but the root mser */ if((int) parent != i) { /* search for the maximally stable parent region */ while(! er [parent] .max_stable) { vl_uint next = er [parent] .parent ; if(next == parent) break ; parent = next ; } /* Compare with the parent region; if the current and parent * regions are too similar, keep only the parent. */ area = er [i] .area ; p_area = er [parent] .area ; div = (float) (p_area - area) / (float) p_area ; if (div < min_div) { ++ ndup ; goto remove ; } } /* remove dups end */ } continue ; remove : er [i] .max_stable = 0 ; -- nmer ; } /* check next region */ f-> stats .num_abs_unstable = nbad ; f-> stats .num_too_big = nbig ; f-> stats .num_too_small = nsmall ; f-> stats .num_duplicates = ndup ; } /* ----------------------------------------------------------------- * Save the result * -------------------------------------------------------------- */ /* make room */ if (f-> rmer < nmer) { if (mer) vl_free (mer) ; f->mer = mer = vl_malloc( sizeof(vl_uint) * nmer) ; f->rmer = nmer ; } /* save back */ f-> nmer = nmer ; j = 0 ; for (i = 0 ; i < ner ; ++i) { if (er [i] .max_stable) mer [j++] = er [i] .index ; } } /** ------------------------------------------------------------------- ** @brief Fit ellipsoids ** ** @param f MSER filter. ** ** @sa @ref mser-ell **/ VL_EXPORT void vl_mser_ell_fit (VlMserFilt* f) { /* shortcuts */ int nel = f-> nel ; int dof = f-> dof ; int *dims = f-> dims ; int ndims = f-> ndims ; int *subs = f-> subs ; int njoins = f-> njoins ; vl_uint *joins = f-> joins ; VlMserReg *r = f-> r ; vl_uint *mer = f-> mer ; int nmer = f-> nmer ; vl_mser_acc *acc = f-> acc ; vl_mser_acc *ell = f-> ell ; int d, index, i, j ; /* already fit ? */ if (f->nell == f->nmer) return ; /* make room */ if (f->rell < f->nmer) { if (f->ell) vl_free (f->ell) ; f->ell = vl_malloc (sizeof(float) * f->nmer * f->dof) ; f->rell = f-> nmer ; } if (f->acc == 0) { f->acc = vl_malloc (sizeof(float) * f->nel) ; } acc = f-> acc ; ell = f-> ell ; /* ----------------------------------------------------------------- * Integrate moments * -------------------------------------------------------------- */ /* for each dof */ for(d = 0 ; d < f->dof ; ++d) { /* start from the upper-left pixel (0,0,...,0) */ memset (subs, 0, sizeof(int) * ndims) ; /* step 1: fill acc pretending that each region has only one pixel */ if(d < ndims) { /* 1-order ................................................... */ for(index = 0 ; index < nel ; ++ index) { acc [index] = subs [d] ; adv(ndims, dims, subs) ; } } else { /* 2-order ................................................... */ /* map the dof d to a second order moment E[x_i x_j] */ i = d - ndims ; j = 0 ; while(i > j) { i -= j + 1 ; j ++ ; } /* initialize acc with x_i * x_j */ for(index = 0 ; index < nel ; ++ index){ acc [index] = subs [i] * subs [j] ; adv(ndims, dims, subs) ; } } /* step 2: integrate */ for(i = 0 ; i < njoins ; ++i) { vl_uint index = joins [i] ; vl_uint parent = r [index] .parent ; acc [parent] += acc [index] ; } /* step 3: save back to ellpises */ for(i = 0 ; i < nmer ; ++i) { vl_uint idx = mer [i] ; ell [d + dof*i] = acc [idx] ; } } /* next dof */ /* ----------------------------------------------------------------- * Compute central moments * -------------------------------------------------------------- */ for(index = 0 ; index < nmer ; ++index) { float *pt = ell + index * dof ; vl_uint idx = mer [index] ; float area = r [idx] .area ; for(d = 0 ; d < dof ; ++d) { pt [d] /= area ; if(d >= ndims) { /* remove squared mean from moment to get variance */ i = d - ndims ; j = 0 ; while(i > j) { i -= j + 1 ; j ++ ; } pt [d] -= pt [i] * pt [j] ; } } } /* save back */ f-> nell = nmer ; } colmap-3.9.1/src/thirdparty/VLFeat/mser.h000066400000000000000000000322641454702036400202340ustar00rootroot00000000000000/** @file mser.h ** @brief MSER (@ref mser) ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_MSER #define VL_MSER #include "generic.h" /** @brief MSER image data type ** ** This is the data type of the image pixels. It has to be an ** integer. **/ typedef vl_uint8 vl_mser_pix ; /** @brief Maximum value ** ** Maximum value of the integer type ::vl_mser_pix. **/ #define VL_MSER_PIX_MAXVAL 256 /** @brief MSER Filter ** ** The MSER filter computes the Maximally Stable Extremal Regions of ** an image. ** ** @sa @ref mser **/ typedef struct _VlMserFilt VlMserFilt ; /** @brief MSER filter statistics */ typedef struct _VlMserStats VlMserStats ; /** @brief MSER filter statistics definition */ struct _VlMserStats { int num_extremal ; /**< number of extremal regions */ int num_unstable ; /**< number of unstable extremal regions */ int num_abs_unstable ; /**< number of regions that failed the absolute stability test */ int num_too_big ; /**< number of regions that failed the maximum size test */ int num_too_small ; /**< number of regions that failed the minimum size test */ int num_duplicates ; /**< number of regions that failed the duplicate test */ } ; /** @name Construction and Destruction ** @{ **/ VL_EXPORT VlMserFilt* vl_mser_new (int ndims, int const* dims) ; VL_EXPORT void vl_mser_delete (VlMserFilt *f) ; /** @} */ /** @name Processing ** @{ **/ VL_EXPORT void vl_mser_process (VlMserFilt *f, vl_mser_pix const *im) ; VL_EXPORT void vl_mser_ell_fit (VlMserFilt *f) ; /** @} */ /** @name Retrieving data ** @{ **/ VL_INLINE vl_uint vl_mser_get_regions_num (VlMserFilt const *f) ; VL_INLINE vl_uint const* vl_mser_get_regions (VlMserFilt const *f) ; VL_INLINE float const* vl_mser_get_ell (VlMserFilt const *f) ; VL_INLINE vl_uint vl_mser_get_ell_num (VlMserFilt const *f) ; VL_INLINE vl_uint vl_mser_get_ell_dof (VlMserFilt const *f) ; VL_INLINE VlMserStats const* vl_mser_get_stats (VlMserFilt const *f) ; /** @} */ /** @name Retrieving parameters ** @{ **/ VL_INLINE vl_mser_pix vl_mser_get_delta (VlMserFilt const *f) ; VL_INLINE double vl_mser_get_min_area (VlMserFilt const *f) ; VL_INLINE double vl_mser_get_max_area (VlMserFilt const *f) ; VL_INLINE double vl_mser_get_max_variation (VlMserFilt const *f) ; VL_INLINE double vl_mser_get_min_diversity (VlMserFilt const *f) ; /** @} */ /** @name Setting parameters ** @{ **/ VL_INLINE void vl_mser_set_delta (VlMserFilt *f, vl_mser_pix x) ; VL_INLINE void vl_mser_set_min_area (VlMserFilt *f, double x) ; VL_INLINE void vl_mser_set_max_area (VlMserFilt *f, double x) ; VL_INLINE void vl_mser_set_max_variation (VlMserFilt *f, double x) ; VL_INLINE void vl_mser_set_min_diversity (VlMserFilt *f, double x) ; /** @} */ /* ==================================================================== * INLINE DEFINITIONS * ================================================================== */ /** @internal ** @brief MSER accumulator data type ** ** This is a large integer type. It should be large enough to contain ** a number equal to the area (volume) of the image by the image ** width by the image height (for instance, if the image is a square ** of side 256, the maximum value is 256 x 256 x 256). **/ typedef float vl_mser_acc ; /** @internal @brief Basic region flag: null region */ #ifdef VL_COMPILER_MSC #define VL_MSER_VOID_NODE ((1ui64<<32) - 1) #else #define VL_MSER_VOID_NODE ((1ULL<<32) - 1) #endif /* ----------------------------------------------------------------- */ /** @internal ** @brief MSER: basic region (declaration) ** ** Extremal regions and maximally stable extremal regions are ** instances of image regions. ** ** There is an image region for each pixel of the image. Each region ** is represented by an instance of this structure. Regions are ** stored into an array in pixel order. ** ** Regions are arranged into a forest. VlMserReg::parent points to ** the parent node, or to the node itself if the node is a root. ** VlMserReg::parent is the index of the node in the node array ** (which therefore is also the index of the corresponding ** pixel). VlMserReg::height is the distance of the fartest leaf. If ** the node itself is a leaf, then VlMserReg::height is zero. ** ** VlMserReg::area is the area of the image region corresponding to ** this node. ** ** VlMserReg::region is the extremal region identifier. Not all ** regions are extremal regions however; if the region is NOT ** extremal, this field is set to .... **/ struct _VlMserReg { vl_uint parent ; /**< points to the parent region. */ vl_uint shortcut ; /**< points to a region closer to a root. */ vl_uint height ; /**< region height in the forest. */ vl_uint area ; /**< area of the region. */ } ; /** @internal @brief MSER: basic region */ typedef struct _VlMserReg VlMserReg ; /* ----------------------------------------------------------------- */ /** @internal ** @brief MSER: extremal region (declaration) ** ** Extremal regions (ER) are extracted from the region forest. Each ** region is represented by an instance of this structure. The ** structures are stored into an array, in arbitrary order. ** ** ER are arranged into a tree. @a parent points to the parent ER, or ** to itself if the ER is the root. ** ** An instance of the structure represents the extremal region of the ** level set of intensity VlMserExtrReg::value and containing the ** pixel VlMserExtReg::index. ** ** VlMserExtrReg::area is the area of the extremal region and ** VlMserExtrReg::area_top is the area of the extremal region ** containing this region in the level set of intensity ** VlMserExtrReg::area + @c delta. ** ** VlMserExtrReg::variation is the relative area variation @c ** (area_top-area)/area. ** ** VlMserExtrReg::max_stable is a flag signaling whether this extremal ** region is also maximally stable. **/ struct _VlMserExtrReg { int parent ; /**< index of the parent region */ int index ; /**< index of pivot pixel */ vl_mser_pix value ; /**< value of pivot pixel */ vl_uint shortcut ; /**< shortcut used when building a tree */ vl_uint area ; /**< area of the region */ float variation ; /**< rel. area variation */ vl_uint max_stable ; /**< max stable number (=0 if not maxstable) */ } ; /** @internal ** @brief MSER: extremal region */ typedef struct _VlMserExtrReg VlMserExtrReg ; /* ----------------------------------------------------------------- */ /** @internal ** @brief MSER filter ** @see @ref mser **/ struct _VlMserFilt { /** @name Image data and meta data @internal */ /*@{*/ int ndims ; /**< number of dimensions */ int *dims ; /**< dimensions */ int nel ; /**< number of image elements (pixels) */ int *subs ; /**< N-dimensional subscript */ int *dsubs ; /**< another subscript */ int *strides ; /**< strides to move in image data */ /*@}*/ vl_uint *perm ; /**< pixel ordering */ vl_uint *joins ; /**< sequence of join ops */ int njoins ; /**< number of join ops */ /** @name Regions */ /*@{*/ VlMserReg *r ; /**< basic regions */ VlMserExtrReg *er ; /**< extremal tree */ vl_uint *mer ; /**< maximally stable extremal regions */ int ner ; /**< number of extremal regions */ int nmer ; /**< number of maximally stable extr. reg. */ int rer ; /**< size of er buffer */ int rmer ; /**< size of mer buffer */ /*@}*/ /** @name Ellipsoids fitting */ /*@{*/ float *acc ; /**< moment accumulator. */ float *ell ; /**< ellipsoids list. */ int rell ; /**< size of ell buffer */ int nell ; /**< number of ellipsoids extracted */ int dof ; /**< number of dof of ellipsoids. */ /*@}*/ /** @name Configuration */ /*@{*/ vl_bool verbose ; /**< be verbose */ int delta ; /**< delta filter parameter */ double max_area ; /**< badness test parameter */ double min_area ; /**< badness test parameter */ double max_variation ; /**< badness test parameter */ double min_diversity ; /**< minimum diversity */ /*@}*/ VlMserStats stats ; /** run statistic */ } ; /* ----------------------------------------------------------------- */ /** @brief Get delta ** @param f MSER filter. ** @return value of @c delta. **/ VL_INLINE vl_mser_pix vl_mser_get_delta (VlMserFilt const *f) { return f-> delta ; } /** @brief Set delta ** @param f MSER filter. ** @param x value of @c delta. **/ VL_INLINE void vl_mser_set_delta (VlMserFilt *f, vl_mser_pix x) { f-> delta = x ; } /* ----------------------------------------------------------------- */ /** @brief Get minimum diversity ** @param f MSER filter. ** @return value of @c minimum diversity. **/ VL_INLINE double vl_mser_get_min_diversity (VlMserFilt const *f) { return f-> min_diversity ; } /** @brief Set minimum diversity ** @param f MSER filter. ** @param x value of @c minimum diversity. **/ VL_INLINE void vl_mser_set_min_diversity (VlMserFilt *f, double x) { f-> min_diversity = x ; } /* ----------------------------------------------------------------- */ /** @brief Get statistics ** @param f MSER filter. ** @return statistics. **/ VL_INLINE VlMserStats const* vl_mser_get_stats (VlMserFilt const *f) { return & f-> stats ; } /* ----------------------------------------------------------------- */ /** @brief Get maximum region area ** @param f MSER filter. ** @return maximum region area. **/ VL_INLINE double vl_mser_get_max_area (VlMserFilt const *f) { return f-> max_area ; } /** @brief Set maximum region area ** @param f MSER filter. ** @param x maximum region area. **/ VL_INLINE void vl_mser_set_max_area (VlMserFilt *f, double x) { f-> max_area = x ; } /* ----------------------------------------------------------------- */ /** @brief Get minimum region area ** @param f MSER filter. ** @return minimum region area. **/ VL_INLINE double vl_mser_get_min_area (VlMserFilt const *f) { return f-> min_area ; } /** @brief Set minimum region area ** @param f MSER filter. ** @param x minimum region area. **/ VL_INLINE void vl_mser_set_min_area (VlMserFilt *f, double x) { f-> min_area = x ; } /* ----------------------------------------------------------------- */ /** @brief Get maximum region variation ** @param f MSER filter. ** @return maximum region variation. **/ VL_INLINE double vl_mser_get_max_variation (VlMserFilt const *f) { return f-> max_variation ; } /** @brief Set maximum region variation ** @param f MSER filter. ** @param x maximum region variation. **/ VL_INLINE void vl_mser_set_max_variation (VlMserFilt *f, double x) { f-> max_variation = x ; } /* ----------------------------------------------------------------- */ /** @brief Get maximally stable extremal regions ** @param f MSER filter. ** @return array of MSER pivots. **/ VL_INLINE vl_uint const * vl_mser_get_regions (VlMserFilt const* f) { return f-> mer ; } /** @brief Get number of maximally stable extremal regions ** @param f MSER filter. ** @return number of MSERs. **/ VL_INLINE vl_uint vl_mser_get_regions_num (VlMserFilt const* f) { return f-> nmer ; } /* ----------------------------------------------------------------- */ /** @brief Get ellipsoids ** @param f MSER filter. ** @return ellipsoids. **/ VL_INLINE float const * vl_mser_get_ell (VlMserFilt const* f) { return f-> ell ; } /** @brief Get number of degrees of freedom of ellipsoids ** @param f MSER filter. ** @return number of degrees of freedom. **/ VL_INLINE vl_uint vl_mser_get_ell_dof (VlMserFilt const* f) { return f-> dof ; } /** @brief Get number of ellipsoids ** @param f MSER filter. ** @return number of ellipsoids **/ VL_INLINE vl_uint vl_mser_get_ell_num (VlMserFilt const* f) { return f-> nell ; } /* VL_MSER */ #endif colmap-3.9.1/src/thirdparty/VLFeat/pgm.c000077500000000000000000000320351454702036400200430ustar00rootroot00000000000000/** @file pgm.c ** @brief Portable graymap format (PGM) parser - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. Copyright (C) 2013 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file pgm.h This module implements basic input and ouptut of images in PGM format. Extracting an image encoded in PGM format from an imput file stream involves the following steps: - use ::vl_pgm_extract_head to extract the image meta data (size and bit depth); - allocate a buffer to store the image data; - use ::vl_pgm_extract_data to extract the image data to the allocated buffer. Writing an image in PGM format to an ouptut file stream can be done by using ::vl_pgm_insert. To quickly read/write a PGM image from/to a given file, use ::vl_pgm_read_new() and ::vl_pgm_write(). To to the same from a buffer in floating point format use ::vl_pgm_read_new_f() and ::vl_pgm_write_f(). **/ #include "pgm.h" #include #include #include /** ------------------------------------------------------------------ ** @internal @brief Remove all characters to the next new-line. ** @param f file to strip. ** @return number of characters removed. **/ static int remove_line(FILE* f) { int count = 0 ; int c ; while (1) { c = fgetc(f) ; ++ count ; switch(c) { case '\n' : goto quit_remove_line ; case EOF : -- count ; goto quit_remove_line ; } } quit_remove_line : return count ; } /** ------------------------------------------------------------------ ** @internal @brief Remove white-spaces and comments. ** @param f file to strip. ** @return number of characters removed. **/ static int remove_blanks(FILE* f) { int count = 0 ; int c ; while (1) { c = fgetc(f) ; switch(c) { case '\t' : case '\n' : case '\r' : case ' ' : ++ count ; break ; case '#' : count += 1 + remove_line(f) ; break ; case EOF : goto quit_remove_blanks ; default: ungetc(c, f) ; goto quit_remove_blanks ; } } quit_remove_blanks: return count ; } /** ------------------------------------------------------------------ ** @brief Get PGM image number of pixels. ** @param im PGM image descriptor. ** @return number of pixels of the image. ** ** The functions returns the number of pixels of the PGM image @a im. ** ** To calculate the image data size in bytes, this value must be ** multiplied by the number of byte per pixels (see ** ::vl_pgm_get_bpp()). **/ VL_EXPORT vl_size vl_pgm_get_npixels (VlPgmImage const *im) { return im->width * im->height ; } /** ------------------------------------------------------------------ ** @brief Get PGM image bytes per pixel. ** @param im PGM image descriptor. ** @return number of bytes per pixel. ** ** The function returns the number of bytes for each pixel of the ** PGM image @a im. **/ VL_EXPORT vl_size vl_pgm_get_bpp (VlPgmImage const *im) { return (im->max_value >= 256) + 1 ; } /** ------------------------------------------------------------------ ** @brief Extract PGM header from stream. ** @param f input file. ** @param im image structure to fill. ** @return error code. ** ** The function extracts from the file @a f the meta-data section of ** an image encoded in PGM format. The function fills the structure ** ::VlPgmImage accordingly. ** ** The error may be either ::VL_ERR_PGM_INV_HEAD or ::VL_ERR_PGM_INV_META ** depending whether the error occurred in decoding the header or ** meta section of the PGM file. **/ VL_EXPORT int vl_pgm_extract_head (FILE* f, VlPgmImage *im) { char magic [2] ; int c ; int is_raw ; int width ; int height ; int max_value ; size_t sz ; vl_bool good ; /* ----------------------------------------------------------------- * check magic number * -------------------------------------------------------------- */ sz = fread(magic, 1, 2, f) ; if (sz < 2) { return vl_set_last_error(VL_ERR_PGM_INV_HEAD, "Invalid PGM header") ; } good = magic [0] == 'P' ; switch (magic [1]) { case '2' : /* ASCII format */ is_raw = 0 ; break ; case '5' : /* RAW format */ is_raw = 1 ; break ; default : good = 0 ; break ; } if( ! good ) { return vl_set_last_error(VL_ERR_PGM_INV_HEAD, "Invalid PGM header") ; } /* ----------------------------------------------------------------- * parse width, height, max_value * -------------------------------------------------------------- */ good = 1 ; c = remove_blanks(f) ; good &= c > 0 ; c = fscanf(f, "%d", &width) ; good &= c == 1 ; c = remove_blanks(f) ; good &= c > 0 ; c = fscanf(f, "%d", &height) ; good &= c == 1 ; c = remove_blanks(f) ; good &= c > 0 ; c = fscanf(f, "%d", &max_value) ; good &= c == 1 ; /* must end with a single blank */ c = fgetc(f) ; good &= c == '\n' || c == '\t' || c == ' ' || c == '\r' ; if(! good) { return vl_set_last_error(VL_ERR_PGM_INV_META, "Invalid PGM meta information"); } if(! max_value >= 65536) { return vl_set_last_error(VL_ERR_PGM_INV_META, "Invalid PGM meta information"); } /* exit */ im-> width = width ; im-> height = height ; im-> max_value = max_value ; im-> is_raw = is_raw ; return 0 ; } /** ------------------------------------------------------------------ ** @brief Extract PGM data from stream. ** @param f input file. ** @param im PGM image descriptor. ** @param data data buffer to fill. ** @return error code. ** ** The function extracts from the file @a f the data section of an ** image encoded in PGM format. The function fills the buffer @a data ** according. The buffer @a data should be ::vl_pgm_get_npixels() by ** ::vl_pgm_get_bpp() bytes large. **/ VL_EXPORT int vl_pgm_extract_data (FILE* f, VlPgmImage const *im, void *data) { vl_size bpp = vl_pgm_get_bpp(im) ; vl_size data_size = vl_pgm_get_npixels(im) ; vl_bool good = 1 ; size_t c ; /* ----------------------------------------------------------------- * read data * -------------------------------------------------------------- */ /* In RAW mode we read directly an array of bytes or shorts. In the latter case, however, we must take care of the endianess. PGM files are sorted in big-endian format. If our architecture is little endian, we must do a conversion. */ if (im->is_raw) { c = fread( data, bpp, data_size, f ) ; good = (c == data_size) ; /* adjust endianess */ #if defined(VL_ARCH_LITTLE_ENDIAN) if (bpp == 2) { vl_uindex i ; vl_uint8 *pt = (vl_uint8*) data ; for(i = 0 ; i < 2 * data_size ; i += 2) { vl_uint8 tmp = pt [i] ; pt [i] = pt [i+1] ; pt [i+1] = tmp ; } } #endif } /* In ASCII mode we read a sequence of decimal numbers separated by whitespaces. */ else { vl_uindex i ; int unsigned v ; for(good = 1, i = 0 ; i < data_size && good ; ++i) { c = fscanf(f, " %ud", &v) ; if (bpp == 1) { * ((vl_uint8* ) data + i) = (vl_uint8) v ; } else { * ((vl_uint16*) data + i) = (vl_uint16) v ; } good &= c == 1 ; } } if(! good ) { return vl_set_last_error(VL_ERR_PGM_INV_DATA, "Invalid PGM data") ; } return 0 ; } /** ------------------------------------------------------------------ ** @brief Insert a PGM image into a stream. ** @param f output file. ** @param im PGM image meta-data. ** @param data image data. ** @return error code. **/ VL_EXPORT int vl_pgm_insert(FILE* f, VlPgmImage const *im, void const *data) { vl_size bpp = vl_pgm_get_bpp (im) ; vl_size data_size = vl_pgm_get_npixels (im) ; size_t c ; /* write preamble */ fprintf(f, "P5\n%d\n%d\n%d\n", (signed)im->width, (signed)im->height, (signed)im->max_value) ; /* take care of endianness */ #if defined(VL_ARCH_LITTLE_ENDIAN) if (bpp == 2) { vl_uindex i ; vl_uint8* temp = vl_malloc (2 * data_size) ; memcpy(temp, data, 2 * data_size) ; for(i = 0 ; i < 2 * data_size ; i += 2) { vl_uint8 tmp = temp [i] ; temp [i] = temp [i+1] ; temp [i+1] = tmp ; } c = fwrite(temp, 2, data_size, f) ; vl_free (temp) ; } else { #endif c = fwrite(data, bpp, data_size, f) ; #if defined(VL_ARCH_LITTLE_ENDIAN) } #endif if(c != data_size) { return vl_set_last_error(VL_ERR_PGM_IO, "Error writing PGM data") ; } return 0 ; } /** ------------------------------------------------------------------ ** @brief Read a PGM file. ** @param name file name. ** @param im a pointer to the PGM image structure to fill. ** @param data a pointer to the pointer to the allocated buffer. ** @return error code. ** ** The function reads a PGM image from file @a name and initializes ** the structure @a im and the buffer @a data accordingly. ** ** The ownership of the buffer @a data is transfered to the caller. ** @a data should be freed by means of ::vl_free(). ** ** @bug Only PGM files with 1 BPP are supported. **/ VL_EXPORT int vl_pgm_read_new (char const *name, VlPgmImage *im, vl_uint8** data) { int err = 0 ; FILE *f = fopen (name, "rb") ; if (! f) { return vl_set_last_error(VL_ERR_PGM_IO, "Error opening PGM file `%s' for reading", name) ; } err = vl_pgm_extract_head(f, im) ; if (err) { fclose (f) ; return err ; } if (vl_pgm_get_bpp(im) > 1) { return vl_set_last_error(VL_ERR_BAD_ARG, "PGM with BPP > 1 not supported") ; } *data = vl_malloc (vl_pgm_get_npixels(im) * sizeof(vl_uint8)) ; err = vl_pgm_extract_data(f, im, *data) ; if (err) { vl_free (data) ; fclose (f) ; } fclose (f) ; return err ; } /** ------------------------------------------------------------------ ** @brief Read floats from a PGM file. ** @param name file name. ** @param im a pointer to the PGM image structure to fill. ** @param data a pointer to the pointer to the allocated buffer. ** @return error code. ** ** The function reads a PGM image from file @a name and initializes ** the structure @a im and the buffer @a data accordingly. The buffer ** @a data is an array of floats in the range [0, 1]. ** ** The ownership of the buffer @a data is transfered to the caller. ** @a data should be freed by means of ::vl_free(). ** ** @bug Only PGM files with 1 BPP are supported. **/ VL_EXPORT int vl_pgm_read_new_f (char const *name, VlPgmImage *im, float** data) { int err = 0 ; size_t npixels ; vl_uint8 *idata ; err = vl_pgm_read_new (name, im, &idata) ; if (err) { return err ; } npixels = vl_pgm_get_npixels(im) ; *data = vl_malloc (sizeof(float) * npixels) ; { size_t k ; float scale = 1.0f / (float)im->max_value ; for (k = 0 ; k < npixels ; ++ k) (*data)[k] = scale * idata[k] ; } vl_free (idata) ; return 0 ; } /** ------------------------------------------------------------------ ** @brief Write bytes to a PGM file. ** @param name file name. ** @param data data to write. ** @param width width of the image. ** @param height height of the image. ** @return error code. ** ** The function dumps the image @a data to the PGM file of the specified ** name. This is an helper function simplifying the usage of ** vl_pgm_insert(). **/ VL_EXPORT int vl_pgm_write (char const *name, vl_uint8 const* data, int width, int height) { int err = 0 ; VlPgmImage pgm ; FILE *f = fopen (name, "wb") ; if (! f) { return vl_set_last_error(VL_ERR_PGM_IO, "Error opening PGM file '%s' for writing", name) ; } pgm.width = width ; pgm.height = height ; pgm.is_raw = 1 ; pgm.max_value = 255 ; err = vl_pgm_insert (f, &pgm, data) ; fclose (f) ; return err ; } /** ------------------------------------------------------------------- ** @brief Write floats to PGM file ** @param name file name. ** @param data data to write. ** @param width width of the image. ** @param height height of the image. ** @return error code. ** ** The function dumps the image @a data to the PGM file of the ** specified name. The data is re-scaled to fit in the range 0-255. ** This is an helper function simplifying the usage of ** vl_pgm_insert(). **/ VL_EXPORT int vl_pgm_write_f (char const *name, float const* data, int width, int height) { int err = 0 ; int k ; float min = + VL_INFINITY_F ; float max = - VL_INFINITY_F ; float scale ; vl_uint8 * buffer = vl_malloc (sizeof(float) * width * height) ; for (k = 0 ; k < width * height ; ++k) { min = VL_MIN(min, data [k]) ; max = VL_MAX(max, data [k]) ; } scale = 255 / (max - min + VL_EPSILON_F) ; for (k = 0 ; k < width * height ; ++k) { buffer [k] = (vl_uint8) ((data [k] - min) * scale) ; } err = vl_pgm_write (name, buffer, width, height) ; vl_free (buffer) ; return err ; } colmap-3.9.1/src/thirdparty/VLFeat/pgm.h000066400000000000000000000044641454702036400200520ustar00rootroot00000000000000/** @file pgm.h ** @brief Portable graymap format (PGM) parser ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_PGM_H #define VL_PGM_H #include "generic.h" #include "mathop.h" #include /** @name PGM parser error codes ** @{ */ #define VL_ERR_PGM_INV_HEAD 101 /**< Invalid PGM header section. */ #define VL_ERR_PGM_INV_META 102 /**< Invalid PGM meta section. */ #define VL_ERR_PGM_INV_DATA 103 /**< Invalid PGM data section.*/ #define VL_ERR_PGM_IO 104 /**< Generic I/O error. */ /** @} */ /** @brief PGM image meta data ** ** A PGM image is a 2-D array of pixels of width #width and height ** #height. Each pixel is an integer one or two bytes wide, depending ** whether #max_value is smaller than 256. **/ typedef struct _VlPgmImage { vl_size width ; /**< image width. */ vl_size height ; /**< image height. */ vl_size max_value ; /**< pixel maximum value (<= 2^16-1). */ vl_bool is_raw ; /**< is RAW format? */ } VlPgmImage ; /** @name Core operations ** @{ */ VL_EXPORT int vl_pgm_extract_head (FILE *f, VlPgmImage *im) ; VL_EXPORT int vl_pgm_extract_data (FILE *f, VlPgmImage const *im, void *data) ; VL_EXPORT int vl_pgm_insert (FILE *f, VlPgmImage const *im, void const*data ) ; VL_EXPORT vl_size vl_pgm_get_npixels (VlPgmImage const *im) ; VL_EXPORT vl_size vl_pgm_get_bpp (VlPgmImage const *im) ; /** @} */ /** @name Helper functions ** @{ */ VL_EXPORT int vl_pgm_write (char const *name, vl_uint8 const *data, int width, int height) ; VL_EXPORT int vl_pgm_write_f (char const *name, float const *data, int width, int height) ; VL_EXPORT int vl_pgm_read_new (char const *name, VlPgmImage *im, vl_uint8 **data) ; VL_EXPORT int vl_pgm_read_new_f (char const *name, VlPgmImage *im, float **data) ; /** @} */ /* VL_PGM_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/qsort-def.h000066400000000000000000000120041454702036400211600ustar00rootroot00000000000000/** @file qsort-def.h ** @brief QSort preprocessor metaprogram ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file qsort-def.h @section qsort-def-overview Overview @ref qsort-def.h is a metaprogram to define specialized instances of the quick-sort algorithm. @section qsort-def-usage Usage @ref qsort-def.h is used to define a specialization of the ::VL_QSORT_sort function that operates on a given type of array. For instance the code @code #define VL_QSORT_type float #define VL_QSORT_prefix my_qsort #include @endcode defines a function @c my_qsort_sort that operates on an array of floats. @todo large array compatibility. **/ #include "host.h" #include #ifndef VL_QSORT_prefix #error "VL_QSORT_prefix must be defined" #endif #ifndef VL_QSORT_array #ifndef VL_QSORT_type #error "VL_QSORT_type must be defined if VL_QSORT_array is not" #endif #define VL_QSORT_array VL_QSORT_type* #define VL_QSORT_array_const VL_QSORT_type const* #endif #ifdef __DOXYGEN__ #define VL_QSORT_prefix QSortPrefix /**< Prefix of the qsort functions */ #define VL_QSORT_type QSortType /**< Data type of the qsort elements */ #define VL_QSORT_array QSortType* /**< Data type of the qsort container */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_QSORT_cmp) || defined(__DOXYGEN__) #define VL_QSORT_cmp VL_XCAT(VL_QSORT_prefix, _cmp) /** @brief Compare two array elements ** @param array qsort array. ** @param indexA index of the first element @c A to compare. ** @param indexB index of the second element @c B to comapre. ** @return a negative number if @c AB. **/ VL_INLINE VL_QSORT_type VL_QSORT_cmp (VL_QSORT_array_const array, vl_uindex indexA, vl_uindex indexB) { return array[indexA] - array[indexB] ; } /* VL_QSORT_cmp */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_QSORT_swap) || defined(__DOXYGEN__) #define VL_QSORT_swap VL_XCAT(VL_QSORT_prefix, _swap) /** @brief Swap two array elements ** @param array qsort array. ** @param indexA index of the first element to swap. ** @param indexB index of the second element to swap. ** ** The function swaps the two elements @a a and @ b. The function ** uses a temporary element of type ::VL_QSORT_type ** and the copy operator @c =. **/ VL_INLINE void VL_QSORT_swap (VL_QSORT_array array, vl_uindex indexA, vl_uindex indexB) { VL_QSORT_type t = array [indexA] ; array [indexA] = array [indexB] ; array [indexB] = t ; } /* VL_QSORT_swap */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_QSORT_sort_recursive) || defined(__DOXYGEN__) #define VL_QSORT_sort_recursive VL_XCAT(VL_QSORT_prefix, _sort_recursive) /** @brief Sort portion of an array using quicksort ** @param array (in/out) pointer to the array. ** @param begin first element of the array portion. ** @param end last element of the array portion. ** ** The function sorts the array using quick-sort. Note that ** @c begin must be not larger than @c end. **/ VL_INLINE void VL_QSORT_sort_recursive (VL_QSORT_array array, vl_uindex begin, vl_uindex end) { vl_uindex pivot = (end + begin) / 2 ; vl_uindex lowPart, i ; assert (begin <= end) ; /* swap pivot with last */ VL_QSORT_swap (array, pivot, end) ; pivot = end ; /* Now scan from left to right, moving all element smaller or equal than the pivot to the low part array[0], array[1], ..., array[lowPart - 1]. */ lowPart = begin ; for (i = begin; i < end ; ++i) { /* one less */ if (VL_QSORT_cmp (array, i, pivot) <= 0) { /* array[i] must be moved into the low part */ VL_QSORT_swap (array, lowPart, i) ; lowPart ++ ; } } /* the pivot should also go into the low part */ VL_QSORT_swap (array, lowPart, pivot) ; pivot = lowPart ; /* do recursion */ if (pivot > begin) { /* note that pivot-1 stays non-negative */ VL_QSORT_sort_recursive (array, begin, pivot - 1) ; } if (pivot < end) { VL_QSORT_sort_recursive (array, pivot + 1, end) ; } } /* VL_QSORT_sort_recursive */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_QSORT_sort) || defined(__DOXYGEN__) #define VL_QSORT_sort VL_XCAT(VL_QSORT_prefix, _sort) /** @brief Sort array using quicksort ** @param array (in/out) pointer to the array. ** @param size size of the array. ** ** The function sorts the array using quick-sort. **/ VL_INLINE void VL_QSORT_sort (VL_QSORT_array array, vl_size size) { assert (size >= 1) ; VL_QSORT_sort_recursive (array, 0, size - 1) ; } /* VL_QSORT_qsort */ #endif #undef VL_QSORT_prefix #undef VL_QSORT_swap #undef VL_QSORT_sort #undef VL_QSORT_sort_recursive #undef VL_QSORT_type #undef VL_QSORT_array #undef VL_QSORT_cmp colmap-3.9.1/src/thirdparty/VLFeat/quickshift.c000077500000000000000000000343631454702036400214400ustar00rootroot00000000000000/** @file quickshift.c ** @brief Quick shift - Definition ** @author Brian Fulkerson ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page quickshift Quick shift image segmentation @author Brian Fulkerson @author Andrea Vedaldi @ref quickshift.h implements an image segmentation algorithm based on the quick shift clustering algorithm @cite{vedaldi08quick}. - @ref quickshift-intro - @ref quickshift-usage - @ref quickshift-tech @section quickshift-intro Overview Quick shift @cite{vedaldi08quick} is a fast mode seeking algorithm, similar to mean shift. The algorithm segments an RGB image (or any image with more than one channel) by identifying clusters of pixels in the joint spatial and color dimensions. Segments are local (superpixels) and can be used as a basis for further processing. Given an image, the algorithm calculates a forest of pixels whose branches are labeled with a distance value (::vl_quickshift_get_parents, ::vl_quickshift_get_dists). This specifies a hierarchical segmentation of the image, with segments corresponding to subtrees. Useful superpixels can be identified by cutting the branches whose distance label is above a given threshold (the threshold can be either fixed by hand, or determined by cross validation). Parameter influencing the algorithm are: - Kernel size. The pixel density and its modes are estimated by using a Parzen window estimator with a Gaussian kernel of the specified size (::vl_quickshift_set_kernel_size). The larger the size, the larger the neighborhoods of pixels considered. - Maximum distance. This (::vl_quickshift_set_max_dist) is the maximum distance between two pixels that the algorithm considers when building the forest. In principle, it can be infinity (so that a tree is returned), but in practice it is much faster to consider only relatively small distances (the maximum distance can be set to a small multiple of the kernel size). @section quickshift-usage Usage - Create a new quick shift object (::vl_quickshift_new). The object can be reused for multiple images of the same size. - Configure quick shift by setting the kernel size (::vl_quickshift_set_kernel_size) and the maximum gap (::vl_quickshift_set_max_dist). The latter is in principle not necessary, but useful to speedup processing. - Process an image (::vl_quickshift_process). - Retrieve the parents (::vl_quickshift_get_parents) and the distances (::vl_quickshift_get_dists). These can be used to segment the image in superpixels. - Delete the quick shift object (::vl_quickshift_delete). @section quickshift-tech Technical details For each pixel (x,y), quick shift regards @f$ (x,y,I(x,y)) @f$ as a sample from a d + 2 dimensional vector space. It then calculates the Parzen density estimate (with a Gaussian kernel of standard deviation @f$ \sigma @f$) @f[ E(x,y) = P(x,y,I(x,y)) = \sum_{x'y'} \frac{1}{(2\pi\sigma)^{d+2}} \exp \left( -\frac{1}{2\sigma^2} \left[ \begin{array}{c} x - x' \\ y - y' \\ I(x,y) - I(x',y') \\ \end{array} \right] \right) @f] Then quick shift construct a tree connecting each image pixel to its nearest neighbor which has greater density value. Formally, write @f$ (x',y') >_P (x,y) @f$ if, and only if, @f[ P(x',y',I(x',y')) > P(x,y,I(x,y))}. @f] Each pixel (x, y) is connected to the closest higher density pixel parent(x, y) that achieves the minimum distance in @f[ \mathrm{dist}(x,y) = \mathrm{min}_{(x',y') > P(x,y)} \left( (x - x')^2 + (y - y')^2 + \| I(x,y) - I(x',y') \|_2^2 \right). @f] **/ #include "quickshift.h" #include "mathop.h" #include #include #include /** ----------------------------------------------------------------- ** @internal ** @brief Computes the accumulated channel L2 distance between ** i,j + the distance between i,j ** ** @param I input image buffer ** @param N1 size of the first dimension of the image ** @param N2 size of the second dimension of the image ** @param K number of channels ** @param i1 first dimension index of the first pixel to compare ** @param i2 second dimension of the first pixel ** @param j1 index of the second pixel to compare ** @param j2 second dimension of the second pixel ** ** Takes the L2 distance between the values in I at pixel i and j, ** accumulating along K channels and adding in the distance ** between i,j in the image. ** ** @return the distance as described above **/ VL_INLINE vl_qs_type vl_quickshift_distance(vl_qs_type const * I, int N1, int N2, int K, int i1, int i2, int j1, int j2) { vl_qs_type dist = 0 ; int d1 = j1 - i1 ; int d2 = j2 - i2 ; int k ; dist += d1*d1 + d2*d2 ; /* For k = 0...K-1, d+= L2 distance between I(i1,i2,k) and * I(j1,j2,k) */ for (k = 0 ; k < K ; ++k) { vl_qs_type d = I [i1 + N1 * i2 + (N1*N2) * k] - I [j1 + N1 * j2 + (N1*N2) * k] ; dist += d*d ; } return dist ; } /** ----------------------------------------------------------------- ** @internal ** @brief Computes the accumulated channel inner product between i,j + the ** distance between i,j ** ** @param I input image buffer ** @param N1 size of the first dimension of the image ** @param N2 size of the second dimension of the image ** @param K number of channels ** @param i1 first dimension index of the first pixel to compare ** @param i2 second dimension of the first pixel ** @param j1 index of the second pixel to compare ** @param j2 second dimension of the second pixel ** ** Takes the channel-wise inner product between the values in I at ** pixel i and j, accumulating along K channels and adding in the ** inner product between i,j in the image. ** ** @return the inner product as described above **/ VL_INLINE vl_qs_type vl_quickshift_inner(vl_qs_type const * I, int N1, int N2, int K, int i1, int i2, int j1, int j2) { vl_qs_type ker = 0 ; int k ; ker += i1*j1 + i2*j2 ; for (k = 0 ; k < K ; ++k) { ker += I [i1 + N1 * i2 + (N1*N2) * k] * I [j1 + N1 * j2 + (N1*N2) * k] ; } return ker ; } /** ----------------------------------------------------------------- ** @brief Create a quick shift object ** @param image the image. ** @param height the height (number of rows) of the image. ** @param width the width (number of columns) of the image. ** @param channels the number of channels of the image. ** @return new quick shift object. ** ** The @c image is an array of ::vl_qs_type values with three ** dimensions (respectively @c widht, @c height, and @c ** channels). Typically, a color (e.g, RGB) image has three ** channels. The linear index of a pixel is computed with: ** @c channels * @c width* @c height + @c row + @c height * @c col. **/ VL_EXPORT VlQS * vl_quickshift_new(vl_qs_type const * image, int height, int width, int channels) { VlQS * q = vl_malloc(sizeof(VlQS)); q->image = (vl_qs_type *)image; q->height = height; q->width = width; q->channels = channels; q->medoid = VL_FALSE; q->tau = VL_MAX(height,width)/50; q->sigma = VL_MAX(2, q->tau/3); q->dists = vl_calloc(height*width, sizeof(vl_qs_type)); q->parents = vl_calloc(height*width, sizeof(int)); q->density = vl_calloc(height*width, sizeof(vl_qs_type)) ; return q; } /** ----------------------------------------------------------------- ** @brief Create a quick shift objet ** @param q quick shift object. **/ VL_EXPORT void vl_quickshift_process(VlQS * q) { vl_qs_type const *I = q->image; int *parents = q->parents; vl_qs_type *E = q->density; vl_qs_type *dists = q->dists; vl_qs_type *M = 0, *n = 0 ; vl_qs_type sigma = q->sigma ; vl_qs_type tau = q->tau; vl_qs_type tau2 = tau*tau; int K = q->channels, d; int N1 = q->height, N2 = q->width; int i1,i2, j1,j2, R, tR; d = 2 + K ; /* Total dimensions include spatial component (x,y) */ if (q->medoid) { /* n and M are only used in mediod shift */ M = (vl_qs_type *) vl_calloc(N1*N2*d, sizeof(vl_qs_type)) ; n = (vl_qs_type *) vl_calloc(N1*N2, sizeof(vl_qs_type)) ; } R = (int) ceil (3 * sigma) ; tR = (int) ceil (tau) ; /* ----------------------------------------------------------------- * n * -------------------------------------------------------------- */ /* If we are doing medoid shift, initialize n to the inner product of the * image with itself */ if (n) { for (i2 = 0 ; i2 < N2 ; ++ i2) { for (i1 = 0 ; i1 < N1 ; ++ i1) { n [i1 + N1 * i2] = vl_quickshift_inner(I,N1,N2,K, i1,i2, i1,i2) ; } } } /* ----------------------------------------------------------------- * E = - [oN'*F]', M * -------------------------------------------------------------- */ /* D_ij = d(x_i,x_j) E_ij = exp(- .5 * D_ij / sigma^2) ; F_ij = - E_ij E_i = sum_j E_ij M_di = sum_j X_j F_ij E is the parzen window estimate of the density 0 = dissimilar to everything, windowsize = identical */ for (i2 = 0 ; i2 < N2 ; ++ i2) { for (i1 = 0 ; i1 < N1 ; ++ i1) { int j1min = VL_MAX(i1 - R, 0 ) ; int j1max = VL_MIN(i1 + R, N1-1) ; int j2min = VL_MAX(i2 - R, 0 ) ; int j2max = VL_MIN(i2 + R, N2-1) ; /* For each pixel in the window compute the distance between it and the * source pixel */ for (j2 = j2min ; j2 <= j2max ; ++ j2) { for (j1 = j1min ; j1 <= j1max ; ++ j1) { vl_qs_type Dij = vl_quickshift_distance(I,N1,N2,K, i1,i2, j1,j2) ; /* Make distance a similarity */ vl_qs_type Fij = - exp(- Dij / (2*sigma*sigma)) ; /* E is E_i above */ E [i1 + N1 * i2] -= Fij ; if (M) { /* Accumulate votes for the median */ int k ; M [i1 + N1*i2 + (N1*N2) * 0] += j1 * Fij ; M [i1 + N1*i2 + (N1*N2) * 1] += j2 * Fij ; for (k = 0 ; k < K ; ++k) { M [i1 + N1*i2 + (N1*N2) * (k+2)] += I [j1 + N1*j2 + (N1*N2) * k] * Fij ; } } } /* j1 */ } /* j2 */ } /* i1 */ } /* i2 */ /* ----------------------------------------------------------------- * Find best neighbors * -------------------------------------------------------------- */ if (q->medoid) { /* Qij = - nj Ei - 2 sum_k Gjk Mik n is I.^2 */ /* medoid shift */ for (i2 = 0 ; i2 < N2 ; ++i2) { for (i1 = 0 ; i1 < N1 ; ++i1) { vl_qs_type sc_best = 0 ; /* j1/j2 best are the best indicies for each i */ vl_qs_type j1_best = i1 ; vl_qs_type j2_best = i2 ; int j1min = VL_MAX(i1 - R, 0 ) ; int j1max = VL_MIN(i1 + R, N1-1) ; int j2min = VL_MAX(i2 - R, 0 ) ; int j2max = VL_MIN(i2 + R, N2-1) ; for (j2 = j2min ; j2 <= j2max ; ++ j2) { for (j1 = j1min ; j1 <= j1max ; ++ j1) { vl_qs_type Qij = - n [j1 + j2 * N1] * E [i1 + i2 * N1] ; int k ; Qij -= 2 * j1 * M [i1 + i2 * N1 + (N1*N2) * 0] ; Qij -= 2 * j2 * M [i1 + i2 * N1 + (N1*N2) * 1] ; for (k = 0 ; k < K ; ++k) { Qij -= 2 * I [j1 + j2 * N1 + (N1*N2) * k] * M [i1 + i2 * N1 + (N1*N2) * (k + 2)] ; } if (Qij > sc_best) { sc_best = Qij ; j1_best = j1 ; j2_best = j2 ; } } } /* parents_i is the linear index of j which is the best pair * dists_i is the score of the best match */ parents [i1 + N1 * i2] = j1_best + N1 * j2_best ; dists[i1 + N1 * i2] = sc_best ; } } } else { /* Quickshift assigns each i to the closest j which has an increase in the * density (E). If there is no j s.t. Ej > Ei, then dists_i == inf (a root * node in one of the trees of merges). */ for (i2 = 0 ; i2 < N2 ; ++i2) { for (i1 = 0 ; i1 < N1 ; ++i1) { vl_qs_type E0 = E [i1 + N1 * i2] ; vl_qs_type d_best = VL_QS_INF ; vl_qs_type j1_best = i1 ; vl_qs_type j2_best = i2 ; int j1min = VL_MAX(i1 - tR, 0 ) ; int j1max = VL_MIN(i1 + tR, N1-1) ; int j2min = VL_MAX(i2 - tR, 0 ) ; int j2max = VL_MIN(i2 + tR, N2-1) ; for (j2 = j2min ; j2 <= j2max ; ++ j2) { for (j1 = j1min ; j1 <= j1max ; ++ j1) { if (E [j1 + N1 * j2] > E0) { vl_qs_type Dij = vl_quickshift_distance(I,N1,N2,K, i1,i2, j1,j2) ; if (Dij <= tau2 && Dij < d_best) { d_best = Dij ; j1_best = j1 ; j2_best = j2 ; } } } } /* parents is the index of the best pair */ /* dists_i is the minimal distance, inf implies no Ej > Ei within * distance tau from the point */ parents [i1 + N1 * i2] = j1_best + N1 * j2_best ; dists[i1 + N1 * i2] = sqrt(d_best) ; } } } if (M) vl_free(M) ; if (n) vl_free(n) ; } /** ----------------------------------------------------------------- ** @brief Delete quick shift object ** @param q quick shift object. **/ void vl_quickshift_delete(VlQS * q) { if (q) { if (q->parents) vl_free(q->parents); if (q->dists) vl_free(q->dists); if (q->density) vl_free(q->density); vl_free(q); } } colmap-3.9.1/src/thirdparty/VLFeat/quickshift.h000066400000000000000000000125571454702036400214430ustar00rootroot00000000000000/** @file quickshift.h ** @brief Quick shift (@ref quickshift) ** @author Andrea Vedaldi ** @author Brian Fulkerson **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_QUICKSHIFT_H #define VL_QUICKSHIFT_H #include "generic.h" #include "mathop.h" /** @brief quick shift datatype */ typedef double vl_qs_type ; /** @brief quick shift infinity constant */ #define VL_QS_INF VL_INFINITY_D /* Change to _F for float math */ /** ------------------------------------------------------------------ ** @brief quick shift results ** ** This implements quick shift mode seeking. **/ typedef struct _VlQS { vl_qs_type *image ; /**< height x width x channels feature image */ int height; /**< height of the image */ int width; /**< width of the image */ int channels; /**< number of channels in the image */ vl_bool medoid; vl_qs_type sigma; vl_qs_type tau; int *parents ; vl_qs_type *dists ; vl_qs_type *density ; } VlQS ; /** @name Create and destroy ** @{ **/ VL_EXPORT VlQS* vl_quickshift_new (vl_qs_type const * im, int height, int width, int channels); VL_EXPORT void vl_quickshift_delete (VlQS *q) ; /** @} */ /** @name Process data ** @{ **/ VL_EXPORT void vl_quickshift_process (VlQS *q) ; /** @} */ /** @name Retrieve data and parameters ** @{ **/ VL_INLINE vl_qs_type vl_quickshift_get_max_dist (VlQS const *q) ; VL_INLINE vl_qs_type vl_quickshift_get_kernel_size (VlQS const *q) ; VL_INLINE vl_bool vl_quickshift_get_medoid (VlQS const *q) ; VL_INLINE int * vl_quickshift_get_parents (VlQS const *q) ; VL_INLINE vl_qs_type * vl_quickshift_get_dists (VlQS const *q) ; VL_INLINE vl_qs_type * vl_quickshift_get_density (VlQS const *q) ; /** @} */ /** @name Set parameters ** @{ **/ VL_INLINE void vl_quickshift_set_max_dist (VlQS *f, vl_qs_type tau) ; VL_INLINE void vl_quickshift_set_kernel_size (VlQS *f, vl_qs_type sigma) ; VL_INLINE void vl_quickshift_set_medoid (VlQS *f, vl_bool medoid) ; /** @} */ /* ------------------------------------------------------------------- * Inline functions implementation * ---------------------------------------------------------------- */ /** ------------------------------------------------------------------ ** @brief Get tau. ** @param q quick shift object. ** @return the maximum distance in the feature space between nodes in the ** quick shift tree. **/ VL_INLINE vl_qs_type vl_quickshift_get_max_dist (VlQS const *q) { return q->tau ; } /** ------------------------------------------------------------------ ** @brief Get sigma. ** @param q quick shift object. ** @return the standard deviation of the kernel used in the Parzen density ** estimate. **/ VL_INLINE vl_qs_type vl_quickshift_get_kernel_size (VlQS const *q) { return q->sigma ; } /** ------------------------------------------------------------------ ** @brief Get medoid. ** @param q quick Shift object. ** @return @c true if medoid shift is used instead of quick shift. **/ VL_INLINE vl_bool vl_quickshift_get_medoid (VlQS const *q) { return q->medoid ; } /** ------------------------------------------------------------------ ** @brief Get parents. ** @param q quick shift object. ** @return a @c height x @c width matrix where each element contains the ** linear index of its parent node. The node is a root if its ** value is its own linear index. **/ VL_INLINE int * vl_quickshift_get_parents (VlQS const *q) { return q->parents ; } /** ------------------------------------------------------------------ ** @brief Get dists. ** @param q quick shift object. ** @return for each pixel, the distance in feature space to the pixel ** that is its parent in the quick shift tree. The distance is ** set to 'inf' if the pixel is a root node. **/ VL_INLINE vl_qs_type * vl_quickshift_get_dists (VlQS const *q) { return q->dists ; } /** ------------------------------------------------------------------ ** @brief Get density. ** @param q quick shift object. ** @return the estimate of the density at each pixel. **/ VL_INLINE vl_qs_type * vl_quickshift_get_density (VlQS const *q) { return q->density ; } /** ------------------------------------------------------------------ ** @brief Set sigma ** @param q quick shift object. ** @param sigma standard deviation of the kernel used in the Parzen density ** estimate. **/ VL_INLINE void vl_quickshift_set_kernel_size (VlQS *q, vl_qs_type sigma) { q -> sigma = sigma ; } /** ------------------------------------------------------------------ ** @brief Set max distance ** @param q quick shift object. ** @param tau the maximum distance in the feature space between nodes in the ** quick shift tree. **/ VL_INLINE void vl_quickshift_set_max_dist (VlQS *q, vl_qs_type tau) { q -> tau = tau ; } /** ------------------------------------------------------------------ ** @brief Set medoid ** @param q quick shift object. ** @param medoid @c true to use kernelized medoid shift, @c false (default) uses ** quick shift. **/ VL_INLINE void vl_quickshift_set_medoid (VlQS *q, vl_bool medoid) { q -> medoid = medoid ; } #endif colmap-3.9.1/src/thirdparty/VLFeat/random.c000077500000000000000000000175671454702036400205550ustar00rootroot00000000000000/** @file random.c ** @brief Random number generator - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. Copyright (C) 2013 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page random Random number generator @author Andrea Vedaldi @tableofcontents The module @ref random.h implements random number generation in VLFeat. The generator is based on the popular Mersenne Twister algorithm @cite{matsumoto98mersenne} (which is the same as MATLAB random generator from MATLAB version 7.4 onwards). @section random-starting Getting started In VLFeat, a random number generator is implemented by an object of type ::VlRand. The simplest way to obtain such an object is to get the default random generator by @code VlRand * rand = vl_get_rand() ; vl_int32 signedRandomUniformInteger = vl_rand_int31(rand) ; @code Note that there is one such generator per thread (see ::vl_get_rand). If more control is desired, a new ::VlRand object can be easily created. The object is lightweight, designed to be allocated on the stack: @code VlRand rand ; vl_rand_init (&rand) ; @endcode The generator can be seeded by ::vl_rand_seed and ::vl_rand_seed_by_array. For instance: @code vl_rand_seed (&rand, clock()) ; @endcode The generator can be used to obtain random quantities of various types: - ::vl_rand_int31, ::vl_rand_uint32 for 32-bit random integers; - ::vl_rand_real1 for a double in [0,1]; - ::vl_rand_real2 for a double in [0,1); - ::vl_rand_real3 for a double in (0,1); - ::vl_rand_res53 for a double in [0,1) with high resolution. There is no need to explicitly destroy a ::VlRand instance. **/ #include "random.h" /* A C-program for MT19937, with initialization improved 2002/1/26. Coded by Takuji Nishimura and Makoto Matsumoto. Before using, initialize the state by using init_genrand(seed) or init_by_array(init_key, keySize). Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names of its contributors may not 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 OWNER 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. Any feedback is very welcome. http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) */ #include #include /* Period parameters */ #define N 624 #define M 397 #define MATRIX_A VL_UINT32_C(0x9908b0df) /* constant vector a */ #define UPPER_MASK VL_UINT32_C(0x80000000) /* most asignificant w-r bits */ #define LOWER_MASK VL_UINT32_C(0x7fffffff) /* least significant r bits */ /* initializes mt[N] with a seed */ /** @brief Initialise random number generator ** @param self number generator. **/ void vl_rand_init (VlRand * self) { memset (self->mt, 0, sizeof(self->mt[0]) * N) ; self->mti = N + 1 ; } /** @brief Seed the state of the random number generator ** @param self random number generator. ** @param s seed. **/ void vl_rand_seed (VlRand * self, vl_uint32 s) { #define mti self->mti #define mt self->mt mt[0]= s & VL_UINT32_C(0xffffffff); for (mti=1; mti> 30)) + mti); /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */ /* In the previous versions, MSBs of the seed affect */ /* only MSBs of the array mt[]. */ /* 2002/01/09 modified by Makoto Matsumoto */ mt[mti] &= VL_UINT32_C(0xffffffff); /* for >32 bit machines */ } #undef mti #undef mt } /** @brief Seed the state of the random number generator by an array ** @param self random number generator. ** @param key array of numbers. ** @param keySize length of the array. **/ void vl_rand_seed_by_array (VlRand * self, vl_uint32 const key [], vl_size keySize) { #define mti self->mti #define mt self->mt int i, j, k; vl_rand_seed (self, VL_UINT32_C(19650218)); i=1; j=0; k = (N > keySize ? N : (int)keySize); for (; k; k--) { mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * VL_UINT32_C(1664525))) + key[j] + j; /* non linear */ mt[i] &= VL_UINT32_C(0xffffffff); /* for WORDSIZE > 32 machines */ i++; j++; if (i>=N) { mt[0] = mt[N-1]; i=1; } if (j>=(signed)keySize) j=0; } for (k=N-1; k; k--) { mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * VL_UINT32_C(1566083941))) - i; /* non linear */ mt[i] &= VL_UINT32_C(0xffffffff) ; /* for WORDSIZE > 32 machines */ i++; if (i>=N) { mt[0] = mt[N-1]; i=1; } } mt[0] = VL_UINT32_C(0x80000000); /* MSB is 1; assuring non-zero initial array */ #undef mti #undef mt } /** @brief Randomly permute and array of indexes. ** @param self random number generator. ** @param array array of indexes. ** @param size number of element in the array. ** ** The function uses *Algorithm P*, also known as *Knuth shuffle*. **/ void vl_rand_permute_indexes (VlRand *self, vl_index *array, vl_size size) { vl_index i, j, tmp; for (i = size - 1 ; i > 0; i--) { /* Pick a random index j in the range 0, i + 1 and swap it with i */ j = (vl_int) vl_rand_uindex (self, i + 1) ; tmp = array[i] ; array[i] = array[j] ; array[j] = tmp ; } } /** @brief Generate a random UINT32 ** @param self random number generator. ** @return a random number in [0, 0xffffffff]. **/ vl_uint32 vl_rand_uint32 (VlRand * self) { vl_uint32 y; static vl_uint32 mag01[2]={VL_UINT32_C(0x0), MATRIX_A}; /* mag01[x] = x * MATRIX_A for x=0,1 */ #define mti self->mti #define mt self->mt if (mti >= N) { /* generate N words at one time */ int kk; if (mti == N+1) /* if init_genrand() has not been called, */ vl_rand_seed (self, VL_UINT32_C(5489)); /* a default initial seed is used */ for (kk=0;kk> 1) ^ mag01[y & VL_UINT32_C(0x1)]; } for (;kk> 1) ^ mag01[y & VL_UINT32_C(0x1)]; } y = (mt[N-1]&UPPER_MASK)|(mt[0]&LOWER_MASK); mt[N-1] = mt[M-1] ^ (y >> 1) ^ mag01[y & VL_UINT32_C(0x1)]; mti = 0; } y = mt[mti++]; /* Tempering */ y ^= (y >> 11); y ^= (y << 7) & VL_UINT32_C(0x9d2c5680); y ^= (y << 15) & VL_UINT32_C(0xefc60000); y ^= (y >> 18); return (vl_uint32)y; #undef mti #undef mt } colmap-3.9.1/src/thirdparty/VLFeat/random.h000066400000000000000000000077701454702036400205520ustar00rootroot00000000000000/** @file random.h ** @brief Random number generator (@ref random) ** @author Andrea Vedaldi ** @see @ref random **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_RANDOM_H #define VL_RANDOM_H #include "host.h" /** @brief Random numbber generator state */ typedef struct _VlRand { vl_uint32 mt [624] ; vl_uint32 mti ; } VlRand ; /** @name Setting and reading the state ** ** @{ */ VL_EXPORT void vl_rand_init (VlRand * self) ; VL_EXPORT void vl_rand_seed (VlRand * self, vl_uint32 s) ; VL_EXPORT void vl_rand_seed_by_array (VlRand * self, vl_uint32 const key [], vl_size keySize) ; /** @} */ /** @name Generate random numbers ** ** @{ */ VL_INLINE vl_uint64 vl_rand_uint64 (VlRand * self) ; VL_INLINE vl_int64 vl_rand_int63 (VlRand * self) ; VL_EXPORT vl_uint32 vl_rand_uint32 (VlRand * self) ; VL_INLINE vl_int32 vl_rand_int31 (VlRand * self) ; VL_INLINE double vl_rand_real1 (VlRand * self) ; VL_INLINE double vl_rand_real2 (VlRand * self) ; VL_INLINE double vl_rand_real3 (VlRand * self) ; VL_INLINE double vl_rand_res53 (VlRand * self) ; VL_INLINE vl_uindex vl_rand_uindex (VlRand * self, vl_uindex range) ; /** @} */ VL_EXPORT void vl_rand_permute_indexes (VlRand * self, vl_index* array, vl_size size) ; /* ---------------------------------------------------------------- */ /** @brief Generate a random index in a given range ** @param self random number generator. ** @param range range. ** @return an index sampled uniformly at random in the interval [0, @c range - 1] ** ** @remark Currently, this function uses a simple algorithm that ** may yield slightly biased samples if @c range is not a power of ** two. **/ VL_INLINE vl_uindex vl_rand_uindex (VlRand * self, vl_uindex range) { if (range <= 0xffffffff) { /* 32-bit version */ return (vl_rand_uint32 (self) % (vl_uint32)range) ; } else { /* 64-bit version */ return (vl_rand_uint64 (self) % range) ; } } /** @brief Generate a random UINT64 ** @param self random number generator. ** @return a random number in [0, 0xffffffffffffffff]. **/ VL_INLINE vl_uint64 vl_rand_uint64 (VlRand * self) { vl_uint64 a = vl_rand_uint32 (self) ; vl_uint64 b = vl_rand_uint32 (self) ; return (a << 32) | b ; } /** @brief Generate a random INT63 ** @param self random number generator. ** @return a random number in [0, 0x7fffffffffffffff]. **/ VL_INLINE vl_int64 vl_rand_int63 (VlRand * self) { return (vl_int64)(vl_rand_uint64 (self) >> 1) ; } /** @brief Generate a random INT31 ** @param self random number generator. ** @return a random number in [0, 0x7fffffff]. **/ VL_INLINE vl_int32 vl_rand_int31 (VlRand * self) { return (vl_int32)(vl_rand_uint32 (self) >> 1) ; } /** @brief Generate a random number in [0,1] ** @param self random number generator. ** @return a random number. **/ VL_INLINE double vl_rand_real1 (VlRand * self) { return vl_rand_uint32(self)*(1.0/4294967295.0); /* divided by 2^32-1 */ } /** @brief Generate a random number in [0,1) ** @param self random number generator. ** @return a random number. **/ VL_INLINE double vl_rand_real2 (VlRand * self) { return vl_rand_uint32(self)*(1.0/4294967296.0); /* divided by 2^32 */ } /** @brief Generate a random number in (0,1) ** @param self random number generator. ** @return a random number. **/ VL_INLINE double vl_rand_real3 (VlRand * self) { return (((double)vl_rand_uint32(self)) + 0.5)*(1.0/4294967296.0); /* divided by 2^32 */ } /** @brief Generate a random number in [0,1) with 53-bit resolution ** @param self random number generator. ** @return a random number. **/ VL_INLINE double vl_rand_res53 (VlRand * self) { vl_uint32 a = vl_rand_uint32(self) >> 5, b = vl_rand_uint32(self) >> 6 ; return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0) ; } /* VL_RANDOM_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/rodrigues.c000077500000000000000000000215151454702036400212640ustar00rootroot00000000000000/** @file rodrigues.c ** @brief Rodrigues formulas - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-13 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #include "generic.h" #include "mathop.h" #include "rodrigues.h" #include /** @brief Rodrigues' formula ** @param R_pt 3x3 matrix - array of 9 double (in) . ** @param dR_pt 9x3 matrix - array of 27 double (in). ** @param om_pt 3 vector - array of 3 dobule (out). **/ void vl_rodrigues(double* R_pt, double* dR_pt, const double* om_pt) { /* Let th = |om|, r=w/th, sth=sin(th), cth=cos(th), ^om = hat(om) Then the rodrigues formula is an expansion of the exponential function: rodrigues(om) = exp ^om = I + ^r sth + ^r^2 (1 - cth). The derivative can be computed by elementary means and results: d(vec rodrigues(om)) sth d ^r 1 - cth d (^r)^2 -------------------- = ---- ----- + ------- -------- + d om^T th d r^T th d r^T sth 1 - cth + vec^r (cth - -----) + vec^r^2 (sth - 2-------)r^T th th */ #define OM(i) om_pt[(i)] #define R(i,j) R_pt[(i)+3*(j)] #define DR(i,j) dR_pt[(i)+9*(j)] #undef small const double small = 1e-6 ; double th = sqrt( OM(0)*OM(0) + OM(1)*OM(1) + OM(2)*OM(2) ) ; if( th < small ) { R(0,0) = 1.0 ; R(0,1) = 0.0 ; R(0,2) = 0.0 ; R(1,0) = 0.0 ; R(1,1) = 1.0 ; R(1,2) = 0.0 ; R(2,0) = 0.0 ; R(2,1) = 0.0 ; R(2,2) = 1.0 ; if(dR_pt) { DR(0,0) = 0 ; DR(0,1) = 0 ; DR(0,2) = 0 ; DR(1,0) = 0 ; DR(1,1) = 0 ; DR(1,2) = 1 ; DR(2,0) = 0 ; DR(2,1) = -1 ; DR(2,2) = 0 ; DR(3,0) = 0 ; DR(3,1) = 0 ; DR(3,2) = -1 ; DR(4,0) = 0 ; DR(4,1) = 0 ; DR(4,2) = 0 ; DR(5,0) = 1 ; DR(5,1) = 0 ; DR(5,2) = 0 ; DR(6,0) = 0 ; DR(6,1) = 1 ; DR(6,2) = 0 ; DR(7,0) = -1 ; DR(7,1) = 0 ; DR(7,2) = 0 ; DR(8,0) = 0 ; DR(8,1) = 0 ; DR(8,2) = 0 ; } return ; } { double x = OM(0) / th ; double y = OM(1) / th ; double z = OM(2) / th ; double xx = x*x ; double xy = x*y ; double xz = x*z ; double yy = y*y ; double yz = y*z ; double zz = z*z ; const double yx = xy ; const double zx = xz ; const double zy = yz ; double sth = sin(th) ; double cth = cos(th) ; double mcth = 1.0 - cth ; R(0,0) = 1 - mcth * (yy+zz) ; R(1,0) = sth*z + mcth * xy ; R(2,0) = - sth*y + mcth * xz ; R(0,1) = - sth*z + mcth * yx ; R(1,1) = 1 - mcth * (zz+xx) ; R(2,1) = sth*x + mcth * yz ; R(0,2) = sth*y + mcth * xz ; R(1,2) = - sth*x + mcth * yz ; R(2,2) = 1 - mcth * (xx+yy) ; if(dR_pt) { double a = sth / th ; double b = mcth / th ; double c = cth - a ; double d = sth - 2*b ; DR(0,0) = - d * (yy+zz) * x ; DR(1,0) = b*y + c * zx + d * xy * x ; DR(2,0) = b*z - c * yx + d * xz * x ; DR(3,0) = b*y - c * zx + d * xy * x ; DR(4,0) = -2*b*x - d * (zz+xx) * x ; DR(5,0) = a + c * xx + d * yz * x ; DR(6,0) = b*z + c * yx + d * zx * x ; DR(7,0) = -a - c * xx + d * zy * x ; DR(8,0) = -2*b*x - d * (yy+xx) * x ; DR(0,1) = -2*b*y - d * (yy+zz) * y ; DR(1,1) = b*x + c * zy + d * xy * y ; DR(2,1) = -a - c * yy + d * xz * y ; DR(3,1) = b*x - c * zy + d * xy * y ; DR(4,1) = - d * (zz+xx) * y ; DR(5,1) = b*z + c * xy + d * yz * y ; DR(6,1) = a + c * yy + d * zx * y ; DR(7,1) = b*z - c * xy + d * zy * y ; DR(8,1) = -2*b*y - d * (yy+xx) * y ; DR(0,2) = -2*b*z - d * (yy+zz) * z ; DR(1,2) = a + c * zz + d * xy * z ; DR(2,2) = b*x - c * yz + d * xz * z ; DR(3,2) = -a - c * zz + d * xy * z ; DR(4,2) = -2*b*z - d * (zz+xx) * z ; DR(5,2) = b*y + c * xz + d * yz * z ; DR(6,2) = b*x + c * yz + d * zx * z ; DR(7,2) = b*y - c * xz + d * zy * z ; DR(8,2) = - d * (yy+xx) * z ; } } #undef OM #undef R #undef DR } /** @brief Inverse Rodrigues formula ** @param om_pt 3 vector - array of 3 dobule (out). ** @param dom_pt 3x9 matrix - array of 3x9 dobule (out). ** @param R_pt 3x3 matrix - array of 9 double (in). ** ** This function computes the Rodrigues formula of the argument @a ** om_pt. The result is stored int the matrix @a R_pt. If @a dR_pt is ** non null, then the derivative of the Rodrigues formula is computed ** and stored into the matrix @a dR_pt. **/ VL_EXPORT void vl_irodrigues(double* om_pt, double* dom_pt, const double* R_pt) { /* tr R - 1 1 [ R32 - R23 ] th = cos^{-1} --------, r = ------ [ R13 - R31 ], w = th r. 2 2 sth [ R12 - R21 ] sth = sin(th) dw th*cth-sth dw th [di3 dj2 - di2 dj3] ---- = ---------- r, ---- = ----- [di1 dj3 - di3 dj1]. dRii 2 sth^2 dRij 2 sth [di1 dj2 - di2 dj1] trace(A) < -1 only for small num. errors. */ #define OM(i) om_pt[(i)] #define DOM(i,j) dom_pt[(i)+3*(j)] #define R(i,j) R_pt[(i)+3*(j)] #define W(i,j) W_pt[(i)+3*(j)] const double small = 1e-6 ; double th = acos (0.5*(VL_MAX(R(0,0)+R(1,1)+R(2,2),-1.0) - 1.0)) ; double sth = sin(th) ; double cth = cos(th) ; if(fabs(sth) < small && cth < 0) { /* we have this singularity when the rotation is about pi (or -pi) we use the fact that in this case hat( sqrt(1-cth) * r )^2 = W = (0.5*(R+R') - eye(3)) which gives (1-cth) rx^2 = 0.5 * (W(1,1)-W(2,2)-W(3,3)) (1-cth) ry^2 = 0.5 * (W(2,2)-W(3,3)-W(1,1)) (1-cth) rz^2 = 0.5 * (W(3,3)-W(1,1)-W(2,2)) */ double W_pt [9], x, y, z ; W_pt[0] = 0.5*( R(0,0) + R(0,0) ) - 1.0 ; W_pt[1] = 0.5*( R(1,0) + R(0,1) ) ; W_pt[2] = 0.5*( R(2,0) + R(0,2) ); W_pt[3] = 0.5*( R(0,1) + R(1,0) ); W_pt[4] = 0.5*( R(1,1) + R(1,1) ) - 1.0; W_pt[5] = 0.5*( R(2,1) + R(1,2) ); W_pt[6] = 0.5*( R(0,2) + R(2,0) ) ; W_pt[7] = 0.5*( R(1,2) + R(2,1) ) ; W_pt[8] = 0.5*( R(2,2) + R(2,2) ) - 1.0 ; /* these are only absolute values */ x = sqrt( 0.5 * (W(0,0)-W(1,1)-W(2,2)) ) ; y = sqrt( 0.5 * (W(1,1)-W(2,2)-W(0,0)) ) ; z = sqrt( 0.5 * (W(2,2)-W(0,0)-W(1,1)) ) ; /* set the biggest component to + and use the element of the ** matrix W to determine the sign of the other components ** then the solution is either (x,y,z) or its opposite */ if( x >= y && x >= z ) { y = (W(1,0) >=0) ? y : -y ; z = (W(2,0) >=0) ? z : -z ; } else if( y >= x && y >= z ) { z = (W(2,1) >=0) ? z : -z ; x = (W(1,0) >=0) ? x : -x ; } else { x = (W(2,0) >=0) ? x : -x ; y = (W(2,1) >=0) ? y : -y ; } /* we are left to chose between (x,y,z) and (-x,-y,-z) ** unfortunately we cannot (as the rotation is too close to pi) and ** we just keep what we have. */ { double scale = th / sqrt( 1 - cth ) ; OM(0) = scale * x ; OM(1) = scale * y ; OM(2) = scale * z ; if( dom_pt ) { int k ; for(k=0; k<3*9; ++k) dom_pt [k] = VL_NAN_D ; } return ; } } else { double a = (fabs(sth) < small) ? 1 : th/sin(th) ; double b ; OM(0) = 0.5*a*(R(2,1) - R(1,2)) ; OM(1) = 0.5*a*(R(0,2) - R(2,0)) ; OM(2) = 0.5*a*(R(1,0) - R(0,1)) ; if( dom_pt ) { if( fabs(sth) < small ) { a = 0.5 ; b = 0 ; } else { a = th/(2*sth) ; b = (th*cth - sth)/(2*sth*sth)/th ; } DOM(0,0) = b*OM(0) ; DOM(1,0) = b*OM(1) ; DOM(2,0) = b*OM(2) ; DOM(0,1) = 0 ; DOM(1,1) = 0 ; DOM(2,1) = a ; DOM(0,2) = 0 ; DOM(1,2) = -a ; DOM(2,2) = 0 ; DOM(0,3) = 0 ; DOM(1,3) = 0 ; DOM(2,3) = -a ; DOM(0,4) = b*OM(0) ; DOM(1,4) = b*OM(1) ; DOM(2,4) = b*OM(2) ; DOM(0,5) = a ; DOM(1,5) = 0 ; DOM(2,5) = 0 ; DOM(0,6) = 0 ; DOM(1,6) = a ; DOM(2,6) = 0 ; DOM(0,7) = -a ; DOM(1,7) = 0 ; DOM(2,7) = 0 ; DOM(0,8) = b*OM(0) ; DOM(1,8) = b*OM(1) ; DOM(2,8) = b*OM(2) ; } } #undef OM #undef DOM #undef R #undef W } colmap-3.9.1/src/thirdparty/VLFeat/rodrigues.h000066400000000000000000000014121454702036400212600ustar00rootroot00000000000000/** @file rodrigues.h ** @brief Rodrigues formulas ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-13 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file rodrigues.h @section rodrigues Rodrigues formulas - Use ::vl_rodrigues to compute the Rodrigues formula and its derivative. - Use ::vl_irodrigues to compute the inverse Rodrigues formula and its derivative. **/ #ifndef VL_RODRIGUES #define VL_RODRIGUES #include "generic.h" VL_EXPORT void vl_rodrigues (double* R_pt, double* dR_pt, const double* om_pt) ; VL_EXPORT void vl_irodrigues (double* om_pt, double* dom_pt, const double* R_pt) ; /* VL_RODRIGUES */ #endif colmap-3.9.1/src/thirdparty/VLFeat/scalespace.c000077500000000000000000000677121454702036400213750ustar00rootroot00000000000000/** @file scalespace.c ** @brief Scale Space - Definition ** @author Karel Lenc ** @author Andrea Vedaldi ** @author Michal Perdoch **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page scalespace Gaussian Scale Space (GSS) @author Karel Lenc @author Andrea Vedaldi @author Michal Perdoch @tableofcontents @ref scalespace.h implements a Gaussian scale space, a data structure representing an image at multiple resolutions @cite{witkin83scale-space} @cite{koenderink84the-structure} @cite{lindeberg94scale-space}. Scale spaces have many use, including the detection of co-variant local features @cite{lindeberg98principles} such as SIFT, Hessian-Affine, Harris-Affine, Harris-Laplace, etc. @ref scalespace-starting demonstreates how to use the C API to compute the scalespace of an image. For further details refer to: - @subpage scalespace-fundamentals @section scalespace-starting Getting started Given an input image `image`, the following example uses the ::VlScaleSpace object to compute its Gaussian scale space and return the image `level` at scale `(o,s)`, where `o` is the octave and `s` is the octave subdivision or sublevel: @code float* level ; VlScaleSpace ss = vl_scalespace_new(imageWidth, imageHeight) ; vl_scalespace_put_image(ss, image) ; level = vl_scalespace_get_level(ss, o, s) ; @endcode The image `level` is obtained by convolving `image` by a Gaussian filter of isotropic standard deviation given by @code double sigma = vl_scalespace_get_sigma(ss, o, s) ; @endcode The resolution of `level` is in general different from the resolution of `image` and is determined by the octave `o`. It can be obtained as follows: @code VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(ss, o) ; ogeom.width // width of level (in number of pixels) ogeom.height // height of level (in number of pixels) ogeom.step // spatial sampling step @endcode The parameter `ogeom.step` is the sampling step relatively to the sampling of the input image `image`. The ranges of valid octaves and scale sublevels can be obtained as @code VlScaleSpaceGeometry geom = vl_scalespace_get_geometry(ss) ; geom.firstOctave // Index of the fisrt octave geom.lastOctave // Index of the last octave geom.octaveResolution ; // Number of octave subdivisions geom.octaveFirstSubdivision // Index of the first octave subdivision geom.octaveLastSubdivision // Index of the last octave subdivision @endcode So for example `o` minimum value is `geom.firstOctave` and maximum value is `geom.lastOctave`. The subdivision index `s` naturally spans the range 0 to `geom.octaveResolution-1`. However, the scale space object is flexible in that it allows different ranges of subdivisions to be computed and `s` varies in the range `geom.octaveFirstSubdivision` to `geom.octaveLastSubdivision`. See @ref scalespace-fundamentals for further details. The geometry of the scale space can be customized upon creation, as follows: @code VlScaleSpaceGeometry geom = vl_scalespace_get_default_geometry(imageWidth, imageHeight) ; geom.firstOctave = -1 ; geom.octaveFirstSubdivision = -1 ; geom.octaveLastSubdivision = geom.octaveResolution ; VlScaleSpacae ss = vl_scalespace_new_with_geometry (geom) ; @endcode @page scalespace-fundamentals Gaussian scale space fundamentals @tableofcontents This page discusses the notion of *Gaussian scale space* and the relative data structure. For the C API see @ref scalespace.h and @ref scalespace-starting. A *scale space* is representation of an image at multiple resolution levels. An image is a function $\ell(x,y)$ of two coordinates $x$, $y$; the scale space $\ell(x,y,\sigma)$ adds a third coordinate $\sigma$ indexing the *scale*. Here the focus is the Gaussian scale space, where the image $\ell(x,y,\sigma)$ is obtained by smoothing $\ell(x,y)$ by a Gaussian kernel of isotropic standard deviation $\sigma$. @section scalespace-definition Scale space definition Formally, the *Gaussian scale space* of an image $\ell(x,y)$ is defined as \[ \ell(x,y,\sigma) = [g_{\sigma} * \ell](x,y,\sigma) \] where $g_\sigma$ denotes a 2D Gaussian kernel of isotropic standard deviation $\sigma$: \[ g_{\sigma}(x,y) = \frac{1}{2\pi\sigma^2} \exp\left( - \frac{x^2 + y^2}{2\sigma^2} \right). \] An important detail is that the algorithm computing the scale space assumes that the input image $\ell(x,y)$ is pre-smoothed, roughly capturing the effect of the finite pixel size in a CCD. This is modelled by assuming that the input is not $\ell(x,y)$, but $\ell(x,y,\sigma_n)$, where $\sigma_n$ is a *nominal smoothing*, usually taken to be 0.5 (half a pixel standard deviation). This also means that $\sigma = \sigma_n = 0.5$ is the *finest scale* that can actually be computed. The scale space structure stores samples of the function $\ell(x,y,\sigma)$. The density of the sampling of the spatial coordinates $x$ and $y$ is adjusted as a function of the scale $\sigma$, corresponding to the intuition that images at a coarse resolution can be sampled more coarsely without loss of information. Thus, the scale space has the structure of a *pyramid*: a collection of digital images sampled at progressively coarser spatial resolution and hence of progressively smaller size (in pixels). The following figure illustrates the scale space pyramid structure: @image html scalespace-basic.png "A scalespace structure with 2 octaves and S=3 subdivisions per octave" The pyramid is organised in a number of *octaves*, indexed by a parameter `o`. Each octave is further subdivided into *sublevels*, indexed by a parameter `s`. These are related to the scale $\sigma$ by the equation \[ \sigma(s,o) = \sigma_o 2^{\displaystyle o + \frac{s}{\mathtt{octaveResolution}}} \] where `octaveResolution` is the resolution of the octave subsampling $\sigma_0$ is the *base smoothing*. At each octave the spatial resolution is doubled, in the sense that samples are take with a step of \[ \mathtt{step} = 2^o. \] Hence, denoting as `level[i,j]` the corresponding samples, one has $\ell(x,y,\sigma) = \mathtt{level}[i,j]$, where \[ (x,y) = (i,j) \times \mathtt{step}, \quad \sigma = \sigma(o,s), \quad 0 \leq i < \mathtt{lwidth}, \quad 0 \leq j < \mathtt{lheight}, \] where \[ \mathtt{lwidth} = \lfloor \frac{\mathtt{width}}{2^\mathtt{o}}\rfloor, \quad \mathtt{lheight} = \lfloor \frac{\mathtt{height}}{2^\mathtt{o}}\rfloor. \] @section scalespace-geometry Scale space geometry In addition to the parameters discussed above, the geometry of the data stored in a scale space structure depends on the range of allowable octaves `o` and scale sublevels `s`. While `o` may range in any reasonable value given the size of the input image `image`, usually its minimum value is either 0 or -1. The latter corresponds to doubling the resolution of the image in the first octave of the scale space and it is often used in feature extraction. While there is no information added to the image by upsampling in this manner, fine scale filters, including derivative filters, are much easier to compute by upsalmpling first. The maximum practical value is dictated by the image resolution, as it should be $2^o\leq\min\{\mathtt{width},\mathtt{height}\}$. VLFeat has the flexibility of specifying the range of `o` using the `firstOctave` and `lastOctave` parameters of the ::VlScaleSpaceGeometry structure. The sublevel `s` varies naturally in the range $\{0,\dots,\mathtt{octaveResolution}-1\}$. However, it is often convenient to store a few extra levels per octave (e.g. to compute the local maxima of a function in scale or the Difference of Gaussian cornerness measure). Thus VLFeat scale space structure allows this parameter to vary in an arbitrary range, specified by the parameters `octaveFirstSubdivision` and `octaveLastSubdivision` of ::VlScaleSpaceGeometry. Overall the possible values of the indexes `o` and `s` are: \[ \mathtt{firstOctave} \leq o \leq \mathtt{lastOctave}, \qquad \mathtt{octaveFirstSubdivision} \leq s \leq \mathtt{octaveLastSubdivision}. \] Note that, depending on these ranges, there could be *redundant pairs* of indexes `o` and `s` that represent the *same* pyramid level at more than one sampling resolution. In practice, the ability to generate such redundant information is very useful in algorithms using scalespaces, as coding multiscale operations using a fixed sampling resolution is far easier. For example, the DoG feature detector computes the scalespace with three redundant levels per octave, as follows: @image html scalespace.png "A scalespace containing redundant representation of certain scale levels." @section scalespace-algorithm Algorithm and limitations Given $\ell(x,y,\sigma_n)$, any of a vast number digitial filtering techniques can be used to compute the scale levels. Presently, VLFeat uses a basic FIR implementation of the Gaussian filters. The FIR implementation is obtained by sampling the Gaussian function and re-normalizing it to have unit norm. This simple construction does not account properly for sampling effect, which may be a problem for very small Gausisan kernels. As a rule of thumb, such filters work sufficiently well for, say, standard deviation $\sigma$ at least 1.6 times the sampling step. A work around to apply this basic FIR implementation to very small Gaussian filters is to upsample the image first. The limitations on the FIR filters have relatively important for the pyramid construction, as the latter is obtained by *incremental smoothing*: each successive level is obtained from the previous one by adding the needed amount of smoothing. In this manner, the size of the FIR filters remains small, which makes them efficient; at the same time, for what discussed, excessively small filters are not represented properly. */ #include "scalespace.h" #include "mathop.h" #include #include #include #include #include /** @file scalespace.h ** @struct VlScaleSpace ** @brief Scale space class ** ** This is an opaque class used to compute the scale space of an ** image. **/ struct _VlScaleSpace { VlScaleSpaceGeometry geom ; /**< Geometry of the scale space */ float **octaves ; /**< Data */ } ; /* ---------------------------------------------------------------- */ /** @brief Get the default geometry for a given image size. ** @param width image width. ** @param height image height. ** @return the default scale space geometry. ** ** Both @a width and @a height must be at least one pixel wide. **/ VlScaleSpaceGeometry vl_scalespace_get_default_geometry (vl_size width, vl_size height) { VlScaleSpaceGeometry geom ; assert(width >= 1) ; assert(height >= 1) ; geom.width = width ; geom.height = height ; geom.firstOctave = 0 ; geom.lastOctave = VL_MAX(floor(vl_log2_d(VL_MIN(width, height))) - 3, 0) ; geom.octaveResolution= 3 ; geom.octaveFirstSubdivision = 0 ; geom.octaveLastSubdivision = geom.octaveResolution - 1 ; geom.baseScale = 1.6 * pow(2.0, 1.0 / geom.octaveResolution) ; geom.nominalScale = 0.5 ; return geom ; } #define is_valid_geometry(geom) (\ geom.firstOctave <= geom.lastOctave && \ geom.octaveResolution >= 1 && \ geom.octaveFirstSubdivision <= geom.octaveLastSubdivision && \ geom.baseScale >= 0.0 && \ geom.nominalScale >= 0.0) /** @brief Check scale space geometries for equality ** @param a first geometry. ** @param b second geometry. ** @return true if equal. **/ vl_bool vl_scalespacegeometry_is_equal (VlScaleSpaceGeometry a, VlScaleSpaceGeometry b) { return a.width == b.width && a.height == b.height && a.firstOctave == b.firstOctave && a.lastOctave == b.lastOctave && a.octaveResolution == b.octaveResolution && a.octaveFirstSubdivision == b.octaveLastSubdivision && a.baseScale == b.baseScale && a.nominalScale == b.nominalScale ; } /** @brief Get the geometry of the scale space. ** @param self object. ** @return the scale space geometry. **/ VlScaleSpaceGeometry vl_scalespace_get_geometry (VlScaleSpace const * self) { return self->geom ; } /** @brief Get the geometry of an octave of the scalespace. ** @param self object. ** @param o octave index. ** @return the geometry of octave @a o. **/ VlScaleSpaceOctaveGeometry vl_scalespace_get_octave_geometry (VlScaleSpace const * self, vl_index o) { VlScaleSpaceOctaveGeometry ogeom ; ogeom.width = VL_SHIFT_LEFT(self->geom.width, -o) ; ogeom.height = VL_SHIFT_LEFT(self->geom.height, -o) ; ogeom.step = pow(2.0, o) ; return ogeom ; } /** @brief Get the data of a scale space level ** @param self object. ** @param o octave index. ** @param s level index. ** @return pointer to the data for octave @a o, level @a s. ** ** The octave index @a o must be in the range @c firstOctave ** to @c lastOctave and the scale index @a s must be in the ** range @c octaveFirstSubdivision to @c octaveLastSubdivision. **/ float * vl_scalespace_get_level (VlScaleSpace *self, vl_index o, vl_index s) { VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(self,o) ; float * octave ; assert(self) ; assert(o >= self->geom.firstOctave) ; assert(o <= self->geom.lastOctave) ; assert(s >= self->geom.octaveFirstSubdivision) ; assert(s <= self->geom.octaveLastSubdivision) ; octave = self->octaves[o - self->geom.firstOctave] ; return octave + ogeom.width * ogeom.height * (s - self->geom.octaveFirstSubdivision) ; } /** @brief Get the data of a scale space level (const) ** @param self object. ** @param o octave index. ** @param s level index. ** @return pointer to the data for octave @a o, level @a s. ** ** This function is the same as ::vl_scalespace_get_level but reutrns ** a @c const pointer to the data. **/ float const * vl_scalespace_get_level_const (VlScaleSpace const * self, vl_index o, vl_index s) { return vl_scalespace_get_level((VlScaleSpace*)self, o, s) ; } /** ------------------------------------------------------------------ ** @brief Get the scale of a given octave and sublevel ** @param self object. ** @param o octave index. ** @param s sublevel index. ** ** The function returns the scale $\sigma(o,s)$ as a function of the ** octave index @a o and sublevel @a s. **/ double vl_scalespace_get_level_sigma (VlScaleSpace const *self, vl_index o, vl_index s) { return self->geom.baseScale * pow(2.0, o + (double) s / self->geom.octaveResolution) ; } /** ------------------------------------------------------------------ ** @internal @brief Upsample the rows and take the transpose ** @param destination output image. ** @param source input image. ** @param width input image width. ** @param height input image height. ** ** The output image has dimensions @a height by 2 @a width (so the ** destination buffer must be at least as big as two times the ** input buffer). ** ** Upsampling is performed by linear interpolation. **/ static void copy_and_upsample (float *destination, float const *source, vl_size width, vl_size height) { vl_index x, y, ox, oy ; float v00, v10, v01, v11 ; assert(destination) ; assert(source) ; for(y = 0 ; y < (signed)height ; ++y) { oy = (y < ((signed)height - 1)) * width ; v10 = source[0] ; v11 = source[oy] ; for(x = 0 ; x < (signed)width ; ++x) { ox = x < ((signed)width - 1) ; v00 = v10 ; v01 = v11 ; v10 = source[ox] ; v11 = source[ox + oy] ; destination[0] = v00 ; destination[1] = 0.5f * (v00 + v10) ; destination[2*width] = 0.5f * (v00 + v01) ; destination[2*width+1] = 0.25f * (v00 + v01 + v10 + v11) ; destination += 2 ; source ++; } destination += 2*width ; } } /** ------------------------------------------------------------------ ** @internal @brief Downsample ** @param destination output imgae buffer. ** @param source input image buffer. ** @param width input image width. ** @param height input image height. ** @param numOctaves octaves (non negative). ** ** The function downsamples the image @a d times, reducing it to @c ** 1/2^d of its original size. The parameters @a width and @a height ** are the size of the input image. The destination image @a dst is ** assumed to be floor(width/2^d) pixels wide and ** floor(height/2^d) pixels high. **/ static void copy_and_downsample (float *destination, float const *source, vl_size width, vl_size height, vl_size numOctaves) { vl_index x, y ; vl_size step = 1 << numOctaves ; /* step = 2^numOctaves */ assert(destination) ; assert(source) ; if (numOctaves == 0) { memcpy(destination, source, sizeof(float) * width * height) ; } else { for(y = 0 ; y < (signed)height ; y += step) { float const *p = source + y * width ; for(x = 0 ; x < (signed)width - ((signed)step - 1) ; x += step) { *destination++ = *p ; p += step ; } } } } /* ---------------------------------------------------------------- */ /** @brief Create a new scale space object ** @param width image width. ** @param height image height. ** @return new scale space object. ** ** This function is the same as ::vl_scalespace_new_with_geometry() ** but it uses ::vl_scalespace_get_default_geometry to initialise ** the geometry of the scale space from the image size. ** ** @sa ::vl_scalespace_new_with_geometry(), ::vl_scalespace_delete(). **/ VlScaleSpace * vl_scalespace_new (vl_size width, vl_size height) { VlScaleSpaceGeometry geom ; geom = vl_scalespace_get_default_geometry(width, height) ; return vl_scalespace_new_with_geometry(geom) ; } /** ------------------------------------------------------------------ ** @brief Create a new scale space with the specified geometry ** @param geom scale space geomerty. ** @return new scale space object. ** ** If the geometry is not valid (see ::VlScaleSpaceGeometry), the ** result is unpredictable. ** ** The function returns `NULL` if it was not possible to allocate the ** object because of an out-of-memory condition. ** ** @sa ::VlScaleSpaceGeometry, ::vl_scalespace_delete(). **/ VlScaleSpace * vl_scalespace_new_with_geometry (VlScaleSpaceGeometry geom) { vl_index o ; vl_size numSublevels = geom.octaveLastSubdivision - geom.octaveFirstSubdivision + 1 ; vl_size numOctaves = geom.lastOctave - geom.firstOctave + 1 ; VlScaleSpace *self ; assert(is_valid_geometry(geom)) ; numOctaves = geom.lastOctave - geom.firstOctave + 1 ; numSublevels = geom.octaveLastSubdivision - geom.octaveFirstSubdivision + 1 ; self = vl_calloc(1, sizeof(VlScaleSpace)) ; if (self == NULL) goto err_alloc_self ; self->geom = geom ; self->octaves = vl_calloc(numOctaves, sizeof(float*)) ; if (self->octaves == NULL) goto err_alloc_octave_list ; for (o = self->geom.firstOctave ; o <= self->geom.lastOctave ; ++o) { VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(self,o) ; vl_size octaveSize = ogeom.width * ogeom.height * numSublevels ; self->octaves[o - self->geom.firstOctave] = vl_malloc(octaveSize * sizeof(float)) ; if (self->octaves[o - self->geom.firstOctave] == NULL) goto err_alloc_octaves; } return self ; err_alloc_octaves: for (o = self->geom.firstOctave ; o <= self->geom.lastOctave ; ++o) { if (self->octaves[o - self->geom.firstOctave]) { vl_free(self->octaves[o - self->geom.firstOctave]) ; } } err_alloc_octave_list: vl_free(self) ; err_alloc_self: return NULL ; } /* ---------------------------------------------------------------- */ /** @brief Create a new copy of the object ** @param self object to copy from. ** ** The function returns `NULL` if the copy cannot be made due to an ** out-of-memory condition. **/ VlScaleSpace * vl_scalespace_new_copy (VlScaleSpace* self) { vl_index o ; VlScaleSpace * copy = vl_scalespace_new_shallow_copy(self) ; if (copy == NULL) return NULL ; for (o = self->geom.firstOctave ; o <= self->geom.lastOctave ; ++o) { VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(self,o) ; vl_size numSubevels = self->geom.octaveLastSubdivision - self->geom.octaveFirstSubdivision + 1; memcpy(copy->octaves[o - self->geom.firstOctave], self->octaves[o - self->geom.firstOctave], ogeom.width * ogeom.height * numSubevels * sizeof(float)) ; } return copy ; } /* ---------------------------------------------------------------- */ /** @brief Create a new shallow copy of the object ** @param self object to copy from. ** ** The function works like ::vl_scalespace_new_copy() but only allocates ** the scale space, without actually copying the data. **/ VlScaleSpace * vl_scalespace_new_shallow_copy (VlScaleSpace* self) { return vl_scalespace_new_with_geometry (self->geom) ; } /* ---------------------------------------------------------------- */ /** @brief Delete object ** @param self object to delete. ** @sa ::vl_scalespace_new() **/ void vl_scalespace_delete (VlScaleSpace * self) { if (self) { if (self->octaves) { vl_index o ; for (o = self->geom.firstOctave ; o <= self->geom.lastOctave ; ++o) { if (self->octaves[o - self->geom.firstOctave]) { vl_free(self->octaves[o - self->geom.firstOctave]) ; } } vl_free(self->octaves) ; } vl_free(self) ; } } /* ---------------------------------------------------------------- */ /** @internal @brief Fill octave starting from the first level ** @param self object instance. ** @param o octave to process. ** ** The function takes the first sublevel of octave @a o (the one at ** sublevel `octaveFirstLevel` and iteratively ** smoothes it to obtain the other octave levels. **/ void _vl_scalespace_fill_octave (VlScaleSpace *self, vl_index o) { vl_index s ; VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(self, o) ; for(s = self->geom.octaveFirstSubdivision + 1 ; s <= self->geom.octaveLastSubdivision ; ++s) { double sigma = vl_scalespace_get_level_sigma(self, o, s) ; double previousSigma = vl_scalespace_get_level_sigma(self, o, s - 1) ; double deltaSigma = sqrtf(sigma*sigma - previousSigma*previousSigma) ; float* level = vl_scalespace_get_level (self, o, s) ; float* previous = vl_scalespace_get_level (self, o, s-1) ; vl_imsmooth_f (level, ogeom.width, previous, ogeom.width, ogeom.height, ogeom.width, deltaSigma / ogeom.step, deltaSigma / ogeom.step) ; } } /** ------------------------------------------------------------------ ** @internal @brief Initialize the first level of an octave from an image ** @param self ::VlScaleSpace object instance. ** @param image image data. ** @param o octave to start. ** ** The function initializes the first level of octave @a o from ** image @a image. The dimensions of the image are the ones set ** during the creation of the ::VlScaleSpace object instance. **/ static void _vl_scalespace_start_octave_from_image (VlScaleSpace *self, float const *image, vl_index o) { float *level ; double sigma, imageSigma ; vl_index op ; assert(self) ; assert(image) ; assert(o >= self->geom.firstOctave) ; assert(o <= self->geom.lastOctave) ; /* * Copy the image to self->geom.octaveFirstSubdivision of octave o, upscaling or * downscaling as needed. */ level = vl_scalespace_get_level(self, VL_MAX(0, o), self->geom.octaveFirstSubdivision) ; copy_and_downsample(level, image, self->geom.width, self->geom.height, VL_MAX(0, o)) ; for (op = -1 ; op >= o ; --op) { VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(self, op + 1) ; float *succLevel = vl_scalespace_get_level(self, op + 1, self->geom.octaveFirstSubdivision) ; level = vl_scalespace_get_level(self, op, self->geom.octaveFirstSubdivision) ; copy_and_upsample(level, succLevel, ogeom.width, ogeom.height) ; } /* * Adjust the smoothing of the first level just initialised, accounting * for the fact that the input image is assumed to be a nominal scale * level. */ sigma = vl_scalespace_get_level_sigma(self, o, self->geom.octaveFirstSubdivision) ; imageSigma = self->geom.nominalScale ; if (sigma > imageSigma) { VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(self, o) ; double deltaSigma = sqrt (sigma*sigma - imageSigma*imageSigma) ; level = vl_scalespace_get_level (self, o, self->geom.octaveFirstSubdivision) ; vl_imsmooth_f (level, ogeom.width, level, ogeom.width, ogeom.height, ogeom.width, deltaSigma / ogeom.step, deltaSigma / ogeom.step) ; } } /** @internal @brief Initialize the first level of an octave from the previous octave ** @param self object. ** @param o octave to initialize. ** ** The function initializes the first level of octave @a o from the ** content of octave o - 1. **/ static void _vl_scalespace_start_octave_from_previous_octave (VlScaleSpace *self, vl_index o) { double sigma, prevSigma ; float *level, *prevLevel ; vl_index prevLevelIndex ; VlScaleSpaceOctaveGeometry ogeom ; assert(self) ; assert(o > self->geom.firstOctave) ; /* must not be the first octave */ assert(o <= self->geom.lastOctave) ; /* * From the previous octave pick the level which is closer to * self->geom.octaveFirstSubdivision in this octave. * The is self->geom.octaveFirstSubdivision + self->numLevels since there are * self->geom.octaveResolution levels in an octave, provided that * this value does not exceed self->geom.octaveLastSubdivision. */ prevLevelIndex = VL_MIN(self->geom.octaveFirstSubdivision + (signed)self->geom.octaveResolution, self->geom.octaveLastSubdivision) ; prevLevel = vl_scalespace_get_level (self, o - 1, prevLevelIndex) ; level = vl_scalespace_get_level (self, o, self->geom.octaveFirstSubdivision) ; ogeom = vl_scalespace_get_octave_geometry(self, o - 1) ; copy_and_downsample (level, prevLevel, ogeom.width, ogeom.height, 1) ; /* * Add remaining smoothing, if any. */ sigma = vl_scalespace_get_level_sigma(self, o, self->geom.octaveFirstSubdivision) ; prevSigma = vl_scalespace_get_level_sigma(self, o - 1, prevLevelIndex) ; if (sigma > prevSigma) { VlScaleSpaceOctaveGeometry ogeom = vl_scalespace_get_octave_geometry(self, o) ; double deltaSigma = sqrt (sigma*sigma - prevSigma*prevSigma) ; level = vl_scalespace_get_level (self, o, self->geom.octaveFirstSubdivision) ; /* todo: this may fail due to an out-of-memory condition */ vl_imsmooth_f (level, ogeom.width, level, ogeom.width, ogeom.height, ogeom.width, deltaSigma / ogeom.step, deltaSigma / ogeom.step) ; } } /** @brief Initialise Scale space with new image ** @param self ::VlScaleSpace object instance. ** @param image image to process. ** ** Compute the data of all the defined octaves and scales of the scale ** space @a self. **/ void vl_scalespace_put_image (VlScaleSpace *self, float const *image) { vl_index o ; _vl_scalespace_start_octave_from_image(self, image, self->geom.firstOctave) ; _vl_scalespace_fill_octave(self, self->geom.firstOctave) ; for (o = self->geom.firstOctave + 1 ; o <= self->geom.lastOctave ; ++o) { _vl_scalespace_start_octave_from_previous_octave(self, o) ; _vl_scalespace_fill_octave(self, o) ; } } colmap-3.9.1/src/thirdparty/VLFeat/scalespace.h000066400000000000000000000067531454702036400213750ustar00rootroot00000000000000/** @file scalespace.h ** @brief Scale Space (@ref scalespace) ** @author Andrea Vedaldi ** @author Karel Lenc ** @author Michal Perdoch **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_SCALESPACE_H #define VL_SCALESPACE_H #include "generic.h" #include "imopv.h" #include "mathop.h" /* ---------------------------------------------------------------- */ /* VlScaleSpaceGeometry */ /* ---------------------------------------------------------------- */ /** @brief Geometry of a scale space ** ** There are a few restrictions on the valid geometrties. */ typedef struct _VlScaleSpaceGeometry { vl_size width ; /**< Image width */ vl_size height ; /**< Image height */ vl_index firstOctave ; /**< Index of the fisrt octave */ vl_index lastOctave ; /**< Index of the last octave */ vl_size octaveResolution ; /**< Number of octave subdivisions */ vl_index octaveFirstSubdivision ; /**< Index of the first octave subdivision */ vl_index octaveLastSubdivision ; /**< Index of the last octave subdivision */ double baseScale ; /**< Base smoothing (smoothing of octave 0, level 0) */ double nominalScale ; /**< Nominal smoothing of the original image */ } VlScaleSpaceGeometry ; VL_EXPORT vl_bool vl_scalespacegeometry_is_equal (VlScaleSpaceGeometry a, VlScaleSpaceGeometry b) ; /* ---------------------------------------------------------------- */ /* VlScaleSpaceOctaveGeometry */ /* ---------------------------------------------------------------- */ /** @brief Geometry of one octave of a scale space */ typedef struct _VlScaleSpaceOctaveGeometry { vl_size width ; /**< Width (number of pixels) */ vl_size height ; /**< Height (number of pixels) */ double step ; /**< Sampling step (size of a pixel) */ } VlScaleSpaceOctaveGeometry ; /* ---------------------------------------------------------------- */ /* VlScaleSpace */ /* ---------------------------------------------------------------- */ typedef struct _VlScaleSpace VlScaleSpace ; /** @name Create and destroy ** @{ **/ VL_EXPORT VlScaleSpaceGeometry vl_scalespace_get_default_geometry(vl_size width, vl_size height) ; VL_EXPORT VlScaleSpace * vl_scalespace_new (vl_size width, vl_size height) ; VL_EXPORT VlScaleSpace * vl_scalespace_new_with_geometry (VlScaleSpaceGeometry geom) ; VL_EXPORT VlScaleSpace * vl_scalespace_new_copy (VlScaleSpace* src); VL_EXPORT VlScaleSpace * vl_scalespace_new_shallow_copy (VlScaleSpace* src); VL_EXPORT void vl_scalespace_delete (VlScaleSpace *self) ; /** @} */ /** @name Process data ** @{ **/ VL_EXPORT void vl_scalespace_put_image (VlScaleSpace *self, float const* image); /** @} */ /** @name Retrieve data and parameters ** @{ **/ VL_EXPORT VlScaleSpaceGeometry vl_scalespace_get_geometry (VlScaleSpace const * self) ; VL_EXPORT VlScaleSpaceOctaveGeometry vl_scalespace_get_octave_geometry (VlScaleSpace const * self, vl_index o) ; VL_EXPORT float * vl_scalespace_get_level (VlScaleSpace * self, vl_index o, vl_index s) ; VL_EXPORT float const * vl_scalespace_get_level_const (VlScaleSpace const * self, vl_index o, vl_index s) ; VL_EXPORT double vl_scalespace_get_level_sigma (VlScaleSpace const *self, vl_index o, vl_index s) ; /** @} */ /* VL_SCALESPACE_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/shuffle-def.h000066400000000000000000000047261454702036400214600ustar00rootroot00000000000000/** @file shuffle-def.h ** @brief Shuffle preprocessor metaprogram ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file shuffle-def.h @todo large array compatibility. **/ #include "host.h" #include "random.h" #include #ifndef VL_SHUFFLE_prefix #error "VL_SHUFFLE_prefix must be defined" #endif #ifndef VL_SHUFFLE_array #ifndef VL_SHUFFLE_type #error "VL_SHUFFLE_type must be defined if VL_SHUFFLE_array is not" #endif #define VL_SHUFFLE_array VL_SHUFFLE_type* #endif #ifdef __DOXYGEN__ #define VL_SHUFFLE_prefix ShufflePrefix /**< Prefix of the shuffle functions */ #define VL_SHUFFLE_type ShuffleType /**< Data type of the shuffle elements */ #define VL_SHUFFLE_array ShuffleType* /**< Data type of the shuffle container */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_SHUFFLE_swap) || defined(__DOXYGEN__) #define VL_SHUFFLE_swap VL_XCAT(VL_SHUFFLE_prefix, _swap) /** @brief Swap two array elements ** @param array shuffle array. ** @param indexA index of the first element to swap. ** @param indexB index of the second element to swap. ** ** The function swaps the two elements @a a and @ b. The function ** uses a temporary element of type ::VL_SHUFFLE_type ** and the copy operator @c =. **/ VL_INLINE void VL_SHUFFLE_swap (VL_SHUFFLE_array array, vl_uindex indexA, vl_uindex indexB) { VL_SHUFFLE_type t = array [indexA] ; array [indexA] = array [indexB] ; array [indexB] = t ; } /* VL_SHUFFLE_swap */ #endif /* ---------------------------------------------------------------- */ #if ! defined(VL_SHUFFLE_shuffle) || defined(__DOXYGEN__) #define VL_SHUFFLE_shuffle VL_XCAT(VL_SHUFFLE_prefix, _shuffle) /** @brief Shuffle ** @param array (in/out) pointer to the array. ** @param size size of the array. ** @param rand random number generator to use. ** ** The function randomly permutes the array. **/ VL_INLINE void VL_SHUFFLE_shuffle (VL_SHUFFLE_array array, vl_size size, VlRand * rand) { vl_uindex n = size ; while (n > 1) { vl_uindex k = vl_rand_uindex (rand, n) ; n -- ; VL_SHUFFLE_swap (array, n, k) ; } } /* VL_SHUFFLE_shuffle */ #endif #undef VL_SHUFFLE_prefix #undef VL_SHUFFLE_swap #undef VL_SHUFFLE_shuffle #undef VL_SHUFFLE_type #undef VL_SHUFFLE_array colmap-3.9.1/src/thirdparty/VLFeat/sift.c000077500000000000000000002146101454702036400202260ustar00rootroot00000000000000/** @file sift.c ** @brief SIFT - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page sift Scale Invariant Feature Transform (SIFT) @author Andrea Vedaldi @par "Credits:" May people have contributed with suggestions and bug reports. Although the following list is certainly incomplete, we would like to thank: Wei Dong, Loic, Giuseppe, Liu, Erwin, P. Ivanov, and Q. S. Luo. @tableofcontents @ref sift.h implements a @ref sift-usage "SIFT filter object", a reusable object to extract SIFT features @cite{lowe99object} from one or multiple images. This is the original VLFeat implementation of SIFT, designed to be compatible with Lowe's original SIFT. See @ref covdet for a different version of SIFT integrated in the more general covariant feature detector engine. @section sift-intro Overview A SIFT feature is a selected image region (also called keypoint) with an associated descriptor. Keypoints are extracted by the @ref sift-intro-detector "SIFT detector" and their descriptors are computed by the @ref sift-intro-descriptor "SIFT descriptor". It is also common to use independently the SIFT detector (i.e. computing the keypoints without descriptors) or the SIFT descriptor (i.e. computing descriptors of custom keypoints). @subsection sift-intro-detector SIFT detector A SIFT keypoint is a circular image region with an orientation. It is described by a geometric frame of four parameters: the keypoint center coordinates @e x and @e y, its @e scale (the radius of the region), and its @e orientation (an angle expressed in radians). The SIFT detector uses as keypoints image structures which resemble “blobs”. By searching for blobs at multiple scales and positions, the SIFT detector is invariant (or, more accurately, covariant) to translation, rotations, and re scaling of the image. The keypoint orientation is also determined from the local image appearance and is covariant to image rotations. Depending on the symmetry of the keypoint appearance, determining the orientation can be ambiguous. In this case, the SIFT detectors returns a list of up to four possible orientations, constructing up to four frames (differing only by their orientation) for each detected image blob. @image html sift-frame.png "SIFT keypoints are circular image regions with an orientation." There are several parameters that influence the detection of SIFT keypoints. First, searching keypoints at multiple scales is obtained by constructing a so-called “Gaussian scale space”. The scale space is just a collection of images obtained by progressively smoothing the input image, which is analogous to gradually reducing the image resolution. Conventionally, the smoothing level is called scale of the image. The construction of the scale space is influenced by the following parameters, set when creating the SIFT filter object by ::vl_sift_new(): - Number of octaves. Increasing the scale by an octave means doubling the size of the smoothing kernel, whose effect is roughly equivalent to halving the image resolution. By default, the scale space spans as many octaves as possible (i.e. roughly log2(min(width,height)), which has the effect of searching keypoints of all possible sizes. - First octave index. By convention, the octave of index 0 starts with the image full resolution. Specifying an index greater than 0 starts the scale space at a lower resolution (e.g. 1 halves the resolution). Similarly, specifying a negative index starts the scale space at an higher resolution image, and can be useful to extract very small features (since this is obtained by interpolating the input image, it does not make much sense to go past -1). - Number of levels per octave. Each octave is sampled at this given number of intermediate scales (by default 3). Increasing this number might in principle return more refined keypoints, but in practice can make their selection unstable due to noise (see [1]). Keypoints are further refined by eliminating those that are likely to be unstable, either because they are selected nearby an image edge, rather than an image blob, or are found on image structures with low contrast. Filtering is controlled by the follow: - Peak threshold. This is the minimum amount of contrast to accept a keypoint. It is set by configuring the SIFT filter object by ::vl_sift_set_peak_thresh(). - Edge threshold. This is the edge rejection threshold. It is set by configuring the SIFT filter object by ::vl_sift_set_edge_thresh().
Summary of the parameters influencing the SIFT detector.
Parameter See also Controlled by Comment
number of octaves @ref sift-intro-detector ::vl_sift_new
first octave index @ref sift-intro-detector ::vl_sift_new set to -1 to extract very small features
number of scale levels per octave @ref sift-intro-detector ::vl_sift_new can affect the number of extracted keypoints
edge threshold @ref sift-intro-detector ::vl_sift_set_edge_thresh decrease to eliminate more keypoints
peak threshold @ref sift-intro-detector ::vl_sift_set_peak_thresh increase to eliminate more keypoints
@subsection sift-intro-descriptor SIFT Descriptor @sa @ref sift-tech-descriptor "Descriptor technical details" A SIFT descriptor is a 3-D spatial histogram of the image gradients in characterizing the appearance of a keypoint. The gradient at each pixel is regarded as a sample of a three-dimensional elementary feature vector, formed by the pixel location and the gradient orientation. Samples are weighed by the gradient norm and accumulated in a 3-D histogram @em h, which (up to normalization and clamping) forms the SIFT descriptor of the region. An additional Gaussian weighting function is applied to give less importance to gradients farther away from the keypoint center. Orientations are quantized into eight bins and the spatial coordinates into four each, as follows: @image html sift-descr-easy.png "The SIFT descriptor is a spatial histogram of the image gradient." SIFT descriptors are computed by either calling ::vl_sift_calc_keypoint_descriptor or ::vl_sift_calc_raw_descriptor. They accept as input a keypoint frame, which specifies the descriptor center, its size, and its orientation on the image plane. The following parameters influence the descriptor calculation: - magnification factor. The descriptor size is determined by multiplying the keypoint scale by this factor. It is set by ::vl_sift_set_magnif. - Gaussian window size. The descriptor support is determined by a Gaussian window, which discounts gradient contributions farther away from the descriptor center. The standard deviation of this window is set by ::vl_sift_set_window_size and expressed in unit of bins. VLFeat SIFT descriptor uses the following convention. The @em y axis points downwards and angles are measured clockwise (to be consistent with the standard image convention). The 3-D histogram (consisting of @f$ 8 \times 4 \times 4 = 128 @f$ bins) is stacked as a single 128-dimensional vector, where the fastest varying dimension is the orientation and the slowest the @em y spatial coordinate. This is illustrated by the following figure. @image html sift-conv-vlfeat.png "VLFeat conventions" @note Keypoints (frames) D. Lowe's SIFT implementation convention is slightly different: The @em y axis points upwards and the angles are measured counter-clockwise. @image html sift-conv.png "D. Lowes' SIFT implementation conventions"
Summary of the parameters influencing the SIFT descriptor.
Parameter See also Controlled by Comment
magnification factor @ref sift-intro-descriptor ::vl_sift_set_magnif increase this value to enlarge the image region described
Gaussian window size @ref sift-intro-descriptor ::vl_sift_set_window_size smaller values let the center of the descriptor count more
@section sift-intro-extensions Extensions Eliminating low-contrast descriptors. Near-uniform patches do not yield stable keypoints or descriptors. ::vl_sift_set_norm_thresh() can be used to set a threshold on the average norm of the local gradient to zero-out descriptors that correspond to very low contrast regions. By default, the threshold is equal to zero, which means that no descriptor is zeroed. Normally this option is useful only with custom keypoints, as detected keypoints are implicitly selected at high contrast image regions. @section sift-usage Using the SIFT filter object The code provided in this module can be used in different ways. You can instantiate and use a SIFT filter to extract both SIFT keypoints and descriptors from one or multiple images. Alternatively, you can use one of the low level functions to run only a part of the SIFT algorithm (for instance, to compute the SIFT descriptors of custom keypoints). To use a SIFT filter object: - Initialize a SIFT filter object with ::vl_sift_new(). The filter can be reused for multiple images of the same size (e.g. for an entire video sequence). - For each octave in the scale space: - Compute the next octave of the DOG scale space using either ::vl_sift_process_first_octave() or ::vl_sift_process_next_octave() (stop processing if ::VL_ERR_EOF is returned). - Run the SIFT detector with ::vl_sift_detect() to get the keypoints. - For each keypoint: - Use ::vl_sift_calc_keypoint_orientations() to get the keypoint orientation(s). - For each orientation: - Use ::vl_sift_calc_keypoint_descriptor() to get the keypoint descriptor. - Delete the SIFT filter by ::vl_sift_delete(). To compute SIFT descriptors of custom keypoints, use ::vl_sift_calc_raw_descriptor(). @section sift-tech Technical details @subsection sift-tech-ss Scale space In order to search for image blobs at multiple scale, the SIFT detector construct a scale space, defined as follows. Let @f$I_0(\mathbf{x})@f$ denote an idealized infinite resolution image. Consider the Gaussian kernel @f[ g_{\sigma}(\mathbf{x}) = \frac{1}{2\pi\sigma^2} \exp \left( -\frac{1}{2} \frac{\mathbf{x}^\top\mathbf{x}}{\sigma^2} \right) @f] The Gaussian scale space is the collection of smoothed images @f[ I_\sigma = g_\sigma * I, \quad \sigma \geq 0. @f] The image at infinite resolution @f$ I_0 @f$ is useful conceptually, but is not available to us; instead, the input image @f$ I_{\sigma_n} @f$ is assumed to be pre-smoothed at a nominal level @f$ \sigma_n = 0.5 @f$ to account for the finite resolution of the pixels. Thus in practice the scale space is computed by @f[ I_\sigma = g_{\sqrt{\sigma^2 - \sigma_n^2}} * I_{\sigma_n}, \quad \sigma \geq \sigma_n. @f] Scales are sampled at logarithmic steps given by @f[ \sigma = \sigma_0 2^{o+s/S}, \quad s = 0,\dots,S-1, \quad o = o_{\min}, \dots, o_{\min}+O-1, @f] where @f$ \sigma_0 = 1.6 @f$ is the base scale, @f$ o_{\min} @f$ is the first octave index, @em O the number of octaves and @em S the number of scales per octave. Blobs are detected as local extrema of the Difference of Gaussians (DoG) scale space, obtained by subtracting successive scales of the Gaussian scale space: @f[ \mathrm{DoG}_{\sigma(o,s)} = I_{\sigma(o,s+1)} - I_{\sigma(o,s)} @f] At each next octave, the resolution of the images is halved to save computations. The images composing the Gaussian and DoG scale space can then be arranged as in the following figure: @image html sift-ss.png "GSS and DoG scale space structures." The black vertical segments represent images of the Gaussian Scale Space (GSS), arranged by increasing scale @f$\sigma@f$. Notice that the scale level index @e s varies in a slightly redundant set @f[ s = -1, \dots, S+2 @f] This simplifies glueing together different octaves and extracting DoG maxima (required by the SIFT detector). @subsection sift-tech-detector Detector The SIFT frames (keypoints) are extracted based on local extrema (peaks) of the DoG scale space. Numerically, local extrema are elements whose @f$ 3 \times 3 \times 3 @f$ neighbors (in space and scale) have all smaller (or larger) value. Once extracted, local extrema are quadratically interpolated (this is very important especially at the lower resolution scales in order to have accurate keypoint localization at the full resolution). Finally, they are filtered to eliminate low-contrast responses or responses close to edges and the orientation(s) are assigned, as explained next. @subsubsection sift-tech-detector-peak Eliminating low contrast responses Peaks which are too short may have been generated by noise and are discarded. This is done by comparing the absolute value of the DoG scale space at the peak with the peak threshold @f$t_p@f$ and discarding the peak its value is below the threshold. @subsubsection sift-tech-detector-edge Eliminating edge responses Peaks which are too flat are often generated by edges and do not yield stable features. These peaks are detected and removed as follows. Given a peak @f$x,y,\sigma@f$, the algorithm evaluates the @em x,@em y Hessian of of the DoG scale space at the scale @f$\sigma@f$. Then the following score (similar to the Harris function) is computed: @f[ \frac{(\mathrm{tr}\,D(x,y,\sigma))^2}{\det D(x,y,\sigma)}, \quad D = \left[ \begin{array}{cc} \frac{\partial^2 \mathrm{DoG}}{\partial x^2} & \frac{\partial^2 \mathrm{DoG}}{\partial x\partial y} \\ \frac{\partial^2 \mathrm{DoG}}{\partial x\partial y} & \frac{\partial^2 \mathrm{DoG}}{\partial y^2} \end{array} \right]. @f] This score has a minimum (equal to 4) when both eigenvalues of the Jacobian are equal (curved peak) and increases as one of the eigenvalues grows and the other stays small. Peaks are retained if the score is below the quantity @f$(t_e+1)(t_e+1)/t_e@f$, where @f$t_e@f$ is the edge threshold. Notice that this quantity has a minimum equal to 4 when @f$t_e=1@f$ and grows thereafter. Therefore the range of the edge threshold is @f$[1,\infty)@f$. @subsection sift-tech-detector-orientation Orientation assignment A peak in the DoG scale space fixes 2 parameters of the keypoint: the position and scale. It remains to choose an orientation. In order to do this, SIFT computes an histogram of the gradient orientations in a Gaussian window with a standard deviation which is 1.5 times bigger than the scale @f$\sigma@f$ of the keypoint. @image html sift-orient.png This histogram is then smoothed and the maximum is selected. In addition to the biggest mode, up to other three modes whose amplitude is within the 80% of the biggest mode are retained and returned as additional orientations. @subsection sift-tech-descriptor Descriptor A SIFT descriptor of a local region (keypoint) is a 3-D spatial histogram of the image gradients. The gradient at each pixel is regarded as a sample of a three-dimensional elementary feature vector, formed by the pixel location and the gradient orientation. Samples are weighed by the gradient norm and accumulated in a 3-D histogram @em h, which (up to normalization and clamping) forms the SIFT descriptor of the region. An additional Gaussian weighting function is applied to give less importance to gradients farther away from the keypoint center. @subsubsection sift-tech-descriptor-can Construction in the canonical frame Denote the gradient vector field computed at the scale @f$ \sigma @f$ by @f[ J(x,y) = \nabla I_\sigma(x,y) = \left[\begin{array}{cc} \frac{\partial I_\sigma}{\partial x} & \frac{\partial I_\sigma}{\partial y} & \end{array}\right] @f] The descriptor is a 3-D spatial histogram capturing the distribution of @f$ J(x,y) @f$. It is convenient to describe its construction in the canonical frame. In this frame, the image and descriptor axes coincide and each spatial bin has side 1. The histogram has @f$ N_\theta \times N_x \times N_y @f$ bins (usually @f$ 8 \times 4 \times 4 @f$), as in the following figure: @image html sift-can.png Canonical SIFT descriptor and spatial binning functions Bins are indexed by a triplet of indexes t, i, j and their centers are given by @f{eqnarray*} \theta_t &=& \frac{2\pi}{N_\theta} t, \quad t = 0,\dots,N_{\theta}-1, \\ x_i &=& i - \frac{N_x -1}{2}, \quad i = 0,\dots,N_x-1, \\ y_j &=& j - \frac{N_x -1}{2}, \quad j = 0,\dots,N_y-1. \\ @f} The histogram is computed by using trilinear interpolation, i.e. by weighing contributions by the binning functions @f{eqnarray*} \displaystyle w(z) &=& \mathrm{max}(0, 1 - |z|), \\ \displaystyle w_\mathrm{ang}(z) &=& \sum_{k=-\infty}^{+\infty} w\left( \frac{N_\theta}{2\pi} z + N_\theta k \right). @f} The gradient vector field is transformed in a three-dimensional density map of weighed contributions @f[ f(\theta, x, y) = |J(x,y)| \delta(\theta - \angle J(x,y)) @f] The historam is localized in the keypoint support by a Gaussian window of standard deviation @f$ \sigma_{\mathrm{win}} @f$. The histogram is then given by @f{eqnarray*} h(t,i,j) &=& \int g_{\sigma_\mathrm{win}}(x,y) w_\mathrm{ang}(\theta - \theta_t) w(x-x_i) w(y-y_j) f(\theta,x,y) d\theta\,dx\,dy \\ &=& \int g_{\sigma_\mathrm{win}}(x,y) w_\mathrm{ang}(\angle J(x,y) - \theta_t) w(x-x_i) w(y-y_j) |J(x,y)|\,dx\,dy @f} In post processing, the histogram is @f$ l^2 @f$ normalized, then clamped at 0.2, and @f$ l^2 @f$ normalized again. @subsubsection sift-tech-descriptor-image Calculation in the image frame Invariance to similarity transformation is attained by attaching descriptors to SIFT keypoints (or other similarity-covariant frames). Then projecting the image in the canonical descriptor frames has the effect of undoing the image deformation. In practice, however, it is convenient to compute the descriptor directly in the image frame. To do this, denote with a hat quantities relative to the canonical frame and without a hat quantities relative to the image frame (so for instance @f$ \hat x @f$ is the @e x-coordinate in the canonical frame and @f$ x @f$ the x-coordinate in the image frame). Assume that canonical and image frame are related by an affinity: @f[ \mathbf{x} = A \hat{\mathbf{x}} + T, \qquad \mathbf{x} = \begin{bmatrix}{c} x \\ y \end{bmatrix}, \quad \mathbf{x} = \begin{bmatrix}{c} \hat x \\ \hat y \end{bmatrix}. @f] @image html sift-image-frame.png Then all quantities can be computed in the image frame directly. For instance, the image at infinite resolution in the two frames are related by @f[ \hat I_0(\hat{\mathbf{x}}) = I_0(\mathbf{x}), \qquad \mathbf{x} = A \hat{\mathbf{x}} + T. @f] The canonized image at scale @f$ \hat \sigma @f$ is in relation with the scaled image @f[ \hat I_{\hat{\sigma}}(\hat{\mathbf{x}}) = I_{A\hat{\sigma}}(\mathbf{x}), \qquad \mathbf{x} = A \hat{\mathbf{x}} + T @f] where, by generalizing the previous definitions, we have @f[ I_{A\hat \sigma}(\mathbf{x}) = (g_{A\hat\sigma} * I_0)(\mathbf{x}), \quad g_{A\hat\sigma}(\mathbf{x}) = \frac{1}{2\pi|A|\hat \sigma^2} \exp \left( -\frac{1}{2} \frac{\mathbf{x}^\top A^{-\top}A^{-1}\mathbf{x}}{\hat \sigma^2} \right) @f] Deriving shows that the gradient fields are in relation @f[ \hat J(\hat{\mathbf{x}}) = J(\mathbf{x}) A, \quad J(\mathbf{x}) = (\nabla I_{A\hat\sigma})(\mathbf{x}), \qquad \mathbf{x} = A \hat{\mathbf{x}} + T. @f] Therefore we can compute the descriptor either in the image or canonical frame as: @f{eqnarray*} h(t,i,j) &=& \int g_{\hat \sigma_\mathrm{win}}(\hat{\mathbf{x}})\, w_\mathrm{ang}(\angle \hat J(\hat{\mathbf{x}}) - \theta_t)\, w_{ij}(\hat{\mathbf{x}})\, |\hat J(\hat{\mathbf{x}})|\, d\hat{\mathbf{x}} \\ &=& \int g_{A \hat \sigma_\mathrm{win}}(\mathbf{x} - T)\, w_\mathrm{ang}(\angle J(\mathbf{x})A - \theta_t)\, w_{ij}(A^{-1}(\mathbf{x} - T))\, |J(\mathbf{x})A|\, d\mathbf{x}. @f} where we defined the product of the two spatial binning functions @f[ w_{ij}(\hat{\mathbf{x}}) = w(\hat x - \hat x_i) w(\hat y - \hat y_j) @f] In the actual implementation, this integral is computed by visiting a rectangular area of the image that fully contains the keypoint grid (along with half a bin border to fully include the bin windowing function). Since the descriptor can be rotated, this area is a rectangle of sides @f$m/2\sqrt{2} (N_x+1,N_y+1)@f$ (see also the illustration). @subsubsection sift-tech-descriptor-std Standard SIFT descriptor For a SIFT-detected keypoint of center @f$ T @f$, scale @f$ \sigma @f$ and orientation @f$ \theta @f$, the affine transformation @f$ (A,T) @f$ reduces to the similarity transformation @f[ \mathbf{x} = m \sigma R(\theta) \hat{\mathbf{x}} + T @f] where @f$ R(\theta) @f$ is a counter-clockwise rotation of @f$ \theta @f$ radians, @f$ m \mathcal{\sigma} @f$ is the size of a descriptor bin in pixels, and @e m is the descriptor magnification factor which expresses how much larger a descriptor bin is compared to the scale of the keypoint @f$ \sigma @f$ (the default value is @e m = 3). Moreover, the standard SIFT descriptor computes the image gradient at the scale of the keypoints, which in the canonical frame is equivalent to a smoothing of @f$ \hat \sigma = 1/m @f$. Finally, the default Gaussian window size is set to have standard deviation @f$ \hat \sigma_\mathrm{win} = 2 @f$. This yields the formula @f{eqnarray*} h(t,i,j) &=& m \sigma \int g_{\sigma_\mathrm{win}}(\mathbf{x} - T)\, w_\mathrm{ang}(\angle J(\mathbf{x}) - \theta - \theta_t)\, w_{ij}\left(\frac{R(\theta)^\top \mathbf{x} - T}{m\sigma}\right)\, |J(\mathbf{x})|\, d\mathbf{x}, \\ \sigma_{\mathrm{win}} &=& m\sigma\hat \sigma_{\mathrm{win}}, \\ J(\mathbf{x}) &=& \nabla (g_{m \sigma \hat \sigma} * I)(\mathbf{x}) = \nabla (g_{\sigma} * I)(\mathbf{x}) = \nabla I_{\sigma} (\mathbf{x}). @f} **/ #include "sift.h" #include "imopv.h" #include "mathop.h" #include #include #include #include #include /** @internal @brief Use bilinear interpolation to compute orientations */ #define VL_SIFT_BILINEAR_ORIENTATIONS 1 #define EXPN_SZ 256 /**< ::fast_expn table size @internal */ #define EXPN_MAX 25.0 /**< ::fast_expn table max @internal */ double expn_tab [EXPN_SZ+1] ; /**< ::fast_expn table @internal */ #define NBO 8 #define NBP 4 #define log2(x) (log(x)/VL_LOG_OF_2) /** ------------------------------------------------------------------ ** @internal ** @brief Fast @f$exp(-x)@f$ approximation ** ** @param x argument. ** ** The argument must be in the range [0, ::EXPN_MAX] . ** ** @return approximation of @f$exp(-x)@f$. **/ VL_INLINE double fast_expn (double x) { double a,b,r ; int i ; /*assert(0 <= x && x <= EXPN_MAX) ;*/ if (x > EXPN_MAX) return 0.0 ; x *= EXPN_SZ / EXPN_MAX ; i = (int)vl_floor_d (x) ; r = x - i ; a = expn_tab [i ] ; b = expn_tab [i + 1] ; return a + r * (b - a) ; } /** ------------------------------------------------------------------ ** @internal ** @brief Initialize tables for ::fast_expn **/ VL_INLINE void fast_expn_init () { int k ; for(k = 0 ; k < EXPN_SZ + 1 ; ++ k) { expn_tab [k] = exp (- (double) k * (EXPN_MAX / EXPN_SZ)) ; } } /** ------------------------------------------------------------------ ** @internal ** @brief Copy image, upsample rows and take transpose ** ** @param dst output image buffer. ** @param src input image buffer. ** @param width input image width. ** @param height input image height. ** ** The output image has dimensions @a height by 2 @a width (so the ** destination buffer must be at least as big as two times the ** input buffer). ** ** Upsampling is performed by linear interpolation. **/ static void copy_and_upsample_rows (vl_sift_pix *dst, vl_sift_pix const *src, int width, int height) { int x, y ; vl_sift_pix a, b ; for(y = 0 ; y < height ; ++y) { b = a = *src++ ; for(x = 0 ; x < width - 1 ; ++x) { b = *src++ ; *dst = a ; dst += height ; *dst = 0.5 * (a + b) ; dst += height ; a = b ; } *dst = b ; dst += height ; *dst = b ; dst += height ; dst += 1 - width * 2 * height ; } } /** ------------------------------------------------------------------ ** @internal ** @brief Smooth an image ** @param self SIFT filter. ** @param outputImage output imgae buffer. ** @param tempImage temporary image buffer. ** @param inputImage input image buffer. ** @param width input image width. ** @param height input image height. ** @param sigma smoothing. **/ static void _vl_sift_smooth (VlSiftFilt * self, vl_sift_pix * outputImage, vl_sift_pix * tempImage, vl_sift_pix const * inputImage, vl_size width, vl_size height, double sigma) { /* prepare Gaussian filter */ if (self->gaussFilterSigma != sigma) { vl_uindex j ; vl_sift_pix acc = 0 ; if (self->gaussFilter) vl_free (self->gaussFilter) ; self->gaussFilterWidth = VL_MAX(ceil(4.0 * sigma), 1) ; self->gaussFilterSigma = sigma ; self->gaussFilter = vl_malloc (sizeof(vl_sift_pix) * (2 * self->gaussFilterWidth + 1)) ; for (j = 0 ; j < 2 * self->gaussFilterWidth + 1 ; ++j) { vl_sift_pix d = ((vl_sift_pix)((signed)j - (signed)self->gaussFilterWidth)) / ((vl_sift_pix)sigma) ; self->gaussFilter[j] = (vl_sift_pix) exp (- 0.5 * (d*d)) ; acc += self->gaussFilter[j] ; } for (j = 0 ; j < 2 * self->gaussFilterWidth + 1 ; ++j) { self->gaussFilter[j] /= acc ; } } if (self->gaussFilterWidth == 0) { memcpy (outputImage, inputImage, sizeof(vl_sift_pix) * width * height) ; return ; } vl_imconvcol_vf (tempImage, height, inputImage, width, height, width, self->gaussFilter, - self->gaussFilterWidth, self->gaussFilterWidth, 1, VL_PAD_BY_CONTINUITY | VL_TRANSPOSE) ; vl_imconvcol_vf (outputImage, width, tempImage, height, width, height, self->gaussFilter, - self->gaussFilterWidth, self->gaussFilterWidth, 1, VL_PAD_BY_CONTINUITY | VL_TRANSPOSE) ; } /** ------------------------------------------------------------------ ** @internal ** @brief Copy and downsample an image ** ** @param dst output imgae buffer. ** @param src input image buffer. ** @param width input image width. ** @param height input image height. ** @param d octaves (non negative). ** ** The function downsamples the image @a d times, reducing it to @c ** 1/2^d of its original size. The parameters @a width and @a height ** are the size of the input image. The destination image @a dst is ** assumed to be floor(width/2^d) pixels wide and ** floor(height/2^d) pixels high. **/ static void copy_and_downsample (vl_sift_pix *dst, vl_sift_pix const *src, int width, int height, int d) { int x, y ; d = 1 << d ; /* d = 2^d */ for(y = 0 ; y < height ; y+=d) { vl_sift_pix const * srcrowp = src + y * width ; for(x = 0 ; x < width - (d-1) ; x+=d) { *dst++ = *srcrowp ; srcrowp += d ; } } } /** ------------------------------------------------------------------ ** @brief Create a new SIFT filter ** ** @param width image width. ** @param height image height. ** @param noctaves number of octaves. ** @param nlevels number of levels per octave. ** @param o_min first octave index. ** ** The function allocates and returns a new SIFT filter for the ** specified image and scale space geometry. ** ** Setting @a O to a negative value sets the number of octaves to the ** maximum possible value depending on the size of the image. ** ** @return the new SIFT filter. ** @sa ::vl_sift_delete(). **/ VL_EXPORT VlSiftFilt * vl_sift_new (int width, int height, int noctaves, int nlevels, int o_min) { VlSiftFilt *f = vl_malloc (sizeof(VlSiftFilt)) ; int w = VL_SHIFT_LEFT (width, -o_min) ; int h = VL_SHIFT_LEFT (height, -o_min) ; int nel = w * h ; /* negative value O => calculate max. value */ if (noctaves < 0) { noctaves = VL_MAX (floor (log2 (VL_MIN(width, height))) - o_min - 3, 1) ; } f-> width = width ; f-> height = height ; f-> O = noctaves ; f-> S = nlevels ; f-> o_min = o_min ; f-> s_min = -1 ; f-> s_max = nlevels + 1 ; f-> o_cur = o_min ; f-> temp = vl_malloc (sizeof(vl_sift_pix) * nel ) ; f-> octave = vl_malloc (sizeof(vl_sift_pix) * nel * (f->s_max - f->s_min + 1) ) ; f-> dog = vl_malloc (sizeof(vl_sift_pix) * nel * (f->s_max - f->s_min ) ) ; f-> grad = vl_malloc (sizeof(vl_sift_pix) * nel * 2 * (f->s_max - f->s_min ) ) ; f-> sigman = 0.5 ; f-> sigmak = pow (2.0, 1.0 / nlevels) ; f-> sigma0 = 1.6 * f->sigmak ; f-> dsigma0 = f->sigma0 * sqrt (1.0 - 1.0 / (f->sigmak*f->sigmak)) ; f-> gaussFilter = NULL ; f-> gaussFilterSigma = 0 ; f-> gaussFilterWidth = 0 ; f-> octave_width = 0 ; f-> octave_height = 0 ; f-> keys = 0 ; f-> nkeys = 0 ; f-> keys_res = 0 ; f-> peak_thresh = 0.0 ; f-> edge_thresh = 10.0 ; f-> norm_thresh = 0.0 ; f-> magnif = 3.0 ; f-> windowSize = NBP / 2 ; f-> grad_o = o_min - 1 ; /* initialize fast_expn stuff */ fast_expn_init () ; return f ; } /** ------------------------------------------------------------------- ** @brief Delete SIFT filter ** ** @param f SIFT filter to delete. ** ** The function frees the resources allocated by ::vl_sift_new(). **/ VL_EXPORT void vl_sift_delete (VlSiftFilt* f) { if (f) { if (f->keys) vl_free (f->keys) ; if (f->grad) vl_free (f->grad) ; if (f->dog) vl_free (f->dog) ; if (f->octave) vl_free (f->octave) ; if (f->temp) vl_free (f->temp) ; if (f->gaussFilter) vl_free (f->gaussFilter) ; vl_free (f) ; } } /** ------------------------------------------------------------------ ** @brief Start processing a new image ** ** @param f SIFT filter. ** @param im image data. ** ** The function starts processing a new image by computing its ** Gaussian scale space at the lower octave. It also empties the ** internal keypoint buffer. ** ** @return error code. The function returns ::VL_ERR_EOF if there are ** no more octaves to process. ** ** @sa ::vl_sift_process_next_octave(). **/ VL_EXPORT int vl_sift_process_first_octave (VlSiftFilt *f, vl_sift_pix const *im) { int o, s, h, w ; double sa, sb ; vl_sift_pix *octave ; /* shortcuts */ vl_sift_pix *temp = f-> temp ; int width = f-> width ; int height = f-> height ; int o_min = f-> o_min ; int s_min = f-> s_min ; int s_max = f-> s_max ; double sigma0 = f-> sigma0 ; double sigmak = f-> sigmak ; double sigman = f-> sigman ; double dsigma0 = f-> dsigma0 ; /* restart from the first */ f->o_cur = o_min ; f->nkeys = 0 ; w = f-> octave_width = VL_SHIFT_LEFT(f->width, - f->o_cur) ; h = f-> octave_height = VL_SHIFT_LEFT(f->height, - f->o_cur) ; /* is there at least one octave? */ if (f->O == 0) return VL_ERR_EOF ; /* ------------------------------------------------------------------ * Compute the first sublevel of the first octave * --------------------------------------------------------------- */ /* * If the first octave has negative index, we upscale the image; if * the first octave has positive index, we downscale the image; if * the first octave has index zero, we just copy the image. */ octave = vl_sift_get_octave (f, s_min) ; if (o_min < 0) { /* double once */ copy_and_upsample_rows (temp, im, width, height) ; copy_and_upsample_rows (octave, temp, height, 2 * width ) ; /* double more */ for(o = -1 ; o > o_min ; --o) { copy_and_upsample_rows (temp, octave, width << -o, height << -o ) ; copy_and_upsample_rows (octave, temp, width << -o, 2 * (height << -o)) ; } } else if (o_min > 0) { /* downsample */ copy_and_downsample (octave, im, width, height, o_min) ; } else { /* direct copy */ memcpy(octave, im, sizeof(vl_sift_pix) * width * height) ; } /* * Here we adjust the smoothing of the first level of the octave. * The input image is assumed to have nominal smoothing equal to * f->simgan. */ sa = sigma0 * pow (sigmak, s_min) ; sb = sigman * pow (2.0, - o_min) ; if (sa > sb) { double sd = sqrt (sa*sa - sb*sb) ; _vl_sift_smooth (f, octave, temp, octave, w, h, sd) ; } /* ----------------------------------------------------------------- * Compute the first octave * -------------------------------------------------------------- */ for(s = s_min + 1 ; s <= s_max ; ++s) { double sd = dsigma0 * pow (sigmak, s) ; _vl_sift_smooth (f, vl_sift_get_octave(f, s), temp, vl_sift_get_octave(f, s - 1), w, h, sd) ; } return VL_ERR_OK ; } /** ------------------------------------------------------------------ ** @brief Process next octave ** ** @param f SIFT filter. ** ** The function computes the next octave of the Gaussian scale space. ** Notice that this clears the record of any feature detected in the ** previous octave. ** ** @return error code. The function returns the error ** ::VL_ERR_EOF when there are no more octaves to process. ** ** @sa ::vl_sift_process_first_octave(). **/ VL_EXPORT int vl_sift_process_next_octave (VlSiftFilt *f) { int s, h, w, s_best ; double sa, sb ; vl_sift_pix *octave, *pt ; /* shortcuts */ vl_sift_pix *temp = f-> temp ; int O = f-> O ; int S = f-> S ; int o_min = f-> o_min ; int s_min = f-> s_min ; int s_max = f-> s_max ; double sigma0 = f-> sigma0 ; double sigmak = f-> sigmak ; double dsigma0 = f-> dsigma0 ; /* is there another octave ? */ if (f->o_cur == o_min + O - 1) return VL_ERR_EOF ; /* retrieve base */ s_best = VL_MIN(s_min + S, s_max) ; w = vl_sift_get_octave_width (f) ; h = vl_sift_get_octave_height (f) ; pt = vl_sift_get_octave (f, s_best) ; octave = vl_sift_get_octave (f, s_min) ; /* next octave */ copy_and_downsample (octave, pt, w, h, 1) ; f-> o_cur += 1 ; f-> nkeys = 0 ; w = f-> octave_width = VL_SHIFT_LEFT(f->width, - f->o_cur) ; h = f-> octave_height = VL_SHIFT_LEFT(f->height, - f->o_cur) ; sa = sigma0 * powf (sigmak, s_min ) ; sb = sigma0 * powf (sigmak, s_best - S) ; if (sa > sb) { double sd = sqrt (sa*sa - sb*sb) ; _vl_sift_smooth (f, octave, temp, octave, w, h, sd) ; } /* ------------------------------------------------------------------ * Fill octave * --------------------------------------------------------------- */ for(s = s_min + 1 ; s <= s_max ; ++s) { double sd = dsigma0 * pow (sigmak, s) ; _vl_sift_smooth (f, vl_sift_get_octave(f, s), temp, vl_sift_get_octave(f, s - 1), w, h, sd) ; } return VL_ERR_OK ; } /** ------------------------------------------------------------------ ** @brief Detect keypoints ** ** The function detect keypoints in the current octave filling the ** internal keypoint buffer. Keypoints can be retrieved by ** ::vl_sift_get_keypoints(). ** ** @param f SIFT filter. **/ VL_EXPORT void vl_sift_detect (VlSiftFilt * f) { vl_sift_pix* dog = f-> dog ; int s_min = f-> s_min ; int s_max = f-> s_max ; int w = f-> octave_width ; int h = f-> octave_height ; double te = f-> edge_thresh ; double tp = f-> peak_thresh ; int const xo = 1 ; /* x-stride */ int const yo = w ; /* y-stride */ int const so = w * h ; /* s-stride */ double xper = pow (2.0, f->o_cur) ; int x, y, s, i, ii, jj ; vl_sift_pix *pt, v ; VlSiftKeypoint *k ; /* clear current list */ f-> nkeys = 0 ; /* compute difference of gaussian (DoG) */ pt = f-> dog ; for (s = s_min ; s <= s_max - 1 ; ++s) { vl_sift_pix* src_a = vl_sift_get_octave (f, s ) ; vl_sift_pix* src_b = vl_sift_get_octave (f, s + 1) ; vl_sift_pix* end_a = src_a + w * h ; while (src_a != end_a) { *pt++ = *src_b++ - *src_a++ ; } } /* ----------------------------------------------------------------- * Find local maxima of DoG * -------------------------------------------------------------- */ /* start from dog [1,1,s_min+1] */ pt = dog + xo + yo + so ; for(s = s_min + 1 ; s <= s_max - 2 ; ++s) { for(y = 1 ; y < h - 1 ; ++y) { for(x = 1 ; x < w - 1 ; ++x) { v = *pt ; #define CHECK_NEIGHBORS(CMP,SGN) \ ( v CMP ## = SGN 0.8 * tp && \ v CMP *(pt + xo) && \ v CMP *(pt - xo) && \ v CMP *(pt + so) && \ v CMP *(pt - so) && \ v CMP *(pt + yo) && \ v CMP *(pt - yo) && \ \ v CMP *(pt + yo + xo) && \ v CMP *(pt + yo - xo) && \ v CMP *(pt - yo + xo) && \ v CMP *(pt - yo - xo) && \ \ v CMP *(pt + xo + so) && \ v CMP *(pt - xo + so) && \ v CMP *(pt + yo + so) && \ v CMP *(pt - yo + so) && \ v CMP *(pt + yo + xo + so) && \ v CMP *(pt + yo - xo + so) && \ v CMP *(pt - yo + xo + so) && \ v CMP *(pt - yo - xo + so) && \ \ v CMP *(pt + xo - so) && \ v CMP *(pt - xo - so) && \ v CMP *(pt + yo - so) && \ v CMP *(pt - yo - so) && \ v CMP *(pt + yo + xo - so) && \ v CMP *(pt + yo - xo - so) && \ v CMP *(pt - yo + xo - so) && \ v CMP *(pt - yo - xo - so) ) if (CHECK_NEIGHBORS(>,+) || CHECK_NEIGHBORS(<,-) ) { /* make room for more keypoints */ if (f->nkeys >= f->keys_res) { f->keys_res += 500 ; if (f->keys) { f->keys = vl_realloc (f->keys, f->keys_res * sizeof(VlSiftKeypoint)) ; } else { f->keys = vl_malloc (f->keys_res * sizeof(VlSiftKeypoint)) ; } } k = f->keys + (f->nkeys ++) ; k-> ix = x ; k-> iy = y ; k-> is = s ; } pt += 1 ; } pt += 2 ; } pt += 2 * yo ; } /* ----------------------------------------------------------------- * Refine local maxima * -------------------------------------------------------------- */ /* this pointer is used to write the keypoints back */ k = f->keys ; for (i = 0 ; i < f->nkeys ; ++i) { int x = f-> keys [i] .ix ; int y = f-> keys [i] .iy ; int s = f-> keys [i]. is ; double Dx=0,Dy=0,Ds=0,Dxx=0,Dyy=0,Dss=0,Dxy=0,Dxs=0,Dys=0 ; double A [3*3], b [3] ; int dx = 0 ; int dy = 0 ; int iter, i, j ; for (iter = 0 ; iter < 5 ; ++iter) { x += dx ; y += dy ; pt = dog + xo * x + yo * y + so * (s - s_min) ; /** @brief Index GSS @internal */ #define at(dx,dy,ds) (*( pt + (dx)*xo + (dy)*yo + (ds)*so)) /** @brief Index matrix A @internal */ #define Aat(i,j) (A[(i)+(j)*3]) /* compute the gradient */ Dx = 0.5 * (at(+1,0,0) - at(-1,0,0)) ; Dy = 0.5 * (at(0,+1,0) - at(0,-1,0)); Ds = 0.5 * (at(0,0,+1) - at(0,0,-1)) ; /* compute the Hessian */ Dxx = (at(+1,0,0) + at(-1,0,0) - 2.0 * at(0,0,0)) ; Dyy = (at(0,+1,0) + at(0,-1,0) - 2.0 * at(0,0,0)) ; Dss = (at(0,0,+1) + at(0,0,-1) - 2.0 * at(0,0,0)) ; Dxy = 0.25 * ( at(+1,+1,0) + at(-1,-1,0) - at(-1,+1,0) - at(+1,-1,0) ) ; Dxs = 0.25 * ( at(+1,0,+1) + at(-1,0,-1) - at(-1,0,+1) - at(+1,0,-1) ) ; Dys = 0.25 * ( at(0,+1,+1) + at(0,-1,-1) - at(0,-1,+1) - at(0,+1,-1) ) ; /* solve linear system ....................................... */ Aat(0,0) = Dxx ; Aat(1,1) = Dyy ; Aat(2,2) = Dss ; Aat(0,1) = Aat(1,0) = Dxy ; Aat(0,2) = Aat(2,0) = Dxs ; Aat(1,2) = Aat(2,1) = Dys ; b[0] = - Dx ; b[1] = - Dy ; b[2] = - Ds ; /* Gauss elimination */ for(j = 0 ; j < 3 ; ++j) { double maxa = 0 ; double maxabsa = 0 ; int maxi = -1 ; double tmp ; /* look for the maximally stable pivot */ for (i = j ; i < 3 ; ++i) { double a = Aat (i,j) ; double absa = vl_abs_d (a) ; if (absa > maxabsa) { maxa = a ; maxabsa = absa ; maxi = i ; } } /* if singular give up */ if (maxabsa < 1e-10f) { b[0] = 0 ; b[1] = 0 ; b[2] = 0 ; break ; } i = maxi ; /* swap j-th row with i-th row and normalize j-th row */ for(jj = j ; jj < 3 ; ++jj) { tmp = Aat(i,jj) ; Aat(i,jj) = Aat(j,jj) ; Aat(j,jj) = tmp ; Aat(j,jj) /= maxa ; } tmp = b[j] ; b[j] = b[i] ; b[i] = tmp ; b[j] /= maxa ; /* elimination */ for (ii = j+1 ; ii < 3 ; ++ii) { double x = Aat(ii,j) ; for (jj = j ; jj < 3 ; ++jj) { Aat(ii,jj) -= x * Aat(j,jj) ; } b[ii] -= x * b[j] ; } } /* backward substitution */ for (i = 2 ; i > 0 ; --i) { double x = b[i] ; for (ii = i-1 ; ii >= 0 ; --ii) { b[ii] -= x * Aat(ii,i) ; } } /* .......................................................... */ /* If the translation of the keypoint is big, move the keypoint * and re-iterate the computation. Otherwise we are all set. */ dx= ((b[0] > 0.6 && x < w - 2) ? 1 : 0) + ((b[0] < -0.6 && x > 1 ) ? -1 : 0) ; dy= ((b[1] > 0.6 && y < h - 2) ? 1 : 0) + ((b[1] < -0.6 && y > 1 ) ? -1 : 0) ; if (dx == 0 && dy == 0) break ; } /* check threshold and other conditions */ { double val = at(0,0,0) + 0.5 * (Dx * b[0] + Dy * b[1] + Ds * b[2]) ; double score = (Dxx+Dyy)*(Dxx+Dyy) / (Dxx*Dyy - Dxy*Dxy) ; double xn = x + b[0] ; double yn = y + b[1] ; double sn = s + b[2] ; vl_bool good = vl_abs_d (val) > tp && score < (te+1)*(te+1)/te && score >= 0 && vl_abs_d (b[0]) < 1.5 && vl_abs_d (b[1]) < 1.5 && vl_abs_d (b[2]) < 1.5 && xn >= 0 && xn <= w - 1 && yn >= 0 && yn <= h - 1 && sn >= s_min && sn <= s_max ; if (good) { k-> o = f->o_cur ; k-> ix = x ; k-> iy = y ; k-> is = s ; k-> s = sn ; k-> x = xn * xper ; k-> y = yn * xper ; k-> sigma = f->sigma0 * pow (2.0, sn/f->S) * xper ; ++ k ; } } /* done checking */ } /* next keypoint to refine */ /* update keypoint count */ f-> nkeys = (int)(k - f->keys) ; } /** ------------------------------------------------------------------ ** @internal ** @brief Update gradients to current GSS octave ** ** @param f SIFT filter. ** ** The function makes sure that the gradient buffer is up-to-date ** with the current GSS data. ** ** @remark The minimum octave size is 2x2xS. **/ static void update_gradient (VlSiftFilt *f) { int s_min = f->s_min ; int s_max = f->s_max ; int w = vl_sift_get_octave_width (f) ; int h = vl_sift_get_octave_height (f) ; int const xo = 1 ; int const yo = w ; int const so = h * w ; int y, s ; if (f->grad_o == f->o_cur) return ; for (s = s_min + 1 ; s <= s_max - 2 ; ++ s) { vl_sift_pix *src, *end, *grad, gx, gy ; #define SAVE_BACK \ *grad++ = vl_fast_sqrt_f (gx*gx + gy*gy) ; \ *grad++ = vl_mod_2pi_f (vl_fast_atan2_f (gy, gx) + 2*VL_PI) ; \ ++src ; \ src = vl_sift_get_octave (f,s) ; grad = f->grad + 2 * so * (s - s_min -1) ; /* first pixel of the first row */ gx = src[+xo] - src[0] ; gy = src[+yo] - src[0] ; SAVE_BACK ; /* middle pixels of the first row */ end = (src - 1) + w - 1 ; while (src < end) { gx = 0.5 * (src[+xo] - src[-xo]) ; gy = src[+yo] - src[0] ; SAVE_BACK ; } /* last pixel of the first row */ gx = src[0] - src[-xo] ; gy = src[+yo] - src[0] ; SAVE_BACK ; for (y = 1 ; y < h -1 ; ++y) { /* first pixel of the middle rows */ gx = src[+xo] - src[0] ; gy = 0.5 * (src[+yo] - src[-yo]) ; SAVE_BACK ; /* middle pixels of the middle rows */ end = (src - 1) + w - 1 ; while (src < end) { gx = 0.5 * (src[+xo] - src[-xo]) ; gy = 0.5 * (src[+yo] - src[-yo]) ; SAVE_BACK ; } /* last pixel of the middle row */ gx = src[0] - src[-xo] ; gy = 0.5 * (src[+yo] - src[-yo]) ; SAVE_BACK ; } /* first pixel of the last row */ gx = src[+xo] - src[0] ; gy = src[ 0] - src[-yo] ; SAVE_BACK ; /* middle pixels of the last row */ end = (src - 1) + w - 1 ; while (src < end) { gx = 0.5 * (src[+xo] - src[-xo]) ; gy = src[0] - src[-yo] ; SAVE_BACK ; } /* last pixel of the last row */ gx = src[0] - src[-xo] ; gy = src[0] - src[-yo] ; SAVE_BACK ; } f->grad_o = f->o_cur ; } /** ------------------------------------------------------------------ ** @brief Calculate the keypoint orientation(s) ** ** @param f SIFT filter. ** @param angles orientations (output). ** @param k keypoint. ** ** The function computes the orientation(s) of the keypoint @a k. ** The function returns the number of orientations found (up to ** four). The orientations themselves are written to the vector @a ** angles. ** ** @remark The function requires the keypoint octave @a k->o to be ** equal to the filter current octave ::vl_sift_get_octave. If this ** is not the case, the function returns zero orientations. ** ** @remark The function requires the keypoint scale level @c k->s to ** be in the range @c s_min+1 and @c s_max-2 (where usually @c ** s_min=0 and @c s_max=S+2). If this is not the case, the function ** returns zero orientations. ** ** @return number of orientations found. **/ VL_EXPORT int vl_sift_calc_keypoint_orientations (VlSiftFilt *f, double angles [4], VlSiftKeypoint const *k) { double const winf = 1.5 ; double xper = pow (2.0, f->o_cur) ; int w = f-> octave_width ; int h = f-> octave_height ; int const xo = 2 ; /* x-stride */ int const yo = 2 * w ; /* y-stride */ int const so = 2 * w * h ; /* s-stride */ double x = k-> x / xper ; double y = k-> y / xper ; double sigma = k-> sigma / xper ; int xi = (int) (x + 0.5) ; int yi = (int) (y + 0.5) ; int si = k-> is ; double const sigmaw = winf * sigma ; int W = VL_MAX(floor (3.0 * sigmaw), 1) ; int nangles= 0 ; enum {nbins = 36} ; double hist [nbins], maxh ; vl_sift_pix const * pt ; int xs, ys, iter, i ; /* skip if the keypoint octave is not current */ if(k->o != f->o_cur) return 0 ; /* skip the keypoint if it is out of bounds */ if(xi < 0 || xi > w - 1 || yi < 0 || yi > h - 1 || si < f->s_min + 1 || si > f->s_max - 2 ) { return 0 ; } /* make gradient up to date */ update_gradient (f) ; /* clear histogram */ memset (hist, 0, sizeof(double) * nbins) ; /* compute orientation histogram */ pt = f-> grad + xo*xi + yo*yi + so*(si - f->s_min - 1) ; #undef at #define at(dx,dy) (*(pt + xo * (dx) + yo * (dy))) for(ys = VL_MAX (- W, - yi) ; ys <= VL_MIN (+ W, h - 1 - yi) ; ++ys) { for(xs = VL_MAX (- W, - xi) ; xs <= VL_MIN (+ W, w - 1 - xi) ; ++xs) { double dx = (double)(xi + xs) - x; double dy = (double)(yi + ys) - y; double r2 = dx*dx + dy*dy ; double wgt, mod, ang, fbin ; /* limit to a circular window */ if (r2 >= W*W + 0.6) continue ; wgt = fast_expn (r2 / (2*sigmaw*sigmaw)) ; mod = *(pt + xs*xo + ys*yo ) ; ang = *(pt + xs*xo + ys*yo + 1) ; fbin = nbins * ang / (2 * VL_PI) ; #if defined(VL_SIFT_BILINEAR_ORIENTATIONS) { int bin = (int) vl_floor_d (fbin - 0.5) ; double rbin = fbin - bin - 0.5 ; hist [(bin + nbins) % nbins] += (1 - rbin) * mod * wgt ; hist [(bin + 1 ) % nbins] += ( rbin) * mod * wgt ; } #else { int bin = vl_floor_d (fbin) ; bin = vl_floor_d (nbins * ang / (2*VL_PI)) ; hist [(bin) % nbins] += mod * wgt ; } #endif } /* for xs */ } /* for ys */ /* smooth histogram */ for (iter = 0; iter < 6; iter ++) { double prev = hist [nbins - 1] ; double first = hist [0] ; int i ; for (i = 0; i < nbins - 1; i++) { double newh = (prev + hist[i] + hist[(i+1) % nbins]) / 3.0; prev = hist[i] ; hist[i] = newh ; } hist[i] = (prev + hist[i] + first) / 3.0 ; } /* find the histogram maximum */ maxh = 0 ; for (i = 0 ; i < nbins ; ++i) maxh = VL_MAX (maxh, hist [i]) ; /* find peaks within 80% from max */ nangles = 0 ; for(i = 0 ; i < nbins ; ++i) { double h0 = hist [i] ; double hm = hist [(i - 1 + nbins) % nbins] ; double hp = hist [(i + 1 + nbins) % nbins] ; /* is this a peak? */ if (h0 > 0.8*maxh && h0 > hm && h0 > hp) { /* quadratic interpolation */ double di = - 0.5 * (hp - hm) / (hp + hm - 2 * h0) ; double th = 2 * VL_PI * (i + di + 0.5) / nbins ; angles [ nangles++ ] = th ; if( nangles == 4 ) goto enough_angles ; } } enough_angles: return nangles ; } /** ------------------------------------------------------------------ ** @internal ** @brief Normalizes in norm L_2 a descriptor ** @param begin begin of histogram. ** @param end end of histogram. **/ VL_INLINE vl_sift_pix normalize_histogram (vl_sift_pix *begin, vl_sift_pix *end) { vl_sift_pix* iter ; vl_sift_pix norm = 0.0 ; for (iter = begin ; iter != end ; ++ iter) norm += (*iter) * (*iter) ; norm = vl_fast_sqrt_f (norm) + VL_EPSILON_F ; for (iter = begin; iter != end ; ++ iter) *iter /= norm ; return norm; } /** ------------------------------------------------------------------ ** @brief Run the SIFT descriptor on raw data ** ** @param f SIFT filter. ** @param grad image gradients. ** @param descr SIFT descriptor (output). ** @param width image width. ** @param height image height. ** @param x keypoint x coordinate. ** @param y keypoint y coordinate. ** @param sigma keypoint scale. ** @param angle0 keypoint orientation. ** ** The function runs the SIFT descriptor on raw data. Here @a image ** is a 2 x @a width x @a height array (by convention, the memory ** layout is a s such the first index is the fastest varying ** one). The first @a width x @a height layer of the array contains ** the gradient magnitude and the second the gradient angle (in ** radians, between 0 and @f$ 2\pi @f$). @a x, @a y and @a sigma give ** the keypoint center and scale respectively. ** ** In order to be equivalent to a standard SIFT descriptor the image ** gradient must be computed at a smoothing level equal to the scale ** of the keypoint. In practice, the actual SIFT algorithm makes the ** following additional approximation, which influence the result: ** ** - Scale is discretized in @c S levels. ** - The image is downsampled once for each octave (if you do this, ** the parameters @a x, @a y and @a sigma must be ** scaled too). **/ VL_EXPORT void vl_sift_calc_raw_descriptor (VlSiftFilt const *f, vl_sift_pix const* grad, vl_sift_pix *descr, int width, int height, double x, double y, double sigma, double angle0) { double const magnif = f-> magnif ; int w = width ; int h = height ; int const xo = 2 ; /* x-stride */ int const yo = 2 * w ; /* y-stride */ int xi = (int) (x + 0.5) ; int yi = (int) (y + 0.5) ; double const st0 = sin (angle0) ; double const ct0 = cos (angle0) ; double const SBP = magnif * sigma + VL_EPSILON_D ; int const W = floor (sqrt(2.0) * SBP * (NBP + 1) / 2.0 + 0.5) ; int const binto = 1 ; /* bin theta-stride */ int const binyo = NBO * NBP ; /* bin y-stride */ int const binxo = NBO ; /* bin x-stride */ int bin, dxi, dyi ; vl_sift_pix const *pt ; vl_sift_pix *dpt ; /* check bounds */ if(xi < 0 || xi >= w || yi < 0 || yi >= h - 1 ) return ; /* clear descriptor */ memset (descr, 0, sizeof(vl_sift_pix) * NBO*NBP*NBP) ; /* Center the scale space and the descriptor on the current keypoint. * Note that dpt is pointing to the bin of center (SBP/2,SBP/2,0). */ pt = grad + xi*xo + yi*yo ; dpt = descr + (NBP/2) * binyo + (NBP/2) * binxo ; #undef atd #define atd(dbinx,dbiny,dbint) *(dpt + (dbint)*binto + (dbiny)*binyo + (dbinx)*binxo) /* * Process pixels in the intersection of the image rectangle * (1,1)-(M-1,N-1) and the keypoint bounding box. */ for(dyi = VL_MAX(- W, - yi ) ; dyi <= VL_MIN(+ W, h - yi -1) ; ++ dyi) { for(dxi = VL_MAX(- W, - xi ) ; dxi <= VL_MIN(+ W, w - xi -1) ; ++ dxi) { /* retrieve */ vl_sift_pix mod = *( pt + dxi*xo + dyi*yo + 0 ) ; vl_sift_pix angle = *( pt + dxi*xo + dyi*yo + 1 ) ; vl_sift_pix theta = vl_mod_2pi_f (angle - angle0) ; /* fractional displacement */ vl_sift_pix dx = xi + dxi - x; vl_sift_pix dy = yi + dyi - y; /* get the displacement normalized w.r.t. the keypoint orientation and extension */ vl_sift_pix nx = ( ct0 * dx + st0 * dy) / SBP ; vl_sift_pix ny = (-st0 * dx + ct0 * dy) / SBP ; vl_sift_pix nt = NBO * theta / (2 * VL_PI) ; /* Get the Gaussian weight of the sample. The Gaussian window * has a standard deviation equal to NBP/2. Note that dx and dy * are in the normalized frame, so that -NBP/2 <= dx <= * NBP/2. */ vl_sift_pix const wsigma = f->windowSize ; vl_sift_pix win = fast_expn ((nx*nx + ny*ny)/(2.0 * wsigma * wsigma)) ; /* The sample will be distributed in 8 adjacent bins. We start from the ``lower-left'' bin. */ int binx = (int)vl_floor_f (nx - 0.5) ; int biny = (int)vl_floor_f (ny - 0.5) ; int bint = (int)vl_floor_f (nt) ; vl_sift_pix rbinx = nx - (binx + 0.5) ; vl_sift_pix rbiny = ny - (biny + 0.5) ; vl_sift_pix rbint = nt - bint ; int dbinx ; int dbiny ; int dbint ; /* Distribute the current sample into the 8 adjacent bins*/ for(dbinx = 0 ; dbinx < 2 ; ++dbinx) { for(dbiny = 0 ; dbiny < 2 ; ++dbiny) { for(dbint = 0 ; dbint < 2 ; ++dbint) { if (binx + dbinx >= - (NBP/2) && binx + dbinx < (NBP/2) && biny + dbiny >= - (NBP/2) && biny + dbiny < (NBP/2) ) { vl_sift_pix weight = win * mod * vl_abs_f (1 - dbinx - rbinx) * vl_abs_f (1 - dbiny - rbiny) * vl_abs_f (1 - dbint - rbint) ; atd(binx+dbinx, biny+dbiny, (bint+dbint) % NBO) += weight ; } } } } } } /* Standard SIFT descriptors are normalized, truncated and normalized again */ if(1) { /* normalize L2 norm */ vl_sift_pix norm = normalize_histogram (descr, descr + NBO*NBP*NBP) ; /* Set the descriptor to zero if it is lower than our norm_threshold. We divide by the number of samples in the descriptor region because the Gaussian window used in the calculation of the descriptor is not normalized. */ int numSamples = (VL_MIN(W, w - xi -1) - VL_MAX(-W, - xi) + 1) * (VL_MIN(W, h - yi -1) - VL_MAX(-W, - yi) + 1) ; if(f-> norm_thresh && norm < f-> norm_thresh * numSamples) { for(bin = 0; bin < NBO*NBP*NBP ; ++ bin) descr [bin] = 0; } else { /* truncate at 0.2. */ for(bin = 0; bin < NBO*NBP*NBP ; ++ bin) { if (descr [bin] > 0.2) descr [bin] = 0.2; } /* normalize again. */ normalize_histogram (descr, descr + NBO*NBP*NBP) ; } } } /** ------------------------------------------------------------------ ** @brief Compute the descriptor of a keypoint ** ** @param f SIFT filter. ** @param descr SIFT descriptor (output) ** @param k keypoint. ** @param angle0 keypoint direction. ** ** The function computes the SIFT descriptor of the keypoint @a k of ** orientation @a angle0. The function fills the buffer @a descr ** which must be large enough to hold the descriptor. ** ** The function assumes that the keypoint is on the current octave. ** If not, it does not do anything. **/ VL_EXPORT void vl_sift_calc_keypoint_descriptor (VlSiftFilt *f, vl_sift_pix *descr, VlSiftKeypoint const* k, double angle0) { /* The SIFT descriptor is a three dimensional histogram of the position and orientation of the gradient. There are NBP bins for each spatial dimension and NBO bins for the orientation dimension, for a total of NBP x NBP x NBO bins. The support of each spatial bin has an extension of SBP = 3sigma pixels, where sigma is the scale of the keypoint. Thus all the bins together have a support SBP x NBP pixels wide. Since weighting and interpolation of pixel is used, the support extends by another half bin. Therefore, the support is a square window of SBP x (NBP + 1) pixels. Finally, since the patch can be arbitrarily rotated, we need to consider a window 2W += sqrt(2) x SBP x (NBP + 1) pixels wide. */ double const magnif = f-> magnif ; double xper = pow (2.0, f->o_cur) ; int w = f-> octave_width ; int h = f-> octave_height ; int const xo = 2 ; /* x-stride */ int const yo = 2 * w ; /* y-stride */ int const so = 2 * w * h ; /* s-stride */ double x = k-> x / xper ; double y = k-> y / xper ; double sigma = k-> sigma / xper ; int xi = (int) (x + 0.5) ; int yi = (int) (y + 0.5) ; int si = k-> is ; double const st0 = sin (angle0) ; double const ct0 = cos (angle0) ; double const SBP = magnif * sigma + VL_EPSILON_D ; int const W = floor (sqrt(2.0) * SBP * (NBP + 1) / 2.0 + 0.5) ; int const binto = 1 ; /* bin theta-stride */ int const binyo = NBO * NBP ; /* bin y-stride */ int const binxo = NBO ; /* bin x-stride */ int bin, dxi, dyi ; vl_sift_pix const *pt ; vl_sift_pix *dpt ; /* check bounds */ if(k->o != f->o_cur || xi < 0 || xi >= w || yi < 0 || yi >= h - 1 || si < f->s_min + 1 || si > f->s_max - 2 ) return ; /* synchronize gradient buffer */ update_gradient (f) ; /* VL_PRINTF("W = %d ; magnif = %g ; SBP = %g\n", W,magnif,SBP) ; */ /* clear descriptor */ memset (descr, 0, sizeof(vl_sift_pix) * NBO*NBP*NBP) ; /* Center the scale space and the descriptor on the current keypoint. * Note that dpt is pointing to the bin of center (SBP/2,SBP/2,0). */ pt = f->grad + xi*xo + yi*yo + (si - f->s_min - 1)*so ; dpt = descr + (NBP/2) * binyo + (NBP/2) * binxo ; #undef atd #define atd(dbinx,dbiny,dbint) *(dpt + (dbint)*binto + (dbiny)*binyo + (dbinx)*binxo) /* * Process pixels in the intersection of the image rectangle * (1,1)-(M-1,N-1) and the keypoint bounding box. */ for(dyi = VL_MAX (- W, 1 - yi ) ; dyi <= VL_MIN (+ W, h - yi - 2) ; ++ dyi) { for(dxi = VL_MAX (- W, 1 - xi ) ; dxi <= VL_MIN (+ W, w - xi - 2) ; ++ dxi) { /* retrieve */ vl_sift_pix mod = *( pt + dxi*xo + dyi*yo + 0 ) ; vl_sift_pix angle = *( pt + dxi*xo + dyi*yo + 1 ) ; vl_sift_pix theta = vl_mod_2pi_f (angle - angle0) ; /* fractional displacement */ vl_sift_pix dx = xi + dxi - x; vl_sift_pix dy = yi + dyi - y; /* get the displacement normalized w.r.t. the keypoint orientation and extension */ vl_sift_pix nx = ( ct0 * dx + st0 * dy) / SBP ; vl_sift_pix ny = (-st0 * dx + ct0 * dy) / SBP ; vl_sift_pix nt = NBO * theta / (2 * VL_PI) ; /* Get the Gaussian weight of the sample. The Gaussian window * has a standard deviation equal to NBP/2. Note that dx and dy * are in the normalized frame, so that -NBP/2 <= dx <= * NBP/2. */ vl_sift_pix const wsigma = f->windowSize ; vl_sift_pix win = fast_expn ((nx*nx + ny*ny)/(2.0 * wsigma * wsigma)) ; /* The sample will be distributed in 8 adjacent bins. We start from the ``lower-left'' bin. */ int binx = (int)vl_floor_f (nx - 0.5) ; int biny = (int)vl_floor_f (ny - 0.5) ; int bint = (int)vl_floor_f (nt) ; vl_sift_pix rbinx = nx - (binx + 0.5) ; vl_sift_pix rbiny = ny - (biny + 0.5) ; vl_sift_pix rbint = nt - bint ; int dbinx ; int dbiny ; int dbint ; /* Distribute the current sample into the 8 adjacent bins*/ for(dbinx = 0 ; dbinx < 2 ; ++dbinx) { for(dbiny = 0 ; dbiny < 2 ; ++dbiny) { for(dbint = 0 ; dbint < 2 ; ++dbint) { if (binx + dbinx >= - (NBP/2) && binx + dbinx < (NBP/2) && biny + dbiny >= - (NBP/2) && biny + dbiny < (NBP/2) ) { vl_sift_pix weight = win * mod * vl_abs_f (1 - dbinx - rbinx) * vl_abs_f (1 - dbiny - rbiny) * vl_abs_f (1 - dbint - rbint) ; atd(binx+dbinx, biny+dbiny, (bint+dbint) % NBO) += weight ; } } } } } } /* Standard SIFT descriptors are normalized, truncated and normalized again */ if(1) { /* Normalize the histogram to L2 unit length. */ vl_sift_pix norm = normalize_histogram (descr, descr + NBO*NBP*NBP) ; /* Set the descriptor to zero if it is lower than our norm_threshold */ if(f-> norm_thresh && norm < f-> norm_thresh) { for(bin = 0; bin < NBO*NBP*NBP ; ++ bin) descr [bin] = 0; } else { /* Truncate at 0.2. */ for(bin = 0; bin < NBO*NBP*NBP ; ++ bin) { if (descr [bin] > 0.2) descr [bin] = 0.2; } /* Normalize again. */ normalize_histogram (descr, descr + NBO*NBP*NBP) ; } } } /** ------------------------------------------------------------------ ** @brief Initialize a keypoint from its position and scale ** ** @param f SIFT filter. ** @param k SIFT keypoint (output). ** @param x x coordinate of the keypoint center. ** @param y y coordinate of the keypoint center. ** @param sigma keypoint scale. ** ** The function initializes a keypoint structure @a k from ** the location @a x ** and @a y and the scale @a sigma of the keypoint. The keypoint structure ** maps the keypoint to an octave and scale level of the discretized ** Gaussian scale space, which is required for instance to compute the ** keypoint SIFT descriptor. ** ** @par Algorithm ** ** The formula linking the keypoint scale sigma to the octave and ** scale indexes is ** ** @f[ \sigma(o,s) = \sigma_0 2^{o+s/S} @f] ** ** In addition to the scale index @e s (which can be fractional due ** to scale interpolation) a keypoint has an integer scale index @e ** is too (which is the index of the scale level where it was ** detected in the DoG scale space). We have the constraints (@ref ** sift-tech-detector see also the "SIFT detector"): ** ** - @e o is integer in the range @f$ [o_\mathrm{min}, ** o_{\mathrm{min}}+O-1] @f$. ** - @e is is integer in the range @f$ [s_\mathrm{min}+1, ** s_\mathrm{max}-2] @f$. This depends on how the scale is ** determined during detection, and must be so here because ** gradients are computed only for this range of scale levels ** and are required for the calculation of the SIFT descriptor. ** - @f$ |s - is| < 0.5 @f$ for detected keypoints in most cases due ** to the interpolation technique used during detection. However ** this is not necessary. ** ** Thus octave o represents scales @f$ \{ \sigma(o, s) : s \in ** [s_\mathrm{min}+1-.5, s_\mathrm{max}-2+.5] \} @f$. Note that some ** scales may be represented more than once. For each scale, we ** select the largest possible octave that contains it, i.e. ** ** @f[ ** o(\sigma) ** = \max \{ o \in \mathbb{Z} : ** \sigma_0 2^{\frac{s_\mathrm{min}+1-.5}{S}} \leq \sigma \} ** = \mathrm{floor}\,\left[ ** \log_2(\sigma / \sigma_0) - \frac{s_\mathrm{min}+1-.5}{S}\right] ** @f] ** ** and then ** ** @f[ ** s(\sigma) = S \left[\log_2(\sigma / \sigma_0) - o(\sigma)\right], ** \quad ** is(\sigma) = \mathrm{round}\,(s(\sigma)) ** @f] ** ** In practice, both @f$ o(\sigma) @f$ and @f$ is(\sigma) @f$ are ** clamped to their feasible range as determined by the SIFT filter ** parameters. **/ VL_EXPORT void vl_sift_keypoint_init (VlSiftFilt const *f, VlSiftKeypoint *k, double x, double y, double sigma) { int o, ix, iy, is ; double s, phi, xper ; phi = log2 ((sigma + VL_EPSILON_D) / f->sigma0) ; o = (int)vl_floor_d (phi - ((double) f->s_min + 0.5) / f->S) ; o = VL_MIN (o, f->o_min + f->O - 1) ; o = VL_MAX (o, f->o_min ) ; s = f->S * (phi - o) ; is = (int)(s + 0.5) ; is = VL_MIN(is, f->s_max - 2) ; is = VL_MAX(is, f->s_min + 1) ; xper = pow (2.0, o) ; ix = (int)(x / xper + 0.5) ; iy = (int)(y / xper + 0.5) ; k -> o = o ; k -> ix = ix ; k -> iy = iy ; k -> is = is ; k -> x = x ; k -> y = y ; k -> s = s ; k->sigma = sigma ; } colmap-3.9.1/src/thirdparty/VLFeat/sift.h000066400000000000000000000270031454702036400202260ustar00rootroot00000000000000/** @file sift.h ** @brief SIFT (@ref sift) ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_SIFT_H #define VL_SIFT_H #include #include "generic.h" /** @brief SIFT filter pixel type */ typedef float vl_sift_pix ; /** ------------------------------------------------------------------ ** @brief SIFT filter keypoint ** ** This structure represent a keypoint as extracted by the SIFT ** filter ::VlSiftFilt. **/ typedef struct _VlSiftKeypoint { int o ; /**< o coordinate (octave). */ int ix ; /**< Integer unnormalized x coordinate. */ int iy ; /**< Integer unnormalized y coordinate. */ int is ; /**< Integer s coordinate. */ float x ; /**< x coordinate. */ float y ; /**< y coordinate. */ float s ; /**< s coordinate. */ float sigma ; /**< scale. */ } VlSiftKeypoint ; /** ------------------------------------------------------------------ ** @brief SIFT filter ** ** This filter implements the SIFT detector and descriptor. **/ typedef struct _VlSiftFilt { double sigman ; /**< nominal image smoothing. */ double sigma0 ; /**< smoothing of pyramid base. */ double sigmak ; /**< k-smoothing */ double dsigma0 ; /**< delta-smoothing. */ int width ; /**< image width. */ int height ; /**< image height. */ int O ; /**< number of octaves. */ int S ; /**< number of levels per octave. */ int o_min ; /**< minimum octave index. */ int s_min ; /**< minimum level index. */ int s_max ; /**< maximum level index. */ int o_cur ; /**< current octave. */ vl_sift_pix *temp ; /**< temporary pixel buffer. */ vl_sift_pix *octave ; /**< current GSS data. */ vl_sift_pix *dog ; /**< current DoG data. */ int octave_width ; /**< current octave width. */ int octave_height ; /**< current octave height. */ vl_sift_pix *gaussFilter ; /**< current Gaussian filter */ double gaussFilterSigma ; /**< current Gaussian filter std */ vl_size gaussFilterWidth ; /**< current Gaussian filter width */ VlSiftKeypoint* keys ;/**< detected keypoints. */ int nkeys ; /**< number of detected keypoints. */ int keys_res ; /**< size of the keys buffer. */ double peak_thresh ; /**< peak threshold. */ double edge_thresh ; /**< edge threshold. */ double norm_thresh ; /**< norm threshold. */ double magnif ; /**< magnification factor. */ double windowSize ; /**< size of Gaussian window (in spatial bins) */ vl_sift_pix *grad ; /**< GSS gradient data. */ int grad_o ; /**< GSS gradient data octave. */ } VlSiftFilt ; /** @name Create and destroy ** @{ **/ VL_EXPORT VlSiftFilt* vl_sift_new (int width, int height, int noctaves, int nlevels, int o_min) ; VL_EXPORT void vl_sift_delete (VlSiftFilt *f) ; /** @} */ /** @name Process data ** @{ **/ VL_EXPORT int vl_sift_process_first_octave (VlSiftFilt *f, vl_sift_pix const *im) ; VL_EXPORT int vl_sift_process_next_octave (VlSiftFilt *f) ; VL_EXPORT void vl_sift_detect (VlSiftFilt *f) ; VL_EXPORT int vl_sift_calc_keypoint_orientations (VlSiftFilt *f, double angles [4], VlSiftKeypoint const*k); VL_EXPORT void vl_sift_calc_keypoint_descriptor (VlSiftFilt *f, vl_sift_pix *descr, VlSiftKeypoint const* k, double angle) ; VL_EXPORT void vl_sift_calc_raw_descriptor (VlSiftFilt const *f, vl_sift_pix const* image, vl_sift_pix *descr, int widht, int height, double x, double y, double s, double angle0) ; VL_EXPORT void vl_sift_keypoint_init (VlSiftFilt const *f, VlSiftKeypoint *k, double x, double y, double sigma) ; /** @} */ /** @name Retrieve data and parameters ** @{ **/ VL_INLINE int vl_sift_get_octave_index (VlSiftFilt const *f) ; VL_INLINE int vl_sift_get_noctaves (VlSiftFilt const *f) ; VL_INLINE int vl_sift_get_octave_first (VlSiftFilt const *f) ; VL_INLINE int vl_sift_get_octave_width (VlSiftFilt const *f) ; VL_INLINE int vl_sift_get_octave_height (VlSiftFilt const *f) ; VL_INLINE int vl_sift_get_nlevels (VlSiftFilt const *f) ; VL_INLINE int vl_sift_get_nkeypoints (VlSiftFilt const *f) ; VL_INLINE double vl_sift_get_peak_thresh (VlSiftFilt const *f) ; VL_INLINE double vl_sift_get_edge_thresh (VlSiftFilt const *f) ; VL_INLINE double vl_sift_get_norm_thresh (VlSiftFilt const *f) ; VL_INLINE double vl_sift_get_magnif (VlSiftFilt const *f) ; VL_INLINE double vl_sift_get_window_size (VlSiftFilt const *f) ; VL_INLINE vl_sift_pix *vl_sift_get_octave (VlSiftFilt const *f, int s) ; VL_INLINE VlSiftKeypoint const *vl_sift_get_keypoints (VlSiftFilt const *f) ; /** @} */ /** @name Set parameters ** @{ **/ VL_INLINE void vl_sift_set_peak_thresh (VlSiftFilt *f, double t) ; VL_INLINE void vl_sift_set_edge_thresh (VlSiftFilt *f, double t) ; VL_INLINE void vl_sift_set_norm_thresh (VlSiftFilt *f, double t) ; VL_INLINE void vl_sift_set_magnif (VlSiftFilt *f, double m) ; VL_INLINE void vl_sift_set_window_size (VlSiftFilt *f, double m) ; /** @} */ /* ------------------------------------------------------------------- * Inline functions implementation * ---------------------------------------------------------------- */ /** ------------------------------------------------------------------ ** @brief Get current octave index. ** @param f SIFT filter. ** @return index of the current octave. **/ VL_INLINE int vl_sift_get_octave_index (VlSiftFilt const *f) { return f-> o_cur ; } /** ------------------------------------------------------------------ ** @brief Get number of octaves. ** @param f SIFT filter. ** @return number of octaves. **/ VL_INLINE int vl_sift_get_noctaves (VlSiftFilt const *f) { return f-> O ; } /**------------------------------------------------------------------- ** @brief Get first octave. ** @param f SIFT filter. ** @return index of the first octave. **/ VL_INLINE int vl_sift_get_octave_first (VlSiftFilt const *f) { return f-> o_min ; } /** ------------------------------------------------------------------ ** @brief Get current octave width ** @param f SIFT filter. ** @return current octave width. **/ VL_INLINE int vl_sift_get_octave_width (VlSiftFilt const *f) { return f-> octave_width ; } /** ------------------------------------------------------------------ ** @brief Get current octave height ** @param f SIFT filter. ** @return current octave height. **/ VL_INLINE int vl_sift_get_octave_height (VlSiftFilt const *f) { return f-> octave_height ; } /** ------------------------------------------------------------------ ** @brief Get current octave data ** @param f SIFT filter. ** @param s level index. ** ** The level index @a s ranges in the interval s_min = -1 ** and s_max = S + 2, where @c S is the number of levels ** per octave. ** ** @return pointer to the octave data for level @a s. **/ VL_INLINE vl_sift_pix * vl_sift_get_octave (VlSiftFilt const *f, int s) { int w = vl_sift_get_octave_width (f) ; int h = vl_sift_get_octave_height (f) ; return f->octave + w * h * (s - f->s_min) ; } /** ------------------------------------------------------------------ ** @brief Get number of levels per octave ** @param f SIFT filter. ** @return number of leves per octave. **/ VL_INLINE int vl_sift_get_nlevels (VlSiftFilt const *f) { return f-> S ; } /** ------------------------------------------------------------------ ** @brief Get number of keypoints. ** @param f SIFT filter. ** @return number of keypoints. **/ VL_INLINE int vl_sift_get_nkeypoints (VlSiftFilt const *f) { return f-> nkeys ; } /** ------------------------------------------------------------------ ** @brief Get keypoints. ** @param f SIFT filter. ** @return pointer to the keypoints list. **/ VL_INLINE VlSiftKeypoint const * vl_sift_get_keypoints (VlSiftFilt const *f) { return f-> keys ; } /** ------------------------------------------------------------------ ** @brief Get peaks treashold ** @param f SIFT filter. ** @return threshold ; **/ VL_INLINE double vl_sift_get_peak_thresh (VlSiftFilt const *f) { return f -> peak_thresh ; } /** ------------------------------------------------------------------ ** @brief Get edges threshold ** @param f SIFT filter. ** @return threshold. **/ VL_INLINE double vl_sift_get_edge_thresh (VlSiftFilt const *f) { return f -> edge_thresh ; } /** ------------------------------------------------------------------ ** @brief Get norm threshold ** @param f SIFT filter. ** @return threshold. **/ VL_INLINE double vl_sift_get_norm_thresh (VlSiftFilt const *f) { return f -> norm_thresh ; } /** ------------------------------------------------------------------ ** @brief Get the magnification factor ** @param f SIFT filter. ** @return magnification factor. **/ VL_INLINE double vl_sift_get_magnif (VlSiftFilt const *f) { return f -> magnif ; } /** ------------------------------------------------------------------ ** @brief Get the Gaussian window size. ** @param f SIFT filter. ** @return standard deviation of the Gaussian window (in spatial bin units). **/ VL_INLINE double vl_sift_get_window_size (VlSiftFilt const *f) { return f -> windowSize ; } /** ------------------------------------------------------------------ ** @brief Set peaks threshold ** @param f SIFT filter. ** @param t threshold. **/ VL_INLINE void vl_sift_set_peak_thresh (VlSiftFilt *f, double t) { f -> peak_thresh = t ; } /** ------------------------------------------------------------------ ** @brief Set edges threshold ** @param f SIFT filter. ** @param t threshold. **/ VL_INLINE void vl_sift_set_edge_thresh (VlSiftFilt *f, double t) { f -> edge_thresh = t ; } /** ------------------------------------------------------------------ ** @brief Set norm threshold ** @param f SIFT filter. ** @param t threshold. **/ VL_INLINE void vl_sift_set_norm_thresh (VlSiftFilt *f, double t) { f -> norm_thresh = t ; } /** ------------------------------------------------------------------ ** @brief Set the magnification factor ** @param f SIFT filter. ** @param m magnification factor. **/ VL_INLINE void vl_sift_set_magnif (VlSiftFilt *f, double m) { f -> magnif = m ; } /** ------------------------------------------------------------------ ** @brief Set the Gaussian window size ** @param f SIFT filter. ** @param x Gaussian window size (in units of spatial bin). ** ** This is the parameter @f$ \hat \sigma_{\text{win}} @f$ of ** the standard SIFT descriptor @ref sift-tech-descriptor-std. **/ VL_INLINE void vl_sift_set_window_size (VlSiftFilt *f, double x) { f -> windowSize = x ; } /* VL_SIFT_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/slic.c000077500000000000000000000334471454702036400202220ustar00rootroot00000000000000/** @file slic.c ** @brief SLIC superpixels - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page slic Simple Linear Iterative Clustering (SLIC) @author Andrea Vedaldi @ref slic.h implements the *Simple Linear Iterative Clustering* (SLIC) algorithm, an image segmentation method described in @cite{achanta10slic}. - @ref slic-overview - @ref slic-usage - @ref slic-tech @section slic-overview Overview SLIC @cite{achanta10slic} is a simple and efficient method to decompose an image in visually homogeneous regions. It is based on a spatially localized version of k-means clustering. Similar to mean shift or quick shift (@ref quickshift.h), each pixel is associated to a feature vector @f[ \Psi(x,y) = \left[ \begin{array}{c} \lambda x \\ \lambda y \\ I(x,y) \end{array} \right] @f] and then k-means clustering is run on those. As discussed below, the coefficient @f$ \lambda @f$ balances the spatial and appearance components of the feature vectors, imposing a degree of spatial regularization to the extracted regions. SLIC takes two parameters: the nominal size of the regions (superpixels) @c regionSize and the strength of the spatial regularization @c regularizer. The image is first divided into a grid with step @c regionSize. The center of each grid tile is then used to initialize a corresponding k-means (up to a small shift to avoid image edges). Finally, the k-means centers and clusters are refined by using the Lloyd algorithm, yielding segmenting the image. As a further restriction and simplification, during the k-means iterations each pixel can be assigned to only the 2 x 2 centers corresponding to grid tiles adjacent to the pixel. The parameter @c regularizer sets the trade-off between clustering appearance and spatial regularization. This is obtained by setting @f[ \lambda = \frac{\mathtt{regularizer}}{\mathtt{regionSize}} @f] in the definition of the feature @f$ \psi(x,y) @f$. After the k-means step, SLIC optionally removes any segment whose area is smaller than a threshld @c minRegionSize by merging them into larger ones. @section slic-usage Usage from the C library To compute the SLIC superpixels of an image use the function ::vl_slic_segment. @section slic-tech Technical details SLIC starts by dividing the image domain into a regular grid with @f$ M \times N @f$ tiles, where @f[ M = \lceil \frac{\mathtt{imageWidth}}{\mathtt{regionSize}} \rceil, \quad N = \lceil \frac{\mathtt{imageHeight}}{\mathtt{regionSize}} \rceil. @f] A region (superpixel or k-means cluster) is initialized from each grid center @f[ x_i = \operatorname{round} i \frac{\mathtt{imageWidth}}{\mathtt{regionSize}} \quad y_j = \operatorname{round} j \frac{\mathtt{imageWidth}}{\mathtt{regionSize}}. @f] In order to avoid placing these centers on top of image discontinuities, the centers are then moved in a 3 x 3 neighbourohood to minimize the edge strength @f[ \operatorname{edge}(x,y) = \| I(x+1,y) - I(x-1,y) \|_2^2 + \| I(x,y+1) - I(x,y-1) \|_2^2. @f] Then the regions are obtained by running k-means clustering, started from the centers @f[ C = \{ \Psi(x_i,y_j), i=0,1,\dots,M-1\ j=0,1,\dots,N-1 \} @f] thus obtained. K-means uses the standard LLoyd algorithm alternating assigning pixels to the clostest centers a re-estiamting the centers as the average of the corresponding feature vectors of the pixel assigned to them. The only difference compared to standard k-means is that each pixel can be assigned only to the center originated from the neighbour tiles. This guarantees that there are exactly four pixel-to-center comparisons at each round of minimization, which threfore cost @f$ O(n) @f$, where @f$ n @f$ is the number of superpixels. After k-means has converged, SLIC eliminates any connected region whose area is less than @c minRegionSize pixels. This is done by greedily merging regions to neighbour ones: the pixels @f$ p @f$ are scanned in lexicographical order and the corresponding connected components are visited. If a region has already been visited, it is skipped; if not, its area is computed and if this is less than @c minRegionSize its label is changed to the one of a neighbour region at @f$ p @f$ that has already been vistied (there is always one except for the very first pixel). */ #include "slic.h" #include "mathop.h" #include #include /** @brief SLIC superpixel segmentation ** @param segmentation segmentation. ** @param image image to segment. ** @param width image width. ** @param height image height. ** @param numChannels number of image channels (depth). ** @param regionSize nominal size of the regions. ** @param regularization trade-off between appearance and spatial terms. ** @param minRegionSize minimum size of a segment. ** ** The function computes the SLIC superpixels of the specified image @a image. ** @a image is a pointer to an @c width by @c height by @c by numChannles array of @c float. ** @a segmentation is a pointer to a @c width by @c height array of @c vl_uint32. ** @a segmentation contain the labels of each image pixels, from 0 to ** the number of regions minus one. ** ** @sa @ref slic-overview, @ref slic-tech **/ void vl_slic_segment (vl_uint32 * segmentation, float const * image, vl_size width, vl_size height, vl_size numChannels, vl_size regionSize, float regularization, vl_size minRegionSize) { vl_index i, x, y, u, v, k, region ; vl_uindex iter ; vl_size const numRegionsX = (vl_size) ceil((double) width / regionSize) ; vl_size const numRegionsY = (vl_size) ceil((double) height / regionSize) ; vl_size const numRegions = numRegionsX * numRegionsY ; vl_size const numPixels = width * height ; float * centers ; float * edgeMap ; float previousEnergy = VL_INFINITY_F ; float startingEnergy ; vl_uint32 * masses ; vl_size const maxNumIterations = 100 ; assert(segmentation) ; assert(image) ; assert(width >= 1) ; assert(height >= 1) ; assert(numChannels >= 1) ; assert(regionSize >= 1) ; assert(regularization >= 0) ; #define atimage(x,y,k) image[(x)+(y)*width+(k)*width*height] #define atEdgeMap(x,y) edgeMap[(x)+(y)*width] edgeMap = vl_calloc(numPixels, sizeof(float)) ; masses = vl_malloc(sizeof(vl_uint32) * numPixels) ; centers = vl_malloc(sizeof(float) * (2 + numChannels) * numRegions) ; /* compute edge map (gradient strength) */ for (k = 0 ; k < (signed)numChannels ; ++k) { for (y = 1 ; y < (signed)height-1 ; ++y) { for (x = 1 ; x < (signed)width-1 ; ++x) { float a = atimage(x-1,y,k) ; float b = atimage(x+1,y,k) ; float c = atimage(x,y+1,k) ; float d = atimage(x,y-1,k) ; atEdgeMap(x,y) += (a - b) * (a - b) + (c - d) * (c - d) ; } } } /* initialize K-means centers */ i = 0 ; for (v = 0 ; v < (signed)numRegionsY ; ++v) { for (u = 0 ; u < (signed)numRegionsX ; ++u) { vl_index xp ; vl_index yp ; vl_index centerx = 0 ; vl_index centery = 0 ; float minEdgeValue = VL_INFINITY_F ; x = (vl_index) vl_round_d(regionSize * (u + 0.5)) ; y = (vl_index) vl_round_d(regionSize * (v + 0.5)) ; x = VL_MAX(VL_MIN(x, (signed)width-1),0) ; y = VL_MAX(VL_MIN(y, (signed)height-1),0) ; /* search in a 3x3 neighbourhood the smallest edge response */ for (yp = VL_MAX(0, y-1) ; yp <= VL_MIN((signed)height-1, y+1) ; ++ yp) { for (xp = VL_MAX(0, x-1) ; xp <= VL_MIN((signed)width-1, x+1) ; ++ xp) { float thisEdgeValue = atEdgeMap(xp,yp) ; if (thisEdgeValue < minEdgeValue) { minEdgeValue = thisEdgeValue ; centerx = xp ; centery = yp ; } } } /* initialize the new center at this location */ centers[i++] = (float) centerx ; centers[i++] = (float) centery ; for (k = 0 ; k < (signed)numChannels ; ++k) { centers[i++] = atimage(centerx,centery,k) ; } } } /* run k-means iterations */ for (iter = 0 ; iter < maxNumIterations ; ++iter) { float factor = regularization / (regionSize * regionSize) ; float energy = 0 ; /* assign pixels to centers */ for (y = 0 ; y < (signed)height ; ++y) { for (x = 0 ; x < (signed)width ; ++x) { vl_index u = floor((double)x / regionSize - 0.5) ; vl_index v = floor((double)y / regionSize - 0.5) ; vl_index up, vp ; float minDistance = VL_INFINITY_F ; for (vp = VL_MAX(0, v) ; vp <= VL_MIN((signed)numRegionsY-1, v+1) ; ++vp) { for (up = VL_MAX(0, u) ; up <= VL_MIN((signed)numRegionsX-1, u+1) ; ++up) { vl_index region = up + vp * numRegionsX ; float centerx = centers[(2 + numChannels) * region + 0] ; float centery = centers[(2 + numChannels) * region + 1] ; float spatial = (x - centerx) * (x - centerx) + (y - centery) * (y - centery) ; float appearance = 0 ; float distance ; for (k = 0 ; k < (signed)numChannels ; ++k) { float centerz = centers[(2 + numChannels) * region + k + 2] ; float z = atimage(x,y,k) ; appearance += (z - centerz) * (z - centerz) ; } distance = appearance + factor * spatial ; if (minDistance > distance) { minDistance = distance ; segmentation[x + y * width] = (vl_uint32)region ; } } } energy += minDistance ; } } /* VL_PRINTF("vl:slic: iter %d: energy: %g\n", iter, energy) ; */ /* check energy termination conditions */ if (iter == 0) { startingEnergy = energy ; } else { if ((previousEnergy - energy) < 1e-5 * (startingEnergy - energy)) { break ; } } previousEnergy = energy ; /* recompute centers */ memset(masses, 0, sizeof(vl_uint32) * width * height) ; memset(centers, 0, sizeof(float) * (2 + numChannels) * numRegions) ; for (y = 0 ; y < (signed)height ; ++y) { for (x = 0 ; x < (signed)width ; ++x) { vl_index pixel = x + y * width ; vl_index region = segmentation[pixel] ; masses[region] ++ ; centers[region * (2 + numChannels) + 0] += x ; centers[region * (2 + numChannels) + 1] += y ; for (k = 0 ; k < (signed)numChannels ; ++k) { centers[region * (2 + numChannels) + k + 2] += atimage(x,y,k) ; } } } for (region = 0 ; region < (signed)numRegions ; ++region) { float mass = VL_MAX(masses[region], 1e-8) ; for (i = (2 + numChannels) * region ; i < (signed)(2 + numChannels) * (region + 1) ; ++i) { centers[i] /= mass ; } } } vl_free(masses) ; vl_free(centers) ; vl_free(edgeMap) ; /* elimiate small regions */ { vl_uint32 * cleaned = vl_calloc(numPixels, sizeof(vl_uint32)) ; vl_uindex * segment = vl_malloc(sizeof(vl_uindex) * numPixels) ; vl_size segmentSize ; vl_uint32 label ; vl_uint32 cleanedLabel ; vl_size numExpanded ; vl_index const dx [] = {+1, -1, 0, 0} ; vl_index const dy [] = { 0, 0, +1, -1} ; vl_index direction ; vl_index pixel ; for (pixel = 0 ; pixel < (signed)numPixels ; ++pixel) { if (cleaned[pixel]) continue ; label = segmentation[pixel] ; numExpanded = 0 ; segmentSize = 0 ; segment[segmentSize++] = pixel ; /* find cleanedLabel as the label of an already cleaned region neihbour of this pixel */ cleanedLabel = label + 1 ; cleaned[pixel] = label + 1 ; x = pixel % width ; y = pixel / width ; for (direction = 0 ; direction < 4 ; ++direction) { vl_index xp = x + dx[direction] ; vl_index yp = y + dy[direction] ; vl_index neighbor = xp + yp * width ; if (0 <= xp && xp < (signed)width && 0 <= yp && yp < (signed)height && cleaned[neighbor]) { cleanedLabel = cleaned[neighbor] ; } } /* expand the segment */ while (numExpanded < segmentSize) { vl_index open = segment[numExpanded++] ; x = open % width ; y = open / width ; for (direction = 0 ; direction < 4 ; ++direction) { vl_index xp = x + dx[direction] ; vl_index yp = y + dy[direction] ; vl_index neighbor = xp + yp * width ; if (0 <= xp && xp < (signed)width && 0 <= yp && yp < (signed)height && cleaned[neighbor] == 0 && segmentation[neighbor] == label) { cleaned[neighbor] = label + 1 ; segment[segmentSize++] = neighbor ; } } } /* change label to cleanedLabel if the semgent is too small */ if (segmentSize < minRegionSize) { while (segmentSize > 0) { cleaned[segment[--segmentSize]] = cleanedLabel ; } } } /* restore base 0 indexing of the regions */ for (pixel = 0 ; pixel < (signed)numPixels ; ++pixel) cleaned[pixel] -- ; memcpy(segmentation, cleaned, numPixels * sizeof(vl_uint32)) ; vl_free(cleaned) ; vl_free(segment) ; } } colmap-3.9.1/src/thirdparty/VLFeat/slic.h000066400000000000000000000012721454702036400202130ustar00rootroot00000000000000/** @file slic.h ** @brief SLIC superpixels (@ref slic) ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_SLIC_H #define VL_SLIC_H #include "generic.h" VL_EXPORT void vl_slic_segment (vl_uint32 * segmentation, float const * image, vl_size width, vl_size height, vl_size numChannels, vl_size regionSize, float regularization, vl_size minRegionSize) ; /* VL_SLIC_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/stringop.c000077500000000000000000000323001454702036400211200ustar00rootroot00000000000000/** @file stringop.c ** @brief String operations - Definition ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file stringop.h @brief String operations @author Andrea Vedaldi @tableofcontents @ref stringop.h implements basic string operations. All functions that write to strings use range checking, which makes them safer than some standard POSIX equivalent (see @ref vl-stringop-err). @section vl-stringop-enumeration Enumerations @ref stringop.h defines a simple enumeration data type. This is given by an array of enumeration members, represented by instances of the ::VlEnumerator strucutre, each storing a name-value pair. The enumeration must end by a member whose name is set to @c NULL. Use ::vl_enumeration_get and ::vl_enumeration_get_casei to retrieve an enumeration member by name. @section vl-stringop-file-protocols File protocols @ref stringop.h defines a few file "protocols" and helps parsing them from URL-like formatted strings. The supported protocols are:
File protocols
ProtocolCodeURL prefix
ASCII::VL_PROT_ASCIIascii://
BINARY::VL_PROT_BINARYbinary://
@section vl-stringop-err Detecting overflow @ref stringop.h functions that write a string to a character buffer take both the buffer and its size @c n as input. If @c n is not large enough, the output may be truncated but it is always a null terminated string (provided that @c n >= 1). Such functions also return the length of the string that would have been written @c r (which does not include the terminating null character) had the buffer been large enough. Hence an overflow can be detected by testing if @c r >= @c n, @c r can be used to re-allocate a buffer large enough to contain the result, and the operation can be repeated. **/ #include "stringop.h" #include #include /** ------------------------------------------------------------------ ** @brief Extract the protocol prefix from a string ** @param string string. ** @param protocol protocol code (output). ** @return pointer to the first character after the protocol prefix. ** ** The function extracts the prefix of the string @a string ** terminated by the first occurrence of the @c :// substring (if ** any). It then matches the suffix terminated by @c :// to the ** supported @ref vl-stringop-file-protocols protocols. If @c protocol is not ** @c NULL, the corresponding protocol code is written to @a protocol ** ** The function writes to @a protocol the value ::VL_PROT_NONE if no ** suffix is detected and ::VL_PROT_UNKNOWN if there is a suffix but ** it cannot be matched to any of the supported protocols. **/ VL_EXPORT char * vl_string_parse_protocol (char const *string, int *protocol) { char const * cpt ; int dummy ; /* handle the case prot = 0 */ if (protocol == 0) protocol = &dummy ; /* look for :// */ cpt = strstr(string, "://") ; if (cpt == 0) { *protocol = VL_PROT_NONE ; cpt = string ; } else { if (strncmp(string, "ascii", cpt - string) == 0) { *protocol = VL_PROT_ASCII ; } else if (strncmp(string, "bin", cpt - string) == 0) { *protocol = VL_PROT_BINARY ; } else { *protocol = VL_PROT_UNKNOWN ; } cpt += 3 ; } return (char*) cpt ; } /** ------------------------------------------------------------------ ** @brief Get protocol name ** @param protocol protocol code. ** @return pointer protocol name string. ** ** The function returns a pointer to a string containing the name of ** the protocol @a protocol (see the @a vl-file-protocols protocols ** list). If the protocol is unknown the function returns the empty ** string. **/ VL_EXPORT char const * vl_string_protocol_name (int protocol) { switch (protocol) { case VL_PROT_ASCII: return "ascii" ; case VL_PROT_BINARY: return "bin" ; case VL_PROT_NONE : return "" ; default: return 0 ; } } /** ------------------------------------------------------------------ ** @brief Extract base of file name ** @param destination destination buffer. ** @param destinationSize size of destination buffer. ** @param source input string. ** @param maxNumStrippedExtensions maximum number of extensions to strip. ** @return length of the destination string. ** ** The function removes the leading path and up to @c ** maxNumStrippedExtensions trailing extensions from the string @a ** source and writes the result to the buffer @a destination. ** ** The leading path is the longest suffix that ends with either the ** @c \ or @c / characters. An extension is a string starting with ** the . character not containing it. For instance, the string @c ** file.png contains the extension .png and the string @c ** file.tar.gz contains two extensions (.tar and @c .gz). ** ** @sa @ref vl-stringop-err. **/ VL_EXPORT vl_size vl_string_basename (char * destination, vl_size destinationSize, char const * source, vl_size maxNumStrippedExtensions) { char c ; vl_uindex k = 0, beg, end ; /* find beginning */ beg = 0 ; for (k = 0 ; (c = source[k]) ; ++ k) { if (c == '\\' || c == '/') beg = k + 1 ; } /* find ending */ end = strlen (source) ; for (k = end ; k > beg ; --k) { if (source[k - 1] == '.' && maxNumStrippedExtensions > 0) { -- maxNumStrippedExtensions ; end = k - 1 ; } } return vl_string_copy_sub (destination, destinationSize, source + beg, source + end) ; } /** ------------------------------------------------------------------ ** @brief Replace wildcard characters by a string ** @param destination output buffer. ** @param destinationSize size of the output buffer. ** @param source input string. ** @param wildcardChar wildcard character. ** @param escapeChar escape character. ** @param replacement replacement string. ** ** The function replaces the occurrence of the specified wildcard ** character @a wildcardChar by the string @a replacement. The result ** is written to the buffer @a destination of size @a ** destinationSize. ** ** Wildcard characters may be escaped by preceding them by the @a esc ** character. More in general, anything following an occurrence of @a ** esc character is copied verbatim. To disable the escape characters ** simply set @a esc to 0. ** ** @return length of the result. ** @sa @ref vl-stringop-err. **/ VL_EXPORT vl_size vl_string_replace_wildcard (char * destination, vl_size destinationSize, char const * source, char wildcardChar, char escapeChar, char const * replacement) { char c ; vl_uindex k = 0 ; vl_bool escape = 0 ; while ((c = *source++)) { /* enter escape mode ? */ if (! escape && c == escapeChar) { escape = 1 ; continue ; } /* wildcard or regular? */ if (! escape && c == wildcardChar) { char const * repl = replacement ; while ((c = *repl++)) { if (destination && k + 1 < destinationSize) { destination[k] = c ; } ++ k ; } } /* regular character */ else { if (destination && k + 1 < destinationSize) { destination[k] = c ; } ++ k ; } escape = 0 ; } /* add trailing 0 */ if (destinationSize > 0) { destination[VL_MIN(k, destinationSize - 1)] = 0 ; } return k ; } /** ------------------------------------------------------------------ ** @brief Copy string ** @param destination output buffer. ** @param destinationSize size of the output buffer. ** @param source string to copy. ** @return length of the source string. ** ** The function copies the string @a source to the buffer @a ** destination of size @a destinationSize. ** ** @sa @ref vl-stringop-err. **/ VL_EXPORT vl_size vl_string_copy (char * destination, vl_size destinationSize, char const * source) { char c ; vl_uindex k = 0 ; while ((c = *source++)) { if (destination && k + 1 < destinationSize) { destination[k] = c ; } ++ k ; } /* finalize */ if (destinationSize > 0) { destination[VL_MIN(k, destinationSize - 1)] = 0 ; } return k ; } /** ------------------------------------------------------------------ ** @brief Copy substring ** @param destination output buffer. ** @param destinationSize size of output buffer. ** @param beginning start of the substring. ** @param end end of the substring. ** @return length of the destination string. ** ** The function copies the substring from at @a beginning to @a end ** (not included) to the buffer @a destination of size @a ** destinationSize. If, however, the null character is found before ** @a end, the substring terminates there. ** ** @sa @ref vl-stringop-err. **/ VL_EXPORT vl_size vl_string_copy_sub (char * destination, vl_size destinationSize, char const * beginning, char const * end) { char c ; vl_uindex k = 0 ; while (beginning < end && (c = *beginning++)) { if (destination && k + 1 < destinationSize) { destination[k] = c ; } ++ k ; } /* finalize */ if (destinationSize > 0) { destination[VL_MIN(k, destinationSize - 1)] = 0 ; } return k ; } /** ------------------------------------------------------------------ ** @brief Search character in reversed order ** @param beginning pointer to the substring beginning. ** @param end pointer to the substring end. ** @param c character to search for. ** @return pointer to last occurrence of @a c, or 0 if none. ** ** The function searches for the last occurrence of the character @a c ** in the substring from @a beg to @a end (the latter not being included). **/ VL_EXPORT char * vl_string_find_char_rev (char const *beginning, char const* end, char c) { while (end -- != beginning) { if (*end == c) { return (char*) end ; } } return 0 ; } /** ------------------------------------------------------------------ ** @brief Calculate string length ** @param string string. ** @return string length. **/ VL_EXPORT vl_size vl_string_length (char const *string) { vl_uindex i ; for (i = 0 ; string[i] ; ++i) ; return i ; } /** ------------------------------------------------------------------ ** @brief Compare strings case-insensitive ** @param string1 fisrt string. ** @param string2 second string. ** @return an integer =,<,> 0 if @c string1 =,<,> @c string2 **/ VL_EXPORT int vl_string_casei_cmp (const char * string1, const char * string2) { while (tolower((char unsigned)*string1) == tolower((char unsigned)*string2)) { if (*string1 == 0) { return 0 ; } string1 ++ ; string2 ++ ; } return (int)tolower((char unsigned)*string1) - (int)tolower((char unsigned)*string2) ; } /* ------------------------------------------------------------------- * VlEnumeration * ---------------------------------------------------------------- */ /** @brief Get a member of an enumeration by name ** @param enumeration array of ::VlEnumerator objects. ** @param name the name of the desired member. ** @return enumerator matching @a name. ** ** If @a name is not found in the enumeration, then the value ** @c NULL is returned. ** ** @sa vl-stringop-enumeration **/ VL_EXPORT VlEnumerator * vl_enumeration_get (VlEnumerator const *enumeration, char const *name) { assert(enumeration) ; while (enumeration->name) { if (strcmp(name, enumeration->name) == 0) return (VlEnumerator*)enumeration ; enumeration ++ ; } return NULL ; } /** @brief Get a member of an enumeration by name (case insensitive) ** @param enumeration array of ::VlEnumerator objects. ** @param name the name of the desired member. ** @return enumerator matching @a name. ** ** If @a name is not found in the enumeration, then the value ** @c NULL is returned. @a string is matched case insensitive. ** ** @sa vl-stringop-enumeration **/ VL_EXPORT VlEnumerator * vl_enumeration_get_casei (VlEnumerator const *enumeration, char const *name) { assert(enumeration) ; while (enumeration->name) { if (vl_string_casei_cmp(name, enumeration->name) == 0) return (VlEnumerator*)enumeration ; enumeration ++ ; } return NULL ; } /** @brief Get a member of an enumeration by value ** @param enumeration array of ::VlEnumerator objects. ** @param value value of the desired member. ** @return enumerator matching @a value. ** ** If @a value is not found in the enumeration, then the value ** @c NULL is returned. ** ** @sa vl-stringop-enumeration **/ VL_EXPORT VlEnumerator * vl_enumeration_get_by_value (VlEnumerator const *enumeration, vl_index value) { assert(enumeration) ; while (enumeration->name) { if (enumeration->value == value) return (VlEnumerator*)enumeration ; enumeration ++ ; } return NULL ; } colmap-3.9.1/src/thirdparty/VLFeat/stringop.h000066400000000000000000000043031454702036400211240ustar00rootroot00000000000000/** @file stringop.h ** @brief String operations ** @author Andrea Vedaldi **/ /* Copyright (C) 2007-12 Andrea Vedaldi and Brian Fulkerson. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_STRINGOP_H #define VL_STRINGOP_H #include "generic.h" /** @brief File protocols */ enum { VL_PROT_UNKNOWN = -1, /**< unknown protocol */ VL_PROT_NONE = 0, /**< no protocol */ VL_PROT_ASCII, /**< ASCII protocol */ VL_PROT_BINARY /**< Binary protocol */ } ; VL_EXPORT vl_size vl_string_copy (char *destination, vl_size destinationSize, char const *source) ; VL_EXPORT vl_size vl_string_copy_sub (char *destination, vl_size destinationSize, char const *beginning, char const *end) ; VL_EXPORT char *vl_string_parse_protocol (char const *string, int *protocol) ; VL_EXPORT char const *vl_string_protocol_name (int prot) ; VL_EXPORT vl_size vl_string_basename (char *destination, vl_size destinationSize, char const *source, vl_size maxNumStrippedExtension) ; VL_EXPORT vl_size vl_string_replace_wildcard (char * destination, vl_size destinationSize, char const *src, char wildcardChar, char escapeChar, char const *replacement) ; VL_EXPORT char *vl_string_find_char_rev (char const *beginning, char const *end, char c) ; VL_EXPORT vl_size vl_string_length (char const *string) ; VL_EXPORT int vl_string_casei_cmp (const char *string1, const char *string2) ; /** @name String enumerations ** @{ */ /** @brief Member of an enumeration */ typedef struct _VlEnumerator { char const *name ; /**< enumeration member name. */ vl_index value ; /**< enumeration member value. */ } VlEnumerator ; VL_EXPORT VlEnumerator *vl_enumeration_get (VlEnumerator const *enumeration, char const *name) ; VL_EXPORT VlEnumerator *vl_enumeration_get_casei (VlEnumerator const *enumeration, char const *name) ; VL_EXPORT VlEnumerator *vl_enumeration_get_by_value (VlEnumerator const *enumeration, vl_index value) ; /** @} */ /* VL_STRINGOP_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/svm.c000077500000000000000000002062771454702036400201000ustar00rootroot00000000000000/** @file svm.c ** @brief Support Vector Machines (SVM) - Implementation ** @author Milan Sulc ** @author Daniele Perrone ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 Milan Sulc. Copyright (C) 2012 Daniele Perrone. Copyright (C) 2011-13 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file svm.h ** @see @ref svm. **/ /** @page svm Support Vector Machines (SVM) @author Milan Sulc @author Daniele Perrone @author Andrea Vedaldi @tableofcontents *Support Vector Machines* (SVMs) are one of the most popular types of discriminate classifiers. VLFeat implements two solvers, SGD and SDCA, capable of learning linear SVMs on a large scale. These linear solvers can be combined with explicit feature maps to learn non-linear models as well. The solver supports a few variants of the standard SVM formulation, including using loss functions other than the hinge loss. @ref svm-starting demonstrates how to use VLFeat to learn an SVM. Information on SVMs and the corresponding optimization algorithms as implemented by VLFeat are given in: - @subpage svm-fundamentals - Linear SVMs and their learning. - @subpage svm-advanced - Loss functions, dual objective, and other details. - @subpage svm-sgd - The SGD algorithm. - @subpage svm-sdca - The SDCA algorithm. @section svm-starting Getting started This section demonstrates how to learn an SVM by using VLFeat. SVM learning is implemented by the ::VlSvm object type. Let's start by a complete example: @code #include #include int main() { vl_size const numData = 4 ; vl_size const dimension = 2 ; double x [dimension * numData] = { 0.0, -0.5, 0.6, -0.3, 0.0, 0.5 0.6, 0.0} ; double y [numData] = {1, 1, -1, 1} ; double lambda = 0.01; double * const model ; double bias ; VlSvm * svm = vl_svm_new(VlSvmSolverSgd, x, dimension, numData, y, lambda) ; vl_svm_train(svm) ; model = vl_svm_get_model(svm) ; bias = vl_svm_get_bias(svm) ; printf("model w = [ %f , %f ] , bias b = %f \n", model[0], model[1], bias); vl_svm_delete(svm) ; return 0; } @endcode This code learns a binary linear SVM using the SGD algorithm on four two-dimensional points using 0.01 as regularization parameter. ::VlSvmSolverSdca can be specified in place of ::VlSvmSolverSdca in orer to use the SDCA algorithm instead. Convergence and other diagnostic information can be obtained after training by using the ::vl_svm_get_statistics function. Algorithms regularly check for convergence (usally after each pass over the data). The ::vl_svm_set_diagnostic_function can be used to specify a callback to be invoked when diagnostic is run. This can be used, for example, to dump information on the screen as the algorithm progresses. Convergence is reached after a maximum number of iterations (::vl_svm_set_max_num_iterations) or after a given criterion falls below a threshold (::vl_svm_set_epsilon). The meaning of these may depend on the specific algorithm (see @ref svm for further details). ::VlSvm is a quite powerful object. Algorithms only need to perform inner product and accumulation operation on the data (see @ref svm-advanced). This is used to abstract from the data type and support almost anything by speciying just two functions (::vl_svm_set_data_functions). A simple interface to this advanced functionality is provided by the ::VlSvmDataset object. This supports natively @c float and @c double data types, as well as applying on the fly the homogeneous kernel map (@ref homkermap). This is exemplified in @ref svmdataset-starting. */ /** @page svm-fundamentals SVM fundamentals @tableofcontents This page introduces the SVM formulation used in VLFeat. See @ref svm for more information on VLFeat SVM support. Let $ \bx \in \real^d $ be a vector representing, for example, an image, an audio track, or a fragment of text. Our goal is to design a *classifier*, i.e. a function that associates to each vector $\bx$ a positive or negative label based on a desired criterion, for example the fact that the image contains or not a cat, that the audio track contains or not English speech, or that the text is or not a scientific paper. The vector $\bx$ is classified by looking at the sign of a *linear scoring function* $\langle \bx, \bw \rangle$. The goal of learning is to estimate the parameter $\bw \in \real^d$ in such a way that the score is positive if the vector $\bx$ belongs to the positive class and negative otherwise. In fact, in the standard SVM formulation the the goal is to have a score of *at least 1* in the first case, and of *at most -1* in the second one, imposing a *margin*. The parameter $\bw$ is estimated or *learned* by fitting the scoring function to a training set of $n$ example pairs $(\bx_i,y_i), i=1,\dots,n$. Here $y_i \in \{-1,1\}$ are the *ground truth labels* of the corresponding example vectors. The fit quality is measured by a *loss function* which, in standard SVMs, is the *hinge loss*: \[ \ell_i(\langle \bw,\bx\rangle) = \max\{0, 1 - y_i \langle \bw,\bx\rangle\}. \] Note that the hinge loss is zero only if the score $\langle \bw,\bx\rangle$ is at least 1 or at most -1, depending on the label $y_i$. Fitting the training data is usually insufficient. In order for the scoring function *generalize to future data* as well, it is usually preferable to trade off the fitting accuracy with the *regularity* of the learned scoring function $\langle \bx, \bw \rangle$. Regularity in the standard formulation is measured by the norm of the parameter vector $\|\bw\|^2$ (see @ref svm-advanced). Averaging the loss on all training samples and adding to it the regularizer weighed by a parameter $\lambda$ yields the *regularized loss objective* @f{equation}{ \boxed{\displaystyle E(\bw) = \frac{\lambda}{2} \left\| \bw \right\|^2 + \frac{1}{n} \sum_{i=1}^n \max\{0, 1 - y_i \langle \bw,\bx\rangle\}. \label{e:svm-primal-hinge} } @f} Note that this objective function is *convex*, so that there exists a single global optimum. The scoring function $\langle \bx, \bw \rangle$ considered so far has been linear and unbiased. @ref svm-bias discusses how a bias term can be added to the SVM and @ref svm-feature-maps shows how non-linear SVMs can be reduced to the linear case by computing suitable feature maps. @ref svm-learning shows how VLFeat can be used to learn an SVM by minimizing $E(\bw)$. @section svm-learning Learning Learning an SVM amounts to finding the minimizer $\bw^*$ of the cost function $E(\bw)$. While there are dozens of methods that can be used to do so, VLFeat implements two large scale methods, designed to work with linear SVMs (see @ref svm-feature-maps to go beyond linear): - @ref svm-sgd - @ref svm-sdca Using these solvers is exemplified in @ref svm-starting. @section svm-bias Adding a bias It is common to add to the SVM scoring function a *bias term* $b$, and to consider the score $\langle \bx,\bw \rangle + b$. In practice the bias term can be crucial to fit the training data optimally, as there is no reason why the inner products $\langle \bx,\bw \rangle$ should be naturally centered at zero. Some SVM learning algorithms can estimate both $\bw$ and $b$ directly. However, other algorithms such as SGD and SDCA cannot. In this case, a simple workaround is to add a constant component $B > 0$ (we call this constant the *bias multiplier*) to the data, i.e. consider the extended data vectors: \[ \bar \bx = \begin{bmatrix} \bx \\ B \end{bmatrix}, \quad \bar \bw = \begin{bmatrix} \bw \\ w_b \end{bmatrix}. \] In this manner the scoring function incorporates implicitly a bias $b = B w_b$: \[ \langle \bar\bx, \bar\bw \rangle = \langle \bx, \bw \rangle + B w_b. \] The disadvantage of this reduction is that the term $w_b^2$ becomes part of the SVM regularizer, which shrinks the bias $b$ towards zero. This effect can be alleviated by making $B$ sufficiently large, because in this case $\|\bw\|^2 \gg w_b^2$ and the shrinking effect is negligible. Unfortunately, making $B$ too large makes the problem numerically unbalanced, so a reasonable trade-off between shrinkage and stability is generally sought. Typically, a good trade-off is obtained by normalizing the data to have unitary Euclidean norm and then choosing $B \in [1, 10]$. Specific implementations of SGD and SDCA may provide explicit support to learn the bias in this manner, but it is important to understand the implications on speed and accuracy of the learning if this is done. @section svm-feature-maps Non-linear SVMs and feature maps So far only linear scoring function $\langle \bx,\bw \rangle$ have been considered. Implicitly, however, this assumes that the objects to be classified (e.g. images) have been encoded as vectors $\bx$ in a way that makes linear classification possible. This encoding step can be made explicit by introducing the *feature map* $\Phi(\bx) \in \real^d$. Including the feature map yields a scoring function *non-linear* in $\bx$: \[ \bx\in\mathcal{X} \quad\longrightarrow\quad \langle \Phi(\bx), \bw \rangle. \] The nature of the input space $\mathcal{X}$ can be arbitrary and might not have a vector space structure at all. The representation or encoding captures a notion of *similarity* between objects: if two vectors $\Phi(\bx_1)$ and $\Phi(\bx_2)$ are similar, then their scores will also be similar. Note that choosing a feature map amounts to incorporating this information in the model *prior* to learning. The relation of feature maps to similarity functions is formalized by the notion of a *kernel*, a positive definite function $K(\bx,\bx')$ measuring the similarity of a pair of objects. A feature map defines a kernel by \[ K(\bx,\bx') = \langle \Phi(\bx),\Phi(\bx') \rangle. \] Viceversa, any kernel function can be represented by a feature map in this manner, establishing an equivalence. So far, all solvers in VLFeat assume that the feature map $\Psi(\bx)$ can be explicitly computed. Although classically kernels were introduced to generalize solvers to non-linear SVMs for which a feature map *cannot* be computed (e.g. for a Gaussian kernel the feature map is infinite dimensional), in practice using explicit feature representations allow to use much faster solvers, so it makes sense to *reverse* this process. */ /** @page svm-advanced Advanced SVM topics @tableofcontents This page discusses advanced SVM topics. For an introduction to SVMs, please refer to @ref svm and @ref svm-fundamentals. @section svm-loss-functions Loss functions The SVM formulation given in @ref svm-fundamentals uses the hinge loss, which is only one of a variety of loss functions that are often used for SVMs. More in general, one can consider the objective @f{equation}{ E(\bw) = \frac{\lambda}{2} \left\| \bw \right\|^2 + \frac{1}{n} \sum_{i=1}^n \ell_i(\langle \bw,\bx\rangle). \label{e:svm-primal} @f} where the loss $\ell_i(z)$ is a convex function of the scalar variable $z$. Losses differ by: (i) their purpose (some are suitable for classification, other for regression), (ii) their smoothness (which usually affects how quickly the SVM objective function can be minimized), and (iii) their statistical interpretation (for example the logistic loss can be used to learn logistic models). Concrete examples are the:
Name Loss $\ell_i(z)$ Description
Hinge $\max\{0, 1-y_i z\}$ The standard SVM loss function.
Square hinge $\max\{0, 1-y_i z\}^2$ The standard SVM loss function, but squared. This version is smoother and may yield numerically easier problems.
Square or l2 $(y_i - z)^2$ This loss yields the ridge regression model (l2 regularised least square).
Linear or l1 $|y_i - z|$ Another loss suitable for regression, usually more robust but harder to optimize than the squared one.
Insensitive l1 $\max\{0, |y_i - z| - \epsilon\}$. This is a variant of the previous loss, proposed in the original Support Vector Regression formulation. Differently from the previous two losses, the insensitivity may yield to a sparse selection of support vectors.
Logistic $\log(1 + e^{-y_i z})$ This corresponds to regularized logisitc regression. The loss can be seen as a negative log-likelihood: $\ell_i(z) = -\log P[y_i | z] = - \log \sigma(y_iz/2)$, where $\sigma(z) = e^z/(1 + e^z)$ is the sigmoid function, mapping a score $z$ to a probability. The $1/2$ factor in the sigmoid is due to the fact that labels are in $\{-1,1\}$ rather than $\{0,1\}$ as more common for the standard sigmoid model.
@section svm-data-abstraction Data abstraction: working with compressed data VLFeat learning algorithms (SGD and SDCA) access the data by means of only two operations: - *inner product*: computing the inner product between the model and a data vector, i.e. $\langle \bw, \bx \rangle$. - *accumulation*: summing a data vector to the model, i.e. $\bw \leftarrow \bw + \beta \bx$. VLFeat learning algorithms are *parameterized* in these two operations. As a consequence, the data can be stored in any format suitable to the user (e.g. dense matrices, sparse matrices, block-sparse matrices, disk caches, and so on) provided that these two operations can be implemented efficiently. Differently from the data, however, the model vector $\bw$ is represented simply as a dense array of doubles. This choice is adequate in almost any case. A particularly useful aspect of this design choice is that the training data can be store in *compressed format* (for example by using product quantization (PQ)). Furthermore, higher-dimensional encodings such as the homogeneous kernel map (@ref homkermap) and the intersection kernel map can be *computed on the fly*. Such techniques are very important when dealing with GBs of data. @section svm-dual-problem Dual problem In optimization, the *dual objective* $D(\balpha)$ of the SVM objective $E(\bw)$ is of great interest. To obtain the dual objective, one starts by approximating each loss term from below by a family of planes: \[ \ell_i(z) = \sup_{u} (u z - \ell_i^*(u) ), \qquad \ell_i^*(u) = \sup_{z} (z u - \ell_i(z) ) \] where $\ell_i^*(u)$ is the *dual conjugate* of the loss and gives the intercept of each approximating plane as a function of the slope. When the loss function is convex, the approximation is in fact exact. Examples include:
Name Loss $\ell_i(z)$ Conjugate loss $\ell_i^*(u)$
Hinge $\max\{0, 1-y_i z\}$ \[ \ell_i^*(u) = \begin{cases} y_i u, & -1 \leq y_i u \leq 0, \\ +\infty, & \text{otherwise} \end{cases} \]
Square hinge $\max\{0, 1-y_i z\}^2$ \[\ell_i^*(u) = \begin{cases} y_i u + \frac{u^2}{4}, & y_i u \leq 0, \\ +\infty, & \text{otherwise} \\ \end{cases}\]
Linear or l1 $|y_i - z|$ \[\ell_i^*(u) = \begin{cases} y_i u, & -1 \leq y_i u \leq 1, \\ +\infty, & \text{otherwise} \\ \end{cases}\]
Square or l2 $(y_i - z)^2$ \[\ell_i^*(u)=y_iu + \frac{u^2}{4}\]
Insensitive l1 $\max\{0, |y_i - z| - \epsilon\}$.
Logistic $\log(1 + e^{-y_i z})$ \[\ell_i^*(u) = \begin{cases} (1+u) \log(1+u) - u \log(-u), & -1 \leq y_i u \leq 0, \\ +\infty, & \text{otherwise} \\ \end{cases}\]
Since each plane $- z \alpha_i - \ell^*_i(-\alpha_i) \leq \ell_i(z)$ bounds the loss from below, by substituting in $E(\bw)$ one can write a lower bound for the SVM objective \[ F(\bw,\balpha) = \frac{\lambda}{2} \|\bw\|^2 - \frac{1}{n}\sum_{i=1}^n (\bw^\top \bx_i\alpha_i + \ell_i^*(-\alpha_i)) \leq E(\bw). \] for each setting of the *dual variables* $\alpha_i$. The dual objective function $D(\balpha)$ is obtained by minimizing the lower bound $F(\bw,\balpha)$ w.r.t. to $\bw$: \[ D(\balpha) = \inf_{\bw} F(\bw,\balpha) \leq E(\bw). \] The minimizer and the dual objective are now easy to find: \[ \boxed{\displaystyle \bw(\balpha) = \frac{1}{\lambda n} \sum_{i=1}^n \bx_i \alpha_i = \frac{1}{\lambda n} X\balpha, \quad D(\balpha) = - \frac{1}{2\lambda n^2} \balpha^\top X^\top X \balpha + \frac{1}{n} \sum_{i=1}^n - \ell_i^*(-\alpha_i) } \] where $X = [\bx_1, \dots, \bx_n]$ is the data matrix. Since the dual is uniformly smaller than the primal, one has the *duality gap* bound: \[ D(\balpha) \leq P(\bw^*) \leq P(\bw(\balpha)) \] This bound can be used to evaluate how far off $\bw(\balpha)$ is from the primal minimizer $\bw^*$. In fact, due to convexity, this bound can be shown to be zero when $\balpha^*$ is the dual maximizer (strong duality): \[ D(\balpha^*) = P(\bw^*) = P(\bw(\balpha^*)), \quad \bw^* = \bw(\balpha^*). \] @section svm-C Parametrization in C Often a slightly different form of the SVM objective is considered, where a parameter $C$ is used to scale the loss instead of the regularizer: \[ E_C(\bw) = \frac{1}{2} \|\bw\|^2 + C \sum_{i=1}^n \ell_i(\langle \bx_i, \bw\rangle) \] This and the objective function $E(\bw)$ in $\lambda$ are equivalent (proportional) if \[ \lambda = \frac{1}{nC}, \qquad C = \frac{1}{n\lambda}. \] up to an overall scaling factor to the problem. **/ /** @page svm-sdca Stochastic Dual Coordinate Ascent @tableofcontents This page describes the *Stochastic Dual Coordinate Ascent* (SDCA) linear SVM solver. Please see @ref svm for an overview of VLFeat SVM support. SDCA maximizes the dual SVM objective (see @ref svm-dual-problem for a derivation of this expression): \[ D(\balpha) = - \frac{1}{2\lambda n^2} \balpha^\top X^\top X \balpha + \frac{1}{n} \sum_{i=1}^n - \ell_i^*(-\alpha_i) \] where $X$ is the data matrix. Recall that the primal parameter corresponding to a given setting of the dual variables is: \[ \bw(\balpha) = \frac{1}{\lambda n} \sum_{i=1}^n \bx_i \alpha_i = \frac{1}{\lambda n} X\balpha \] In its most basic form, the *SDCA algorithm* can be summarized as follows: - Let $\balpha_0 = 0$. - Until the duality gap $P(\bw(\balpha_t)) - D(\balpha_t) < \epsilon$ - Pick a dual variable $q$ uniformly at random in $1, \dots, n$. - Maximize the dual with respect to this variable: $\Delta\alpha_q = \max_{\Delta\alpha_q} D(\balpha_t + \Delta\alpha_q \be_q )$ - Update $\balpha_{t+1} = \balpha_{t} + \be_q \Delta\alpha_q$. In VLFeat, we partially use the nomenclature from @cite{shwartz13a-dual} and @cite{hsieh08a-dual}. @section svm-sdca-dual-max Dual coordinate maximization The updated dual objective can be expanded as: \[ D(\balpha_t + \be_q \Delta\alpha_q) = \text{const.} - \frac{1}{2\lambda n^2} \bx_q^\top \bx_q (\Delta\alpha_q)^2 - \frac{1}{n} \bx_q^\top \frac{X\alpha_t}{\lambda n} \Delta\alpha_q - \frac{1}{n} \ell^*_q(- \alpha_q - \Delta\alpha_q) \] This can also be written as @f{align*} D(\balpha_t + \be_q \Delta\alpha_q) &\propto - \frac{A}{2} (\Delta\alpha_q)^2 - B \Delta\alpha_q - \ell^*_q(- \alpha_q - \Delta\alpha_q), \\ A &= \frac{1}{\lambda n} \bx_q^\top \bx_q = \frac{1}{\lambda n} \| \bx_q \|^2, \\ B &= \bx_q^\top \frac{X\balpha_t}{\lambda n} = \bx_q^\top \bw_t. @f} Maximizing this quantity in the scalar variable $\Delta\balpha$ is usually not difficult. It is convenient to store and incrementally update the model $\bw_t$ after the optimal step $\Delta\balpha$ has been determined: \[ \bw_t = \frac{X \balpha_t}{\lambda n}, \quad \bw_{t+1} = \bw_t + \frac{1}{\lambda n }\bx_q \be_q \Delta\alpha_q. \] For example, consider the hinge loss as given in @ref svm-advanced : \[ \ell_q^*(u) = \begin{cases} y_q u, & -1 \leq y_q u \leq 0, \\ +\infty, & \text{otherwise}. \end{cases} \] The maximizer $\Delta\alpha_q$ of the update objective must be in the range where the conjugate loss is not infinite. Ignoring such bounds, the update can be obtained by setting the derivative of the objective to zero, obtaining \[ \tilde {\Delta \alpha_q}= \frac{y_q - B}{A}. \] Note that $B$ is simply current score associated by the SVM to the sample $\bx_q$. Incorporating the constraint $-1 \leq - y_q (\alpha_q + \Delta \alpha_q) \leq 0$, i.e. $0 \leq y_q (\alpha_q + \Delta \alpha_q) \leq 1$, one obtains the update \[ \Delta\alpha_q = y_q \max\{0, \min\{1, y_q (\tilde {\Delta\alpha_q } + \alpha_q)\}\} - \alpha_q. \] @section svm-sdca-details Implementation details Rather than visiting points completely at random, VLFeat SDCA follows the best practice of visiting all the points at every epoch (pass through the data), changing the order of the visit randomly by picking every time a new random permutation. **/ /** @page svm-sgd Stochastic Gradient Descent @tableofcontents This page describes the *Stochastic Gradient Descent* (SGD) linear SVM solver. SGD minimizes directly the primal SVM objective (see @ref svm): \[ E(\bw) = \frac{\lambda}{2} \left\| \bw \right\|^2 + \frac{1}{n} \sum_{i=1}^n \ell_i(\langle \bw,\bx\rangle) \] Firts, rewrite the objective as the average \[ E(\bw) = \frac{1}{n} \sum_{i=1}^n E_i(\bw), \quad E_i(\bw) = \frac{\lambda}{2} \left\| \bw \right\|^2 + \ell_i(\langle \bw,\bx\rangle). \] Then SGD performs gradient steps by considering at each iteration one term $E_i(\bw)$ selected at random from this average. In its most basic form, the algorithm is: - Start with $\bw_0 = 0$. - For $t=1,2,\dots T$: - Sample one index $i$ in $1,\dots,n$ uniformly at random. - Compute a subgradient $\bg_t$ of $E_i(\bw)$ at $\bw_t$. - Compute the learning rate $\eta_t$. - Update $\bw_{t+1} = \bw_t - \eta_t \bg_t$. Provided that the learning rate $\eta_t$ is chosen correctly, this simple algorithm is guaranteed to converge to the minimizer $\bw^*$ of $E$. @section svm-sgd-convergence Convergence and speed The goal of the SGD algorithm is to bring the *primal suboptimality* below a threshold $\epsilon_P$: \[ E(\bw_t) - E(\bw^*) \leq \epsilon_P. \] If the learning rate $\eta_t$ is selected appropriately, SGD can be shown to converge properly. For example, @cite{shalev-shwartz07pegasos} show that, since $E(\bw)$ is $\lambda$-strongly convex, then using the learning rate \[ \boxed{\eta_t = \frac{1}{\lambda t}} \] guarantees that the algorithm reaches primal-suboptimality $\epsilon_P$ in \[ \tilde O\left( \frac{1}{\lambda \epsilon_P} \right). \] iterations. This particular SGD variant is sometimes known as PEGASOS @cite{shalev-shwartz07pegasos} and is the version implemented in VLFeat. The *convergence speed* is not sufficient to tell the *learning speed*, i.e. how quickly an algorithm can learn an SVM that performs optimally on the test set. The following two observations can be used to link convergence speed to learning speed: - The regularizer strength is often heuristically selected to be inversely proportional to the number of training samples: $\lambda = \lambda_0 /n$. This reflects the fact that with more training data the prior should count less. - The primal suboptimality $\epsilon_P$ should be about the same as the estimation error of the SVM primal. This estimation error is due to the finite training set size and can be shown to be of the order of $1/\lambda n = 1 / \lambda_0$. Under these two assumptions, PEGASOS can learn a linear SVM in time $\tilde O(n)$, which is *linear in the number of training examples*. This fares much better with $O(n^2)$ or worse of non-linear SVM solvers. @section svm-sgd-bias The bias term Adding a bias $b$ to the SVM scoring function $\langle \bw, \bx \rangle +b$ is done, as explained in @ref svm-bias, by appending a constant feature $B$ (the *bias multiplier*) to the data vectors $\bx$ and a corresponding weight element $w_b$ to the weight vector $\bw$, so that $b = B w_b$ As noted, the bias multiplier should be relatively large in order to avoid shrinking the bias towards zero, but small to make the optimization stable. In particular, setting $B$ to zero learns an unbiased SVM (::vl_svm_set_bias_multiplier). To counter instability caused by a large bias multiplier, the learning rate of the bias is slowed down by multiplying the overall learning rate $\eta_t$ by a bias-specific rate coefficient (::vl_svm_set_bias_learning_rate). As a rule of thumb, if the data vectors $\bx$ are $l^2$ normalized (as they typically should for optimal performance), then a reasonable bias multiplier is in the range 1 to 10 and a reasonable bias learning rate is somewhere in the range of the inverse of that (in this manner the two parts of the extended feature vector $(\bx, B)$ are balanced). @section svm-sgd-starting-iteration Adjusting the learning rate Initially, the learning rate $\eta_t = 1/\lambda t$ is usually too fast: as usually $\lambda \ll 1$, $\eta_1 \gg 1$. But this is clearly excessive (for example, without a loss term, the best learning rate at the first iteration is simply $\eta_1=1$, as this nails the optimum in one step). Thus, the learning rate formula is modified to be $\eta_t = 1 / \lambda (t + t_0)$, where $t_0 \approx 2/\lambda$, which is equivalent to start $t_0$ iterations later. In this manner $\eta_1 \approx 1/2$. @subsection svm-sgd-warm-start Warm start Starting from a given model $\bw$ is easy in SGD as the optimization runs in the primal. However, the starting iteration index $t$ should also be advanced for a warm start, as otherwise the initial setting of $\bw$ is rapidly forgot (::vl_svm_set_model, ::vl_svm_set_bias, ::vl_svm_set_iteration_number). @section svm-sgd-details Implementation details @par "Random sampling of points" Rather than visiting points completely at random, VLFeat SDCA follows the best practice of visiting all the points at every epoch (pass through the data), changing the order of the visit randomly by picking every time a new random permutation. @par "Factored representation" At each iteration, the SGD algorithm updates the vector $\bw$ (including the additional bias component $w_b$) as $\bw_{t+1} \leftarrow \bw_t - \lambda \eta_t \bw_t - \eta_t \bg_t$, where $\eta_t$ is the learning rate. If the subgradient of the loss function $\bg_t$ is zero at a given iteration, this amounts to simply shrink $\bw$ towards the origin by multiplying it by the factor $1 - \lambda \eta_t$. Thus such an iteration can be accelerated significantly by representing internally $\bw_t = f_t \bu_t$, where $f_t$ is a scaling factor. Then, the update becomes \[ f_{t+1} \bu_{t+1} = f_{t} \bu_{t} - \lambda \eta_t f_{t} \bu_{t} - \eta_t \bg_t = (1-\lambda \eta_t) f_{t} \bu_{t} - \eta_t \bg_t. \] Setting $f_{t+1} = (1-\lambda \eta_t) f_{t}$, this gives the update equation for $\bu_t$ \[ \bu_{t+1} = \bu_{t} - \frac{\eta_t}{f_{t+1}} \bg_t. \] but this step can be skipped whenever $\bg_t$ is equal to zero. When the bias component has a different learning rate, this scheme must be adjusted slightly by adding a separated factor for the bias, but it is otherwise identical. **/ /* @section svm-pegasos PEGASOS @subsection svm-pegasos-algorithm Algorithm PEGASOS @cite{shalev-shwartz07pegasos} is a stochastic subgradient optimizer. At the t-th iteration the algorithm: - Samples uniformly at random as subset @f$ A_t @f$ of k of training pairs @f$(x,y)@f$ from the m pairs provided for training (this subset is called mini batch). - Computes a subgradient @f$ \nabla_t @f$ of the function @f$ E_t(w) = \frac{1}{2}\|w\|^2 + \frac{1}{k} \sum_{(x,y) \in A_t} \ell(w;(x,y)) @f$ (this is the SVM objective function restricted to the minibatch). - Compute an intermediate weight vector @f$ w_{t+1/2} @f$ by doing a step @f$ w_{t+1/2} = w_t - \alpha_t \nabla_t @f$ with learning rate @f$ \alpha_t = 1/(\eta t) @f$ along the subgradient. Note that the learning rate is inversely proportional to the iteration number. - Back projects the weight vector @f$ w_{t+1/2} @f$ on the hypersphere of radius @f$ \sqrt{\lambda} @f$ to obtain the next model estimate @f$ w_{t+1} @f$: @f[ w_t = \min\{1, \sqrt{\lambda}/\|w\|\} w_{t+1/2}. @f] The hypersphere is guaranteed to contain the optimal weight vector @f$ w^* @f$. VLFeat implementation fixes to one the size of the mini batches @f$ k @f$. @subsection svm-pegasos-permutation Permutation VLFeat PEGASOS can use a user-defined permutation to decide the order in which data points are visited (instead of using random sampling). By specifying a permutation the algorithm is guaranteed to visit each data point exactly once in each loop. The permutation needs not to be bijective. This can be used to visit certain data samples more or less often than others, implicitly reweighting their relative importance in the SVM objective function. This can be used to balance the data. @subsection svm-pegasos-kernels Non-linear kernels PEGASOS can be extended to non-linear kernels, but the algorithm is not particularly efficient in this setting [1]. When possible, it may be preferable to work with explicit feature maps. Let @f$ k(x,y) @f$ be a positive definite kernel. A feature map is a function @f$ \Psi(x) @f$ such that @f$ k(x,y) = \langle \Psi(x), \Psi(y) \rangle @f$. Using this representation the non-linear SVM learning objective function writes: @f[ \min_{w} \frac{\lambda}{2} \|w\|^2 + \frac{1}{m} \sum_{i=1}^n \ell(w; (\Psi(x)_i,y_i)). @f] Thus the only difference with the linear case is that the feature @f$ \Psi(x) @f$ is used in place of the data @f$ x @f$. @f$ \Psi(x) @f$ can be learned off-line, for instance by using the incomplete Cholesky decomposition @f$ V^\top V @f$ of the Gram matrix @f$ K = [k(x_i,x_j)] @f$ (in this case @f$ \Psi(x_i) @f$ is the i-th columns of V). Alternatively, for additive kernels (e.g. intersection, Chi2) the explicit feature map computed by @ref homkermap.h can be used. For additive kernels it is also possible to perform the feature expansion online inside the solver, setting the specific feature map via ::vl_svmdataset_set_map. This is particular useful to keep the size of the training data small, when the number of the samples is big or the memory is limited. */ #include "svm.h" #include "mathop.h" #include struct VlSvm_ { VlSvmSolverType solver ; /**< SVM solver type. */ vl_size dimension ; /**< Model dimension. */ double * model ; /**< Model ($\bw$ vector). */ double bias ; /**< Bias. */ double biasMultiplier ; /**< Bias feature multiplier. */ /* valid during a run */ double lambda ; /**< Regularizer multiplier. */ void const * data ; vl_size numData ; double const * labels ; /**< Data labels. */ double const * weights ; /**< Data weights. */ VlSvmDataset * ownDataset ; /**< Optional owned dataset. */ VlSvmDiagnosticFunction diagnosticFn ; void * diagnosticFnData ; vl_size diagnosticFrequency ; /**< Frequency of diagnostic. */ VlSvmLossFunction lossFn ; VlSvmLossFunction conjugateLossFn ; VlSvmLossFunction lossDerivativeFn ; VlSvmDcaUpdateFunction dcaUpdateFn ; VlSvmInnerProductFunction innerProductFn ; VlSvmAccumulateFunction accumulateFn ; vl_size iteration ; /**< Current iterations number. */ vl_size maxNumIterations ; /**< Maximum number of iterations. */ double epsilon ; /**< Stopping threshold. */ /* Book keeping */ VlSvmStatistics statistics ; /**< Statistcs. */ double * scores ; /* SGD specific */ double biasLearningRate ; /**< Bias learning rate. */ /* SDCA specific */ double * alpha ; /**< Dual variables. */ } ; /* ---------------------------------------------------------------- */ /** @brief Create a new object with plain data. ** @param type type of SMV solver. ** @param data a pointer to a matrix of data. ** @param dimension dimension of the SVM model. ** @param numData number of training samples. ** @param labels training labels. ** @param lambda regularizer parameter. ** @return the new object. ** ** @a data has one column per sample, in @c double format. ** More advanced inputs can be used with ::vl_svm_new_with_dataset ** and ::vl_svm_new_with_abstract_data. ** ** @sa ::vl_svm_delete **/ VlSvm * vl_svm_new (VlSvmSolverType type, double const * data, vl_size dimension, vl_size numData, double const * labels, double lambda) { VlSvmDataset * dataset = vl_svmdataset_new(VL_TYPE_DOUBLE, (void*)data, dimension, numData) ; VlSvm * self = vl_svm_new_with_dataset (type, dataset, labels, lambda) ; self->ownDataset = dataset ; return self ; } /** @brief Create a new object with a dataset. ** @param solver type of SMV solver. ** @param dataset SVM dataset object ** @param labels training samples labels. ** @param lambda regularizer parameter. ** @return the new object. ** @sa ::vl_svm_delete **/ VlSvm * vl_svm_new_with_dataset (VlSvmSolverType solver, VlSvmDataset * dataset, double const * labels, double lambda) { VlSvm * self = vl_svm_new_with_abstract_data (solver, dataset, vl_svmdataset_get_dimension(dataset), vl_svmdataset_get_num_data(dataset), labels, lambda) ; vl_svm_set_data_functions (self, vl_svmdataset_get_inner_product_function(dataset), vl_svmdataset_get_accumulate_function(dataset)) ; return self ; } /** @brief Create a new object with abstract data. ** @param solver type of SMV solver. ** @param data pointer to the data. ** @param dimension dimension of the SVM model. ** @param numData num training samples. ** @param labels training samples labels. ** @param lambda regularizer parameter. ** @return the new object. ** ** After calling this function, ::vl_svm_set_data_functions *must* ** be used to setup suitable callbacks for the inner product ** and accumulation operations (@see svm-data-abstraction). ** ** @sa ::vl_svm_delete **/ VlSvm * vl_svm_new_with_abstract_data (VlSvmSolverType solver, void * data, vl_size dimension, vl_size numData, double const * labels, double lambda) { VlSvm * self = vl_calloc(1,sizeof(VlSvm)) ; assert(dimension >= 1) ; assert(numData >= 1) ; assert(labels) ; self->solver = solver ; self->dimension = dimension ; self->model = 0 ; self->bias = 0 ; self->biasMultiplier = 1.0 ; self->lambda = lambda ; self->data = data ; self->numData = numData ; self->labels = labels ; self->diagnosticFrequency = numData ; self->diagnosticFn = 0 ; self->diagnosticFnData = 0 ; self->lossFn = vl_svm_hinge_loss ; self->conjugateLossFn = vl_svm_hinge_conjugate_loss ; self->lossDerivativeFn = vl_svm_hinge_loss_derivative ; self->dcaUpdateFn = vl_svm_hinge_dca_update ; self->innerProductFn = 0 ; self->accumulateFn = 0 ; self->iteration = 0 ; self->maxNumIterations = VL_MAX((double)numData, vl_ceil_f(10.0 / lambda)) ; self->epsilon = 1e-2 ; /* SGD */ self->biasLearningRate = 0.01 ; /* SDCA */ self->alpha = 0 ; /* allocations */ self->model = vl_calloc(dimension, sizeof(double)) ; if (self->model == NULL) goto err_alloc ; if (self->solver == VlSvmSolverSdca) { self->alpha = vl_calloc(self->numData, sizeof(double)) ; if (self->alpha == NULL) goto err_alloc ; } self->scores = vl_calloc(numData, sizeof(double)) ; if (self->scores == NULL) goto err_alloc ; return self ; err_alloc: if (self->scores) { vl_free (self->scores) ; self->scores = 0 ; } if (self->model) { vl_free (self->model) ; self->model = 0 ; } if (self->alpha) { vl_free (self->alpha) ; self->alpha = 0 ; } return 0 ; } /** @brief Delete object. ** @param self object. ** @sa ::vl_svm_new **/ void vl_svm_delete (VlSvm * self) { if (self->model) { vl_free (self->model) ; self->model = 0 ; } if (self->alpha) { vl_free (self->alpha) ; self->alpha = 0 ; } if (self->ownDataset) { vl_svmdataset_delete(self->ownDataset) ; self->ownDataset = 0 ; } vl_free (self) ; } /* ---------------------------------------------------------------- */ /* Setters and getters */ /* ---------------------------------------------------------------- */ /** @brief Set the convergence threshold ** @param self object ** @param epsilon threshold (non-negative). **/ void vl_svm_set_epsilon (VlSvm *self, double epsilon) { assert(self) ; assert(epsilon >= 0) ; self->epsilon = epsilon ; } /** @brief Get the convergence threshold ** @param self object ** @return epsilon threshold. **/ double vl_svm_get_epsilon (VlSvm const *self) { assert(self) ; return self->epsilon ; } /** @brief Set the bias learning rate ** @param self object ** @param rate bias learning rate (positive). ** ** This parameter applies only to the SGD solver. **/ void vl_svm_set_bias_learning_rate (VlSvm *self, double rate) { assert(self) ; assert(rate > 0) ; self->biasLearningRate = rate ; } /** @brief Get the bias leraning rate. ** @param self object ** @return bias learning rate. **/ double vl_svm_get_bias_learning_rate (VlSvm const *self) { assert(self) ; return self->biasLearningRate ; } /** @brief Set the bias multiplier. ** @param self object ** @param b bias multiplier. ** ** The *bias multiplier* is the value of the constant feature ** appended to the data vectors to implement the bias (@ref svm-bias). **/ void vl_svm_set_bias_multiplier (VlSvm * self, double b) { assert(self) ; assert(b >= 0) ; self->biasMultiplier = b ; } /** @brief Get the bias multiplier. ** @param self object. ** @return bias multiplier. **/ double vl_svm_get_bias_multiplier (VlSvm const * self) { assert(self) ; return self->biasMultiplier ; } /** @brief Set the current iteratio number. ** @param self object. ** @param n iteration number. ** ** If called before training, ** this can be used with SGD for a warm start, as the net ** effect is to slow down the learning rate. **/ void vl_svm_set_iteration_number (VlSvm *self, vl_uindex n) { assert(self) ; self->iteration = n ; } /** @brief Get the current iteration number. ** @param self object. ** @return current iteration number. **/ vl_size vl_svm_get_iteration_number (VlSvm const *self) { assert(self) ; return self->iteration ; } /** @brief Set the maximum number of iterations. ** @param self object. ** @param n maximum number of iterations. **/ void vl_svm_set_max_num_iterations (VlSvm *self, vl_size n) { assert(self) ; self->maxNumIterations = n ; } /** @brief Get the maximum number of iterations. ** @param self object. ** @return maximum number of iterations. **/ vl_size vl_svm_get_max_num_iterations (VlSvm const *self) { assert(self) ; return self->maxNumIterations ; } /** @brief Set the diagnostic frequency. ** @param self object. ** @param f diagnostic frequency (@c >= 1). ** ** A diagnostic round (to test for convergence and to printout ** information) is performed every @a f iterations. **/ void vl_svm_set_diagnostic_frequency (VlSvm *self, vl_size f) { assert(self) ; assert(f > 0) ; self->diagnosticFrequency = f ; } /** @brief Get the diagnostic frequency. ** @param self object. ** @return diagnostic frequency. **/ vl_size vl_svm_get_diagnostic_frequency (VlSvm const *self) { assert(self) ; return self->diagnosticFrequency ; } /** @brief Get the SVM solver type. ** @param self object. ** @return SVM solver type. **/ VlSvmSolverType vl_svm_get_solver (VlSvm const * self) { assert(self) ; return self->solver ; } /** @brief Set the regularizer parameter lambda. ** @param self object. ** @param lambda regularizer parameter. ** ** Note that @a lambda is usually set when calling a ** constructor for ::VlSvm as certain parameters, such ** as the maximum number of iterations, are tuned accordingly. ** This tuning is not performed when @a lambda is changed ** using this function. **/ void vl_svm_set_lambda (VlSvm * self, double lambda) { assert(self) ; assert(lambda >= 0) ; self->lambda = lambda ; } /** @brief Get the regularizer parameter lambda. ** @param self object. ** @return diagnostic frequency. **/ double vl_svm_get_lambda (VlSvm const * self) { assert(self) ; return self->lambda ; } /** @brief Set the data weights. ** @param self object. ** @param weights data weights. ** ** @a weights must be an array of non-negative weights. ** The loss of each data point is multiplied by the corresponding ** weight. ** ** Set @a weights to @c NULL to weight the data uniformly by 1 (default). ** ** Note that the @a weights array is *not* copied and must be valid ** througout the object lifetime (unless it is replaced). **/ void vl_svm_set_weights (VlSvm * self, double const *weights) { assert(self) ; self->weights = weights ; } /** @brief Get the data weights. ** @param self object. ** @return data weights. **/ double const *vl_svm_get_weights (VlSvm const * self) { assert(self) ; return self->weights ; } /* ---------------------------------------------------------------- */ /* Get data */ /* ---------------------------------------------------------------- */ /** @brief Get the model dimenison. ** @param self object. ** @return model dimension. ** ** This is the dimensionality of the weight vector $\bw$. **/ vl_size vl_svm_get_dimension (VlSvm *self) { assert(self) ; return self->dimension ; } /** @brief Get the number of data samples. ** @param self object. ** @return model number of data samples ** ** This is the dimensionality of the weight vector $\bw$. **/ vl_size vl_svm_get_num_data (VlSvm *self) { assert(self) ; return self->numData ; } /** @brief Get the SVM model. ** @param self object. ** @return model. ** ** This is the weight vector $\bw$. **/ double const * vl_svm_get_model (VlSvm const *self) { assert(self) ; return self->model ; } /** @brief Set the SVM model. ** @param self object. ** @param model model. ** ** The function *copies* the content of the vector @a model to the ** internal model buffer. This operation can be used for warm start ** with the SGD algorithm, but has undefined effect with the SDCA algorithm. **/ void vl_svm_set_model (VlSvm *self, double const *model) { assert(self) ; assert(model) ; memcpy(self->model, model, sizeof(double) * vl_svm_get_dimension(self)) ; } /** @brief Set the SVM bias. ** @param self object. ** @param b bias. ** ** The function set the internal representation of the SVM bias to ** be equal to @a b (the bias multiplier ** is applied). The same remark ** that applies to ::vl_svm_set_model applies here too. **/ void vl_svm_set_bias (VlSvm *self, double b) { assert(self); if (self->biasMultiplier) { self->bias = b / self->biasMultiplier ; } } /** @brief Get the value of the bias. ** @param self object. ** @return bias $b$. ** ** The value of the bias returned already include the effect of ** bias mutliplier. **/ double vl_svm_get_bias (VlSvm const *self) { assert(self) ; return self->bias * self->biasMultiplier ; } /** @brief Get the solver statistics. ** @param self object. ** @return statistics. **/ VlSvmStatistics const * vl_svm_get_statistics (VlSvm const *self) { assert(self) ; return &self->statistics ; } /** @brief Get the scores of the data points. ** @param self object. ** @return vector of scores. ** ** After training or during the diagnostic callback, ** this function can be used to retrieve the scores ** of the points, i.e. $\langle \bx_i, \bw \rangle + b$. **/ double const * vl_svm_get_scores (VlSvm const *self) { return self->scores ; } /* ---------------------------------------------------------------- */ /* Callbacks */ /* ---------------------------------------------------------------- */ /** @typedef VlSvmDiagnosticFunction ** @brief SVM diagnostic function pointer. ** @param svm is an instance of ::VlSvm . **/ /** @typedef VlSvmAccumulateFunction ** @brief Pointer to a function that adds to @a model the data point at ** position @a element multiplied by the constant @a multiplier. **/ /** @typedef VlSvmInnerProductFunction ** @brief Pointer to a function that defines the inner product ** between the data point at position @a element and the SVM model **/ /** @brief Set the diagnostic function callback ** @param self object. ** @param f diagnostic function pointer. ** @param data pointer to data used by the diagnostic function. **/ void vl_svm_set_diagnostic_function (VlSvm *self, VlSvmDiagnosticFunction f, void *data) { self->diagnosticFn = f ; self->diagnosticFnData = data ; } /** @brief Set the data functions. ** @param self object. ** @param inner inner product function. ** @param acc accumulate function. ** ** See @ref svm-data-abstraction. **/ void vl_svm_set_data_functions (VlSvm *self, VlSvmInnerProductFunction inner, VlSvmAccumulateFunction acc) { assert(self) ; assert(inner) ; assert(acc) ; self->innerProductFn = inner ; self->accumulateFn = acc ; } /** @brief Set the loss function callback. ** @param self object. ** @param f loss function callback. ** ** Note that setting up a loss requires specifying more than just one ** callback. See @ref svm-loss-functions for details. **/ void vl_svm_set_loss_function (VlSvm *self, VlSvmLossFunction f) { assert(self) ; self->lossFn = f ; } /** @brief Set the loss derivative function callback. ** @copydetails vl_svm_set_loss_function. **/ void vl_svm_set_loss_derivative_function (VlSvm *self, VlSvmLossFunction f) { assert(self) ; self->lossDerivativeFn = f ; } /** @brief Set the conjugate loss function callback. ** @copydetails vl_svm_set_loss_function. **/ void vl_svm_set_conjugate_loss_function (VlSvm *self, VlSvmLossFunction f) { assert(self) ; self->conjugateLossFn = f ; } /** @brief Set the DCA update function callback. ** @copydetails vl_svm_set_loss_function. **/ void vl_svm_set_dca_update_function (VlSvm *self, VlSvmDcaUpdateFunction f) { assert(self) ; self->dcaUpdateFn = f ; } /** @brief Set the loss function to one of the default types. ** @param self object. ** @param loss type of loss function. ** @sa @ref svm-loss-functions. **/ void vl_svm_set_loss (VlSvm *self, VlSvmLossType loss) { #define SETLOSS(x,y) \ case VlSvmLoss ## x: \ vl_svm_set_loss_function(self, vl_svm_ ## y ## _loss) ; \ vl_svm_set_loss_derivative_function(self, vl_svm_ ## y ## _loss_derivative) ; \ vl_svm_set_conjugate_loss_function(self, vl_svm_ ## y ## _conjugate_loss) ; \ vl_svm_set_dca_update_function(self, vl_svm_ ## y ## _dca_update) ; \ break; switch (loss) { SETLOSS(Hinge, hinge) ; SETLOSS(Hinge2, hinge2) ; SETLOSS(L1, l1) ; SETLOSS(L2, l2) ; SETLOSS(Logistic, logistic) ; default: assert(0) ; } } /* ---------------------------------------------------------------- */ /* Pre-defined losses */ /* ---------------------------------------------------------------- */ /** @typedef VlSvmLossFunction ** @brief SVM loss function pointer. ** @param inner inner product between sample and model $\bw^\top \bx$. ** @param label sample label $y$. ** @return value of the loss. ** ** The interface is the same for a loss function, its derivative, ** or the conjugate loss. ** ** @sa @ref svm-fundamentals **/ /** @typedef VlSvmDcaUpdateFunction ** @brief SVM SDCA update function pointer. ** @param alpha current value of the dual variable. ** @param inner inner product $\bw^\top \bx$ of the sample with the SVM model. ** @param norm2 normalization factor $\|\bx\|^2/\lambda n$. ** @param label label $y$ of the sample. ** @return incremental update $\Delta\alpha$ of the dual variable. ** ** @sa @ref svm-sdca **/ /** @brief SVM hinge loss ** @copydetails VlSvmLossFunction */ double vl_svm_hinge_loss (double inner, double label) { return VL_MAX(1 - label * inner, 0.0); } /** @brief SVM hinge loss derivative ** @copydetails VlSvmLossFunction */ double vl_svm_hinge_loss_derivative (double inner, double label) { if (label * inner < 1.0) { return - label ; } else { return 0.0 ; } } /** @brief SVM hinge loss conjugate ** @param u dual variable. ** @param label label value. ** @return conjugate loss. **/ double vl_svm_hinge_conjugate_loss (double u, double label) { double z = label * u ; if (-1 <= z && z <= 0) { return label * u ; } else { return VL_INFINITY_D ; } } /** @brief SVM hinge loss DCA update ** @copydetails VlSvmDcaUpdateFunction */ double vl_svm_hinge_dca_update (double alpha, double inner, double norm2, double label) { double palpha = (label - inner) / norm2 + alpha ; return label * VL_MAX(0, VL_MIN(1, label * palpha)) - alpha ; } /** @brief SVM square hinge loss ** @copydetails VlSvmLossFunction */ double vl_svm_hinge2_loss (double inner,double label) { double z = VL_MAX(1 - label * inner, 0.0) ; return z*z ; } /** @brief SVM square hinge loss derivative ** @copydetails VlSvmLossFunction */ double vl_svm_hinge2_loss_derivative (double inner, double label) { if (label * inner < 1.0) { return 2 * (inner - label) ; } else { return 0 ; } } /** @brief SVM square hinge loss conjugate ** @copydetails vl_svm_hinge_conjugate_loss */ double vl_svm_hinge2_conjugate_loss (double u, double label) { if (label * u <= 0) { return (label + u/4) * u ; } else { return VL_INFINITY_D ; } } /** @brief SVM square hinge loss DCA update ** @copydetails VlSvmDcaUpdateFunction */ double vl_svm_hinge2_dca_update (double alpha, double inner, double norm2, double label) { double palpha = (label - inner - 0.5*alpha) / (norm2 + 0.5) + alpha ; return label * VL_MAX(0, label * palpha) - alpha ; } /** @brief SVM l1 loss ** @copydetails VlSvmLossFunction */ double vl_svm_l1_loss (double inner,double label) { return vl_abs_d(label - inner) ; } /** @brief SVM l1 loss derivative ** @copydetails VlSvmLossFunction */ double vl_svm_l1_loss_derivative (double inner, double label) { if (label > inner) { return - 1.0 ; } else { return + 1.0 ; } } /** @brief SVM l1 loss conjugate ** @copydetails vl_svm_hinge_conjugate_loss */ double vl_svm_l1_conjugate_loss (double u, double label) { if (vl_abs_d(u) <= 1) { return label*u ; } else { return VL_INFINITY_D ; } } /** @brief SVM l1 loss DCA update ** @copydetails VlSvmDcaUpdateFunction */ double vl_svm_l1_dca_update (double alpha, double inner, double norm2, double label) { if (vl_abs_d(alpha) <= 1) { double palpha = (label - inner) / norm2 + alpha ; return VL_MAX(-1.0, VL_MIN(1.0, palpha)) - alpha ; } else { return VL_INFINITY_D ; } } /** @brief SVM l2 loss ** @copydetails VlSvmLossFunction */ double vl_svm_l2_loss (double inner,double label) { double z = label - inner ; return z*z ; } /** @brief SVM l2 loss derivative ** @copydetails VlSvmLossFunction */ double vl_svm_l2_loss_derivative (double inner, double label) { return - 2 * (label - inner) ; } /** @brief SVM l2 loss conjugate ** @copydetails vl_svm_hinge_conjugate_loss */ double vl_svm_l2_conjugate_loss (double u, double label) { return (label + u/4) * u ; } /** @brief SVM l2 loss DCA update ** @copydetails VlSvmDcaUpdateFunction */ double vl_svm_l2_dca_update (double alpha, double inner, double norm2, double label) { return (label - inner - 0.5*alpha) / (norm2 + 0.5) ; } /** @brief SVM l2 loss ** @copydetails VlSvmLossFunction */ double vl_svm_logistic_loss (double inner,double label) { double z = label * inner ; if (z >= 0) { return log(1.0 + exp(-z)) ; } else { return -z + log(exp(z) + 1.0) ; } } /** @brief SVM l2 loss derivative ** @copydetails VlSvmLossFunction */ double vl_svm_logistic_loss_derivative (double inner, double label) { double z = label * inner ; double t = 1 / (1 + exp(-z)) ; /* this is stable for z << 0 too */ return label * (t - 1) ; /* = -label exp(-z) / (1 + exp(-z)) */ } VL_INLINE double xlogx(double x) { if (x <= 1e-10) return 0 ; return x*log(x) ; } /** @brief SVM l2 loss conjugate ** @copydetails vl_svm_hinge_conjugate_loss */ double vl_svm_logistic_conjugate_loss (double u, double label) { double z = label * u ; if (-1 <= z && z <= 0) { return xlogx(-z) + xlogx(1+z) ; } else { return VL_INFINITY_D ; } } /** @brief SVM l2 loss DCA update ** @copydetails VlSvmDcaUpdateFunction */ double vl_svm_logistic_dca_update (double alpha, double inner, double norm2, double label) { /* The goal is to solve the problem min_delta A/2 delta^2 + B delta + l*(-alpha - delta|y), -1 <= - y (alpha+delta) <= 0 where A = norm2, B = inner, and y = label. To simplify the notation, we set f(beta) = beta * log(beta) + (1 - beta) * log(1 - beta) where beta = y(alpha + delta) such that l*(-alpha - delta |y) = f(beta). Hence 0 <= beta <= 1, delta = + y beta - alpha. Substituting min_beta A/2 beta^2 + y (B - A alpha) beta + f(beta) + const The Newton step is then given by beta = beta - (A beta + y(B - A alpha) + df) / (A + ddf). However, the function is singluar for beta=0 and beta=1 (infinite first and second order derivatives). Since the function is monotonic (second derivarive always strictly greater than zero) and smooth, we canuse bisection to find the zero crossing of the first derivative. Once one is sufficiently close to the optimum, a one or two Newton steps are sufficien to land on it with excellent accuracy. */ double df, ddf, der, dder ; vl_index t ; /* bisection */ double beta1 = 0 ; double beta2 = 1 ; double beta = 0.5 ; for (t = 0 ; t < 5 ; ++t) { df = log(beta) - log(1-beta) ; der = norm2 * beta + label * (inner - norm2*alpha) + df ; if (der >= 0) { beta2 = beta ; } else { beta1 = beta ; } beta = 0.5 * (beta1 + beta2) ; } #if 1 /* a final Newton step, but not too close to the singularities */ for (t = 0 ; (t < 2) & (beta > VL_EPSILON_D) & (beta < 1-VL_EPSILON_D) ; ++t) { df = log(beta) - log(1-beta) ; ddf = 1 / (beta * (1-beta)) ; der = norm2 * beta + label * (inner - norm2*alpha) + df ; dder = norm2 + ddf ; beta -= der / dder ; beta = VL_MAX(0, VL_MIN(1, beta)) ; } #endif return label * beta - alpha ; } /* ---------------------------------------------------------------- */ /** @internal @brief Update SVM statistics ** @param self object. **/ void _vl_svm_update_statistics (VlSvm *self) { vl_size i, k ; double inner, p ; memset(&self->statistics, 0, sizeof(VlSvmStatistics)) ; self->statistics.regularizer = self->bias * self->bias ; for (i = 0; i < self->dimension; i++) { self->statistics.regularizer += self->model[i] * self->model[i] ; } self->statistics.regularizer *= self->lambda * 0.5 ; for (k = 0; k < self->numData ; k++) { p = (self->weights) ? self->weights[k] : 1.0 ; if (p <= 0) continue ; inner = self->innerProductFn(self->data, k, self->model) ; inner += self->bias * self->biasMultiplier ; self->scores[k] = inner ; self->statistics.loss += p * self->lossFn(inner, self->labels[k]) ; if (self->solver == VlSvmSolverSdca) { self->statistics.dualLoss -= p * self->conjugateLossFn(- self->alpha[k] / p, self->labels[k]) ; } } self->statistics.loss /= self->numData ; self->statistics.objective = self->statistics.regularizer + self->statistics.loss ; if (self->solver == VlSvmSolverSdca) { self->statistics.dualLoss /= self->numData ; self->statistics.dualObjective = - self->statistics.regularizer + self->statistics.dualLoss ; self->statistics.dualityGap = self->statistics.objective - self->statistics.dualObjective ; } } /* ---------------------------------------------------------------- */ /* Evaluate rather than solve */ /* ---------------------------------------------------------------- */ void _vl_svm_evaluate (VlSvm *self) { double startTime = vl_get_cpu_time () ; _vl_svm_update_statistics (self) ; self->statistics.elapsedTime = vl_get_cpu_time() - startTime ; self->statistics.iteration = 0 ; self->statistics.epoch = 0 ; self->statistics.status = VlSvmStatusConverged ; if (self->diagnosticFn) { self->diagnosticFn(self, self->diagnosticFnData) ; } } /* ---------------------------------------------------------------- */ /* Stochastic Dual Coordinate Ascent Solver */ /* ---------------------------------------------------------------- */ void _vl_svm_sdca_train (VlSvm *self) { double * norm2 ; vl_index * permutation ; vl_uindex i, t ; double inner, delta, multiplier, p ; double startTime = vl_get_cpu_time () ; VlRand * rand = vl_get_rand() ; norm2 = (double*) vl_calloc(self->numData, sizeof(double)); permutation = vl_calloc(self->numData, sizeof(vl_index)) ; { double * buffer = vl_calloc(self->dimension, sizeof(double)) ; for (i = 0 ; i < (unsigned)self->numData; i++) { double n2 ; permutation [i] = i ; memset(buffer, 0, self->dimension * sizeof(double)) ; self->accumulateFn (self->data, i, buffer, 1) ; n2 = self->innerProductFn (self->data, i, buffer) ; n2 += self->biasMultiplier * self->biasMultiplier ; norm2[i] = n2 / (self->lambda * self->numData) ; } vl_free(buffer) ; } for (t = 0 ; 1 ; ++t) { if (t % self->numData == 0) { /* once a new epoch is reached (all data have been visited), change permutation */ vl_rand_permute_indexes(rand, permutation, self->numData) ; } /* pick a sample and compute update */ i = permutation[t % self->numData] ; p = (self->weights) ? self->weights[i] : 1.0 ; if (p > 0) { inner = self->innerProductFn(self->data, i, self->model) ; inner += self->bias * self->biasMultiplier ; delta = p * self->dcaUpdateFn(self->alpha[i] / p, inner, p * norm2[i], self->labels[i]) ; } else { delta = 0 ; } /* apply update */ if (delta != 0) { self->alpha[i] += delta ; multiplier = delta / (self->numData * self->lambda) ; self->accumulateFn(self->data,i,self->model,multiplier) ; self->bias += self->biasMultiplier * multiplier; } /* call diagnostic occasionally */ if ((t + 1) % self->diagnosticFrequency == 0 || t + 1 == self->maxNumIterations) { _vl_svm_update_statistics (self) ; self->statistics.elapsedTime = vl_get_cpu_time() - startTime ; self->statistics.iteration = t ; self->statistics.epoch = t / self->numData ; self->statistics.status = VlSvmStatusTraining ; if (self->statistics.dualityGap < self->epsilon) { self->statistics.status = VlSvmStatusConverged ; } else if (t + 1 == self->maxNumIterations) { self->statistics.status = VlSvmStatusMaxNumIterationsReached ; } if (self->diagnosticFn) { self->diagnosticFn(self, self->diagnosticFnData) ; } if (self->statistics.status != VlSvmStatusTraining) { break ; } } } /* next iteration */ vl_free (norm2) ; vl_free (permutation) ; } /* ---------------------------------------------------------------- */ /* Stochastic Gradient Descent Solver */ /* ---------------------------------------------------------------- */ void _vl_svm_sgd_train (VlSvm *self) { vl_index * permutation ; double * scores ; double * previousScores ; vl_uindex i, t, k ; double inner, gradient, rate, biasRate, p ; double factor = 1.0 ; double biasFactor = 1.0 ; /* to allow slower bias learning rate */ vl_index t0 = VL_MAX(2, vl_ceil_d(1.0 / self->lambda)) ; //t0=2 ; double startTime = vl_get_cpu_time () ; VlRand * rand = vl_get_rand() ; permutation = vl_calloc(self->numData, sizeof(vl_index)) ; scores = vl_calloc(self->numData * 2, sizeof(double)) ; previousScores = scores + self->numData ; for (i = 0 ; i < (unsigned)self->numData; i++) { permutation [i] = i ; previousScores [i] = - VL_INFINITY_D ; } /* We store the w vector as the product fw (factor * model). We also use a different factor for the bias: biasFactor * biasMultiplier to enable a slower learning rate for the bias. Given this representation, it is easy to carry the two key operations: * Inner product: = f * Model update: fp wp = fw - rate * lambda * w - rate * g = f(1 - rate * lambda) w - rate * g Thus the update equations are: fp = f(1 - rate * lambda), and wp = w + rate / fp * g ; * Realization of the scaling factor. Before the statistics function is called, or training finishes, the factor (and biasFactor) are explicitly applied to the model and the bias. */ for (t = 0 ; 1 ; ++t) { if (t % self->numData == 0) { /* once a new epoch is reached (all data have been visited), change permutation */ vl_rand_permute_indexes(rand, permutation, self->numData) ; } /* pick a sample and compute update */ i = permutation[t % self->numData] ; p = (self->weights) ? self->weights[i] : 1.0 ; p = VL_MAX(0.0, p) ; /* we assume non-negative weights, so this is just for robustness */ inner = factor * self->innerProductFn(self->data, i, self->model) ; inner += biasFactor * (self->biasMultiplier * self->bias) ; gradient = p * self->lossDerivativeFn(inner, self->labels[i]) ; previousScores[i] = scores[i] ; scores[i] = inner ; /* apply update */ rate = 1.0 / (self->lambda * (t + t0)) ; biasRate = rate * self->biasLearningRate ; factor *= (1.0 - self->lambda * rate) ; biasFactor *= (1.0 - self->lambda * biasRate) ; /* debug: realize the scaling factor all the times */ /* for (k = 0 ; k < self->dimension ; ++k) self->model[k] *= factor ; self->bias *= biasFactor; factor = 1.0 ; biasFactor = 1.0 ; */ if (gradient != 0) { self->accumulateFn(self->data, i, self->model, - gradient * rate / factor) ; self->bias += self->biasMultiplier * (- gradient * biasRate / biasFactor) ; } /* call diagnostic occasionally */ if ((t + 1) % self->diagnosticFrequency == 0 || t + 1 == self->maxNumIterations) { /* realize factor before computing statistics or completing training */ for (k = 0 ; k < self->dimension ; ++k) self->model[k] *= factor ; self->bias *= biasFactor; factor = 1.0 ; biasFactor = 1.0 ; _vl_svm_update_statistics (self) ; for (k = 0 ; k < self->numData ; ++k) { double delta = scores[k] - previousScores[k] ; self->statistics.scoresVariation += delta * delta ; } self->statistics.scoresVariation = sqrt(self->statistics.scoresVariation) / self->numData ; self->statistics.elapsedTime = vl_get_cpu_time() - startTime ; self->statistics.iteration = t ; self->statistics.epoch = t / self->numData ; self->statistics.status = VlSvmStatusTraining ; if (self->statistics.scoresVariation < self->epsilon) { self->statistics.status = VlSvmStatusConverged ; } else if (t + 1 == self->maxNumIterations) { self->statistics.status = VlSvmStatusMaxNumIterationsReached ; } if (self->diagnosticFn) { self->diagnosticFn(self, self->diagnosticFnData) ; } if (self->statistics.status != VlSvmStatusTraining) { break ; } } } /* next iteration */ vl_free (scores) ; vl_free (permutation) ; } /* ---------------------------------------------------------------- */ /* Dispatcher */ /* ---------------------------------------------------------------- */ /** @brief Run the SVM solver ** @param self object. ** ** The data on which the SVM operates is passed upon the cration of ** the ::VlSvm object. This function runs a solver to learn a ** corresponding model. See @ref svm-starting. **/ void vl_svm_train (VlSvm * self) { assert (self) ; switch (self->solver) { case VlSvmSolverSdca: _vl_svm_sdca_train(self) ; break ; case VlSvmSolverSgd: _vl_svm_sgd_train(self) ; break ; case VlSvmSolverNone: _vl_svm_evaluate(self) ; break ; default: assert(0) ; } } colmap-3.9.1/src/thirdparty/VLFeat/svm.h000066400000000000000000000175111454702036400200710ustar00rootroot00000000000000/** @file svm.h ** @brief Support Vector Machines (@ref svm) ** @author Milan Sulc ** @author Daniele Perrone ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 Milan Sulc. Copyright (C) 2012 Daniele Perrone. Copyright (C) 2011-13 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_SVM_H #define VL_SVM_H #include "generic.h" #include "svmdataset.h" /** @typedef VlSvm ** @brief SVM solver. ** This object implements VLFeat SVM solvers (see @ref svm.h). **/ #ifndef __DOXYGEN__ struct VlSvm_ ; typedef struct VlSvm_ VlSvm ; #else typedef OPAQUE VlSvm ; #endif /** @brief Type of SVM solver */ typedef enum { VlSvmSolverNone = 0, /**< No solver (used to evaluate an SVM). */ VlSvmSolverSgd = 1, /**< SGD algorithm (@ref svm-sgd). */ VlSvmSolverSdca /**< SDCA algorithm (@ref svm-sdca). */ } VlSvmSolverType ; /** @brief Type of SVM loss ** ** Default SVM loss types. The loss can be set by using ::vl_svm_set_loss. ** Note that custom losses can be used too by using ::vl_svm_set_loss_function, ** ::vl_svm_set_loss_derivative_function, etc. ** ** @sa svm-loss-functions **/ typedef enum { VlSvmLossHinge = 0, /**< Standard hinge loss. */ VlSvmLossHinge2 = 1, /**< Hinge loss squared. */ VlSvmLossL1, /**< L1 loss. */ VlSvmLossL2, /**< L2 loss. */ VlSvmLossLogistic /**< Logistic loss. */ } VlSvmLossType ; /** @brief Solver status */ typedef enum { VlSvmStatusTraining = 1, /**< Optimization in progress. */ VlSvmStatusConverged, /**< Optimization finished because the convergence criterion was met. */ VlSvmStatusMaxNumIterationsReached /**< Optimization finished without convergence. */ } VlSvmSolverStatus ; /** @brief SVM statistics ** This structure contains statistics characterising the state of ** the SVM solver, such as the current value of the objective function. ** ** Not all fields are used by all solvers. **/ typedef struct VlSvmStatistics_ { VlSvmSolverStatus status ; /**< Solver status. */ vl_size iteration ; /**< Solver iteration. */ vl_size epoch ; /**< Solver epoch (iteration / num samples). */ double objective ; /**< Objective function value. */ double regularizer ; /**< Regularizer value. */ double loss ; /**< Loss value. */ double dualObjective ; /**< Dual objective value. */ double dualLoss ; /**< Dual loss value. */ double dualityGap ; /**< Duality gap = objective - dualObjective. */ double scoresVariation ; /**< Variance of the score updates. */ double elapsedTime ; /**< Time elapsed from the start of training. */ } VlSvmStatistics ; /** @name Create and destroy ** @{ */ VL_EXPORT VlSvm * vl_svm_new (VlSvmSolverType type, double const * data, vl_size dimension, vl_size numData, double const * labels, double lambda) ; VL_EXPORT VlSvm * vl_svm_new_with_dataset (VlSvmSolverType type, VlSvmDataset * dataset, double const * labels, double lambda) ; VL_EXPORT VlSvm * vl_svm_new_with_abstract_data (VlSvmSolverType type, void * data, vl_size dimension, vl_size numData, double const * labels, double lambda) ; VL_EXPORT void vl_svm_delete (VlSvm * self) ; /** @} */ /** @name Retrieve parameters and data ** @{ */ VL_EXPORT VlSvmStatistics const * vl_svm_get_statistics (VlSvm const *self) ; VL_EXPORT double const * vl_svm_get_model (VlSvm const *self) ; VL_EXPORT double vl_svm_get_bias (VlSvm const *self) ; VL_EXPORT vl_size vl_svm_get_dimension (VlSvm *self) ; VL_EXPORT vl_size vl_svm_get_num_data (VlSvm *self) ; VL_EXPORT double vl_svm_get_epsilon (VlSvm const *self) ; VL_EXPORT double vl_svm_get_bias_learning_rate (VlSvm const *self) ; VL_EXPORT vl_size vl_svm_get_max_num_iterations (VlSvm const *self) ; VL_EXPORT vl_size vl_svm_get_diagnostic_frequency (VlSvm const *self) ; VL_EXPORT VlSvmSolverType vl_svm_get_solver (VlSvm const *self) ; VL_EXPORT double vl_svm_get_bias_multiplier (VlSvm const *self) ; VL_EXPORT double vl_svm_get_lambda (VlSvm const *self) ; VL_EXPORT vl_size vl_svm_get_iteration_number (VlSvm const *self) ; VL_EXPORT double const * vl_svm_get_scores (VlSvm const *self) ; VL_EXPORT double const * vl_svm_get_weights (VlSvm const *self) ; /** @} */ /** @name Set parameters ** @{ */ VL_EXPORT void vl_svm_set_epsilon (VlSvm *self, double epsilon) ; VL_EXPORT void vl_svm_set_bias_learning_rate (VlSvm *self, double rate) ; VL_EXPORT void vl_svm_set_max_num_iterations (VlSvm *self, vl_size maxNumIterations) ; VL_EXPORT void vl_svm_set_diagnostic_frequency (VlSvm *self, vl_size f) ; VL_EXPORT void vl_svm_set_bias_multiplier (VlSvm *self, double b) ; VL_EXPORT void vl_svm_set_model (VlSvm *self, double const *model) ; VL_EXPORT void vl_svm_set_bias (VlSvm *self, double b) ; VL_EXPORT void vl_svm_set_iteration_number (VlSvm *self, vl_uindex n) ; VL_EXPORT void vl_svm_set_weights (VlSvm *self, double const *weights) ; VL_EXPORT void vl_svm_set_diagnostic_function (VlSvm *self, VlSvmDiagnosticFunction f, void *data) ; VL_EXPORT void vl_svm_set_loss_function (VlSvm *self, VlSvmLossFunction f) ; VL_EXPORT void vl_svm_set_loss_derivative_function (VlSvm *self, VlSvmLossFunction f) ; VL_EXPORT void vl_svm_set_conjugate_loss_function (VlSvm *self, VlSvmLossFunction f) ; VL_EXPORT void vl_svm_set_dca_update_function (VlSvm *self, VlSvmDcaUpdateFunction f) ; VL_EXPORT void vl_svm_set_data_functions (VlSvm *self, VlSvmInnerProductFunction inner, VlSvmAccumulateFunction acc) ; VL_EXPORT void vl_svm_set_loss (VlSvm *self, VlSvmLossType loss) ; /** @} */ /** @name Process data ** @{ */ VL_EXPORT void vl_svm_train (VlSvm * self) ; /** @} */ /** @name Loss functions ** @sa @ref svm-advanced ** @{ */ /* hinge */ VL_EXPORT double vl_svm_hinge_loss (double label, double inner) ; VL_EXPORT double vl_svm_hinge_loss_derivative (double label, double inner) ; VL_EXPORT double vl_svm_hinge_conjugate_loss (double label, double u) ; VL_EXPORT double vl_svm_hinge_dca_update (double alpha, double inner, double norm2, double label) ; /* square hinge */ VL_EXPORT double vl_svm_hinge2_loss (double label, double inner) ; VL_EXPORT double vl_svm_hinge2_loss_derivative (double label, double inner) ; VL_EXPORT double vl_svm_hinge2_conjugate_loss (double label, double u) ; VL_EXPORT double vl_svm_hinge2_dca_update (double alpha, double inner, double norm2, double label) ; /* l1 */ VL_EXPORT double vl_svm_l1_loss (double label, double inner) ; VL_EXPORT double vl_svm_l1_loss_derivative (double label, double inner) ; VL_EXPORT double vl_svm_l1_conjugate_loss (double label, double u) ; VL_EXPORT double vl_svm_l1_dca_update (double alpha, double inner, double norm2, double label) ; /* l2 */ VL_EXPORT double vl_svm_l2_loss (double label, double inner) ; VL_EXPORT double vl_svm_l2_loss_derivative (double label, double inner) ; VL_EXPORT double vl_svm_l2_conjugate_loss (double label, double u) ; VL_EXPORT double vl_svm_l2_dca_update (double alpha, double inner, double norm2, double label) ; /* logistic */ VL_EXPORT double vl_svm_logistic_loss (double label, double inner) ; VL_EXPORT double vl_svm_logistic_loss_derivative (double label, double inner) ; VL_EXPORT double vl_svm_logistic_conjugate_loss (double label, double u) ; VL_EXPORT double vl_svm_logistic_dca_update (double alpha, double inner, double norm2, double label) ; /** } */ /* VL_SVM_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/svmdataset.c000077500000000000000000000275531454702036400214440ustar00rootroot00000000000000/** @file svmdataset.c ** @brief SVM Dataset - Definition ** @author Daniele Perrone ** @author Andrea Vedaldi **/ /* Copyright (C) 2012 Daniele Perrone. Copyright (C) 2013 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @file svmdataset.h @tableofcontents @author Daniele Perrone @author Andrea Vedaldi The SVM solver object ::VlSvm, supporting SVM learning in VLFeat, uses an abstraction mechanism to work on arbitrary data types. This module provides an helper object, ::VlSvmDataset, that simplify taking advantage of this functionality, supporting for example different data types and the computation of feature maps out of the box. @section svmdataset-starting Getting started As discussed in @ref svm-advanced, most linear SVM solvers, such as the ones implemented in VLFeat in @ref svm, require only two operations to be defined on the data: - *Inner product* between a data point $\bx$ and the model vector $\bw$. This is implemented by a function of type ::VlSvmInnerProductFunction. - *Accumulation* of a dataobint $\bx$ to the model vector $\bw$: $\bw \leftarrow \bw + \alpha \bx$. This is implemented by a function of the type ::VlSvmAccumulateFunction . The SVM solver needs to know nothing about the data once these two operations are defined. These functions can do any number of things, such as supporting different formats for the data (dense or sparse, float or double), computing feature maps, or expanding compressed representations such as Product Quantization. VLFeat provides the helper object ::VlSvmDataset to support some of these functionalities out of the box (it is important to remark that its use with the SVM solver ::VlSvm is entirely optional). Presently, ::VlSvmDataset supports: - @c float and @c double dense arrays. - The on-the-fly application of the homogeneous kernel map to implement additive non-linear kernels (see @ref homkermap). For example, to learn a linear SVM on SINGLE data: @code int main() { vl_size const numData = 4 ; vl_size const dimension = 2 ; single x [dimension * numData] = { 0.0, -0.5, 0.6, -0.3, 0.0, 0.5, 0.6, 0.0} ; double y [numData] = {1, 1, -1, 1} ; double lambda = 0.01; double * const model ; double bias ; VlSvmDataset * dataset = vl_svmdataset_new (VL_TYPE_SINGLE, x, dimension, numData) ; VlSvm * svm = vl_svm_new_with_dataset (VlSvmSolverSgd, dataset, y, lambda) ; vl_svm_train(svm) ; model = vl_svm_get_model(svm) ; bias = vl_svm_get_bias(svm) ; printf("model w = [ %f , %f ] , bias b = %f \n", model[0], model[1], bias); vl_svm_delete(svm) ; vl_svmdataset_delete(dataset) ; return 0; } @endcode **/ /* ---------------------------------------------------------------- */ #ifndef VL_SVMDATASET_INSTANTIATING /* ---------------------------------------------------------------- */ #include "svmdataset.h" #include #include struct VlSvmDataset_ { vl_type dataType ; /**< Data type. */ void * data ; /**< Pointer to data. */ vl_size numData ; /**< Number of wrapped data. */ vl_size dimension ; /**< Data point dimension. */ VlHomogeneousKernelMap * hom ; /**< Homogeneous kernel map (optional). */ void * homBuffer ; /**< Homogeneous kernel map buffer. */ vl_size homDimension ; /**< Homogeneous kernel map dimension. */ } ; /* templetized parts of the implementation */ #define FLT VL_TYPE_FLOAT #define VL_SVMDATASET_INSTANTIATING #include "svmdataset.c" #define FLT VL_TYPE_DOUBLE #define VL_SVMDATASET_INSTANTIATING #include "svmdataset.c" /** @brief Create a new object wrapping a dataset. ** @param dataType of data (@c float and @c double supported). ** @param data pointer to the data. ** @param dimension the dimension of a data vector. ** @param numData number of wrapped data vectors. ** @return new object. ** ** The function allocates and returns a new SVM dataset object ** wrapping the data pointed by @a data. Note that no copy is made ** of data, so the caller should keep the data allocated as the object exists. ** ** @sa ::vl_svmdataset_delete **/ VlSvmDataset* vl_svmdataset_new (vl_type dataType, void *data, vl_size dimension, vl_size numData) { VlSvmDataset * self ; assert(dataType == VL_TYPE_DOUBLE || dataType == VL_TYPE_FLOAT) ; assert(data) ; self = vl_calloc(1, sizeof(VlSvmDataset)) ; if (self == NULL) return NULL ; self->dataType = dataType ; self->data = data ; self->dimension = dimension ; self->numData = numData ; self->hom = NULL ; self->homBuffer = NULL ; return self ; } /** @brief Delete the object. ** @param self object to delete. ** ** The function frees the resources allocated by ** ::vl_svmdataset_new(). Notice that the wrapped data will *not* ** be freed as it is not owned by the object. **/ void vl_svmdataset_delete (VlSvmDataset *self) { if (self->homBuffer) { vl_free(self->homBuffer) ; self->homBuffer = 0 ; } vl_free (self) ; } /** @brief Get the wrapped data. ** @param self object. ** @return a pointer to the wrapped data. **/ void* vl_svmdataset_get_data (VlSvmDataset const *self) { return self->data ; } /** @brief Get the number of wrapped data elements. ** @param self object. ** @return number of wrapped data elements. **/ vl_size vl_svmdataset_get_num_data (VlSvmDataset const *self) { return self->numData ; } /** @brief Get the dimension of the wrapped data. ** @param self object. ** @return dimension of the wrapped data. **/ vl_size vl_svmdataset_get_dimension (VlSvmDataset const *self) { if (self->hom) { return self->dimension * vl_homogeneouskernelmap_get_dimension(self->hom) ; } return self->dimension ; } /** @brief Get the homogeneous kernel map object. ** @param self object. ** @return homogenoeus kernel map object (or @c NULL if any). **/ VlHomogeneousKernelMap * vl_svmdataset_get_homogeneous_kernel_map (VlSvmDataset const *self) { assert(self) ; return self->hom ; } /** @brief Set the homogeneous kernel map object. ** @param self object. ** @param hom homogeneous kernel map object to use. ** ** After changing the kernel map, the inner product and accumulator ** function should be queried again (::vl_svmdataset_get_inner_product_function ** adn ::vl_svmdataset_get_accumulate_function). ** ** Set this to @c NULL to avoid using a kernel map. ** ** Note that this does *not* transfer the ownership of the object ** to the function. Furthermore, ::VlSvmDataset holds to the ** object until it is destroyed or the object is replaced or removed ** by calling this function again. **/ void vl_svmdataset_set_homogeneous_kernel_map (VlSvmDataset * self, VlHomogeneousKernelMap * hom) { assert(self) ; self->hom = hom ; self->homDimension = 0 ; if (self->homBuffer) { vl_free (self->homBuffer) ; self->homBuffer = 0 ; } if (self->hom) { self->homDimension = vl_homogeneouskernelmap_get_dimension(self->hom) ; self->homBuffer = vl_calloc(self->homDimension, vl_get_type_size(self->dataType)) ; } } /** @brief Get the accumulate function ** @param self object. ** @return a pointer to the accumulate function to use with this data. **/ VlSvmAccumulateFunction vl_svmdataset_get_accumulate_function(VlSvmDataset const *self) { if (self->hom == NULL) { switch (self->dataType) { case VL_TYPE_FLOAT: return (VlSvmAccumulateFunction) vl_svmdataset_accumulate_f ; break ; case VL_TYPE_DOUBLE: return (VlSvmAccumulateFunction) vl_svmdataset_accumulate_d ; break ; } } else { switch (self->dataType) { case VL_TYPE_FLOAT: return (VlSvmAccumulateFunction) vl_svmdataset_accumulate_hom_f ; break ; case VL_TYPE_DOUBLE: return (VlSvmAccumulateFunction) vl_svmdataset_accumulate_hom_d ; break ; } } assert(0) ; return NULL ; } /** @brief Get the inner product function. ** @param self object. ** @return a pointer to the inner product function to use with this data. **/ VlSvmInnerProductFunction vl_svmdataset_get_inner_product_function (VlSvmDataset const *self) { if (self->hom == NULL) { switch (self->dataType) { case VL_TYPE_FLOAT: return (VlSvmInnerProductFunction) _vl_svmdataset_inner_product_f ; break ; case VL_TYPE_DOUBLE: return (VlSvmInnerProductFunction) _vl_svmdataset_inner_product_d ; break ; default: assert(0) ; } } else { switch (self->dataType) { case VL_TYPE_FLOAT: return (VlSvmInnerProductFunction) _vl_svmdataset_inner_product_hom_f ; break ; case VL_TYPE_DOUBLE: return (VlSvmInnerProductFunction) _vl_svmdataset_inner_product_hom_d ; break ; default: assert(0) ; } } return NULL; } /* VL_SVMDATASET_INSTANTIATING */ #endif /* ---------------------------------------------------------------- */ #ifdef VL_SVMDATASET_INSTANTIATING /* ---------------------------------------------------------------- */ #include "float.h" double VL_XCAT(_vl_svmdataset_inner_product_,SFX) (VlSvmDataset const *self, vl_uindex element, double const *model) { double product = 0 ; T* data = ((T*)self->data) + self->dimension * element ; T* end = data + self->dimension ; while (data != end) { product += (*data++) * (*model++) ; } return product ; } void VL_XCAT(vl_svmdataset_accumulate_,SFX)(VlSvmDataset const *self, vl_uindex element, double *model, const double multiplier) { T* data = ((T*)self->data) + self->dimension * element ; T* end = data + self->dimension ; while (data != end) { *model += (*data++) * multiplier ; model++ ; } } double VL_XCAT(_vl_svmdataset_inner_product_hom_,SFX) (VlSvmDataset const *self, vl_uindex element, double const *model) { double product = 0 ; T* data = ((T*)self->data) + self->dimension * element ; T* end = data + self->dimension ; T* bufEnd = ((T*)self->homBuffer)+ self->homDimension ; while (data != end) { /* TODO: zeros in data could be optimized by skipping over them */ T* buf = self->homBuffer ; VL_XCAT(vl_homogeneouskernelmap_evaluate_,SFX)(self->hom, self->homBuffer, 1, (*data++)) ; while (buf != bufEnd) { product += (*buf++) * (*model++) ; } } return product ; } void VL_XCAT(vl_svmdataset_accumulate_hom_,SFX)(VlSvmDataset const *self, vl_uindex element, double *model, const double multiplier) { T* data = ((T*)self->data) + self->dimension * element ; T* end = data + self->dimension ; T* bufEnd = ((T*)self->homBuffer)+ self->homDimension ; while (data != end) { /* TODO: zeros in data could be optimized by skipping over them */ T* buf = self->homBuffer ; VL_XCAT(vl_homogeneouskernelmap_evaluate_,SFX)(self->hom, self->homBuffer, 1, (*data++)) ; while (buf != bufEnd) { *model += (*buf++) * multiplier ; model++ ; } } } #undef FLT #undef VL_SVMDATASET_INSTANTIATING /* VL_SVMDATASET_INSTANTIATING */ #endif colmap-3.9.1/src/thirdparty/VLFeat/svmdataset.h000066400000000000000000000053001454702036400214300ustar00rootroot00000000000000/** @file svmdataset.h ** @brief SVM Dataset ** @author Daniele Perrone ** @author Andrea Vedaldi **/ /* Copyright (C) 2012 Daniele Perrone. Copyright (C) 2013 Andrea Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_SVMDATASET_H #define VL_SVMDATASET_H #include "generic.h" #include "homkermap.h" struct VlSvm_ ; /** @typedef VlSvmDataset ** @brief SVM dataset object ** ** This objects contain a training set to be used in combination with ** the SVM solver object ::VlSvm. Its main purpose is to implement ** the two basic operations inner product (::VlSvmInnerProductFunction) ** and accumulation (::VlSvmAccumulateFunction). ** ** See @ref svm and @ref svm-advanced for further information. **/ #ifndef __DOXYGEN__ struct VlSvmDataset_ ; typedef struct VlSvmDataset_ VlSvmDataset ; #else typedef OPAQUE VlSvmDataset ; #endif /** @name SVM callbacks ** @{ */ typedef void (*VlSvmDiagnosticFunction) (struct VlSvm_ *svm, void *data) ; typedef double (*VlSvmLossFunction) (double inner, double label) ; typedef double (*VlSvmDcaUpdateFunction) (double alpha, double inner, double norm2, double label) ; typedef double (*VlSvmInnerProductFunction)(const void *data, vl_uindex element, double *model) ; typedef void (*VlSvmAccumulateFunction) (const void *data, vl_uindex element, double *model, double multiplier) ; /* typedef double (*VlSvmSquareNormFunction) (const void *data, vl_uindex element) ; */ /** @} */ /** @name Create and destroy ** @{ **/ VL_EXPORT VlSvmDataset* vl_svmdataset_new (vl_type dataType, void *data, vl_size dimension, vl_size numData) ; VL_EXPORT void vl_svmdataset_delete (VlSvmDataset * dataset) ; /** @} */ /** @name Set parameters ** @{ **/ VL_EXPORT void vl_svmdataset_set_homogeneous_kernel_map (VlSvmDataset * self, VlHomogeneousKernelMap * hom) ; /** @} */ /** @name Get data and parameters ** @{ **/ VL_EXPORT void* vl_svmdataset_get_data (VlSvmDataset const *self) ; VL_EXPORT vl_size vl_svmdataset_get_num_data (VlSvmDataset const *self) ; VL_EXPORT vl_size vl_svmdataset_get_dimension (VlSvmDataset const *self) ; VL_EXPORT void* vl_svmdataset_get_map (VlSvmDataset const *self) ; VL_EXPORT vl_size vl_svmdataset_get_mapDim (VlSvmDataset const *self) ; VL_EXPORT VlSvmAccumulateFunction vl_svmdataset_get_accumulate_function (VlSvmDataset const *self) ; VL_EXPORT VlSvmInnerProductFunction vl_svmdataset_get_inner_product_function (VlSvmDataset const * self) ; VL_EXPORT VlHomogeneousKernelMap * vl_svmdataset_get_homogeneous_kernel_map (VlSvmDataset const * self) ; /** @} */ /* VL_SVMDATASET_H */ #endif colmap-3.9.1/src/thirdparty/VLFeat/vlad.c000077500000000000000000000242451454702036400202120ustar00rootroot00000000000000/** @file vlad.c ** @brief VLAD - Declaration ** @author David Novotny ** @author Andrea Vedaldi **/ /* Copyright (C) 2013 David Novotny and Andera Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ /** @page vlad Vector of Locally Aggregated Descriptors (VLAD) encoding @author David Novotny @author Andrea Vedaldi @ref vlad.h implements the *Vector of Linearly Aggregated Descriptors* (VLAD) image representation @cite{jegou10aggregating} @cite{arandjelovic13all-about}. @ref vlad-starting demonstreates how to use the C API to compute the VLAD representation of an image. For further details on the VLAD image representation refer to: - @subpage vlad-fundamentals - VLAD definition and computation. @section vlad-starting Getting started with VLAD The VLAD encoding of a set of features is obtained by using the function ::vl_vlad_encode. The function can be applied to both @c float or @c double data types. ::vl_vlad_encode requires a visual dictionary, for example obtained by using @ref kmeans. Furthermore, the assignments of features to dictionary elements must be pre-computed, for example by using @ref kdtree. In the following example code, the vocabulary is first created using the KMeans clustering, then the points, that are to be encoded are assigned to its corresponding nearest vocabulary words, after that the original vlad encoding routine without any normalization option takes place. At the end of the process the encoding is stored in the @c enc variable. @code vl_uint32 * indexes; float * assignments; float * enc int i; // create a KMeans object and run clustering to get vocabulary words (centers) kmeans = vl_kmeans_new (VLDistanceL2, VL_TYPE_FLOAT) ; vl_kmeans_cluster (kmeans, data, dimension, numData, numCenters) ; // find nearest cliuster centers for the data that should be encoded indexes = vl_malloc(sizeof(vl_uint32) * numDataToEncode); vl_kmeans_quantize(kmeans,indexes,dataToEncode,numDataToEncode); // convert indexes array to assignments array, // which can be processed by vl_vlad_encode assignments = vl_malloc(sizeof(float) * numDataToEncode * numCenters); memset(assignments, 0, sizeof(float) * numDataToEncode * numCenters); for(i = 0; i < numDataToEncode; i++) { assignments[i * numCenters + indexes[i]] = 1.; } // allocate space for vlad encoding enc = vl_malloc(sizeof(TYPE) * dimension * numCenters); // do the encoding job vl_vlad_encode (enc, VL_F_TYPE, vl_kmeans_get_centers(kmeans), dimension, numCenters, data, numData, assignments, 0) ; @endcode Various @ref vlad-normalization normalizations can be applied to the VLAD vectors. These are controlled by the parameter @a flag of ::vl_vlad_encode. @page vlad-fundamentals VLAD fundamentals @tableofcontents This page describes the *Vector of Locally Aggregated Descriptors* (VLAD) image encoding of @cite{jegou10aggregating}. See @ref vlad for an overview of the C API. VLAD is a *feature encoding and pooling* method, similar to @ref fisher "Fisher vectors". VLAD encodes a set of local feature descriptors $I=(\bx_1,\dots,\bx_n)$ extracted from an image using a dictionary built using a clustering method such as @ref gmm or @ref kmeans. Let $q_{ik}$ be the strength of the association of data vector $\bx_i$ to cluster $\mu_k$, such that $q_{ik} \geq 0$ and $\sum_{k=1}^K q_{ik} = 1$. The association may be either soft (e.g. obtained as the posterior probabilities of the GMM clusters) or hard (e.g. obtained by vector quantization with K-means). $\mu_k$ are the cluster *means*, vectors of the same dimension as the data $\bx_i$. VLAD encodes feature $\bx$ by considering the *residuals* \[ \bv_k = \sum_{i=1}^{N} q_{ik} (\bx_{i} - \mu_k). \] The residulas are stacked together to obtain the vector \[ \hat\Phi(I) = \begin{bmatrix} \vdots \\ \bv_k \\ \vdots \end{bmatrix} \] Before the VLAD encoding is used it is usually normalized, as explained @ref vlad-normalization next. @section vlad-normalization VLAD normalization VLFeat VLAD implementation supports a number of different normalization strategies. These are optionally applied in this order: - **Component-wise mass normalization.** Each vector $\bv_k$ is divided by the total mass of features associated to it $\sum_{i=1}^N q_{ik}$. - **Square-rooting.** The function $\sign(z)\sqrt{|z|}$ is applied to all scalar components of the VLAD descriptor. - **Component-wise $l^2$ normalization.** The vectors $\bv_k$ are divided by their norm $\|\bv_k\|_2$. - **Global $l^2$ normalization.** The VLAD descriptor $\hat\Phi(I)$ is divided by its norm $\|\hat\Phi(I)\|_2$. */ #include "vlad.h" #include "mathop.h" #include #include #include #if defined(_OPENMP) #include #endif /* ================================================================ */ #ifdef VL_VLAD_INSTANTIATING static void VL_XCAT(_vl_vlad_encode_, SFX) (TYPE * enc, TYPE const * means, vl_size dimension, vl_size numClusters, TYPE const * data, vl_size numData, TYPE const * assignments, int flags) { vl_uindex dim ; vl_index i_cl, i_d ; memset(enc, 0, sizeof(TYPE) * dimension * numClusters) ; #if defined(_OPENMP) #pragma omp parallel for default(shared) private(i_cl,i_d,dim) num_threads(vl_get_max_threads()) #endif for (i_cl = 0; i_cl < (signed)numClusters; i_cl++) { double clusterMass = 0 ; for (i_d = 0; i_d < (signed)numData; i_d++) { if (assignments[i_d*numClusters + i_cl] > 0) { double q = assignments[i_d*numClusters+i_cl] ; clusterMass += q ; for(dim = 0; dim < dimension; dim++) { enc [i_cl * dimension + dim] += q * data [i_d * dimension + dim] ; } } } if (clusterMass > 0) { if (flags & VL_VLAD_FLAG_NORMALIZE_MASS) { for(dim = 0; dim < dimension; dim++) { enc[i_cl*dimension + dim] /= clusterMass ; enc[i_cl*dimension + dim] -= means[i_cl*dimension+dim]; } } else { for(dim = 0; dim < dimension; dim++) { enc[i_cl*dimension + dim] -= clusterMass * means[i_cl*dimension+dim]; } } } if (flags & VL_VLAD_FLAG_SQUARE_ROOT) { for(dim = 0; dim < dimension; dim++) { TYPE z = enc[i_cl*dimension + dim] ; if (z >= 0) { enc[i_cl*dimension + dim] = VL_XCAT(vl_sqrt_, SFX)(z) ; } else { enc[i_cl*dimension + dim] = - VL_XCAT(vl_sqrt_, SFX)(- z) ; } } } if (flags & VL_VLAD_FLAG_NORMALIZE_COMPONENTS) { TYPE n = 0 ; dim = 0 ; for(dim = 0; dim < dimension; dim++) { TYPE z = enc[i_cl*dimension + dim] ; n += z * z ; } n = VL_XCAT(vl_sqrt_, SFX)(n) ; n = VL_MAX(n, 1e-12) ; for(dim = 0; dim < dimension; dim++) { enc[i_cl*dimension + dim] /= n ; } } } if (! (flags & VL_VLAD_FLAG_UNNORMALIZED)) { TYPE n = 0 ; for(dim = 0 ; dim < dimension * numClusters ; dim++) { TYPE z = enc [dim] ; n += z * z ; } n = VL_XCAT(vl_sqrt_, SFX)(n) ; n = VL_MAX(n, 1e-12) ; for(dim = 0 ; dim < dimension * numClusters ; dim++) { enc[dim] /= n ; } } } /* VL_FISHER_INSTANTIATING */ #else #ifndef __DOXYGEN__ #define FLT VL_TYPE_FLOAT #define TYPE float #define SFX f #define VL_VLAD_INSTANTIATING #include "vlad.c" #define FLT VL_TYPE_DOUBLE #define TYPE double #define SFX d #define VL_VLAD_INSTANTIATING #include "vlad.c" #endif /* VL_VLAD_INSTANTIATING */ #endif /* ================================================================ */ #ifndef VL_VLAD_INSTANTIATING /** @brief VLAD encoding of a set of vectors. ** @param enc output VLAD encoding (out). ** @param dataType the type of the input data (::VL_TYPE_DOUBLE or ::VL_TYPE_FLOAT). ** @param numData number of data vectors to encode. ** @param means cluster means. ** @param numClusters number of clusters. ** @param data the data vectors to encode. ** @param dimension dimensionality of the data. ** @param assignments data to cluster soft assignments. ** @param flags options. ** ** @a enc is the VLAD vector of size @a numClusters by ** @a dimension. @a means is a matrix with @a numClusters columns and ** @a dimension rows. @a data is the matrix of vectors to be encoded, ** with @a dimension rows and @a numData columns. @a assignments is a ** matrix with @a numClusters rows and @a numData columns. ** All the matrices should be stored in column-major order. ** ** @a flag allows controlling further options: ** ::VL_VLAD_FLAG_NORMALIZE_COMPONENTS, ::VL_VLAD_FLAG_SQUARE_ROOT, ** ::VL_VLAD_FLAG_UNNORMALIZED, and ::VL_VLAD_FLAG_NORMALIZE_MASS. ** ** @sa @ref vlad **/ void vl_vlad_encode (void * enc, vl_type dataType, void const * means, vl_size dimension, vl_size numClusters, void const * data, vl_size numData, void const * assignments, int flags) { switch(dataType) { case VL_TYPE_FLOAT: _vl_vlad_encode_f ((float *) enc, (float const *) means, dimension, numClusters, (float const *) data, numData, (float const *) assignments, flags) ; break; case VL_TYPE_DOUBLE: _vl_vlad_encode_d ((double *) enc, (double const *) means, dimension, numClusters, (double const *) data, numData, (double const *) assignments, flags) ; break; default: abort(); } } /* ! VL_VLAD_INSTANTIATING */ #endif #undef SFX #undef TYPE #undef FLT #undef VL_VLAD_INSTANTIATING colmap-3.9.1/src/thirdparty/VLFeat/vlad.h000066400000000000000000000024041454702036400202050ustar00rootroot00000000000000/** @file vlad.h ** @brief VLAD encoding (@ref vlad) ** @author David Novotny ** @author Andrea Vedaldi ** @see @ref vlad **/ /* Copyright (C) 2013 David Novotny and Andera Vedaldi. All rights reserved. This file is part of the VLFeat library and is made available under the terms of the BSD license (see the COPYING file). */ #ifndef VL_VLAD_H #define VL_VLAD_H #include "generic.h" /** @name VLAD options ** @{ */ #define VL_VLAD_FLAG_NORMALIZE_COMPONENTS (0x1 << 0) #define VL_VLAD_FLAG_SQUARE_ROOT (0x1 << 1) #define VL_VLAD_FLAG_UNNORMALIZED (0x1 << 2) #define VL_VLAD_FLAG_NORMALIZE_MASS (0x1 << 3) /** @def VL_VLAD_FLAG_NORMALIZE_COMPONENTS ** @brief Normalize each VLAD component individually. **/ /** @def VL_VLAD_FLAG_SQUARE_ROOT ** @brief Use signed squared-root. **/ /** @def VL_VLAD_FLAG_UNNORMALIZED ** @brief Do not globally normalize the VLAD descriptor. **/ /** @def VL_VLAD_FLAG_NORMALIZE_MASS ** @brief Normalize each component by the number of features assigned to it. **/ /** @} */ VL_EXPORT void vl_vlad_encode (void * enc, vl_type dataType, void const * means, vl_size dimension, vl_size numClusters, void const * data, vl_size numData, void const * assignments, int flags) ; /* VL_VLAD_H */ #endif

B[F7C(@<g  7 l$(BPKATt4 "UX2C.a槜>8 X \κ8\r,Z* _Cl`-9(/clˎ4ʛK>kPJ̙SD%HeK*y?Lλ24;͂V3.ϗK`? 96<";=ǎ_zJ~g`)FE߱nZ@Z:e/?^(+,4λl w`ݲe2l$ОlԜ92=}* l @/=B[sa+bD,c/ p1;.d)kP~l1dEѯN8>OZy\dBJHhαı(LApm1_3x =0hi[/[7Ȇ:mR^6BƎBٹ{cR_vR5~*z%L(D nʮ%^{zǕPJ@ (%p\{e=( 0`G?Tj<@:Ll()X%<`;0(Gtlz )@U!5J%6 q.H׬' ?_Xɠfk\vEuI `:, #*?F:ʉ\n:]jٰl_h&lntv?h~lF\B ]nxB`GA"_y$>JRi%c8=17\ (pW3GכBX&0SeߞҶ]"xҤ pyqGn.*'&&OҲQIv5m7m zpi ˫^Gksz#|"t(%$>># eqV|xC ( {^_Ϩ{@;mcJXBڟ}jY2͘OGt?5}#esܹY;vΝ ܉W^)[kj6o6&F-=FΘ!+K/5mq%׈,\YGy%6%i(Gh -b&:gTtEFQBye;G9*AC' >6b!PdcI ti*6 4;D5M ot~5V.$z-c-\ה )pД~:vqlc|(g=I&kzQ,'vG?(v\(Ӧ͔m ҾI<.P 4C7l\u(%04 >z^ݟG@.-QG~ezD%04ģ] %Źy 7/Gv% ^?חHf*1et /H[czMBkI\  \<u|KFqNH2nbX,S}<\\.o2T:X:=[Quoqo'> VҿMY׽7ˁZDk}ȑ{ 8zro-KyD&wq&~V[cvgKNAh@g՟9'OEl^-G?[HBaKVX)$e 5Yfu IJF`Ŷ;%{FdR7_)BcbhU60g .w6"V}jsBXcgGGD"T0M0~p6S3AsA@Vׁ}-(MtAD` va$"!(a D"թpmq@3*{v# kؾ様:;ө8}LYw@ _7uBL 7k׸!p9s幅':?4ػE;U?ȝWIWBMOKɯWMGZhhm߰Q9V]Unx+'(,.6%}Tҥ2M?k۱7ٲG1H"u5̉pQYBa=a qQ<`m̓S_U~)=CmcYV~WyNXFkbB\`0ށ͈[SI,r<B%RTj& ~k6(;X36P3Ғ֔ADs׃MdPbI/QtFKBǺykGiC8Ǭ #C!d! 7|j>F|6 KEyTl&(p-Pjs3 t ژ*۱/?]* Πlvy mEl\ro#ҶZӧnzt59B 5mxnZQU(9;}^M;rzG%z'ѣuDB68aґ}]A}:#rm>얳xaa \S.lB*Gic0wd6mݏ+)nY/x”֎ӱhmyz(.然 pLNU֮{ߔe:\ne?)L;*{VpH.:\15”%Wh0 wY%xPHeMҨ2bǤSh7T;qfD)+Cv5@Gt8n㎃X)))<_T)cFK</2ab!]~|n4ŠIϜ4avYއob(Snm ;ρ%'f:ٓtuxM֑{nR^6nb6 5=ߓNI]ѱOCd @Λs1~]h> _v,h :w~C (n^|u_K^OvC)8ko5"[Ŝ9tG9/)U&l P~5>~_6?*0q %l4^R3a t6=֭\{y&UGSDQ t,\1JAicI5_LA@cO5qL)( MS?!3}t@hgCYf 4=ݮ,!FDIX!90! *uIŘr"rCK` jxB> eZ[> PcN:1W{|?F4!{3f?=v]`[SWam][%r kZGn`[}'t-2{PJwe%r]r虑#GʯyX?kez ]O%"PQQ!?yGIh5M.H/Jqeeg]\Y,2bKEE9%'d3 9T.\(cȔy Mm.0kk? miIƨN\1hO)Qr8\Xt6ԓSP`, X)As"0STc5G5!@ZpqpAhb)zXVs`=w~_W]ζdkZG9_w}Uzcs]L +_r̿\xKM<翘R6 ѡ Yt/I<u[0q G#KCb)ci 3,}rtg[PA~u}L]YD^}_w9*1q#q4pK H&]x!2bbV:SLԶYUz8 v"BS9HFˍepQ̈Q!t?At=c(QW'¹(R9!^9\G&wgh{;Z0 X.ʴ% KY)s"dBט)?H}pű=pQ wY˂sTXBm:C q.L ud1k60Gy<Ztdߘ;弳>///~^֭_S]mY)A"c|lZ.:HaZ:9dsV$5w~UP* |R1jLXُxHp7ɍ7\/k֮-["lx.[ .Dʱ2}'.? p/m}YCڵK(PX9VE6vbK@g_}u-[fyC qyԨs>tˑ6Ks}>O$p !.XsXi be0l?P`UfWOjӉu0 ͠TTY2sROR8c(W 6C3.jpA ̜cN4sc8߰N&FAlPtݱ4RS ]\:9$pQ !;Ka8u(ӌV#f@:P"`).nzWh23\xӡM3>S=;% &G c}yI;6;Dﲖޓ|&O,#!Y3;w,GAJ 匯}-OML(=Һ}!m^[ŁpCbU\צ\/Y6YuJaI>݂P_6ZPJGKI1 E\(/miVO[k,لX E8t1Z,"]L7|o<K2*x !-I,;t@ -fViÄ$`9XK^mX|JI, e+Scv(u׮ƕzL3~싂Q>,/]r:]2e#FEgVJ@ (%PJ@ (AF|UU{dx?g6ɑX!vh^uOe[oI WFeSh=Ǥck6֗RP5kH$ҩؗp{55IҺmXZ D4%KK M>*{4T^sM_ mtsY\eKAڊRr@X ,-2MSJ Xg(+wFôgf\jmtŃ! Jٶ/pk9ֺ} p%q1B,(Z./`t0bsZO*pPb(K)cyP]fo=2#MFלB'q?(jR͎@VϹ)%}jsPPL':qqx)&!0!退g5W%PJ@ (%PJoZׯ}k֘zV8 C[sI-LbK)Rm%Gn+r8 tpdm\XN^F k>'h&:No Ck.q-%8q7 H^kG JRx)h[qIMYk~?k}PJ@ (%PJ@ (%'Ґ9K 8Y_!}_h?s+#Re^y%]Zxb6G "[}ݟU? }PJ@ (%PJ@ (%)QW\!7K>DHsid'Z_{<O?ǯ('QwJ ˳PnK"--{X9s|G~Ux*#+B#<2TWשPJ@ (%PJ@ Yd~ Ƣ@u]w;WqW\j|;曻IAc_J|Rdőߵɜ@fūϯ{(%PJ@ (%PJ@ d/CiAoEO }c֏~$_s`Oy>i7ߔаp\T֓~3U]PJ@ (%PJ@ (%pp6!EW9G\)%H,:d~a-O?-vτ@m,woEf>\gtd mᨳ(%PJ@ (%PJ˥ o =NA9'pmw#3!ĭFH‹sHѬYoHm}GeAUh;_]=7%PJ@ (%PJ `XLŅ@VTkWi{MҶa\jLG :O@3PJ@ (%PJ@ (%pTm6v/Y"@@@<#Qʺf%uxPJ@ (%PJ@ (@{Eٵt\e56_B=%PJ@ (%PJ@ @g }tPh`IPJ@ (%PJ@ (%7h;^A:`0(:PJk<&kSJ@ (%r@ݖ9"]8|v/R(t(߾E~eSF (%@`˶FL:PJ@ (%>/BW@@KGQPJ@ (%PJ@ (%P*πPJ@ (%PJ@ (%K #=O 5 Izu(%PJ@ (%JTJ5 m*%PJ@ (%PJ@ (%p`PJ@ (%PJ@ (%@ otPJ@ (%PJ@ (%8@?CIENDB`colmap-3.9.1/doc/images/sparse.png000077500000000000000000040755471454702036400170520ustar00rootroot00000000000000PNG  IHDRY5C$sRGB pHYs  YiTXtXML:com.adobe.xmp 1 L'Y@IDATx|\ll6HiQ*Z.p{{.ťTh)uwMٸsβF6&y̙3lJKD /st=sA~} ḼhUk藟+kky#w+TUU&tm(MAȞϰ[> b,}ɀȉspuvC_M^47:B#v/^FеKx/UUUpv&o۵\ʟjhv˕%0 dfLd]]P- DH -Pe27Te :,oXNMlj{ NCھ:U"@ E@DV!4"@M|.N&@-u0p/^u\!3%?wpuV 'EbE]:"+=u'}=^hA*27*ym?nZ j*izLuA{cs}C2 #v30 D"@&l~";V"kCM>?@J!"@ D`d yN & Ue2JK%Co $vN}s ~C\=q ex(5ոv ~ep0==|]~fZ#o;8h#h?+)9#SVv돻~'RŹ p|9v6=k> SԺSceD ,:j"@PN(']Y31>X18qVE&;D"@F,04 D<Cln:1UX>GC'@503pg,V#`"-oVeAۨmLݛ FMeoD@ca*L=[N`G8[ػx")z` `BLl~y/Z;v3͏?Bv/'-0[);O->w YB7"2r+V4̛uX?{XuB s{xL8,-'x&ˆ D`\vNu ujpSZ4Z@@V˒"@P&.Xͅ"0 KXM#PZڰp]!5!}xPª < 3!@lsf•"Rjb,Zx+lp"빠kȋNogώx`#=k.ï} lݦ iv,zYԨ=St?ڊbԲ0Qq8a,[43D|UMKg-BSm)=b '۽{t?)soj^K='mƠBLC"@ D`t ^=ttgI"@ %@K"@&ƆZ@]  H׶W&4`a〫BosW˯Sa}K =ow)L[;{Lٗ0mrdfł}#3Gv߮]c:*&ry<&#X9B#3=sx?zBaªWS588 _/vV{N`rl$\VTXVRix{e+͏"@:JQ_t C D O3$ DqO*jPWBBl8+kMO {.l_u/zzc L|2#P܆|7S[S^@8?ZJP~#@#00ABz6jذ~Spq,?]# nuv,fV3ҡhifa_/5ܩ#H$k#ı]_u(Xv5,Xn^0 OAYw'gW^s0Jan&P]^WD!-!֛9#R1 ;pkХKȈ ëwMr #VrϝfG D7:͗"0kW?hp*hvg@Md[ӲsQQY  N(qYs;PTP ohhiW_~}!|!y{*(4+T[ T,+ݏU9f8ft oLvݡ[줶$/ EV*:"0Bjkk! G:8{ ȍ*Iniб#^_dhID"@'@ 'PUVaC]IM tSoYqLζhilA|dx7pbZ"`Ru}xeHMn[`LLOz6imT40%RMBgV.qk8u/?i4\TQ.S" 펏zGS`}ꙗ勳 H% ,g*Z?:+FRW_'zm4+UuLI`M4 DIJ@m&iDIE 8t:Zwm"  ՟N`}tmx7dUˣ]((.SPY^ 63PO 'aώ 00…בw~-L7{:JsR0sў27fVPԁ z 4Ń뤤OΞ#qμlNoEn(aaIa"kpV?ŴY{%˔ D`|,˒@VwBȘe7bSPЏ1 K+7(D yNH D(@Nr, L``J^NJ;,^9ٙxS9TVW"*2,HPUW -gwO4U@K Bkd&c㦇 ~m> $P[%$'N޷$5ͅPAMwpԔ̸i&#o|wNh}b4wYX^fypIq&!.D`B =-_z{B38W_x?utgG)#IAĖ85M85̄{웚) DnHd톃N s3m> D^T|̥{e:h|ԡjUW\߁KoAoB|yOq|"RjDDC3>}&^?r[UhcqYspB@ǐ O0VUzz|^huiX횢RDzN!||<5RI.Tl3~77V-t@aO>@d(@ou3lҿ@Bk7$t455B]]j#X>|d?a,=2⠢maRXWq>l(d"@nI5 DOL\՚ kΰϝCzF&gӬ+n3ric]dd0o~̅ acBE@]EEYe~zy!]R KPBۏduYV7DŽtz?LD1F˦4+Lؿc#V֕壅髩38O̫ZYRIZ8 klcJZ:z0YAMmbK:=S:N٤t\q `"pPfQt:C[y41 UUU>2Ln ZU"H7T}Sf[$ikhd~ 5έe:"@!@VB }(//l"]!cO`Ro+߷%l౧ř"3dg!!&_9 )q2<:X.D {qyc~\3–Xcl(=ҵ[ K{\uB'D$h "@@fR< md:HP7BUKqp"k{Y ]6 dR/vMTAy*鍡i"@F@L*(qIDqyhD"0QT&D"󉲤qNd0m_a3t@ء3ϙ8]:v4eb r됓OOhI s/ܺrhj}r%r_"+'?-PhC7 GKl$>> .^ǐmtX u!PqE|ʴX66`V 6(.*t4s%i ![;R[Y8^Κ>щ @ue9l0Xb U"@@lv {L6l.t(% &E{1m\͜33V.:z78?LKKWf| qQX_p~e^EyiLڲa(r@3ݝSU(jVea &{=f%CGf!c7#'&' \!m.?yCW= 5h1r.$uoDSwϻ |tBpB•YHTA D"@ yNJ"D @d5XXvBu2#c{A]Q *Qyn%Z-(h{ :wb}Iۏ>R;Ȳ-`uY1MK C5tɣcLZ^I=f~G{W4TL@? khأ"/= :won$po*lhb {1 L DjK9uKVԀ"@jJ @LBYl+}Wȋ6 핹n^sa gQ 7ZL|3k.m?mՅ;:yg L<.]$/W̋ci-b--)7/;GU @Mq68֮)&6;wtYsy+^PDFF{3sR~rLW}2^zm> aX)"0OEO\E~nZ"N ؘ"@ DHd5#jA$$ceJD`PGkGsO ǟظae-PmGNz"Ԅƨkl!8R=X__BaeTr,\?2+ʍ>~?]|]Ld^y1 1WAC/̦ ^|~~n:شqZk07/?lދt=^+YhhH:y7̘9D }Li|VqB4p D"hOVG4C"@@mU!ln;Vti8=L'ơ^P;gJRpYR#/{N:zT "6>Ά0-NDF|7?KUTcUpUq0'(f_z܊;zEhoiElT`{ >sVAl;qm,Nk_X.]VdyHzMxg0cHS'rhk<3Y\P.¼tp"못=ҜD@ _$pz@K5\7X΋&D"@z '!DLz%/򄖎K"\/3B3B=W5qM+ }-T <.O8:< ʄhƩ_Eq1ʋ%YYBSӨORǏWQvogUC;y\)]&>N`͎?>/ Êkbl<}m7qҰ^wzkKuCE@CCYGg`1FAOLDl trd~M1xwp|^"'$Xp'$\=Uxd"@&65 '-4 /V"._D- %VW[5&>y96YKw\ [p!1ܾi1 yd'~%.yߩ,DbzAڷIL223ygy[$HKw}BYz8*KQA~ SC`JJ.sny*J`=e<p*DQM /K-T\(?L::l)=ǘ?o51F_G ˔ D y#D"XG-!]:UI$ȍ[ײu{5 Wo^ʊz~(j_;#THr9ҵwŽݖfoᏃq600ۄmS`}MPkjyb/S"+!o!琝*xM!>./uV).;6 v}&b8qa-$/1󼝜y %Ym- w'#݁=Run58#i3٬iD;JႻs3"@D!P[3 _ԉ4Z 7w OGL"@d!PSSc\U5A E!ge.z-k\xX`|4TW{ߙDPCSC$qwT]7hU7^Of7_7z7OE)έY_v}mPU\Hl۾Yh`_\1|cD#/zzrkxe7DOf Y͏NhTpK)xdW\GϹBTXU҉xv~41"0܃8z7RԆ"0> %DQI<>WA&DV-!R^pK[5D@fI DqH .1ť0 yZZex{y,?P& |wU!b'42E:F-m IZ<ZQS^ <3dH`{Λ?c{Ww}w@V!-< * h7ևGb" l2pA]eUƒ5=,V6;hd"8:N5,f#*&B] S6UkCi&lll~:046Ayy /jj/} "nKYWOG7-,m'n~+׭OUWW"@d)r^ڂgO||YA -&"k1\251gΜ֯tA[ZY^9¹lN=8Ds]ǻcO=![g.өb ܀#$Ζig^LdeѦߪ}EjZKg(D`LOa{-.ײs_才}&nY"  XCs^Ö 6e&g8>C ֮r{xХBTWYNLL*-}{ d)P[ڀ>JXN\咡qwI/ی۹Rd wT1[vj?B4VCS GIj24Q]SAomFFZ<\8ID3 \=*>OXkbTUSæk'5D89o&IIR"S3'n݄B|G  t71a]|`膦6ADT:%,vϷ181b;ԋ1:SH_ f i TϢ7/lΉ5f!ra9c22k]5ΎV6qA <Y@@ዏUvt?aȳ8;䁨`=? [Cޟ4%MP14ٸަL"koT"@x*#3 I`{,X ukw^HscS3e`?anOSfRSpitTY=Zja]p!>zu6KC%,g/EAEU T;|ǃ:8:`V@ճ;W{mmPS+] -2r8y2`=hm,YdRV n1Fpf6m~Ggjj‹ʥ O=#7ޱFVR}8cw-OjSקts0vC:tՠ!**63f_{f'c <մyY;KE `Ű)<݇!b^\hgJA@`bM`ͪ/UvQSKܵD P78$?N糶owk"&`YT kdYك%z0 )"0 i:/-mhE^4<3}%~ !ڍ0TA?1I"0Q#"@  Һ{gFF lZXXhDئ*v3FT|4ݻGѨhkB[s54F.LP#t@:T;]в Xv; gC u-0f;P7/{x̅ox-5HAy fa׭_{Pvk0eN\+ǩjZSUڀLI=|x " FzXam Chhgk0\^8 6*b)MVUmT[m=#>'3.s`ϩ\*$3v 2 "2b"Qw17ElM -]35g^B>wmϕKr4=끝?")6 n[ѯ* \*,(wl,[<Z&^pdd7板U+Uё"@FpVbmkpx]Q' qaqnurQU]V: D`@*Zz~7.(2uz$vAe"@ $S!߇w_=Ko1[Ov},~3-MhlQPgto569iJôQh'E :uUphe ?+"cX̨ X3&h裠O@mE!~t]*b']up񇵃+Q[YIJ%LbFT!'BCl1XϝG)L@yqJ %SVÄX{O>jK-B۔:"aQ&Uĉ7u%ġ74ʉ%?`>|3'yquִ݃: K+&Zo2*緑y kqzKU`xM{˔_ٖ%6oFXXR6,[sn&^zK{qm4'"0pM`c{U9uS<=_l.*5e}M[5DIM`c +Ryl74=#/N Mŋz}8"p3,pYYUǤ $ɯ߹n=_~2Ց/v^dV8yہrs̼=o""'a&.l,ƣ 3Ev:'Z)"0'xL`>P9 p\_3 rΚfEb/h$0/֞/DK \D i*SY6*5ő'"0y <Ð h哉@aJ SSk}/acG`^غ{pe Bcw>Ubia+ s6|p|s"2:s̑ sˣP:ʷ-'>Gz~ʋklJ8y_nj+a FjV.Zs n~b\t6"%Wڹ3p>,=XػwVY]>|s ϣfhj`qIw8O҃BWBx/V*R@: Iv vwлS箆!-tŶ^0,[v̤\3kz:B|~~>7k_z$k/DT5V\l,!ޣk[w>X{b#"tKD'%`lɼ޹L"@N?_ft `dMv|/]l޹PXS.}P[3*Kss-s!ُuH 5F}]Kota/Da#$D`زe /N^FnLT}QAWĝ6|`me_sƺ@(lش#w|c'bu[mFPG=-Nܞe%vYY7&$,=n36CijݽRY v?v%&٥7Dzkpy(e⪡Ȏm  ~ j#:"}~z4*+ S\WBrV!|v<{0*Z+W+ s+QXcs.9WmeYg/`@X.n>w^Bdp\=ghB0-Gv7o[&rōR=VPV|߾z02/`bҺk$g 39fVҗ5C D"PZ^y3jxY}c"S X߹[355M~[H"@&. u}~hD"0Lwo؀W}BRJj̚%Xʠ^`MdMMdJ2!†ƦZXX[+'rǁz43tX1Bn = ʧtV>OJCMy1ٹ:ξ~cR\$``䎊Z8zOE+sjd+W}=,ZB EsS M=N`lEFF"`,PSIV,rsnhhׅ#ħ8#aE0049Zf]ss3l]f! PWJz5e}7ߛ&z;6r\A!܆,3!xƂ0 LMDYqQ7=?ènc@(21=wBL(DQ~&WrG\d7B]ML""@P(g[Ɩ._3+@ $O ͙J`-qyO??1ŢҘ΀CAv0"@GTc`=wo&;ww"ڷ <_?c#%9?uu'v}KL@h4䯁Po8 OBWSO{w|_ڜpĦV'- |~L@AB? x'wocx9Ⱦ VT?W('֔Cϴ61X+k-Yx*5?.!7ї i34uybHɋ d墭 \ ;uڌ@GTBl)X|[S%wg #PSRGDC~k8 ۋP j;;1ϻv|H9g^7E 2#:D602A۳u?r`kga{v9y9g8~>7?B#cö#='D"@ D"@;fuU;‰=bJ:F'ð}Hapڈhi)pC 2ﰵN{+.^:`depK̓ {P Wgascud],++fפ"L!33%uLJ7ZKH8w,m`i]|U 5I!7`#>?o~ӑ{I^`:/B]cR`ge VTT'`cˇVyXXNꁚyM 5IAp`jp5IJˠV-mZL`)(glY!Oiϡ -+)};GPKEe,L&D,..\ 5f^*x9V.GQ%fW 51 ᵘ:ou&\ĩb }RM"S&=UkXxrwm-XxRHgߙ%%/res[Gy{jxpcZ1/~KKȝS"D"@ D"@G=#|&SQc?o&w*i'X8mJo4>Ԑ"0V~7$Ƅ"&:yYܹwQ&7' gYj*P[U ;YdCGWy5+XR鿋YXo* ^S뷟oD;֯?{@KN?~9ĬE="1Db{QT 5c6~׃w1VqN*l߷thhB``6&]=ńcE&Gt砱B#BM{U{HJpez3_N\ݒrwQ8lp||"!% p2 qS[v߅<[{hC8t:1d$9&vx/#9 !,P %q.yxÞ+KE 0w/#/;wo[U>ɏ7?^<<\uC)DE"&*nykhဘkǽ/imo+OÇ0a`oPxQ"@O? "Ѝ@ow; \n*JD"@ @EkuT ^ǽwNh' Q μ:=|cVsم0#Ѭ ?// w3N=5 ]d@IDATq|'+QR׊Yxի33-=&*l{6"@%VůLEM]&-" 'hWW4md(p^n10de%p)2vEFXS P9OyGo","1QK_n݉,,fo1!Z55zkEyhµs;yMXWm]&_NR_3 ƍ ze~6T~Zd샱x`x]Ώ25S1W~>ZYuta"A5lyiXo,ĝi&^8i\85Dߟę;7s NHE1C]}; ]9VK] ׮^AuuTSgamܵp/<>V΃uo AFlٖuAfVPW[0 xKACW =6= |  31{9VAsl>NO^99/y,WgHǴ{زu&r핵%}vo?'4tNٟgfh[ W/IJU69—gߦ7Esr2Js4%ci &`>*a3XDn&`Bw 4H'uCjA! si>"ShFK RQ_[[מ[ 5%&p!,vv 5/ QY;!ڌK}7;{NDL@W) \VJ'~gaaWc-L`2ܧ@OvsƼ[- pϟ!1 3 ,iVF& wgr֊bO$1Gu= 3n?ʻue_Myso'|~]][Gǧ=b{&Da? \-k/s/k )<w8r8^?q΀/^nXc]+l5S L8ŋfc<\0wv-'Q^+6`6=.# =4*I؞aaAX~K0s6?_|9m#Dd"+45 $r{t =p;쥇$vrWbX ;7 ƛd޺;v"**K:c7>lek /6l@_lGWg>o~ekgxi#ŧ :J{É7I<M"@>.#sʾJ$N g6Cuh㾵 _ SW@58t_Z/|Sf-FIE,\u6<Eb[YA]:\|ѫv8 S\]u!tin__7ݼnSj8/!kI-wVHr 6xyzs:TTV͏_(˓ B?!b̌ X^oBB\40^ Fԍk^{k[ ; B}| Mz%+VAlnv(`(2Q5Q.Z {wPY^+J^_m߆O;&`uU9UR! %[sк:#pPa}u -&Զ3PtDiq1.]m,e1A˫Zl%z9,Z ihgqyXD8,P>!b/$7L3p4c#z>~ wb3rahc8t.{}_sSsq~Y+֮AЬ2#oơ=7/~u浞^$Vnb,\>;[+E@u,߳؛0b_UXv޾X,֙LBC`rvqez^>WںBhI#qILwls"T]2 D( kw*A|msU nM(N`=wx{wr 06lU4&yR 5mfDM e"~/0}Y9z2qUӘQY{yr+,.Md9{I+@]y& +`bFÊ;ֆ#fjaӞ^~ /zxX:]Ķ 3o==,G3R]scKa^y{T5:S[o!g}{X}ތgK*k0pR+-^evomF"9&yNCcm6}L,`l>UO<*|}n\=ZǨKs '>\xU?.cŎuGUl?nzW $7AE)ݧ{CłDEy4@ i$!!el)^MS{ww9y}>9OBMw/o[ZHW{ $'g )Jr6ELV=Y +?-WܪcB{}:Ѧ6(<uOL q9DLd1$ŜiRőB WN1rUo6~yrTp@16ơk gHe䳏TTciBBB;67Cn[+s? XMH>{.~3C!̈lf V*g*KL51 Q605/4D;Ap8LmV6W W( & G`-Nh#'`LXڻ`$]NDIq1m8-P8<9P"6! z^XmՄӭzQqa9ՉL;yssچxr3Iq"v9ٙE.$Ģt5}7ril"~ykj=;w!묌geÐ"]43G~"zz% vn'"E4M÷HEBf3N&ʋ~ ,(N.\܆NyenECMԵnI}wT[V"-*+A&5&ɥxqgFQWU\,{erUzqdfeb֬Bѯ{‡\ֵ~kurՙ`#+ȳwƺ0=7soT\7<gkGHDsݫꆷ&v1u$8`8#X]"b͞=?ϱ3Uz!RN>.$C%VD~-"#Xk)wTRxP^VTDօjhin@-Db5 ̲$YY:55 1 /ŲT$^8yH""qxBcYoBdK&PT -.*2`F֒Z*wz*,}+nkVRm"$V@KOLjJ"t,Zz0u; +DeBB<)榆-ڄQy }񐿠L3+(>lFϽyEz$kjV@HkDӄ6roT"S?*+Mj[XAO Iu 5U()ļ ؈76~"s=}hwWio(}O=#?@SGK71,/ ;ih鄊yeE%Y ?' BwԤt(7{ځo݊!3'եsd?}NFvF2q!KU0YDzٴ %Y$DO_GUXBs8Gc53z#PG)q0ycP7] d9m""1My2 ֮k6=/Ea^<ǯG|-G#,,k)ۈ7Ҥ&mJFv%c`exsuwop`ݶm3-[GUNvǚ,1Xs%J ֊4ִdu:DXydAMq4ui#͓G~Ù3D.ꢩaXu>]M5{d ɬ;TlxXtuDyS`b޻efĹsɁezo@Y =N^.Fd^^ |U͑lrO͞9 cxi8M:(hFxZBVUp75ZxƥPǿކzptƹ7Cw͸݌܆+p xw;8ug}U_Xt=AxZ!!"xVbscM?l\Frj1Mf]F5aO8}w_u7ގrE]դCvfzݳE ]"l"O>~ L*{LyydUj!ce-GUSAEK 2byR}ۻ[aw{RxQ(4uҰs+qœw܀5ol$~ bU[~S[#+Jq=rPPZ _/dȒٙt7F0 p&'/:g<~jJ"a;cKQֿ 73hKV^`31suXڛBY25rZ[ϰ%IQ^Q˶,y9"RJd3e`ݲu6?h0~05,ip8M~.L.I<~uew(`}U5ZL. eYWQYV )ĕ>+*: <0߯~88h",YK?Z_ݩ=*4(EB5h.#Y;1cUtHbw)G#0$&&ظ6 CoAuQĞt#5f}p%BSЃɪcL5?+Z<"}{W޽(.ʁl)|C5+4VWɉ37Y ]5ɑ!%Ȋ U*R"]ӧC-H4Tߏ6ASIțY݆kɽ5)ֺË,A6u u1X*F&32%B.W4F͑v[KeXVF*ړ$_gͽhDq^&J7U``.^VTQ5=|fZZňe7ZJq[^{"+; Df&:Ydj+D6;FT E;A062FYA>۰ wyoW3MtU(NlAbѪkhJj*fڌ~K,XXjSW}@"p|uyRnoE]ܹ My55Bb.spFSϮ%1K5X‰u@djb>21,^# ܼ| Wa%eKe}BF/Y>__vNҢQOk. IR%CzXyGߋJƦ!+pnmDNFjJD Ǿ"$XpzAසGV.Cq1:zPSds@xBc('q=NecOˆTĎL>/ht{CTuujϘ۞?O%XT8G[on:ģ7ƂVN?r٭a= 쯨̀1%`0dٺF$yJ//"\1mj ")";!b)25J<-JN4ɽpl|^ VURZؠ(=܍@ȎU3#ϡegeKճKeO0ͻo}u>+sd['^P^Jr%҃%=XXc#*8)nЪBMd$'bڍ0 V-he}4 M\ad)&YJޔʚV5@+$U˯.F^vY߆h(-MCK]%dLq q+O%2_LS&f8{* sEzKniDnu02Ё)h#W;w@6†K~O*dJJʚPR!s\7߀G3ƺUUą#ʋEuc?skxB /7 UU~]e>(ǦQP,1֣:DyU0ŗ M1(/VqQϯu%ӮVWR.qEl@Aq1|<&$̆#P5wj(J3OXWRg֨kRɌz-xG^Ui/)X*dV(+PgY>1$x8ɆNTzhl t98Nӧ -xMn\&Ь/"7mP5EBb"t.d:ғb%7 S"Uf̞VA>o'G1FnBeLo?n?c/@K_L֗ >/dĄQ _0k2()Mr8-;ň Ɗ{6]Uɤbq~ܼp$kLu)ޮ޵$hUUp[s5`Pw]#de.OLLſM5PRUGUy>g*ps r!")jZdXBHdž74PN!f́%ŝU_#?5JpߞS,)Iƴ7Թxܴl%45PTP&z;OIokq3 !yōZ@.i9*2 ~`}AZ;[\{\튓&U 괵Ԣ&jߔz6VCQY]w1 %G#0Zq39 S}~7tNmz=FQ^b"áJIp gCOBnf&T L8{!SÔ,~-h ||'\N^@Ld$+pFL4{ Ykp&du'q?y3p8G`,!g\4[9-Um ?]aU]uoJeA, &!D Tܒx1G#0q` />0!y}n2d\*+VYHkZcu.eQ@0y8o>aO_٘Dj0v|@S5qU>^xrꅆ]Mn'R65 :ʬX,`%bfܬLXْAيuPllma e}s<w1*PV|jXnl* Wrs[n[dx?o(.*52ek'r]+V_bZBxix6={ YY+ /c[_̙AYl7[pܦEjN%`osz0*}JQ\ jػ qV[?>+RN'4/]‡`7n{O7mg0rAT6B/ͻpFF%@~ymArb <ٳ>r#P"XQ/T>S ^)D-ɓϿe%u(qŃރc,,e' pOfKQl]-m a5aύQRSQ &J@@0Pz^x UHa&V`)#%Wם'zf4\r)={>[Yi7x6ٰ/\X(π5ZT#n+*6EuoDF x̸UPs۝wwJʪJL@nm}L_aF?0C5%}+2G1sU +p8G#H*yMA*4D۸^4흧;y3htR}0%F֒"kUdHC|Vk;>+kA~y1nLq؟g"93;WxNogtf]WNW`廭x/ڳY~3r+oIeڪUSTPQׁ1QA͏nGrά(Y#C0}&J`<75)9#Z+l1O<[P ~njn38:Xdy =CVVFJb"dsQH87q'‰FaQa;dUIW{,X9_x=@ ,MVa*K̋DD X>dJݎ<&k7HxG#p:ؿ{xxvdt9HK! ĻLPxKn.E8EUe9+o_vVNZH_{<9ChAt{{]Y8xeA fo=D؏a+K '{bɚdF@ L$XYY P[G$$y:`ʾIM+ Uz..c.'N|.#ްnhrez52n[Tz wz^[&WZeM&d p8@AAA`/Qccp_pq÷sti'X% 05p3p+a Z(:!Xٙ\cv5=)!h̽$} 7ļyTe?mǾ]p 5.r!ֺ,jIX9Z:́3ݢt̹G0MEo,َb~`މ1X Xssue8[N__6[OW^$-HVW/9d;1Gkhj,xd%_thL$"hYP VqKhjj I&p8A PRPde {̂u",1\m|ա'А.@7Q'Lfg}S.]]n\Wߎܜv+;RAQY99򊖞Uykkshʏ@ht:~'țѪ^~ZA3x:{FX0y3 z$X=9@`׮HIϔ-N 9[d5WROKq|a?Z:FPUׂȢ,_Hލqx' Bc9+ɓʍ;kZ-ԋ;;NG-/쎀&нS#W~S`=k3Iٽo.bxb6T+j?k*JRŌ,Xi񄉝zk#YsUkJIq.GZIYHUX"%XYV5׃%y$:BF6c)Y`scfE0GGuCax }ϼ#YlYu*4)Qq!sSOAh/E}EP!'G\KInE2` G# pq4E<^9ͺ.OO*,|(Lyq@IDAT8XaܪNOo"KwzhkOO8_zG{;-4U$()L"`*4c>y/X~TCaq|2qvcl&WUa<iByCɋQERU) ~8/bc";m86ݕZYWUko"%״pI)&qSp[4D&MQ0q髪P^ZX@%1]y L.$X**ʈgGgIqs,H$}Ӫkr=.qiWLb2n o(s&,YuIhϘ;!bMOr!PU] FPha֌]o2p|AYn:/G]=w#y<G` d ^pFJǟ{yt찘dݹc <}(FxonBEy>ųx*--Dmm9k28z5G阫{6o3YKBiWw#X{ %_-`*5UXij}5W^xwqKim3M6TFϺu=?_Bgڄw }UV\,^55qoV=U&INnڄ7{gC jM W鷨Q.bꔎBEG%~6=z3/Tsp&&qdibe :j ŋApF ċBߎȊ;b57DTQ|μ} j4[ 1( u%UlOϊG#0w#{^*X0LOGt-ֽ9FY( }gw,.X(<#$FϏ bіj!0 ++sDtj»vT崱 Tj"py5W/cL}iI!CC90qBym%˪$5{iTV6R"=-U5`m+q*j\Cˡ~o&-zdo'_h-9xjmMH师Qh.E%J[IT3`e;VJBp;'qL*gI0'*RxT֦a쁫dą#g:`el?u}i8:7&5TW>C =3aa2O`!}}<.rVRՀ>ر?u-Tar58yz 5PSt/>e>RjPI EZn~8,Z쪄zw+'alԵhTu 2HNKT7wһVK HLL* V?6O:D2q]k_="/̔$(is w_IܾgΆh`ؾg4 p\K" 3;\17@y\c=)~zX4DfBd1ŵx(4U@v@0{x Iށ&DŽY%w۴h6豜pzCqqVq8À@#=5_Ƙ5 *G 焞L6F^u+c$kqnܦV33MWA3;c:_$#;3 I?:KIJ;88CYVkIIqvr8B-`颅RD+'Y6#X{}|{8)sq2Uede0psz GvCf(΅Ο ÙW'6OO?>.RnNJ[V~Ale^n'Wê{E046CSbxݯThkizSPRp5h.׀uՆ!Y;Vpj`UG#00u{w?(&~W/#5%\4ۏ+b)Ul] ,U݉ 7gE76o |-[qR`mgx9[ۍo}-̠8|G\m[w`=mNJIt\nyG#L<q8D<Udu2x7ʈ6:BO V~Ԃ1TWFCy&LZocL{M- 9H8}PE suM`pm?aN66=mz,̐|::t|oowԨGc`s+RHc;E!3V61ۖYё6QNJ .5UP1{#Ǟqbmayw!gdy+Npd4"N)/إ%ńS1\&%)_KoX˥eFuG 1<  O6 ,eVX`2)*fοo r>R05AP7qAVz ,3Ko\;_FFjj0{ܵy,~@uUD"t#p=d!G#0O` mE+ ҡ3Ɋν('`nݽܱ\t@v03Zl?F2QS\GM(;U"]AYp0.n£|/"ڈ|t)!n>ӰEo$&`o.I X& I/BuM9s 6qFA^N!5M]hji#J &&zZ@L@]M54t[elyŮu?G-fkV6j(h iLMfTHGx ϟ?s8C@CS~ՠBi sXgǰ404wkbbn#ϵv7]glmЕWz"Yma dVl*ja!~liUDPσb KEn 3KhҚ+ :hܴr'wh G#L,E&' 24 >G`"nÔ)2j{*K``՝ۿeK-4[:֙yǾ]:v\dG%k^< x" # մtfwf 5"hjJ`ek3f;VG͜9_}+o)4PR,ɏ^s(@MU#2:KЦ\[$[S^ZtZMT KssI PZ6Z ʚhk yHʂ~EsD. ƁgDn:*/F`QĄ@CUmD;>/'ʐx"YwY曯QT\ww13tFDA`]p*bk$RЂ =5vUokkBϞ@m}EG&֨EcC=T4ĔZSĆ.z('p8B`dV'f|68Elu`͋G xsc>= bevK'gW:8a%ydh`~%SY+q8 4NP7sGEM , T%4{fY]==.n_-83Ү\&W9J#ZeInz*XMy?*~Iŵ+yaNPIqy. }=DFn|bff7o ,X}gH֯nE_~Nc… KqϨ+EScuTtF֢-xaW& $x7'G#ȏ@U%T^0Yr)d&׽ubݛâ)}pU2Ȉa?/ߩ:.l!Xu*x{J1q_B󈌺&zOZ̜\|G"X7Hngt:5Η#0`@7e v>[O 1(KV溂[Gq8Å`2c}vK* FVdq׳ūTqt)6RVUEȁ #3/o' 52X7ٹX;`:%RI%ݰ\Ђ\rb6gtX3qA--WX飦TmзSR7G>_H&,%+w)mQ]W.nMiY=Κ\Y)J:K߻spp\4sMuTҟS!: QW1oAO4|iKqݲ[5wûW,j&F"k!,dSP}jX IN%5t="TSզTu.DÇ&hQsgOE N ,9cRT*הydQr~1k2G*3ߩڻ n.{XR6h'Q0$ħBNB4%W[ߏb*OJseIt e&KU-DlIYisP4PRRԈ< (-{qb^ J[[W1sz1NJ=W%*~|榖nF+ߕ$4qkrV !g zo_46` X}\OD\Bpe=sbk[w`ʴk) #+_~AE[Fii=Q<]N 'ړGjla7z̅uɫ !sl׽fz~KG|| m$#+# #`LKeWw-|2fCPƖ~ƾ=1k/$Á[AI@su"|e焱NkWcKO h" *6xWZ=@r:zpqhM1w⹔ΐMrLlhoYK3:&/uxٚuuMr5> #CGT X;odKX|xIM!^+o6+dualツVGޭq;j*L5Sse&rЦm9f }s8BGSϾf"HLml_X"Zh+,u#⏣>VE9mVVעf۬ɬCOBaz#dLxCw!6>qzl0'oG>U}Upglx G#w#,,nQ>4zQs6|^/kO5[>ۄi=o}e}e2+@{p!,?Y&zX|u*}LvwqW` b^%u!̉,*1r7 oCYw/e7{G'hkjCM7|ePi6a*vV߮RDkTDRe?u^# \Plh8v~ eWaIqw5Dp9.ZثY/' nn.W]Ym JڼDǢR<ف%1PԅĮ_b .Lb.G#p8qÏx䱇qw>MG~/hUwJo0\v;2ru8ֆB+ iB\+@VND1zմiBeYjĠp7 ֽ1nV6QFjkμG`!Ī[/54UϤ G#p8C@G*rI\NI(>2 `ͯ(И/l ʥt&XY+FD&0,%%¬LeJfb)Xld\(I"W ^# ;;k \@:74FSSs`jf*Aͧ@sKt[P_S&aLPY002C]<I[Zڹ?ڌ MI$l5CW:(,ɊTb\M?9UҬףRb|x{+Yu;ܑkGO2CT`{nXf8Hr>s LL(kxEeky4e'5,F> pG]U5*[ʒL4S@uL ,,))tȶ<ꠇRdŞS5:[ZX4S}=#1pvHgr9ZJh:\BEQ̦O%aa!ojvhѣѱ+`$.c_2N~HK U$YMe .p EU!I G#4y:%& &z;WM- 0J 0œ͜b[>B%`Y[YI z(4`ۖ M "6X?eSkeZ82"wVE*Z{]x/ #dL!NquL}|0Ƀ{ {+O#MJHV.#/wBGGR-Fv{Rg#?a7G!>m gC Y|;By\LJGEG^n.,%={riPY[ 'gOD ؋p"eEy00$MJ7?cw.wl_@ RԵPR\Np[m>@ue#70駝_~',3Y`eRQ궵5DF>}E|/BkvqCm5/DG (j yFFF-k5p٩У ?/Xzxbkh% xbF~1ΑES'M]jj21mZp!!!XzuGvmPGI/8u͍5Ґ(x|l+ foqdpm@K :6:űK G ai۶@pP =zmgk ;Ϊnǂ q۠k`*NC@xe9XtJU55iCΘ+>yFu)|/)\[( X-I}xTg;d"w7x H)*ۖ.vn]~Bmۭ"HMd?ߝN <7ΕOν3=AVw,92LL ]=0Q<&EjjꂗDiߎ n|F۷Di05&碁[ 9K,(3_M<\ops;Y%w! gLGe~2s1mF0,lQWXOށ L: :*8wg[,bkm]v9,()-F_֮dP*)LDA~6VCֹy˃c7jGЌpr+tt QVVoFz `)`eן9~ =V޳S."PQcTC>w5gw'h5jGKH GΝr5  XIj2Y45B惻& lMY/NW?t(;5?={`y@ Ԃ@@ lm`*[(Iks:B &#_ʒRК~1~+$_9 R*+-w=߼JX{p׍OEAR-h>SJ̬Frmu#yϗz[u  : Z5xp yY4NF@֌{!T)ܿd%(Zǁ߮Y/ջBwz@+X_}}t^E x6l؀BL 򆝵=T(pO2@,V ;_9|k")6:׬?aTHIǹn45!l5aca'ϣ QYA83R[h(&%MQK Jˠ!O̶J$+7 5E8 yɸw+zopB5ba|mV]ƥ톼eMx3 $,6~|YQ. A4Eqf,MIP҇¡ڻ]ˊrq* 5CO0$Jr<\`> -(+.-MI,"-+)x&,,,->mQt_,Kς <b:L}ZmRf~X4/- U%:s~pH.h?5Eca%HB qԵ$=+CHhZ 6ZA7iO455jd6= Z׍p&vtCZK:L!H{k={Dak}U9iz@ RwwW Rl~Ǟx/# 'N]ew\Ƥʥρa>4һכ8جqQ9U`a6! Hѝ1w&! d7|l$i>Ij7 %;(U(`zw1֥Za~y#R30*m54#_W*8qD$[} 9p6z RD~&C_؈#'3kbB?FPN<|'{X6FQU%ʚNyq(Ix-MzKbL{S 2R{VKy@`B}mCw{.R/ b)Ĉ~M ce\9Vsal ^fP_LOGHcn>`1z[Wa~Ν!0jDz]=N a]C3LnYʬ_4R 7"%^yz=f?cw|i+AffdϷi02!`]Ul}?𛵘l8}:IvTêUQ_$7 `UMO#=%s*g$}'9о$e97j]G߃zwstww932 |Z(LnSMcI$4Ķ9|x=pG{@D`q@xD#ñf]褱cu ޾uNm Ѐ v%0vbc ͊' E"BVUb h$aAAla*x `e˵L.oNpǻw2 mEò '͛ b,`G9Аm[\wdK3:%ba=LN1eikhWdw~=0q<5FC3X}۾0K|Ӈ@ma:DzFDyqT$>}h+Pt+WcAKw'{1sk ÿ_D [*KTW7=Ca=TfpVKVv#ˠeJhr뚰1oNyQRe+Gv D"!kL =Dr9ȕɱYJФr\O2E2U t Tvd;M0qGDj6>+X:C^SlQ~OGvϚaCyTGco"]?Wd$km}hZAjm8+] ЦABx9nS|(N }շ`*D^< +{iX[ 6 1t7}>^x+`CТm9Kh' .' TR Wm8H?g.DV":{7İ '{Î킌ڸ`(=}$8l\}l'/_m_m|k['|(FvZ4w)eLm].Ƀt$&+ˇ\Yy:Gj_9@] asއFLA~n<NI"Iiwo_/6cr 6 UMJ% 5RH39:)1 Ot5#)6Iq wt;ܱc;˳ r ^v+k/Gعcq9K)i&W[F[m9bRdp M/&B5h{ N.(+- 'ҭ ;g;?|],]_.䩗Gb֬y:d"'TSW a{bb jESm%K(h'deuV!n meFUxMq=CY˖uy``g+$Ęe K(ʸr^ )H{>GD[g@ZIXbN񟆤S[Hfvb*XRrR+[{6])L(04UMF 9AsO$G˗ՂPnjrjWPQV],,;(g+/1GgOj+ut_8@fjc PC@ 4{\)W:tήWwr eгseEwфhfdA@v5 =RM*&F ykр1[yA:.KC ` 9a>SGW{ TƘ.@g셦H DWA2c }l`[gQ`+޺{pjV>Y7N=0%pxux?Gy(=p *5 a&5(9qT:X޽*MUR+c*ٷ{2_N/1wֺ ފjZj\i\?qH44g  *c B wv jJWp#GXtYÖ-[8uܹCm$o~Dzv~t*`x 5- /\sJ<:in%PE%P.SNƹqKl通 ,Yr:ox` `QQRMXb 'eV$1 a0nވ hCKU$ut! *5!< Rp=xn100,A/s/嘬xij,*3#߿TԉfACaZ+Qi_5HGQ68z>T<'v Ʈʨ@% OլAw{ȒPDy?ʋ_Ѐ*Tv3ZPՍbTQ'[j9 %()/~;3Y0$.C=(H;_dJ7"YK Hi8}`44)oOA\6#J,Ng'ӧQXc "q100sAyA]&ԀW =`U٠+Îm۱{w(ɸDbڊbhk^~_6p K5 :=JSYTv1036X v(*,& %P% (1/89a@ʻ8UVj:76cAIb)KGNIj8&64if=_ȷ@֛`m-Ek]u k%U}Vv c+]MS_Qc `U_teRM?kpuqR;i=աo@;i7h:`7o-@m}]WNhh{*{y +-]&MODmu tئovc= *'aVgj-='Fjzq((GII JilFj)1E"~)z^75 \җ*ߞ.fd@UUe> `^=0½<J' ߠ갡 P Hj\V )^^z\;u $3%X3pIG+?cӫ#`u91oCB Iˑؽ'kW==}b/# x8q$k+@6?TenRb ;Kߐs pp6a]8e3=,f`B -f5SEjf!FK)o$N C.› l|fb}'T;؜X˅hVSAgm=ϝC~2 gCIT ?'S$$UK mfHkAtVP!Ё'#R`MyV[QZӎKgoB:@"#@Z}{4ukņF5a;ݦ3NEɑ0555#0IL߄D󖮆3SŇoaѲjLWLwGqJ$^~QQ a9VB+m fUR#Y'<6ϡ4/A~d]>l0gcc]PNI89@?2pfyho%_(Cϟ®_~Bb _>{vvYkH('Fuy WKbWD_C7Z+Hstk76Fk+W//wb[אǛҎ$M=pCnP+fx=< ?][VMdPkFt2σY'؃7聘H*|El4}\W{Tn@IDATjjf.O h'h|?JizMi.XS X)*QTbT k1+-&&`Tp*Y,-pU/Rbxe8v/ADnZw") HUIʊ=[bϸKYU^Bb9$ Sf&xghg6aǹkڛ(o|ICp1őxC@:Dhښ(Iπ5IWT"#=rIi<ΜfbkAaIeH˯DI*8XL#|p=DptM$^W)159E5 J`߉vY svmSH^ k#ockW|ݗhE+ `cڍ+ #b #[M=LJi>~$!<^6K `Uֿ|?/ArT vYzTc$;[we%rW^U3g-itT!*yo|CMQ@V.[䑃XKtC7I^[8<qv+Oګ⸜f'XF}Ck}1 pð"񰃿ÂC9Pzm4ǩ2AW{"w/.{!mLP\L/ĪUt\JsV4` @j*0ݖ+},X *aL7 ekh[n3̳ <\>7!"֭7xȡphohnM]OLv]^/={=LJ}&&ܢtK0{eʧ!Tqc׻wCJR:+X=7KjP,Bt\ V9'cޮ^&˄[9bH[EZ#ܞAӯJ oiNJ&Cbn d :(ǫ/-ԹيHȉÙ7?o$+ &*̛j6 ' M2zM!ܫew#I]p6WX Ye-1KmHNɇrʔdN]*dQQUC& hC'~.hd9p8}*DgNG CRrG_epBYi!4UI6QߣN+X Y;#pϽAS[ g+C%ޗ{|8ҍx@cL KKL/5ϽBU'l>k<.{m3;o6" byu!!- er9Ak# {C\{i ~XqIУ~ihPзwߩtHΞMXeUP /K0 ]Zf4]Kmu@gZ()*9ExSXմ=\A7YӜ9yi 늩hb*A)_TEt5,MNy1Roa N;·cHJWCqYo]TN۾ S:zLؘHJm4`e7!@`1 mVƁfXQn#=A&gQ>{>Hx;Z\ /,ǯ$onsh4qi!3%ul|VZI^d Z`goFp V @ͧ85MBrT+KZJuuZb&D\Vvhon{Z/vG"dfÍX|}|tb`gLFl=KȅC}W5:gN]=F4I4Y54K>^򕳰vAAY%d*16 b X܅9s=' 3j.MԔTTQ1پ}? 3.Dա l ,S@JUo7`A߿gNPfgB"1\953-91j(N=k{> nx^ߌ^>b/Z&\+$AzɼݟL,e,VyqȚ}lO?`000 ^S|<:-#!6+̸TR 9v,kui1hyET)!fFc0_Ra뱘<\Ǯ(]aFjq,}hz{m?Qbђ:z7皮NA갠;CJQ^_:diO5kZ%\~ɮmbM\wKٴG\V>Jeu03z:\%4Fc- w>LaVv =p=)>YG1|Uy. O an$9ǭYaY0s@j*Cd!xOrn \e˱C8ul/N@'yVUM6`U89\pgUWOs 'Pf(ӣm K+[Y *e+3{gQ2k-=ظSNE+b~΄,(킵t|0j#]kv6ž;(TrwZW=?!O$_Fx#`&S3I=U?P6ZF-xOC$D쵶 jGjۻX]Je#h< X1A$p5Ui>R x$"6Rm&^u֡ [ Y}#)NHJJsboJdXBKOj!Oe;@ lWNFKAM--(--A zMiZZ2ZaKd `eNaױq Ϗ1bYY` T1u|1_6Q_LRmmk h҇%l @A}'ΝGXy  duvFъHs#Xd)]SmD:#|sO jp^Ԗ(X{ Qӆ-ă9g xlj"p sg xGG\9=z$U0û.GnJ<̉ѫ'S|\Yby\T={`~K8' Ց7 `eNhp)6V1 _/թX h ޤ puWQS]By+Xg"9,\)ZÍĒlnjEܶSq'SSNՔ+ G/Ul{،> <ȟ!gꚊ D&^sZHVj `jMg;|+w"od޵ }wڎ4J=Jl\> 8" kޚ{sUڿÆ`~SQZ!;q&)+!%Z1ef&EYA&y!xc(9;~uBʷw/յ_Fmpp@WH]l9 0s wvunUY9 (|K~S{98yp7ܼ{ XO- S * `1Mp6@aq1"ȈAny=C;הίR]sb.mKqpu&C+׭w`ie Z[9ɉ؏) @dluЌS@HTKA hsm-[ZQ,:؄46$3N,U p9'Uxo!hTL1K-$7 z~6ddb'RJAmtBrA1S\5e+1?-%bDtT(W3j* 1^qX`ݢ R1d9P9TeU$;n8 %19Y¼=veQv=fkK(!yk g^-wI,D a$ߛ P#kDuiH2 @0+nXEf-| j<_A|{c +Digs%z4q'r2+MHhX#hɉs VM`-IL+Mt5CWU6z utӏCi_X[hIB ػ}@ ypn-FZLX]FH4 ގ'Jh^-M[Wxh($"h@ԍI8&>$Mԅj u@t.Ą !ǎ\$)=eXr= \?q|[QVOҹa$[ZZ{կ/ec_95pKW>W%Ij1l톆{SyyH/E{S=ISbp)w '->#TJ.zyro^\WSc~+`h7u3,_;#"/3jh#QVDWdXȝ&T:b|r`U7ZUy`sO")3NiDp:4*%lro y,<:г^,.hՒJ[ |^di=zXk{%)VV2xH>S3hU\Mʂb16Sv[؄ T壩bP#ZGwr ԡD9iv-_koR\B㩄Ig*MMKXg`eׅG\ds{E%T3z+OeDy=]ȋUkLͭPЊHDJ{vcƜcfuQC~I|(,twv"! "sqȥ]գW+5ƅ"X9ylrϺ7YYQm{z:/@*cY9Iؿᙇfo)nN9Wtw0O섋#\0M>4.4okiwJԋ1Гި".b䪢bUo%1=N=SUXrW0N CJϭڪ0CSc3j!rVwI(m3Bm8$aC]OYl `5^$&S8N4cHVe=wBWk< Ϥ`1g΅ `Bd< pX:C#:b[s7C2vqp l Ϊ<!JwM?bLư(cbqf XSԒ )o9~+kG{=Y{z#/Wy=Ipr3X\7:meT˪LkZZ)cYBFʣ(! 1hoc[^L6X hvYM韩 ɀ^xeN;p96@h#f.DbYΡ5+)$|oq 33vз {w|5a+:1Gp2a|$f/YsdexMXt#LMɀ,JKI߶#!/υX,yNCNv.*̞7/.TdΉ9 :kC7&` )1D*cXe obUBXdlb%zy˛Z,6I/UVW#+- GK-_gJ5;D<6u#be #+ .+ C|1r*p8iг6$zcfh$VN^;#: yi2%0BGU@@׻~ 0h!J4Hjx\+nqi "CT)m&[[M(  0n/" $6Ν'cwJ}vqRS)3ѨBu̝3``(Ō=ʖ8N\Uz0' ތk7f+[z3X{_\/ YϴrG䁇MA8z|ԆQ Xn\sa~GrR>Ӡ|o Ky=٢MV뀦rr•14i[(R5#G)W9챆zdL {8 |>W>+|K̹1&a .p'x޻k7 YyE*:ںhoof`=w ܊%Cp=|Y\h!"#<<iE 5vfp;x#6%ǃϽ\K̵!''?Xt5%< |ݧ!;zx:421! `}mqWAA.ڢTAǘףvmzAUefA[~ <6Wf.ߌPtP` 4˯{{'(85%:IH  BhJh,劲wX+)NSm% Ukx"oD{:Got53<:е_ 3Yrwί/*oze·w~7g9vjs겟(j`@GֺN@NT>/[Tσ}={fx1Y8x!݆D8vyQTռ:\VZ߶fbآA~C)g gu;sz1ݞVB\^^$/K,99"@|2p`sXXY\y:(LBZl "I6(diOcisuNhi+o68cVT$#/_yM90oJ?MA/@E!7񯃋+Abd Q[Kړ4%bϞw[w[@ 㸴;\{kr;N*+ybf/ʽԜ> nj S=`u<9и`vȻk =* _U=\ #=$PN৶=s&.*,UamЎA/6DUVU2Bۊag#NFBWGfK/Hl(&J(ܭrb@<'9Q uRV;u~A>魣gHW;ZP%n\ yY4Q!%,!^|O < \.mF]}N Ea!zG#RBOlPU OvԔd|\gU[WLK8ZKTΚ{"`c*f^|:g%#]!}fwX `(+'oKJ5))|_Hڴ~ o>sN1b|i Jc*;{`r{*W=odo dM"Պ"R7<Ko5)UÐTix}=`NWkJ|֧u|.BfB80g=(HICasJ)TO've;^bۮ`ssLB1̬>û/t>T6 eY4+"s9 eLסb= ,d;3 XmA7a+`fR7zةB 6\0H[szpbt_=A.w=p# @ I?zx`fzXJȋS [Gw0u(c,֧~zX1? aee5T1~yv<4$` @jaUO5a/pzQʖQ.S he Vr=K x [k򡍈<˸B7f„̘usۺ4k:+,]`A90-B_~Уla&GtZX[XȯlĒ+QNҪ’ǟݻ(("^Osb>0tXEpGh_2ƷИt!w݋#o`\Q u:SY?{W_ű~Oݕ$hbJiKz_jTJK BpC ;mww98n 2khU,`=K8훡qrZPВXUf?uI,MmMhʃ>nyFS ϿR*/"&],"/Z U~U15ǰX|iq툌ݿcyEr#ܧ.udJ [b\1 =2.*RVR7}>/W]]2c_*`#ظqnCK6N7g-f֯d%x:VTV˼ $!()kX Ң4V `5vkkjC& Xxv;;ȞkQ,*'ɬ>yLg{&-Av$+m;ovd]Y(oeee07;~>+ 茚hNC ࣕBZ@6v hd ;liqyj9ڧY;[`- H+-U`XFYaabaibu0;p23,]VݔQ_;'MĖl534+PutuGhiN^YxL8Ʈw͒齲$AWCj*J1c 2GM6~YΗUOAd[N<,~M+g `nfҥIյ1z]0 {.X$`f`~ KU +8s&u=]vľ'ׄ|Շ*rݛDyRnbyX1vb_AV`%*ۖƒ$INn(>?zh)˃Zί>2p 1WqԗXnvR-xè PH@[K٥8}6|Al?\f ωdysD,X1@6u?Mpw\z)硤Ny%%x{ǟa0Im~$%ܢ#OJΪ( 6)% Y[KK~>NPk hԻNP[*gm# 6AZL_nd {L9T[&詷 !1攐ln̦#LHL>|]bϱX{n²[o1)N9ݫ5@AE J*n;?݄d,!hYj11t \鞍O7RmgoV^j̋:-.G`مXe(/9Bke{ܺ zfs1f#NJcurnv`#4cV֘1g-`[rgdI6e4ϼly>TׇǤ@53F?wt'jmv[?ԃC"Tx%@7ƻQXV#@+;P?1V+0qȋ>g݂E8hۏ=0qFCE!( (0_'b*c]߼x/[Цa$zu$ h5UmQP^OZ8de4Z8Ш+F=C{PMQ)qps"+WNF 4z&$ $9]V'%>)(+aG:\eN~"8\TLyh T%8Fں ks^-ptCMi:UdrGߴt9xDhL>Nl2]72XE}Z7Wq"5$(%c,7YnK؊~TisV,=!~[&z5T]LVqžhcV V$?Pq GW*gNm=A֡XM^Fnl- ~v.WbuP؃x=ءHLƴX47@lo*K1Kܐ+\P2v$ehg xĤbO1,GO0)rC'*ssycqb͚'Ц WQVk$ME?~)|ug`磼Nhxhl((@```u/X¶X;1:36NfIݺzfI9%XJ1L76qJcc'EJuF< UX,vh(k.XƆF4@;Fl f@@IDAT틌Xr`v0NϫٶeutȂX u֪NUd0$mR^-ܐp|/<l0N^;,aK:q@hL2y h[jJ6טqHNM@ S M6ŸNSNXmmڰhuE0/?(#QPI՞44u`h$سYx/ Gߵ5U WKSvE]fbJiMRze"nJ7v~i_3ۆu'Ө"3Ĉ1c%2eLK ̂F7nGv6b,S!UgX|.Kݼa_K[**7\(b_JOrB语r 4 j.Or ]oft2C# gOY]&;] vm&4@c_6o2"̚}!AO- nQ:'d&rXZ_͇. ;3AAA/ ÿ|U.K'fá!t66 5`onOo=ejۈ%sw+smWSioɓ%3!S[#z!nkT2d*}r -pI,PUQ7xjId-%!Bšš_Eo(L&}u0Ymo\Ub-.JCXV;tL6CQIQ- ej0& *6S %nϼE a˖%w´ QX\ ?ƴ32=Nݻ :Ty␯PYu)5~PqǪdg$_TM,,Lɞ: ğ&OgYF`ZDHdbYAPD"9&\_L!4!2* ^=JϦ;s/$20f=b@.q4ڊzʲ*R>""+ac 1\=(eIVR!aM&,œz`~sS=%~TwYRBwЪX̛)~ΰLtiA&&.ʭy%8@qP1"5q) [afj.>*CAFUQ jK>c6c4)1yb]rj `HXC7:VMYXeF@H'ŌÈ16k;1H.:f,ԉoa3(Ǎ7ߌ¬T8hG*s)},j,ͬSkjAqVHEJ_N9k܇ּ$ʱ*vQ6ׁ" 9ܫ `ϿȯA8]`RTG7w-pXeC71LX&HJLW -"YX^o|9saō+em9 LК톋+Q 5-?oD HZ2ٝ'aWDT-YskG01Fm6TcLEŢ<^pg ߱2YXFzH/b:L6Mg'XG:յcV j`I.[D%0hPS.F/2jYS 5Uz&Cw٪C;Mkƺ,r={;u˷/.)|2raӏ,k.+F̱#Kʏ- Xj̤w5&ZXQ"RNzꙣ`#3L9r1mej˥Ә:"Џ1jAaA+ދ3m]3Y8m0a"#0TO*zR_u {,,,ׇUnKl:-!1m~w 1in_ܻz1v8u-|@tT 7\8U%0UN=SL:_>{A`:^0ecmI9ڪRs'L(7 ޓf99uH1bl`G'g\7o~g3*IMSWZl%cOIuUejbwZ`XyL 7-A4Ai_EQREZ cE2-+nZy3(TW vۘNK&&|֑Y_Đ$a(.73Y/X`cgǎ[%.l5 o/ ?Dei҄ p2TD{m < 뒴5U;Jėfmyo|eټW{0 }KCui*45)[#DÕ>y=)YwAmϘRD=bO};#JoʶgUWv~Z<2;g1?L0*&XB@3)a{lrvt2c@5כk($+ vU 0zÝM>$5/3/ 5:P)PP8Gi k 457CGS|ɱ6?$4!36 041\\#zhljh(}%\,*JcNt  b I_!Wۗ8_*'#N|] h%Y9h:RE2|BfPIn.6 30w,4_w믇1JeHMɰX<2aZ K/@NV,ZJ2N!mjw1՜=:^2)oԚkmh[Dll;eի`:e >'`8_~y~-KMIAM%EV87s8j=8}܅LJ@gԠ =!!dېѮ `1#EHŦ[=X{C y}{̙8t83CQY- ]P-;2԰):4^fJwNAuF,ȚKnB=rOq +zo`s\TS֦za0m&)[yq,F \fE{`v773%':'JM)%3w*%*EyYjAzesl{mXx!޷}CC{;4$WAd=~x7Cims&˂XɉàGL}|Z\;EUw-0b,ĸ]0yC: (n{{1cþ'\zM>u>ʺH)Uϼ+@K=wgxmr*x ؉3i+16n*/܄h}G*Լ`/eF{$Z; _~Ĕ3nes^!e_9W?[}c`|` n}nFg_|?SLfۘtǹF'TtrBI@ z%}sqx Ux6G%ǥQՆ9d|1c:B} q~ݧCw *?VԹNSf ʋ ``b!+lP:g$TIn. `0 [~FaH.yG۩ƙD%sLKܵ~֒ˤ4X7qDT/{5 )1CtX0ՌSOS&ڵӮ'pG7sv?R70R3E K8̻-θp/>XY[ub[OG'UӎLёf.X\J4&K]0=CZtX(,βaZ>O , s'ٝ2LKgAvӳ7m4t(WH'J:IP|}=߾탳HǢ#qDzqͫ2Ⱥ`vD0X]|&I(5k4@%ţ[ ӗvV) ۪l]G!++_- Ęv!6\U{alӿgj5%jy-%J1P;O|Rb[o^y{N>cH#>X`]һXr áC"حq>dI83h+Y1\@@504_}G[S8#)1X SS4Ty9o?:77 ,̅JC)uЧ?&j;i5XUb1 _p|}{GlDfr4ȊH7 ҘQǶn[ SayV9 \qD/Gb\ŦGv`W ރӌ;(d7 S'!""NXJZ${>9 bPωG}.%M:lcv":6͡5t/Him ޔ*AcH=CvHo}#w޴^vz{5UACba::?U0iDZ 4UUhh̆JƆAbp3i?';!*;=yPZIplo]b)CRSP\oWoTUddɁaYxx#ccGdx$<8xt;~=s708`֫E(ñ48QvN]h!*5=` &{O4/sğٖИ X]&7=HwRCaq/U8.ݲQNkdC<3yk(ǐ7\_/ݺAbTmh1Ӱ]sJ]4Кa.`ޔ[wl.򩓩&}?ʪ꫼W PdXڞv퐳 Ωo"X1#JSWf߂/,]al <F$'dű]?~-Y_YQT^+$Ğ)I R Zvn*|H럚/ ,ʎ6Xy#4ƭ2dmEFa yɴkb̤GY]'L1kTD({ \=XF ;l?t6?c.&O<vdRB:5BSմK%!G73Ε.KHӍ){[X<.cF{I~ ^C3a$cghObEX4n3Wv6M@b5K*=I{tCUImǎc΍tH ^ނŋϙ L*CXsXqaۡٶuV]U{l蚠\--PrOWX`p---tO6Ey_B kWޅ 6"t Cy[`灖|a'Gock ҂,D kg5?d#S7ߦsqhjk‚|::]J7Q>eE8%eUuSgdŖsS1`@*Lf/QTR`Rc&TojtO?.=|py1w魜 0BZ缹1h(r#4Nl{\ rMY !zPUYLמDu#x veYj89R|5APimרjI:Bk{ |T0$!=0rsP}fD;C`6.Vnk`,o '+#pr3>7U85=DD!>6)LGlN"=72ܸ}[-#Ϡ~}۝oc9qo@sWs,23;@ LpW5 #3FΛ"YE:S'(CORcQP\Ƹ>H?|XXcћr2'{!:>nE hٸ('MȫNstO=l7d$'ȶ` q@VY%aA8%Fz>p\2i\fԩH7/DY~"Ф +s6~gkN'p"0t0iRv jʅ9ָ{Mcm9k:"mS,+GOj+Xٻb]JZ:SQwJEE6&&QB{MQ׬'+=ury*!ᖇk; aukEldF`'kK[BF.LXǔcjI(`}q ~T *-ذ;~hSjy31)p !)b"p}mBf=9Vi.5wuλ ,^^0WV$X*jW~"y>{y!~+BNiTP3V[iq}.^bl,Z]]߾w ұݻ!0`#'yM:~#7Čr _$Q>|ʭ[rK]E'zIglW453󷥄rtHQy+dL>sYW-pQ(/-T ey8NO7|.bI9 6CZ;#t"ݸ tBWUSr%7K5U#Pw賓,!bm= *$J+INuײg;k+ -a;BiT.2N2?#7WuHD:W2el{tV:NniL=_\?"]oұ B)N,+e 5Fo9W'>D :"I)wcXM:$U3[Y}\zʪdڟs}wy:J7q6~f5w/ߢ{`%B_$K>oV_)mw7oFu+zӿAց>SO<6$_CI.j1KrRyr -XB(jhBWӈ?G&J.22sjn]3X_C[d0g+UGoq.Э_Y#O^eߎȚ͘Hĭ*r5nppr@mm+ʊ*qPՃ3N5kfM?߉02ATX'MGn,N;BU$˳v셽#wA [B05H![*yo<NDD'/c־@Vq4ZK:IHH >7g<_+rLN/7$pzyذ TSadcs{>xtU T1~xi{Wpo~mg-ZTn9F٘ ,j@3܇>gZQ0>2 Qah(HE~A qg7(Q0Fpd<QnkRzPW@)>ZO(1[) c2Ң\yٔ^uv*_F[m1 :5RK.ﶎ^GlǞCۄrMv&-~|\PWQ̸,Xzbx&ģqir;ް J*Yj)E{]ubuȉ(CƳuN~ݸg9!^r#u#,8XjZWk[c";)gyυ_C0N|g=)k3Qغy^~s7?HV;W%iYTТi_,Z2w; yY8ܽ}%Q^>z2 !@AQCZ!]w2[ =9(߯u.?H@rJrou7z'@' <8:tT}%tzt P]>$Wu[Z@Z'yŸp<ca'v:Tvff2Z[k 4a,H/*1S/ kFHe2;[;>0eCI{Ƽ[4>)K:L3зva<=l#YOU#0U\u7Z^Dl4TÚa,5б6ʊp& *(&Cک4ᕪ?pƸ``D$W#<"E98߇twÍ7,C=|<گ:, +uLM+J ,L ێIKGyr \+h.KC}A470n o'--CJIqq73[O?ӧLCzq  : wO|ר˰bL|RIG#kꘫvM-ph Gw(Ø٥h&ftT`,Bjr<4PQ-RTT(+cVxxcQUU}N IaMƤ,.ʥi,Y1LbD+J%5-) OR*ҿsiIngbu勆xu(ll+/cV!Kmu|yPXA8s̬4ܶr1e@mN3.`1qޢ eׅ5m;<8AXv G.vl `lFϴL: Ye{'moߡYRQd˳cчdҡ)7رc|VuqĬ7EF3GbU=D]H!x;ʤv[2HIuyhD ǷXPdAZ9c'֒{g!IO;@CO^* \ʔ VxLju2 vu)-E{n;XJ`}DZPY pVJJge3J99RUs0~J@_U_Lv޿*>r/Gs sJ_π[.JsgmIz/-"UT5zV b==st353j8Isc;_k] [D͑2[@AW` cɒaky]P|z$~nq:\_1^̫peN;tH&9}K{uX@%௽u: /Z@ΩuE;ͥq"6%hiڃ{|o86Kvu^ev6 lPd &QūOQ&|n6k*B*$i@u4t?5 a`M}M(\shux-C4KPVU )9i Mՠ͈HGZ,ԊP8g|t1&p5A~vT| Ǯk lVݶӞ!gO++ a M^:$ܸr)~,[BΊY@\"o?L~᪒֑Y/7ZW*N(̓'/^0zs&kuPh1wBEIl[ *]^Z&-.RRcNE9=R3p@^N.n[u'&O9Hdi;g7qmdX>-KXyoeKutW^V{QE'w;5ιͲ`gg׵X爓'o0lj,F1EM,g$<+ | yUƵ]ݫJA[4 `u9ѯmgfzJp=7/25L8YyCÎMt*٨ 7nx)P]n)060"h<7@{=(#HX"LP\[}Uƥ„1RQG)0UJMDw hYC lLgJӑ{b抻3(lRRtЭH5λeFZxlXY%Jn݇t3 k(qPmP~M9N$# EDGyKf}t 2ULyIS1F>N݂ܔX5Tc̄9Xl W1ǥB7!9 hL6VJ)uk5fz?CFy **,Vz|Nt, U6}&`k7`Ϻm#5cS/~qGw+g!+u;&90&\0߯V,9!-i̓"U$OMMG y[@n"HZҺ(UM OO"ZLTFBqܼ6XƱ; $;3 z+bn*yB 9K`px_~ ,= 5`=9/冢%c7ʄР@N-W\+WE\qoíGAZX׎8iĄ1qP}p:P˄!F;T(NP>NӱF=Y$#h2,g$P T"+k&`+HPGNp`#41nsqVS,gm D' -5ep4oFtxlձ( B] LFϟʒt-C<-kS6X!6-t.ihAMa +j=16%UT"~uה,'#&:za{ȁ?ѦY\W[\Z@6b]a:6|_0=Ycz@bʮV,P/^*]hހGݺG=RaQ!OƑÇڊ6ELFɠ};61i/~y )o,1r2=v4Ȏsth!K>F,qv`xXc_z%%q.չ052**1065RNu (AKG MM0l(3VL[R"(VCu>vfԜa[3عxLc& &`|ƻL̠o[Ѐ/n2Usot:y|*/+7g:2&< ܁.'wĒ[Tʪ*< XD: |y[V'Ȫ>ٺZ04FY_+K?Nc>e6@{ƈ"(75'|"mhk84 xKK׼x7HF^U'.&BxG"*0R;2kPKGSXYq"~Ká,y}f2ȏh{^aP u]B * [َFrGaqj֢LJz)t )~١Ĉ8u,X6#TXh9]Gz|[z*;xnVJLqlߺ Nf|^ZӋ U" k㜪"a*;ML8t9_*̀ p&N?_>:<>Μy\bIve}x'QPP^~ >oj o6]01ǩ19:.ey-B*:#빯U"}'8i3mUŹI s#UH:IO[7|/: L]ASٗDXE*ʝpKHIaCWU^!inFAMqػyW<(?V~ZE?@u#bn \B^q?\~=CCTUW ,. { Px^XЮp3k=){h "BB.-vd ӡ°¤ItoO 'DALz6FOǰ$c2)To//$!>1qk1WVeKmֻ D@QFYfkk'j`k؂+' 4T˲0r@4ҫS-dqP-O$]Fb(0ʎ%9d||scPk-,<1t49d 5PRN&ӒgrY7Ug0E:W񫷐z {-ƪQԋ@IDAT1B_L9cEFbSoĭ ξ{[ӛvefa7_ˏly ,^G`b `Jض ]cJkgz|: "lX^cY?zGrvtdݾo%D]*ni-\)mz~p]CXL`nsNDɖ^{g\ůw L)k5䢆:t~<5ʰgw<- r \. ғwA3k*/kS  $ld>q51@F&:wM5[,zSv$u=&|-XDw!¦}2iL0טы,CTW8/rŒZFKs*ŵ jʈ F}k;CqC5cܭ'[STJtUlMGd8W#`hjFA^6dޠ'^}z#ArD8FsC(>x= Cv8QiDĆ\`9;XXhJRFT%#-i(Aˊ p2K4$`ËfqtqFfz l\ZƒpsBi|_Td,3PuI9?0etsuy>ǰN |T7Ct&9F+ωFz= 071>,̀3;*sN'f5Cξ}mo1CqPA}K`T`Q{&h,o2UERKY5sZ'3O#fml9֎j$W΢~(l< df碶F窧%f#! :uwoeD>zǗ/_NJ~W޽ku(MNz1y{P34妠筺2ULEUrVwcE67~D@6"1oRMe*c RHe$'pwѨ}ַ"v!35<Ï`߽zJX^V|BH9Ic=NL_29t<7)t_zVNIvr(^+~Ƽ$2~*I#KJ^~۰|ēxpzUS^K.xy\rɦxz0mt vUV}405dL'ό VWrv{Pwo~m,7ghADɡ]&O.x;x',zY-@VUE! ?4_ߵ5tQJ7cGT$L m=F*mE\P#U*Y:s\kٱY:C v0VBz |YUFJZ;X{B3Q Ȣ~F\͆j+J*ѕu Sd'CXJ> RՔL$Gqz`u[,ݲ !MY=R8n"Ȣ! s0@[ⲓћy9MEw{+zQWI[tĒ>:QzKĄ%p'Q1Pl"ʡP%7b]`~Ԍ MJ2dg@fL-?" rY (LԀ) egS i$^y]!_KCƿ.-)6tKz@q†a#@VkssY*]agL7mDOk3sBјl|Hݻ7!"pJ[ϻ|nn~j4 8x tiQ!p5g*Zi/{t#5U8F4wt#lf NTY 9\G޾6#DFv2U71+5#+w>ɡb?r3Rc-FpJm u%EX!%3 ٘ة&dNhlPS}B5 If3̤Zܾ<򤴭9A م8^~̍ɳyz@btd6ˊiYÉVVrD2ACO PXQ'NA>N(NC!lKGXPm`f~V55f~4$"z?5s'kyH# ~ @I?^SІ*~f8XDڡO?^d$%OIF$Z؄[~ak*MF*ESM=~"xd"c⤾QcGIQjiKƬ# E+˽W`/e+VCsKG1]T1/Z_ =prxQ9n1?֞V#. PsMAeqd~9Z:Md!}9P U+`-*gBtb#kT {cZbL+ݬlʪjͣbII ]=mR2ޯ,Mru<Tq +V▕ieӒp0l?*ZyNZvOoKk}_BNV:z9Aj؆˯>x_"9/-)GJ"eh'$dԖ3zs~^9maQ AR-u8:X2LX:cY5<0;D{6#P͈/%Vy((aa54HxxJJf O+~Ե! h置TҩOOLP֬pK›XhMhc/;q^+EfKs-23eo+7TV_EV/ *S f(-0mr9.KL3 ְ)n[ 7B̅myiG7EJz*7c\ln!s̘ s#Mh N< @hs;{>!T#7)}B^EoR*^U m\eo}{2 4xrDEF#+RԂ^F&rE;#Gpal߽u5x̂>L,m?Q1hĝʊ2GH.s(Dk*i5~`:&,>ܾ?iRU%'0;N/3$X(w`NN 52VbijBlb,6l`@kgG70VL(/*@dlcấ+hlFڬhPQ8AR7/%:+hXI6OsrĉteM3'X &&A%}>JP>';B+ÃJ,*K$y_/FVRS1HW4LlQ -*`8mK;Tu`I@؎*_c~TɸEJa+IVhojODqv_hCkQ "\Tcܧ/XϞ9b9{NMk SG.\:T', U֩cfz;M:x||E^WG9`@sH~ T&:F5H9Fu{4ԑX̜w|z9 3m-[7+Ϲ0u3*+W0avZIPjJTds;Z*AG[3??6WҼ$|˻oEdYCjwaoU =0&≧+&&CIKFlKLi-roO|5'L Q85wȴeTE@9pRIVxF  끫`osLsXuBN큋>tƒt;I^Xw 363v7**9GulR1o%`{O<+ VQZTIs 326WV&Iֵr`UʾȽsa ilk2cmrG%;{ۢ4UD+ >{ 5A|A> #ڳ#"jZut궘dm]V)n/fFn0@kި2!>_=JRK~W(()ACU#8N,]L5j/zXKgCӡcDhc , ̹`cf. u͚B?Z{#j_/}{ЧnSvقL .>f`f˗@g ftkkk13]*+C-rʚsh C1*p Ʊ@!O>Њv3"4]2;ԑlD`_{?d"ʫX5pÊթgS1?W56(˪P]KXiIUg(u/NE[M)'u8[*aL[WWggxVeSQw>W|XKX+G5ةp8ZWD+T*]I)kUT 9]0fm>AnXp dso0޵oxadCIMK+H!<+akއkp[y]&I[G`LNUvq;&:R6Uysf闇 uDdSliHUKagiը&@ZWY gOM?Vh(a ^*KUbh9QRź%Ŝāhe Gj~z4:1en|HJZ 8}ո8 "@[Ff2 ?2ZѤAN㣢TUTUT!9pte6Р6l0RFC}+lMh_L&:xh(DnEipOen-"hqJ dؔnMaG8㒱H4jI.(=mn*Ǭv8kRlMK%3+'Гv&PaeQjhIPIz?*mdNV ?'.Uک$ntҢ|c/DE YT l^lt0޻ S'tuu`c \fTTV@9-+nͷI]0ոz9z|$z"ZwY k~K/<#NZ+qz3`qIXb 7a͏[‡*[FF;p?_?|;+č7.;W$a5P srQDhpl+v@vXT W"0ً@4 ꍷo| }yݿƢ~$鑘GU!`jTH&_t7 u p6D"W{HQReA"Vї`MMMֵw=PXm^OGm0%ӛ@{v`iX\I"- U ]_Bmk!~3w0:` '凖*ڀAJM{٨o lplu ]z?V+ݢ itW:z%@N_-[H'[ؐ\mLa<sYRGI…Iet=^|@_WXOأ\(.mB Sκ=mpCi G_AM/F6@ $:~N{)aaHf] /^,t5J&zZlz5 "߁y7AZ dDQ:Zʤ^ohhjZÅQ/\XYHӂH>od Bg $՚8CUP:uSihtIeufs=FG*ʘZLւ-0wth(bakvP1`3hk/yl=Q3m545NtG X82Ebv@ie Jh/HA[@&h+O?5YػV:g=N_:MHWu".|?76Q'PجkT>qoS{.vx\HYG۰_C?KȋhJdы n#?dCij/Ԍw㏏U^ Jdn4bw\-Ul VLwȲY\J."#YaQ!bhIPGVf'm#pk9@ń_G[չ 7gTxPthLV 7d9g4z5@Z%&c޴`qrH  Q5MswEn~,]ߒnuh'aimp3~FEU ZPe}~Ͻ bN+ǟÄ֒bOS-=hw9cY[˷WxbON{.݋i K ϵ `9z(B)H4L9I+$޻=q2N C0Xh1}*ޥhD7Fڛ0wbWM~1Gbvp& 2{Iys0^{p]3+5iEmMm ]:`k9}pnvZjjMEg51/0-TCFT5!"&5jy)d?j{ cx$/ŠJ QۖBuWTŷ? Cy-AdYqd\vPQVL/#pfL{EXBw'C< {$+4z*{k =B6h;\T.*5ֆAN 0 -MhPo8ب@fdG p7ѦT_*0a!QЈ.I+4)!Z:S0wIe(Y|ʍ(Uե<U BQZ2w߉s<0︬;4<errHN# 4z1s}Qbll1!, fp7tV- _UˁR:lY&g,JGYy N`e mb<䦿K k43ZϷ7D (~'iO6ȢF6ZxLQ>َ(wdxO+>xz!lg.L^Mzn!G'^3ut ~5!!׶sÏbn pu`XF>x1zۘ߬Ee1I~21>z*u>%bqA;T\RaȃjMtB q%6y**mk3]i qBzY7>>ۙU=^]X'|]=qpNM0ūO>qZ%ǽ~#|4u P]M† 7jj*8䱓9ӡrꢛճ#2:%Z)1A Bѯ΃R/TaW7O Z-Yу"UU5gֵLso + swAM=ho(܀/i/g:K9F &}-e (P-kAi-B3ʭ̯-`D\ս)?$Ȑw=J0fJ'9f1 L|=uጧ K{$"맗硼"_GӀu¡z˥MU`f=S1a*{a8XGhQ{7 u󠋫y9uއ7K0}Rd+1w 9nR\EDI< 쿟|mROqI܁P`AAI̽ѯICFMy14[@5jIRلt+rŃm.f_=:POPŸ??wtDl!KkCOG"恪Ja^ \,ٕ#k9 ZY1>,BJd~SI> OQ}Y{y,A `)bwM |)sk><* ܽTQhi= g}[% ύ?Kc%#TPy~^&_B[G؅,*0K5w"5Yv g@OWعhoMGWG'&M$;NeOfU\M[Fk~ 4۴-ApiӦ-Lh:8XOIHĴi3}P҇͝2+2hl*27-OBo`6i1s%BLS)I"yGִD:AXua@9mr4$nfqyNؿ e´I׋ 5hQdc@vc~Xx -LΚ\+~9jċo]#3B>e- nNTDBS -0ڲ<7Mʦ(WGI~!Y9 _֠4}blNp5m]*mIPVi)nrFD~b‡VʓW.CEZ +̞:5>kG[#zWofG]17ELX6} <'Onhc{6ZΜ;CO^ w/ocq]7^T3gٗ_R\ ~?XH3#7+C0$z:|;n!UlHPB,*je"A?&ٽg; {c̅3ZsdXEN5R`0Ǽ?tf8|_ tXrp? KX;#*VWzu*Z\nϭ 0gov܈nADtN7X|ח__>XyA_)’]_ܳnې<ν]̎1)T=aX(N7ppP>r&s21c P'RIV-ZݶIq1n"U5΃8dFW۹g klD8CXr-z?y +Bqye>հ/> +F]bGDw8#,ĉk1}*f.mm S?ė߬-<~MX2h<[Zc\O1# %y8u]rY .. Fas+==<RpIio3P[H:ɡ3E=\26hXnld)L ?r+&D)q *S͚};$p-Caz@ YiXH_?:ZlytZ*Bg߸ƴ>m6ູAH*FzNJ[KE5eHL@Z㱇c_v+h Lo7RaB%F!:>aQAGS'˛\*-LT6U! P5s7"S~v;-sj;<Φ𵷂e~YV_*MtWրept(nq1'0u-\ YE+AT$&"_bfs+np4O*~9_,/G(㲨uMUii4ZrA6 ¬ ìffX-b%Hf8J@:.{ɉ!diA'g_bbP1X*ly^&ֆxQxv;˰ 1޶t7Tq9{\1u8+1"-^'?uqAKXt6$Nwc}(Fc[9ٙ'H)0MUzI0^~EX2BA{6؛C.T6S)[ԖJ\*XNtomkdgd>"!G?tk(KBkeIJTfc…P|3B'Ҋ(h^c+Kј6hTTG]oWP^(#0`*qMׅJxbEI1P,L2nx.ΰqq#:ƹ'cz~zKRl SmZZ:2}/ߤiU׎_cGPθ6dע.j C%q ~1 T+Ɍ Eb;6#.-եV+vG7U6~N\ `XM>߾r=0&ʽ{`L-rب!K39?ľAzQJ֙AmW'sCE*+zxOdeJ+62k w&Èu_nXL、9*}}(bCI.5@oՠj?\LfB وYq'A%}[6㹧GTttc[x.hDhG!d\ʞ1RE\F֜*=|/|bHI^[]qEK=km] *𢅫1I0^EDj2b)ya-p6@]^ӥFt+= !dPV@MPd`wCy1?a͖E id_"8W^^ŋ`d 2˒]/:~䕷?=OȄǶTUk'];ۈCby^&m TeMV! tv; {Qߗ Zm6E`Οׄiny VD8?c%k-iڻ|,Yv]^s='RDt-ϝ\-,gtЅ(%9w3֡l]=5t`X|xx{#!6"8yaڂ眼;wD(HDO32RZSQYf#H25ZZV Zc+2f h[JtpֹXr:YܬM$tgW;50yUP\lٲr"ڂE-NYi%K1sluJ>ڠYKq0# yQS0A|ZU`ض>X ^v*p@LY3z;yPVD}2t!9#akS:U90<*  ;Kv8~yXEZ+Nt30bJmp<\]FGRrY˛h%#Z`X,h ٲYngƲ)cSGZ(L0gb3 qw1!ێ:in}m mVSFv ZY1!g`N<$@IDATRY'8݂ >j OG-kY1C'q>݇k-ޏv"oe!w>qnY36],&ӗse"%Ajj* ; FKL]EǤ޽u3^xiĦ$獛**5O.;QR\Ki/JO+\3*D{%3HC÷mwsVhh2䁝 ;ƺ\=8弻),E;3 )Q10L $⎽AkM6^&RcQz=pupF4 }hdd7w Ek!eݚ v4Wӓ6ϢA=梉&7_{ov0$#Մ_WO,Z١ga%@N򇥿}Jp$/6,mVqߓ֢`= ]t-R9!$?5T᝗'3~ FB߈tPс!^poٵexO3Ïk머3ed1(dd5s3 ϨX>[90{# 0 20w|_ص{/44aԀ4:^C{7A,0xO?Cs,i5ke;fMSTDVW]MaNG~#wȜhv@3xUl߾Ҫ&@MKVƻe-u8OrYv P$~8(l}SF^fꡮ F^#A]y Wހ/ 72fvUm8G=ZQA6-ONeNMMexɺQOn so/~ U5t4?\1ΙzOma[f}6tqidei3AU=JsW#n43 I6ƵhUqrN)<erVgFIXdԀˎtˋb=@!%i1c{ח8華5Zyq'Y,'hO? ܊oOצ ƃ SW'#ؗ&r"r<,[&F.%w̄u2S7q(^9xSp@_qvs6m3UZ* Y(IGze=Rdn5sLRU_{e (伳1AF0uA;I@4ˍ 7NÝdmx_ρ]0l,tPOu?ȆQRE>P㓏u۰ ײx vFG4!<>:B_#@NԢ)I05`k9P,hJ(.o@vEe=֥%;2tnÙNQc&`t u=RR7#$>G8u2sKҭi 4Y۳( -(&QZGSf:h'<׍uI uuXlF#ߓ*j5im]ֲ4dX6-0@%ToUِi2ՌLcl!SCEC3Up.#VsoUhFJil?r9~qq!}`B"&Sٍ}[‚ʆS(I 22XѦaZLAGSomHkxLOFA&NpT4h8riQPR/܋߮rRbG{.9@/2_6 ]S9ڶ R OyZٺcn =>ߟ|6!T1BP\&2tlՓE\S6;'] h+42;gam^zZԤ, ZMA9)i` ʘ*T†ġ]\v;`*pM˙#e͚,֦(4sPf*NG?²7``3 j?kԓSvcQE>>6=UФ߀mܗ&rөnvafb43S%[q0qlT`A*VQu&7p@o\,khBIE.ˈ PAjv!zz ʙBLK}T`taT1Y }Mx򥷥W3-6Cse92p~3\To@VqZqy!, !GFE-8, k@̘95>XE]߮ pN}8͐VþD\B"c"`zk2/U{Zul/? &#uHnC9;F{g;eN*L0GaD?/~ON1h[ѪC|<+|G[j :~u#^WV񤫧8:.ϝz%Besh/ QStX: *ښZК3ѱUbml9*z8Ʉ30h=0so=0XZF{<- 5^/EUZDCeL '$2%! @O=:?VK/ٛ ݏ8!ԆǡH<*KDg BVo/*O!vp%iYo^m*LZ}Rq͝sPmBGov*2@SW /)Ascy.Uf4(ܼlYgoʤx].Ǵn$UO=OX~Ϝ=-͍&Z c=Uu"I5p6V{z=M R{x+s9 _A4fvEZ=aejKgCŬ#`\x-#!VFc6=mtq^^GKݒ\MEU0bT* zRAENl;~h뀦0uu Rjj>ㅘGNE1s96(q;R8@;Z`[xD F*ڷ<407UKK5]QpRD'thnne%$Ihύ1>PMt~؋trA?qьkoŴ6'dc sB1ݿꙿE513RxVAElj8/-$1fT #d 8hgM@6B9o'2u4Rlo6ެeɌQDPT&B"nwC Pn*6 :lϮ Nzq=={;Cȝ;ű-I"fg,.Ŕa+SïU rѾ4,2P[8gG[J>V+궏&Ңڈpq+Ue+AsQe1N pu`.`=N+C9*6 9@#{~HSɰ$iŨKyV![&"zJHԐ@VNϦ-aTCq4emL_߀oljQHLL~5w9&FZz<՜v;bP7ٝi3ќ3gnM ̤4ž}{n&  mˏx[ȑjض)٥Xs uwKPPL(1˘as2QQZf0Vࣨ >i'=x[^; 6vH< )݉EFڝ*ZcO?"-A3'xgܹ:of]VĴl<+qMҕUx.;҂%0s:NZ1;o!9 i3S#, Ԏ ,T倵K; jj u \}#sQ8[k l #]Y#?n, /PX,*n G~[_>osٺL1hTvHJB|AR:f F*t ]_|>vI"A<ܼj923%~Uuty.ʁ#'򦩥uZF~nAVo%>"Xv~V7pڈ<[ ,޼qvhV!mKv:)C*} ZׄEK A>d (oc` ~>PƎ8.q"@+'!-t=Z|(\ [c PTӆg_}84Ĉ:V>21DIezJo|6uwoBѥCw݅]eq_P(ԝI=D0mڦ Lf2r3W^kZtC L UT Ξ^T&2y,u02E:xNr ;9 l=Wbs/"=3|'A ^QbKX/̄|O?GZ.8b{O35%\S<yE@cLCs-0oOr{Jr$տUxd?6v܇@MJl .0-%^(5kh1T`Z_5rBՉ[P}s4<6=u{cNs3^Z `lFRc圅&ZT(׃sQ_ 5C' WygZX8h#PARϩp @ky<7U5vC}S\+lͽ"PQs֡'i4@d(q_%fxx>Mx?"6TA$ C_7&*щ6sXXih)Fۅ=1\_?yIx-D+ŭX\JV>Z|>cH8"io$UlIj,,ζܒ/_6{C*=$mPXX|/J:sv"=gW0{ t15M}ظWV"7Jizkq7iDVepK˫p<;^q4֔[O=x73bAjl CΑ>kin q[@dL>F'K+f +D *cbbNKxͫE\{kQBi?1UYK#1ɵkq*ٴ񃷱qhn*r,hc$ Tj(oˇ7mZ|B҉DcR  ف^LmPi_mB 65I[z ֹ{%{S}d1Ȓ8O;CdM+ܶV8;ЫcRN2oKP1,< Aѳ9 Z+h-#SI\ <:hSko32ѡr:1"1LV5 0Q@!/kD-jH5h ngn8X㹷,uYta+) ձ t+/ȃ2=hD>@-\@#A.bUBJr͈&ہ^ B:Tv.FAA)reUHdIjO\$ڸ$ n({PtDVB ?e|wu(v9#Y*h.-C1a|e5|O]CvкZf;s33GĞ[]G{*V@|z/2f&(kGw֎μ365ezbiFz@a{m[6 L, M*@ *}`nnKY&TegeViKaJYe ΀>KlݹE,wq 5qLT 6d{*G^2<.f('%P+j{Q*jTcç/0t݌@ECKQG GmnnB[=VFC̊8=ۚ+NƗB } iy32- ͉ fBZ=JAf& ض+˩z~o'-B) j !8by)lO?'x־1wCta}?b27Cqv ,M`>??(:FpcFf t+֘xA .vcr̔xUvfdf~YU|֚@rNg@.lۓG6fx,޺l9m6~Ch-d ">9/npe- w1'4 X*PEX#3;ܵkĂt50k -1`g ڵ,녓O0,^!ǀV8^"Hr;͘a', KW?POcR5S?3D>:^+˪cm-Ύ#N3T@78 DO'<** CDZ~]+Ӯ\KTt[Aw?3Z,nMAA&ou 7܈ȉ3k 0D^ۨbNmpwtd Y74=ѥme&l|-z˺y-nd^T\nXc?"w’Fgxn7㹮Ve@VChRf '2Y? ym>aM7sy++ Qs*wFv|b"\]Ƙ);QIbQe)"'{L Uh<y1GQPA pD ?TZ(Qʘ&ꨐqRryjgl>}gk0 wwArkiI>dM>>& k N MMZRG>Dk/U }%\S Ě  ݷ߉߷Yi-vt<ΜN#5k=ise~^y6k`Q=+_NG%l-} ص>z \5 P!ȡljKW9ucU"%NO?-+ÚmM6l5 絜W/_qaZj#}ֹfO_ݣ^ynZFK &^:F ~k4$5p3"+.U1G'BVڲş@7cr*ɺ]T([!/̓K8JKK "衶J*DSP G$ [C ?m̸ rLD]?[PD$ݎgQ0Ft:ZY %W=(7KDD2IN1-QPLn_ sHΎ. 6ۇ>>taA-8pu"#RN)I` -KUw^ jf~-cQ]-'5%R;:8mK~*5,HVaaW@Ou9!TBom' ݫV!؍R#Ҏ9R Ft޼w K$7?42B ԰ fzPmlh5lj;pۂM֩g㠖N*u<3`m M#3PDptG ʲnVnSi:$৘߼YK%9J}@buf{ l]/Bk 47`I v"l1b@@W_}w d 9QHYO&Wy)n-<<<`lG^m=u5Qn;DlUT:d@VpmTj2zWKU %U`k`N*PGr["#h|c*nd*Ir:P-Uᩉ17qoԍaGh d瑇YT HMC8f&CN\xVT` \׏E"GK8q~m4q JϢzY:e4,n}Wotf"n~¸G6;6G`U}DŞ@`8PVQ^Nf&Y{z Orn `=/xG&PIylj&Ӑ$8^|ol@BEr$"~cx^ iW,4` <=q4g`gu⹜ӝTQZY-Ite5òǝTL'# 3Sfyrzh3tCe'e\%'x hoFF-,CƉuu'R=J$Aݜ>ygUw+.$(_oחdU8S~і˄!b##KiSH (z_辝@H$޵Ob854ڟOO Iv`Y *STiIq;H35F5y1PކC"hŝaKw"IMC{QIU:=;A!$T[j s3:|?$kQ4|󸓊5H1,q'ћ+(L=gvs~NhT+1SP&bo{yl|sB/jp0 M)شq3Iy/]T_GMȖb)\TД&Gt1oT ͺ Ș9=jq(! Nt4IE:AVorFbAka*)bsz5<§CS`J~2hC֓ΖCZ$Adh ~ϛKel~ אrZ+M%Sգ VK5Kc7Y_wpxrT5+ru:HOJKTQFQ1qhoYӳFk}UȥRT[%  0ua 鎨%]_ZZ:O6pG|:ki9Yɜ־ADB@;K swb$+X xAA+~ DP&oXQo2A8sL3y"PQMWr}l%eIv22:T@_הnʐv6yTr.jhz53eHWvσ[W:Z\ fEE 5`FG2CG9QAtS-VE%zB>ϻN8}Q{kAoZy5h#7@T 30']ukN4#Z^K}jRUTC+}&Jґ>mQ1G˵nZj "3oO ޶3qPV u/U{Gwns甓UKRS*[1P۸z,*u:sc7oڄ;W߅'d^bcr.[6nUtrCCQSΆ;oưVeE'LM G{\>Z{/O?wA!V3L.sm48IG֎&xy .Vo-V $"GPS`a }Dq<Ϝ?TW Lg)S|!7Q*lip}e7bf$sx Q}T/9Jm\1}&BVr4k-?w2>ܟ[%#{f4l^.ո!TLH`H~31)u@+t!//AT嬎;h\@sdXZ[CfcJIM=q-F=KkpQlqpt8'}D<&MʉbmO.ФWǜ+ yU1񪃼f` w^|뎍?2GSO WΟvQwCv@zYL:sp'9]Շۙ[!ن}\vV)Ra gU\Z]uE`V!&EUuI-7/YժTx+8ULE mjZ:3hmaml mԳ(n5umwu;IgaIV1 IstD[hw/^eȤa}sʲG/xjT !gkk{T+9$//f9U$N)[Re+#$(lDU>}6Rqk}τ>,bE+fxq8},*Ui'HTqY8";5cxRgRHX۾$`M]gĤaƁ|{‡tDZhP*o @fCbђhyKW@7.`mD/m_kS0jDh9HXs 9<:)yҵYE5c>sXlo:hU>V~r`%R _ h+"ҏ1Cf`VdM`cecv@A׀ty}E&嬏weXT$I R"R)®T*\7aݸqtߛ:MNUu]#'Y/}?LjT=?HkU2MC=jǺzCàFAdRjj.9V ؃{ f2kp8*k*1-2c;|aBXUk(Xc95PRZn TR\Q9稦n-Dw]s7Y/bց..<.qw8Oщ@ 1ﵞO ]Lti5]ku.0g+%݌E[u*˨(W&tRO'OC&@ܶi}IERy7B"Y g0E0=r6 =y)#XU4אts!tlREϡdf Aj8f38&o$0*.6 oTz{K:~TaLfH(9-C#YFbV,=P +KF|pl 3'ٻlTHyjhZKwJjMy݇EIE jx0˖--p3z^9wt3Zs0f"cZ/1 y{""tԨWWGFFYL@X4ns/'ڐO)U /_#;ˤ*aa8}UR>z[]6t`TDT YVP鄞6ƳK 1hdYc`\ߠcvF&A =[g eUXSR251m hhaRikkN\L\Ag7'X|%~z8 'Cӳpttę18y0.Y}[hjJkRCKaomq=y%twUuQIOMKڛGpcw #Da2ٞy{Iss xC`DK8q]Evؼ[dpv%HkFq &/V)9YhjE9 x7XXj.9X6ucEm_\]R>a}G6O=~vm6($nnAeܜQ|4SǼ OTc"Y'6[‚VFp:<8p%n6\eZGtGjd2ڇ35ε:Ɖ =pr0mW3Camej=iβRtuW?}2Rc r? &,Of8#T )YɓQ*HTsGUa*|P\F`ǡV0<|Ώfص}3:p nfh-=ettT:ej8IA?}F`fّ]2',N,27\In*:cvþBb 0򃡃\LaalvɩOAFʹ٤Y3gaJ%?K6v<_%oቲ2 z++8Ȩ14 dD3F -%8 viPo_"k€kijd:Ĺ[#{Xs y7nFVWR6U{~``!WxEIZ=o1)z>;y =SSUUItd!U\7uuo5]%} LI&@mlgz*{(Pp@T  tm8/~HX2FN *]ԟ/_薓xch}V:ĝ}0J: 1#Mo$ٌ2@Ϗۇ9e"E_0gNN[b'lp; ~)͠5UU8ڶ;Bj#))n_Rl〮4ۨa8tտUz/:Xwt&ઃT7* 8G洅 ZT4"9wv'AB*gIa*Tw*M:!7ɲ3a RFd23W/9q0΁׿qrX[|$T`r[w޹V6ݿp|F%[=^l=CeLa< |ڢewqN֜X~ o;}|Ỵq޲'N@ c5!bhr^cƎlS3si+Ma;n@}yVY25CMztj5)Kds6\XlԕE`@ .]p_Ȣ}kdݨoFK\W v#3aV9pq ;9&QII '%f6.t[oCJ&_*=>)VG`t7_Re*08I᭜R2*+]hax|]{EPYUHw;ұNo '}u~qCVM;X4͝+SaN&,_&GN&3o#-xy,|Xz>} Ҳ qaӧ†Yūv9=w˲GFzG.UTBۚzͶRٯI p60~ZkGcw;9вmP#? {T,NrsC铭-2V֢Yѵ: [7|O?IY2c#mr]QSPݣI;A*9Ar2g;V#8ɼSd׊H{s>A=u&[6z<~|%-V..y8]*r,_C1ϽCeFU+_ǜq*֗^|i(0w.>RUe_,F>/B՞DRN\{4 Ӷ\/΄ e,(YΙ/̇u )^;TddǺbD '^2DMu iEe4a@W+M*Xւ63;ϟO&GKY[y=rY̝*TX k +`΢眴Jy½֓TNsO^K ܵMfS+z%OSGKg} Z$94nlIr6 0irAhl9d.E4IϨO>D̹K s%_| :w~,۱JW vIj̄C+X$9}ŵ:jX{nh\w {%uRN4 <8I#̵ԠAe`Vs7_ZͲrwNqc,DGU.XN(e\JWo;ƸZa4xOFO%= uXib+d5+)ȭ邕#:C3Kj\tkNgM7mH\?[q ,T:[R+u fZ~u[^Z8}^N$v7wOQ`=?OL C6Z{iNLsFkХWn]>TN*Ѳ̌A Rz"~3Hε#<N)Iɉ$GsYL-lO Wg`Aח?`uvH!B\ k 8(/;~8 FX "ی2.lDR &fzj|Oku71vP﨩\|f-Dqn64uHy SG&wd3z .ZZ7pr@x"&N u.}T:#m@L-χkA䤝B %2ءf&\3l}B߉_w+ۿcgZL? 1X#KqrT:O ]WR}#s=Olg8O^.lo$z;UGloypC^JNZ>iR*w;+*/C[Rw* | mX^ڦ>4-/H["d*u{Pe/v lC䄩(?LH=6mc2D3Ed[)$že,ˡF柢̰ms'x""$ė{)N&$!x$l?m2/ 9(c1Y2R]l^8`ޢ8qx?&k"_m ~#446PG%E\O5w0~VS»T??uچg$uPRKdMsZ]f~Xin$ė7m]m ur`d|JI"Šv&-%>Fzlv6b`AKɋWPI mus:~R)ȫ0Tc*:tfsiBئ26Am($[߄ ;:`S] )`K_AݽÙtf:SIJ*lиY wf<' 2 yeŒ2 Jf:3mϠZH@=HLM@&̵l`V2*F]hVI+$~~0KՑjg^wdг`.N br/嬇U72nl f. *HMM8ڪT4}C11i0VG?Mːx&T9c9gփO([<%njeB*'%U-Fە{@ZY#ZEm}E+<>(!kdZ>?{`Xe Y]xwr[nkVv~zVTJl7 %pbllz`-++Ğ? T?їaH @i;cf9ћK.;h˖"A2o_;m6s`@"+\iLڎvՉrs tk>c63P %Xt˽,\_IyI8p޽?!.7wxҪcԙںTlȚ~ZzRH r2c PL5}-O fn1`p,hK%ݽd;(^1^{+zzWhoYM.YĦ50'`5 9+VcIQkNOO -LK1tdRVW;$K.CJ)Zj ɃHk6=T`¸ flGﮓ@V-wh?mn Nmǎ%?EA!M9[K#%}狨B=H!Bf`cDkצ.̋4"'P= }2=k(M5YiO$c*X 0ԗg0؊ɳV9%50vL%ԣ&XuVg9k2Z.WH,ڏ!Aki/bm1b%2V*++9ƙ`lKnŧw \ڻmG7EY ?~.Z3.^4f3ڐ#^0pdOayv<51vР6'4&GAT:1dAX%q47OofA$XZ'nn9p*DL"LDuۏ_"L uS0=2=P :0{1\lGA~) ` d%CBx8q7e;k?lxOpQ]fؘ8fH^f j r_;8%6jfTPr q+= ?yVTpp5.9 $ R=[#|?|+i#/f6-i"Y(d m=fZ2z:8хoAZ[fdr}JkiEA}E<31yL JKʑ(ey",d%nK F27} pI8 U'wN\=$Ŝ<__I/+@_r6oM5 / XūFv"\:Ntym͔P 51 <}/LzN`~l#e X@}hӆ AlFBt`kiqx ~ L!ԝ dkBfʶ`|M!;|(_Y rq\:`\]nl}dUUbE+ zhlz6ȳ-*,){_|?'ޯ: ^18y'[%Vwk!.W_|wXtCxQUM2&݁r(x1(lK/]H83O{ #]`U.gU+ژgZ2tIzrDn]pbݣ^0Y7<1S瑧i`M:mG]Gv1I*ՋVyDTmH\L&E*w`)W%w%Y>u`ϋ[rESIu*E!/ISej2l"gUi,G<c.qfKC[qm8-lĆ-{`m(*2w @"-)&Z:e2C#.?ߔ:ATRA,mŮAzX 2ӱpio؎ӧ"9?lMnAxXVemJD~.nFǸ6 CֹƺTd nXA$'{1` nV8oY Rb"@GqGuYy9b\YƩD 'L1z-:4!+FXT&>*qj^4crIa"#=yǝȏV醢0 }t:oATD2t B{qoYu{{hD]\TFеJs8NTE2hBBa ME(/6 f,:FTZ!'1 cgՙC Ʊ>:NZ;QaBX|ms3 1hVS;^]q'"W&h^U9ڴPDi 8owf]dZ.؂Xc[gDag _$+Fg:5thI2 "o7Ŗҹ1w&0(NiMl q*Tc[Z@cveC>Me9-+=h"Yτ ;oـhN*J : k:dز~,Q8y[7RdVYZM̦Yb%¯0!zDSdW!82핛7'[hosDЄ {rT5Tx-]Inc+ࣺ򶟸L]!C;m)uvn[@Rܝ$$D|ϹaB v7εssq0Y\lĮ% @]'A?MHܩD_iOG\^1I)"l%h ESHd'&-ݰwt㽅7K)oPh,<0yB1PҀ0o4SLט{}Q@Roo2Z^]h>T2ۀl桭 ;~Uru|P^ `bMd\%5-=9/h&M' ]H*K(gz5Ja$ۈv0Zqޢ0\*-,@м|HtvFt},`u5gXD2x[AʪFK|K(*wns_ZI댶#pXž\-V?|/6<*"ކE  0{&Μx_ ]iH߷~\IJNZ-;r}AcL *> :Uz4QZ?Mp(b"Fi V 3]OB*l j hTo0|%}. QXGjbx)ڶLOԴ*/,/W#btS}xP,+_ށP[ZaA,2hM|N$ W/{cwBbByT)0HBK#8r29RW,w=,TfݮrT3ReE2j[u%5ح ά8 up [3ZxY Zq U1:M PRYA)KW/9RZa3 ̪ 08W:ꬅ\7WoDޱ Xb-ESpL&լXԎ䑈I#mZkXQb *[ UTVB;Uݰ2VxF<`]XA qV§.F3j4a+n~`.MtUJfizuX4kKoC5߉>3EkLpX C,iNqJi#㡲f޼p@q?VL6uQ ̜|{Wa-bأښsf(+(D-mn$j>e7C.5X+aww1VUz Y)c7aU+=.?0wCS[22i+mq]}ߟ҂k;|K; #4=G@Xk2GX{㔞|)iq,(L,kR:00f$,CYIi`x\hxVSM;#VmכMQ0DBr.&]<4Ү`mO=4;w;fCRͻ`ذi|m}C'o%8FRd@`hTrHų bZMMIjhj͎HBycж5-&[KKz߈ݐ8 <} \EX6:؁Z{`dy&RyP(-j<ުhBl>yx/ R=xor4A 1e\dY5(nì鳠@&߻R٫c :m-rcR1GjF3=Tecm Efy3+b4fOetnaPՒ!U)!R!}sp93V˜֡2[n.3SFj,37J(H+ׂv*jO8HKf/ K`IBmK|Zў@*ƐTy6TQ8AB?B=Ht{g͓,c|=lKTCXzz&{Cbax-M*;j0 ӧGJf 3iiuKg>&8ҼWlS>^nn y9޹ ~y5qDWt YXsvBdq0Aj)^yd FwtGch B}i>b׾p8P1Y2dYӮBEY74'zi#T楝9VA=8e|LV:ر|b-]g7uS+g޲VVVs?Jv5dٽG.@m`+F8^O34~!'$b-`US3VeCG>e. UΑ4ƥ @~XTCb-:d/pf+^smmd .Gj꘣ĂCssYcho gVq` E^"Gxaftk{F%O d~>3fqtz`_$ spb& c%gH 5<$3ﵛ}wkT(튬#7ksX[SpM8h(a*Q^S\XRvP^ Z{ߙ8^L{n?9+/>MuB>Od:Txձ~Uq*Ө$3;@٘%L:c̽ _BOPKh=?1w .$9jItsn[_"ՠrHzzF^_|S:?e {Q`e##x€cXA6k*!BW_‘,ǫ0*D{u)4GJҢC& lXXdB2U\J<--A _S?qp 'vXwp /ZI0no!RH-Pf*PE.T3O Wk"OT(?EPn 1NyXhd]bZ  XڴJi+'R X'ƍ4}GHbB*T '`Ⱦ尺r|FHJǴU0'?iT[ƟZj2$B3Ks:oG,$e쉍 g1=]-4һW n^YMQHqx"KA-CCWyBnx #ۙv֍_4V`eJ}]Œ`AVw@]D2sOMSñg Vՠ :Qك.7"%\q)-R2ZC2'߽ *MP\JuyZ%&c܏"Z!}}Gq@FL0Kf!FZ׶xMB/?%m" ?| pՏ0-w(h`Ӕ)6C'͈6:uRip<FT..nm5fP^>S9XСUtrƹ$}ӲXdSMjjHB.Skqغupq|ˆ2Y6nlJW= r\; ')))Ȃk XOփ8M1 giށ%eD9~NX g! h\XZBKVFBh}lmg\h+4ԑTlӾtе@1W 馗˞Fs7wd޼.$6KMua`5sK$K\Ⱥ`_] ɭw1 7%8zFU4ŀ֊X2);I#3WS!ո;/Yw%֦F|T#VR6fwN5)_ O݇B*<&JdQhZ$N.EBj|\AUZ2O V̆m)ai:id 8w3DU=sW|xQo@oun$zC@IDATBg@U#I,⡭Lb$^vI۱m]:q&׽[9cx*y#G"DS%P4G-^ S !a0e&8THްd^k\{b`l$9Ql}=HMJ܀FA֑ScVbw#~b2':B)1C[1}mMI~iU'PH'?`;wy떿8{p+Y+Ph#b*<z6;h`<a77Oi1~JǓhD |[:袗f7G ?m(p(MʊN!M겿pQ*1*}<T@߯wꉥlvrS}=[ {bW,;]7*>ڣ/[L}ƃVOU8tvUEM ZI̎:|:誳rJS' =F!q.t%Z€tYj y !Þ@*4&`Y8u22bNqyy-8J^wg+ԜAfQ=?h{[,6TAjqdHт]`M¦ ۏBl%գ1t/+Ha(#VVdڢk`1MUG ]ю:..~E ~:5DW2H5*D[sZ>SCLN6P! , \Dg*QDux7[(Vvws'fPefϦdt2ӕqvfQuݒxai)TcXĪ'\*2K":*o؀_[f bA;|SJptqZ(th/ҨN%Iv$nK*[-a)?5dmcg*E䠁hǔ 풡RyS1ޝvh k0TGGFޕb8s.HLH'z{ƫ Qo50u0-YMuQYL"me9;UF;kڄ?= ˀ`D++.!c&Ѕ[T]o+q_\@S[R|M}A@MXofcQRX(,zĵ{d#:?U$槪֭+7EŰtzVYO;0)+7WXvh1`*/a>B?-BX ^Z%e\|#PV$hMQ>'*Nja=쩆ttfx{n#˜Kpr qd颔5|X{Z촠Ů>V4x ć'x0!l6; q(`h0? (#3M}zyO30YlphO*փQE__rr3yeϸ)U>*.DjKfOC<-C᩿J\¾CXu8>U'-^ǟyUԵhEk_ &Y0꫟ J꒍|NuDE$!#HM $FԜc}d:\_%M|qX$%;qԙf "x g޲ %՘9V&mϊJB*8h=Qqu%|H5QQ(,*D8u!H 0 xgF7ci5, -olO@ -Nvr4rXxD-e#4qEglϬ_bUd)MIՅy(eT,ԡ>82ՑKK״3XXfkc¬0vø ١6Bin6٧@msQAPœ6Ҹ,g>õ Hxo۶M(,, ş*XBTpW߶* hlkaK2 xsH@9( sDSfA9N-XL{i h=/箐 VR Z Ih|k,A !9%v|֣vupU3jC3ϣο|Jغ9֢ vpq@Vi°lpŰ9s 8x@Q*%ammՅTP%nb7t nWF;+K2m;kQVQPEL5ڻ%F:Fe ҳp>1uH$_*&0%!2TD8me@:HίGynQD{G x1F(Ju{Ȳm,xf^s2yO/D 9!1!ޞdz3`OAIFI kIujEh'HHHSsF!OR*F\IhkPM=V Pln]Y= So߱hq`.FYrU_]`?R^l Z3 y#ӔJ =ox{#ۿҼ7QBӂŋp X~e;֟dŕ:](Xo`8ߑTHߑ[YIX<d=փ-bU?H'V$DɣL3 D$tK{N@TtW19eUm*UQD;Jfk.qbՒ ?'תg0rrQ^>nX׉?uUq*g)'m *rІEKa@aW;+Y/ JTir*o$P9p}-$mx9v's_$ ktI'߾<6 +׽mUu/^J(s??-Xt`E`Xlz#M[mڳ`]w\5wAaa]+ԞY[1ے@6օK2#,x9Z׮لXmym#M:"0*NVb]bvP4*ahc e@ZsTj7D8/M/%ݳ2*@tCҁVW٥KEnc\Pxv9$z7U되,P għ$KWEˠ'Uuu\9~-]-EFa5B]Gg c*S{YSUgBztu ݸ95 h}kB:ASsI<wd'%5Xgԧp==Iڀ$Iq/LfL$%e3 y9glS/u]7²6gŸE]+/Z`;ka"ĘK@̡~s3ӵQKj- IPl%/ g.&U=ЪO$2 d-D,Ӷ:J"gSU¹͘%=> 8x Kp@Aj8Z^5=m;ť2Y7EMXxHhR!iGko Lكq>ލHQfjJaem뭐twSq)F(Zf rsC$4]t*Ƭ3l93g&J/WjD+{tV -g464ד)狜[vmڀ]7 t^-H6%v wЁ~} .㖀E)g^G`ojnQ`V7 dݪVWSҢlXٺH Y_!\,;#F:md<JECԙo%ʴ<ݵm6FXz2њ|Zb"77z2@Tn۸k"*U@ 2jjk zuhSbN; ;8 Qd?Oi P@Z**>[ I7el@qmm-;Ҳ&I 랝[NA`##DJCAȊƇ}w&L~ X;K5k>ƌY3x{9Ǒ{ѧwW=^76D fig{cxAZ'q@'C#g4vlټ!J1(UK+rѩ!=NyM*vLHF *x7t; \4;C -C_} ,]{9OBP8ڹ`&;v#@I^=,C_ &h[ g\6ݘp;xfQLDU-,\OCT iGsd/xQw1@$Zc^5fyi1gգ.m#I]8} hFSRlm0#û<'Ed1_CgC@p+ X|mb0 F6Zs:] UE{GB%thdZ6?U+5v^'ئOFhNV2$5@c.\/ k ITϘ5 |p~-x^ mjX9x{~)n#x,CeתP瀿߃JY6z3>vZf`hH R6 }D[t(G6akD=H>}'לpBMܚż3,MN$XXΆFAVZ]{TjwG8ԢOTV27nE `agZGLd3@WRʨ6pQ`NU26fޱ.l\ IsԓOڋom@+bt6I1,gL7M7]=.Xѕ^Y~ճ=~'UXjZ~㥗_3Xؓ02N=`~x% u =YYIxvN`dVCZg 뙄kH:nf2WACM v޼ġ!#'߅H0iƜE;X?Jtp'sN9pM Ool$)P``߸F ăG_P1ҟa\ h+gZYE+DrJpðҐfʴ68)&uo42ĻgC񁟨jCZV>R?0F#Gw›6 MddT&Vm+gT3gSbU՞CN5!?BAWq?g.>oyK `^M袺U]]݌"knW~} )2t3&*l@m3!5=%ts\٪|-J$n1+4U6ubʄt)s=Շػw;d9'A*6 F3-۰7*2u06ƒ@iJy<&ZЬ=腊::IB|0.z8JA2_!Tv2z^ ndЪ1),iB@fEGW/ǵ5A?4̞*B}m=x$ I?o A~T*ؠ1̷bͦ[ iA[ڼĜL`XIel3eVK 4p'O3"9.@C9xk ]MRG{;*X26Tg-*|`onuɆo>0ʩQP. u|em ]I.-ŹtqO7K5zODGa^e+,9({ڬRu[L UpC㯭^=Z[\t$]-0S9^ÊH ףT;ϻBɡ(++ "e ?'mus(C7 ڥT~^;.=T,[ uzyÈumQ۾ޖ$az Gt9{K@V =\Q%$Bm?XX |"H.-_:6߭Xv4胯OR;trkmғ* =v\ 80jż\-l"VGjJS*c65-Vg_ 3T 4c\`γ; *:k`NjK>J01ċG.viRUZiѴ_,~E8B5xPI(_yF Wa}ydK\b4h%@!+ ߈W[Z)$=p=#yUE|;f*e>vREҼl,_ZjPԉCѧ믿@9M"_DGum}{,Y~&koY4Ф N3Dse-aٜxޘ*d;hwj S1alXhx24ca"hjY"fFvg^rh %vMȝ5ᇈEW5|k/{0n*oJhcS&`L@0Њx$z\` :taꍳwaR&;e7'᳗4\iMɧ_;og%G'N{Pe5t BwAQ4}M!k*1'ndީ> FTvKI 焢Uy4dV@G 5JO ȓc(: ]*0oyC+ZS[Y1c'΃7N%sn(mn\~TO9[ZKG/+9%בܢZ' JO>DJ656Ď-?3ͼyfcAVѶtKcSxvoD`X2z _ɇ¬Qveע m7USFm{au4](UITWgc(Ck"V?lz|C"tr+W H$LI x8qUCrDpf,+Y<mۀ Wd[Wz9BUwb}uiٸ`;_VCP;d!ʞEĹسm ɜMZZaN{Y{6mwPE:jpAکcy͜0R^Ӄs*$ @Vd5{d6Va'l'+UeyTD2ds]MJ Np9]ztzx/^UYJ [ 5Jw+>'ELT}N7A>7Av3 b*HYEw#~W}?[C'_: i!~¿b ĦPͨ@ЁOn$CJWwOޚ2~6ﱁz<*X(XEdJV# V30Xgb&*GV7;["sƔ)pdƎ>Ufz? &tߡ}w ivP6.R$ݺwϝŗo$Lm4˾1OŌ}ub Fq2:fNPF2%`m*A[/QC|F?i2Rb$U#~X2Xr$i;6DW݇^iށx )VP ^tuXV]ߪ ss}kש(Hő`Uo+SbY$\[ŭXFܵ$ln^Z8}tvb~F3Ol` {8cIgY1N6TTݬ3Mcsc)t*o釣^/Ւ 63#95s03jք$Ecql7GbjzQ WF9ޮVtAܷftʦԥ` rղVh$.UJRuc p"hD.t1*璡ψ%Cm*J%7cjNJj[Cmr +hi@Zf,LR&k5 ~ NmneC,$SS]s$v8Uld:;;"$8WѡExm1ָJ2?a;FO^y5>ѕK_Jͩ$$geRY((w|%|i͐ٺq\Ig*TՕ?/ h;`ñ.!9ѝt}{qKYcz9h:k^mTkgMWsk,l[\o'{2CZ rmhUfwc~:Ԩr6ő=`Ipvr1('X> ਲ਼SD/wiHuFv#~C1uK['$ϼr][(#;>T;8z[/;:!cL.W$m_(QM\nd6] o?}HW*ZsAu< ,W՗17`C G7qHa)KD;t>TPZNXGTf%t"^< Ga%E#t ;5΢úb\X(,Y8'7{&Ρ*{du梼_|#f!? Cm*YqHGY(Z2?2b`z\\ X1P(vލg}flU?$3-)ڼV]/ eؑ{y*g-`ȭf͛wf)`hqGɎ+Ad9zcpvĵl<~O=$XS |!)H9M+dM2%]:ڴCg1TFۣ = mEFhbJ`,}ȰNG jT)3$S\@1ӒFǘzKSPJVj n{|r2hSOjT.jFFsO{9j]6u+#}/-JJ˩q0%1=OIB0Uá !,9=t;igc%%3}zs:$;OqMH pDb.C7[|s٭Z^pw"UryX%dTz? ktC3 C%t Kg :M+.:+"P 1^̽WQB{[bN„}LOi) M6 Ȁ#Ц7*x+@Đ@l/d5NG- 2Ԁe&F!Зzzʼ3K`AWs3HB*@7?Ef0Y]9H?Ks{:c&L b|e̬ *2^TRm뫴fױjJSL hD!GZvR q &A>&"1)emk%@ORҞ=;>ynvNvhP`KF˸o7l`L#4T hNPYcH,[gG[ 3q{fZ:&NZo.&iBn}BS)8Xj#T"Uz(lԞ5}񯯡C}Pץwb1^JD\n#*!٫O"1Yħ؈}ƄX"GXt$^elܶl.EI>یԂ%^ښE0h%:=6HRٖ$0!m8ҁJ ""ЯMQDXL9͈#655>}Pw8^d-gc-*~x;Wsilz+k)0U7 x啕yNw.|Lvn䟷C[*_\I7?fŽO8⑰kZ-YGb/s `3^㏤B#0 mgb+ӡ󲠯g@uLkm;׭ą_kV}`ϫg͇$2(d"&> % I!ʂy(%H۔NiÉ#̑ kږ 4ij{xJB^ Wk$%T[>3$Ul_6.k^y 튒w_~H tӄ=U*pZSSݴW_OzOs 8{U?GlFT2䳯#+qHOOq^^ .V\$eP Vj D>Y&C3b66 fED޲ r4eGNӲY%}=$N]w9~whbR~_P`Tq ؁n/]ަ*yA;mM dfmn7:$aG^ ΞO;DK GvN^FeY0j9цw<9P`` CzՃZc+/BBӆHzDGfіj/oNjCPV \*$ PK#מGyC7f?*0}IXpN:@"@ Kf Iqe[=ߔ"1m|z75AJ\u<3I2,<9-R9H0hJ[ٔ%(g?üe嚩>Swߎ* #VCnRhs9=0rR0qKVURT$(lqְ84bO9T71 aUig O?% m–7p@Aϳ ϾqrT5jV dyÛ&sGX7~1 NTDLR*kJU-꘵SH;_e="hB2Bfr>\ -=_|)pĉSb~˴iqm~}epp:uXT"dCnLnoGuf*ala=zuЎYԃ1iAeH}i ta3H@Y7ⁿю,<|G̾=Y GL/{opؾ/}%|1FuG@iq Pa]-ObɐQmEU4IJ$=„C鐃wYOfrvBp>.syi.PvG8JSK;aJK,a@An;H30 SǑJ'm$lAʢ.c^)T T֫ EۺyeMٍIq\ C;R`mrliÖ4 bb-EJDPIoo_n+j}l71Pxlj f U~c'<{9+LG-&YW^J!zZ$u33;<;|(gqEStSVa२kw _a8=tRS >p,J|JѧL~N2 uko37MQ}ZP9Xɱ>-h;QW_I桛N[==|ETR}1[/rUif (gq7>OxH*r)thU3yM."I^ :^B.#pN< t 82z* mazql!_F`-g%{ 7OdǞx~5ÞU~~ITΖUIЩ6mFWu"c8m-,QGqD4).ƒ;nCmY,_N$7kcb#~#tβtG`,W F]*4`8R@o<Gʻq'%e_IfX+fT6ٕ\= o{3F9\Z"6W`c CMxi?%Kަ;j;``h Pz"W=5Z<Rṕw7Ng}O ݵŴnF O KmVm `[1kކy.m[)Fښ@5AueʍxN0w} c/$@.v<-~Y!8yv}\瀉x m"rKd~L~}#\cN'CM&BQQVG%  Q8yW=ݑQFG+aц̼3rZA^**Ic}m \?Px&,:5mU*U5bAu hVAХ6fHl!ml[|m쐛]Q dӎȉ`g٩nNAfyLsjuk{W/̥kʹhI:&0;6dm'Y+p-|SagGX2Dž҆c@5*v]f04 daeZ~^hߟժ kg?] X<"<:4FGd(8,qL5~*2KO"VJ.7Zpƙ OkUآ@J0a~ue5-tcg ?y3j^w^GA#S*4P؈;Ym%g/~W4‹EO'i=~9& ckShdjN+s(PSe.DÊjB*~U%zS>$ڠh h=CX{M 'NsP K)eKg$%Yw{iP^ `3R]kH kNj,-*cS+U! n1Q0+pb:o݂ U%fT额甔LfR%j&9)ఒ|hUO:B3Sa(. Ku:p$uv:iDÏ0;ﻏf3v65HYWѤ ,VC:̨ mtRB4Z|QI*3ӢHWs*pߊ;Vtd7В)PBj)P^|xxy( ջ%]IKruOU=>z}<p[ [k!bQ|`I\ &go{:@RBO U!%)*buY}[?omp 巄)8 k=2e ̩HkĪExd NIҔY p G*2D2 R㼝B$nMƔ5P `e#]51jk a˖M;w> J_Z6T3'pk6TyC=}gȲ,o`%zy4sBisR_SAc¨2>e.)w0#.gODLb-d0.Z,ˈAf45;6і8 .]x5_+U*U'f/$Vx+L[YXxGGڰNEu[zO18) P߱NND5֕ro3)r49:&t$gh!!#vCFXkhN+ >X;y` 3B \υE͍c=q1Kbt4]|s7+e1^YN*48}VWSB]icL:ł e>-9S2Yv C;homfaf8M2 #qrg-8 HwkbNDC ݬZ=LȒUzl s'VXSM,^R~ sGqAl{dSO|S{p-W噩l,\#kEIZweU22e;5":E0 F'hnpqEc7g{-AOغoзy=<~V_J+|5 wIUˌF$dTӖ՞3e,xkNP@+Ֆͅs̝ԫj̤+󐙗kӗ?H1Z41]GbWȨطKe:hr-MD07kߎ`J5?IohCPqJ A9'9޷e6Rb_%COC|n}6,Hz(J ;-dH:~nTo{|J[N3PK洎v'T; A\qּ޳0^q+-BK_ogBwߕ2B']rsnXf^tsM\S|ŧ|tL>_`Һ#H udzϿ8d qx'c֗uJǻH\Pןq4(WJ_7? 9W#nc'B){7o1NK'JڱvڎyT7鐘4qBĥQeM;P O$G!XZy Q;׭+BW<ܸ(R;U_Y4gEjj]A!cM|-C4*ӆ#Z׳Z#)X PQ+u 7-֩X g8 o [0"snA -"x0q\ܯ50>/ vw6&]IYCާb}޶cD:tHF57v/O^TLfa.N:tGGxx@e~NZ LhZ050SX6uyX;Aewz4HRU%OE|"eQ"DSFw/g'h<@]q%[5wML",k&dWTM C{v6^sh.)Ɣ9 I0'~/:,TAMۑPzZpעшϬf6nBfJ,]/R!1ݕ`q/x'?3CfHEWX[~-Jn%T$#s:O\DYejsd_@_?IuƂ)>)GGzLj&~T=&>.ׇZm*:|*t02D-JzL,LksT> n}XNhPij*K[pqhA)jbt9*Qك chv3 >4i%жW0!2!vT7@L)Ucl 06jtEu)NyE5:,mtih3ZhZz?rG`9mI.,@_]%AVtMK"ϩӴf9>^ٌ!9l6BV:̶G/ d$+ƫ/9ޣ{dEDVW%ŕ DFsycǍyzbO`>`_Jo\3KS߄8Ga>H#??=k;?ȺA1hvs7"K }ߡ޾~CU~U?{gL[{D`J#YT 8##=#Ԗ3'oT[1crZQƆN+;wx3O5_0_ӧLDya6:%A[7⨨x$_ ΐ-ذyr9!76*& ^?&iQiMRڅ;ox嗑f$7J;֛ )mm-;$@S:i$şC^q"Hس\VF[w7a֧:X9j> w`sC7Rɞ={Z}Mp7 ڸ|ZP c*â_FusV& NL CmU6!5%{ٷ.Q\g$ǓE;+'*AU0*p,lGaji~=^ywaXy]^VR9D?2R%QT]MPVQG@ 挥NP`f. Kh-H:"5-GIكeTQ,BX}$upp3}+Afb˼J*38$s;4u*!1T󹏆w}EOwԬ\$sSiGe㒰 5CjU-v^ iKlF<.0CEvTC6C֖vYDFiq:{Lafyݺy 0'XGBY,2QSZ,Њr+XTU†]g\ M*qFB\WFEwО[q"Yrsͮ#fQE%;W ຫ$Hſ3f]/h@'Ywt\QՂX*#ᢝӧ=-:T(+`O&Y'9K>oђw/Y\bpnEse~`96G7Ǒ=;i[%+VPTwDR9_RR& 3'(`.Ζ#.88c|k5\}e;7ngB2w* kv1}ɐY:\qsxɧۤe׾:}:?*|$Zb㤬d혟kf 2صo7#)F :\X< 2dz7b:l?[\lw߃>wHt523Z3_"ܑ|Sz<>ih8`( x Z5ļ(s3KFpj8nDю.+؀'ȓG ֖u/0EMaHKFmS'|gu٭Xzm̬O=l=<= Z(bni!1cהO+c(H~Uf TY #-"`=Z0%560RZjkSfXJ~ X #Tφ)c4ݐFYws=^?>-;l*͇Ut$nZ1F@}Ņ$hk~`T^߿mIWMRBu%B*@PqQ*-Lr)jߛNQ+iECyCƖzCzؾg˷yWZ׺xBo՟B#)޶3ebu0mv/h>APF^e :Z 82krN1>r.mfIjv5%SU:T9մ-9PCչ+TIkNUGNE"+HsJ59?6>JHpќ}'eAdPgT]>=&#!`g#^|` G3*8qrҹ"Hɮ@tF] `ANښ)C1E>Y+qxhR1w8T6J쯵ɯ0sgR%oiH,a;c( Y }~A"6QQ~5l5f05}];/Uz6HdeGfvP,{[fY4ŗd+~д@aV2rًZ#S ޱ K%3'xQ!F<S&# F/-KOFJ{ JYٲ:*X1\GHM*jcUyHTwJǐb)3[`}_+$ձOJdgϟ șM59{fAkm0]Ti,Cm^"T R|F5Y1ۗ.dYC*[7g~䬩aJA$LmOWj$wgw+JIPB!WORj& @7D V2@?Yv"%>rxB\,.Nl_s"<:&OG$b#^.eVp,xeƇ!z+@f6* d*n0j/ۍz8[1C[ g23uYp ]WtC4<=rly}0\6#_PINFmZ P$oF,@Y xXi-vIp6N!YڴvVJmq!0.Dʳ pgn'F2y<(lS'DdR:Ue̜m2TiCg%X}M%Y^fmjG E1ɨ%(8lKž}xRQ /M&+]<Ǝq6bsFsEP3ىߓ,mh7ܺ;i5wJ0j,} 'MsgoŌ"ڐS!Zb'& }*,#`9V!^sfnQ>YYTW$V,[fAvg<2V> >]8Fl/۶m֭W{erM[p=w=(ޘLaMG{b* 0ocX=zs.D'bvڀERXSBݚt DNgNmOL@t pFb}brЮhg~-c8x=$wkb..Or#.qP Pb0 ԥk싒Vf*mQW\iב!dACԶ⽋0UW @X|Çؿb_wE8 i5 %jdݾo/ctꢒ &x[swآ ʮ<,ISfx.\>ao**$0н-yљʐh"Ԏ3sdTus_-[1o88{QAD,1Ag-[S *FaZں}U0Z2l|0]zHIFrDr.(/\2U\JBV5Iqn vd"O&YS 6$?/m\7qfv1tuB1c1o-aܠʞPh/YW7j"~Tv~fOuNQBZ(j~Gy؏C7،9 aX;TЁ 8s>2Y񻨭y$h1-eXىB9~eΰpSP}FYA^R"ɧ`vE-??$BV1bIT`֭HnÏ#=c>qAPj'Ɇ6S$GVDBB-Ӻ-2"I\.1dq oam쿇vl` #Hx:ul@ `M f_-%7ᄍF8YYێN5uŋZ,Rm׳]UlL<X[$.E?t ɨsHYY>\ŷ_oj4cPI ˦ yȉ9LJ“گ-TCi!VNv:B2!kF hOol/}M0;;p PN}}j O|bpz);~oB'^tM'uʼnc/DY %u̙3l_r?eDz9]UQMK` "xsLjr2ˡ ƝĜEФ"h(\M w>O##5nE;ZFY NTaL؜^_͈'H%&cad :\*6A}2\gG\pfXHMp:ⓆY[D Z\ ^X2,,;&FF蝏֢I %D6, ׼m8h47%qA~$HC.wRM0QP! XDd;h vO;gp!.̰WfYv㤿v洪ٌܜ Pe'h*^,XDl*Ai42#i܊Flͧµ,ίWF\4 UY*r&1E '=3CWO^{]x8ٴ?zX ھOG -ʛBw1Oy~[7GZRT($P},8&2 B[f5*KKnm}e6PF64W>0 9ye/ٶljP cɎvg ȒiQ؇iMq::eX1DaPbk 7WC3cvs z+V'xj 9D**[3~Wx̘:k UZF37&Ui!Ӥ+3TEXFy3u 3hpǯ66F!!A;DhQatiBHPX[b$gyXrp!;(pWg3)#PFE)~Q\ >!/F1)^[4GDŽ"JxJO`rߐJl*.eK h*C YXKJ[amIկK6G/~q;n޺^NpA}$@>)UMt;PơqgUZhۇfm= u%<ua ŌκfXY2.Jө`IWeSޣRcUH6D'ގ$Ӳ_bIU!rX6SMX_$<3Z3@WEk cfب +c}֠R\tX FxrИi).[!&4LBQ|JUKв 6NtlS6j~+ɬX㩓p%x%07<-2`@ HdT45A8-u;Zgj7zdi,w"* ;v Z^*kV0;s]e=zZS6S WQ b6.+x%MQJٱ!dV@9s4PEo=mJYȢL'CSOサ =_U3lS~џ"7S ߯ ~؀:` Am^riYI^~ݱTY5`Y:8Bਜ਼vW[M˝ ֫AKCm]SڪGiA _TYSEawv$ E\`Yb|&'`FzhY,F]y&W"xqYZ*?_.'Fh,^4&OX? ݾnY.-sedXSnBۛ؄ hNJV+-Gf|tL2b&Hko|_~eKRuz =phy$:)PJ_'-w_'>NϿ,ٗd*DZ ".9^|GfXKU&Uk]i2qԖ076o^@v'N;ɬ*wͰ56@! *1Z񻵀*uVV#̹YxӾB/ٺbd|d Tc,&`&7ANIϟ5LRxAjU_Y} ߵ#+5l+ot26cW_Wal^_ -,,XW;;`Wy@0Ӗo/:*Յ)Zg29X͗8O@`9.;ѓ0E:YS>ԩST/;M̄C= IMuB`-wLڃ7Rg3G6~C~y"}ff8J(V^[bE~c-~K| O^U઒SwShJ<3X Dz3?2{ +ZŐ2p1:xPzyꆾ+KOfsY~ I 7`mA1$ѹc-}z2z'e ぷ: nKR6=,...י*tv2˹CJF?|yAp\~I'\Z~*ϊl 7_*V` iaG0wf#=*Tb!ݐI}dsYbH\k *X Qo)4lT_({۩xFLe)ʋW!p׺!P]b ~]͞yY&_a*h*۶c#nu(nv:1Kf^uj-0Ö5UIѺm>e4IQY1JP3 YR͒t[rlj;p>!b[SqΥw,]e&#dVa1$ף8wp*6W:9 6 &y; UOdN5Hs5ߠ#$JX2TWK&Ḣ B [mp>: GOXW0Pn&iv^} NêϾAφɎ@•:}dI!e\P[^M66dwH@ik2/*Qy׸!탷)Wbf`8Ӂ ]TiצTlh<3s:O$4m uj'>{$&„2&<Z]r†YW#b$+1eWe$ޛ*Zd*k!Y TaKH"niJP,>إ*"GkAxP X*@9"xI]n@29?l* J89̍ӱilE'("b xOM{y2rkIy3Λ(ϋ@z@Ӻ.+QG_t4076H+Liq9~{*"6σdPs `a O0~5b;/iy -*lLJ2b eZpD2Mγyx \ҵ3)_:o,7E-Ro:sraIasȖ++@bZ1V`6Bh,M)˄ _OndYΖg#dҺ8tՔ0eD-&J'?HJ8[W<} 7' E( JP5gjCOIli;Gf*:8=oNiAs,uɐ?سC[WA:{"/^x禵/G~M_Qg. ^6Sdo!]|%W%&[V#|85J/YVtmLm~0(g?^6*֋oEmM 6Ӓ*fa9.*EmriM*3"|'`=c)|Uw?vH+3 \H'{8D[w:Mz͢CX5*3”i!;i قU_|;هFc:4CĉØ:g>'a<110#jهTd, {|6} CQG*IR",Yub= gw6oA0c\]=pp6NXO_#(b܌J8‘62QAcl ah"K0%4y*Ű"PZ ɖ{9Q`g0Zo!.Ćsxrqi5O(ǿjeWRF/٪(KnTY !9RbN@;FKe9<@uj7?cN>d%ѭd?[x='3؆igIusMLPb lD;T޽_}).b)j~3zTZX>/5ŇXkֺͺxR$6@*b; Z`Ҧf&& (B)2}Ȩ҂ yJCjh㉌ʇ)Ia/eщot 61;Vg0i(gKYEfr,v06"FJm6!+Ri^AN&0˖1K0*Ԃ p"©:V)CM-߷2VVU3p80%i0R%*Zh|Uy1"g]zy;iuJ$䢉`6u#XNY{7$%P n|4:l ⺙Z Rb /DM'6Ѿ~ oډkv*F=H͢zJN)]|~BB}XX(a꘳ۡe^Ws6M7CM7,%*.A5z㏘?}IQbT\PVD2O mZ:1x_eF\o5j XgǟĦCa&]g<"Ubx;!#ڨI1,Cqx?.ͭ0WXo >g]1a!,?;X1ZڑI樅ei\5^'xStԈ$lt"N7[7HdQq_w:Qʬ ; }z[o/Ǧ)UIO'h7ukxas6=55,bT`dn ww q =v'ۿ_B33=iRDHwlDAA&IY\g_Ǎ(2Ӑ6RХJv5xh#߬3F(g>1~,g^ƴAD\O}-ZJ*l{zzAgP\3/XKAJ:>y"%œ6jl5*lW9%6XTM3> ru0V e ueZ7ޅ\BElR<1 u+s,uX`KFAy[;.Oxtr2w`Sw{vHK\6^ɢfSEJh%lLU5a3Rc)Ƿ%f[~Hc^vZRT*t`d]ܗ: NISpx~yyxQYVfH8u zʁeFqVn=EK~JjsyԱIɍ#mΆT:`-(,wc(7;Z6s`6A\\4xq H35Ο}pUuq3QD ]J*PWet3"(_û"O4Or;IF3@;IB5YPRj~$ mo Օ 7=G{wԦZU}x$O݋[ŠWH$,XbCKײR3Vq[PZe`7{ؼc7i X9IYaUȜ~68I_: R%bơY -CKĭbܝ>&Pf ;\HŌiKf(/(@Gw5<M$ gCwb׏+ŜYϗλQ^+T|TPMJO* Vԃⶕ "槮zCTY/j'ߪ?nWXLM/zx;Ķm`x-q[1X (h|mXߐރX^$-P2s58+jf~S<~5Vݽ3'+(wWkn(c߽ }*'u?,q`~ؚ̙Hy}˂_`0\}s6:irڠ&CM*cM)tILJ.aT b)(0,G|.L\|PCl ˺W cMi鄝횑ۤW iJ$Jfܢ^xI}O3*㣒(jȧb1 yyRV%dshL+cb*9LlTbeŌcE¾Z+բ^NLv IY06䶪`~P ɸbu&lIpqAX/ysF|Q9v2dQgQG2X6B̂-JsIu\XSPSXL#pBBDHµF .h CmMhkm3ERd̒>hjSRGO>N}.10d F,_{vFhxEUWSc8:Xc}.Fӈں S (WN%<'Lup4 x<gwf%D'T~O2 C+<i[b:Z`מ=jKC/6l ~퓞8 d'_s(6uzXffs sڱ(kBd PK zTJ-8+L]b{_ߐ PN 񜈷>f`ōUad*փ"<<`L"ucU3Z(0uh.eB nh%ލ^@+zIRY>6ETY(Zf^I1ez޴G|.*n(3ZLx;ϢPVo*?6CI-=RS=-n/-eT`o˦:XVuj[ۧB+SQ*6TW";K(B& XU%9f EdA{~d-hnl)ZDLVT /CP#';'c3lkL:w[׎!Tw ;Ty}ĈD-Gֱzysc&Zހp/ß.nX\4Qu]g|}`c(-)GHx^{U,Ai5'NƳYW:h=LFB-TX=*u319B&IzbJ0ƼiQA"T/Ze4(qKxzVI0{)W5 8h ,^L(6eU2Gw;ZT"[u獵S5ed9s\PʪK4~>NU4'?b^9Nx/bGf-'T_q[ۻʊ Wd}cse;7Gr[y O™_{XЙ(˜h|7 f՗K{S\ӒR梜1˗,ӱo~ܱtT>9AAS[KEՓLu|,^v0D3] Y繹@ed=ckGw@75Fus)( lւR:~"2u=&N@fτ:-l%ũG@b1xlbx$q>XNUf+t,]΀ڙCq@sʔ}U&LJ@?[N"'kc+I.,@ ?Ug@Av&ȉ ju u#b2Ϥ燬օ@!vT~F%m_O?ZImյM Gd }n KXW=7tlvJ .,$#o<^kfA+{*\|}(l#"A hCd.@8U_~O`jk`N&[W"|z \8yEȨ)$WQ5\no~E[ZO3U?0V1c i]'vh̑_ձ2W J˫q&Gn%Cu JTTg<','Q:ƜL=K@m|dyڬmQdv֤ ?wڸ˖.=vGgC i`bWX3*l|AKdtykEߥwm,#VN2+9/ZyT-s#x@554g|R5#;bHl sؓL LK*!|$!yz,gR? qY6 'Mٲ*t)P|^Yi̡M|QsZTiгUh@0рO mNY+ٳYC5hga(H4a*ݞ9<6Ba:Nzn^ P ݇~}-P#3bkEֆiEu qòېDASc'hMS:O!*v3p=%6.NTabލK1u,2aY0j 勍;v4/woEiA|lk?s/K9PAZTrQ81ߧd{0?K7T=<`*֏nTFzX|͈qC;G%7ފ*6!:WЂeg^mCzu0cM,r.E%tNun7aժU$ogL[;P:61}Pq:xM{`cffk #?VO$0( })xϢTY%hj2LJ1onk! .-UM9UZy;2?l'|9 ٴBBrD $#Xl  e7ރBhGwBj"EО7r&=|f-Dk:x899Vv!W2f',"OX%|b' = *,=*/wsOcE}WeU,7AEJ!!*iw[\~ _Q4*=(Xeŵ ] U~C&OS=sx+NfK=l _$/iu#ky\d%7]ص;kGs@Vy܏J+XŲ̺pƐ/eՌ1QQpGjjvُn | :sUa~lwj*OiO=]={`ufΡv|VuJLI"u'%]G:}[Bde,zͬfDjV^_\6hO $5(C74<:`{Uymģ&P)Tl*?&x+!Բ#kɱ႕_;6zNl2 NEaFDu3fG6V?b Yay1UԜU$ t9(Eit wIgWoeeCH$멽#IQQVC^k:vIÃzFBImq<ٷk-Q}\7WA\&UjW`aG6 6z;T>ɇ%זki)uKꡣUMCsk4dV&m0%:Y#&Y4֬b5 If8ξ{Gݽؼi cDfcTF%iЁ\&`y b$UDԓdHVF=E'l # 4kc&wV^3N]uG`mT9tAu5E-ڪ}x)$ >ΧGoݱʘ;iNPt&E0!Ѯ]|fĖ3gMw1ɎjBoh)ɗjTpE[AQ2=A~%Π]aib0oFsmBnvg/cjZC\-Me_H2ANKk/0Ӈ`\j3TR\ n4ꃆt0ֈvFڪhlC|̊c~'c9CŨkBZg;`j!@P+c궰FD$ر(7][$b=J8.`TVva5+i)LjZ+응ž=n)gO(&[u%"A[} ]~灆l4nc.dԕ=W\<|٫w[Tk듈)q =ն=+-)X/PX'*ZKs}k~CXT^v뤼5*McU =T)WKȞ-38w|GWX#+N\߅,AϻR\6heOYشc>ň8hO?d!8~${7_ػ^t cu:i%!h2 u9Y$~(fCKz4PExLnoPJjhVb&>NRk.Cq"st$f{|Xul{ki=96:?`@_OmĺoZm=H1Xv)۵8\˗ʬj$a!(K9"E,PQlX+d79jdu52g-oOCz% &R68v>v~@+ӘЩʿ^r?<'9q3{ ܠ+X(DM 5 iYFUlB XR[zZ)@tTcd)@4"h+5fΚ* ,`ie2hE;͌u}?s 3Y?,!C`I 9`ZΤeF=՗MrfO_ > :t3OE~my;);Ů+i)p9pB|ᱴeޕ 4Ɏ:l̩FI[ߞyLT=mḼ8Tnm1EkgGcQrYIDle|)Tfdo/¶_b0鴃6mdQW";Tb 1 ӇvG "z@o\~'9/<@ M;Qϋh-5>]W뺍d n% MHN>ɘ:|Z*$o 7\#Q?@VLCM<|^S({ u66o>=d *ߩO' ]~k6?|]LwZ*A{Z+rO 0$~~oA M CX2j$3]l~fˋ-vkG K. M3GX/YT0^j5:2RZ.4FfoȈp:sP+'ּ rrq]j.q}#wt獜+ k Xm@ O}0W FjCVKy)|6GwW<ضֳV"kjK{[H4@af6ڊ dDcs sO}Ib<aR]^dUM$<|p#]Bb:He̾F!|+SU=!)҆";X+֛c־>\T ():he]@brMwF@d Z65ucp@&)Ta?_+ t.- EQ;:;Q8$[;!9 5MTXAkR/m7;=|=M&ްu^H=[NທQCLq}STk6!42Kv' 9€٤g_U mc 8әˤ JBL zV5qt*XP*IVc_sϡxT7#"T,^FZ<ށzO h ^֤&I=" Q[w:y #҄/º=I>Lz⊕+D:]%LD_[d!=~|fW(elBP~ n <}'eܒ:RoNMǞOwM1amn'N.W +gYv;sn/Ԡ߶!TOqץ6<-U)]LHѫt mW>(S+E]CgQQW\)M 8I&z d[,/ыNF ܄~%ZUk< j(_#ȚvF?-B%_F>&LDܑ:}dpmCpGu#@4#+ TDəmBs`WHRqaBR %Fx-4Jًhs">6g93,O%⓷ޤ~1]ii\k[ ZJaABG'U̩M/QfUM .y?pgQ_QQ #V=m:`GFm|MI oWA lp5f<i|ӓ+= j>{O.-'SY4C;D/Rf"-㴼U!y&7[H26fiCA(y-Yܩ`QF;h_qP=s ۱/ɇ=mnRs"tb N̬ /peի% d3͗ C%u %@]]QH*\D,dx7mxᠴʽLbo_f:f 7&$L ]hf\mQ e߇Tq,(2YU" 2'l.I˅sn͜{'OHp`C(XXL:*/)/z*lrm Zr2g¨~5V :\-uy-TQan-=F%/ 1{?fמq\8w_mڄ(FIlFEwXNjq$j|˨DR!*2"vJ:{8jl*\7?izTEE fv0vVy4sk`V*?QL3Es~rQThen-b׳AOC4T @Hhn PB)Ee+$PT޶+2+ϙlRzL+k91 ^1*E 蕞|?*\pU<]B+J\$KulZ+ :L`]2o&z]ZSPDs{4i#w)JaT-x77Ξ%mTK Npi#'8"q%6-Ʈ$pH*mA  q ]Ҭm膧)˨'`ՏejP]E#fT`DB4{FvNH&`gj$P;\_@Xgpe'8{{nBQV*`h3i5D[jpWXmmpzY {@k +oO2r%ՌI.(gQCBҽq9J)9;:fַXAW[Z;  5+[)jB1o!Nk"A#q(huhQ]A-YҒY8j ّ(UuupsǪ5_"͒.8MJ7tWUuHfNY.?>$&KsV- KFFu'S wSZ>VZ0!֙#k>5\ZS-KkQN9L\#8P٧39E5mPSa^G:b ۰ց9[9Z@=K \B:,>#[Dc1-|0eY3g}|Az=8o &탉ax'Om=UćG:zձq\UUtTŤpoܚuUult]iY>O9vtjx/!K,Gw$'sZ)Ij&_SM쳙>=b]Q:M1\"HUqE/q}ڂ!ԸOi:iA꾭Pcڸ{ZT)&Y\R&=L~JqKvERr2-0;;U^Q8*Kp w)!GA}T YcZald]f6L$U0^~O=N%#bCk+yPqQu3??NR|ޫ9"Z1#,ۘ04;ӗ̪rpqŖj1~uv6JhD1 LYY۞MjN\~ idl1-t<"dz,>ᱚtrD-^2Ƴ< 𗿢9n\yi;m q![_~eG~ \Na"&5qE'lI;ɬZ;K]h`FFXH8"'ͺ$* Cߟ~z*iGfPEv@ cVI'9,:XqD[~RFҕO_4Wa%ZZp&(v5Gvf:ޫ$jC])eDvM ~U أTttZfUht%EA֨YÚyl8?"=3Al@9 7ah i12pUHӱ)z.|{6q.:Fř |iĢTIWg/dq?q\ `m}eIȤڻ[vsiCoSG`nSut" u,Xi[R'an@Z^6̰͑I#v>)@ݩ=d螥Uo' # ɳ瑕l&myE+(Y+䡝ؼTvwFMʲSt_]6c0UŹtܙm^y+d6S3W.EC̾n2?q׽c(šd߫I].-Ρl"#8ӥ}aHuS71%rSC_higPxj@n5☙֨Gs_~`?}>Jt`Խ""I؉^pk\TF̱ǓO=A{l߷5$;c$>G U%Y k2knJ`jp@^zyT`Y[BFkg#ߟ'5ŵX[iyF|~T"(nW7~+\dpnUۯF{yw$4^Kw֯*U|dc\*V(URzFR%.ϞXz=K^wmH#fMqM=vݷx;y*Jǟ $sg5yv(ƆdX^~r;SMScShKGn *jW<*x9LhC8AOo_YUch^Ev/a[UYi dٷ .~YE%~8/aUo GcoF%8ڋay:nZb0l2NEuUХ*BL8f⼨1Z(XeU5hp'x|dU,kn}Qu WC~df 뛑xFw?(pS.;һbҍ$ tpt.UAf#@IDATޡ᠁P kmC#M kǑYkM# x~C<f2ft`5h?K{^ف u04:g NF}TZ+0]z֖U#Iͱ-tX_0vfH4 \nt88YYI{. pe κʆ_7_?smbjH.Ds[UtI򀫷d]V L}O? ftNpcHH'ؘDnc`p ړh<жvѶʜVGd!8\*|W׍ J{iK+Iw8JX+0F o7/d3Mmwc)jZT.l!N'9 qyf&NO? _WQC+BJqtB,U]):2lŶa㊬ٻL]t1N ؉'䁳Sq20 wc8gR5loWRti&X }>-a=Ok?XϞ }F1 gknck|uPIx/G{FRPspQ Va;%8+F)\:7\_Zc[h<ՍwEf^W1RNs7|úBݺm93 :23@,2ex3*=m"bt6hsoX2|DM Rt[krxS\GyY%HM<-OG Jsx?s<8AieՋ_/:k-I|PZφqR~$!ts&v͚58tM}.'mֆl/ oO;F֛ 238Њ2wԀ{/!<,jK94c.}1L5b aӦؿ} ~63l,a'x߯gTXزN蜅5c3I CZ+fTىa=>ʕ+Zw3B rP9}A\* uaAfvv0eUJW!ĉȟ׻ہ^cWHI΢GHN6M{/,;G,JFXsY_gb|G>PI^Je@ 975":dٳ~f#1@y Xp"< 1`:ې{d$ @<ǿE71+`L,L-BI5T"t*,-{ {HFbTp ~ ]+ `fh ^,1-2n0s ,zX2':P^V"y]'kߛh,9S o<s@IYLrfʎh# ،9)ǘZYuP!@Y*X^3gP&hE J~FdSu17ߐx*m3H24Ї |~_]?c \<6q*du63 W uD u*yA[tKIἊ ᣯ &m9_gVaִXig8*Zk3\lX&8pdP}{v0GLPpo2R1SajZa]zaa7n@m~5vf&WgsaI"4(ʳzh `iA${O $/ke-ؽu;(Sk#UIy%wN5[~~>>9afFvmw%| JIp=nA)8z; ^҆E-RƪjI631d-m9*[KۄVFߴ gŠ4U܌9eNRiG'[G|0{Ge^m6{GBK;V=IwhZ“a#'n[ ~垀={=N`ZtZb6!Ÿ:kJ~5,t6Um*?n!!?:[67RF ϻiż^4)]<8oʟ٨UـfΊ1'_Ep LjqdjiGl vum+%㓨mG"  (mBfiYo >ޚHM͇1k|r L,6t2@I?'_mTwd֣-B1'7ѴC^ LK姑.&M'綠qKy^&S5);dwE7nX %&Y#@wz qiѫ"h5`+\\ bX?06Fq%Aaע)`jj.]"Ȫobl x[mAUc hN:T!kk" fհi6W>I,w{هobނq)8t|C(X#uAm!+XYYxXT{qÆC`il~i<EEE@c50='?$;DiNAetg]Ze+s 1uu<#n1ѧ:3WHˍGn:^0SiVMڊUa1/hvw!C>-0&{_`ֻ1amɛ0PU_a3*s !wzttTr{U^F[@9t-_#*vД9 zh/P^MUX ؜"ij>o361.#BZr.![ e䤧yyoeƻ/ A 3p:o?y*VWE ŦሗjWeӎO%tΕօ˹\;ۚH ~=7ͣr>u+UZ"ԢWX ]SJd`bT8P Pru1V$pbV|J7rJZG\C+iwXClSc(妸_|/{-ou C\XlWDQ;OOuEesǺ-[ĹJGUxǏ#*vo-)#ކV ػưXϴ̩47ӄY?i,O樞}O: w7⎛oğp7׫{~vGJ[55־<>|7d×hd ϴ h1ԁJZ|5*G2ukc-t4%mFz@_#ibeH'M(,dHpHOF~v.Z uחnKECITD)PIUݬy^ӂĄT\tIbV҃y} ]٫g=C[ؼLZ|kKH4k 7) %p_c.[{k\^0"*pvcj}M!-^ի.>tUpKRG5mMPYQFa5L%fBUTr )~Fё i| T ǧ$q_01v{hKؕT5%ۀ}8Z suvB[1HŌ a˼Fx>mAva :yJKp(2} ơX1rHZudif2BSE$n]47ifj;s}, D[C 3={4 ~y)@GKg{͞fV* ]Ն Zʳ.r@o.] vi,0]WK=KCr2nb#Pƾ$ikKSdgSF4QLJ@@j .{*Tbb&d/iR GOs9X>ǎW* @&~voۀxH -/+(3{~GYŞ!*VŲvUOSiT]t3PlfS9b 75v<I[H{]>}1}!QoV#I}8p騠7:ھ퓷Zm~X{>TtƤYCvdL_bZ|# lOY|\~<ҙ6C6?\6 X6i{]`?yl/NLA@KrBETNs"Ut,& _oƼ%^%b!ǚ?/X" qM7sc7?&*`zPqC#٩qtwqk-hi` 2xG:,iFP/2R>ܨT Qb3.Ajږ_1ց>1mHm$4#1`hj ڽ;pdgu!ra4Xd5F[VJ"9@O;-čYo^*9'C=uU(JFK;`xW@ o< O7r6WH;Dkd]ҕ+|'+Է(An۩Rg܆.8T6jLZ2.?Nӏ/a仇bd-MX #,`VXn/et73usҳF̃,j*u*AoQ1.jK;,ݼPE U?(/Xt,\z؀3f~=mfIeBwqǔ<STr3ǙẒe=,&#O _|ɜշطM7?|WiƱ=yUY= -s^,{ `ŗ6*o 厢?IC-`/<m^,ʼnf=tlV-;zгS\])sմHO%A V߱w.un a]> ~6v:dŤ,4yu]d,Vd@q# ֑W}e_IpHVloǖ/֊]$Egp:#X6<Of D؄njn*93hwEQ<8sW-'Ų^Q%M֮|ҳD&=/.=8ܽ$wVsWM[g"8ԖWq;Zg8s8^bhQ~Y3H]taPGɵU/V7sP1XgZVVڬ5𐍯XץA<.GzNQ ]S5i+:l{!])LG A͜= su!{dٚcNs|xYJ`Ѯ-XJi=UUΊ6l[1ٳEa'l@ĸbC >zի^孿cɵ.G#hlkkBĘ\{fCQ ca,却dI >>m@r E,DJ—֧i9RF`T `c`p˄Mjo- C˔49-Rn* SG6^[z7 rKʻLhkfA}2%faJ^z̡ov`$bU НIς j,2u2uE,xިjQهtt (z*TTHꅲjj Ю$mlK`i;Qx6y7R<$A0?0LZYW iJ,3]}H0O؃QP8?A@8#Gツ=d1_ZaM޸s-,BY47-PnS`WT1jR L 1%)@*V&8Kwʵ6a1ފ$ww :sv]Cv<mUa2Zg]0j%Su{;u m$Qr*o_'elJ⏋'|Grc}=Ӽ`gV"C7LCca<^nd`fX݃_*gBEˡ ?`9Fdx&3#jm3 Fia͘HM 9WfіAҍ|LZyfL]׮G ,$[i^,o.8\}mtd0oW6l~tcMHQ=/XI_B@d1}Jz6=#hH`EfCA??v̻wA.ɿ؀+Xs PS@[箢Ɩ6O=dQ`ABW٫LN+;$T1Tԙ% gc͖qYDP?*7,{[LN߫^Q.ewJx7Z>WuȾ)V>*`\%HYֿﺕ.F-vA ]8e:Ҳ4mW'Ú-$_ۀTְo{ony&^$DB.uU]3 "E I~vpja6tHR:2Zݴg)4,Rai@;V h7@MPKCM&(Hؽsdi'<)Jzb}#ۆzO^  ۛ9٣zT7AOe-%"*ʴsSozsN =i*( ׵v]uok/HHH's{8+o~{ܹ3̝;>99 mzk('M% D'tX1Ij,F^&$T+}=BArpchoBnӘkisNzj B@1 Pτ9Gc)g8&aꎃ#VHwcY$j(.0נ1:;&g݇soNg5[ي_oc4kAnQ] .큺>zT^wUm",:LB{U% E ʯw4QZΙFQ @Cs'&M  LH$ KL)Ӿں25UǚY(*Ȁ Vְj6f0}REpsU._~z4aaªSXE,~ꥭdOAK^7aADKhm#+f;UpL"$*Cn"e+v; t Qku-M\MkbJxKk\ϵ˙/gd?sfLIx$>A݀)<4Y TOY.bh ۯ\)X 4㽷9 =j)VE97L2uM Ǫǟem)맇>./$ P24ԗ0a{mxstF!'yQ0:jiQcvx zLuD~\1%l 24%Rw 99AC?9 E@* s!$䐆I|!Vs[]zf|24ț)"4d%VqP/sM}O>儆7^n3uVyZ 4܃FdEG<$OSS W]]q`\2mW'}$V p&[- /oe఼ޱ`GXjMٮo AAq#*P"> 6FdRǑbIJ釉vV9: u֌A\#(~w2 Gz,ί1*fe]?Y(˟n \)S^ct Zbgu庫ñ 4^YNVRn&9ILxtPj޼0E~w!}ܼG[Fx`5ߡ_Ÿw*ƚ!.ht+ ]jܠIg6Go&&Y]huV\T"&D&N?_'y՜?#4(xEKȏ?SV=t'QA0D )id}zϤܦ%m*k)lC8E~f"BcRb JJ (܂nоUJE_B8󇿈O8O'a=VJsz:b2lw5,zTZ;5`6y&tm?a=1^MώN8RJy1$;'Ht\2EI MWj*DpcuC!V>=Vy+/uLb o^@*l UT]Hˌ-cvc_|) ŏfs#0it%Gy]7~p♜_C<8-\&\S9>Ɇ/$/epL m m9eP@Sg: ҧ=$ 1nW,'䆇dg2$SW#>nͬ{E_Jb$/x*NT :ay;ujwg"8ԒkGe(ίƱSTtQ*X] G/G5SI& CyfI$>F Uզa3ՈL(`ZD L< Q1'hA:WaӮosWF9յM503]늊YMLYaڦXtl6PfĿuu*ެ^4F ??oV7ҧD-p%g(!E 80r `k}4&ɐE7MCIlǁQTQŠ^:y ;c<++P* kIU2FJ1c.8Μ(ժq}̭$bJC{& v`\q/YlOZ` h3m*a#'Zt!ɂUu=bnԼB%B yB9 ,ĊF}S'H-mGYENGeu&Tl/AGUcVpv11'`"pd1nr(-1#QH\xUTnǞ1$HrƠET+ͯCX5IRkꬡ- ӻO塸voG>U("nUU=N~0f壒BN vkA5ύ_K/4kms 0G1m*@-((D[q"AVԷBgMxOR]<*|Ei9 X77Z2DNf&* S=m6P5G S`xw?ooSQn/]%V藐V/{ee8"Л#)sn f2W3dUhsK}t@ v7֘z&NVjqe1vY3F(;{%8_/^}Lψ:&(@/Ev#N]nJP] M)ԯaؚ jࠏ, Z~?-=G1֕ҔXxT4/-Ifxi:)^+UQ*VN /¬IFઘD$/gCFdop fe+*8J#%޲ CU>>Ǜ鱗3 }| icB G(1Htp2~QTEΆ/9`m?I pO\ŠՕ"`YnCz U+ksIޙ?wP~dXk+᳏0}3__% 0?￧_=*u4z^|%b&C[Y3s;1! D~TKO9V/)6x_Ĥepe&z51qrVϡO NI Ճ27AOP_[\ UL5 ,"] %I. 3,RRfoz #7>DŽ:PmjYg"dwuUÎ=I߂v. 2p6ueB)U\^Ӈvr5ٗ[(BzU+^z5~/a7aːA%"LwTL &p\{Yu}ԏ+/`Xbސ2RBɛ8Z+񞟑v|OTiY۹^fx#4t2 .<}Fdw{څk?AFJ2s7_cJ٠YqFЏU [㵽}=9-$$_cCuNS Eyʤ^ZXűG~za}(~8sZcSE?ݻs3cYX]fͯ`/VTFU0?cbr4La8N  iҟ*NSN1eߏȣ֏c288IJ$묏_Hg-sg> bPXK1.I Z,3V\thO(oZ-{x-I`ySmXL43i6oفSY`5WH> TT#ܐp(Muh+AqQ=C I1IY$pzCe즨fgՐs%f A"Sa# E+QKv=YŪӹ5s ۚ񫅅 9\,F^'@L([K֪aÎGuQ1耤bfd.YEWcf VSzל$P '@CX*HzC"u۷m;P8UP2_hK( lT3!lM@e[ukF6@ݩDښ$f@JwOS\*q}3MG,]AyV[J-٨MIb I;ru6A#uv#FEu$ Ik`tR>Ҕus8IUFT%pW1X8}"2ڑ4`-s͜fcoG }'ћ=82ba{/"1vzI-clPJfwOe 6i3&566,K)̍id fM X %shRR^| хVLRYZC<êE,?u:1ur s"Ep=ƚE/ +qjuȬEoS!tM߭Đs|P6뭼XuYo4ʴ4U8 tA\#ǫp%K"~m*Ո*:EIZ{yCQ:O60bGҋ8i*&m`; dݾIJJ ~!5{L oӰHwM6((Ϸg&z`X*Gk,d:{)U1_i.9b{~) kmmjk**d%fbpwdt`D|,u@IDAT:$ӫtd+5AA3c9אM {sd4*#,$ _,Z<ǎ<\ȐCY|, b =֨ÑH&H1sr7<]t6H!sG"2@"7a;Yε H*VtX&u;_A77 `ˊωs†^[Q"Ҙ+b21~/5DOt.M /Mv&>;woǛ}B\5pڡmMiF ="CUH_G̘*0!p+m1ϰ ; B|YfrKX⇸"1%PZD]{$Fž΀k k hw%CULYZ_/+]p<9[nǺOסV 6ƴ&99ZWUUU0M%g_y?¯)Nr,\JXOa[ tGǥ❏c뾇{Zw  WzՎLJm|@Xz߯2OEa: Lݡc"2Y9dd$AZ wkoO?J?\*)0lL&YЕOzϏBEi=i$J}C>M{zӓfɅ{VD5v^F AHewY]*fI#GhӵcgbGcc(.ɅAG߀C>J^hwl 54lY/crFr"%~zOTYr+ի  Tw 9 /ʂ ,9\ä.dկ`l*NkSR_W~H ya 短o߽'LXJֳ;ގ[Nsf]G.c_DBfK[(9HZQHkց-19=>?ի>r;GgPKpoo',s v0+7 \YjFV>U*W+1?>m'*ˤd?1dmRES#!{|)ms(.5Ul5TR6`W)xF]-h+~| vf"hg Z>' i9`ڜxGDkb?Ԅ,L!!#9X4;o[=@b TE#4˔õ"flk7x'jd5R D[C -(KгA_$S\C*0eNݖѣB7hGZ=cK(㵖d}m NDݓ)c>x1誯b %LYXJ`1sYػ` Z7 *F3 +N/TuAܹ. @xg5ЁVBVFΘ=fZH)wh3x!s b9ĜlT㶕#m&Z^T)!A<;ə(&lmU+&$F޲Si_;SmP6ga^:Ռ*3YYMV%֎$ҿ1uAVMWЊ }U$I̜2(U w'3v 8h<.{gH=fy v㇨T6lˊSmͱh#RIU<:j-)z'z;Ĺ'ы-A[u1fMj@q +-FT$_±7k6;OMDaN(M\?EH3|+cK]bq \NC-&*]oA:r 7bqx=# Z2H:V3 \@2W~3h3RI)ٛxNyUXx D'$7{GiZbJJLLQ Sb*m4:}ZER4'4ZfN< WmR@!"67'5lVy=ll2x # decoDJJ >_!iM)ڲ\>Oj)N ;퐍^AiRbV*ۨE[2rD2J2R+9U% LԔƔ^n.|ҾߴSyjdD29zF~lm ӴiC ׈C($7Kn[1>pv̓;J s06tIC'xVP 9(g/t66DĬYȯGM J=Нyor=,'y^X837 .$~d#+zqih,Ʉ3j) uEXp"W5k{4LūoK/p7EjF:Jxӵp@Ǫz&6GW*# )Eۺ;*=dG@OQ`,=gƩ#д΄ 6scPA r{K+-+_ #V ? pu8z D221CI ^^# =Gzd欒֜z[ӛU VңwmbRFEeGy%9 oW `,O:C[ Yoj<_sveJsW]'AA!38{|hb!Mj/6mIHln{;PaTUݽAz$1kEQy2sW%bhUl';$ * u,U' WͽE~@>Gǫro|UwK(+Q51m9'1Aʀ)_чIRޗXǣpwp9SQ.9;Zht7WG#9 8TD^C§bvh&/̤TWT&=HN`kI.%n z>x[{\tȣSJE'to:e,G_oOg? c+_g,?DNJ ۚs|iܵ<`maD`Uɀ8.?pR򔞡)bʺMAyc'JXHb-Anz{D[8",8@+ݔ hV4jQ޹~M=AxM+Z`J+͹L,)QDb$$!<nn_~lJVӉD$ɷ}ܦG/=ku4jSyxY(mf*`ؓ_ q)]RWRpre)7FK vo-kO/kģhya#b~X/doX~Y56h ł7@E#JPModҶ$CxjQ-iJ&ռ8Yxd&LjF1j{ĶkHrґD&,]h\YH!\Oֱf̛Prc.5%\Xkԃ-5CQe-0 J kUa) Blr)n[I+,^4o[+^n[@Id)|$Iߏ䘃r*@#?8/oҋȑϤg>{'= {ܖFYںXQjե lkeU^'`}j@euq1xz:(T HʧTx| ͐ -CQ}< Wslj-)9Yy>Mu<_?nJG;Z_}]0? Jz$+mwTew}_ƺ8z˜VxK,I'{u$̘-BÆv+? 0t},<S9 3'90bbcz^?&s^D:`'b 3kK8ذ~Y8g'L Vَ _8/71|}w9~c}:xY.h~,<)*zjbA%NL0}ܧ/}/~OJ$ U>fOJT O>!H&رcBоڸSMk>l𴳹rx)Iޖ-XyXB^~&F}w?R `2h9J1rVob._y.^z".F}".B)i|6߻r>ת-_[Ux Eo/m^.._zQVqE''QÂ.cWD}\t΄Z8u)-sn^q? Z EĜ$G'y>w 3Uc*ڄ` S&1q9-qʰ%sA ib7s9w6V`G$ QNT \><1b X?mš;"H%ZZ2}[YA> NHJMV?+h&$jP#y[Si gWw0"F4aK|(YkIG|8Ց5g-=:F20@sfF:Rp,>Mʍamr:%,&N&<:PϠ5 'Fc̝܋8dW T-X]r`a9R5a-:[WsgU9OZ@T;Kh$tΒ۠.AՏC)J(¸݀NTp=?ϿV˘ 5]0wCyº=ш&/޼ MɓQ[\ 7_/VHff&@ 7hS1>3kCq&(wL8&p%O{fm,q9AuW!%eRhA5Lp;-Š&h+ՁĹiﷆӠ:fџwDbs; /J4Z˞fD0 \9UKJj+B{?g#H5qmv}M7Ʋ:*45J,mMC[?d`zy_/<_zkF:4mZ閵hz&?#;4>xg|-ܵ9v—A!ej 3ȢtּP J5T>y}8S? J^`ͧѵ)+YF7$hdugU ~@ $WS<0(dGӓeļsk"%vPnm !#V'Q%"Hq9b*ǻ+\ԉH|ף_X+Lz#zrkiiCBZ[UNxS6DägMuJ .k?˼i1ob `(n]C5{#>JQ`UedZX9~?xjiÚQSKPՈu}Atwh>v(f_%ÒS6Ǜ';Q-M2ǵfъ@*~Mf쒘x-&ݰsx< }?c;vb]|N֨PAY^ _;t梛d ocV$%9ARJ;ݼJ:4l}yX KyxVx.ЙF(zo:Ix=:Cј7klErM:$1Y>1.mnA5! ?y?zL9i5d2n}YWgd% {὏?6Ib@&šh֢=|(Ť9xo-X9akFB#}"^M["5 P^E-g̈́}3-8 )0"7F1 :̈́F4Qvʤ(CnA֪rާo?}x|e7+7OPKL5 TWp8e 8>UH-t Jd)#ɱKpxVN; S:oohߌ 6+ b2zi F%87eIoqz:%qkv1ckъYY$ٳi9QY~5Dd[$݋T66%,t3p(*KHUmmj2Ҳ%UV$j7z]b 33-GTA`%>+Ɵ*Q$kR9T}$޳61r?GǻM60#m,rS6GGDR1,a±-CU!r򋐲w; N?<ܐAYICraG4ƬEk)'ࡣxW`G`jtZ;0iTz{v0z.~hׅ>ɴ5m匎M]1؅ 5XaiFޘS hjc @7!~@ PLSv 35 Tӧ=Q h;SXFhjюA c۴ T?Rh$} `/1.UaldNUS$$pME-*jyoV' F[26{TƤM0Ez6}a#ngg-ߵ^Vj26pYR2N1nD1=jCi{+ "19<7CA1V[RX-xӪ_?؋ j j{dB2Jf1F׷q$ o_b0/U-HE+54uiPU=>K[VT1k-N:ӿT ~N|!c8\Q 󯵱 ۷PLEuy7q#@MajU5}yDD=R:ZSQcgv"ժ6&ܲ>jj!#7:1*{}xCIEa#bTQAޚR3nS1S>]TE$``O0njn L H&t?ڨ5.|DW-瘢*լn}'Ѭ"Xu^\H٧P(ZەrY ^2IEޔO6ykki7S;^%rz}G?wg~߿+Y6,/@H |CLD(fu3qȀjB:9KvB'XKc-UE78+,ċW#g%԰ceg`߲S8[\4{yhsKm<<|Y:\G2en5b[m}Ę\̋rD }4ZLM䞓NPJ0-=R’vc5:d_<sQ@_#C0}ѫ_oZLI23zhFxM!j@sph+JWOPlΞ25_01@*ɓBpObʿп5v @کè,DeCy6DZzt[go(!4o0rBxL_gkeZxrR814Nl#̅=>鋖l>G1SeM&$=,H9V $w_׊{aԴ?kա3 | ;)G6yPڸB)hQQ) 8 <60$[<,Pˁ{bqXMLn&9`"SW1؊u Y 'PZش&';(Ä$#Vr \ qCj NoR&di<7TWI;r9Gϼͫ7- w@׎L|NZdd̆ +Zx.ywPw¸gУY^uv7IA-6m'[o&<QJoZ4WV df R*RVclm<i)I:]zdˍUΊ&~~ntvgCv^x`E#i%I(qmci]EOG7O&(9 koC JpT~+z<ו $~C)t(l.^~ bcbVk2?<6z*Fc_վg;^X'SJ9b%\eUad`Gݮ6Zсm'xb ]Z$8-i|$ƭ X%u-[[/$UZ"_s~q Un8 )/'?uJ4_;__~.X\^-<1|}V3z5s9.-8 wVMꢩׄ猊qW%Y5`TMTԠwKIhnƴ֯7AA( Ϲ t?N%!68 Jc{f:zg oJa\'&![%,]LP\KZm*'Qj 8v=yTd`{&_; .NHb7aVcuw@yv@ ,*BJ%;©2Vpjb;H8J$B"1Ay1Afфt>F"OFK:9>e Ful= Cz2s8 7y- J$'~/6(f_ 4:`ŪnM%<yTRjAηjRx3;yjSI$`E!&@X4 Rbm"O๙|2N va Oc B0auC(eN Ƕ66CJ>ٟ>^#(m c9ϫlVIQ2ҦlM+"t(^-EnBUxYPƸY$eU?+n{~/T [(w#R-H; `M:䊳.)(*҇ۢe4cbMjxWP\3%z/YSբq uY!jAM&A&Hì;?M&=gj ׂ;OšrO aa%yl_^?wfdτRboPQZJMHk[CxsUQ V&;&jnjbU+bbʔ 赟R] \tP'߆+zp74cXyc8;؜vXՏ_| CF}^ <u I&Q*G9DTdwL+9@hʛ;qv[3YA],Jcvb֌Yg9GCOG ,=_s5;/oY6*MȂ 53/Kr}6 ttΗܵuwK exI˕<(sןwA6Ҙ6m5%bp]QZ`;Ff NDpsPUrP(nתIkРZmJmGj"P6s͙ Tl.COKU`~3"yy3oտ?Ww1Vql$쯐S5f"_^X OA4D:(*ˀr=cpV\ xO?ԺJQW;/x>u ;6QX^pD7ݛ qmLX ^CeM :kѯA9Hmm (mgI_Μ(JFS^8dSTyRrw`M>gJ*W RǷ~c,YGӌIVa5k0Gɦ0 ^ {c0I G&0Q<3VtlN %W i+H<ŝwA}~<'@iTC@$IzXA rm,H4ߧwy'I֔qkW%X#SQK ̉%|(X;|Mm?x܇PQKtk1*z1w?@UW-N SU|Ey]'\ݜYTq< *Be.Fƚhդݏ9D ke;NfV2^SFo Am*"U7L= wm'.--QދrŊDz=k)Mf̌3ByB>+ڠeoō!?j3}2q= gzWk1R] W[J:7,Wf̋PrAti3'ƞ _Lf 7eGyJrǵ }2tƍl#%'d VrAj%'0u \hvR)BϘ73kr̝:uye%,iyitVb:9<!>4,)>mή:v V`JmĞؓYl`d)HL{zId;c<UQ)7-H=%I[=d0 Ei*TګQ_!IŖbɏgxb`OC1c\{ʈ~\ċM7 =7~gW4Y G_c^sm` >`FVG?J ERIY]8e*,K#>7J0:#M*; 2{mGu$~&ڪlb~eFY1"fcߜyBJ5"=|@L= c3 ^Ɠ>Ck4^XDVQQIF8<|5}Ph)ZhcaOOmr&Vc ;4YsACgl V,#7+^3g俐TUI^!D KQeBiQYQi-aD+|\O3g BN>n۲aGVPбӋHZ,) ozh$QU ,c&֯]x}=~QhL\/U8&1!^>xCl%L]2]|e~7^I'AB/RD@bvv==ޝ]v *]z5${{މBHB@Pfgvvfvv}O[J(55Y5PJ>:3kv}ߢsfb"O*NDRP(IdlXq9޲gX$|zPj/<='A\EFPTedM':v` ZFT#$=":m6 2X1KUrm:pZ@IDAT bnZZ}=Q@o^^iBOݐǬ直YS'#3=䅌<+~x7y"OŐPw:2E02%.6{ۑC5o *fg S JB5{t4ҽ<$+k13" QTbD^H57ۊy!Ԝ"u ] }cDHWK+?C]^y3NO;p:$A"z[ZDVrdj*9SIl74=t!\V̈(3d&v M2fcH3hVi9'‚P aޠ H1:$ : ޝ8s]ظDdT 8xo1iHrdCZ~>-KyT͍ 018%:#͟zb `9|c"F4:k`Ut)M*!o=Դ ^ Lq-P)E1E  ɩ0w6O߻=\V@C2IkA=FzPaA@b;jz:ac >PiF$g =G=m x![8:vdҍt{aJTu$Uzer]yC$Db]bFX<Evvg_ʞտSjAo_6tL;"O q$kp; Ep)*,`A5oōk@.;P;|xhlMfh yz$I *T+OX;fJ4(K \yE%^Ϭ'S=@ n㍌xAMM7E P{fH+<aT9Zv# 9 uxC lkW.etxMX5z6_x Ęa'@J>25tٗ}Ks)Km$53<ҒD?`\TI|B4%pU pu,U,Cx:f`݀,'Zi*lh_sFy9!ME`xE!<=sYX]v2q/PBE&B+Wkr͏?fĊ;j FViӽ EHK-pr$2 g47V,zaOg7( H3[Uz@~:Ωwݶ \Qv3.m06ns).YEmߟVE!*tQ4'`j ӳ H omPU+{TՔ9"U@W3${~QO#N1k0ye:4_1 VrhgyVUjDUHqwkԌPTgIA? j fZ 9L=Օ6 <,W={)lFyu_+1!%U0P5D+'=~$gI TgbSH;Nc_@+\4кRAE<^d6ʊ,dT66Z5.R^Gzi)\sRGU ;톆4=^HKjӆ\D'e՚ i;0q# Y[ oK[d׵<(De*)VZ.ƽ#\oV܆jsz hѾ}WPz>svm8WU'Qy<^ZpF}O^.*==Q%0=srF] `'eւIfhB(<+PƌT:j 餬 4W#˘}czq:!.}X0#.CD4GG7OG:գ8GN!K>!pA2k5u&lwo~h[6I/_6}|Rs}tyځ\EP Zu ٴ糒>\PnEuC5l!)Ts`eGsK"ɧ².2%];,د`5|jp;3֗ [c kn7o eAr6;/㑇q",6Vd|`u-6d7[~8wqVT LdO`Wjy0T1nbb~?PS`s6ny;6RbD==~Y4"9fj0b>礱FZ%%`0**"c9Z[Z$W,ˈW%h\qN6ޭ[x_}  ;01h&݊}w~[>| 6Nշaxe/ЪְU4} ]  x?ƌ6<&SHHj-,$v,G{jT-''>-T6t8 @?}-6nۆbi08:?F%plIxQSۆ+cuֳ0AwodL[{oR!,MNjeX_ֶN_#)XsshmT+MCZr,qa(*>Ԃ[nt=Xd  U5ѳU}vʊC ,(r ,Nn,nG#@.A<2̘Mic ֎+QĔ>fPovO0̃1꩐=t~U ٭R֭B&I?'KgLGMt;Yac's;+={A *qS#pZ!6Q}.YlCœ47'%()HE6t,bO'mE1iMՐ?+4mA#;Fdm$RO9c9RBN{wmDF8wy.ZcyOIrb"Y+nᳮ\Ua@I\l(cZB C+A4i,4C"$gMM*Z[1 2xn=‘I*mk@p͆_n4$p16xs 6L$j2DdHN^@kWe޿K)07D$aM 5Gt7gK6}*GF`Hߍ?ف ~A^h;!t#J4cedc*Pilb OF|z1 3} =T(#""mJjZR'-8V+'\\Eע<|R_љmccGq:. z 2oJjMt$U߾fv.ttxg":ᦢ:Ն$gzACnq}c$,c]5:i,tf'bckcn'H&"rrR؅l5v8K (tRlC^ЩIdvR1k#h"GV>ߨpmGeg94 ,w\֡MB-' 4V؉I96 mD5ػ}72i1״U=/@R4w1-Y!0Q'ϱ#s4Ì;>JWNŁ̟m (>Mjw/`df_DZ)2ȢnyZmT Khp8RlYM{F4 /6zgͰkZfy.uڨQҢ'_4uN+IY˜7X/H;Z @pr+;7z |BYްK\qjsD9!!a|sʸR7oFXm`s@mђL}TZR1 Y`{=}Ap+y0'/=~l "i)٭5ML678}d _XQt3E.PNY5Q +z2〱n؉_ZE;62K,'E#+5s00fp2AYfZ3#]ӦK;;e}w=Y=W$?[릏߆>YV%:d_i AOvէ62%2h,IVhNI o@ N ;~ oSx_xL;sMn"CcuhUG"=ZIEz 8XaN$zjw#*Xiw!X:Ud Mښk' Dr;30<$\{LZ6P-t"hxUʷ?hgNRw|$ֈe_ʑ93i ض|cT(^JK$t^_<ՍEtmy/SdgeE,'`:TYhP `9*-b%շp0 KX>t!Z)gmYg-Z!b$Մ$% 7=<nfK kpTPƼ(|soi_>Jt R*;#.,̌[rID1`A}D8k8|%oiT[d?~*bⵝO{vC[K6*HĘPL+J$_ ua_]ih[d1IctsSs"{ނW$9裏 ^+ BJHC?S|t桳o%PUUˁG(?7C0=|˝Dܑ .XP(-l#Q9YJ5\%]xv5YY)=%F&p vְDRQTRZTQiYe'@[HU1H(hš \i!O"-#wEhFE؛U g%ec:slLdZ$BL!b]-K^ q,d`df2*EJ"\IYR Aum shF ԒJEeuk6Ņg:<5V@ kTez*Ʈ~:1#d?#X:ۨbMsI A=*OERJzZ^2:AuQ-Yzp6튕q&%QxzT]׌ʺ#y$8|8i.3ڜO%ai.p%hhC`;cocl2DmE\GXƇc^x`*Nv懟Nѐ›j*/;qtAܭqdf- 2--fԦA.b>2B߉%򊩂#ZZ<:k2}3|b27I[C =*dT+f vsoE`@@N1%> Y,TB|^QVkvtO>y pr &rGh\Z.N5s-͑iN`dQI{yg_$n++`KPW ^ @"g2a**\XaA;d [#5 *hVر1"՚;~>BS2t)ÙivHy m`jBb\^yض{x/a}5 m$S:%w>=H3ie{/*؁x#1%.fNjoIU5 zT%6qޚ 2q0Zf4wMc].'Get=om;ΆP U E+kN ~RrR^|q5]}o"ܕmT {mm ,'Ȥ[\DZs ٍ,,B:w^{N*Y<5-ERV݌M-نJZh?~wc˗ʴCaTZ]_($6 wr9l^88!i30' a7aQ=)a<F{:QdɖC=^fL2k!ê,bҵ,(&w<CXfz!'X_I<13g/ i:i xR#Hf:$~(z T33]4ji|SIM$X %m ͧ)mJB yb7GgX؏9Ȫ:)]cnJ 9/-9s; rȨ8HS!,&y)nɓhlR ߌZ+* ֺ06C| GgZ*Q_SS {̧k˔hjڝ0W6mv$+Yԇ$jb]˖ćT*E߿˓ipM$D!a|((Tj>uQ~ g[du * +' VZ0ZY:0KC1 v-*WP_h8u`TSeHB=T0OOtAge:4dhheٵ駡Ѓ"@avNT%7@M?Sbΐmg{c0VX:X"=jȰnO6Zk &! fXH2ŌI.tu( =uhے<\źEN7d"*Z:! k2uTZ9HQl& EutqŶ71VmhhP[YkelǸa1҆/g[h;L7#7(dĺsUH;~@ʜmVw0O-&/XZŭM~}+k es({?kN[7^~9Ϗ'xwH==΄쀷o06} dM Md h $D nnŎoc\oBdt,>rz "z/j9}6l| TcR!ۅ^ 6`ᩍWϜD[TroD5B$̕oK,n>*`nO!NJX8 ٘5k Aׯ;ibg:MOdu+"*!XQ~+WH! !JNOV57,Pf^T_BécDnq3xg8i1e(EVh꒬܇r\x Yl{XhOǍH禛J1V#S6sϣ@~׏3OJRѦU|’ՀN pjJT gZJe#;CB"hlYg5iko!#))"l5Oއ=8=DgKu#M)!g6Ṇ ٖN\o&I$NBvq=1 0S8tu]]QX-a{a \KaAЯyBmT2֜.IBXc݁i >5z0uUAX1b?@I?j/xxfR ؗzYeTW]AmX#2dpAu{3iP#u^rT7ٹJƮ'0w4 V&jCn{r]$'&_92s [A7k hha6.Aq)FDng9S6DSu4Kl Hhhh~iC'f"v̛B BcH5aq'\yTuxgfJWc_kaߧ4)bEs=6}g#u#cB wk o<i̕Jb"h!Pۮs>AP/2$P pm)Ec|&X jɧ=ċ6pprrcȠE5^$ Җ>ڸ2Jwp3m-TݖBۓEDd.gbotC֎:X>xY6@m 0̵6Çzz,VLVo݂عy琓y"TaDfZ GxdoGPͼq8ݏaq4 Ȃߦ7NP&vصg/󗭗@K C?ǂ--*}{y`*;&F= IuE:&Fd!2p7qgaS>%+82w)3xC5BqS%?-O,d`kLpRs|Ư*cD9=sVɓ}:Z;X)V^**)V+gv憵S̠rO`_Y(,mm̍`f~A|%nWj5-:){ e0uvl߆7yiغZs%Uּ>XWu5ϫG_yUDvحlJ4>oŠ[F&;xWÃvUխ{enqd;m8y|86Ws3 n@L Ʊ*<^:@F豃11-mÛcǎ#٨̧7q`T+QV;.[8y"v% zO+(ZL5 A1:I/ACM%\ZEͫ ADBP((xv#Z6t(XXfj<_I|~5 EE?|ZO9hOUea>2RA%7%lѫBA=|Jft_GY׽f_,ז][ (aQǺur;9 պc8u-(on#> f$٘٬_ʻȠw`FX\}~~jέxEuPДs))!رz9]:ȂVfO 3 Zp!{ޡjV^eeHȤק_DFNaɍk0;(`T5Ld~DFc*-";hg7@3v+/< xu8mk_#Zb슚B4In*dwߩJ'mTY /':toE?J%ֿxE\rUX{z%PEWئ6/T6'^#gL!ջFi+V`/$t]_&j "Cu(V;T1+Ų`:(blg4l$D8W4&>Bc¡_Ft1b倪mebX8n?&>}@©Kyϵ2 **ՖQ Y $ >l歂9tLm5b]yit#G]:;:&켂-}\w_C)DLb3e[` 9JӞf̒BKIA} ӆk]It"ܲ"H2=XF9گ!ʛ'Her=36SRRi(-MI%)zCW"e-EejfZtML IsPϱ#g ♣ZVWgsew46({|˺|?b'pn6(j㹬ߊbQԄfsL%@oCGc;Ke-ŕ߮}Z˶s.Y"s KǑB*k<5 wo .'꭭$1$VL-u~ )gdDS_3TpUݻ >> 9mȋ< y~ F-XԘAaJn'ݎZGbK1]H+BcI׍VާRN^gOddV h[Vg[=j{dX6f1&I?qGen4P*@ 񴭮)PMJK=y4A_m{>O[]y#ѣN3 { Vf Ni*́1`rU|=o.5gc襬 *8g'v88w`nF\'KP뱋`m8*V̸֥PyCXPVPunG/< otT4dsP|aŌ M*cs* dMe7\EA زOg`ML|/C ⸉] W0Id_k61<}>x; nط5_ |'r",2N#t"2#S 3 ETX-\$$w[$t)tS H ̅ד숏ywfîAU[(uՍY&r#$|xzeX6=ϗ5u>3GqNNmw.]1AEk'ZHbAwJ |}"6=]pcZ|9:RVՀ#61U|;tpE:ssATu.T $$$O{GW,Y vMKZcs{Ŭ.+b ͧBq\hsFO<(qPMAc:9vp&ȕbl2rj ԡٹ'Ed Wu<]63{uX4]f}\rUlq;S"]y$ȳ V8quO#D-RӳQW\iTL-AIa)J:Z/k7 5)~B sGf{6*rq*[J%i2#Zq$1 nyS-Z** Y`'3MJdz>rka'9u0.F'#p>3ꣴ.v o6nWA2#ғQ!#5hRe'TiѫehT9ݜ U~M[!JoK5W6HYUMbu=;ͼZ}: )63֚͝}*ȣb?_Kqݽ{faI4Nz0EmXĸuH6WK]:n#_ Nz@Lps3YYhccn$)II>)`ͷvy*UXxr@?m%zh&2i}y T*` ڲ>x uT8:ïcgdn^MXŶpu8*^BY(XjZ=TǏB\?"dD$䙞7EnA Z  ɦ*Ǐ!ǡg/xhA[%@e%oT@v hzRޖ~>^yŻ()*ez,=mG؀~˗-Syݲ{ZJ,[1|/#@4ݽs+sVPQYŽL ]miTfefC6He̥h10N7AP"`J1Um-Msi6uڬs6y&k$cv1I ?}ȜĽ~7w[0}jj+$+VE))SD/T~L 袍{7W_Ef{#zVR%#T'bƲiC[eόc3{I^!9TƇ@-̲6~$pu8zhGo Rְ0lx7n6i`]<|3 zNǩhɜoÊuw);(o׬ffG ʑZ.bÏ1"ZE]ľגY!M$Cv++-A@V>$bZhjNW$ZAҠbs&ퟹ"'9ng.7TK$ΚP!z31(AЇݩ̿J߿(,ژ^]cpL0f?QÙӧe?}ӧLv iY<0SW֖0u* b{8=ԳmO Dт`UlmNw~)3{U~'LZm>gmk. K!aA1f j"$wDX/_1P%}1sq?2WDjkx[yB&c5lX%KY󳑦U.`yU`zZR\ԌDh3'6ndHN+ QYFP9y$tkM*^q5q]z+)J]P4s$q c$q:.*OymA+UB3iA3Qh㯯o]]s2֑XX땓{*Ijk~2Kj0s=ؗ|v~֣GwbUU蕟At/I1fp6 2U:,z8N<BܞϘKGD-XIUTRfLՍ}$%k!ak'nNtL /CZcт4@Wg("ZPs[/imePAwEUax~e̞(m w*u{or+0k:PGn_mjMR[] ':Hx+2pچ.P)ʆ䥰?lRVaN9?}]muK^3ĮıA(G:&@+q]*.`<2%Ȏ)EoLע_| :bBmivki#vBY4YKabCPkGwu# %GWJ[J?CM4YEu(*߲?GK/>C.d%P ڲg} gRSe8/j` غiJݞVC]Bdm♧\?%LU k{eܩp'ƢWb6q9x>?^{2Bm KKKſʜpf,_v%q{\׮ gIEe;yVֶ8th;<#78$!)v&%)(,(:; R'^nj83XWѲ3f.oa#I0g1cxPl!7I 3LC_hmv▛Ӛr.sȴ ܘ9,*[RĬN7>I9IokvҲCL:LOFeu 4;щ’ lfT̏]͓<{J71x|ֵeG?UTVw9D[ky Ħ W쇮܂I l3}k.}Nqf hU9 V0^VLcRGض}ѕ'd]qΏT<T GP]]|e=/BH#B*Xpյkok^@AEQ@!  {;qb ~yy͛ͼy3߽sTS~('_ÊޝєsԌiy q÷^ VGc; ;b+eL"Zgu ͔_MF?g/P70 ]59UM`ey#eY*^r^ ԆjbP9%XTjd߼e#w`R tXdRa)殬;IRX2ͱ\ؚF!Qޥ7mpAzN}G+S~yqm9NXNY~Q{n^)aQ>܈7};w lSV:JZ |H'w/l0''Gz7;@ Ds(U:#d ƱHKr&~*V,wXH7_#hNwz"݊Ⱦvm&k2[F6{#?{DzްsoӚ*K3Z\ &ipwVT_odߧ3), %yϤ bؔq\;& B4cǢ$ ;dJSAVddWB(nͻg~2e^3ƎND2wpud/Pf}򩇐[Q\,mX 7p7`dfS: 4 {DX|M?ofq1BxTۍ^C &wq{ĸI겗7-[y^Tp-nա<W7_aW9d4l=-?jLp{9ʤ2NV'[|0߾KXؗRg իS)VTdw~t腏0R@Q_Rf/S1, Xj/V?UɞsiqUKN6Kɹ#$MWd[vTbIU7S{Kc"8n{'-^x9Sw. pםw*on-6> v0pFLƨ1Kű SʷP!kX9͛.|; ojZia^\<+dkZqs[nFGT2 XcT62JV hgNtH;v,Cimeh@Fnvn {GcL #󵔠eنl\Xib N;GMƒu*֣"" uiz=Td,1 rx|y舤X1}D:/`.bPTZ^@v;.?S0v(]u9! J^{8ɛ|D!p&d@7l^f(ϯi)#AthСd!HRPQ2Jc%Ezv5S]KZ0zHe]ՉseA8P8S2ݬ` 6C) +6{ql˕h8/%tXpʒe_\򀽋i_dm.A.'ңˍޣ7^x- 8x(i c?iLV Dz˱;uDDe?̬Xy=YvX{2%hu0nK׹2Mq!ҞsN?Om &uu!6gHf:,j+}LYsf) ]k|;:ceP _q^o6Sz8kӿ0`-$n}}- l۶5d zPSJ^B@4l%qm+z@Vg21Vm7LdՄ>sMU%1RVhl,pՒ{XI X"VBlLgKN=MLqR'ЎJeB>o:9C"H_ [$7?mل@/AՋ!jo MhKh~k ݱ~=ْ1a^=&R8y3\ f݉'?{V0;JiLXxѣsnW442_x]rp(55CR>{8.O}5oooL4FG,q84^.kN)(eu~5UQ)t`"7~@r##0l5bҢPzlSLaam+w$_?.J9 FeRUm<MEL\x-9f? \1c#P[/z"'f硆?&Yf]dzq>ﱸ!1z?ke>sNVƏ}_|[8*+; *ޭo2L)7L 0XO299|vJYqB0Q{o`l׎/$gU ?\7uҀ ̀3@3Xֽ 2kZpQEUyt@]d~.<{TvÖ4Ljk@A2~#+,vͯ-GrquK0BG*f m0ߝ|yU&~8Rc#khl[n^eX͌N;__W&v4';&{z2qvw C˯0>w" c׫`e,cv:[ØV:;ť8Z]"ZdϯǼ3ǣg$SA8fzd~T@ WtbπeS> }eoet#,{K%V~91k,)MJ/kQHSz߮+Dz?oVesB=ހ:]e$6}Y+ j,~+"`mmGZwo*;AdmGY^~Ǝ VDJ;1ERLRkE̍]AgMd2:VKO_A7ޏޫ8˳k'|:LP[MܝG5]N2 ;q<^ʦLuh"FWSC):aHWG6 I "9y=#h2;v$a׳TqGj"6lY Nyeap sq|q../^*Q-· 8vG叏Xij[[tZC=VyQdn2Tl?a)M hsqS Λv`vk ԏZz[k6! RMQ -{gu=yeMoSE9 SqE+ë\G-Yzzo^MQvn1UT+g=vT.Ep!C]9jQ^yqؚaɬ|; "y0՜ CÆc(ѕj)AbIRBm `??;*ō)lD'wjw /e#!. )Xh*t*X,ʺNJyD,@֮E d]ZG cN@/v7ј.y1wP6%2~dY٠T)ӻSt!>8ZV0e=QQtS$lI!Df5EnhL9%}VD]XeGMXۅʦ:9h|x OHP CZ%v NGɌ za儧OT4S'5 3(caSS俲Sڝs%~UރD+IN͚]ܻ}_Wd '(l(8wn"2VY}o\:s(j یSfPc($‡JOxŋDRmgM;}8ȲG:i.=i(ʬ!5 I*[!7ZG3@5onYt%Ojڙmj)`ei k5VcM&CH>ŻitDjȦ9`)dx&8?&1"; <ưR}͵ 1qq5W#c̓{y.v5ֳc>\js# l>&$ЎM}Dw>|- sK`e]`HZG2'XePW[QM )7vY(m,9\ĥYRy,&yt'L&ҤZR-ͧPVv&Jz'Y!+[^%#q(eJpO_}ѓ59U㽡*# LFSZ@&~9gp"s3!VdaЗPFNLV6s ڸy)ECckңw`ߚO1dlTϪ\a=ӦOD}HjG1R+(Ubliqij"Ys@D1 [bmhGfZ*Ҫº3)1{5~#p`魪o;~2bﺝ?|iLt[8mABA-m )0 wOʓ5ä$ W,xzfϚ#ʡ%Fӻ{N/`u>T2a a3wnBe[10wu6ـ)UT@0¢rlz_%DEUBzhMX'X>ptv.aoѦqFd!Ehzhۋ:eniSrq7ʣUT|iPE+#' mia_UO>튱˴ဧ#V򱰨Jզf`W7] ǩ(>D4!knؙ飚d&*qT뵢JZSǴFfU袽yJ; LQ*0CghݴL"C]+ǽ[/|nFy Rhiv0a1}k}Mƌ]FiLhcZ+g﵈)Mg,IӲeX̢Ip+$X(̹:yRwƌq s'QG#^T0W~6,B46en)^x#;5Lv&Tjq%uT3.Y:dF?RELwٚQ!).|iω*rcvG IRȩh1}of|XR] Y%Uu䘫8s2=J$m#+7g.? &m ظv-ꫲK"fUԒӪk+&$s_+h2fHG` 0U5 \_dա_5/ 8pvt$ʥ1LvHV&FL+CzA^y-o?C6'>w|bggDS64bϊq&m Aa)}"=8H7E;!:x#ԱRHYEIjq5qZ -pM3(iVVR40ApE6p 0DA']IGcZɬƎ 9`d=HiǏ+Sի^+k(*y<1@ڬ 9016$] V禥 "6IG AJGSY~.pX!}R#)1|Y̜5Qe̮%:7+^ρi9>-Zt-=~E\HuXX%pQ%g$kMH)="oI %XsԈ)z>_HXSܹsZ)%sr\^La _]]BV* CB9hu`R+'TJpk̀tڪ*i~w~oE_yt?)}ym>`*zlLPQ:$i ^dm;5j!J0V+ퟺ/:q<~; 'swdAڕʹ!燀QfuȜLn=Kk쿞ҥ5+3S|O FM9V[N/}\c::tǏ57ٻ+Z[|wU{Nu VIPT.6*%3`bjJCn!Q&0̈́ 3VNd>`}g^.qz:k^i~ 3&3/XwʸĠt1ј>} cA635Ě/Bxn\X.*l(""dô\ ؅< GLFZi0Fpؾ? #"(t2{ڊ7z/;BC5?[؉u},j\Y+.y#;0t^MPAVyVVJdUMS]bͺ/xV 雜X/~3[ܾ88 ;UÕ>S# i5"kvod绛ExG#":W磏:vګ.̸~g;5KygI0x&ڨs8V:0ja~ մK\ڌ JET&)Blƌ$U܎6ܣj#?\ ǢEGC%Z16gL_:lPc-8R 6_!:mY&}\=#di꯺jny"b {x?:D̳:1݊ys:~ÐLō<* a>;+5A#'(+I!ǷcKȾvWNk324$<(Ew0&51 Mr2*GCR>xj=hCu#=C(F'3+z"=0=XfӐ'bw4tK62e^Mfʼn0r'q<M5HI u0ɀ)jAsdHH4a.ŠGoo<:ixNUv/Ŋwi jYWcBpY .K\97^ǏK)-wUԤw&< ”GXL>DV`M C'W99؟'_~xC^c3e>}Vly1Ge:ع'n~ p/Hn]QÂMoӡ|ƑÇ@ܜl{{뙻ӆ1}"xPv]0o5=Ъ1y|08P^N P MV/$':+&M?/j4''c`7x·'8 aŕ#|1@507}[sg^w HuKkz(iI-Lo:j+JD̎m[ D0\"/r&֔jʣzJR|M5"/#m^HN3g·g &VLݎA?Sc3zz{YY)Y;fU_umߴ7Lqܾurs^c (Ji<XI'Ajw]OZ%*j }z|,7Z:F(4U:K]^~R-wE~d#*Z`zVi? Ѓޡ# [}b=e,9:ӻ$RQSO{JM;)-˪\r\v~# V9>3Xof~sCsc=2hUhv w>"X1>*Jf?r>ݬ\RDaGFah4uO cw"P-cGn #`Pt\MͱM,>"g-D޽ 382m6}p0@\Y@ʉF#Q6d+x}V=rNJRNVd2zDy_X1_'J ;EZT?s߰ppaK5# &0: ?,Vw ψ8> 'Æ_W2Zz%VK?ߊrP0 +qsa0_}=FI!6|'Ūϰ`e'L;7L o/y!^0]L>Lb&b4Xe>)L2ӧLW};o3I, C,̬#V,W 8 356lD谐^/YLIYtwݸ\uRʼn4m?f dF9C :[ׅ ѕ 7uKduwS@ZB*U}iI,2݌Cxn z*UƦ2>̵4EI[\_v4G=}U>{㭷0^~.w̧mu4&X[@IDAT˽|j,W.$v>PY{W LcRu3}}]./ӥPWUtbwZT`,s7GYa~NN3,$Vp6>$Eʢd2;N.,Ԫ#9vgڴ +3^  c MTk5UvLpp?v{:ȫhނ,|+|n[ΧC`OB~{ql 3stÍE]6-{TLYBRGS6^dQz06*d`_0 egfDu.)RWb"l LM?P9^ƥ cYERF$4SfἻ5G@7*z%rWoG$|@ᣟ~3.1[\^ Vd;ߟNVU)1]ta:(\R%0s&tn<62Μ<Ʃd$ E%/.{D%G(4<.}sb䨑ȤhuVV MC9X\X1#{s0c, PYg2x10@VQ E^mE| ray?F'b`eh#y n/QhCӌ9A.0vJ b$}&\tlQ 4F;9?Ǫ af7<\SP׺JtP>/eY\v gRmeaP Ӧhڷ֭50 _>{G"@&Ge-(8~GC0uXDs_v$!GUG2ifwiL kȊ򻐒MSu5=A '[lI=֯eR7lpeZ6}]>rZ̗~/ol^Kx?xUj(o,O| ֎Hb?pf. >Lԏ7AMbQvX},X.`ߡXLp}#%0iEjdəR.^fw؀)ǽnb֬0%KCt>__.rgUSf@E,20c"\ |pJ@> G 8ǙIˊ2ƿbg _@kLgaAl \ݲ vv62uY,I zȨS"C::|"}+}g$x&Y,?}Hk pjsJħwg>wtP|a;PҨ;@( $K .$`,}V$#+jbwv@Og2k^@gAOeӋ5.c# Rj@)q] J2qhG`8࿭Y,$܌RL7eם5*xZ.w j+ub2b5w(@ [-AޗAMfoB%1:y=u),'^HyI;qu ;9~`ݟ^7f#>UF8]kpVFz*; H9+pˏq>h'O^)im12[p7‹2(Y3b~J;WwHxiDzIb  {#~:Ƴ8J%'J nZ6QsN% `|ŒDݺpd=]qϒ=odG_Ճ :ѽPrm8|z-K`']H)tOo_ `z19{6M֓No9VsJ `}dܶ0A)ce}^;4XcȱJ{Q8Xa@judzMkȘѦ8ExL>#^/Yv^*:BUK3+&O,3'*AeʛbȪ%&IscQd~T؟T88wӅס_@T_S-\i2td2̸BBq&md't>/!m=ibRURgoL]/K)^Yhl~)ci^i'SF QD2Yu6`W(!UQe_XH1kVElsOwdOOxd<\Iͼ!X4^|R*RRafM&zh2!#I>E9YKKcM7c愡?{nܲ&BaX;_vb1L{e6rԷ#^5Y-[SZQB[Wˍka5AC)Rf7PF.sR70AY9l@RJu8tl@CF`+9t^xI>p}X!h@bNQI–1e-iC0,w*1oZ|>Ϛ2䓢ߖժcahlF`crpB 6ӌ|]Vt/M.$ !|Q"m *:ˆ)jDMwpruPoh\{[bgJA]Ɛ㚾M~W^W:5zbZ|nFTnn}YLe[+#|y;jz& )f͍ǶY'G>5I->6L:?$92l`9YdzڢEm` Êim7~t :udȩxEٓ Hl)=m;J8L[+Oj˲gs:mYW2R΋LP./NP2)ӃYa#Ue( 8][ݳm+,4znjGBWs)FE5~)S %OOJGS]Ro;/M<00\ 3f`M<.vxZhLɍ GGK/_}hOϝCxŧ .b]ʧ1cg\UV~NnsެYefbة~ e.A JTbI#r4jՖI}2 Y)&kouۆ/s)2)&}-,?Xj 'to܎d;`_R<ҳqd=8 ^o@AZye= Ձ_sMW&4`MHSݏ'y p$#PL<ة0f՘Oߡ&BN<25PΆM]25L# L/]DЕpt }75I5ee ؋%&1&sj"-liiٳ°#1ΚEO@ֶ6tvuA iKXx0Dӣ{5"zZOLG5?Ŵd3e(\ut۞X^sux)wu2{qn)No[A| ^~fϜE P #묇Y[/!*,6o1[!]dli F]%j2ZX4lFll;nk ڔܦ-YTm~ܵwA-1j\}񽖞);-?rLHEkWacƊZ^wãϼDV#FHJM7{3Z)Ek,:gX|2免1cR })Akny\w<h?W~'Z`,]:|,k hjljOq:0|(3(LdLQ9TCl/mSD1#Ǐ,)ǘƆ;:Ӆ 6U'|A6:Zp<5o 5$@Зv|zմL ˘ǝ![Aտғ>M 7,Tt:[jEc(lSt6"Fִ"+}gho"J~Eߥ6jNMZZt{ Zҋ7ehLҀ%X6)z\E[ΚѬg8̶N4vy u~߈ te~벶Ww}oCqa>f] Fs%0tBiQcZf*ȴMEM@+9H>Ivk mK h埍 Ϗ X0k^fQt(oj_MW)Af0I}dElVYgk9Ɨ8kx)~]Kv˹lR JgߖAF3]υxogcͫPH VB袩YWb539B zqsl Xۡk1rlҽ*AUcx 2P(A|븐:v A0:Hy;L.!"::ŸUz'jFC[&W󘤳(e?f (ۛ́Jq5ѸឧX?b %c1 z;Rf3Oce^{)L6 lW mAQۓKެ?A؛AIP@ԆwK75?|VY Kť%M܉75f?9`-N?V=9H-Z5̌X5jr6oڂo0 kUyS}+y 42`;Zpbژz_T&$c/pp2U@LՕxOe9s桔 6?]@ϕU-7#eBf-_K-NXeZ9}GBfF6n^5:[94\L@~( *{n,/ZXF3b%gЈ h$MAֳ=r^~7}%ivrΦiiOgmdJBɉ+U463Qgj@OH63z>&@ {)2 ܬ! EYA*8NӒDYRu3D /e>: Pr9Lާ00''p?UʏoxNjN jibZ7,=@sn1jla_q؁=3 K/UNg :`GܞQCĘ ʹon۳^Ѥe&JM qS.Gf&)D0J(Y扇)(ULxgewXԳo7|$%sYD.YQk  `;"JYR8U|w++PUll *@`ݦ>+qч1oij*xң_S@aHx2u|?z]dq2)50PNwБk?[7ԬuCcS5}^oVQ+5Q):XBU1$o!lT˔XFNWWS_QmxBACޫMUdW쓨S#Eg,1_7JXby?f@K<8ViqAdX+_}+fgޘ#u?* ;_Fpd}7^OX(㭷G 9f,WӉk ?/l%Zw۶hv$h5U@1ЮMk-Dw!ِD'XKWv$P>Ð~8t)++2z>ԛ@Újɉ˙y@G*s?&$k̬,K*QmjA1:0&Hʼnv*BgNxZ@l*2PE <Xpt'BX,UWWa;yUcځlM[q ڔyK+HCh+SV-r>T#7%;SȐy4Sjl߲R1,cFK#m[ۀb{*Eyᛕ,c]k_DsUǿ`^SNȡ]D=9Lm޼,& BGxzDmv_d xņv-Q S .=MېVPIhCv1c֝b02YL>LhWsJ(ⱳy^[U kc eMkhCc#8ⰀSr d1I3Vӎ9 pO]tԣ\*S1p\g' $"L {kj@ƑCE]s;~ڴ qc٥G{*M &>O>d`ό.~fTic/1Fn`EHn@řh#j>4wiS *u*r 7Jy{]v 9('*xjiC_\`|:O] WYE9P;ݶw[3ʷ'eh*LY ŬGc}\ JVUX @O{ J V=(?*t2޹|-9CZ`  æR^f 9,{a ZxQ˲g=ap@$V c{0lן/@ȲF>[nk `g}g?8 #,&WQ/sYE"Fd95*ըGŪ.]{0iJj'L;~8DUIa1 ~6F;Ya9[+1ωߛ n4R.מfCy]ɠײA502& t\uM [T=t6d„sY V?:Y0TV=7hi^|7ӮSlc'<&b_l+QuUx_jJ+97f?mF:c|j¦2?ڮ,CgZ_tU}n]*L Ovk'V6N'N|x =~i#5N=F=*!8xZѭm`@BZ,`!SCIj]ʥ=5¨Z8">K+gmq{+jٔscD?q 4nlzc.X EEߺ3d? -Oۨ^aê$2xč/ GEe9q rWwڵ[],Xs|fgƐ5beDޟWFlD]l[#ZoDl#B`sodfE7nɞ3#ѳ "~"~ -nȥ4]9WKYeQp)tH{w(}= x=ܞ Ț|01P:_/eװ辞)qXQu>~F5@`X9* RyH0:$dM=V}_ohXlbD['dun+͛3JSr&`<@E)KKK# b3ԮUVi_>쓐~7w)9&-5Ǘ"9+6=x-,9]ٗo~ǹ2 Jl 9X=q0XOս>+>PQɒy;2;u#_±obxh>tZo Eէ9N5<6$>Tzdž*UM^KC !2XN*Se=省.Kϼ K(l0|ri<FXeT7aJʱZ KtA_ Z-eUxX_vh) %a>0@bc =[oClqtVc5]ux䃇\(36L](i O' -S4Av ܇Qv,ȡYaIuR1g|2, W]$ [Å =5խ'WovqC,GkE-(bu.-Ÿۨ0*X]Sy ջnYʹ#cYGÊz~Wۣ_&-O w6D}jI}c`?TIշ"x:Z \l5UaQs8*g):2>k:BiL@ bR2Je7X9{2/ibs;~Z)f/Zl~̯,*G>J2B[;;1 ;^TK{93#7ǰ/8kMeo v2W˜G#6v/"|@7rpmt]`j?|# A8agʕuض/\eRVy p_Ap}: B9j D`C5`i#B=xn@\ Z+57Gc^e. DfƸ9ٻ?4zUZAZhH(+D~y}뉑<(JVTU"]hzvh%H/vQ"]NŘѨo19ͫ9}! ,cW0[bҥMÈ Όnrk<.&wnss]i-Y+1D/%釐H†?{E~ RIH Лt lػkYW]EEJSI/!@ Bzキy'LLBwg}ߔ;%3s}{ΡKF_ܽx!}` SgɽoД9-\G'T"{j۹fS u57YIqQfaH~6ꒇB`xdUK˙W۫uC_y`bZT3v}~w#=-]jyvߌ) }w 2Spx8 i=K>LVʒV0d8J6,d9?f&ö .RLL0o<6 }6`sYV̲dkJpL*M3дٶQ4gP3thSGάzi piqגh|LN3 tpXe_67̹ 9Ap_7Gf"L"\(Caȡ|ָ00d~gF%QQV䟨CdRB(k\rLD^f{56M96 .]?K:ܴmE@ҥicp  V%d ;y͵(Uh&׵u3sjt5d+CP^yE{@"A>>ؾBT03iSC0速*D c ÕO> L,ҡßUvl, 3N SzJB 3s ~ A=^|.Ƹ0 o]tJA3>WN#aPUv@a_PȾk빧$$."(/w~QfmZj-x#MSdnnf=":: aw۳+ŝCP؄LLdp0#/.x`n4;a_CbT -J߰! e148:FШж㻜/IgqP$B-D3(8bfWb:0R@֫Q*3O@jTԑz zY` I8OG9JT>wʄ6U~CLʨ@8X4Qַ=X?Y]ިf8vAE2Qm'VHL?Y_}}}#=DŽr5E11cBյ(Q mlz]E@`&m o޶ 0K 0XUtrzZbQŠun+W hiCgoCL|\r8y`3s~pQSП9Zh?OX= o'nI }BH%s:)X%~!j;˜l/U\:yNl%)r$*e"UGyp"઀*.f9MMmW_4Rɧ0X)xU64T5sO_!q2-V ak~_1cQPDJUeV8ux2At&󱐲*oOˣܲfDUe5ʳqKp 0> uXu"}F@?7#8{z2YQ1L;16$.7zcvHiZY ''gEȈHȃ^Ϝ]-(4!@̸):4׳ǣƼ[Yf~ P(:;+?g-0!U-+tN9Z]$8u$41Ä$G?ym c`?jN\؍!Ah(-]gf@ WG3IdZM) p űx d76BBrrqfRT?F6rȄ542fh5p;^" 7;,S'`hx-hyPs īumKL#XjFH+A*V6"QQIPCV}a A}3J8BAjla{t:5cܼv.u9t*H9`)Ӧhi(Üde[^  E X "91E,gYY~Qu6I?]J6SO<>nw3IVzA]h| QW_ #&@ɤs0g03F`rc= >Ad9a)zU8סN$ Ich['^'?O% B]8 xr0ʛ 4PX9E̺*jCيsk,_J5v1`X 6O?bNLW+Hc6k%v`}[q+D!!>Np}?kV;;4#߱G1؊0|e$fUf29 iɃ0 zu8$ssc *$!TҌ8=YӦSg!3qTPTM#H]+iSœ 6_EW^w?3q564d($Y"KP:ݽVo2_n:$G*}f&1ͿlP\:Q0[YdN?bv@. n!X:Rf*i2$b`87Ԣd0xy*z%bWR~[C3&lЗZXh7vsI߷gN1]PK4zY'i#u0,PSIUuZ2^ 'MMMe|>UVI]n Q?!H.kSXFs~oK 4|7ԖeJj3:/pxJep>}^=6wRGx!dh/nϪZ4Ut؏qe{gUV2܏D Ai}z_`m#̎JƤI7ȬcdV0&dбxQB1HBiqkϩEלI(N8f0O:^Op{)ad=sy#o]a ٷnfu/#n7x㯕 UБ΂# Qr#Qx\ `M|VU0P=J:e%7yG*6, ]Gsw|F.\gΜfv u:0E ~&ȚIVZȅVח磶 #k?V'V" GzA+,KM8rV/9.K%#^m_<\0I.3gNǬ\Qi^?|-7a2Q FZL,I`8v(3x}TP9Cv%’;{sL9j,uxD@,bRU^T PUD:WbaSZ=X"jaxJ+H:}&٣!|&ۖ-O߁AZ3$c$*jر}7"s(O i ֛y~4vE :- 3yp?liKGIޏ$jBOZC vRJۿ W}QYY =7F"#6Gq*>E(T û(5 Dg6Mkj3DS$dihxR(+%^L8n3Exf/O(e~ڜE8d Alۼ 5BU -pq>&amU_d\)TJ s`&y,X!ʋr } &J*̙Ol3%jgAnbPOlgXdj)6^NTQkPBCbqeR g\ן2ںzp `>Cw٪]wV8d~SW9#Ow5cvڵk̘!C{ qoY '_2E64cϫ93[?'NL\*0gysTٜЈ]B;qSɊt:[tV$J=*@IDATف%H:{F=GN,CFǜٖI<4F}>7j EW՛c)Qp`RJ  ^F 4Sco<nj˕X|F#{Y2H']Hu[Z%&y<:g_erMDZ87_} 3o @Oxvmc^sIәb *,-gdRX#O>@M'o][xE"{b??JTy:L W nZ6m SYɪr2!(2TH?LP{r֋Km8X:ߚ|5u/X!۳'ْLDI{۶w,CkǨn_Q1cIU%ZVa YErH@֮KWmD</>y0y:U*ZZyibdxls>,\:r1~b + _ŒQc7h9MA{[nmc dOH:vS1<>C&ݩ'r?X؈LQz8;eIcDq|Z74w9cdgyy9a-uf&-Y0m݉)odb0~; iGU> Ϣvs`9`$\d7J97=ٶڷR'O^C)}:Rb~/wL `"WX?$]q9|9DY)|oՓJƵ?Q0_ZL&͢ԣȘ~\vT~9NRXt"-m7ۋWƩVפ#{ ~ێ)d\T"# 3bv춅+LV+; c%?_sKm[&u섖KCǢ1oR{/vUϥ=W_VNҦ026O1Өwu)-q)SbUP]]W]j| q' m}?ŞG`9l1/nY7;5c?A Qq"0srENRWB/ˆ# +PL76l&c0Sz2l c^9KYKDž21Lx&FN@/+oc PZKM${Q20+mÆ|^X̜1|P_€pD].}0mDe;Kt±dd'2Ky}̈́H_G|=a"%0aMV~%Ra ɴ}[?YKll?n<_^ #SHd^uw7Ķay >"# ʩdhğzc:>fJB>nhi{+1"z?{^ՠK[+΂B7LJkoltHH'ZJ L6m@/_ $5-QQZ[+1cHmAaur)/csPP[{AԴ3>*:5\O9$Cޫ}2;Zb퓀\Z_d EaA>N`q!$(+rf„Rh6>,@v34`n)ţ?!SQ~qѧяrkn_4E~c7e-`GNn(ypdhضĊLeRu KuHtV9. I{ց2>_)ыU-o5! ?Jve) f p`J}1At/J  Zfv`ڴ”r)}Xe ZDa fy/dE3X[yܹa%x_ Fn^lfCԀ}mU6ڡo[/Fފ,Xðy&diŇ߳A샠siZzxwaLO2NSrZOV鄳b,0>Crz.)Qq K e9ܾYp HHNJD }pDٻg7E[p 6c&8nZ.`mNs#6h=LOfL 0ZI:i&Ϣbǎ ZvmS 'op¼q|/W_CzJ*|: ̮㭋,U*jM5_z6Cf` z?}1.q/^8ZG3Q͂QEʟ0W?f7&qGO!8'O¾߁V!%TF0!#wy;ӋًM,Rڦ;#HL9CVq$LJ3tUyZN]0so 2<`5I`C{QY.lxJK/Sm}==g`dpVѥΒD5SR"k+W`]`fE6T0 _߮wܟ>}qj/=HAy q=W)їuW◍k`R? έgF\f8 7=qS *w᥊ SkeĐI{7 V!LRr9ގuX_dHMXe3EYSA֘X1i5E Je$A10sa[s̹Wpo?a;,2ᴵ0cBd\7f)d:'Oic:Y UkOXVh+cGe%{Njd?GV0Rk`l004?\TZGO?d\*LqwYYٙ3Sa\ cr & V3~OLh >dG&q%&C9g䔉E7?^z6e f^cRRT-@Tb4Nme~ﭓ,<ش]^[IYJngMD%$zIUJe;Emwzޒ,/ :TE%{+P;JW skT ʺ?ɧݺq vLchH1A`DI3r])@Ӗ덴b]n>)wN&nV m&'U>AQq$2z8YI ypv@2AC-&hj=&5.CXu#lIh!0[5 @7L M$nM c 3b8*90SeT [!p8uMJԉGqÍ PavkUMwF𜅌AhQ+՚pj&f@us,\o=a{1u)hK 8H%(ޣNCbJumF8q +oT +'k\tH0!-y2 !TZ~`9pɓa-;]X\@.@&>; ?OqJxΉv/nK`53G $ں [MFSJ[ɧaM1g0ӂ4cdbI~;CQ0kZ&{E.}31j8yJz>]#" =/8>؞cp6#a 5%$>%H1Y)glE|^A<8ާ-Ʉ7(<LG=gwG8+J[p<$Jz99܄{HF$g$A_i4ϱP]h|"P|hb}*g3 ^EV/ڱL`(crVNz}I ,+c'&jMOlU۔G=94ZBbSc]fM7SV?IߔBZ"ߌ1GD[Rv䦜T3hJt3Jb~}`N:{}@L8 ~݋S}{Űgʉ1o`FI-tkرҿĠGW(<< ή{rRGo cғ\j]Kf$ZA TDG7bO>KXsӭ *u|?o[옑ˀ6~.O`ࠑJQ^|4JIMI_OT(Yx1@V/=R 6X-"?, 2kZEkS"[O: 12Kqm Oa꒡I'69ppcPO NZ!&> aa(qU4DIu;"M ,A/3Av skՄmove嬬NY%QqfܸIl9@gS34^zlDu1Y }tGP ri>́]9JeEe)ܞqIIlEeޭߚ+pYR?KrU Tv/&%żO?M~b;ȈՅ3Ϳ cv1s0VNf1N- #ى;> }oSPY J䦤!(lX^oYLL d*r5;b+O` 7v8UXm Ab"vHl0ɏaß&;eߝJ̒º[=r/$zuPVԤk>,6}J&NLtfN3Eӝϔآ;3{ vU5̘аoQK{^}}B:pg m2͘ zQIea谑dg ţ}: (%{L6dgV_Rvvv_m $ ym_KĆ{qM7a^:=r,>yoLZ1'k\bmj+0VLru3(WFPeYOظ]5u҅+";`HC#;؉}ꆝV/ ֋ê/^r Eje݃W8+ݼn"A-$T1XuOk/7)vIB^5X\<2Ӛc G`yRБaRM EJZ*)Y6X]kOj+X#־I2:}&&.m c+﷠̩ǜW&=;f'ze @c1>y4RXdڭ#'3<` ̙1߯Fa:;9p?**7xo4iV2wx*r~8 cY/ؠ/.cPQpUI^#h`MqJ-HBf$cq$̜"TvSǍBDIiʳd˄3pcBGV[IR/mi!n˾$_T&(o2_Cs?~[nDyuѴ܍/j}L!\L6i]jvf9`ȔVuZxhT }PG_(cFi?3?/C';#dtJ`\B]:xddee(Qؾs75 33 K;;z僭 /S zZ9^`ÐNtJlmHjz*Q^,nЙwX,ԅ+GNe~|v^wd~ }7_}= s´ֺb 'ɪ2]35*ݹHs4߿|pFF& (紊r:x(POټ(ɤM}7d*ҁoQ.nWϋdf@xݪyNkVOxJ檲ZHvΰf]Eb٣`lܲ~uL7R/{6C̖怊^GmNY GkÞYLz.Qը,b`6jIQr dw4I7_ ^}EȀYU0 3Lc`„TDފcɥ¶;ۊD* +t\@6U2B.KI ޏL`@zsAAAe6$,4zA,njpC{+n.FYr,9/Kaar=*R&]]} zXՓl\Mڙig *+z`:{Vu?3G}2[C;:r<(Y7׃ 404( 2(bFV. `AǮ iIĊ٘ z{PKaOfϫ_}Ә>k.z7+p]uzOjr>/yBM/GБW1ň_2udef(tm\.*wgLI>%'!= Ȼ5|1w, RDw\~d.2KsڶWLʈg`?֡ǯ˿A6:h4w۵V|@cdwXP7;vG_>3~MV.y>4QKyPhe;ݹ:k kR?~ώcO/>GH6H1fH.k,S4KñX|7q×aAnW/m|@b#>X* +)DXv=ܛw"e?G_LWل4.+f̟J@~ xJ9J+ Jv|ɲReJ5SLֹX `?YKG>F2 ԪW|J }W\T/TVR G(?s@t[Ӷa.n68/I#'LLB&=uc_`Ɣ:YFi-/0c9iu5a(OA6u[[oXekR q%K2[ASSsM~X S3ӏ wO8lzE 0C82wf| V3 +[9.f\`"qH@Gx0Ffq&@(TiC؈ j7el *mM籁\ _=~کX T%F)۪jeZkwV BF+k{:_vJ[9e =!`MUu uj,%zT无O?fz7z3 IAψ6~קּ>ǝGǣNHR*; ;NA1c8_"y"!@f􂿛3 f%XVYY owk09ɦé@ŧh3+biXW4jD"hA&n%jElgɃ V M`zܰ 蚻a] `o+ 29vs, ~,dWS4lAhHCѻ{MśV≿dFphlxnڍ8N>L\0DI98Ir0;@lٯ-zz-GDQ^bHd R TC%öY˶tl& ZPڕ#LYS/XӮTd/KyM261Gc%V-ܙ~ v3j ~L{ 3-Mk&41gMfL֮h3U5gjUzOFad򲰇AHsWAĒ^' r3MG 6\?5Pe]̴"CWJiQ>B` .GzzjW%1$)\TAD R@i--mdz4CKuWSRfLU(2?d B:78J )@fc '1śxmw$>ryy dǤbQЪ/B իE cu͡ou+W"((fr0AY^v<,U^4;5$.8ZǖTV^^AgY}+ʔ@MpFpG #c*2GA*OŦNz9(ϱ%W ;ov&Ai~FO?9m.)6ryQZZ{\w&@syA3 ǕC#>?*{f?UJ΍)7T|bO%K)xSx P0s ;L=2k-¼Keo1*31[HوǶ"STU] JL2sfhk>!xe~Jg4:5%9,2)1܀}|`rZNP9.yKu)}lTM;WGoCt` $Bkt3Q{v.WC$ȻQkquڮygNSA\Dָm{:O%T0&[8MGߛ m@YKÁŊi6e`OFܥpoSY9B ?ԳWVJ#;8~<f.V× z1qQTK!XW~<Q{ab_*70X75 '-3U-**%Yi8/r ObT&ϙ SLC'5c06x E!juݧ#%2OIec# LXi/+qksX;*(I`rr'*b*>zn",clmj!Fi^E4cOҗ5ptOJ ;#jN;{,_i8x,Ǝ7z YH Ol(BLZ}X k&f4]FA QS:-Í''x)3y@kX9{ѮK(iDjr***+PS sS/*dsK], `KE#< Ou#zq4jL -58س'%}@`jqc !&q(T#ؐ4f"*z8wXt?A=1Zwb|fK5H-FtÎLL-c)#c2Vʘ֔ç ja2!a_*>6L^@nBfAa=Лʚ٨v"šhu0r[nw/EN)>/g[J+Jz9tI+LW6cULv+B@:S"y!es/CE Sg>q&j/%jKdOW3杛 Gnꏸ'OlnħCxaQY[1vUnida\ECCSI,s($1qxhTxB#%W5L3l׊ZMRZ+ N> qPOƚyL0潕DO{G{"loo㦠4hq@,'-%@'TT\m 0p-*o9f:ǭ.$.ϒĭn]x x O`PILO%۶ЁL%6A>p<O/8@Æ H)V=mC:lqsQveUGuX7c˪'>+rnd*5iUk& iتD@yGZ]mi[|ۡnٯEw- [_WY ΒόpPgfҌР=zO#)Uk7P<krB>}a8)QNRmf(@E! 12&LgwOL aA/[`;`0h/=X")2`H-rz+VRuy˂V9+ʺIy*L6@^ 2Rqde{>߲)0kr +Q$jiN]zd8Juu03ia<;f. cc<0=ؘ|F&fC7WfeL6Bb^?Z)ַo_{O3Н'wFѓ9ƬdКvcNJh"ퟃa[ >rW/c84 0oʎNFY1B9Hy&=&壎i^a ;{"0*t,)b̌-;~Ffg/o >*Ϗ"ʁd/vij-l,uyanW%Aͦl$ϪS9) pz LUٖ+)'1 zJGf+y /}D Tg"08bNvP]:Y@6Jr:}Sa4p]m8$冯_ o/@HҦ?U\}rCF2%~ D^[ĆU)rl`G02Ҿ sL#+7CJ9>lEB̓Amۉhb|\]Qf?a1>(2wZS}.dҾɺd0̝ҫ_Q@Ѱ|k^SY&aQ} ڗڶ[j(kA[ёrUq(K= kd6GZUhFx>c8D2lb M;Q 57eFL?_Tt'b"-8z(+i|1=*gXȁ--!ۮi@h@۲grW:ȩ)GtxOfL,1 ^2JX]>:-l VPW{ 7`7Q noj`2&9v7m 5Lt=[}Z0|LFTd htD/c#̚6 pw(*{L`;vMN^a5Of몕 kl &tdT!|J?S۪u\*z.lfLd&v$Č,wB,ǫy:p5$)( ȑXKPcZ(^d/^6dڬG:^{ "Qg 4-}q}i{ -xe84mNA~͌M@_9$b@[w s-) lKA)=CؠMvb-,_wx F!vdsjߌ(C\_ W#r=&Mƕmݨu7o??GOس Xx";~eanQ1u4e ȘCWK9G6=./ޮ:]!ޖM&#Üx}Liq&l#3rl`I >HOˀ&Mփ C `9{w `liV"V1VbO>9n[o ßk+ؙsbA9drctu G4qQ,tU7^vBV*fEGUIhP=u=wPrU뀁YUvͤ#3rtZɪn/NY:Z~w>^u+@JJV$/|W-{UUٔ+eޝOa?zXp~ֱT:4taNV8&U~C@FY.)}|Y5@Xd9"'!@IDAT0!S*vۊ "`޻ƒخ4V(kJpf\&`(Co/oM<_`U>f2>;%cu2 ǧvoޠē#|z@oW~gT@Ͻo6ߙӟ*VjƠ3Uس}+ۯ jg`nan8@'(jR >s0Hi6B jJ v}ՏSe 9asãy>?du5U>\ `c5)(<r2L W/ VX?*4{ޒP4)6@et~DZcAEEP" RIBH{7 3{w߹w9gB/Pʆ?7m:d{(iLG+&t8`U#׾OTf>_{U7^Y`\YNmkx9IXe~> 9_V *'CIrԯvtK30Jcc,ZsD|˔&GN*Z#|x#O'P`мZ`=tz3M/4J_B| \N8F#ia I {+vتmƍobuW&VvARĩ_O;HYWm0YTQY;5P.zl,F c|q`Ƴv=v(^~e,[5ʹ}D{}v܃[`$JTC U{Rʵ$g44curd8Z2t\!/F%B lv (h0A } g)䓼jK I$L=~'c7[44"*1Ɔ@mi8*KGzj*p4W6oN:3Ss^V<,(|%zB|j&'bI67ŹL8YBL˨bT%6> /8jp1 po0O= RY %A܍ [r2w Nd$ן$o$Óx//t X4jȈ245FDÄh,~12Ya4O7d!Y2MI9 œAUR4D=z(ύ9s ː >dKΓ<Pn*<"+5AQ/JdH+BCp̙7^O֬' N>Ʃ~9{?xLEO,ƳO?>DNϜ={ q~pl_+Yx"cր"2ɻtLi_^+[d.}.ରC ϒv/>!h S0u"ɡ`ZAv)wSU߄3~1{ 3q5?/nɳ]d Idh -KRVz+bH'Vﶢ9ot1:VӿA YHIPƷ*TGC4U|W :=M!40LiMŧZԓT 1}D鷴ϹeEmlgUWK]}w y/m7&^~Tи׳v `R̜W>u*쮳\0'Yb~r1{߅GԸoݤ8u%}{H_n_K7?_x>uʐ]P?|u[Vc2˘\F S)3"6#Q^B5߽81TV)+Yμ&>˯w(>zxOF!a8y81c:ngW= H ?+,<ٛc`bfVO[;{Nݷ?GT%F b!#u -;\VFVHfȁkLP &I5ΜȤ0}&'1K`T1*[ S槣=G Ѹuˆ3]Gw- 077Aph(Ahgaݷ8g>Mu= GWdR"Q3"Q#Fs@hP/&:Be$yCmZuUl+/eb䨴'}e"yfg#1;SFUxIhOJd2vc@icAGJutv  2d"領3 %=Ww专SGȉ׀ۻ(=3ut_E&>X:;b8eWS8 y$R >ȆsJgieFNd7`p+{-."8?DX/=;z(cdo0¼*0WWS7o[cA~DVVЁ2-0:) 8 n:96/o/s&y3aLgM{0`k/z> :.j{jމ,<ׁvs#3R~ ~X΋Xm 9W<[&8gD˧gO`U70<غ5j4N|g+­,9IW,1 UJXx.VtzSK]dvFwMݫa^ `ڶ|Ft;ua~ßϕ-ˣǿo\I6d*l;خBJ z||(M`vk] ${D.XWSAb36EZF"##tb}=ꪛ} JB>QV=cl[z;VigV~1eطehX11#&sg"&FΧOSޢؔYs.q^w%^;+A'yIUVڹVb1h᧞}~2~%'`׵YUM6)`P「:!v- :/zra'E` 9ER] @W]=欏ʭ˄i%Wm᡻_@v8vK:9_W jwaVjmV:R-҂D\J-@oF0Ԙv.z$YGƟ>swRaWR?l :o;VUYۄ?G3gASM)< ɗ{b/&7^ ? d EϺjb}} hᤃ~SU0m7}|X=q&8wL[C&rrh¼1MLV KӣsHRʁsVѓ%6P jރ1ؗ~\`,9aTv GC};tAF% p2,=qH I?)̕ݛ[_u%  w}HrF%(blNq88DjkyOٙpfx[žsEjMF}K=)Q'p} pe?~KncR ;`0 pޯ(i6̡֨1#7)f dcI0>UD+ YW+\`AmL`L歮>R">Tl N?U|՝nak~SWG:I6i_2]m2Z+hUj:YeБZ Zq=tdp΅QR[>nƻomw#ұ9f l:1`΋(87d6Fa;0;8s| hbf(|$fa$ ~!^f.21#k yT^"Vۑ(/n3mAS/8^z<}>Ǩ|&!ȗ+4vYGo&C?O?F2Gh`ʙG1Vl|S> k>O2})sKzTV5cƌ٘5]8j^֎ۆ%b<,PxR~c *u2g;nvő:755 REɋmKg& J *6fp*`fD~ (A̹F _æ*=y򳏙*$0W1 b)Ds # 4߅c'Q6c=z,TTЈd~:(8Cm9mS"'-ˍ- 6#P)TzFH*7Ǻ}!c b4:Ή &)f qs$ ʹ3f.k[u4 s`Ƞ޺>v*,~3\@Vְ ̕\.mΊJl*yim. 0џukׯe7dNуb(&.i-^y-xy13_qpjؘb]ɶoϺr/0UcoW:3&Mj^OYtƫxVz WY. Vm565kH;vh/CbR${A!.WPtssVu?lxor,ja!JSwɾ*..:|y..U6W=kt× 1Λ=QL?!Hˊ]䒇+X~׊nvA Zq愲^(b{ *| M@F|OiWoR+N(2k]? 5K#+2s-Ij3Q=v*r1OB^Hؾm9--WO۾#f?-V歓16񫈔lSOmLPexo_)N З穽eO3[Va}q:27f^"|߃Gj%٩ p`Qp0gdBs%dںÖ9Yo[8Ӧ%IFmf \i KH\(bM(8atZ%Retɘ3#)"g*ȼ֦Ȁ}e 7e!zkP {| );d>mf E, 돍?Ĭ%K ZO0Id%fרanBBcbR ul'ό,xTQNb| ÖAʦٙSDW)㜑kB>ea"k9ނ3?rnAtm.z )ם`ChZQ $e34`?OwC۽9_aIs$d86!h,~UBHVA*w 1x ИX23i # /JRf-/-喕ܮg@oy$@XXo' w q#} AlPHb[ 0x0la/L.,t42JSO6M7{SȀl׀}4{S*% A.Xŕ(%sq}Oai0bxSSܶt2؆>2A v|~N&CA4pab>#D)$LUwr+U|ɸ5B_ÅLl4 -Zr's0{툎8sYFu1q]|ftA&ArL2ħddMĬ12ptp8/OT{"SˡksM3唆6F9Go|KC Q ^Ւ@o0z+}r/+N.kĿޕoXXPsHAfZ"ɸqܹҶ׳>qJRuѝTj{)(J^GjJr j]k_LWւ}9AZNMC~M"XbEDKˈ|H?yTFiPUoi1ܔmF Ňk٩euLW\0yF1kT|- K և H=ArcG( 醼 ,gq\[J=,$+^4<2X4#fiMmgfbJ M9hHbʗ^*lټ9[W?KsXJ?IR1d Y0QL D]8i3Rb #'l޲I~e>dgX_dUϧj&Xny 3L2I[Ơ* ~,80#J[ NjKZ䘤|Y} s]M>P^&&aVN&G .U޷#**ϕ;,Ғ(хV~F/J_D'G "]t;[Pƚv ss?DYM,@Tvc, %*td۽ԙQOV%E>jl۩Qfx?ftE#-ٿzÏ?[owXe+``0PcgpT;ϝ{oX7lJaJp\UMpOuWU)23b !U Vy {(io?i/fd3Jv t)˱fڠF͙9bd劑?S;8!`t7(ot@S(.^Ͼ{P l=pzZR + ZDl|kϸ"F/?áG,t~<?]JW`"Q@o7Fж2GVf5Ǝ/߿߶d=uLٝ"R{Y@֜\F@'^|+AVkGѐK}p,j)f1,H+v e$w[8{y:/~މl#l+{~阷2Ҷ3 l׎W/ִ:S?OQ˒v,27~L~C %^|(+~ߧ#(}S1{3?QOƀރ@^{Wbu {)&y6hcyEtfM\WgŖ-U!J7bԻszW+ ,|z Z;̧`U;Vi+4(BC6}]aLn$:,m~9w,CrB0FRN7C`n:o}4f2\i[8";{&33g,nNvwU2';T T|hں5g+DX|yr l+~u/kAC \O{/o]+ޟfyVnXfP'v\$Fme +vH|Ԣ]$#e_rPhNj{}J=tky߶+&*9qe{_ϫ2hF-v_JgԶLzzuLL"3o0psQ\<{V}`ܐ ((%]2tg%oҭ0!([`&T_f'eJkw_DEŧ"R_h񆧳=TS*!53V>aу>#WqSVo̢,FVn5\"8|8|^DG\9,Dxgw-rr|UVWzؔg)f#9<.#Eg ?39}CJ6)yK/gc&GnU€PSQdd޾d13صC'UlǏoFNJ&3TGNb6Y&ze#kz䖐QTXDr؛@}eMO¨>/%e)G[Sz23H![=.z>%w̆.eEԵ% u'!>>F6Ly,pv{is:vbO?伟V68DU|·tt)I| npf:=m1a'YE\㹨aJ܇b8xl/d2x+j+21^T>o>A`5GᆰH4i t`q'eP}ع}&QF[0kYoYF9J?&/)][|䢯Ie\r+2Zoa[ҧ3GzMePFd|bbn[g:._||WF n5 ~'i,Rz`<#ؽi5#"8v_`LjW: ^x*77ؾ}"=HCיZۺD6ϛI^>Zް"R$dW62؈q"QfZZ",Z%#@7SZ7vu{y{cx,y&{}%[q*"9cJE %""wKcbKaJdk$0Vy'Wؿi-z i{RJ_V&GaeOHHvYyQDz3?/xҝx?*;w_iQ^*sg'/dPjDd\{AJV6+r"*Q_Y4bѢG\‹ 'UM[Mu3h8|8IJe0c,-8l$e;g ge vߨˍf-h %ަrbaeخ>ƐDZK8u(ƌRWOs*=Ss?Bͩ=.gDwZJ(<3K~>F#6eL̓fT|zL<ϝw+l`^*Qaah<éP 22̻ MNd7ƒb/'S a9KY;ݱe}P/3Ƭ[1hp)ņbܯ'RqB܉5y?0p¢b"NTvG4R+ōtLHhrrMJFmׄvyMU$"#!8Gt:A8a~/++TX*:[>sG…&{;ڶn/aиݱ jGj¨zGձ^~ےjGUĉP.QY8A_ˀisAb^\A'Q6\ !Y}(k#Τ5c(_||.`^&7wƌ$NA0D+g[L׬L #a$֘ ոLT")=E`ق JN,4dwBÀQLaG@mSu1@-,M:u5 S7>dܺ +28%;g׶~B ?I\sGכ 1n?KʰdƔ9n:\xMs h$FS %uv,IL?̜'TA>rchXl<آwaTغpwr}!u IG%y}|xw8,ʲzjz9RWR EVY67H/eh^jDFa%M?ujZxMN\É,>78v("+q.`R:3JMSD5,+5֮h&9h%;ISm=_Oo4gu#(<}RufJ?VJ]ɶ. d5SH^ҿlҦK}]OҘ֛ʎi!ki5uCj*,P:SZ Yc0ldjٺc1~8|T wjm3c\DaU ܺ.>Ə5}*vB@FY. J1L8aE!%-r>%=۟Χ(Eiponը/;+#hȐSVCPC7 f)**}:, +CѮJ# % WGٌPYX/W̫jm'!֖**m܌1 HYUb @ZN"ݹ꧳kO<}H] 00'NTrIu+ĺc4#g"(=q)-1~c1"s-è"0=%3 sa I.CwQvW Seֹ ͞IBl(ӎNӦ`"i]QG)KqsqR#іM >|lVkF@}VK ˃p&g0F9NV)<^@ܖVWQc'10uC>̌*]W{Y0;a6io,oNRK\MhϿ1k/2dSJt(6μO(cJ2uڱ' ,FRr55OYsbR v5Ŋh*ѣszjO :KWؑCS2풗NW_sskֻV-bfM`O6"}8>Q##ϕ&-t,p2+< ҺEtTC>8S^=ӛRXD}A9k};}VQ|0=HQK(c`q"78h~;ˍk>XgڎE]ӵC\ H2{[Lp'_@mu=z]:J["z^mϋ'Fܵ+d޲n& $z=1ѱIB0T/ w-ձ~6V|n5FѶ։fk\5<4(w[6zT[6|S(h Ō=mLƥoRATN߉ 1\{鏯 Gn`=KplgOe(/+7bx-EQ9H]0XV9Tܹty6{0Y|Y:]ڀVG2VٱqcsЭN*~z|LUBFh\;pFGi H(E]X6嶃{XK4GV`H9E_{Q~vUzRGWbN"][me!3p49fCL1.0;n _u̹ >94>.t3+> M8z4j0dLڶ5.B2s>"rȼʽ<5%e/?\Z7$B~8 4TO3<ɚ~K qJ#Hhj^.rCL JyƆH, j FAJFn8p? o8c,.CyI}Ux8zuk[NGc}h@)_2igҷCطF2gk LK(Lo/7xa0I]ɱL!dHFxxUe%ƌrFT hnTU` s1 YHo% ?g-S&ĕK9s #Im20@]0aQdapJy \ɉ泚K0uȦmb a8t4zb73;>x$5_\ [XF}>>ZEL lv)NVw0Žx'POUxꑗB.;4h߅8d6tJ<Jn-̾ۘjdGVz|(,è@,)F5ʽ_R|46iJ@剟`kG${C/O/\c_[ .䜶f,|t4CHD2,Kփ56/T]Rʹ;YỈ\Ge~[Hy?a᜙ 9j(}8xDO}:eĜ q@. ʓ1>`ccX3Pd"4P۔.rW>CrT.C5 V,9yR?u]dvEUI-mQW`;S^Q ү{w}*9sT͛N{?bz1.tQZcgfcm7Fu,q1du\͇<jhaGyYhMX`~.ii0~8I<×@EZIҁYJyq0%(}>Zd/`42iՕJ:V~YޕҤ55m:ۖ3'Tèn:>K[ċjȨr|Z1~ˬؾk~KI6J[,XPXO>H%F9\Veb$)()`t;:3JAKq4>`c981>:pEu-~Ԏ_ _}ÙO5!"uc$RV^ Cw/,Xt!=lomF‘ iII8=OǹK熾2O5-q{0lb>+o Hgwb\<;p")a_ [T@̱Nd@h`_{NdɅ:9v( 8^k2$j( ΊOe&ƼzQ]ɲqҨ&*jS-*ًMThL=3'(" צTĨmNlܴ "oR~^;} I^txܾ.H:Bo_:3kuLSjag'E,:R(9g~ -E8x ]85FeQд,Lb3#}KUU%Xr;&00YmK8 `lY(ҍקSrs%2&wկ6lLeɯ+KN|(TW#171wNVU` `z~܎ H esm`4W*?t9]':T<' t<Ϸ矧=:(g$"@aKkwD̖\K&#_ 6O]_)5_YK_F˥d(w7ƵNOGrYj>rٿyؿUkI,5+175LVߴyuV-գRL&2?+p-lUJߛˡJRSɶ?p[Ymbq??4PXt}J UOl%XYvkK7> NtϨ!nY0X?ob p1`YU_4bR^Ѷ e 3K#>X9gE+0ڊ3(0ptxPW݊^Rf7HyɆeC1H&Y(lچ&/}q_mw>87t!)c.\#=y`F `DHN>g.4-5aj% 434h!k|LFߧ5:ۈ ǚ*lM)+=_L!enZM!)fd\ON! &"Vncu[띷>__b*>p 1a0HtΜa$?A&9 -#+g?6(jٌLctV(\lsS3m K'{0Vfux= lrr ^ޱgֵsy eYuCPLRWV~MDpyn-Lq&,&Vprw!`HdeN\ͨ*124rKxGi]B2Gysԋ5e0yb6dV2]]x*nUֹThΟMEl߱ AC$!Sn׏ [vR93>֖r4pJIǘR-I [wwpxQI-R TSaIߌC`@vNc:wWU6 EI(.Aј,QTZ C2c}65#>79=}6uyPùi|\0 &K6@2黴95d$!,b\U::\X_8* jXTMfp4FZvdYMmkL8z1e$5 !6 LƘ+DL쫍_15κ|zPܩ#8s2UȌ%]Jv mxT3X|x8S}{IhfBNUc1GL3R5--!A!'7+}>[.6;:3Uaz'k6:XVVxԗc[ﲟ'kenY{6}e[$qMAÏIŏ\Qm2_́#ZR,Yoz;jp6,%n_oɭsȺ ǜr&u>~* o&c`kwރf`A/od>)P֠_z MXU1HYNKg)h9},OtofS ]`55/;1iMe@frF39+,69apw^tt2Ps 3S(y/mmc}RaǪ%773]ֽO:?)pV: gg͂ u4tذj 0qdn,pFQ2`vL<?''Piǘ|dV ow֙n=S 0o~HNº߆>oZb4O}atPSBfR*Lt _x<)7 ;/#t9`)j!ʲ<'t٨<321R^;1e"!zߋqTV:FȤ-bXDGdAMhlAq{ca*Dk:ш;^6m+;bYʘK#̑d{:ilp!6/cJq8`}w˘'R+]aru뎯ИW*2y~N&=zmHv<~>7hޭW-ޡ.dzy˷IV3ϽvxJnЁ)qGɣHd~l>##K:m N yZk`\-!X-CsVY/ yF`.T3IwcJE}\cROJ_6z2O૯s*،z,(eFsBTfHG,()'p2߆=,t#clJ[3=#&xC_=cg; `lv Lx|o7zŒG\FBwwX8Nӑ s{"S[`>p3LYVQ\Kάj^:w}? .GÇ##1ߗ`/233O֮M8 K^|FnN28ѷo o6̩c(=O kA'93@ G HՅゴOw[sǫ쿎Wct):ݕJtؓ]\n돲,O=LM[\~qfŗ ;}f%.}0HX93?>~oolDYU0qgw;wd @ b%d}ѻN= {k^ardF"=; ?ۤJQa x~r; %]|E\rҶ>U [kw7r.D`҉? [YM)E7S>ckvT߁`/K''{ X0Kԭd3UvXg=IrHxGnO? qmz3Z(Z\AЎEl=B$㸤tdyvTnk!یtaGs>1 dQ9ױv)%[YVc -dPK1Lu() ygB([5J`-ߵAn*YI [xw9C,)Pu"S:{KƂ2+N_Xqv_COK a ä`_/<<̱K_MV%?ދKU7as Hf^*lG k#2^qڀBK[ƒmҒrg]֗ 3pC|d?LW(?D&H?PC wY%5XCЫ*֣/wc3#%xWdfeL78>Uex˘'9 Iiq4i퍼r`:,#'X8arLt*bh@V(WI]乓~KT;_~@ToE{E`"mҿj\7^s覦nY J$gHphmܸ 'އe^ UGfȿJ'Dp+=%uÚƆZ2H={eόZiLA^V<>(ܩw/saE?u)dFT0!=0FMNXؚ77SF2lY).^ P2"#p:&Xj4.}kbK86Z9_9'd EɁ]z=/πn3~}~X_ r܇$~jin’dhIY|;{'ogzAJhyd jS9>@%#nn ؏>?;rMFjI\rFM(Y(WWM)?Nx~L"}WWw^?b[Agv;r4v| [w oN )Hr:5LD4*JM-T&$ʢ8*ƌݶ ];y::^<{9,W0ssxW0gÕ4vLXi?֠A5,nYKwAr-zJQ~mic/J(15AbBJ[ )c7(jhޥp˲qeQo bz3HIM{eNӇ f{壶_Ԍ3Wlym3e+p*Ci^fc[zX`AQYRKYpNM[1}/:͝E?8nBYf($o7Q>̌b F#X0 Je:%׵J %zJ%wrzq5MZ3]-ylGLbgVR bC(kdZY\J,Na}ѓqdvP:'Aֶ"%99,:G0`W7!h pUXR.͕Qʯ >dtW},ovv5Gԭ? @ mkg;?qa* emߺߣ*~!Q܃N;OXUdSKxjJ,cOK ,'nI $X\hPY-Vw+.ԍw  B=s&LxBB~y睹s̕sssBA=dnM 2NWEl+QAcuWa)7}=:r-/ߒk!z%*c4I?cg}Izu5%%C^lZJrU"J} s(:lݣd(]DJ9r"e s5 n^tkrEF&`P]iI7A/r⩪,$,}Ʈ>%9R%zmin|tb(llmU-WVItHꐹv#~$44e,u_d1QԨV:qmM?VzR9RKL V8zy93v+-s]7wߑq\db邡A(ieH2xP&Э'%j VALKI.j2><!nv~i Nszr:`L|B9ix$j2GB~Nc_7ۿ2&*u4 us 1Ɯr~؞m#Cu0Ѧq9o؆pF+LRjUf "t\UIL?4R1D<0 !A5V5rס @AQ;M8|<ne|FP(E(cwu+P5a!JUg:F 'Ir= jBR#=QRkY/t/EIu^hG {k݆W{@(e"'ᅨ$R4R)U魖w3<'`X2N_Ii&8v(qӚO !TI'?PO r2aA(򣪪'OpeBx|sB{΋ {&oyN!+>U ;8ueoO*{q2ϾhѨ ߅M=iX0-,>ē\ n 'Wsd%}xJ_U'75ވL/ӬG}۵aShx~2HS|U4Wc< -g[PM,; 4 hnO3ɣzȪ܇,Uc U2V,;wbl(wvN;={ 2J%mXd^x.^O}]۶f<:yAVK*>u'=ԣYڏ:~vX.\9]D<8 s-ܜlz4dwYW }KJ $]McN~ک ohEq^7V`{b-O} >ob2l}É1p #2;&0uI\?gz1i@?q0-4XM%ga@TPO"m8Ǐ @cSZ !cjZjmZl0ô֣r!H(2pOM{VmXf@O>7ӧ0vS?PoO/-9Qi!j(ʟ] {~FG(<ãg $jelRfQXq=F\υ=뵰/::8mBg?4-& X@>_PE~nmo7࿑{_57>1H-* 5Ӛ +vJsz 9YmNBNNTr\ÕGDb[oS=QُZgqٰ&֌G6˵ x=4өD^xmswWs[9߉QfYڮ(/|v r>zD ~Nv:sƟ酛f*FE-3TI7trxj9*г뙔d|3C9Rk ?ݣyok7( }@[M>Y"h -2^^+/Ӎa[ o_3[KY\gq~.7 :|Q~I~{^/+L]PT5UsF9@[Gslmkڋ5 jXhlPB{J3 КueW/hQ+PYTJ xr 86j:=1~LB;AJGkjn}cpßQUذ[˘YFQ fIjO3RZT}zL5FT4L-:u8E~RQw*;h\ݤ/^ 'eCt01Uܣ7-"$Vg/e؁6C; h_tb.ڴL~i `zQ76+6f&0 1adpGA#'_"ޜ!hB^e--SB=CAjGF#hyrZ  ׍G.]*`A)e ;cJI@QU<}HaҢ  w eQOVQ[ iz&ZkKM55IgI4s?p$.$mIq6̛G0^K*h7SVuESLHclDOm43 Pk}H*qN-Qư/d)Eb ),zŬ7eod\/ ޼r2PQ_cǎ(?=}|7?^% K0HAPz=-'vz>a(3TzWIfn+GՇ>Llq2Qg d_1M:h@S C.wP?$MXr gLd[23_Ax8΄!;Ʃ 2a=43GeS;==;4i3`K a3c2 Bw0OP_SW[SIboR 7 YPq3fý {8`Ͼ,˕Z$zTaOAS;eʎdz`Ekq $=-1 Og?|Ŀ:0J2*T|hu6x$~SGyN6/鴬ԓr/ϕ-iujIeLZ7u҂ԓ:D}L97xw+w:?ҟd Ljo46AGջZu,Hzk/2aHdBRٞNsp6|FqХҋ],'r'Ã:e zR!LB!+  +,4C$E­޻q^L/LjJ=bNWۻ7VY LS:꩷sz:VUKKsb5^Laѽ 05W;&n/(ܾ Q6+^6}s9W_dleZ–r>|kW!|Y^c"q)ΗO^˧ cI"t-Gԁ(zX iFe,cOZV8 _Ԩ 2l4L_~ ~1Y^ SoU ;pH\1H/ hG#wp%g#(4z%[֧!Z:}pz09@zhWףoCpz댓ۊ:ƴ7'T>\ EOh)RI)qС1eÌʐf*7 a%? ߘA>c=v0dY~#ϔ~˅)oxct <"gU,QtɜJѴږI˿9f+/Xx ʖ3n@4律MI SlR W`=r ixhDm~~øPnu02jxi U?N57b6namϗ~^>\1l45s7Y&gS =ΓRN"  p"~,~R}d"E>gp3/°!1w&5|z_'QH=/*ǻ{w(Rqgoٌ1T0uO^뛤q733FeU{*_(. SWNM;kV/K+{t򛏑BW3꒷{eޜb?~ĕ]w]P$Nl%M z{3?0:u\;.-'^Rwر U{1sa'm+1#)է'P?Jc|IG'(뫄CJ+zYX(arIIa=8 K;7ܣQU ^KQ/\v0>B4KS-u M<"z0ht¢':RE'fpY+:K$ 73˰' f3//PMtQcZވ(fقFdeoペg/˹PLOGO7mVm!2JC44P'BFz񰇶|†)j : Eԙ:K aNoPݘ6:DױMS|7f t8ѳ6sƐH\2F& f;'n ['$%%Wdr's9("Q>vCs.5I]Nq: J9͇I a0vDT`%kAA. rN# mo<(L-ȺdGMٰEt 6"CB SO6#^-ݫvsP< #A1^*SeS RdM/Aa2yTzxO`u,KT#MrM)'+^,-*?8p(%qUG9@XR=Oi˯>3g&'{3 #Ժ Q#nCNF&NGPqZ8KQA_]֘I ;nzC'z'DK1_KoI҈fT>ΕNqt PB+3(rJ K:u}j}zJTb2)#f*H& :~p_TC=Ńc RϙԁRii4W&E#'A]}99Pa0efg٤P.NO_;R#[x7y}IߙW6j)$DFKƝJǟxRu'd'%7ZaI|'k5FXa8srrv;L$H32Z-}B"YH1e|jݻ7p=/>y!Lڛ6mp:OuWD>RĠEUi"'|i&( #lQ܃jY$oWF]0Ro6UKR 9}ҹ1UΑ.yrl:k}v_[c[tҶ;v@ttt+@*?`BOS8oI}{$.j{V\KٌL&=NhaEVʷ#=i63c~ʒ&zm7#1dPiz[I=̘0@l*,{Y:8*ޞ_XZ|"e,65FJ+=2ه} [{o7[to"/ M]n 5,e W/ǀ 8Uғ옯V7-٤ܷI ]T: z> iFIˈpgJ/mXs$'IU6,j4L R$ݖiA*//zɒrq ~ 5pzp<T.ʧ{4XO,76[m1J_e#]KO'clJӳ=ufƓ>0T9©xvwz@k3@s@Rf |~ʘ*J .άW[j0q}'wacbuLV3}3 QG Z,0nuR[ifE=cCLVf.I۸q./#7$oЅi}.Op]$լ^CIk?E)j6;Tr%oex?{1=>%M؃wh8l9gxv\3=ޞVπ~GGVi^|cJJ?~ ?'={}-}@]cӃJ5Y]s jwK;WU H)ΞV\wǸ$Vy`9Y_~[ՊUE#Kз5'KCaQXꯖ#W>F[o+}25u1#;~@p-U-;zkߕ@ր<ԙ EI FJz%1f!l(dٌ5iĮȊǟA6DO4&xc DyQ.LIwnz ǨvŠ_аDŒz0[Kg `P'聝Y6!! s9B`py-#**Shh S PK=3B6Az>@weݵIlچj6è׭ǩ|>+d;|@P؁Fo5(;t6w7{OTse zz2~.*z8~: K([j@^& ڇX1ommGJebsC7=>1ਨA7#2R bI[^Y݄>Q`fzW7a}hHlhb>_`ú[ɖIzV<&ORޓ+ BDGfFO;MG9QM UxW(s뢾6RjC^Qej:9iXz ?'N1?a0&}yI!vXO^+VC1wo|N$ahk7+-(gLIS'cYJئnK=>[z bGIfK𰝴Պ!8%ظ7}PQZ 蛘bЁĐRazWT6!G! bn s>7R6m(WOŊj ZN C~6"F e7erKǏ¤E)bo-e }}kߪxôӠ%>F s26u/C3@IDAT٧=g``**?sU?v]︄ܼ6xS՜3FL$Y p>oN]_3msݩϗ~ձMk]{Vv/C~zӒAPs:X%]iU)*|(ϾKJ>(ry{w/ܿY@+EI|6^|Y2S®5ɷBBh~kaÇX$ҲdYIhte:8~ے‹".Q\osbWgC.N)oL@V44dj̽{kBp̣HqțS:,d^$~pz?~m4ab}X*cO"~_ SҌ=v^#Ve?*z;ŏ>5fw}~?˷FRZnj{nSJy_z7>|g=m|},@,6ko)T^2\S9W{_ş|f̘z{}{STtǴj2|4r7'󮇻շ_2`̍wpP -]Y?M 043=Hew= ذVm64Lv]s%$:ȵkۦ#xI|o|œrn}awjH=GZ7 PGƧ߾5~mU:}X8L%mLosY)5h]_ǯ_ cuŮM[Y_Pb+{Լz_4]\\tbiJ=.Wwψ1E^p8Ds xؓj҆zp$kl]4vSWT=b%6`ȑ?/,FkaZ 7_B,-?RW4 -5E1>x3z$nXPBbDCJqM]x ;k "3C Rݖ,~U 0ﺱH*SXo!u5+J[U4Ʊ03C騩'z!8i(,ȟލu-($O(jP{|6:x\23U`Œ)h3y7͘adt:ېFM* B.fr8/z2i&C2XoԈH8d:)-4 +~_VaȱA LA؏l9_# W؊6"W1)iГ`p63B-׵5ھh8o&;j"OFyf3 51NAϜBVDx/|$_mcÖ9-9 1t 5.i̧C=co-ن@M2o*_6pmǯYMnӠ%ߕ'33[n=E*q]̻q8L=ƛ[ъӌ\9*PK+T jdݜHpVAD- _u&NߒW<.-hh3 Z5|IKJvq¿xqE9x@\^"otMWKF,PujRQP 2M +6B^]5C,E%vm{{вv쒪̹-’ b&rLBIcǎ}<0abTz:>]!Ln4J鯜ۿ^'򃵍 F+W@bG)? łnu? Vbw"wrی~;/j!YyW5V9&H}[,H*pvtySI6w?|^ ˎz[}ZS]Hc5^,83MڪkG0R?OfByDϗ~F x[ 3M~ G_c+kʼn*ƥ!%%-'jz ܇xj>{r_{>')[QhzYK<_ѳTM˸0nji"#?Uҫ|5۵M IUŨ(#Nk*-sAOdOy[L,Hc\L&uX[X^n\q(4k=%g?LE9KZ3马۳; H: .5cjf^|9x(qG{2(~|)Eu=f/\wZNғžSFIی;5fҟqr w EaQ|ax.nW;t=X mE􀥯=[p<~$ЃslOABQ*YzFP( h(5ϗD^qZpEFc-tpJ: w W` Aׁn9JkĊVЧBua\iQ9ZM&e]CG)1z;?33S;,.Y)}'Erz坎KNt9̏=b⇹h =幖Ϳy.HBaЄߩxՓX'2xi빲 uF |wOiɰx6ײr5 %%2Fn+{qL`i6*0X.@.x%ŬGuc+>+*7-jwRˌ1lɮ!eF~ |\I6J K1O!\O{ÉQ^uNk ~g ߋ4\_p MFXaƊ񻟕Kx\8uels^* 6{WWN&)Rn{e1sŐOX83()_^X# mD>Ք/̫STO5Tb:gh^Ls繒+ gzE;\TS' w_q"dէەJBR^N?2]vRa/ItD(eg%U 3=uaDf*w5ۤA!}zCr\ڃ&Y-ZZ -qNC,J?Qמƚ\wЮy1J Pv9Od"A65h@ZOfݔY7:8},CbĄPsj+RSr|/L3Ǟȣ7dY A=X1%pf՛&N\C|]vO_+o0fLXߙsa!﹓iݺy/k 6tH$xd:%i(wQE lމYgJ  SSj=7ՁچAZC*dr~T<\<@pS\O鴣`Eoѵ{ˆwvcz`?'KAߖY'Ȫ,϶3j*gk'CLM_OOT0&-u0c7߂Sim& ɝkVD*I{Ԕ>[FyUMah#g &&EVyz49id,)A/Q-E.n8Ba}}酚 CGe c:~ uw f֠-R Q] ]KT2&̂ B׼&}>ryŘ8fIHÔCA",y&̽ CkVH9 jI:S`SهdC E*Ai>ްyS^\LxEFaQHt FzrXW.QFwل^Zv07€0_IzS j +'p8RN.4$iv&";c3~p;Kn̈Gkڭ.zھ޾E>ɚr kom,cWf-< uiҒ^ezRt{4~d;^Y3# K]1ضk7Ft!pX8sۮ G\u?-=+fP 4 ¢b÷0Vz'I?evQGݍG}q-asZaēʂAӪ@;7 ětZ\עUSvm8_~iQ't>$d#*D5EhŁ B]$hrBa9_ud1hnNI,V1y~* 1T*Sɇ4y g|w>7wwpDg.yָnTe KKg̘ ijA3v }iZN'}U|D`3V%VRϜz+,%3@U>?Qvo}`pPƏb R/sM'R0P8M`vkWܻ6WNI-m`{U$ :號m1oW`kky .V򰌱D$>56 \dT³Ofu.]Fdt1ε\VVy6E|R;EK# 4mb`'"ɀ:)#ABxdH4UCFT>У3#i( RCC#Fbh?*]( fR8{d\ΣΡ:%zSU>t6$U9aP̗0"HE:l5q:.+hZ=v;{ ydo7zsoI9' FN:G6LFbVΪ0w)|Z8lHTgu'Uمm;V()nK˿'\.,6ޭ5pì {qz8RVgMOY+n|-HM؇!'a뚟aK0[*Ӯd^;:j̞y>؃ؽʴ*,y5*kqۢ[q,7,IDP6ܓã)8?/} 7Ϟm{7ѻ+#v#64V Z-[LqyE.]>ȏJ9Z=+sVI!:X(pfٺy][P=!{eRQ.5_r-o`5fZLlS%jSKߝ{+Vy7rWГHd+;ێ9E^K_}1Ub^IU/k@yRk1]m_]ḪTۤ3ƹ"?vV:nOaEUuR|,TLRi7NYЗt>#{亪OEtj_TRꄢ^J;I;Lr\S\GZugVLǢ,2Cs1 ilǤDCyt LyowtF{d1e9O wU1,WbmU‡ɣG&Ü5ڂ L\G7-V_twHW_">.a 44b,'xڂqOWF+ڭdp4"HGEAǼ<=agm]\W Rb9Yڳ~CSXLxv#8DM÷Bˋ`qҳ /1ް?>[s 0m4-چv@nn6IOຉa89t @fQ=*{տ_0)zCZfEAq )H"o˜Cp,'˹ш JFCVV^'.x\Ӎ`oImHzTI^hŽM57~vfP'@Ra_g>^&8' N;H@zMN%D.) Xs]".|?%mnFuKL -+}4dDZP܏ƄuH͂!Zkhm:Ʈ5m B}]} Uj3eKKa?# dY=y``+AzR,Fv4tㅭ1p71tp$o;b63n >>#Zh!nXyd=7݈kû r @] 4"6wV 7wմ z}3P:λat jOh8dʸ^^>q:ܴHDNP O!ufd`oz6،(pb1u aߪA+߃%#H `ih1#Y^iAÌ~4z./,[ˠ]?&yXE #FӖZb3*'Rs}SƓГ:8剡2-2F_h6. dulUޗj`Î{!ϋV9smD@X8Y2[-"mIﰪʠ&:57:tw¸VqPmlaG W2&z;c ޝ=n,,Ķm[akesQ@ZH/vL  ]`w…\W:z(b% L}˱z)| |e^(Osܲ/?lc.qpxF6--]g!=#M`OF$H{3x0v,~%W #z7cldr)P5c&vbKZ[< Hgl?{I~[ Ǧ`ڤqx/ `=tvC ?xn͢^-i"Gvyͺ˻IOݓ)/Km_jY%%TT I_ Wb@)5s9Gв,E.T1uנXGt,dm ?g)JIީby%r\&ՋNNNVbc 7|.ſ[h1vJ}h_yʢ&zV瀬j eQ' Fyq$I@>JZ5JEIm -ʟ~-{)đ Ҏh%oɒ^f. *u8]Y/hpBI^.i\:{(E@ h>twϥUqE)) _%ğٞokONTuTM?nY֢ d2q<<-aLӳ߉ A"1*/(=r]XF *my}Bˏ.5WZ;=t>zEOrekI!'kMN=pM,þq̼)$W`}p7>CS*+DeNcN`OS)[9OGU>!ᰶ>+kɳLyn pGpW̻2||1s\Kyg!s`آvíL#ϧO/֎g GU@[5-Gh0(kwuؐ *uU>P"`D^7qrKʳXt=pI{K;}<|bz}o?6~xy&r֭ƍCH}{,nZyi%99} G^k} ?ҀbNڟ0sl etP>ZogK[j\jW3&*˰%dUelhJ|8PoecN@Eev?_iXR5lT%dR_-;y޼=y8R.ufs.`^^r*uX{YG `^օ~Reب%S/{ھͶ?)#R$zuPd5;w9*XOh#Eax&+ FVeSK2,Hϑ쌬}[UUEboF59m*,9e{JIPvȏgiii7Ԡ"Luc|4r*x鶷7xN= K 7;!\J8ƥːz:UUYFsxZalʨEQn |=b0MC:PfuAJfհ^$7)3oŮ K=g)OLd'={c#Vذs#0'"H?;kƑhoףSn2ue$ vl_2BZ4os \ɀ1hطw&DnA"Ƥ ,[s6Jv@lߴG#,%vw HƓFAU)-iY7L>Á@)Xz#3CXՊ uvR;8kaj#9$P'5 y+ +Zت ݲ"6ԣԈfX7eaN2La!桁cnm5ds:8ػ"tI~2M?vd1 r~(.H>a'2Ғ %@Caj^ٸÕ * rj{ rPGO@:cjNqs?`#BV !O%ڬYH>ͦ}>PRZ {xO!5cVBwad%x@5ҥiH< Ι4ko;{S9Z :(8]V>~z+w4zRbcþ5o6J0~H7+#Xs<9:/GTN*qGb+>!j+1f8 4ިsj3C' \lCP %5uJ-ۉ,޹Csx^ٌ[o_`c jipDmk2NvGimu <:חˆ6nH#AmTL?z J٠f!1_L`/HY_XDsnvEw`;f͜ 3$&+śT6)Ŕ=f ო#cht`7EzJqfT7T}1,TAJETY\/"{J'*j"t3"F9K:ыҴ j ?&FGҋzWO# ?V>}Zz6gD4%Iglܐ"o9'Сp72K&tk_ H$$ Hwm0 K[|`$eW+=;^|Ty,BR܋D{;f_ r.2fq*^Z߿cJefzG}u޶-(kEWϑ{6ݡ{ߒ42ɳ^m[_i bR!ts;2<"tOjǯ{c`(VzKCv۽j%ME*ӎۯR/`)'1ԯ. `#7x@Bi- xJ(ov#Z<5n0n(G|\)Bʪ__6g 2ܢ݆n~aܠhVVf.-kQt \DǷa&f^w$SOshC4gڼLXzDYve. ԇ-<߹R69ϲyD}QeDWi>U6Q 9|AʒDG!ߕ1T~uMi2O'C $=JݥΚIm1yH}4 -4r `1X@ϸ+Dk֞Y9BR 9sJ:ޡ,DE7/l݈#;جЈ8nH/ɓhIcG&5MmFgD3lo_U&,ľ'`DOʪOV1CUաO_0U;#'7l49: 鼤=+G(Œa`Dz<)Ejm,Rf߉Ti*lЪ :f嘴MY[5OL"D3EZa -Rg8N 2@_ ttuHQ܈c)Tj^>=K aOy)bCn:A3iF CI*:ªVe# ӡ+2 )Ő v5͞*dffq@nRN$Z`.V-m ƒ@)[zr!BС^vtlvA>.4, $^өojƑ*V8>4EacCf4L5UHþHJ:)݂x{:#͂ k={Фw!**0 q[qb8j?:rc߮ B[1C? -xfbXX#b@JC s:kE#GԠﭹM #N)`)޿ppm'=K ,7] ` @[*TؖmmT[j)P%@B37L>Ph7ܹs̕1>Y/Ɲ^ 嵠nQ32`e5|ɰҠٟmw}5 $c샅6AJ#Վ|6@=D"5vx+x6KXC^ե`Pax>a Ӯ^" ZQb pMzJ'n0V7NflL/Cc ߅~uek`Wt a뭕J@V%&q5}s8ВN]"ҟG-q.vv&m`YMhՓYg(YFXz1Fi:8}'=$YffVvtl#v߶ xw2@;xlrړn><у#ƥYHaPXu-HǠ!5HGG^hԬ1*[i-Pjh)iW}L<٬YGˌ ^kO3XpkRbݞ9Z}X)8 .bP@;9iYŸ [Ͳb`o~ثx^{-iYI 2xO1c45D!?X仟ըvǛVkM^Dj9,Kvry%k9jSZ@ iZI+f+-))B B[uǁHx;UTS̜t"B{#<*H,<ŚT6-%UdApP\̦Q콳oxfNVI >4d|z:MYxys0yhU&;BN[o56vD1̸={u$;6RZ()))A57ˢ_!T^dTu=ώwvݚ'^޹:xP KF|WCn2,^Mrq?5 ^֏>x׶ɢBTSIi *50,YSmt愕%5;3wV'&sHEhwx2SIz>9p1D\}Q+Nj[3@ތlQgFo@8|$i&q.W_ǨIW6;?k{ʖ0BeoǯvСZpY]yb͟NhbGz); \Dm(UCc5U9G/%(E!: BI.y;H7F5A)*Qߙ:G|8vj8y'ܣ׋E9W/-B帰(}z:E+#Li(azN{֙,A82dY$Me>/N)d?י;mD^j o>A]6c٣{pmǁ8wÃOT?3ȤoXX;rܺ'K91H̷oT4VaSذt1CqIHN`W$LA=id,y y#yeu'"<$^tXKl* ΙY}FS66ӏ(AY0] ڠ1d{XqFD_O?X`V^F28:`/ 8fspӉuXfyarF!:nH =J-)2w;3; ^B4щ=bLB vP GqQ9J9^,$@e*˛^Cg>;Pп$W{G,a_eWs.5{*JW&piH9U_׮GoOwu'B7㠰`޳X @n߀bᚕՀ4w ʲ Bh=z9œ9Ockc/+'duDz 珶11 uS0/*FX?R ܂@m)!*ѭ*ɰŬ'CQ^&,q0&{o=wp\bolWY n>3GSOr%@di_MᐒTQv&ыo|(%-"hiA?l[nT`Ԉ8]X9+ҲKB:S(( HJ:.AZ԰~BBtڴ.d[%)o_뜲jj#: 2.'Z2‹ y ~eAA)c:4!.tC E{:=0ŵ<  `gl;sSKw0}E6# j#؞5hm ZR:} t~/)Ⱥj*,ZAdUŌْ&M_E-aŸ982e.疟 6Q/s"рWNOt ~]OF+@M9=k}?Ђ`̤I~="tFƴn`kdE:>`<0Q@^>k؈16/o+Oy6R 75/.FÕRG ޱ#;vƢgDUyR];/jH}d[*vd%mٱ ]﹯)LRTcyw"٨-@ W78t.֭0yT.SZLrnĄc[<ޞrr"<}vY{?/mGeE":zyUsdeff=t=SkjڔtFzYr1oJ+R>yߥ= w)!r=HZ1@0c*9&b8Qj wTƲ_]Kze[jwlHlHҥt`}՗{oo9I+IԵ7b+@r_r\ՏiZW&&O}Ed s4ckH-χ@.Gui~AVynzyN&O]rZwp 3[+٢'.6ȪA$*c< \5Xs%@IDAT;Rשg;K+_PQjy+G+(i)˟ NjZZ"cy&v؊tXE 4:O<<I} Bk }CsY=*'gQ{v3.Ld{,?AhO+,NbLH`hٚ4w㩙!FE:dBX8yܦ.c^*zo&#Kl>zu:O Ymڧ6p7Ò0!p6kSUˣF Ý?(ۙM@瀁L倗_xq:TL>n39EXa?M#H3 e"n%?ߓH =)SqޓdSUick1bZD^m d촋)i'k!"Rڭ;šI4]fBC~ ӯŦ+Dܬ|9G@Hcc)|t DE^4rɚ߶bH͑Ů^;Q+c@/wγH# VSX5ARr7>0yq~>I;qFj`GSma=F0aaqd^4whd- vXшщtǕE|eiW C!.+ 1gy Eo2ۙ !02uƮ%+xN2+v)+28rܕuZ?##!Ι~fuNOɊ}41!!{8˺J10# Nz6fc5p~q[9F8x;cdž`eWΤ\4^~-6+,48d E+Yw5M{MS0ZYrF =ɏPQoK'1pak`=v$^ʭqB>t2 qi4u@%}z"(Bf׈X{sHK>}'IT϶y7AG{n_J[=rugQQ #ɤBnfu%]tƌm-P;t8$bѣaeq}FjUՎu'E }Bྴ>Vch"KS%(6ctL&EG2aafO*6 4pEˑJnF>2i k .U{t?bT&mO>zEi%Ubh#Fs<$7.of3t,7?žFTHS3z'rP*rd^wxŧIGTڦb[W@'p21 i>(;Ϳ''˯pj:ry|IzW_60#97v"0jwl)Z]]\"ʍZQdh;[8h6)P[9[RBgJZZ9Z/zgX;gqdgaBG Sw|-e%ZGr,⦵wQݟ:,h:QI>b)zr_uJ9aLEMK~}&7\Qlhl,j6*H(oO9vš+6@+_5$P菥N'I+/q{&eWG֛9@+H|:g!;vѧUu)T=ȶzFg#E쯢lZgS]>[էcwdLa͊!|Wg,Ey}vMu/r*vewX_f{@,^| rC>;x ?WÏSWZtrŊ^#xV"I߯muۇomZ wF%m_ﺉPXq,HNCHYmh9N?u}v8z/`׼Q13[7WusT⿴;"Rz]-Ms-Fg%6oI;5fm~Sl@ѾW.'`jr՛'C7*ug`AThb7 T HvH]i,D˧}Hk;wkV/C5wLzыA/GOwkv#^wRe_G?} z}g=zi +on׼n;?24羷y I]4nو7ΦbQI]1zQ7mSO|Cc(;f݌{zaJIb5k!]=} GW^礰)W>\燎O>vbdcw5(" lOn$ˮqg0j>/sg>~ dէmݱ'ܛ'urzeMi"UZZSom["ѹvɏ~~+ߡ^ga$ߦ!q.u9?srG![mGe5zP_~&3]zdޅ'8ۂ 6!8=ifuؗW^7w)8Y+0(Qޝޖ[ޏ;1;Je+12Pr1=pb}&k/* O?Pً`jA:NvVc9DaZ#p3ȩ|y9!ccd@v?_z d9J2j@oV+8bږfQWp!Ω ¼T;2M%yVΜbŚ?v$s1f8|{ŏWhzQ}lB E B?2ӥzR2+Y %ݭ z@"8yZ%X~,Z /'tSDƐ܃qMc2pNîBzvw?C`Ɣ!`ȑt*ĎkOV`Rq8C#F2l*cf8G9g @Ρl|>LB5pwGޡcJ{‘tą5h<cIfc&#GKbu;qؽ}§8%824s5 U[.(KpE8&]pVgfCa z; S'pu Bf#3~9X7}/{k%sı' ^v('v+2v<ʊPDVJOR"[ #s>p/ ?טmFbSq-з5 Xy!f.X;qㄫ'Òuiy2ȴB!m.\xC_~byvB7D|ཷCpyh$;\m\b*?!(` %h1o:\8b2> X1wrn w=>Ǐ iwu$4/7_>ztƀZ,m?}Ы-B '(*0,_иf il͢ {Gʂo6z<*9"V8햾(BS2e &ߠs;m;pը=BȰkghmNih$w1x✶ۻwL/KYbvn][K S Ovx;A@Rx&wa/VBv Y #L9#5?.x0szI_~LVA}{vQR*Nc_ph@_} K*zи@c%<zAޞ %^*/Vu?n8㭝⫫qn:3R{c)֥o87ɞ/4Xͧm3I⯟$zΣ֯^>})f6{|^/}Vܖ*˲?"J^K&ҥ jȻgk PEĒao1Pʥy2eȽH$mN⌷߿3d,c}QwNGͯDʠc̦lV%QVk->+sRGX)m7*n'?Nu;' Ɔ>,FD j |ʤ5 oU03Hla t҆SI\ KBQvXb<Ӟ>'=8X-)0ݻ!ٗjZ e@WOHm3qҭ9tNKJ{mOw?XY?~5 տ +:bZn߁^t#jQjkWOzՓ)Y/F| 2h0t@'F N`Guܾ.MҒbf 50<z񓦴 J, G*XtJ꭛cԘ=kHH@Rq>LoM;s]^bUk{t)Ӹu,J`178i~Y VGS31}ˡV|cBu?1$3LnP%},7G)jk|bK ~8b˴릳Ao#TYW>WLl+AV5T20TzoL D s{5߿g)C0x3FRKDd%Jߛlh"Uh+ u][t_g[/sN͠Hyyg}U2Vry)3T'fU}gR>GyRnZiQ!< 9} ټ23K1CJJ& rш!xME `WUF"#}5, Uџ@}98Љ4t&3+LS"`k`XPK69dHuzn\\TށԽQ_M|O`u4kșjgalKng3yDɭsGFY#5޲ [E e|6("ڽK*3XWJkLv#zLtUщ]5vM K#m}׌sR;-s)¦6T-mDԗq[ڹ[[pߞ|Y_r`)WAK7h8Mx8QRm0tx˃Y}FQu1vl%eFJ.gaoK[ᛴ6nv0nD=)ڶhӾ ݐ- #2~n.LRTVDUK|)G-Ԟg)W э.gu;;z YOZ Taà9՘~56=mS{DԖcvF5z;$*ӳH 2r\R2%Z FDM\LHu c5 2 rq忢QcHF(ƥ 鯥Ց4qK.RPWZhXZ@DUHz;N+TlyxF_uUTOw!Yȳj䘈Jվ5(';iWZ}LӴޗ|7y9-).BN'r-;B-뤫'`?cc\,q1l۱ ;wXdS/n޼s&R7.w?t rkb-a??לWZ- s^ z򝙶v>SNPmֱ[qhtz٭)鴝ծ}A;"ZwʟۭSXsim~}wKou0, rSӟg^}7O zu+vo,i\kI&beg͘ WjaLWO3҈n53Й W_cC7g^"6Fz"VaTLvᅤH&?c,3?篾+]fϽ|yќfBnwƌZGy伧''&hiB4/d=r88)V,bi_Kۣao." q(-I"B(J%G-+#?CsM}@(CbcwbWL ۹#A;kT&\l,CvJL掊Lϻ\Ar^rɱHVE3'>[k4WsCcj͠u|P^WЧQ۪LjUW%*֒F+q5Ri;VeiUζҨcr掠kZiM_+W%zRZ9O3{91OEi휋+%\ lmxe`GU x&%l3OcagUi\x`!aڤ c|mkeܯ[c1Faie+Wb-w_0<(?̎!|V[cɢ粳3ݷƔf[ӻ/9YHMi4c$$f#jގz-11nͯ.CS;Eƈrz'9[ÅqP+r RZƣe  /;X]z!޺yޜhJ/ՙGu rtJ[{`q 膹?j0܃sާ;VE=={7Xӓל^L?z#qe?n,7W/[GV"f tD3 &kNgۺwu3[C>1vp}k;o%VÛ`;B扝H+o'ˊu~Gwpqs<{v`Oi͘ס߫g)R k_z0z9weDD_2`Ͼp5rPO@}UӉ%02c?CYKSK)B ,G5ueZ(M*۪mӷ}ZG>LK究/'\xL2MfQ!iȃ <2hү@U_|(-vz?$&ͷ܎z&bذHrf0C L*4owߣrd&lPAE/iBa WדC2]BPEkK?uK{UǖO]WYn窒 6ŲN,;ZWKC6%)'Pof&n-ONO cX{c-ZҡsР'bLZ!֖p sϾHk,Rqwթ*żJRju֞:.kyICC39&HbY^kpǍ ޒȵe1Z SBI'ߵYF]KG%@'R.oL}kg(R[3߭ԭ[HvQ,[)CK"Ir 3a iYhF+"9t|GЊ)$;"ߓ94҆uk FcԦCͳ8H nگWӎV6|’6Z"eRRQST*Pְ#*"Z+t2`PjZQ7#ϵ}+<9jHe.sq̓϶X5Lf:=;KֶX 5p)moTTT G/;tG>x7h9L2L0Yr~ݷpxј=1zmYvh^2 _ NWҲOLTe eSOΆyctG5ǒ65/V97\A@S"Y7ӓ).qCQQ#Eǵ-YMC[_#6ރ]"rp'9NQGp.,@ KCu*ƀfYk{R;odd!8ڕc`_'4L:c,Ha8Nf'O˻dOtmQTX:cW!T(:AѡHM/hX\qVs}J{g=lkil!o)휴YđDgz1UE#a3zlM7[\Z"gwyΟ/݅C@y?~wV/ g'9r/6Do\Xv%EiCOnzkvN.CN:Qc0x8@wE/O'-]t ~Ea!K2>%dm *z :IsE"¡ӈʀ (" )ěŠ Lf`'R6 &tKe5 <_q"- Ԓ2Pf:uc-.EZ{u+\x?ZL Wb / f͚uNNF`I@Vx:ewuw6 ~\$}E*oYs8k0)j.DGu{_M`tdH s-]3Xk=Xq/gLF`|95`bq|?' @ֲ<;;,V|zi-?#'31}v*]˳ ɻ%tM15=_P 5lmaN+ɶD4-ԝ<or*6SuU9=xFX_I-Sn"ޫxh1;9 $+r9I0c_~~9=:4mQ(L@]ߵ=fnVW1[h|}Pձ~5[O6SB{qk{l aܼ\:ߺ RC8 )tWQ+uG|-_~))g~:41L3J{~dcP63E )ߡQ& cuਡD۷<9(zAEQ)6$Qt`](8c }-i}ƌfUhlE@gZ{bZ!:)iŘ'Z "m،C wɯ|߮A*j&I9 k[ضy ^|9lں^c4rRzNoVҋP୮o% 4NoҪ&^iă䑒t]"zû5W1oNM/`YwDăB֗^xETppeHY.fG|ݻ ̤'L~*kdz.l,O4qR{ΩXDWMs2`[~1_4$#F 7݄b>7WK t3*0~.T%=vǬ_P:먳GsνǢO3šQ|+컰Xe6LFj]JyCt%-͹d̝ߊQsc?q1K'/Ⱥ#TO,wzUBح>wGAVy+һpLc뀭Q%`7V_^͸\WDyh;t?{k E? ɤ;M'#k+"בյQr^+HIZ}dH~qc&7kIz8@s~PVDYQ ~.݊[ 9b"P>-ό#9tPt&OW#.Olu8x*k6+ºl}{q)'J pB9V)Gx8}1pmr:ꫵ d]j1kWc4q>>ޓCe1j]b"k2NeJn.B;#;% ֌њUX9CW-ޫ!w( Gcc=|O$ㄽ3,FGpvxpq [~>M DR-Ǒrg忬'l U\vZr[]kb2lߏHY=z^;ͪ(9H82\ y- -h7P{FXh7z:R3" uhϐ+u($f *2xzc=H]iPQNZZk,=@Y k`K-^-%\üG I~vqLwNЮx=pɃ##o{K 5w76lg u <-&߀?LF/ Wp(@:t@IDAT6dHq9pнȃ -яn,$pZiKQ^COA6}c[4..0qb? GCwK>L:u2mGuǑI4=ss Hmi"}'rz"Of<cρ#8ZtW]7koOMeChHQHS{IQZ>.^^1e}-e#oq65jɖ 를?dɩLj{3.7 ެ ^-A퐓1ki /ZE7_a ]"}:v@oX%Ƨx7c,7Lzli3MșNSV^0r\:S zݯ[,jHuM\EVG<@*|> >ضҷq=N[?В#x=y=cζ}LdS[]>.@ICl^[a.(cL2,];錢RњJD7XSx<&S߾F+f\`I(קCFm :T0`uy~<{!`Lߔ ~R]twRΓAknIg0pI#S퀬RcuOk=Đ4=9j^?gŝX% 3R7Gswĕ" 5 o &3oAMVd%(G  ^/S,ںOy2ViV,U0]ktP|ƴ2Ɖ~Kߜq$J[$֋P4{DIվ WP}{O[[LXMJHfd YBI=$9t$=~tW3n˓ aǎ AXXD|xh˼_>"mgv]@)ks-A|,Ό}-o?[I Gk<&u:-DE'}-'o2iLT}عs; [׊zUvz3Fϙ23^Xڅ/Lľ72ΉK* 90) hՏ^-ObEΕ>QРCi^ӫfo$@!֖XX kqF8joԹ 0z@ȽKG/ßn5l?;.d?*{2'U"1ūS%CwsA xZeeGNkJQ xi+5cfuA3TM6Vs+kJyy@|W)U^]K?YD$,-B *lͧlhT{{Gu'kc&-<¶{^mRGwbڭ .,H6FMw_=rc9-qEG"Ƶڟ Qmz=FKY9fK{-]|փYaOTTYHyloE~H)\hi;/Gs^%yF/22|^SVڴlr[:P kr},힌zs:,z֤g'ŨSX/9ꧥ˼Y[$o%-c֘ٔN@ׯmUF}Z>F)2z+O/4nR=/9GYq!"u ze-:T&FAֻ#?|mn}qt/CYf:р&2&'+26#qg]~'-6d{ tšS=]HjJIPnciGlD(Ի^!r<}Ħ3?kA([b7a=S1! U6? {19KG y%p֛wܜ@K?o8 Bx 'q Vڸ#ca腱"sbO#Wge#aaaCO.58Љ*WEy5HIA'zFcr \]]?1$ |RLKOp=ux;<+ri.)k;;4fwhQ&b /5RL΃Sl,PRHORt0gbB]±Tql 6"'4{ne2zw.^8v`٣܋k=Ry͙7L`WgY# FhAZ Axn[QD:`Ɗ6u3o0|>suU{4zvEU\]epmrq#%|QE8"1Gb )[\iNo>zap5]=M(gWIlӹBu\\h?F`-*ȃ5$ R{ ^~-}Z3!;!);,R^.%HNlj҃+as-!(z`+lVA3AvA>ֶ֮c:E*\M+#-uYzEnUKWӶL/+܍/YÄ? ԡ;D`ѱgU5s{a;E`m#a㯯hޥ#r .6b"/*\R96Z{O{S8 s%r#㮗j1xǪ< {*>DX^eCul,/ KrLI|ͷط? r7b[\2\IUpΈ8a|`>PVy;yH]=pUpa{noڄp,ib6p 'A=c̼ }ޚ8k;p3 T/9!;7,ä;_(Z޾(zܴRˡQs/qp^*?:kHqkPS-a#T9rKŇYEyKXFAc/H P{6^i{>_.mU8_n[h o>X:\S_chװ5ouSenOUyJʶm}E򔳎rF9)W:[xiNFOrx)VʗzSrcl~yĚjA-q]wJ;fd]_}p]n38}b|'ض{} n2z_OG0_`EH9URmj0wrE~Cf:͞hվмz=/ /_n}k5[K~1!c` Vug$)JV;u(FdZt9H9Tx;t;t-B΢[aሹb/0Eypl8@|Ğx>&/LJ?=aoVMB#q(.ɧ n/˼ÓAqf؝pܟpwKk4`ḱ)g{)d$^"~©ע|UFVa$&#-1ߣe54nWn[-(L/ Qלz6hN&ahvO?e_cx }<׷1ס}ȣJ?T@ߌۧ݊x jJ6f!˜>BGu- pJL#R6 <"I.՘6(7T)[V-Z#`٦'E#j!@@B:KS@D3*%6&E5)-\ @Bi1GɚSNnZPѯYz3#e+GʒثV'C!khJ>W۩cA. .~08;3:2ϔi1yY0f35,7_ ^"cu .#$11vB/`݅%+끈Z}_YFS○*/ 6=RZ=kV' ]Jb<"h7[4vW*TK}#"5u3^?z6U;e:9''4(WaҲ՜U 2]5d.)B%1G]ӷO'BJ^0Եv<Q?uI:˷~puv6 }~2jl%m4ԉD3U*S$Nᬒh]Tmt#eĔS*V}U3=gnƻp(#$SR/\>IKi4>jktv𠦲`}v V:9f5ic<ҫ Mnyn[# -c.=X=},NBG+8k W3JiZݻc`K-|VhLd` F1ogM/wSBU\P9O fw3#-[wҌ6=@{M 0M"]kv%b+Pr.Vmb[`к\{f`A-| 99?(/gg4܍*|qRXؽܗ6Qp>^ؼ+ڴBiy!5ߘcH0Ycɺ9oi9 GQ#Gt$'r.پ>mM$"9 .[s0k#S#I`nFIf n썦8[xRD8%'r$LEwnOHOC\iܟFDCq?fJa2>ş¯A~e%s5-ⲭԬRʁH%B:%[?-~ ȡt% ƒ}b15Cg {lέ9" `!\;pۈe~d yo>k 46ްo"O`p>vKdݸw%tǂaM蓏ʉEq \3|FSCĒO-9i̸y=|ť>nBhhxRYw.xI*xLRTME Tq1ɤt ͳ?e##c $7z@4Q֗!p#Ad!ݗiC|iFss{ 4IDSqw}47^SksRpU+;M3nE QsY鏙7n mz)mͥwq:ae(QqO/$"$=s16ר+NңV_G\}M%O)5gp,|%CH=8xSno,zK}/H6ӗu *}>Q#r;<-q',Me Od"5u+y_gvnOszXuܽg'KikO/$\|]"c{+"^ywͼ]IXކ%TfNS}6̍،^m6}֦B\n4ǡ1i>LAcaWMV)ժ4Ԇri=+j?z)YFB))~H¬-kᆪǟ%qKb4۰NnJ^WU)j]v4w=K2R{Ec2'8?5SG!H.o?P̘:d,ΕvJ]5S/oEV-Nժ`XOLYI *}ֈjy&NayKjBhȪG"eK؊9Hկ'I֩-q#yv׏jwCYP_\)O0Czi}V&~YlKTLlHrt[5K/?u_9V''KDQ@Mzv Ţx9&쩢51\"~<6)x"LŠcg?ר,V&>pr_e~Xٶ3F/2rPwǎ> .6pomވ_Am}.Z@(撐Iۦfv/.Erj\)յS(»| //O'R>z1nAԵ-NODӗT\F6D!.>7hn]T}HD7~,h7^*fyq29{^ (}|OQM-atg( }l%[!= N%#~$^O W1Ӱb~y9{Q۟?QS~A{&k;0 2YS6*F#Es;ERRv|ϰ.CA#d tza#Pt<~ZѪC{V?֣6V@\8xcD&UžilHo:bj :/űa~ UE+H!mڻ#pؔ$k%!n3D^>R\߽%~_ۜiӇyiׁsG_dQ"7084/7>*[6=ܫtEлb]q%^S6+د/ē:*|@)߅+}r9{C~<%5u`aHHL͝XTՎR%ex[Wv]Pr:׵ю܍sǜ߃Dַco !]"N&\\˻thWVH+q̘(=r^O+߿j~] b'U!PtY%&&Yr_l=s6@QHX?/]m @KY8-c?/B[Y~2f3' Gaٲ GENB;慿=>r{e%KRE>i/1 zړKΆf _~WsB!Q:\kA'mTx ]46OUyW:y7ջZOPo"G){1?ѳ2wv%n܊=`Ǯm6hdu_*OPƇo''=㧎#׫%_۷7fc-mu-z'c&N>Ccc%;Lb ղH>ӷR.nc"yc8$EaKWKZm1=C>q՛?z:8 Βų4Jܿ2ԡRh䟂~Q ǡCpGa'59\ǖ̞Zrp/,|Yߤ]7:b\읟Yszq`|wZn3фowsD̼bL /3?.‹o6ҝë}5ꃻ)uT_*<7]R%}_i?ũXms߼j֊!flyݥOA &~M|SmK|}e%p0qMH?c!ðm$l{NlYj_iyϩ{p@}ð1ho9/~8)y7GD6{%ZH1e%vSuo. 5 o9-{ DUq!ms;cQQڬC>{ƕtK3ѾP<5GТ>QE~K73T%V7!8K[$_*[;"9kW"QÒ*zzu-Ɵ+W-4-Ψa.V۷l[J)})e^m).r?ڵCuV,\qX ikg3`h6=~nSz2.˥:ټ3)zTM'Dt ]vƈP7 YMu۷D@pJDXmvZZh6شy 6{y;􀽅FPn9G07)c?,E߫>cwhƏfGqaŻa(H=vT+h܃iЖ}ifcG۷h =FVmܒ5 &8H(\JmEÉڮrN)|fF)ˈbxiWnTke(͠PZٻnA&*)^ݰq{"qt6ҎAډt1vlߚ4C׬{Y -θca_3_|Je>T/4*7%ؕ$l*mdjNL~ƞnҗmmv*^ڥMD>% ړ^'R8&ypt M{wtDEjۣL޿оpl9C; ;t[+T梂p;[{S*>n(9XA J=RńXsݐCRn>fWskٽSM-EBG]U4/ŷGmH1_z UU4}J4{J240O5KS+P|.<7Jʩϥx#g_ Z^f2R?=FC{`WcĨ1Z,澃զgo͵`-dK={Ѻn`z_F f3\1DdiW ۛ-/S6Pabf]Y5gk1mز /l$9:O7[kA^Bں5N0 q2kZT=8m ̥)u¥par(um1mA͚XˆWM/,/وT3P.~uB;Z[bco. JP@{/j S(0OGW;'j Cv6SC\ Φrۑ1J9Ի?\'M ҫuغK<XpsQ^SfRm۷p</c'uk4 e)NI.>ok|4i=zϷJrY9KhT!1rX̼nSSSx9K׫:W8[^ySړJWٰPO>*8"gc_{4/#x|v 5p^DmhN8Җ0X~I)<z7Y> ͽ/+T~Yi/7Q=pqt"T J)x28T[5eQ -G4e2)`kkdS±5YWoX t_RoѮC:Ғ ;ɧܘmXWQEQE^.95k0;w;3IyKGǮC?_bU Mb/H>$n^|j} Շz%\"JeVN9haKb}<X8ojAܑ8G-jl|avd/H$|m6SyJ|-3ǡjc yeg.mi箁y7EAaZ\]7 |9(kF4ւEiɷ/ig.qb8\,"sݏOIw?Q h!ѣ m ^&CjkR+>pRS3+qAui2@~Ix <&Gb᜗6|*>}aP6'2ڦ"pv{p&8l()8b(%ܯiJDڒJēXh脔asl?NP^z |7!-`?Em߲5vYpv./A&mSpj; -vM(ݗxx?]: 6lBs(n!&4'qLI:"9O4o,\)L݇Z{Tϼcz#8$QL AweZM i¢BLYkڢF:%sL *(($s7bSi4NfjJ JK% gN# 殤Ŕ8Ug t:DJ+lgN=c'`AHFI3+85}Y_8>S@,v gaJΞ[Kؑx)8G{ޝ1a8p Q'J΋^}/ā;;f'':+w6[KSzC8aM]%=gNN`Z M\urMQ TYDlV{Sה*o@. _~3hMҟĉS@U+[5#FХkKۓ yZ-&4}x 1z?D^ M2'Ϩ!pPeJ;}{$ ^BȽIS 8;Oq|Q7\u$no<RRih.Pi_}A cNW. 'AyOj[SEn+LMEbU֝s5NxJ_ >5#$ I -<.o 5 R0c\CpCʱP֘z qHb0wAU!&6^C)D77f;p.O~*N=vDvl_Tj!07's [(F=jH`U\#'uҥQHd e'iKP,{5 ]X9]0G%kcڈ5|<1vg|?l_\`ԥ۶bLS&܂$d! CrB`N y梨sQ] p+oYǤ-!R-ZS¦=,*~7 "N.WC6Dq!<ԌZg[spuU3d,U=U#{Zß/pH܅mLNmNE7 ~Y^y gRgw /պIR׫g_LiJm6>>#nXB۱ú.x]~BdEv"Lak=8foW`mje8C@%26u]iu' 9ٷХJ8,㮂x.7%J;<`^|]KBk/z`^|LyǕ6B\VY+G~r,L IZP::/pC#HCF4YXU_jPM'x&9$؞#rQO`(^B:ZUӢ[Uy!D9)g>?3\=1Dʨb\ũrRX.N !B/}*"HZ=ӘO8r=KYd[5rؿ_x>RهwdW砾T8=XQK9c롪C78MS)}Fs8p B^URmג~'/gx 'CBS~Z@#j}mD H?)<|>>+_ͥwB {*zj7 $&O\GQOc.gO<"U?Luף֫4GZF'RO쇸dv{XUx(*3KzWOu?I#5^Tpe/]OQD 9o]~spsqx884miv'$$^aq5%C[Ľ(h\2R՝mhO z $ ~e69[Vԭ[ҭk^ k`z{[+dRn$8S_EΩC<8=)1wȬy %&2`{ v$$sEsf\zª_/mM;}'p` lMQDbsGs|F!LŸg"e[rFx`Um7DE4il dG3,jV A8piBc~>)\Iҹ ι/H$Fv$rNbt)2\z3LIX=s͐JIjtauSDZy*ر_-j=MIytvȤ֓ڀĸ!=Տk>0GQv!{S#Zjglы唝&VќX KB&~v@6ptqD#6Pyu%4>fEnD>h<Ǐ"A9f̘ Ʀn3wޏxP-r7ƃ,\{,wN⡇SK³b~?λAcXWYQ d.,ZاNbmQ"֑i$2+w5ʊRb6=85ԍ pKўquB`vڿHnI5Е6\n1\nvd_-+Wy. /UW!hk΁R0W/}KNX bX:sr3QBTkJy5}Vڀ-3K)0Bcp#]sO^T֥L6C`i[$G{t6w/kQ{3[hd.{hH~RK]{M:pPga?˗R/~?@T@I֧o-i}mʼncxxt!uqu|$I*b6;V$M62Rc=e㢅XȮE3_T9Gcc2~nd1EZd$,1'NS)4wIi:\#nqcnATuH丵G½`I֯coυCff Ϥl b&í? 8s]Iֻ=nڊshN⥣wHN-T\kdҸ!p:S6L^Jxsa(lgmM2H\enB6GƢ #\0/)oHT4avP#QIi֭;x`b׌6UW~ &HN/ˋƏ v=ҟp&= Aطw6m{ŽDh_7XU ҥU&8J"pd)%_G$u)NEC,%?xzhxR4E> 1T[VYV}ҩϸW [q/m>gτWDHC)9-ި0Kעgy);u-7>6Wx-_y5$$U'̓2c'#>j7|Y Oj'Spa~cM6Cg%wo )%2={ "'3HЏ9)hKvUE )mݣk;DY=U_x;XRƵE `c1_'$LT{xsO΄Z}nia҇%S:[H3h?ִY2#mZ8ambϞ]hѱ /gS!n>$DF%%gO/gsD+O>{M1f| }&T_>f ޛ uθkWź(r'FL9NCam4XR:wo̚u?"e8<(1eUo_:I`q'Nܝ(t}M*eX:Zw&MN67I0[6sA |oD:Dڼ"Gɪ QLT Iu#]Ou ]pǬYܔgI EY&n>'ERBi o/?Wj6Y#T5) .g$^BxVM!FNJԴJ.Nux'ߛ8htK~ȭL9UZ|Z8DOW#ôOO3$:bK̤„7'\JRTQˀ])N$_8oʵ> u"HH:몳>5n[}zut; g2oZ")h:T|<HgaZ@h\+w/>Jkolgl9q]Wz~{hƹ&"}!\"$Q CM!lXM֢]Zz0uÎ~n_$xtyjXM?4H*.>&Nx]{WCi]9=|/U7Џn~ANppxq`%^]efCJ_hy*u9Ն~H Ҹ$4c0MnDy9x]S)xqX&PbǧSj! ]zԑ[eIT?׻wL8Y}p*P`]֯H\4gk8 U6uPQaTOʪ IdkYIh|Խ1.mT?cH[vy(JYs4yRGdZsFC}SUOxFfIoTX^TxFߌKj꫋q9Hqf6Wpf"qnA%%K}%o5p⢍W[c!y4h}y4HDյƷ_h2o8 nD} _Zl/ҏv$Y5jc$6$R a_M׎-!gݫ5-Ծa> [ѡ7>m܌ؽ6D;JӵeLE g]6zWVLKGRMt"Fވ;̋YVQӢ595ʎ>$ey)J !U F{ԓSciu(J+z%ѢJSbiTriKfؿgX3LL8w#׌ZaOOȘ'R痯W`+mO}]XO'߸xvXEcBGh55)]IvĢv;E1}S6saѱuK AZ9wܷ;\(̹OiWTƎαk m|<\i x7BC(YY{nd ۱ BȻ6cpsb[,!fG]GTgnF ;G'Gb$yRb̴{]_ADe)DErXM}n&䁯 n [PSO= %)^oM;v+(S⮙É"7 TDd˲W?Kl:"%ylM L{.pEsT iv U} S9K;I9p}8 |SkAũz2 _ E+e~$ԡS+' WO8Sa*z8|v7uZtnA>T%3"fnA>GI"(7)nlu i 1عTyشiVG@x~x@_d U7LZO)1\WUDU\WCпG[ йcϦR':&R{> vCi₲ګW aؿFq+}G%`_ây ؼ9O>© K̡˅)r |I5w9%N]qǔ7OշI̳(KV_liwWڣw .Θd KܺN+-%=-N}:=fnd­o/P7w4+{rK~^As2O,v(ӗplh~7 kCI=,2(9\oQE`^.)q$>S&iw*? 9^[E>_B`}G(ڻ V=/*Woi[TO Xq״i$^gkfmΑdջϗtjWk[s*ֶᜑo '**G5| VuRO}?5Z{{j*KBZNҨ5>j|,3;el&q4%sace*'_II}{ A22sͨ6 ~TwkEICǎfK[0$<_~mN?Äh̨jymq AeU5vHF̩4⃣7Ca/?wcĐuO:eb`dM5%vY*3t tU58ycoAJl*:NW^/?-/o{&V^~Cqp,a!:Ρʷ%n<o>$仡G݂l%Fݍ~?-/}b ]<-‚ λRi,D@zb,gcHׯۋXJNgRk@ۛs.t#8p8i'v"'qLv08W48(ɧ;u|d;7_jsNI8?EѴ@q1ڮF=OV32Q(iB1;71rd)~hfjko9Q͘16KEg4x ?U+IЪvd]$Xel|R_Y^֧!yK{ő1BUNWOXp/yoLZqq1yiQx.Nu Uqq'X+/8C[Dv)VSR`2xyv'?z GMJ7wjm-Q|.];w@19~zq$_B\ꪻզ\U$@GF]<0W.rE W`Wc8s76ͥ[—_k~ЮVpw/׾ y9VI'sYֳW \zг{oSAnQUYZzx8Z_C-2{O ^o̼/|Z7U0$8U.Ydim ;d^B7c 8h̝NOBF)M(txB"\brTzQ}EFeG9/f*\= lS*hsI'+/KlljC\Re$%#cp N E4xjcpW"lx\ʳ _/q ˨Gړ?* '>}}u=k7 Jq5CS+db'UnJGPE$X'NT!\PelhUׁV|_.}PȖ* u}PjFMחR9(]3Q!:q>fKZ=TpN?ko9O"֬<ջp!YC8ɳ6LZUS$e+Q}$U);$F(çUYqԷԪL5N:{u=!?TW(flM4k䎃xE4(¼x١π~}jK="@ZBꪯ;ƌUw_m;2+ 6m:PU#|)l^UNNG#8 a]˟ Q/'e&ᩮX?'{bU 6QAXXhM\q=ނ6S;_DɄMS?#%@nH)ldd%lI2wŔq#h: 6rG_T0wȡ~V,G֮8u AC5g-"[baֶ=P3IM{r|XEG2OvDvȌ?Jژ-JA-sk[i㓐K;bՆ}žX 킰6A?jL-8x{b!HN;DƎ +S+|" RGA(!q{%ޚˌ1h(D:1~tj㋤ͦBxy$zQT>%b耞p6ah i ~MHB6`UѽGۢg`"v_/#Fb)֧Ь$=x?wUMMgTHH!$$"M);^Q‡"{o RHϜro {ٳ;;gwvvggfj`f+4ڬGhn½;wAQ"(,|p臍عc?IQbM8B} cm)4m\,AI56&;[X㑛HMc8^#@uHK,GMM SȎV6*?#4 h] bm@FSI^ӂ *#49)~[8ۋa]{azS"&ObY$R:CX$<0;)]z >"'|i!38Zi4.7⎻:T7"Ns+wO7Y{gb4驜r}u0?@#Z4NMVUƾ}0lذvkle+u<kUlkxq:wk*O?MxsBr}sg͹. ^0 3L`GnA6yd6zw"ܾ.oA&eFF6 펴#*PD l1Q.\y!:+jQ8!-ڄO3~Q+iGƙCGJ ʝՑ,~71U^Jŵ{d!7ml ֡ЛoA,YbV=|r=Cz!,1ĜԢ" ||2@ĝ9O(ĦdkNhɤ~C- f|ThnϤ) ߆ 1BMӊgnoqRۈ>+`{UC]u0D<=ʂ&yll XꮢƉdz,u#9t)G'3}Y]}DG7 `q&u΂V&:R'o,(as7wQmWJ~38c M?FYQj7yYj|௫Aߏ#o%y[3y<|O^?:b܈xü+ uB\Gt6"oWPNqz]X g93r#v6\'^{p3iT`7_['*%z.?t-/K-i䬃'Tִ X~. X݁t#O'etN=KWh3ɒ8rɝ֮__ vxwBD̛8]/ RhTu kZ9&t:>9iS۶t 1L9 /|rj ݵ7;o<c ;s }Ԟu$GqY5\]+zq@mGeu5JN x'~ohX⁷磪Q&d޽fѭl#3=H!\]P`';G!L#{Y`5|~WpwwP`G}\*քB􀐁кJ{p9 b}n ̋|%w*E^Pu>| sVc9FYUU%pWs=@i*y}s_w"PwpBr+OW^ze>{y_ZC,|I_vU(,<9v--0FbA}ow+Qdiݪz!"[gSB=y[KM̨,,4W_Be]|OI|{9|qDLl3 C)V;[ xWPrNR8qL7j4 W3, fueI/'²`ݖqzau?&E-Fܱ]hp ӵiK(ʛދLa Eކ p qg1܅d7v8Vn܍t1^44_~ "B0z< W]mL$CK϶jnK~&]h1>LgVו{NbCV!'i{K}5(C[K!=K?t73G/G^r:~J1})޻^3ZrP%d^g.Gjq w 5n0(6:Z(ӊ 6mtqGRD7epz1T!3KҾ oHr٪E9mQVq{l>(iSpL/77y+p NBVUA.Hk0ҟȳ91E>:RA7p 61]/B>9eV-,d6A"Of+`Jɚ| Qd d>Ȕ 9?^+.) YE1 螘{>sCR?Ue2|1M&3,ݬv`y3v6ul6LpCq#O"?.V0wª}=`?xLj-2B~)7S:ixpW G]6ΝAI.|"sɶ+?!dt,aebQf92Eڍu)l)h c/WK:|w%7$\J:!D<| ԭAKZ W=ˤߦisr\:l03B;-,c|'KE -'+Q|P]l'+(,*ü9Ө.dR;ٓGQRYhe5+Yz%F> XẒ}4 JjpjPKRt !mAz`QX|9Hkp:~u=A[>̭á#$ ;FFڝزg토^06C|B"mp09uF(#M-;z,} $ֹBDK%݆b'L/OV<-,[N!^Nme05p;V f} [l믽E>ba``E_|OlҶ<$X q;K cLaAJ+eeEeUuaAfϟ/'ak'7?jD %gPEm۟,\L#cX~euշ$KbPE$\h{74ar9op= I0}IdR.#%9646FA ',9$ n݌_~GSF"kEK @1tP:p + YuH|iۑ`c߀09L `֘Xi y8$ϝ#Bs!l 7>aF'~RThXoQ:ճ C Y[L2WIXl;?@WT7`d L3ɂW [s>OϬ^"@n;u0,0ԙ,zfи=U1$o#AMr Lg"% fT9=̫0&$f#9S)`fA$fZ,ग8` &$-V!5Qƃߩ=#iH&Zxe6B"8_B ?އb1dpQZ#0djFXf8HFBT=1O̿zx%[nER~?yxťD#w"|m({+Nq=bه*SZ/ g򷝂ɓqmW^]nj7w@U?,L;)h 󢟃g|ez"oqP@8 b2rXrxjWGW,`8QO7y[ӜΙPC~Ir]E;v e_&wrr^')Ͻ`է+ѯOku> X9щd5N{ 11m{wnq߃b^JA>/ǃSyNDX(:#lOT؊pcd&Lؽ=Z+|X 4Cs4;4fhp9q}_uq b!1J$X4qPeɤςXIkD 7vݭU^>WE}TȕXq4F`ǂ<-3G ZU5v)<}C)6(NCjRzzeO ^7?hSimJ&D<Ѹ:*o"\<Z%⮧?p\"E xVl=`V"9MUo,ʁq08qZR˕-MD uhj(BBj% SOGȁ$3$HO ``ioT4h59~xv.FMm GҦ%j3Ͻ} )} E= <L[݄#{O(ҴtAcN"Ap>mI֌ȗc$=ge!%#ij6>-rJ9$H(v$tb=B`p_O2뇣۱n+Z\O<c5dqa<--dS~)4=餐 / 8|/|/%୅$m;ub٘LYT1`Nn!Xгf,3h̬IGA +%$!bϭE`9 OeɟUʽ d[.l A9|bPfjlUm$/yX+oK 'y94G)ƯW Y;ӝ7O=z]3'a<}^F$dqU~uKOA X] Lzb{y޵ k7o{-=Ez+WX=ig"VސV]n2܊)+CW6Ҙp*-'1gJ];y#$ Hlڼ]z>w#=W_EIr,F]d~=d$ې^z:OZr 4ʪ$Lij/DVv;Hq`Yy(Ci^\%󲩘zt$+`OByc!$$vljˆ,LZn=Μ8+uYH"ai EMb/$`{H tbϜ>.aGG{ y3SSgO#sfF:h8 iTFV93w6:@|O) t,y"OŢIXmdn&U`cRbh0[hVU KPX^A0 ѧΥt ÇQ\s[gسҽdΥ1ID>{ 2198|6nہrw4^"<ѢIW^~---k{pYIyâ%iU$~y2m} }}?Ó//ZaH US8>֭ 58w.wbULfg 3;'nj xEQێ6 YSΨ3p()e|*04bN>^OV mILzA[9hiROe_c1'_dz5lg"L'ya&rÖIDtȿ.VCӔ^3-Ϡ rq Yڏi|:3w6Ӫ_r%4;8#T0##d}7p),|DA*9OaDl;_['qc'\oc 9565u?,/N #ԁa.(y'm{͛C9>Ay&qmIΝMD"tu!|l؝'fhɸYm91SGDeXrZe,*y.L_Œ(Cb OO\9XPĻ\ EW4\|hʢȩ{o>f԰#w S8FoDC~ aOjt ƆصgHX ?N(G/V}1vo?CdfÓN%&⡹3Ѽ"qrS\ze!̽&MS{?Ό#s) T`7|d#ػ(V7:8S_]w"R2[(;FTwC$LX$1 O_ xiN]׽u evn޵Ӑ hFeS^-J[4O'\8'|]x‚jP s*‹#_3O<-(_;N䓏_~HrNkD6D+|îtEԻ"Ϳכc@!ߚɅOQS}ͿgE:um(]"H'ʇFTGtl E俞uٕI+nJ=e3REbk0i>Df{0qX"ٷ }(HKBi4k264;F4y邜.遺Y=Ñ۟; Бs<+L]D0Ԏ O>>f#sޫ}46݇HP-]<rǎ0nDv#b1u@dQYw[sr1{ikrwgGM[jT-oUyb2}{:֊z0tC,̴5QE.so7زf #k{GH4BB#wWaDad6[vv5)Mp1T@Duȯn&<}LD,Md=l`o&2=YCXc<+8{={ D0rǖ-ɚ@o]p=vn 2WzNH)b׮7 FFH~ Oι 7E*]M My\QMJГF5=u)>8Sj*MǀSp:@|r8GsCI/9% L|7*RNM' u Mϭ5 Q^o‚RGCUaC2PI:b3H𩧏x QG#H{JE)pwIMU&z,2{gA90n@}LH:d@nmR -tPEV@a {[{(Q^C#2 XO VmPiAEw1]ЕtF2}`L/~'uH/r=X$ ;{Ν1/2h Z(x2"[Wc9'JƋO-c3ipM4z*޻Ttj='~)Ch2xTmm/I֫tKtJ$΃)}B$X]|'Xh)̭m1hpDCЃ0"] sZ10^x/ )B×3*ju;Z n&׽a3]zo7vK5Y:ij2lnֶP}o4\*zR]=}"Va e2g+˿@31pZ<qcַ;3gͤ59I(;ȗ3s "frtiSIY((!siks'ڃs, .ع(.S70yLhϿ?W@!fĤER6.z-ۈ9YHÑ WۿtLڢ#QURL4zH!MZb.|XzE<_lwJf'7:X<@S ϓ;'C!=\1}O!*?imzn˘AO 7Hy,NWT>01XE}׎xy:q/en?bN`X9$JI~.rYpniU-梁>"pyXw"AQufӫW.*^?EQ r J?8~ێDEzH*ڜ[];؍H1/,7uAi5V3?D`Ocm;RL!QN!nz+K3s rȗdc&2˚HckrSOƗo{LJc5OaE4VSEdbHd~(-} 2YL^I{=8I pq2 ]\7-2CKڍh! ب]@·̇5W#F>,oJ Xslv *un79=s'WW) ;C, d s SK{ڬǕvs,ta.+2 u$ &:s||r \Z&KkN?FUTD,9zJmڸ '=Ss8:^I{|ūd>h '݃_L'2"C&!y?3|D:и1'$!GC!?xnΜCxtVKn͈ OD 7dm(' tyOܧ0Csg2"wy-Ŧʺ,`pLgWQL$fb/k̦!~}K%S0?k(_[bX ߋZY#tX*KK#Sfhh!$"kW%tD/wWhuzk>G<ԔtBbϝ&%CȧAButD:n?6G.1?(NI;'g'x0o[^# $njfP;'$}\MB}zЈ| ~(?+fbZ,"FqtHN-񍥇g,ȿ@UmšUD޼F}U/ƚ,aQ/s㡻n=.}_񚹩I=&*,-U&=ibg xfʛUiѦm?:sBvX-GpK1֘.'iqLw8a@S{U܎GHsEucoF t4wXUǟ 2NLW:ZvWrm SFڰl7jZ!}7mތ&l/cYžUoؼW:^߂mc %&8Ybh]Q [$mr`A+aF)nlJ*8Gɬ?7o^v|C1e Z30>i> +z*yÍŸyUl*iX+b`y'Wg;]GuT~H/Ѣ$PO$R$wvU$d=u*Ohq4AKs{y+r /W؛L )t(A0&=8?C jjDT΅#+F} Ŗ=0gBͪ-89Mbڍh -29!4I T)W;!dR\䋴  D 1Al4P]sv! p\ (lӁ4#Ԑ?SK`8&wOsO߰Vpu5.]lkJUm^p\ SpL>Μ>;_`eUx"eF>4hwK0X=LmC_FڧfC HpQ@vM?!Ў)'0@|r^Q2[;Po@C&ƌ`A$_K ^~4Xvl%Cq- s1=GTs.مgr1<,;)#DZІ?P\TcaHZ ^tիhOYMkʓ1WFQnsS3< X2dMH~nuH!.es?'G9OLBJ2q\\PLBW?HL N<Glrܵǩsɬ ub}#W{C'Vڦ'i6atQRmxR$I,Z=|((FiY:q:y>N(&}$s+aQc`o:MEF03CMc & _;Z#_F!%֗yĝw!t i3 y^b!Mˇ^\MN;#_OUt_PUsF>Md>b{wB4F.Dd;!$A'P7ƺVN #F4KW.Ve3i/%41<1."_ 9L_'OmEǴWL AywDE뽪KW 毊h*0yS#|g4fb}M1 jÕ*>-8SkwPU^0 f,ڳ;ڽWQ Sk1ζ7> k+]ՕSi.dvv6ݮoo:/g 8Ւ_WI59w4 ^1IG$oUuFGUFA&l٭}qbZ).~j_nWق|&CmX97xeX| djۯ^<ٵk?L5H+5 N@|n^49R oj4k?SL>hy{źl't1zdƶq}飺kG899|V y,:EւJpߔ{ڒ'=5ֽ01l0M1#no0UB[GT5y?WN#]9WټsrQږ|ojʜ2$AT!S{`W )8zgce#]#5AZOgPtuX|jlmQI*5̈Ǩ#| $nwDOЪZQKsl5\1&-KMm2rIηhbW.SQU4T |)3 ^ܮ?}0m RSZr Uc ;+lt%o&H~CНeO7qZ[6.0:E_-C٦ߤ=_|*}=jsd8 _hCCwt2zr@嘒OfI? dqkbxpc\U!70~?v}?h: ~RJuzd^xZ-H2DV|nA )T9X#]4d"1;ӊE= `get̥5J_D/1#[˦jGFuQ>jim:lv=izRXcȻT6iY[J>5hY CЏmI^ wŘDWA2 r7hGsX~B:t졽iBIMKLFMߵ $"[" =ԂoBbe:qqhӸ+.&Ⱥ󶂄$E;ȿi) pA Z8F&"y"4HPOu>W:23fIӛODl\(W%R)4:'cyz>Loy?*0\ ޖg^)軘:x/'D K&|O]'ߙa'շ$C#ң/ 1x_ -1NqNjH,D14IٺW}qgzQ9b|(Qw%_‹o~At 7|?J0ncǧ'.xq`|IBPn,T:{6ŧXFN ?Qw|7#0lQOZ0Y`uyflTUfX + AT' F \LwZ)ulW.|4aĩNjS[ WDG.ŗћqꂨ+s3c\+בȁn==GŸB[ēS1Kb"?KN # $$ ȟH:q N'S Lh:آ-M(+̃9uػ/GNGFn9[ɇ9b?N+[#-ɚ2ۨiRă3 _~5$* ]֭уĠJUĹt37o5p _^Jt1*=!/ o$0LKWMr/6_ ᴌ_hl>aAƧT.:ظRf>C}Rj_`;Jz'WSqs=y!V7AG\|9~0xeZd B BFJxzwƟ,G>#XOtzNu :EUN >N~mǿoķO=jaxF7oD{u0ϯ$o:\/Ϝ9MztD4V*xćbC 'x05Z5('ȑ#xӅbX+{RL(?:53j7tX/63ڤ5s&7F!LǧѯO4 f֘Wxp +5UtHӜ|FmHPiЂݖB2}yEK_7AZ.<}+;mķW~': ѽ2cX>.?E\<:hnƓ=˩h$ `e?[ؒ[[(ei]Vd]>MBL=TF\6ۂc"[5  Ҿ v#wQL*T_QL:wDKek+mL$YZc{HP-1YrsD!l*jvmyEBi#$i/nv퍆>dFu~5#R.]h{Tm/ě=V/K(pǴNX1e*9|Us!N!SNIYx{.r-Ni`y!N-pk.&0́ CAzd_xQ nj/<7f@`R| "FM$⤉Sgb{hX(̜r/>o6, 6H!T?9~`.:I]ŋ;uwttQYʃē'3c<0ʌ`¸3Q}oV{dMx\SU6'h f|E=DqpYlza*s^tbLp&0eDfʸI+K>Q+-iDPFɻx󣅘 n͆FPN&ƓJUu5< IV|-&MPb.&e/$DI&A5 dNdFzoGhϭS72h(WLD`zmX0+L+8,z/3nb1m0xa.WEg*C'b.5׃]G&-zp<)gӱ{e o>t8& \)^/^O 1#Pbro\ Q?%N?og-"tGΛ7O{E3ΧނOذ.:zӐF V*D:_,wf& |vc{gc˱3!YIK7Lw\V@fN!ܬ IN_E|y"B˜L946%0`l"eeY}adU4gnLZ*?Jf"nzpΟDcY*DK_qQcޮFddֳD$#Eia8yT9y?O 87*AY?of:;tPi^GUNE&k>{!EsB[JtO;~r<;m"+pe;KqpwK{TmæYgW_L҈ o/¨gÚu`4 J]#,"31 ?F_Qz,&0&ʘ5)n=T.K|rOv Gz.3DR|धޡQ 7{'TBXZV2zb M-eH/O<0鮝-Ŧp5A=>mMZFo*m-,[xcỎ#-l;߬E0/=g{CTTȪ09T)cs‚H,ʺrBy'@~/o 5u8c%f _zXIz0F0p-6XQIYTYƵ{c9 !C-x඙Yotuv@g Ӏ@죢;-,8 skuR/MI΀ |PTVSt5FSywIxj,{:Kdᑻbi0v@г =\н?C[ovF'ka߁( [f߉ "kvl>6,;؄6֯wt$[Na̠B>ehln=Л鸚JTw 82v#=_^q'*i5  Ihk$ݾ{7|˶Qޣ$ xu<^1Cӏ\U;_l$$2!_5f̞ɾICKIͯW4Tk'ֽGyz|;TFGxN^16y~y]с 8H:Hx⹗u\oEA+ mR]UB=}U;jξ"0b@p|嗄7LZ~:ÿ\˓&mq{+ V!16qѧ^ogRN-@ZLf̹>~=E8][Xf|iu쫥$i1hP̽NЃpvrPz]מw,ڮS,yw PPcHVp%*a|B9 16ONۦževgv'Ş%וId&i3<\:*3Ej $ "6 2%d| vIIPr$u \_S iS]RF'UE!o!@z*mWc+G "Mʒ)>GI?(aۻS?<]G1zA.f Ngd\W'ZoVv,nŔ'"L-~V`l&+YsXWγ+9.Pr%kMB䓋WuFtEO`)Sڣ&I>b ɣ"eh/eGd {ƭiS&룆Ue Xɼs;#F ͷZFAz[ObʭVފ#Gƞ+c+y Y2hݖ-\чb(Tx[o_?E=sy'_Jߝmt=!4~I;g^v:Jށza;g@(p|_8S`u9<[#|kRK}r:y_‡"(nr cF~ _xGg8~!,o$Վ qqȿqع2~ J~?iWJI{emSߡߝ>9z4LnrOw,t|ED}UʻW Lޝmx zه sdzUʏ%ZP>H}ذKd6r=2#w?7=r֖Mhb ?&]eWo#=[5j{4XP6 ?b10tQR yYq Q FQ3xc{dFг՝a~I#==@UQ@ݟWT)I2Fr XU`=֢>JhUMqbLAea mJ"O֓޷l3-%m)Bp`,tҔpr0?-tjk)zቻn 6f\ҵIH|}B97@e}m9!sb$cz5#JatX83Y%oDF3i-cg?涮ؾi= *Z״C=kqDkJ̐yO2:>q^?91啥3' GߟW tX{9xבthjKl _O>-_EO 9o6v9tw4'>ix>C+{p1;]w`qD(ǑB8P ŭfNϥcI"iCHj;׎t(m"DE*Wl[^joVwx7 <;qXA{4L)=N7{pY0j`L2V)8uO@9r֏eH@f1j: m'Oٳ GB 5rΛ)OV!l$(eol iaVP\%ӧW?bܐPx1Ax{g/tXڻzʌA`ݶx AyUbHIgR(ݹ!-E&Rۡgh/.RU("4ZW*uJ܈SGڄ+IqWtB ;uMDdjJKOs}BybMI|ƣ/,_# û4~節Iqq?ZF Y 7K" D %(~y9h)Ă?BIqZڌpt ^{o_u_v 9oxt*ZXs}V_+@л%_:Uqgͽ^*Vٹ~Ί+]4F#twK˵] ~@[ޕ -A-iLd>kQ z1BOG3^=S y6+yyY~*FHp 2Mʚck`x-dA^I9NDC!^{86dKz"H:פSaͲ|Jivyo3{1'ׅ$| ."IRQw܇ޯ~({̼}Z?BCu]WUUuɧQQv;W5dZzOw]5=}PMcDh"C/snfzo !|wy6=;6s̞{z;a8o?JR o|T)ןx풓i[l|K|t؇?|K^:8yFsgq[}?_ȼ[FDeIüyw`B}w]MEa#\~s+ڤU~.'Ё>AC~^̼x"ߓ:`LNs'\:O?Ȣxji X%J'" gO%c 1qzt720j0TWc۾cܛX1p{Q즷gs- d}wa8͵/Ow*b&3ƥ3CcH` T@ 1aCL[aAe pޞ((GEe za_miGel oJ=|i,ĸ{Vi0Gc:k5Aj,z7`=? ؂0Y1aCZ\=˿]F+ = yp U 90n#پR/?Ìp QG%#`*%6!ܝ=zP/QJ*uL8+NUMHr;F^/|RɘeZLk u;YYG#2=I3K:Rs;[Z7掬F#F􋀍VDY !]PJepVfzrsЖTcwmel`h_~IOp 5 %F͈vr>M0M8`#۴{jqbF<^'1bZ ^jhЄBƭB‹9k;)WD;R 'j¼')߯ i{"O&an4PynvгS3Ű1Fi9걧_#Lcׁx>fس}kS{eͳvy"4Usʧkwy#ʳ`KE}ȱ*R啬j$ko<zǟ!|_02y&[(Z%|zg()3h2Pe0E ;Qekx Y ˿@b61ٰ` 7R[U:}7_چZh3y(ܖ@T3vg@iםj]}̕V.}xYmRJ˙hrvK) c_STUbUyoTmqè}WGCKT(b]r]M*sQi^oj]_6nү}jSA!KyYsa;U`K `~[ C|.׾e8~(6dN :ڣ7I}G?0検E ՚`0Ţas+0-~d\ԪjOD.33}bfȰ6Z  ydd K0 y'O 0eCaƭuӯ÷!!C`gם.#SU7qno$cKH;mOPo$ԮQTw{ur;rM_QŲ5o7KiLGOV}zI{YΤ΄_RޘCE :4wzeuۧ{F'=Su uv9{A: Æ߆8߾ nĐ q0Q?ns3[:n_ %?-Q%4RG dDw0IZc&]AiU~J9'uT2UOȭj[eW|'#/  oWXZ9 ~]b"dP̙92hp+IqNBuWsSSkq#ƛdm؂`z}IyO-xƏGPF# 됙øʎ n^͖4Z9 }V$ҏ3Z^!F|`fj!r`E>5lei g-ϼǏe{L?Xy#{T؞e̻F H=Ja= B]TxPQf&[-/X-oyÿ~ڕ@? G"Izp3g4R`^+}쏎“TϡpyRYggj6X.tV+k^Wu:- |B3% ߮UI>)2柖i=v^[{N)O'섄xRq/J**Wކ>ېGW~\y/O?CXRCO1|SPS[[S`^^=SN%a~.E*{y;v;(X4 -؇?Re*uK-O_f1gƝw?}G]`[qzЖXӑh"ij>m6jqA˚5v"q 2V&loajeF81^Hr8pNC_ F܀S}cԾ8E/KD̊13PhmiC#4ufv6BYuRQإF C2CL9 F= -8Nتl4z"Ȳ (Vh Nfl)7.pswb&qF᭗eV4$^9zؾ:ptEZYh$t6%&ln]=N{O؟Xn=u3m7"*5Yƍk8ፋ+{O(Xw M;Y3D:&6N8z0|aoѓJgGkl pfQ16z4rI#ȗl]!za7{K%vmHWTYayu81O LHc0fnum P\Z cN'ADK{gbhdKFu w[B:]ݽk0=z%-LQobx֌[K=B1qh k{ȗTYՅA˚| ?oKT'_蛘tH+KæQO|qa88adziڔqX \XZkzZӑW`dp? KL?p`d%˂]ξD ]H^^w0&?VjK o3gsaAG<iC(h"@x!a01qVT*%0k4[E-È{>(g.O&jyag|d冞 #IUv> zv~6gI$? >_~͊ؔٶfF_ TZ_ƛKKE{U!(r/Jg*0ePpVeQ7YrM혇\v1AbK(cYSi:Ժi*r] Iõun&u-<2p&abjuǃVUb=3ظ !96d`$'ivD JQޣMp CMėkz`[nAMM Fhɇy_|.&]sRW"xzCȜ։H:~ CWZhyˠe|^ywq7 ~a2g @T1X|N>jU}2Tz|kL2iw'a:ssOe1rK5K_h)ytB[Иm^xDz!VHw}JQ'&-OggUwh!˩Ꟗc ?i1t4neAWE\ yLQ%<`^DxR:cϹ+?#GIQJZZzKD*Njx4V[Ew*׮ԿcO~{?N*jh[iiu]d ›|I%uU?g @),Y)޺moAJVsiMjTR ,7?d!"-g-4 wtB@\5a.R*MtRiuGc3Ź䥏`܇PX jJ>Bz4ףG30,7JGS|UW}Jgeu5O-IӒG-?z'tN[VSnt5?m!xp/gX >wt*X!s=xjn~WJ;. t.9R6irM}|w%eHyJ$uTűj롶SO?&k-K@|[^î8)  >ڕ*q>~l&CsD^0{lvm>v6+5TG"wv -5E(:-nB*ne>ִ$|1IT^cE]pѸCxس\"':z?)ؒ+\'>y;Yޥ:&q;mNvsCw4T(.#c>Yh$2UZG1CTq.rbypm#T"(Iz3|0ssÒ+c:RJuk0C i7kǂl8Ux+p(qsw,d7a*!t([z2FaV //w{Ax%gTPiT(Y3U+ @m' P,_<9=}qΞc}V:< EC}5222@n;# u؟A|l"akJ~8 g&4G c:…0GҡȾpF|y&RMlJ0{=ޫTbvX;|PbM/<*?;|vqiF5 H ̔EF&[[8LQT^vm.80&T[wCoh?\JMoF 4'NmԺj$K8g0i JG2h3coѣI'Py~ГÛ_!q{yksk;#ŭ TcQ3m,_RJ/B̼aʩ7~H$Z!ƌL#*=f8ÅJCzZ Ul AB\7Pȿ QX7^;4|ˍHXRDҜ]+zHOO=h6_O?Zxـu,FCSDy=Xtyj.wz(L}_$N ]5UѹX$/z>?I/X.iM "I=cG nzHl|ZkF&OUFch)FPpk [9GT^+:S7MN_Oyj2wqНq*ejۼ{ϫv'}1ͶNbK>.+QM?pݍsNT$H@41ni+TAD 7vRQG2|dDnn"Fa9(;Z)ė_Gn ) :yeM|V { $A6wK ZF+Y^4kK+gG1έZ&<("ДԝXr FM6w^̮.~>N3ґ6T: A.ּ 8Ӭ?y8C?Wh,4e|,:- \xYÏ7/Uz%,=ܠUAfX.%[F_B ccr~ʶ|^gNw/1tv]rAj5]|6>gLI=:KGe3/Ir*Ԯ stL2TF c~]?x# aMZT@!>N1z5z vH&L9x"ߦ'50Go=\|ه8yw=BOR3*xJpH (Ust::y*͸o7V~sYtAq>7{%=hH<H51<{^MDQZX gzV 6A=m quy)=|.kbCG´ vzVfMxdS0Ǒ?ŒBd{#4HЄ/mNj\{/U||S? Е:&cWX2eLrRQf zy )Kb ϕe06^NYQ’RVL) ЛWoXnͺUlC8܇߉ޤQK}+I|DvS}6ޝm)4 WTS\* XΒ-A\I$8c"aW8_{F/r7nCbR ^KhW¬1fu{PN"w 3 Iu*~Bw^k޷65cl&8-?'S]P^U7ބk$;lHMO!oFTЃ3x:j =TG)HD%a5%NwJ۳5h> [ބw 4+FXZ4JLndP1;%A6Iφ3"$Cem$Z$kvwr-_3<3~Ũf7ci g(?`lj#'hB_DzzF1R:HL4C|B6>,iic\>Pױbvbgޭ(Y؃Ti/Je̟}5V+,mlY hd܆+m^_VS1w ?6֯[^y7R8ƗV|0rKjD]ԅN;d "y%I~9 ]yUa V)#1 r)I u@\JW-G6:G+P!B8iWQ>lv"YiI[֕8V#R=[mZ39)վ ɟ8y+몈?t-##3/ԜPU 8""'Xҫ }R Mi^^!2لypuuy.c$Y"7 EkbeO.o7OAǓ(Ysr21ƩH+4*5a2O&cu4`XƭF SO#V@v(,.2%O=<,@-#s] sJ*Ntg\s ~jSMjװO+̫FEX5_0{ˮu:+Usьɰ7a0brnXa1< n0ӯ3d%m<=K^YC6x$V@V#C6ɗaތ'/P[0 ԰)2C0>ܼ. 3;D5KDo!>JUVg|=֬X޹?dug :xh?aX]d.|o7mOzֈmz;HyWNܼk+q%ku<;d=~yضhWX= 4Dhr_O=ezj߱{U٢AOut5/JV.WXƠ>E 5ϭ?L R_isoWW& 9tFz ГԎ^]&BhE_z@4^ BMA+A q5g:IS8x Dc*cajD3zQ46ٓ9¬ldΐ!{>|}?;v-3Au0N'b=0Nπ1NqOHP;i3p"щmǣ}Py"B8v iC j ڽbTJ8Qyc5JT5G;{*KB;{߹6ȵC?NЉ!|#*mMW+) 1:VUT1-F!m j[ER'sdnv䅴UkH5(96J)W]\9uh*$ ɣ!+V.++%aj d\B޼Y&ՃS9I?a)W| oዔTz=r-ʫѲ 6lěsG")碀yKVW/uϝ(>4k dsЇq%/(O'Bʳ3߀YTÅ#{pH!etR1J#09ND2 СQZRx6/=,hHJ"8bu,ξ/7‹Yyj&+Iq;0-v\"FOgP` kAT @)A4 = c`8+^{"}kjz!.=O?t7~ܼpHMNEp{lIoYG<^pܫFQ= -QOz=K ?)ɰ&lyrz:"T  kG`ۡ4z"m jXM!#]"-͑Gq, QK-ǻ~`gs[\uIM .nT6O ~+>%KvBDlMNɇkS*ICp1eDu7^lTM q;];{ $S's'[]\\D_8T«$Q*_s$: 7s }(CI \N0 C Iu2BN=ь37MPHIˀ ?7# ׎}}c̹! -SMaC3: IwwJ%),pj>Z]4[_qlӿA%ߠUӃV#xX;r){/X ݽ^wwc9\ 5gM>o87QhaO=IS# rvF5^5 ={{hœɱzw#0ԐLȚRQūFw;Q0UiL?wcW M>nAɎm)i=Zgn\J s]x&u$pм s>#M6 Yg BuJK _PWۯ-MJ^(4[^T++;-c;x7"$:i[Wu駟O]0jȻUaqx-v|//X* Rag$[BzZjZ7pՔM@8lx,cs@' PL&M19gIq^!ax=J3ƺ°l)IҒ 4ll洮 7vnWfGAgI.GBheAB2!o K+YIBA9>C/ǘOkw7'b7nJyj/Wo/e.K潌cW+(ec&2i(BrJ ӏ^:Ȝ2:kWm86 +VT:ٻa/_-77-d3є73/\C#Զ%Ȼt9e=VN݊1aSxW7:u\[/>3C@>ܜ=x.׻r^ޝQNo^u+b|g'o?j?KcEf1|Ƽچ|X^x_~ozr䇗?1Q]"nFԾ͌s>'dQ8X^qmVDz.߿Go㶸Ns]ϒHIڟVcx)xXrm՗V3ǵ~3^^x1_OdtmCҢs} }c[ٗۮ׽>IxSᏄ͗ϕ%ݫA׹x8_캄tB1H﹪mTZQ2&Ux%*?,њo4lG(q69̼Uܐy!b“{n}J2"Lh ċDy U|YwaCV}QOٴ-(-o0X=a[40/t7G=22Q]W}==iQ{]x"WOUo'xaT6bMM3--p^[ *r$Xy0]vT1JH 3g\*;W4ACyU˃6 c]P#cM;ekj>p>˽mQGkKijVU~**ZͺBNq=Bf;}[G\f&2u #1y/o~(jT>4bQ 礞ڽE(.yO-ʡVbLYǒdWBu($$.Vܫ-IΩ)ol\' 9}҆v($IT>r^dHzeٟBq5y,2mh!qT޳+s|5rM6 nk YiWwSQPYV=t\/M5>BKI]ʗRe[7ęOO=Ŋ]=gM2FzU) J=0ۣ\:fptx?s]dO8JI`;&5\WeZMte̻\ S*cZUQ{9#ĴTzF1 D_)sXk!ul{zzB>Q)J03'V#l&in&<~6Kژ gfٛzkKh%iRhnb5 ЍU76gP-:hdsF^v(*N@ܰP+.-%ll,Hr+zsGzFK]z=#"pY[qZeMp5A_zR60%Y(l04o1qƎMMĈ~A&8Z5)T9`gf9܇G B*N4 Ʌ-,HfXͧK脞oD_:!,46`ӰYش-ٹy !ὰe^baڌ&>?lQ@#}q02C,cuĕk'k*'ze;77n AfFE,a|8J&4q4NE_iTVSK%#I|t$P!]P4 ^g1~Kh4,E.e#V`q@IDAT"D:] M0Xu,H/iShx9|2 &Tnczh>t`?#v 'JMgMվ.{^ɳ#}-OUIqm1\ Kf3ʷg~|J _D $cFhW^5[atRMcI38$GAxWԔ2,4%QG/0մ<,=a:f@] Vg Gce&ۢ7ጂomilZنPQj6Ƕakfd}|dG;rpC#-gD>2%\-qT2$+iaӎ=5IC4FbPBo"f. O2>]O(Uh𡁱.tFt;' 7:AŸ]>Nh($ފV릮SH)nCCu|TAcwYUGU{^{< bLn'8s뜚#\R#} 붬&dnOo1'P^{DXS#Ea=G.*gI{(lݹWjHE94?q4:QOw |_ƒeaƻɝeyR15RǑǔ˷̟@ <`a[<|ڌ~*Fx^u./?zNhkݭÿ9ݭϕםNuUOv1|B`]$H% _l7$yϒKټHI:;2D*|gG%A&>p%0 =kNź3LO&d2Z(8q:qp&-'3qT(-ɥ}<SbI8"Cj%qJ$qcidoȐ0+PB*Ӱ0v؉:ZW7[@2z&2ka|'㿦#qC=#Z)o1 $ycp1ymxl:lo- L)Tw MUF,3{"tM:~ bF ?+iS8oG`t՟V3r )Cz%`Ӯ]IH a?@TzϘ+er6>Vݝ#=Zz&wDWZگ~$V^Sr )KԣM[ox 3$ Q 5t+`[rһ m2cXIcǍbyUs,_%#\!cA$Cos?e\ΐp WOI@C/;WEyR?ym嵠`m 9E JktFɆAGf1D{mZj ֡nje+yJ ޅoMJC);$V;(5%nle4+2!#xc>[;q@ /9g(q\YfB801™c}GpH< KʙԄf4 v]إaO:`kSIJCӟ́N]=]M[# a+CBN*Izݖwݍ 1hFf"t'xS€4ct5fL F;I`;.Jt(R#< __/JG sMuKSO[.C *Md 75B)iN1TT9 -Ugt~*PFps%h,ex?P(QPb.D7@ V+'"R^^w 9G E6;D2Fć&`ogx8z\ 8# 4Ű&i`P,*0،cD~1ر:eLefF:2uyϖ5P2;QxLx ͑Y#zp $nm=0v5]gu6yl*tSbHZ :K?(пaM ؋SR KiSчABԾBXddropAd7: f,Ah燯>#L#+y˴ ^zk5l{ALhKV > `\ IdT2 |edf ѪZ2?o@ֲca,1_3l' !vNcŕ̵QN!=$;sNM>Jn~sHш8.;uɪ9d_߅6m >xsl;A-cY]{y"qϻ,$oGCw.\FiP9z[2Gj\ n&2jN,u^jߐEG1K)Gdq&ξ +ej<,|Rf5Q\ouk|R@~iTe?Ϝ`>н!.6_PoԘylr,M̨3vJupC]sӏDFJ1_uZv.;e3\czFva=|#"Sb1Z!o[^|cb-jkD9c9*cNI~hf$GCM&ϜyKW".Ә4i #Q9h?tM`jk`QS+ӰvZDr>sJsr#;>gHZD.&nPцI-43v^꧖#.91=wMDϳ. ULIpd^ZQژk"֪eH:R8=PRՄe &Q$k31u6l:85> eU-@@E3础9OcHf,L rO\k%sh{BUl͖b|>^` CO!,ͻH؊bэ062q?S*8_vMv6tvToYA^rq>i ǯh2s~FNtwvu\<"e۴iK݌x.G3\X4=sՐ\ 7lD`@_DOo^6{F0ڢuX|)_~-^{!UOWgT z <*P#p~:-xP1$*YOWk̨3+]֛:: f)ΟgOJ"ESF[~K}u=>yH~*zQ]叟Ss@äH[UߟoX+sLԸao5߇_zofRZy j0"l51/7/gVHM>Ŝob=эm80z.VDKsfƎE #!>!ulHuM<uFL\: H*:H;y 1!';'kRx:a$]# LCT~ F&z#&Bf^,nN$3شtqque0>lA%k3R`gA'Nt1"5}k:$y?-: jXdPfѡj1;[#B@i\n.$:k6G[259~~ñP,6`I[>S\;OLB3"G-,<1$[7 ݷFVhK>:kСMbW\썳s鐸t=Ŕu=ؿg?ܼeaN84]Fel:3pʆF^+ n!Mz=gJJyh㥣Ut}̒3vfrq籡t_Fms\5`kQ>)հy(J|A-o)(bnB6Z푔E\ݜIfܫ= p4Rdkj0sTl~倗_!X0Ir2ݼ4K. 4g[y^L6 Dg~Z1g/^yW̅ƍup-c餽 3#O8/[1(*h6a1rC.$"lHbyJ,&c.EGa#L rʳ$*V"0 r*_>UDS,SbQa28]hDᅢH>JJ(Z5vm"bb SN YWeo~1/dRm՗\7>*"zѓ,e݋-RRٰr^Tƭ'K)r ZOk _=I'(Ne'baMp IO5~ /PLՁCDd|ęx[llZ(el㍂Vx6eoÊje0yYXzxDĉju:3k(sOH9#s RS8hK}3|_hᓂ,;|RR||}< r%e%Űupؓ'jDcӂc־[HHM Xj!`eQ[Wk]'`an.i2ֻ}Ս>}z|H:@E9iюEIHt r5sz8$ʇ_Q}o֮?[/t:Oa۱dS2V`zi~yb^P{-N9 GY")(&}/:梦 عǎ'bT8}K꿾.*gNCPA^5u[@me~!L8YÓ9L Z[]w#8zuAٜxqgHèRw̻|z^IĔɧWUs$ȁ6[P13]k;?Ն@Ι*;Cb-WllM8_oYY=dA*Gq=|8"^{ ^A:pp8,mRUeu9N'A>!0c #ޚG҆m @)3ܬQZ_} =FvtHNTsfLCw2 cDŽYԜ2ae>pVO;Z;,7F&QkHy6X:WΞ\FĆc 'Jy+\55.hh-Ֆ0vGO F9vQtXܤd8VVvk{{ C*iNSw>TlO;}=P'u$Qfۨc _^\mff(F̑]xMXEb:q|'pRU[b"mcMFD?(3m9@C/?adDIɧK`!̘H^ww?b(bjjx1ZwY|]ySPm J(G_o-%/=5m^J_G2v\l%JV^ #aÒ{>v,Fq[eFe=_/~ 5R|Pgf|DBF AnOitO(:Hq/pb#ܦZf 49ͭ:xGk >y}:nRj>D֑]s{o |6֓vIɾdVLcFTzd'χ`P&NS}T"Ac%qaʬjQGj~(n&rxT04 _%ɄLtDK7lCUP`h:\ЎFSTCVC 2/݇qw3uVTVn]w7z;,SѨo,Ge-͉d*Elڢ\MJW` ()ä+nfg9>0}|D|8>vg䕴5G]#ρ`uvW,;ʱs `C?SFuq;s^~?lqQ5mHs.A94@>zW#tjÍm!J2/ij6raK*)C ř,QC TrƁci`.c$r(r>#F63}qd+w̴_$m`s9!6# ܘՒJ3J0 7nC`nr!~4X1:=s@A\*1-TZ ԑ PI m0;ԗ >GS#È;ôyepqqWhctk=Shp^ +F|Gڌk0f7dL tUNZdF7G(TEvYfgD97^ _^Z IM]::0#x>r0 l22QW^rBK$vr*mI#yӃ~ψ-96 u qNܫB $yHYJ.ⴴӸ<|pڇ\-o*}fĿW mo jC&`EțƧ_~ oV&ܮo}\μ(<2Btz0eU0|W~*2T<Ƈ.d "p{XBVu[Sb᥂xДEӠme߲J?~~bC]]1 r-_X)疣;G%NQErƨ!z. EPw>YsہK}`U pa=:8P[~(d(߹R]j51o;S EňPQ !6Xu]#ԭ..ʒrYCnxnX&a]v hה"`4L9E"2ǢΪ jS"vdiG/Z޹ɰmrq–pΖ́a݉DJZA@Z̿p \Ș7d~bJ55]s4Р\ >ILM by{x?. IMwDŽf*̼  :\03pA&#-%H(l axO/Љ X(DXNRQ{V,ǯ2IP]LnVVm£/8Cq[q͂e:U\`Eƿ;tyvȒ~nJBr9'?mU LP֓q騥W(}ֱDO{4Neqjr٣`UWR8gqL8[WE?'>#~j`r>t,0"]Օm:q-vZƕx8?/T-dbK䞟PB94gft$XO*EW\o06R,{ Y ]]Μ._y/|HHYMi˗R-6U3H !JzՊy2#hhcH7aFlF]_4US?eqCmGR2TW{\ҶܜL`V?ږ:t/gTψN--CվO\<^r1٧uJe9P 2vTnjmq`9sȾ@6Н?mr2r4;|ૐ;)!w.Y~ L1S[p,6GMtz 衬m@ghfWZDØ2#3RLK|z:!U&FJ5*XXa9 h\a[PcQB^C$i;QF;SL( G&XjE2񉸂Q=:# _a(,-9rrIEcƘ:fz@0_x1*À8+FSU5-#I(ŨV+,D{85Gf4n]5,i2e =8|/*'M pEwUe@MU6}E&{4+/-!n}f| SÌƌS0&j$sc%hTT6j'/OYÏJ:~$',TZKj|I) ż$9ܘO֨ vV&hd_@ sYqggU|Q&هQBz f/C F#pt!HgM8j1rMD--NYv$cۧ@cʳ(kU?NJa} KiQUr^o`UB}X.dp^|7Jb_Je:a-X³ϭ8a!}u৺@!&1ݘq[OeIpW/੬#?6"}IL>Yi`ٯ $yN)6V*jV+$+%E|n6'\ϼB L%N$|Vy0& cs?r hD8ؙ1?scsdW{NuLhZ[chÔе5}5S,w~$:x^НүNp&۵|FWד]!~ܑ{͉m&ނװ S>̇ozY:CIdb~PNk$1C3ZI=`?S3x פ<4I1"uľ8ϙy>*m|m9NJf;t*Ff~)2!hiieaѪpw=oZ˛@ V gę<*NX,%1acပ & Nԓda3-F8#W}|q*8P؊[{ ϑr韩+ 6u-SajdʦƐ%k)ve1ʗ^hoaz fzrDxJ[ĦX+t wL[GG*R2>g"FJ[w ,؇ijHqEɲ(;'+YB_13Јtx#3~Tۨ@]Uh&xzi&`rP |j'AUΨ1Bc-a>l*̧󲱈`uw F2?/v\-&qXSD chc.}F7,]>| .iG+Y[q EF0sȦSқo% I3`sf0Gܧ6paI[/lE|:d"dtb•YsQͭ57/ICa,C0fb$H3}PƼd[]t[ش[8n$}çNF,8vS PHE ڠhu|)|>|}:IRmu=wI]+>xjK*zN5KoYM9=2Q~Gճ7vsG"8d)Ws7=P+]K3];Aۚ50c*c1e$Դ1H'9Vy%|$S}ߴd|l7NKٻòu 2Y^Kܩ7Ö,oySeKnXk' q ֝>X/Xˆ3@ ImpX2NƞSJ)t(>ѲwlZU9k *"?>`W客x LR9B2gήo/߂NZ:&UgxijJ,!bu4zF:>?v㯹XN4|`:!X@q75D2e}q/BB~etV~?mQm#YıYz~3`Q{axD/(w4Y*@{CS5ǰ^{oƯ}4,[˽!YzwClfW%UjDQ.Bh@ΏĎMyǝ`b@t#Oʵ~SοB!7lRpojzFݽ7 h+8w'E)/bnV3J ; Fx8! 8r(Y`1l]A .$2tz22']FLGa62?%UȢ3ۃ#™?R7Ša9)5u0\)w;?hgTk),iFLБgS8+ X.0k\v'DhfZR共*j#R5i{K1`\5$3z#$ 3'!e)O''9@IIgFpP)"eNE+X顳60'X0 ѣ s|INGOkNŻ쪇nFWh$l3.|cu1B hkIw EՌ{b;vl9/S?^[iߢqh=_~gHIMAU~%^NEsgIj)t_{6SDEj43᦭pߞb^[3^O"FVď hod.J'yt6{`EVrC)Qlc>_vme?\\,8/߽{ vSGX12}ncP&z8%E x##L"Js|6aMl#{Ќb+@H>jx,+/{](n6`ʬR7$iъ˛[2zz0<|FQRVvA@g*0vy5mI GoM׶Cƴ$0j(a'.k#M(nB gռfdeV[%#WEaqFv6'0}LTu`y$ClеAaU-J 񽱱`.nZv-)N.2RTyTYR'(筞j_K;ēѤ5C;ĜVLY0ha؝QLۊ4yb]065C8RiB%F$x,v3V p$[B뜚*D!\^nȲ"=6a|udt9:) T3J)ESi>6PB$u MH<m+t1JZV6̍|$#4 N}2biu:8nBmQ5}>jVST:18Hic.bER܊ĶmyoCmjq>R6&@.zr9ud$>=OU"Mӎ;ϦP|]kdq򏂩3_n4R&Gǚa"g7*C G`Œy [UۑV{ 2?YV>2rτ+3p ^吕ApJ)G(;ҿAΌt=WCEm"yppmOGxQկ<[gΩ ZՇ.UCi7J#Fb_H `z?~^ܩ֚]/¸'AοtIGUV̷Qiըřd -rr_ -7QHyωLl`UC\Y84'xqP4۸VD0ݮ޷ȃN ϟkPTTJrD2/rlvZk<|NbJKS(J| r5 Ek"{h4G׾YdN@bQѐ4Eye-Hd264hWZpLE$eɄ/'onM&^KO*Mhi]e MϦ/h6q4Q3'ZXIչb>,8rU"kՋĢAn񨧁/l.NK̈VL4YT 'y]Ԓ\r)=hCC5.BUkmAwwԴgL->J]4 myU7d(``uz:Ϛ䪬a_- W~1o>.p}Z8sEdxy#w|4"£2BxϑMHka@'c)^qEXnŴ;`no_w?|et$eUGf/q?!@k &K3/]"_H<@ʩ<SlQA)$!:~GfnKOw\p>AHSA2 *`.1扽HLDQ)kgdԦ`@Uc#fmX*VVqkachW=SJ/ Tɉ߻CyKkh+Rz:<Xqӭ?>L22zDi-P>Nr:ƌ Exް55󼆎>sğ.D)8*F:ű]UgFKVVbhƖi F04vˇI,-l f+J=-trJθ'{ <9thjii!0hztƎBJiپ SgNpym@_|H(4٨,ʅ'mnge 1~<ˆȟ s~ahb%xFSNz[|vfvJvW,[7g)bvo%FzܱtO3Ϩј:{N;nGFA ?[P89{hFj_gm^2/j ׭d`h-M(fʮ0Gp2 6+a=ƹ9%&DOtFcj;ݷC-mݾeL @CCÔO%'j59_} >rn3Rs͌11j8vx72d0H[kE5( TJrR9Z K\wzN~UAh'Ըkk>%}Ճ)%t"F+#WIC&cׅ?Y|o&"vr,bH%U/? #{0 Td[ΙTU] mk` @:<*Q\_;&0hhҭ t?~TYH< S‰Q'Πτp1ZV$$aT7BF-bjPʽ%r4U =Fe`ɼUxׄ /dhUBҿ9mmcfw_{ z%q|dfM $;|8 :Fa^/qHGTu`\7U4bh̛@5dK#o([ISWmd ==fa8Z|o |]l Hᩗ?Dʘ`ܙT[e"x>aCrA7)b*}wL}p:u5Tc#fm(M;az$l^d 1O#m0wE(.3'?&^U׃Z91->'19TlC@{3h* 77Z`@cU=ܙ7uX+] =3FnFlZ xZ\cM >3@]=· &00#x?1X-ecC)\A@NΒWIvfSKo_'vduE}T"Ŕ'zWf #J s.CF ٫MXt= EdQ+c^NF:@6-9 Yd6U3Ͽ#GEQQ *ܾq[7J6PKt6%n/^D^d)Y QMF.9r .F[Qdh:;oőSg˿hDCE+J<^G\K].ưWbxj:=4=?.i@E:UH!EHMSgiaxT[AHxU$4#їիT6GT{Vya+*̡QWaZ9vmJm릑R4wqs LC9_1hb 0{ qػ )[IDBNO OϾ]lqFG5%1-J3_ݷ_È?548ێ923lZ 9HB ŝ0G Ӱ,Yf"X|*|h4c %Z`&Vd+ƏMn}#%`TK s3q7oQjEW6[7Ɣp%S89C[ed-Ѳg-Bof5eg&Cēȕe:*lH#RhHQ<;3[ ~U/l-9`dD1єtU~wmhq-$I"q(+^܏dH^c d}5G{$p(~Ye(EwLk\F#y;GSiiRMSVבOu)E^_ 4k߶tF!{EYq#2Gy]~ڰ[~ `+MRǏXFzT`J X5˥zr ŧ}+|)/~Ȉ)Y_~9(L}#;J]҇Z9ؗ @x{Lb7ܒ7T")\WrŬTU9O>$)I]Դni18.Ĵ 7~@ϑ8E_h?ʈ׳@uHox1L|u'̢,ʪMky'}x:X6vՕz:U-`R2%.c?uSVoJ}u?L@yՙ&}~ȹُpK_9 Tq2>3G/X"hu S>eLT^q/ghK^ 35s OBȽ#GAgPaQxE,50okU1qV1QJb=9ȌNF0 vK#%zi5*<={ID,Tj:!3aGyC]F!ȇ2nꥦgƭ]Ą _LJ|3+ ѹx{S)eTM`<)1zUQiIIX#ظ#z8.P,(3F2ZyB8>2LiSTvh F.egNL)=WPZ}1 D@d=SusD`pjcFSy2"6;zuy:z?QF2cF1 I_dWRR~l5~Ipur43Ԇ&w+q|IgUlFv63Cv<ќsW3kq:[yZuBIqhSБ)GѪq սƿ2JO;kuK++82bUnN0btt._vo>_|6([3 <N?bdFi0l% /[ /ʓԪkN⦛n[oSNGI?O <UA_8O&ϼҊxZGS27q Vp%ֿs*<ᡣA Qg;d!2_O :y=/AT l\ 3B'`Z`y5i4hFO7ޠMMm"cB Krt!_܅DJDpm˭AB~l#2bˀ?}QOU)e/ Mh;C}cJ:w>`HX9FB/4UNլ+!Jw`-I&@7*˂ooJ#r⛑y3 `"n4瞯}  ڪjwbSz{# ?! DVvro=-1Ic?{52"*QOoyqIanYScɌxcF#;/n7&w}$A$G5'_N3SιLaLjkt6+FԩJJ- J`+P 'TOokL2.KOR%k(\Bp,+c=J@ 2 Q* ߕ(McmhKHGwU6 Dڂʖ7QŁ=-$ y}(KEH%2y/N% m$p^LүSU]iK(Lgzt1쩔TdtE澶}ZmX;64H2r9vw,IfV#]jpej0-lЩצoNߵX;n?L2X.@~'Ý=Xi7&?o{V0Gn;P8F<'IhIaX$VxոH𥕝"mMt6qvӪk4|6pW08r*ƴ%8XӞ{̲Pp)‘]<#ZP.-xȺ דCՋZF2!CYg8v``35(`Vmt4*h=dfm2ed#wތ|CK;]݊J_}XvrrNr4f4y4Vþ(6 INLB[˝;vmI:zh2V2^($7r Z=,C9?9 |ʂQxW 5c<,+BqA&wF):zcC:^$c?{WOeիz_=BgDx(iFd^~a쒿8r^},[/ZLzvcR/__|Fs7q\.P eu0`\WQ,lSZ!Eg-_r$FyQ_&y "E܉l6:FYrRoԛ0ibLM.]oäp p`FږƘsM.#(EڭJȺjqyA96lUW?mqI~ʴ\/& bvsRI#̉rYĴbKK/S -4 䘏Q s9E*ƌ,'Nt8R*QĤ vEs 6-2~LCCJɦwsvsnԉmtD0jl>/>Dz""^WhÕ_oGN0#Hk>nY#`xݟb^})Qs/Ye<7\u2X_›o) 8ʔh>F]JQ=c_~@d2a2x5`zMb$ҖdOa2OJ~ٿ-rk PDQˉ ̙rK_}&]F-o  FL6}9-/} eS-+j!}BfߣmsebXQTaYEڷd/7Ɗ>|]f"|)5<|L_ߦ^_8&i1~ 4Ɛ(/c:Yr?O,S_D=b<0B,Ө撾':JepO/us2M-mj!7#NFlg/\5]ƱJM-et3C)K1cpgt1}ݞ3yB"khA8#ƊUwH9}"/cfPO0T Kf.EI1ww]GCגUHK:,̤Tw ΜΧs9 O=4琿 l@scLX0:x-'xNf7\=TA0#M3XVs8UQ$FN'*?}>?_1C)Ac {fV=GOLu"ŧņH#%+faj- W:w~/\~|^d+9j #₝aaJ3>Ǵy*Xc'etۚhIܺZ,`U\~"*XP-F&Ĩ$/{=I{E){2J0}pzf9ZwbH*+ˇe돟`АHa;f'' bBCT!,`95Dϝwr@|̘D&\N LFܷ',$/@"FP_WbK"d@kwC7eF@˭*ݝɅ)/;_jkzQuڻY]yjRI~iXN6PюTdHRGfUwu{iԌ0\dHLkk!ZV};uv3ﱞkgT*2qtl1 &YdRܛюҾ=n셢&Fwbo\LXN$ð.FPgvq3Gx> "58H^/[a)ccg+oQtTa]3+[6lLv2f8עp,+ yvPԲ|Fo Ϫ0=60%O{;ރfޟtFO:ң 8u Ɍ> s]R2l iAUB 5& 7lJ `_OʏԠ p(X5_zLi#,~Jdafdv;Jҗ1}‡͟~ݾٗIWN@Y.[.TZ.!׾mood{_eB;ɏ4Bu;xq\{6Lύj} +ֵoK{biۣjT;KM'^ɟ('.TdMGAnD^˥dI]m<iW2{'cI:cxw$87qde"a:o54cjLU䚥^9$'"StHRD!xPg/_<عm+}H3Ŏ|o0'ad!ibH;tP:y+VE=j|y*ӦNnV21,N1/S@x'6 ) @3cq8wǬ'r׽(/,0k h6(wZxL-1f\|;:kYCp֩tSL2A ÆΞL&@bՙZ+SPX/2>Hf&͊J-i`f,R4PÑԈOڄv(/ctk)n&PJG;58|hedKP쫩Pl4]cTVy0Vx9^Zq񁧋 ,Gkv6ɨC!>,3I[}.35n4L?d%54FUQ8.u2mlٵEF0Zke^|GL 7A7Bʿ!p"p]/ 614 w0 lf :ov I_\fK|p|ص; v Pׯ)gҙu܏`XFN,LOүsol<:Pb t8_}) qÛSa!-0HcveD$A.OlCٸyqǯb5YS ΜG|p: {kϬ߭^-3tM܀?xy̫`!rSȠQR&A Ga݆ x5Մq<(ۈ_]߱}v>lNjc G}A:)yTDv~>XhŀsO@,ǭH:~-*/"^~ ys1o220R`˸޶֦u?O>FzS)s7dKY '.:g92J}~Ϲfʟ^@g}(}3} H6È(~>zXR \+qմh$WjLt [&$ խ:أ+ {w?za?-bJ)FpQV_(!>ԀA%¬f8$j;;j,r˦/主fL%U(;9|WO'S }۩<N6fX9u0.oiKfѣ~$ 42YظҊhaH@7qU-9zXzd4dBB1j]@WLA5&p1~7q;wPZğzbٵW9ْz T:@aq9yZa\Ȋ)b ("uvj\iPj@%dn BZnA 5`ST*&dЗcsF[)FH\ah?o|nTJhj>2‡¦W۪ [v=5 }{I/Z;ql|#*2U0_Px$8Cah4x>[ n҂3aVjث:d!Y)zw?KМ J3RkБ5liLLjVepvؘa.~he.&]O@֑ ޽ƸDJ9-/fDS:@VvF[nC箍z#d0"OQ2XUUz)]&t 1ҿɾ*jۑwHU.N1?9H/}w^QXY0z) uİQ8v@cZnzt:CL]w)tM=Zm\ʟUͼOX#R8! G:/Ǎ~/@P_yIZoKnF,?}r{yO??AV\9jx(bJ3b3E9AG5DwOm#nt8P>Mem_ϕ!בHJ/k,;tR޶;Կ)Clq:}N.)驖w>?+ F])C_{d#v/ۓV_]7qw}[]^zqK:V,yXz_M 6/.ꅖ-(;fRɖʶ(;" 2v 3S@??_{w{Z5yt``\6GKJot{DZɄK;Թ=ܛPwq!u 3O]nRZiVk16'UʯƆ"a^OJ?SX9nSv ]LX#0Ȭj o8SiP3A#x~ iBÓؑPĕ.]Vb8{N&13SlD)g'ZjzqSCG_;AmR2ZbM#~Ç2/c 0x6 (c6+3\ `}Gq{R~HzI?w夿\8eΞ: Ro‘q5㮻•ӯZfL6Z >A̪mJXZ)eom#h㵗BѓA<`ggSq0AWꋋI!c5i4tLA;LQG},!\#|9ү0@hOk@Q“ӗ2: i]>``{`p3ZA""h8 )Snc:$>Np'cٗ3 2+`WVfK_:ٵm=)}0aEٸbtb7O߯V NÈTպfzvэ[rO=NXF@t3?p=68~=pW~fEV) IOnGdIVSV~T aͻ(ݾ,W}8XRl\@` ꫨ!1W]\+wuDz;o=CS=txOA~N?ć%~$uJS* G)mF `/&Ois}o]KmǾ8Rےzלťl9~ (Gw*m'+YJL8ZZcOA( ȐF:WCGGhUbi v֜=le1p*ZꬒM̬5AgaL=?[CVزs?rŷ~lνl fMd菍w vb ]Q‰5ML91`HTWTm~!?g;$=D0drs$ $-~mQ 1ȅt>~MHZ]FFiS8ɸ9v5%Uo"#1Ak:yoL@Y`̀wY h8ۛ10@WC#PM˱RTBz:9ƻ{\:{xi[5vU%V OY@QF>3SMIY\i'[<4) j۳%v3`m` IlAIp"WSS11sK5%N3(nf (݀pgx8m0bBG0M _(6/̻:(P16͵8sq.9>6&3t$Y2ӿkyz5:HyWϬE'0mjz+w; k/'\<sVbjl}"#GO!CS2 ~L3>`𛭓RexĖ;2.n^ڷ\oXy\E#8Y.vY8e\5*GFEԎW S3'OGw72; h{y+?+3+-+{-b%8)^|GnO(FFQ3nfDli=X#XrvDDi5N~f'6݆TnDןbM4Ahu/ʓ} cE;vaԨ=Gz)gBLbLG:NۆM?!gܞEhƴr(e?Y-sTbO*c.E  u6Yg1ڲrWKmej:Y.Mo?ׅV@v1er2΃^v9M8#,cq@ 1{ܹ4ɗ9o—*L\\$ipNn }o܏^@,V]FPf*sȳ6gsN~g]qw綫}/[GB,3" ˟oTUu|X<Qhr^~g'eQ<=w{dm!A}l#Σޫ[[-]{OVe ]]Nz?G0yR=筞0y}Q?& 7gAߌXU{_0{\8vLj f3B9JSO"q>E' f1%D7:e/iY47[߫k  ?;CYEeJ`ʘI[*j뽮/3KN:pһ-wV1hN? ظ;:H_Uڕ2_j=4dʹLƆ)xOXUMeNJ-M5ͣ}osʽT}}ʳmQ}g.yO[קkiGW,[Tgj,%E߳~yWՁt 5#-n (\7_5%Y+c"㤱0Ͽ}%' =ӱGmRE3Q` .A6>s~2s415 koabi FEs&AY)u]9g~ 7 .Kg,i"K NCuҨ^+IZ[YQs+@IT:JI\sQKP,]4C{RkPOؤ eꮘ@IDATط0R$*gN1v!,r4`aA6uhp(a=Ǝ0R5>1D؈I$ȤqNC#4P Ngq9@9, /f^Gt.;KNMپե9N62tm1SK-K$)"$ď2OЭ/:f%wx &ݏF\K G  +&*MMйN.'tH= 3~>(Ylنc K+0@o` IZjRLr j P^9nD3sY P[ cpM7c[g@YhܛnIL~f5dfdT ]fL;Zi L~z"ݫ|nZ\J!v'sk6h4Nġֻ} +N[oB%㘸x~C4EL"~qK]/T 2гuu|Uc1c⽽̻~6>(} 3*Ӥnͨʦ6N&RFi;cKVֳ,S.O6'3H%ao {}1CP@08z:% ~)vxp$(RTRg.Qc|dO܋Ժ.X} ė޸ C773%ٹrz~| (0ոal'&Lk|7qP,f y{%cM{g\]IGkqDj d# +z*&8WhTi@J_pB>-h՘cϡLDBAWD_`?M:I&F]'1(ȟmgpL `imkGܪ76fԨȤ"f8e̦懆O߄%&7=CU% u8VI_]HAOL knzLْU_`q5 `+ϖ66,mlX4r2n\R_Lz*ʑoϾߘygr1ojښzhmc}%*˙=^6U֎P=Fl_ ~SǫsaH?~yiE]u(E.GIqW <(=f:2SAtbŽw}0YK{"/`Q~5u(~^9resfoO n} '`8iJ/tNy@=/:,[N"Ȫ?rm}rs`@#]=l@!+ /FX:b')X+ +.z:yj'n,kCHxM7*eמQth\|sTP$ƚjGM֋mEf+9}9KtRΩWR",v#ϙ܈}$ucɤ2DJZ]1vs`HubM-0:Hf;:$mW`hS#ı14_}#:skg #Za92\Yž4L񂦳YMH<\3{&) r8{xltaabA=lzh)xS`y1x2f}3 SIԵ RhTt,ݺ NX`[⤛zT4WuY AZlŷ⣏{=H6Wפ_MWvQ,wejKb!̈V'~V!ij{r-b4*_ďoHN.Hݥh?OeA:u>^$uO]l,v rITVp%_K.{S/~$QyW}:ڛ %5OS>~Ͻ )0}p1餣=TXDB tFW#,'N#$`}\f eҾ/wf1GgRGQ;8D.rہL̹fu)B2Jd^h}Czi{R.?z;z6{6;O)亥_w_j]SQ:\(3q!]e@ލ<\pM {|B*m~d;1el4iB5sJN(j jqӭKY\D]dX:y(2J@Cu+ }lN*ɼ Go訟zB/kIyˢ%zS70et8 f~'bb.xh ^pw}kχ2ۧRJБ~,&Ixi=5f4S⑷s'cq_Λmg]AmZ qZ_8OG|J< /"RMwzLC܁ 6;a[-됑ׂ:5"m~ܴ(dэڳ`&P/=wf8iHB-T9WM@s^|]}xWZSF6.xg0y0h ~McƳ0 f,⯾",Evʮ<(m[;=X{/y6002>oZە _~-ḿ#I=@{ PF9N8:Qr$2NaP4רOUؕi?~L mR G20B-b 1*d2S|Q&+{_Sri:}}f4:ljƺh5D^M;4`wj7*绐E5߭GP`gAYـp&@ ij[حɻذ/b`;\H(0AYn0a|O*ךs<#^%nna֭ %zqY3q9g`'N`aB퍵b_8`0CItE#pCt$bKNC r5@=0ѕԎ'Mn QꏪT8z #%ub="\}ƴ?tݰuj6P&,aRrcw"@4q'pxfÅ[5.-)Frv٠s.o1AdhF\V d_|g ذIjS<bPikQc g=ihh#gNṋy5Fz'_>w3w008Vd0X@t0Hb`GzZ-LX>oV%S4UL۝6[j 8PNL%t8Rēe/iͭ @ss+:QJ 阽ѠGف0A~E9^NmXC\2VѷWHăLu(MFCE Zt-QL LkB{z;+dr ja. =ڳzF[vHF  kАc˫Bw8?o!sWΑD\N5~d^y9w? ֟D O~}Q-EB) 2`Bg)L2X ؇ҲTѬSHӽCAe\Sg$:T]O =6m6Oa4h78HZ) %SH}3ڰJYp VM |=ǐsIץg;1.\%Yݣ Z_xNkK:iD9eKRغm;3UU1.αa!|&8y꘽WK&74tg(r16 %:SԽ_7%?P l#z[&n-qj>RxV(:UBɪR46{Z݃ ߢ?u U'KkΪ6Fo9KۍPvhH a쏊 &uԤ~5C- S:**aSX PIUX[RvPO{#:F?CM5&0)&IdB`xl'!T؄HKMä ci,(vς>;SQ)u06c`J rdE lIs ??/(!HI:PjEXb&V*8ɜۑK06N)AeC#K) 1zfIpx-_|zRMdn 12} Zz"2r.CtiWڑLV&^q-~# ðeZ70{5͈zEEco,ƒ=>OcW~o?yqlwoÔS{U_#}R7.D>A9ϴ3)/^8I~'=d8/ooH@0g2k$9PU0wy  g:|==])ݠC_'굗:[?s.ξTt^*Rdc(Cagu4}_ăv`  (,yҖ{'2gUkwI3#[f_<O= OBOm;ICp]3ۂ|Y[Sg%GÒrj]Z{H(.fivcw* jSsUozv~ȳ?>UtK>ýi/Mj]~SMO|WKmhW?޿_9m2әQcUhfbs1S#4C$8Q[~9dyg=f2r䪥mMt830",m.fp$$}|L43(_fӏ#O>? .ޘnEcdD alXιq&HJ?8PbYD%(5+Pil 5Vܖz :!Qa׉q6aa9]\9jBcp[>kS] R&_{`̬]= j; kl#[ɧplO0󣽕u;n}Xx?(Q 7㦛n%VYZA'!3'ن sǢy = xIE@M4C"zOc"b=H?),aD>F]H$M:U\DS`珫y q^Ë?R_T 9wڷi f'& ƎkE _ .3+Xr24;\|uߡh_81C6=6Dy Als5ƜsfSG&0W+#1Fay"99YLn¢st'jɟЁݮésNAMU-@!\%AJ.؉Ӱ̘um*LrbJL;Djvri<'O܅hUg; ~`l*]027͟ãQRG@O)>~"Fp~^iΘԳu shXю޿T Xxi >.g%)Ū_a`(avԩ:hCI~νnX5$}B9S%XFoٴԵHIIe" 2ÃlR>~SԇY?.:S e#}G+/[q;Qk}HêO@ub[VU#TRw [4)kV{H}{$'cBbKcf~QX;|H1gR[( &OBX^R#aCx PoFv.b+f)j0cmeꨆp)=K bO2 D}ciysn(;L##Fb`8?HSI=w&j]p0f'[a@05}+;$QL0 u;sN@Y%e8 򀭳R*qצ~"5̎wwPXF\jn160bT u]{;NP/;afg&Ot0H:Eh>i>f9R ՝㥱n ܔ8v0J3Nz0q5tu*^]S3ܘO]JO%uh=x=Q^Tݶ {7[/6P^y(c sgnYP6᷐~na:Ra̹VԐG 6+MJ_ )m Q1TkQBQdh™p% օ9e |mB֋qcKV:㨱[P_T)Cuu@-%,Hojnsω6Y d GE㺥72{(@Fkҿɺw ]hK9~R@ֿɔ (Zc<ԩ(!"40Eؾs )5($@JJ >xVTa'O7_gD㗟a$Z̉Ś"|4DHH 녻_Uߒv&T1_U/b%i_w*ڍzPNwıNDIro(9 h8+Ǽ0/ìĵϼŤ0bʸfҔ:ib\(l{c]z؊^^Sk>uj_ijb x2 G}Ϟ ՌJ5C+Пd!# F *`VX70jQi>Hg.A0C]WW(G9cN3qlDcO2v+Gi(V\IVi/uu9ֶ4o E5I| kv(6%Q{KMGWW@{Md2m1pFlGPnjQnz"&L::HS0qlGC=4 HQ .8p9QG}T`iO>ٳ6 P8 Vi;SpɅZh(:D]1MYINA8gaN3v҉EqCdu4EaFnqg +LqLH-p߱t)ͨ9To;8|H&Gl=~C8ݡV_(-E.3] ?hxy1;vM!x~JЂv#AAs)q$Z5uªKlM5m!DRU_oEKkSUъ4~zi۶sm}K?m3]Mշˁ s?KM9U]cfhˋO8" :>K#4<|Pwo ʾ#ˤOT@T}Gܲi::}!ڂB#K=J}1'?͜0y(ݳaN/=\d ]d)/ X(YݷJ05m04q Px vn=-GWaKP1 |8򷣠4qg:m:9jSYh;\l# pՂ2* tXFXn?N5KR:uIB&Mأ54PJ?O_KFu0sxavRʹͫHl#U^na1sUTIJfb@ tXfc.Kc)vft$0#Kc :ݘD{_N䤧VQ{-8s>PH\h=qWKp/- /BCLcfs{oR+gѼZ@N\ ᑴI;1M'>Nn7gW{k:ba扵;a݆7;3(܂r*Š 'fV!a Ч5%>Z졺l}֤YE+T|fɠӆ0дlG 3sDjʒ8zɠ:≇YW'v[jH%|=iIF0e^99|5H>?L S ]'{K)MA ǛFE[PB+=v!A"eѿ`;bNjg/^f9&]qV6'Ng] eqwA}FK]>&13g~=lC8992@;A3u\}kڙiFZb72%v˯wS{2vhbjԛ2TI8袿"s~s-Vg.+r%DFY2}_cU_k|W@`V jowOL#eЈ.C/S3ʡ bٗìFI0VVLAchaKsTL &cD@pK P97^= QW"|[CqxNWhfeNυ>)qIOl8*f¡꣨P7b!/QXys`])WǪI=0Y2T oMV lw?ޓgѦC/:ROaGlBy[2Ò UBy2 DJ!&FoEogzY1X@ xq*` FW#Sm47à m쭭n0!P Lw"<65A !t& )05c澡ZHwʂY?I5][@՜״lݏ-H!mmKM]zvqC3[uI FvdJdDp!Xq>籠\?ΐ:7 ̠c/&mXÛ8ǾN92;DJv2(Z:MÔQCQZ}s1H q%l|p,) [ccp^{#tO.ɦ'o}A@ Fj}2ѫ: ±7g !\L.38`|Y>8ZN[]qY ]}CְsOǟx gV0*`&wK To_朗纔6k :+@ֿ i,8ٲԞ]1D*iK,Q()4"#OA2$)[oQS|ucqNZ&1| .ʭ9~`ˬwjߗ *g:>sdWMl%XH'[' CI[{զи( 'ɗ8:rJ-[y Pf8(+})٥bPOX"t ꜔|2]=̩#ȥACOf&( ~ȱC:M #!qR(=_F8HFڇq01Ŵ}>חx1AϢ4(6Dכ54w:\kS_߀<|5AC=Z8jjٯetq:78%%&aDv ‚~R[eBŎu( uou0gr(x匆:aN(ӌb-g\ 8WF%{#رt0 rX31F*,0f{O9L3/:d1-Ű'_icIW#]Rա+U GQ֤*BCUdzoގQ.54 pǫ۠S.Vה6'Nݧ\-4/˥SJ>Lng^qR|w ͽ㼫L?oej!07jV>;ouu5ọs@ۥQB3lwi_jS?.ӟY! t/(^}!Fzr/ds!Y=`@rV#I _zH3.=v*#HM_DLnՇ*񭹡rD.ꩥ"}`f>-p դ ZYq۝ hq `}Z:q+z^uOR_.4#EO}hfpﮇ #XOo:G:̝< CG^Vl.X"l|W0$rOXxWmGbS(c_Qw9fHК:jʟ9ͅ>/{ȱMcF_>L8A=c7޹jD0{~ᅠ眂y :fM^ynksBG]cP2TeI0/Q=x%駞,ܲ#F`Yغi  ޥ˶ Nj)p?Wf"QrllZ1ZODsI&\ tBW { ntQAՔt 'ofl:#m?jyt9*l,?,NsՓ5p\0}z}X(}e^rNf=Hs, #xX^S^I`dyUDZ]{2HuʷPohGJhs2iش)8{Jp.0yk2NAm9M&4vsnVO?yab)v0xi/\:$3<JNS>s9dF>l|ܬ5_]x2fDo| Gj>ps~a?>ߝw@ 6z;Dw_G#2#vI0A]f& ndIE1.ۆLO|Ⓞ"!w(-P(PHݗԷRow^@J@qww熛C}?ܹ6w}><*B\\hcvWݮedxm=%d{y0㰷q{KiWD=o^C{yh# _w+S5Z^V붾VY #x9#Q) TH3".@kdh825$ c* `(Hn@'U«n20pB*vSsЪHjAmߨ]5YA5$&z%֒c|UoK=O͍hKgA샯;>X_tlCΕAAn*K%Ǵ(rrPpd*F29wskNoiO~F233?Tٌ*O4Y]܅rlb8 ≔,hA2}ZܩTYg{:Isě_#ΊbQ<7^~ m\?:!(j"PUQB"R>y hCT4b%;5(;bǂakez3`c+gp/\D€ZQ0+? bEy'$䗕#z UXI:kJ:?H8@ Vxr"j,L/XW&9 d‘{8`\Bz6y frN998L@y-ݼYLik0]Ws JbDL_w9!tPx"qL( JI.a>+*T1 9l*A h)u{ݔ-fwaD:_&3Ч5Up+X53dn?m wOb+XeVcc*, SAf9b#|Џ,*e /GRᑿՔvŸ# n*|ı`l3I_#PWWVoC $6ixXfq~<#/orƎZe ~$uIՓW?ns +8ړAqi݁Ϳ+_ Vd:[E|aX |= >eh+`ijoNBŊQ+BmŗwqXϡվ2 ."|V&{e{{MZ z9$UXt2R YF};4(Jy~N5]KAwܮ[N9';OJY6bLV,p3hJCk0]&LJ| b%*y݄&%SG {'Tu+$6Jة[𽅉-`G %';NGv6plԍ bn]%x%@s 1Vx1M`k<61>XKfqr h0ipwi5ac>X[KQ0>s Ay031讠VE)d9dA֝ s+La.~#xb /RȈ[`{#2 J%PقJM,*z \TF.4eB ¼|Jt"6 I &p )R1̲Ƀ(AFe[l d!W?:$g%(sCk!ݿ~E~rlWn<#Ǩ)ϛ_ {D+]vw)߮/2,)gURVM)|GcZd]0_*\XrV][Ȅ?YQIhd@V@U"&0e})}xo)~y~DyG rzDN iEo9L0sy?zo\o݄QLoҋ0rVn4VC79IJ՝>TFXIFo|I=uϡo̓cRAN5!{#ɟY}H.V# OӯK=f~yNwH'׍~A\R>rϟ2R9AXVDvf~nAo~~?Zg_~2vl`gf};Hx+5=2RP̪£O/=#SZ|fF`{=% ?ֵͪ|E^i=sO$XLQ14hAUɏaTS0V쌲6K9{2QYws\J'U:[s!4r9ٔSji嵬ͧ,9wuj]3iKڊ>D@"߭H/|ظq=:ZX:] `qr? Y=+,QWӈ/Bz^1^y{(B.,^6EP_GyLَ++J̛``a ŵ:I<=&y( \;P}~Tbvv J'Y<,Jվ7}9U1ƟѮ  `EҠhCq3 }ʇFu Wl@IDAT}9R 9y&aJf֦J|=ǡmzcN1- ]l aeBG >;͛'x}^ӕZqlOr4V>,V\U QC򱀢(%89v* 灇DE _ʃGq)ي,SD~`1Q#zVY΄ %O?@G2ImVΆ7/fpY/4Xe 3ga_^DEb}m8UDf->BIUO_A$ {'u[ zߥw0o0NwuX8 <ǟ#1t3gO9o=`U7G/?SY=SW :bL7IBu?EJ I9?tID 齐E~~ISb˾rHS$Տo !o282(cp%ȱYMVIeߛvέ砎d y Vu`tPizl{Ec3(J6v)7Ձ33$D%!^^[IS$Ӳ,Ø WQbي5n$kY'0=|ChoF1'(筥ȸ@o$w!B1r?H6Cdg+] >Ɖj#["shء#Χr1hA7qa"X+d7vG)eYmL2^TB kF! ?r YM%=9k:>{~5z@ +__+Gv\p}y !$L:OU%/,/ ne)oP-egqK='I*8v͠R_Nd@.2?)8WHf7ijЁ}:[[:{ S^'j4|ܷaDP_EFc6*.ۋ{^nEw3Zە>mp~v hR6p&U6^p>d:gbrIvY@8unO)cWߟ`3d};/=I oc+Ϯm :b$Sz##ݟ@s7o18t8cyϤ49ij@ Sޫu58Ho;Kf~{ۖ?  7U>^MPpk&fXh.\͠} p*)T[n#ws{أtI\Sl R~%л%Ԧ2&+@O¶Xq־3\;{/>/xgTc},(7fKFX{{;v5T~1V1f_MzޕY&$aJS NW⽣{02vV&6c?KEI8|=צz7M=NO{RԿ7+#Ȓ]}zD6ZqڿvM=~KKg O!ҸO? >,ϛxZD9IH=Z{ssJS  vH$BF&9Hsi|,Yj'8٫xi߇56CۏcbD8t<* EP`q Y: yUBp]==. ax󟫔uㆎ`+Fy}rݫݗZ!>mvo XfgouG=+\ ن<]~P=)wn;_wK<e1cP?NuT~ug^n|[5ۯAw׷9ivϮ73.yNXg5p(dQkN$xV޽i&uK+qRVvHGTD";%M":c+K=]S2jh-yf51&0jjLV+4"0ЗpH-`A#Vv8PIe)bbR)5`2e3Z(L%Hk-Z2Hihh@ -~~ѓYkƱQwN${o7e'?q}OڸM碶1 Vv$9 LM:08W\1 ܯfRALjJ3חI꟰gq4:bM]~3& AP@ _|H$0{L: ì( ~Ίs nEHI>7lxU3jCo?d+GCK؀S#Of/nݷߎ!L?Z|7.ߢ2Δ+g+s Koqˍ`Ʊ~7doAj4ȧ8DZ:Z-=Hʹ(+!6__c-eVk֮_`wCp'XJX zJ b!p611|uYCUg]Ͽ"p`[s^۩V_CS-͘(Hv,Hɡ Ѧ5Z/3yX,zU|(tIˤ>W[Ʃ/1|jaIG4q^xOw<'}z&ӣp^_hf EO҉\h ~Kei7ߴ*F_Яٽ㍞u+gɆÈ ;㗝NB2y'3a5}9cˏq`_;ʠ>bD (%mcgo1t==;'՝b(/QH _~RȾx\]O{qXV=~?Mrj̪wL-I#3Ethð+Ok4XUX:슧}m5)$,.je,kl%=NmE:`_ 9PRQI:l?b0gڻb9'Xf/ioiNP9ƎZ,pq¯0a1G{r z[~Xj}MASA>Z,v0@,>/Cf DYfNC)f~LTɦog{8s.osnohxlݴ gZaVAJLq{U5h*~a Q\݌cH:2 :=2O;66JB>O.-:bIk >O7=uDzbtK*yn6ibVc*g$񻠡*b'nk&N1Xg*%PmNW"6毉=4S!''0Vx}w߱ޅc}?Tv0Rʠ?ee`1~/U*<-7bo=>,wю2N`A|^Ha_*gÇq86ӅZWG P-;"HXJGCEv9l ~&'UrKSSeK%lzV2y0/^YQwYBF I0xUYbfؐV2=q3L1W50g&aИ߆Rid1M`h=Oye凕ϵ\g[t)ZymՅU0 [2C=;G=[єNv^Osor Y iڷKs kNs@ԣO1*_A̐J ӤcDX0x+&dDQR)I额d8w Ɣ% 1ouDFQ޾ؽcҌ,V~7$=*vVQ7*48?,C~}u5aPjPvI7HOY1jw=Ni?Y n5D6$\Mq~VNdkXZ[ICQA`zO o˖ﱗug72&! R1#?\Az{xZIX *ffk''NhPze*ٷ"cnv!ca'fSeZLG8rЪVpw.Fice(32-A.|xx'reMH̿?{<>\d*UR"{a\+3V%eҗs~oAY[KAcCWUR$]v cCֿzBBC߫ ׫HMeBژY/xJ3rײ{vfsIoAJ{_Fksū{m> X)W ?_<*ܺl>+nd$#hfC=,6Ymp5xW4`{:pvRm d]o?4%X_XB_F.~ ۔tms":ᗖ7SsіCJ[x >B{"_nr/.*۔ oT;cꅖ]ײ#.0&"1wDkN-mshFČRtV݊l1~_lY:G.T{h`Ŋ'mpS'1ydzQeTX[ǒw`kڎI#a$:KjUu3l(GL3Ρ!Rbς<&֖d)էeTS2BS'+& ZϠ>s@9-7Ͽcϥ †wP"1i2ylq # .l5T$oq<YFIb'x:tZwg"vs6նeCXAdT2ΪtC1>! &Շ` Lz:q|e kf͘dk,Ĭ zR|bb1<,;As//_}^2d?M>fX/o7'PQ?"\rtTW Yɸ>lL W*4 *;bq#pwRA}HL<wr}7$sFN$}mغ};]QZVo| imS^QkĖe>XyXz-NЀ)X0>JG}X:qrc>Xtږצ㑝̘pr9F\b9=e''8AO*Č@OAuO}]à% zu%{E~-P3}mU?9DQE&-ΛdnGI#(I ]u(@ڱ4 >AYEgBێz-r^mVM⊜"8vچ[ՅRnlS?۸ڹJL(n\[ܡCbޖ5ڠwYǒgTb,PǫWH n4zj՜lCl ~$nҝ,tԼnQbU].kpŒ1}/XV}6v4&i@[N'XPFx9<i CV; s@RJFű*{&cTla|QJN/ '7&8C:YK{=\PQBR0;b_wU`rSO D!Șk YL04# "5"Y͒|N9+"meXVk0ghkڊ9zT-(>θ6(Vzp03|6z 2h%fE GP>(g|m>n}a^ hd֘2WՋNhH(?G E*uQܰr􀮠Zߗ"9`un̍ )J g,Tr%ŹjJ]/ԫ(anElŊV %O9JKq}}yK$U|ŸmU*{d+qY*=^<~OYQͼyr7Bs"^1^v֟)I<9v5?U6ykь.O83KvvSas?oO~]|Aɍ 4YGjهX:EK_"vP|@Vd~Cˠ?%O뀝7e#hh J<#IGH<01e!lV<.phJeXOډ{O[ qWi0&t2ZLԂ<Lń9@2e<`TD93`ZL2ZB J6IB ӄLϿ3n. BCrF9NTuCE%9)>D1(q,T2OXvydoRg1!{znø[Y hZP&dA&\@rS8>Fd#g r-|YߵSW./5i?T ֢\J錆n%Q *+EI_ѝ2?Fo.:z7yU=?}p d~tgn ePRYY+=.E_PV~~UmrZѡh?]Х]d_.?caHIqSg<{Y9kqyґՈ/}뮹o0sWSny>bʧW⻍{;Q(YɠOlBXR~fT8>ZXhQDvOK fBȦ:J M̫ܕ ER: ]TNyh9!M$c4N쓋 452YAM"{-mxgXeGy~ЅmndEQ`+BXSILV7Ŷ f;gcLΫI܅C pʧk׉QUILEs[sϪ6y}7%jOU9v^@^Rn&`{D>'V$mYe*R!~o9+F9jߧesun†ҏyrPב9ƾ?S*g4K;Fսvz礢wt?to޻o)*'}l߅?"[/9~v.1L>x*iUlJVQΏ@n_(ODjz2.V#X ɱ|ƟY9alfIs+Dr*LV`7_n!\i+,p02u ,GtCU0$iM}51&Jnltdt` |>bd4fFL|a8*?GĊ[p=3NJ#dp$4pUe=RkDsАwP8ͽA9/<եaIPpᅕ+f.8AyU! q}3 BKm ߌ]12"6GYs E?*%N&QPh̿F9r?y%9:uL8zwLpAS'%ݡ$(++˰Gz/`ͺu#=|ZFPi@gS5+nv$` OW4+R8|(ד\1-:$4:0WmXZTj.]<wu WK-xk, 0&˘uSTK)gRy# gH'6|PL̬'1|HeM::B!C OL[cM GAaQ;{v܉'`U !tS%{>_u.MǏsXqA9+Q[D"% ][MYȫ^|ػ{7eW}WU/7'Ip$i*D9""z(yM4R<+$i8@e؉{ ]΂}SY q5rpE.zX"*x{/<ԱE g?RYu (y*:){a_UEclܚfCJ>-FCu+H #۹GuaDL#V(cS-&E65V󖞓IVc<ΓPcVcq,'@nL%m'2_Jq:?4<|,)%_No@x2;<&ڲ\AkX @"֟{В_'NPRȐ֧4”$Sy@(ՑZPNPNǩf̸f*V!+ox |HnߪJ&PFQie ,AxZ"k;μE'[~kK9!aJn0lHjXMN 2ҋ /D9SLō\pGID mSK),0lx7g }"n ʩvHtieΑ> P9ST$͆/*Ih3b h `C B1 ZIdAȮ0=*j'٘yQFMbXJ+ieNV͑ A(Ah6#4{ݐERvp~xp҇*T84);uo*o8I)#s2ﲆUΘ{3aeg6S$c2Hл(~&$p@i2ӣ0 Xht^`d'aܲUؑSUS [H)yp:;b!i03u+RĠvOw/$SzD O!&O+M V?ohŒ3pC[ic?^jxAV鸥P}`0u{~b^Edʸ5ט0k1}37Ŏ䃁4VMe0v NN /wy,dN7UR֞.ŢE퍸adt!zزɍ/5IBI[$ 0S F;t<˽W{Oj* GoI4St;2IjCSYA،C^yu8z*o&%4w:C  n\(*$UUy6І\};O?3o҉pJ@iG_{X1 j?nۛ9Cw~GCb@:&7z&{0ǤQCQʜ 1s_JDZ?\'w 3s#ޅzfSs])wżbp ؍4s1qsɘ#񤝒&H4T z j[}pr,Mw9gk܂|Y{cMwdK&Hk7\& 1cFÏVOi0 \6uSd"s51dr@&O/ _:dP4(r8a~f6a?s E^a l(b6Dಊ2\x jȁmE<ѯtf`S&嘭׭F刻yM}F&RQ&%'m vmL<9Ktr}_=?1UYl']@7q r)BdU.9^J_8@-A}%qwUz^':ո%ޛWbKi} ^b/%~='V9'; de1}unV,#oN M" S ZkkQֈ(a}DۛJ_Wl Xu^>DŽԾwMxw0Qdb$ћWøasH { k r}24-{1g\9SIH{qC)ٟ *39b+1nĕ^HynS| \яC6&e :59x땯3ӧ\]( J5/ ?W!l_8]kj/{Pr 4)glF9QJL$ 7,vsrM'H"zRbr{ا 8D?d*מlS9ξf3(S*KI.k`~Ȋ*?5_7^A!Hb`U*N҂ܖU"Ԕi ] 'b7IMqլqX9D:bA׺"j R*르/݂L.np"޷f92 n=݋WX{ۿAo3uedgW2-Q(࡫kReQVo#D_~z>eŬkfH#P?iyqrRKZj |*hH[C'(h ?z86U`{b&-b%`*pp,8^oLʧSx]{K}*V}Y s2i/*N8"qU_әŨMq[%SVj[ȑT-Ț}v4(.V,ݏ~nꩋŠ*Vw܄Is'( TWR/ 魔mǑ|pիowߢL/Tz믫~+2)ʪ;MO?`捰X%,; PRw/)1{eQ5.MT>QHӫۿ۠*/YbTLI'Gj|,OYc56S6s q_rےcKKEXQw2O55VK{gQJr'Ag:y7ENy+rYjOUgoSߵ >~pf+aϑNgIqr$~ ʾ[QWg):x/'P6e>TZ%TF\hxL\s SEp+R n|7&+A:JڐQ9\lQb K; wӃy5N2؂(H|d χH79a!3 5Î`/:d(v8[!ܫL27 e@' +`QnRVNh{P}lІV OkBy[m)RVZ3{) P^g/08hpU38?(,ΐuF!8zʊ .<yJ? mFyw c܇y>VVdd典P/bFPp0VoWHtpS`?ۄ@G;kTIxK==`sLof*UV65B1۟N"X]y.d Gj ;q-)d{wPeE,87߭%јvp02xb@S'iD3m5xq@"HN/T*|qAj[p}=)~mmOlѦit;/v/+~߱Kن[ DBץl뿱EAVդz`_N\rRoglܴYX% ^9cPO]'O#IL7LhJ Kd^2ȈxXڟ@wy!g]v*U( ;ppf93w#f:%zJp&1ՕuIRP}Mc@COEW^ҚjB./xqn2r2_d{c=ܟXLȣ)xgIOSڒkV Z b+qJ#;uKI2$jy_ylqdtu5YkCcl;tư##Р#Id7+/և|!0VRFT֏ i)څwi(q<7Ldžw"di@Z6"4pJFFpq :Vo0klDҹ(*HF` G:Ω  K++#An]yG"n]Zce@!n&~00 4]z=ز%%8?V/Ffjʣ)*ٍC|.IE 5\-䥐 1a\`򑌙dP&Ϥϑ BPT!>ڤ7JߥgKr?ҩOPAK;}oʵHyTґTP[Jԋ`]d|Oزp*+jKËoϾ GOew7PXT+cOe9,~`%fi[4b3mI mMCd7@&<`dnM9R,b1hj7%p ZC0tEķ3I!Ph&#BsDNa ɑ8-3`ll)kc=qFMHVz3aB\ wVd!ۃ fޒLb Jic_ӻkmG>vc+pчŠGjoT$🃳;cǭKh De?#JegeL&+Rq<؟Y|͘TP)(,5@{s+aL&C91\7ܰs3cn`ˆ-c0t8e7H*OGW0#iΙ@evҝ.kK՞Yh4! E3тg!3^HV:]l.:Em 7HnB)LK} 1֔u;NH_ 2z0sGArŋ Z%W^ xZRw ü@6؏>v? Cݗstf平"(}W_xWjDMN#j^u>P J1ryŗFqk B_'={V93X*j> ft_w"Qz:)W"wlv5Z['%`%XՅ#m(:Ђm7p~R^ *k)y%:M**2e]]3Ubn>̾jڰ;w eZ1w 'W*WA]x?{o!Y5Ta!ά`Ko?IMB(8LojCɽ+c-ǯ$U 8 ?5az6/H`Bc)h%M}1t_=7eoXtWel+y@u$8;n[$N37)9A~ڋw" `μJ32?(,laH({t)$H 6VUTMe*W0.4nd+lg9aN_ 7.Y)3e!DcRcaKД-'iJ5w1NמW|OOx-(vԐg>-IˀVӟJKo_޲u:e Snð;k`_ψA(jgmS+~ϰf孬Q^V~>5x/D ںҗ!#S"I363oDr +eSO!%|r˗L$)Ĝ6|xo{HHk {V+H޻#ugXKk.t{/2]Jܢ/5 zUV]$IZs^ k,UzE}15E†9IBkU|8(r+--o&^yuel݂e/?ouXՍKN Wqp曵l5Ѧ.VQrFZّ+`N(fš32?yRټ2ퟲ]&f@B"i7Z+<xK9K}sQwa ]OvMlb$lxMK5_i]ajE6l%ݎfiX]LT#xjDfϬoL3rN[o!/^t:쮻vq! &ū۾ yK?匭x~y.jTC6ALN6ԝLF~~1*yr;j aq#a[;A1wB\:%I:3%JDpm>z(K!6cy_߭+3U  ʟnS5/bHLL,|dݩk$Rܧ2 )^਄2^kA?z&4Y&H &(=ݺϛŻ&l;w92SaUVL9%i\baeYS޸./V56Y@I$H[g V'RVo`Mjkȟy=?Z+RNҿ@ܝA)}nS֑㕩<zE~缟dvo-ƬyW޻O[.XH Wlڲ&`Xos6/odu=K , ^nlT{h1}܆^}lhv|dUW޽o7}[Ͻ.LjO8{2c`91S?[ /_+W}m H$2QG5ާo YL ox"z*ŮUz>]!!Ms=}h]mWVU5'jKcK)]:ƅ^/qEd.e*&.֤E}{) $9:(+ۑ / 2-͊wor0 ԓrϑVεԚ2rO՗6VNY?(M%Р_]_B{[*X9$}t'|ZctY>Sz|,YF~KJ(g'{~L]u*}W%H= <)\DlaE"ʾ^CB*Bh!tHPTP:^Fѱl{H#${־Gx/}Z{=_Qև}n eeF* l"N@ͱsu ȪLlfdj vs&xY[BVram/%";wA  }Z`d 6RqcRGOO(#+=9nцzX3'.>A?;/s3 .J쎟8C̣kbPD!UYڻOZA@G7'!عh#+0y $3GFj2gD'3YA7HQ1k%Њ%i`= 8a0|f=+@{Ԣlv% ]!?\iؿg^-8˿kKIMs~xWP 㕏Ϛq./M'2 _ 5(ė}J?_$Ir#q"#݅@yʖ H AۑW V}>8 .Xh9Ff͞Øv123Ʋ=mF˨ejnNB{IMU-s H_pxR);bE漮<^ʨʒcg# mDYn #*v/.ʲn Φd'£@vIt0^i?k#44BỶAdQabdSoNM{zqDF 9<Tj"QÚ$/{l徃w (qTr\,L/kzO֒*$%έ]reM ehց`G,Y%^*nÚU(h’9HJؘx3V~ yYYY,*wtTea&Z;\B,6oی**$16IMN`OW[hםE *מAi[8 Z82WIEG+B6P+K( A!>aLH1KX;1c2%́:ТmmZAiQ>n1}w }%vOF`x5gB1##G(/Oa!`&?#%b_>'Ak[V&U EGuYS*׻k=>=W*jq[0oou[PVv`P$ݓ ӻ7`#ߗ #kK;Xg'Cl5(g}ܻEcX|#6ډc *jD5oyLײ-k'~8#篜xT5/dݺ'TnV1*ە-v$EkmҟI^R 15 SM*Ys=/IOB'|w _BU{!TfۯCy2x_c)X$͊: `333P}M , Qn[޾IHܱn,>:I^XndGf¦^riVL8y;n%6Vm8 J&~wGPH(f_$2 .:QfE6=jIdDž JO =gt l[ ? 7,' B]I"b# qIu]%9_G$'㦑HݍǎaQGp%ꦔǟ=Q'2၈d[$OT 7-ƓeLG Xؓ1]FaY&=ޅ?zr~Uv(R#7o1rH8 7Ŭ^sdu9 $yg[qAfKà^tǎݛAmw%Y3^~Yû]Dϻ =f>Bjc6?mݨT7=]\nfo_'c{|6*ƛT.d߾ZRrT5W^1ir9Ul'W_n[ݾ<_q-MB"#c/Z47*X5jG.7R郤oNgYT~/O6479X' 2o+qX_Ǩ;O)mo)2GBȓ~Kbys#R0'2rчiJ&ĺ)ւz="5 q]()>Ci@/bG{狾iU|9s;K05F1޵};ZjyIs^ 7%-ݺvxowVJ s궑bS{_lx0ꩁ+oQ1q=U0z$nn&}k_7Pۘ0Wׅºo} `ǞCitunCsc=&OƮCX +EM7i)C0J,(Q:c%V.y=8V #C [ă_qleǏ1ٙZ~:0jD0ΚDbg ބx0v |xfV6Fr~}=Stqkm%l6dDDga)z:3A\S"+GJה}O?lۘ͡˪9*e@X~UnZן~,*{1*Y j> h)j9tqr Qcu%Շ[PK֏ysm ~updyz//quI *.E`?nSfoǍ"IJp&=Qcܷ+۴g3`A={OޢX+hǓH IBmbcC8S8~;{8OPa c'4RiI TKKAJ1_ç(>Ws7\5Kfe.Tڮ G 0r/z;X982^vǸc2rv[(Y!/vԴ8CCS‡8qVFsH4Vܳ&-\d㣠kaSnǠШzÒ 4K {xOZ究ăɭ$o Z> 'Y=giSF,Vs'9ğ7` :X嫵*cg3ݝ⚱3Yhb$7Hk MWJ&E}U)I(y~~HeNSx8E+ B(]\,'ai[OǬʸ^HU<ڃ`(%5$#J E1}%IAw Z81A=Aܼ B\vJ[Pͼ%ᥣKYZV_, +MޥfVov0QCӪt)/Vvs 7GCd„fmd-_DRE2, $Rf1L " V mcKx݅6 #LL5.TOu2-#( }n(0FV<}<}Zedsg^3z/YԷ9u*]lfGjo5GC1:781c?D{cWQ>f!a㭊* :-Ti7D9j kVg?̷:Z + wܶ@Ql-c2 qR.y(jCk*Hh5"8|Tա|&h6Q*k:ublςiWbeXe;/$K`[me2TkJۗG3@p|YXˍ9\mY;8FvO<Bfjo|zd (meٿKXYZ%FY/.RKm8 W׍m~7_6vm3s,*IL$RDsHJr_Re:2iڲVٞT5IS-Lbې}жg8 O@#ijG %q&xɳlSXoYI_@ZF|C*tO0e'+\o$f՘l[U{p f'r2c63zBb޾Zc2Qyn%[j; 0%zX=7 k:1u4eςw fj)][6b¢7;Mk89!ʗP)a79kZmiŗ+)5 CC˭ _<!CiI`&ϋvz-  y > ~1޿RXt"u>GJՔ\I޶Ύ֔{ } ~i -XHٰzH@gZ`٬ٔ6ƌ}NѿyX!->4 "q`hDjC!u_\ RҎС{/վpĽP|Sc_\\+1j)|̰2箇O}1җnQq\3hJ]OXB2Z-,`3ywTb.8L ӅK- :_u\dekV]w 2zWW>0}؃[^}9^5QO}UiLp+IO7ͦ}t45)խBqMnrM])ɦnOW7^S YFEE˸&C@.7cwkaL79{ü./!}ȳH$kG 5c|jx?XJmm щ݄Sؾm7lz&60|烿qɂ_Ӄ]& *q䕎 1i9?3sfLz{`xX@ɳb%n}= mZӿUL~Lz.*'TYaxL*x#u[?#42UhvC· :XOrysS vΊ=&j892e*rҟ_嵑KCF!ur`ww)* r9q]~Jysp˲>Prr N ύCfjs9 \QWY Җ#xd׽o9޾4T"2ԃU􀳻h{]-8vZQdAr#Ewm- ^cJFP"Ii?/Œy#S~2ɰD5+`ȎQVٙ~AlܡSgmIMTXu`&eo`Vr[OP>9\=iusdi1P>*'0崛{ \|IR܋XILnW;A;}uT , @!L5_N !a^xk%-]LSϜ(o" Qa{"9jL4ra֍7p^Lr#6=9AaC7V#vFE+PDp%$[#:f2Ti@lz,̌PZ ƒ&U|&WVyʆ\v'HsD7ivmſOHH$ UB&ng< ʑʰu_V}13>յ(<<:‹<98u: >Bɤd`܅)NX"rsҩ KFn1sS~* YO>xsGf&+KgԹ9gmЩc% Lr~Ax[޳o!W2FT`?au)ߡE$2Hb Z(*(9V \p)%fvnՁQ^MI'@dW`عzLf!jZS&҅60L~ -a0wc,w)%kBY Z ܊֯cEd(J Έ__vR GW0+AJimk̾1 RoI4s[mh`6'tu+0Dkmfr;(L:enEYffĜL%X%e%,EkZI#XE?*fMEj&+͚+o*M.`Uy y]y$B{aqmd6Ҟ+2xM G!~$ϣw \|ѪmSլ(mVWLPJ_ {aD_X*0ODK#/z&$xь;bٝTGY`oGtSU˩"'۷gh$y6w,ΤH.U}|Up[?AǂbʩǞɡt)\Gyg>ջ}2ssݴG̽v+eO[.9*@ЬDL~^.5urjQ璘eYd_PI1Zettt![.Cρ#=G(1G3myH۲e nJ( QA(~?\ɼ*073Qv%vST #ҴrKLaұկ[." ĮH{&.(:`E}ыw-i8IƋ$zp{15tEVSOI]X_d9chyNEnj_}K]Z|nd-df'L9#OO8pE"Av'Cf^!:(]O'ӀFyݘ8q^ 3;7_4^hn=ߓ oD9{es83Gg~2ڬ`΍dYl z׺7=0~M ^4Mf'ust 1{$N΢4 Dx4TLpEC9[ػVZ}uHbZj<deqiMg])WD)%,.mC rDQ'nfT1!ǰyA2!P'>Y(qlVU3ʉ)p$L&r 0ʵ/l2tη? B[uC}ĕV9&MCLEHo2<+T#w=A5 }QhO̡`8< /YQ\u+xw]$ܖ,S9LZaDdV<erK/+*׃$ $^r-$:׉4C"RO"+ڕ\SnEe,$jwo/BӾ$nE구_s.ZWR6F (`U6`W^|RVp3q牴xs= ?;]|) :g ڿMPş$(}+025TmX]t _ITiaO(H2aJ=b)?t +I{AeS3̂-'X(Vm#\ pBĉL0l7}O™_@:^TSLdi*dQ){I;F,t6҄e-M:hn˅^7&ƔԈ3RFV7q3B Hkȋ$d>FN5=2D|C)Io ȵ-¨W eǷʛIT^PڐNkc( }fvjE5%cѸӧzcZ;NCqrerB&~iy}^ew-W*]@"NJ<uUHz?reJJ'_{}N'Ýd&V)fJR~N'lFx(nW뇩ؿ?/Y}+tf5ɫYю'&\~!O=YC|kLMMXmᆎ l߽ Yu%O_ ЂQTU6TqCBJ*l)kNPJ cn$b_ÍsKi7R5͵k9АƳJ_վdT ?9Y҂Uv"Ÿɬ <{~LP ?/ɋ^T_= 0M }?2V)R&L: S'?E q\4՚861Y=S4QC$"4_@e/I:l.6~:7Q?d<$|?}oI%~$uTϨ<Aj8Q~$3ƞNFI$^upp MHgy8Y#f}*Qy߀306ǃ?;y(q:9fO?-b-8Jlp^Xu$ F ߉o_]zگ#٥im05d5H>cɴbx:[Zw;Rd }(./X*ttӊKqAL2P݇ ^bøa&O2N4`%8ϋY^BP`";Qr ݰfeV碶O[a"Mg XML)Vԕe̎g|zıZ=ɓ$AoVSIyɝh $Ee 5e oV+WRzA+0DVP9.Yw&**˙Y(it;Ildz?ـ̩AE ̘q> VmWXߧ:lEsY3Bbz{9ߵ1SA 3(6ڲqS#/ j.,s,ٖ .NV3ZQV}_s_Zb+mwhIHL7!S##_CHWZoqIl&MbBY_i-3"z~|v( B77ڙ>d 뫄dyoP^:B,C^y-^8}t,u2^z*#Xo[ sX&ܿa>T iA+ ל9 E>OzrIgE-aY}Z#a@vV$cۋfW6`|jAݦ$ϴ w?SU0ij~\cw-?9ַ+l}! t(|>RiZՑ֊YD7 NdV`XT4GEqF"#&!-=zBrr@PB\Hҹ VSoYUWo{s,. ^ 8B4Ԓ ߃ RegrLY2I 8_Yh)g3]'nMnd $Hq\L84ALIfJ0# 29v²|I*DtWڎ$$%MY([2R 8pAT3n iN4}* ~ !PATՔ 4bЮ69*+ApQS烇2HʃvGM){V]*Gρj &d]>R+ko\лbDZ6+cRVN}pre ={6omei,)2j>.c J廒3+faLqؾybFbؔ(v{ahO,RTqU3{(ƉOZ0$[nƩS's V5'r~X]Bm1>: ')39pJOb5%E)#u=ڃC5-]럴h&W{ǀ؅U .%_BF5ρ&LoHp;" (O`~9rLgp>@R9~HXt;A?)W7h.#AJ_yM a(k~Xg9*/<]u_ʅUl6D]wYRAG!f$VU5z|GU*/=%G z!Jyk^>rzD7Ͳ*Ԏ-/#0}%*b_%]y #\/oo, jm+ZƭkǮ=_bڄ)?je^Y(gcGU6LO?nRm;EO Z2 %DCh(w!<>kL12l5OYIJz\u`KxOm#fGL2@IDAT Qbnb d!UOzB.hg&P;-HG1`Uۮ!Fiw}c/u  zzDs*O]vI!DK: e7^'S%`BՆ &g /I;6ã8?_+j|'ۓQbxC4?W_ R˩v֎dPR}2H5lkmOJL}/B@{HqbU3L!,DM-fL{6}?׼r)gB^m8ꨥUE5Ȥ,HUTQjH;%(p1@g!|`NF`hXkJ?ܳlabX7n|4;>*9){wnWF ,S^Tƺec6Si3L>wn=n{߄ OHcxwp\,H́OHJsR著9W&ͺk~oSCA#`" ߘ%qrf$Tl^Dᯅȩ-)n{1ڰ3Xs3+dz껭tO<#G+՟}ﻋD{dw߭e l8ޙ6s' z6$I~¤z%-p68P3^$ʂRI-C8[_--X3g7ަ/PHKhض>#KJL\ˋrHȤt#[*)2B(.@VF%QI>.8 KHB,i)>_rGzPɊ;OP7U|q$)ݳ~'gV1+eQMBd$H˽bFưX/UfH9+nnB(  &3"!z՗eN;̴N.at ]1&0s߽z9x/mVӜcK,|pEמ0)h~G̨q[w 0g/ `m>sϵ#2jl|C'Ѣe n[Gc5c8Im_΍Çӕ׾QbRth;NU GX٣KJ-e*Ƣ tU3[J%$dz1f"U)fPcqxoh*dR]؛ UuGv㖒 KHray%A^><0b47#(lmYSEe{0nHZ܁'|X"N7ѫ89<;нp0#M cC1vd縆E$K.%eۋ`UZmVsZt=1i~JHxaMw#xRF!&p4Cv9wuAga> ч^# ߡX vR9hk$ڮD3CЭmjgTٍQcG#DkmqGwF q>}A+)imՅ|vu yϧח2Cm_z3/Z  M?sd$&%c$.i)8]WL9!ϦmdH$yU$XDӵ&Kzh*($ k_2 P(''{wӁARק˭yؒHF8 h:V#kC-Y^Γx):dT@)ssن,cًu(Sy2ٮ$RUa!C^Y!,f"I Ǖd^+&cb|(i!)`J!#b߾]%P[cH-A0 rDV(meZ4upӂFqcL|RT7 <Zdv-rELBB3-z}zpwu;\9_Z&sd8?5Vh[ )?D'ʣڙBnm'%Z7nVvoLCh)24q55\X뭿{iʥ hר$wႛ{$ۂ︗ ׉l,l~nX(u4_hhCE. }9+L~OMJm(X "w,<ӏ!&Y&VmR,`ne*J8#A=՞?¤3'P5d"Vơpps 9yP_2Kvʬ3}ҶJ]}+44VeDij{.u |V7| ǏP+O[ NO}*yV+u[fK?t-R&|x)ӀsL}r5`|Y_WվD=K% YFW]쒵<~8׺e{ҤIT *+ש<^% ܷ\j|~. Ț.1}MqX>F2c'Sz~ 屛=y3VXװo?ūlGv%.Oɷ`E ⡇%lq7m8HKh_by7f挷7ϿaOh\XXV[/5w!p+h:WT6 = 9r</k2--FҙTƘ9o>}c9sU@Yfl$襗刉V70&eM\m]T*PzRfaN(f߂S|Y]ϵظ0+$r۶&EoI0Ģ++ 7N6/dQ NNRȵe%XZ28D/g0F?`UV~ v& :)- ¨VC": bߋ#YC&XIkhbo;%|@ ™@2 L{U0nVyO=S8uţsĬ' |&v-r ?D42diZILCݶزhLIGQ0*=ذ}tq|arXV;ӷր$%[݀y31-8ƞݴgsAT[ bwo%xr3_ s̛񿩥&"7lihhĺ5_cbب!_}%[}9{II۟ؕT:RQzìˆ _% ^{fUAVV! X@|ٮ ˆB2a5}=5QOIF)U'P~ iʡS]gϲMA' l{{3OSP+]5ĒOAVO2XIdHLJ,ohRy{z8X(Wc^ Hk#}j0pfd29ǐzNNxLʌmsN&g2aC'Q1<&`Q -o݅i"I9 o葉J{d3 ~E;4#(о$Lě+KO6VY+d BWK8eOyj:I?wP~3lnNs =],aKovZFyYxmFhա9ȋt]gsG9ʄHJmг!CLĤ2*XJipԕU۰gcǺUh+O 7q"FE :֎cg$>fih>rA9{yCDUtz-t)DPOet`9I43 YYٸ}TKNd_2| `K ?l4zRrL1=ٜX+aL!DC*N@4׻k77ؤQLgD}- KKԪK\Y&^mWbdK5x-eb+%=uٽ~#eQI)}r+2ǕwqTk]r <@-cT68x~ܲRtqB}X݄{֩\ CUKKK艶P<"ɄD'~YM9\EKjU=CQ/w)I1eT&Z5VٖXu;^k6_K6~nV |ZCnŴI3pY$*UUopYׯ?DCm-+$X*vSN;bˎLpMb%>}mPMS\9=r8FD % ÕmaW<^T 3CvUh|i++Kiwwގ&% f 9ڞ޸ 0v PfoYX\qW@ӴG c@H2p$,>t Y=x;vdOVLV4O?0I␐P> LT-=nsPFk`h=[8hE%nC嗶[aq/ v+ ,Xi1aATnb`'/sVa+nglb`CTA뛣͂͆0\EoYyDOH{^W|W󇯿طc C7% kj7J,G#yaaH 藏w&¶t-#9bO$MxIHKu$YŬ&$! }'} ?2dB@x A-t_<Գ`]eA=s)߳* n JRᄉDPbcb_g]ǾEOL~2߽ t<^G sqY6E+=pqu0_~Z#DrB,6 'aBT$4(:{`մ2Qd3".^ aD5DAAuB L2) q<.}P7{ !ԿP8 wq%+│7188t˲Jr&214@Ϧ8N?}$Dk2\d}Ɠ/bׅ=%aoo*?{UvO^I%$@{ bAUWUwUUPQT.B'BzHキ78@tgms{眿UE Y]6WUU}܅3A9rX2@+Aru뿵9I~J]MKė$9,"CY=CvFGbRttRr둧5u1Y Htc IK=g{j&#5&PU+捁ڳ|ڬcyMG35*.>'%v<Ρ'^FLf=0O+譔j q6>k-LRGֶqF:JVzHӕ 7KՑ a1Ǒ~$b/#J?1 `WC[j^@<) Q\^bj0cn-OLKJf{r-k]6ih w{ ӅA.lD>_ H@ٜYH B%)uHC(&zcʀɋ^:>*ehHxz󴒹*wN`۾C٫Ek9ݓv|1)~'fJf =D<2Vm!<ܳ:Vg0}b&*X%CV1Ī2WgGX.>R 8| dSy|.T5'y% W>̚5dJNGEX$|nL &Ӊ nή42=|ѳg{Y~#y̢cTu^mn'UOĩwTN?ZҾ۩O\Gln֛Ǘ_|Dr} \/2N~ N@&?N|;yi'?^9 VbKɢ!C)HE/ jV~_3keIbc G)zW-^&&=c@if#ɠ(#QH->BY}MVx!뢏eYd͔-ncAO@Ei'l8E<$%&k}}De/#̾~ CGMF흔o)I@4UVVa9Lƣ3eZˢlV->>ˁDzQIbCnEE G2 QӶn` ȖZ Ͽ!ڻ丣3=я}~zPvє'.$Ȣ5U&GK&L aL#*9I􁽻Ȯ`t:d7șdzJ#ΆdF6a/0"cFzػt ޅ&Z db)IʘcaFf6*Xd%QHg 5ds㳯b[o(଀ԆƦa׾&/w}GU3,69đRF9N{^v^d='QVPggf < k"*߉AYް4(/s2"XۇLL0Ymd`sg1Ӹpb;FJR.7dd_oE'I$JOAUƳ ]=^j+--?6raa(J>mȡgyavLf06'ihFBnh!QG.9|'⮥ H_!CB9ˆ11b?B A橓,_"*m.|3}P9^dZvV< ܴg6]Q').,bH8K@1b?*0 }mۯ/'A`'}(^Ţdp 00>,'YF1t;D2%h%לt8pnk#c&L2Y[zT1 E}kIHdvccPtdx0YyM!c{S)_~r{w}su#+_;,DN3)J7VVYC9b/wGp?,xTyv)hW,0mX9/YlVX}"'4y:@_,θa|Msg-(ےMQcȾao,Ajr>tA,y2+rb%;?iy)*<(^,|(|_}jj~L~ُęh#S^K&igk+Rio_3d QWJbEv|G*@Ztć=ξ|# }U,du$d|[$l0]P̙YesKVFdb˞8n aۑ SSs3i*B #)5Ye>ܴbJ!>% 3'2]DkyY= ".\oɨXGoW?iX̹<pXx{̑ŝ9)>!fȼx$1;vVTt|wNo朘c-Bqash} k?f%4Fڐf_QKp*>&Ms+GQ -NYdmRlJw /N=Kd% o߉=byf? ,f3pPT= &`gQy%=ќ+W4pĬ/U&ٙ]̋qla589 "xzّZqˋ 8 m+.2hdW!m7:9Jٛo 3…s9hDFE"?>TܩR p[8W/P@VW)*19ٮ둾hձߴZUm.<`:g 7#x{s!Ip6,3{غkpb{#͝dZtBdمŠ:2irimv&#RՈ?<ӣϘl[9E͑#T58P{ڠN ASI_-ؑ(<ϝr͆C-V@^ng&N^h<Ms) LÚbץfiEff)͒~>q ,1N=R$J6j+Zfւl9 0dsG;"O0W-GnXؾ(̥!=t_L[QGJAlQZipӆPƉw)#02@joo1glgA6-*(p BX8+;w'5I$؂سE0ÜJk 2pGjv/ pU')#+ ׸xB6V{7vCל[QFBIU+u)INزG>! ކ& 8E'c)HhHN=gĩj;JߋbPHMdX%qUŬg~4͑Iכ7~;͒'O<OGi]+&\fxZr[1yTXN" ==g9܃&M& /$g~VSYJY='px k_{lG u{s7]*2;2C9t$WĒ'M.aaf ,ʀ>@$Isr#ȹ .;0.CTT'ݿҤQny'*ZY>Wj(h$-*:e`*W45yb^ƭ ʂb64Raؑ|}homό>|k7α '2I p6nv܎8^d!yqӍ}h~66nS0iv,A>!6JaG&(&'r_YW7R罔e`rzHBjͧo>5q̒r(#iNUz&0%qLXFUkxT dR107ڬ\N1~.n'("M{sa* o<1t4!!5$*:XSXA-dX^В2iֵ\NY<4$?0upG»/)',MV(YA0߂a_X/$)b}\LbTúA+ʉ8W&dK/Y0zZxq*PdV$7?y7:U;zbKĩ3gƼI|е2*w? STTcu1W=10M~W=9Ov  ^>L yp#O =$wIT郤KEI Yʦk`gBVFC3.KmՕlFG FG  W~-5= L=wC& 2 pNV8y5j,s}v'/1ޡwyeFRO)'r߃1dZV[n-tM?U(y)Xk55Jٕ0G xïPgbdd ;/ 6^~ee kO>Iq}y #L9e*I~ReR&՚x{eWXJJ\!o*?+Wx#hA'6AaJ^l=3Sr׳G3Bz԰xXq&Sp_L\. _˙W4MeI Q_N7S Zkaɸcmx罷GlދגvQM44CxH :ښޕɑ=Z+Fv.Ŝ/19tR߆d԰ ʇKsL9vdd]>hiUN ^oͫ*c{;|G_6UYu;P(KM;}oRmU3R-:`>SmG%i!۔e#Y`yEmq}/5ov"a+Ac]}'1ށܲf;1Z0[HGcc-I>uY*HdچI˞B׫61XaG6 |ϔݸ'W!:^pr7Adim pCM]tV$Ĭ$󣬻љw1&t]JЖtG_إgi[[(IpU;Z5R8@a?#hL_^bA61&v`RZncnRݮSo!\ Fi(ߐQFLJޘw' S)g'|S3yZVmg gd0Jbqr`e?x-\L'][ӊ&d[/ !}&U%cG_7ÓڴiyO0T=Sq2e~ITZM0Lafכ)nr|Ou[wkY uW߾[}\-QA}1ڱ ;""#9 rJP+x_y:-@IDAT휨ZIlCd-r\A^:e\Zז\|OERJ+_0,zdgTVIfi( #ΞH9(W6~E tJ~O,_Bܺ^YYYUGZ$iC(>&Mq91ɾ(~W%Д sc[jG,tY?b%I91|[z} &ϘU{G*זVe7F )b=>O#;Q9i5/O?XZZ:LŒ~N͞Ay,FO2n$p-oބ#ٛ{JEJ (jZa|%3 |^]Qo;iGaoIsq";6o3M|b!ީ;yh@`}W0sՒ 7BNb]ϙAU._}DuKOO l m __5 ux%`^(|p3ӽ%0fى &n!QWOssO>Eϵ|x:Y.&:db塤vfh  0Z a־mJ&XAmw1oغ44 DDV5{wŘqH ZSp܄1L *кh_=l ̌J34E% SyƂzPBfw׵aH_͌*4;#߻NtG}}\'&M)N gǰʍ*F1>u@!Yc'fb<gxٷ,&Y|ӏ [wt<]z/8//V\xu 2ȱ {Y8.L F"5f&Ba&/m3ŝfO 6m&hE_=oezJYЧ,XW) x_陘N` ;3?C߹k }jd?9;}f9S">|2q$S^@,h\B#ȩ/Td{0mb0y"RRREmqRXGaJ6%LkOW[C-}9{,hא M@ LKC (f,p,Ͼ}UNҷ;G-#J0~wٷ,ְ=r%{T dhMIIÏdaW[%3z%g;А";qh6esj7c{A,nr1c T* 7^k^k5'yM;~&( ИmmJږvՇ$'s0ɶ}5~K2\/]˶}w𜼄;oeWx/+UY}?h(;cnTK3(;ATQQīz~G47`,+u(ȜUԒ,K⎜QSeq~~diϘ{K}C+7!-y+n+#A&ZPLq YdR-GQFuTh4AS ZjqBnpܩc(UϢQ3hՕmf16D9et7lZ%׹v-jo:֥zH_ch6B ^|8gmUBh"-1ȘX/.f*9Yd>cH`p?6Y"jd2/2d~a!q6Dy>1|x>BE7؂16o=*ZEVzG)˧r3&P(;'Y!Eh-z]6YSn~kr/ aQ/G1f2ر,(E:lP(t6d!7@;*ְɋp )WOo -. s|lcАz={]X2k<4,53#FQU4FD 9J ބ,ID dm-\/G't^FkU>|wӵY+.|CM-z6ʾCb;[TYKKJY[yo"8yD sT Cd}\oQpwNSK> IȚ JV<ȳ2ɤNt3hLAGP2)TyB\e( fojwFy |;  I hG_'Ke- zp4S~8'0;D. oణILxo$dTPJNHJC4Ρ3JgZMY_;NaR33v Ky[< @H{cwub#4eM`evbDDD̹qlEqE QKYǓHyc&سu8r&ҟΘQl,m!ѯ5,wlja(Qp݄8WNzO ξ 3 v@uqW%v.r3$lSРk`_La2ŁK͉mٺ^8GK(x0)Mހt+0ȚEٸ3'N B ꊊ(Ul$B&B衣MUdeOqt} &sA?;kHHGSrT{-wk)]}'|\OC`ߒJq jN &ׯZ!@7S8>x{Ӭ^G1&99ؠ('CYoi`&Z^[C!b^Iw'6cथ" 7n8sݫcT/?o]E9iX@uSWf{ۧ2Zz:2J#sGKD\ @/O* &|_ ?89Z?;7Il"c O'y`biOvˆIq.>X&c"5Z`@fRCW){/ᾇO3ƍZ[ Jg) $ybIi쯌xm隴ȹJvYP5}׼1N8CP&U+lߺE(?}x9ǮIcךr& A ;$X#o6;b\ɪCFf TsJw!a<qeǍlJKE)V0导~~U|vĝ8k'7C:}Guz<Ǐ `+ VmUOF=2V]&$\@eai@҃.صcY娫)qz_eq b_dO>ul[Zak k4) `QK! 4ZI2u:N؍g?{)wߧm OK+} ڲarFYYr=kStzɲ-AMi&Ɓ,*S=:ʽ\ 8!}t5M_V,`FeGv &_8w j:wݪ(o;>c8j2Мp 47?YYHlH Y56Щ/ކO￳nc4]oHi,~" Y䏔l`)U7ƈjEHi9kk謜ޖh} ggZ$/:/Xe~۶cޗ~c"2Fk1G2P/FÅsfRh9GPCeU5nf~7nn&i2f0)/‚2[+1nL;j }|.115o!pȢlmq^d_Y>3˜חe(*IEnYd@;}.U% o9ni7X*ytWR\,++>T$p^"~t+ǡ]rב.dBZՖv+qC^C2ħ&NC8&W1~M_o:+W8|xa2ߺu!¥_ϛF/_B4=}@?ָ񔵾=|cbY͘KۨJT+g ått 6') Cḻ")_>ODe0ʶ;+Wދ)O'1Gp˕j3gNcDj)܉ף ooAr+PGZZ:}m݁oގG7WՁ9?+Taq_ ɧAˍfޘYw\v Oص#͐VKݷ (/F^[d3̻i==)+O\VTQTX՘d>G,|/eNy>9DZ=|ݤ\Ds:FF[1x4f=|kƒ#C[LsCLl"1Eb$" )Ps׻,׹?[X=u\7,)c߲.7z TT?l[=G{&qM&[*u\FnG=7OHJ@r5J6Zst/zanv9WbMzupA" U9ٗ,:U'>&juS޿2vݙUYY=yG^ 蝿3+ jݺv3+ؚka+%YwW?Y_W-U_1P esu͏ӻͺi(OX5:bv?LZ<4ji߇Iӓ %AbdTb3Xr &Ȇ 9o]Ԭ85[.1`rE,xsP Ȉc'qFsa-q7AY^TQy==ZBS2euϡЋɖnT{S*_A,XD.^I ~`\O?40@iE`EvJ|UƎǕ lrR5dљp[)٘ N? ӃQ]s蓏Ql[IX0XoQ fgn7{i5liBϞ6y,"`t_Z`=RDkdDUWf7Mچ܊MƥX:b"gM`U&ёQb_|^XSv1jH"~]~pI֓Zɏ1f`=]cǏVwWWw峇{PI<Ԗwy؏H{z >0}~ո}Wm$]р0'a͸.K4x.cc0KJ=N\]^g[ѽi_zL܆V6LjU@[aJg]4̂2Lk'3!i>~,rspd1%C vn߆cOAbIVx+ɿ$ :lkYY *+WJ3pyyz=d8|0?Ti=-gmc{*x:[+ Ҽr$'a;γxyWX(_RR),ܲs ڡ$iN ,,`";wvt/9 Hfs,Ońx\>fpB9_[~HB=lٳ 5#S+1,E rO8QWX95 oO__T׷c(jd =<\ٱP/R47M u EƤ0Dx3- ->O+FBV1]0n8 9L+6u?ֳ-%ྕw5wO˜&1sY;-MJ6-6n?l_~pt|-O>eܕyH:X%Iu5L7,tлQ>k Y,}ɜyٰ!+.^w0wzuG|N>  ˔Xke˖w>|E)J w>x>| k<:߿^aQ.;E΁?L}pɧ/G4䜾f-æom)<>,F'jZCM58>('#\WgasDWLDVgAa ?5U5j0ϚGLXUU Wk::L0:* dַ2GPF9ys|)60PӎOF$v< rn'0|x$eAtbB=-Y 81At[+Ku-G>`dEۤ|d!ʂTi&:f1ԔR@6*:^\HDZC?_7Wr1S`FP튥 rpuEG`*{cЮ "‚86w^VY&ڶm+h#`& ɅىX0bI,bmI1Q`jT+"w}KV6!=d"=NO ߽ڌ45/(9GCЪ?&H/S&E#ɏ>)& __JϵZ7^|~,_sfB&gÌN |ded$`3*Q4UNk,ɺ ϴCXt݁M!(T ud[ҏwoүKL*xUB~CK>Q) *ȳ=z#ug=w/MO&z\~%x?%dZqfe }zަpr Ο;E5m( J2\QYz^?'X`tOܻ|%? d$FF2CMI<j1!BX7;9C,ɂ՗EQXkwSCo7=go}0elɶ_ofz֧S1ޛ1[\S(g. pnN(dfSd;줠c[԰_f pR `={*@ ?<(5|a2yZ*1갅.JJH)9a݆D<2OcOMJDڥ| M9.x}\Q~4EmT(uGM-ۂr|(0h;rAeRz"5l\ZK g( ub+Őg^gȗv*4"WXe{{VrvIӮvjs%?iOY-j=޶1qyz7چ<9L2wd=u U;o0UhpU`1lwјx;̿M$$пUGGcj;#Q ) "!]kR;bJ#U.UAǓU=ʲvV8 ccbjd]ɪNOO?O S9w'~gKPPv~Z҃^i =48*LUQ.L eX/mj"!8W_;S;I{yL<sweMO:;o NtssCpW}L.!L&$]KX ½hdh5Xo6et$k+";ahkAR1@d4Pa"lJ<8 qCs Ktx#l:?}e:Rd2 L0a , =f 7dg'J8('xa߀Y?u_Mr×2!J3 ۇ Q#HzTW`i[ye,X`'_>Ho:TO?}Nt8V15ÈEAQx2YQQH'!EUU]O_l8Oody\94kּY%Ksay;49RN^c~yXtQmO> {Ba :z)e-/fܠQQ^vx qA z:7ǪAK}_oiTk  d5'Dו`TT L8F:ڗiwLZyյd|}\?maT[o&gTU>{_kDʟYZm/m+  j\iE)/|G,?{2*Y )jii/>OP}d!ܾ#䡶V2(){b-5d$uĨ)+'[ <$[[3os}k@{6mVaڳo4И(9_}|xXLf$POI+qL~Gƥxq:9D]h'ɘ-? Gczy^6V,sR[Ν?yKYc(2>K\>sA%=1R̦Ϯ>YT=e.j+KJz`2MM)Y}q/2L=^̯|Qlv_c҄iw^{9^YUw**e2\6062"!;"Tߑ OmfL\{_t1'. ӣ?b?vTbYӫ]ÂAyKKf}ISf^0>W}"/0d,BFfd#;r9 bSNd #}4+Vǟ$hSԳVJYQA5DE^ii{?r>ZT ')iEjREvojj1sttm$8<|tPdmypJL:fPbb2Z8owxSs;<]e*+5-_Ƹo;y,9'OG|~?*_Va;LJJɧv}x}KN 'ۖ}e]y#=)oa_ƒ+W*J2wҁU#Ǡ{|E)hm]T5&jN.ޞT3e>/.ϸ#2"W-+ag@s݅ӟ؉Sx>E3\6N~%Qagq1M\V~stnӱR[H_-ɣ3s'UfՀlO*9U )VGU7,cyOM3KV́oV 9[v/.:0;j؂d:s>Z߾!HmbwzGs5c$zfs v\~s+<vV]|dڀ( rˮ*tJox ;Ejóg kIn\V,Ĺyz_ɧ܅x)VQ@ळ'p".p (.$lyˆlf{cuUKIB:|Qda\j0ko !"˘P^Z, 7U)d2e#/23.`9'߃} iGEYN,gDu1xf8s.>~X:)7a&#!9{Ny6һ%}\B62{#:r"NИT.1 gh_G0MS@~,qF<մJ AhԈV>g7Z#k{#Q@Kxmzn)E$jEz[GUNVzpX8%V1.=Y{:+<~IS&cYJ܀2w-JwOur3J^ݗbG \]IMKtZj&=4^6H _@kr LA:,=%^Y!TC2ǗUd0-EMU)*(`oCpZv%[m6J嚆 /o&#@Hm2Prq\S&~ UصAu@9B:<ÔB`K\ּ߬$.`>+D_Yn#p}N[(wPL qT )Q *uY_ S񔠳cp6xu4ZamːE':͜71݆cŬl\HΆ9+JӁl}O M1dzd5"jd@omY3y8X@6HzFH~6>s~B8>QLs`BHAvNyrT~ovu,%e(BP,e ܽPˉHɦ%8w5bfMGom!xԩxg0c"Z0QVwN][/V"-`4$)$@ ޾ؒQЩ.uW{̫{::dyz=CXT*A%PqEȑVݫ]{;t&Xl@G?+h n蚯ɜ:~30/**D!R>0$3-;ݶR8CF՝wc+ ʬl+Vf`OIֽI3^ER؈ɸ6&*MD+JJ1;r8r>Vm2>Jw&@.b(ǂMYdsf- `L7joK^/?*",b]k掎UuE~N`2X`ҙZ⇣~OOH;-VsC?cǜrI/56|'͔dA1'0AC+LVu'PyiOZcC ')w 9Q}{zRH&_ewcB)m津L7E@ \UVuXϘv,:;|+2:2f ˣ7zۛj`G֝)GK0uG V^u;d" KHMlOu?;]c *%1:o~7c0% p (ԹPRD,4oX_ iu]3N!sL7`}}OoV<1}pUe;y*o& 1YlXaDe;HfP6#ZCAX밃 @9ix>UOiwdQF\/Xo1=i*/#|To9zܴ3VyԜ|lpXeM@V2Q/GL}5S[1 㣔LVNӒÜ&Nd;J.&-WeNop9윯t.!qeH{|#8A[ XY:<ؒk߲gr CT'nӳ1s׈Cڙ|ȚEpʒJ@Eu^XTuJ^~eooץ 5Y3wf nЭrqq35ӧ>Îa:҄;#vE`|Y#עi$ZtW%n|NkQDj GN\{w$?po˙knA?N6ay5,xW9ތ8p/cUשּׁl~vg/΅)̬ jLPlmhk)X9ry;(5-n8{#cPȶ͆4BhXҙ= `Ӹc# 5̺hT?'9ӰD; rƨ;GN5I1`t^}86(3&"x]M7FyB?o5}-WTN K,#~ak$5dFT0Df2(V9du!Ott?} VGfމI8jIeUz~ڻo»(>L0TWH{Fe3'/3\GۅȈpw>*$DM2Z } ^Z(ae([=5_dNJOHY9w,8a-g֨ ȤF \^~l_T*@ 3=c VNdBRa5vT'}^."#cQeQ1:i'â ^UTRbJ1S7WUSZ-nЏFWi'ա:.YiϺ'-Ր_1|Oٯ|Ud":v|0o-BMSTҦLFP|T_q3޿-\9#pͺdAKS- 9vZgꥋIf$ofws쵘u- *MH KG'!n6OY`=gS1ǙQQ4!;j̙s:Fm,/eǁ*jiw7R/,,!}1W n~;zcK93YZ`u#HYhw)Q^7 ~7@IDAT\PZ̠qWwʡM!:xzU)T[un KOdl%ta&x_7VNʹuH,\ ޵/1ʲC./[}DzytvU;aZSHq>}]5 h#+gxDDѱA=zj!Ҧqaմnj:][AY[D:ItKFg]dF礠pJ2!Antumٞ…þZUout0"9˯p]`[b7P<hb3bZ2R'^ev9p(FGrj3"0j$L:/Twϔ*Nn+ouPd>3=YZ&fy%m#6U`fM2OHw x*2Ig\ie.s|i'ͤ*zs6YHڲ͔m}4 Ӧ_dԷ?.$U( B#؋rB9zڐJ$.vނտ$_d}H FBΡ:p1{jظa栶11<ҟ7.soHMڝpYYT~+on ?Ϩ,/ |NE4+j/2(ً:70xmaVqc+)915e~'N14̢*@#}VdP`vi )g h/]9VHkLOUWy>^? 7>Q ͬq[k4=<ɲs1xÕp:?kcιBqq zfNqҞ+޿v I&PP6ķ?d R¯q3`i716Os٤Ŝ „oaAG L2='NG=W-9 ;׾dvQ,*kRqͳY{/g~kǜW~sXO iSV3Rߚ̌4z;Q1nw.\B=ҏK.Nf'kSkDB2qLq{H~_J?i먉/y\PwQ9> ̭J,\?DIH_5\;#wr0ZV6Zju|f *[-leP7~ 3lN؈>tG&}x4G֎Վ͞ :_d#3L,p>JÜio'n /AB_6uC}Jq *ȾOy2 xzS“s"" EVQX"-~v|hHB>jqm\su~s(A.?jЃЌK*Py.ȳ.es|2K;z<.7Ϳt^82C%/]J] Ժ=[Rw=ne0I?W#ԇoH$7 7-cGZtN\1ROYp o#E *N.Jf-IrGve,u$h-XO/rp>2VۓzE>q,։TJ|u&]C]Oe?<k9?CWu}NVXqgjw^#՟XGϿR\0ҷ#wcѯڈ~C1;_(Z}H:ڌbdn qie܃$F_W)&[ER z*,yHdVO'3Z1:ڃ:[ɏP5z?fc x & )xaGQN,x.M0%- :+HYb\\3u ?Y7`O![d4SO>?GC.oFk|7|}ZHBXɬe'L]°0aTo  ش[db_}@:HE2&?sCS0SC2mWG`__xqI\1,YH Ǧ{i0BD ,q)%'ө?nЗȚCg7`KY×q#_`(±.'c~Zn5բ t RI3p' j?/S( 9N$()%n n[P^\H*@ҞMH9@M ޚ@TH x/:ͤb -)|ԹʫԪ}_ުu8XYt/8s{|v".U-{:oE[f&&ņ;џ m/pXbB>Z`ڊZYwa/rO:^Iw=l5C3a!~{9) aaec6>0 \^KZ<:f.~>ܼ6qoGP:jc kQn:&\ORf_$lA8QYR #NעR}| y8P^Gq6'9jxq!ɀ@Ҽ׹Wm~62[:_ JQv*j~ݼba_`)~AbU 8&K()'1|؈IhN X_J!3*)3m0W9$t12-dE}j-*Hy#"=,dvkLXo.lj ŎB^YwVm/9a_AARw3^H(:TٴVJݿ6Ҕ3E9htG]l)v C؂t|W 2rا {[ZW*ܽ[54ҧNm5Pc29=8AvL ?#f9cAkfD@P ҳgtz=oM](~(Y/ jn@~ b=x{H<سgkk/?3nfl!: zllm`BT3~e9~ w:C \nVs:IwLN9MAVrXV""T,wކ~XCq:9eɸifݟ}'wEm&}֑9}׼:_(r՗'vK%jthj[Mɜg_,z+0g$\Ӭ 5J5ûYWv(>6b; ƕW۵R|6e+;:,CC)7ѯo[f!=$n>IL։D2m?-]WQP`}S?awlܛ/ϼT]5E"x,;ʡh~N?wȺUh."Kq9;ɔ`pdW+rz 59\_[O~8E 54ԇ\$(x Z+ٻVbtQ,v@L2x) 8I\*`$U9HŠ*4 CG[2,Ãa^\q24?gC1%ؙ֓>"Bof2p*vUؙX/,GG6n<)ق0i a`1]o*σYZQC6Ld&C#9mnE| P|j3<7̸R$ڀ#NFUmRV)o3o 'nU7uz!EGkg,L|..?myW?m];iӽ'݇m^_݋<7ԼxB-YX\DGᖙR|<..QC*-Б(|;A~KDPZH$P *Ƈ-2\D<2[[zJ\OU,VFE@V3 .1$ M}-Fa0pvfF`WԊZn^ްdւlZ]8SB~;)ZrnrwlŤebm B-bȩ)@vmtQVL*)4ܮz:zٜ83;7g-ɥ qtz?p7iFj55Jlpt CQfčh8a#ӓmڔ ݻ ǩƨ~Yq5/6"Teh61nZJm">}P|llZ Ԇ@BcBe F7/)jlN`"j=C*?$P֬ l:A#GL K38"lhKO]ZL0 ۗ`+0*ٙNf (;Ra\Ce~0w H%6oރ&cwV+)ݲml7QxH1s:n:O+sCfGe Έ/)#YHwB=Q)$y|?F1ZJ:Y"d瞽Xniq畫I 遜tnN% cP+ޖ1WR~ݔ X2;#^heɽ(ї'Hь̄˼/F}}Wσ!GHFy0㩡:f((O(W2lmm{%.*ǖ_γ|uB`bةPۚ3멃A;ei:(3/yw3w9~'F6JXJ f\v9NDi2N28;9(6D9V;„Ç 11OS]|OuP٤@-?z9g5ֆTm:_h豘Et䙿22VSAa.G O? ό8FʎKlڰIS9TxA?U9 #GƬYvWq#* 2)I]%w03i[I,ߋcXq+vyS:3Z2ea73f99{2k3N ,t+Iijn'Uھ}d-`fדƊq `ҝץxFi$0![K/KCŏjk~5䝠&dF^}~;i]4'k}21`BŷcP)hɆZP7:WD((gFg\ld:I{9lH,|Y<4{6,,p3D>^{' ǓΆ3̥㒔nKYp o.Ya-,N޸!.AtA?`fk;cS]qA6>yX,Ggftu1K6vn1~f^32`ZH,f /*߫ݶYX(#ܳw?bGULU?| N"z@=j-ÒX|~=q*$U&LyNP[F-ĎX'xߥl*ϡ[U28VRKרHzxHpNfu{WPd:δ[pp3~l [J|f yS NJo)+.mXh%Udcذ8^U\OHئQ>/͑s$yō޼smHmx(|']w pV¬%KRFsN=Sk+1ٌ}C(s8r;M[,Q/XW}O@1և ŧe1a5q!J|_ G'>@K:j[⽹o1̆}A"L:Y^&)g3R+p4QJ*:O=K {h!Z,)"h>‚H+6$`Dl(_:QTRn\W<*9TD(;W23Аݳ\\PdfNIoFFfm)DG>{u#鈑écR3!/T9 -'4cֆ;6Y%)8Q#*9;W<$u/ɜbT;2ӯ,ŔP}ܧצ zf3/>4o'9v><)5uפyHtU-a#1yӊ߳yڊ,_?jзf_uL*61Y`KacD]rM#evl;>ed8ONӲ M}"s嚊4wv>WӲY)~@$C=: SoN,[|&(6 eى֧ʥI;J;z˾ATX"9'-0GJ^5>{l߇;FI ^wgy' t̸ M|D~9YEd #fX6w&kĤf!vp{ڻBsTOh(tϓXy?#gܦgM+~e+qŎ~`xWDI3Ø0z-.gdI:[fD3ӊUʬg>NqFNqbO=iPrVi_:YboR:=\;#&a/f\<$ 7R^Icou"oС2d {Cyz~<\n37xu_h~K)˒7r4H.t4ԩ=LjfnZx?J諏^s4t62×/73/7APB֕?YKmF ~%ԩϪ?o㼇f P+L+Y]>tٺ1o]ymfX$&N- hji;rٖ7vdoA uu1RM5 xxi~uo_XH{&eHM<r6K YoR֝ ;Io쥴!hs n~i'S7~ }' ~%È~ X̘Ilz.?!t%ؒ75\ TZ+NeaѢŤKm2R w"އԂ#=`f W{KjR$muw?}k9nڄm&Zy2>/ BP\]Q#fBUdn(4pϟ yy)sGtI|Nm&ٷ]r9|Vg Q"X#`E(݆rw*ct&뀘FS[ %uJ89c`4εdc;JJZr^I=SVcꖯZ ?v"8q Eup_Ufa|Vf & O͞}-C)} G>R]l\{O_`Ҹ!xG8?NxG4VߵWE،A[+6ԓ{fi2᮫ltɜsO>|͟N$DU \S0ሊ YfR(sxS;_k,f , sS^ڹ%;O?LelKaA[ȣȀpĀQӶv)>T \뼏W*n234 nbw%.Ҡ;= 2#A9_e 'Ǫ[$ _*=RڋAª4~`ڈ8$uerjYðIʩ01K΍wchtv'gV6 CZqԒ~^8~.4)o2L%!ff}^zŘt4䑒|-Xg]bp@EzvdE-\|-28H0Y7G;f9N4#[:̚oXC,hc )?[ԅ.m&%#zLCeq&G֕55𙐠 S^il+ 7y;cH74;0އ,S`Xd;C "c;mk6Au`>%,Y  ㍼C)0ep;X`jjk>d2u'ʶ?[QY0UeKJsl?ZIb7WdҴ3@%U_ u+nto9|5uO|i.}oplw:aEMKgBCc0CAw,WĨR1vgǗ.ĺRu-ȧ^%9uWMgwcI!qu*|A(f@2im=7٧ ~9$Zq7ds6?h' ,N#CfO/hUmF=KPe\u+Fj<KBL*dƝy`]w8e$z.+%v謼zNlBâF1@u _>Cdd~cmV&mk@cd #Fvݓ@:ֆFg3#,F䬉HjŦBNZnђ{Y((m5_G dZ0V|DUL%&`5 $]\+#۩1[PıK`Ȟ^>~\{wGu$+/ 5$o_;0NXQGgJAxLFd Z,^sjOjIï)ǁΈneևw @Cdb4ytHR91UXx)Dʎw`` JJJp{la;7͚Cw2NrQL|80`vG3>p7mwފ 9ݞb? ZVgDqlh*VndFMژ6շ'29P_B˸MD 8T?r 6edY]9\N=/ml+ |䣊L庿%X^]CΕuw~nO>DMM>q+~Pcc1zf"G$,*~I)Neį f̶db3#-nJ8V̙tM'= ujE$>]Q^؞=;ʫ\P{Iݟ|4)3Τw|Ckk+ 0O̴̾\ x1}1{*%x#}QC4@VXaӾSo\#fP ^`MJmA'&fY`5t"%z<~351㗼ܼV9L^/XUU2X *m1rs)h|qLFcbK>m{`XΣO>eVV$ލVa؝TX6vX;4#ꞝX?#ܾG,oYO]iػg7r3m}V$ki|%?jje9w`. ":VrNWxzι. e*4p`,OxZfrJyy8k^.D㱩9gq~̚:}AkdS^@Cs#"K罇7l~-9>fƾuG̔^AdY8!G,&2f^2)=cTuW 0ZgwP/OaO*/Z\:\m}RK;̞96{iDm+莗W,Y'}"%h2|(n/^ybuAKI؄h"˾D˱6M;r36n^[Vcī&-}]JD5{+RRPʌ#ɜPپj)vH@1 t;̎|V$3aPD%ƦjX3@:̖lRfrM&Pl}ks^s-JmQ_]E)f[aQXswdȁw ˾Be 5=v~x㖻;=#ޠhF (:Bugd!.7zs۱n(~bFz 3aZ :\%x٠a˱}z[0aMl%& *l`5(i¦1nXWj9w6ÞVkr/ܑlѣ {}AΩisv89qU+ǟ")1oPMFA w:!'Ћy;f_PWG=XtmUU1[0;?X!ȁgY×7 Gvdׁ &a8^nz%Vs^.e-8qO;Ind\JNOaGCM-13WqDzIt/˸.9/M @*}0%U6w?-?{at@-,6vaD`&H8o0X{LVmȬAL? * : @7@  CEE~ 2C36 1p`X ?2!ԝf3[O{{tfɆsijZdž %1ZJ 3Np}k6dzX# r>,E(F'{#)KE`{ȔWVZ`YԽZ]g|WQ-9{?c dkS#qZ6vxsU7zՆ6A/"AeL>>Vhzu`-mb1Ф*btLbhj {}oeȱLSz\,9Y"5^YQs`:mk^SB x3YQ(rFEwJa_Zj7TN'aw`;+O14K3/afXךZhx# ܃OX=v1d6?߸WaB{+:25-9 ]3$S.`L{p)Ŝ0f k͖kVS}K? aGRvն 6 J1go*23rgKuEKOGJ?y"~E37y XuAVg4;WnFr9>܏ö nߠm{vlcȸપR7cGY㭴ϳ54R>X)iT-зv)*ͨ@O~}Ͽ% Ǭ?B7xcnǖA+p%*l @IDATܳ 6uR|~j˒/Lj1~fc[LW)Yf2a7- T%T㤯"6teΛ\S})oRO`_}K)꘢yS1ǧ1;:q"\q34]s뮜<}毢dO?Klgjnd?2U^غc?<$Ó|J8tLZ7Aqdɢ~\JO>]gg8[ vNwKt9̠2Tcw+cOϯH\KغfّHe n~i Lu3n$ d^4a̾="cSV'OGeV6I"Hlkoq3bG%Eکv8a:FVN8S kFG7`Z9'u~WyWs=Un@%i7 2h, kRuQM"DiY%8{F0f^&LD 8 wA*J;X/|3bQҢ!5k m2atvR0"_yEldO9L`:R v6yWaΫox c; <38LC땬V*YZ;>/0bC)jrye@c˗-~j=~X8am p2 i(}^O o=jj({o53tߖ5D 'w%~XL&}}Y[D 6tU~pt%3 K82 !)Bl^gH@:FZ6(V$vщTlXYU4OC㺙7z:Dq)Vk ;xhjlcʙA@3mg{g%Pj:N˩-y6\njk)8fN CXҡZ@mKK;s2n}2FaPr9a uPٯ\.*=Vu)Gu*8F;Ál+M&/ CGqe9H\{4, GnET7jmA GG7Ʈ&6)2Ls=K3 Hʜw(s'Y%vF&vG`9k88tt(pscnYvd"V[mn6yub >76-b e^]CN~/*z!O<|nо}5:?XΌAΆT> *c;oj&0 u;\Wd¼f62ep,*}뉒fU[EV _{V,Z@[M?\^B]p2G"eyjHm{g͠t_L&1{bi܇+GQZgO:~LF d {f\koE?'kr%N:_֬FdOD \W1(5X+.*!*&#L2\ l{Ocz-)5I'31x`u`ݽ ZS>^pQ3V?뻟ǙAFJa&%<S8ڏe}xX?֖uK!coʼns fJhmm87ީl&}<%Ҧ >̋~V9߇N9ҧJ{o툻;PW; gX{;|^'Di+ CSNlzJv~~C܋C=g9_@+ek$E WKHxJ/cڱDi^y77Iw=o^ۮy Rd?r-,&+gނG}@;{ p6%0Cg'OjZ1#54ȸd]¹_y|FA4Np_x%tT,R}l# qN>hM[ہ+1AT ^ƱG`$T,N#+{Z 0ge5 9)Oaګ Cho';PIٰgo+~2DOSb s[-b+w^ @ws/'~;H9~\Bx`}4Bu[GwPoMǭ1n{lLA]oՁU) Q^yi1eyJKË/, 5QdG(z$DjJ*#1o-_88E0PlXQjU#Phe} ѐ8e[cQh1Ltۑĸ LBҀh즆MQGwo]tr,eFgy^v6Ֆu#SW0Q);܊uBj&Fv $A#3{B5'̄  ox>DЯWY[ X1C3X22=㨣 i)!d=:*ĐaA5ێqC yCF[Y 62)zG+mK3:騑Rw$Yg0L،p{;f]e=E7p!NfkX!ÙFWxl8V)Y1H#NzFOD+'D'1ڈ6f&rR'MFGљFmu'YFi FPmk-T#L|(Ga:A뮻 o3ꮎ 58!"P&tjiiWhU.deF:3SQ@J:n?3zww#F,PKBK}ڙәNuBK HqZ$bDHp:y?{Z頑O w>\TBԓ&Αu[,qf5qh 0woM-d"u)rX ܳGƸyΚ\'&{+HNhtY/ft+k!bFj,+QkA38x + A=iLkb3UsMt[ `M%w˨Ĭi啶gs/, #Fu2]xbj{v禩DkT[YۈB?g+a;I Jz2Mϼ2v*$)+N'\0\n@Vi.d1τaox2N)'0v+0gJBgLa2bkϿ9'2"lړudCQI~YbdbM})[P5dH:#ML(aӉ) [@\tnR TU{=/E:I?V́Q#47V]gGX<E!:6`?~<3sUE+"A^ ' $92&@̄  VdRJ[ @3)mK H\ ?.;.|t=-WHf+]6I@e}C+Lg2% g `r^|KM_XlT KƗgR^Uϧ<},f9 :/~*8p4>6UwXO]Nj`^`rLvw_7} F{VŬ*ͧˤG ɳТ՚{1Md.'%w dU"J[M]V~;-ց_# LQ]JH&:`HAbLZ[7%uMG+$Oʶߙtgr?E:dDREC7 }3 O>qc+H YHَi?L N[3&^3Q.,j 0[ YyDo -7RҲ] & a)%l[Lg XK -mU\qxo=S S ],`up8z& W/2 ???Cckj:::dpp!-=֌#fbOŗ_S' "#6lҒӑ{Rޟ>PoeB|c&(|Ldp"ux^_gOU~E3G)j ;U~ݮ~].plU.??{ *hٷ~UJ1$d@6u ??n2ʘMuT@$̚<1t\v1OMA<.zJ$*$<:u LcJOG~Ex' j3^EyԜw 0y?uFrj"Dž[ﵾ6$ ơ>ʠ#w>x;v+&[XTB{"tA詅ZI>W~u#ȏT;K0Ԙ:'RC0K}j #`5`F*FG'71nk檾LF)鬺Ӥ^GVdpBH:vDmlP_8}O Z3KX^= ү5Rʘyf<`Q\7^W#A'D >ؿz!;7/Ncb *cׂ"߁714~LOUo> ޝ@&ЮHDIGD;Oo80XxJp%rqC {1vh_6 AhH}=[^N,fsk٤^kұjU*E-2,m;%(bV0m!H{YԒ|ʸJ[z֔]j~(%\m+i>_bJL0Ǖ䞇碀 r@mi,gݥT^nԶg5 :M`=|t/,ߢTO7Ro_apJ 25^'dumjzV%Ns}Vƛ z`>nSKQ }:||;pյUp&Tu#Z=;uS;z ]^ܐXؔTw1)okQqw45};bTA6&?g.i[2C%I`Bk=wܭ )wG2 ޴ˊkX`ff~IU$DICX}-R0aUL<ȖdďUlޘdy/Ɉ$j7sўٵJU]VO#}E*ُf z 3"iboʶS_~*NѬ<?oZ였/w[)SgʸJq8 #FހS+cn}'w$VRm@YE5|vh Wj6#ҏ֔r0rS b 0 x pB,zagFHMAlڴ~QQг @S-LQEI5r?~YV˪T[#h) @Q=p@hZ702pRcV̘IfL2qQl*bUmu‚eNͤ5V[&#\9XEFNZ2%Z &T`D|Ȥe 8yr '= (1z%:N5~%^j;<X~8}W;:s)h iI $dpvC'˼Y]dlbdjSt7 K09o1ޚ̌)=sѹ >trMGj.B]]1k#Qa|6'G2q`C%)m f4jVu\ȶQ/ eW|u]=HDU#1TL0檑$:󒹬j-ײrM5|dڱ{_isX" ?v2V< W3+x(bH³ګ1$Q5gAh.Yxnިa̡1bŎ[utH;;("gtm[g54~_`OVeY3 B3p&*a6+jG.Can)~msLُ]0Ê$FX)\H3:8v}{ޅW_{z/Dcf3+/ O& "eN:{FF Ym/V|{ׇAx׮)6 nFlVѓ6:BGAl۸|Kdz[_ٳ[ f:Vʭlw 0!rOYѳx>^0kTIeU i4wZJO ǁ=߆e_$B}OdNO7O>D;ՉiJz3)eL1tLQ x8*A:~CԞŚիI!q#չ)XmguN;| 7vg_~$j„ٳfC@/TFRLoض:8}ݖ(HOCZ9Ekꕸۻo޽}B@+gaޖt%%:rMb|c@1 _7<`a}} ަ3p?c8U3.ܓU4 6wf[ZY Øc^硬ZΉ| ׁ`APXumX?XH#;4*󭹱^I 'K˥VY܉'8yEgJ.O3i%_0ňPagO'/{/ " h7T}f='UYUq|?6mPlL$&jI">:^oU&?v cH=;Y鵏QyRRN?^wˊ^ܮSր]H&O'Ig"YkDPnP&UPkt۞OL"Tϱ u5d1BᙓJ0ӥaiU{a7kŴ 2Bغ0Uy&/1\α؄un?~ ROP1FҽU=<'Jra!l;g0rZݢjZCDf&ذ {O&aƍhml 5hOc&NO Iba40:(dv-܆Es#=6'WVK*a&mPR#'N"ɻBSo̲n{ F&DLK>xj(E*tz&nt`LG[/Y[tF;=X#2 ̤~Z KVvnxjLpf0nmMx̊ze r.oAT1H_>a&8 2M,+2oXEw[i2-Cgw3-jُz|}h Hg0]̘Ϛ6*۔>:@R,E%ITҜ/u>\UVogXmΞ%`h #2K|Jկܶyadl&8i/$#6[P׸:&FSkIrX8z~?2'?"lMʉL%A qw"{/;ˎ "!^<L1ښd&V (q{N4JaF?⇅Hm^F@GsS#]& \&|>]\K 뎞z<˗DKS.&z}bdV Yw/XUc k3,xFX^~}Q\VܬX- {eV64ymm~VJ:˨.& 4III:A.ZwށMyoz~z˿XH5[i@`HzrlYupF=~EL:v27߯XǪHH î㍀|me )]+l7y e`ϙC6ju+eqxxIJ(P>a~zX6}G^N>[hu|k/aQ}&=(ՁA[5K1| I@&\(aB@ȱ37EX;nymj{nG$+~+Um&PWXdS0fxԹ&= ߓ &?4#L3=W$u+XA&y0# R᧯@fF6|]wsh6iVRo7=+~tɇxU5; w&k鯪}m|^FlX{jrm8_>xoA[6-52oh}0PM}[93p6m\+OiO}CecX9zDWuʤݏa[i#( C&P9%#'rb&mâ`CˢƖr#sR;չ.i! Q+Hލ2͌QLynA;i9w&"U׻dÚZؑ=ߏr Kf*-.Yͱ?*DV6 ލA>H=9{6K.>J {Ixvx ȟ_+y=BM?]oIf y&$4@>+$Tt!BJhooעZ&Az{&Ӧ18xd O-tR *ؕUtyj&Xf`gawC+c ڛð,:)1!Xd5:=aS~% Ka嚍ʼ"Ue-eFN%SO?߹o x:1^nm {vRV&[>oo**AC-~`O]]J}׎֗d={h|tiUc^M6q@?52˘BRHNNƤIT(w_g> <:*1 f?DI8LlA 2_;|Æ(_0~^Y3K`2?J&n[-g߮,:L"9B!Sڎ3|tĬ{0OMx}63--Cyvz1ҵ_ {:ms{]e'j_ISmJ֓e}$ەj#]h/۱ϰjois5^W࿢҇_t*Ÿ/OݛsmuXFPتHM$T[2z3ugC}R N(g3+y 椏6y(f̝#0M\ K?+oTq6A!lOҽ Mw1\2#B_Ȍ Je\Oio jtʪLNphCavTuA:="j<<ߣnU2$3G%UhQo@zS=VV7vbU+:YvH6 I%iO* SaKʎHwdhfjDCm x7î# "& %A54A1AfQ3l_Ȋ2Xl.:ɠΜ܉>a2~+O[ RՒ*gO#Cj`g >NjUСp42ŧ[p>Iy0v.+C$)R人3sv++S#HL]wFpWw[f<4;&LA4@U)|l}}O( ŲW8D3+f3f?Vߵ3qCp4IaˊJIXG!gY *\kJ%MOBk3)j]0[֮& _zCP Ɍ8̪!C7hL-LIUVbsk 0#}gU;\"Z2m{= JJvbCT <:KX08eV/:M覲B8ZQG@(ǛfD'ē#T!ܷv۽n2t |yuR1 _.\3v[YǙ!zm߾}@J؞~j?)W^Ė @IDAT4b KOo4c'LRٿ>d>#XϬs g_X6Jwc8;`f8W 3XSͫ2#RwU.̈H˞3M*؋Ƥ?dcH=sŸ FGzL 4cK~Zj AuQҗJ֚o粡G&BflK*˵G"_G>J'oOq3 ۛ?8L#H#wÓN!faeNJ&f?D)u>.=]J\a3025U:)S>M(9O-hi x;_ 2**N"0 '[Č!^0p+NcQbP^Z9P}G[R?@/* Ek_|:e~])UBJkwnʘb3]S*E9IXێr\J+##]f=MeE>Fo'Ǖu^H?WW!>r~k?w54GjivCR8Ҫ0 1Q杞~ZT;{[H(KA]:i'M²kD-7`0&VɵU8r,&Oc+i/K0}܆JE{OGn3g+{-sPǑUڹrRFټH\<gw0ZOX~#yjURfL=q5M&Pl& d*뵹5(;B~{Cl?жz[O{%O[Uwѵ+-6ܗ=Z]Tϥϑ$w]&ϒT)J%NWqְJߵߓp }1 NFɴ"=t lfݩt2H}ݎ;7p+6e*3&$\>h0É$3侧IICJkc$ck!\/>BYtQ3v%0(t:m#,j#;}|Rzh* rĠ.OEaLm꾍1){kʱldBFa#|h1f6ڄӓN!` Ac0h$S+*Dy ǎcMP|%j`sV@m}הUK7\4U7H ;ay/ ʄY>zwaxx`N&v&7_ i36!\>*崽s2BJJҘ>, 6C16Rt4ԲZAVV%e-E# R^2MCA@L蕝J mE:b  D-]8CR"mOFsB(ǹ4 ?N=vT#jXb! +ٍwpeea~|ގ7d3xwk}t/XebVH~}]Ncm۩7qd"<| ; [UW7 c;?`WQӚ0%b|n 3cz> I::HAڰu=̳Wߺ8]RX҃KXEeczXe$}p!Xy+}Ss KcjVڱ@C~s'+|T`-b_XW΃`&T6V!}{aa;u-AxSi2OVjPXOxp<| 8 \nJ&RzדNY&+geBGkge4ͬru *3e_MwtVQ<<1ƛ. Aʲ"_ s3|2.fgL,ԫ7U@uT')Ah l̨7 BÏris؏Ó6cuǖ}_ۊǓH~u^OOȞabZUUK)@S/']yZ1H{/8:Jb{pWVٯdKddRoio"g̃?ACO%)ڳixYpt .M7иX|9)+0m 3QK@.d@^X{xֶ!z5`k'Ծף%!.<(N2hu*AQ5Cu %‡Iz-\ `~^P r6.iE&)pv;%3hwHٛX),.<4{>v̝, D[KR]KV>V_ >X`؀rRwߖ_d `uUWV)"oO6.t٧תJv$'>ef]৑q(DYO^* l״J40ktGUEhcքm8pҎWox^6Ќ){jUkt.e:Vuu+q^|ϖ$F9Sx;pS"0֮"J>f;K#C3L@.Ǔ:f) OΤw!vDi 32-$l^Oz|َ(ۏɊjDTR)*I{w۶/I Sr$,Z8ծdb4==dNmͤ1dWBIa6̘fhnTʘ+IhlPR&e0k[2QjIyhhk1,Zw&B|OD_ X4ay o_==%"K9*őv XnDz29GlBij߀kfHKr= $ӕG˾U-{wT$%ϻ܏ɦ>'r }.RI\4X6y4f9'aOZr=5b/jg vnqӞ?5?uI RX(Sh:)Jv樂yqN!UߚshW:s3P N8y* m[`na3FF%𫨧fh)B7PO ttCʼt6P/6c2xtf9hgHFpۍ_.Ƅ;e=h9R78 =;/}AWgl<:UI:tš}hf7m( ]؇Y^ȥK=̚BЙNC[aon}dm [0yGb )vȧZGZ8Q2knGs/"ɷ`Lgݻ?j ȥA2 ieFȇ1e(c0V0 BQm;/;ҲlmYE, aj@A~j!H'0a!W`ox:KR7z6`B/fU gRU%a˚6C 7? KPxG$6: g,S–ut *1{p dTd݌m z-< oO:[`g:sd/ٕԅ5v\kMX1iӦzNI0d*_X=bC oq`O#WO] 7WGH&xsz G Li3gP\2h2yt 1B,Hm}ABcF=3+(t8|rZefB1 6Ѐʲ,Q@e1b<}mlv?C1zڇV*R Z _ M2ߥJnSr/}/J4ڻ 7'UqQd:Z~o}_xU Ye]]Atc dlY0,z& HA?~5nN3q~5;9WC۔ vZ'W穟S< Ȩ:IGwd6xi|ȊaʏzꚰQzbǖn7 Sϡ\7o\8Jַ8r߼ 2iTPJ@VKsBнv:ixՄAdV]VP^?a_oU4+:ťWڰ ).f\0x'8ƻ+ *I/ޥְFi*B*첟TS3S̺2{6v\g͎FS uH|$=aZd:x*།$ g%Z-3仅az=~~K<=λ|ǝOpi3l1|ꃝ ]~iyZJ.?n "da؟ROBMev`M._3_"^HL4Umm0`LNzc]@&U+JףzNqjIL IPO S۟S/(r1!`|ZW֪Sء1`@J(2o fkk5ΥOtj*H(Bʴ@&X7nÏ*\X@ՖS7Z 4$&,c⍷)T4deF݈2aAOMNI#c#pjVٯ}ڱa DD?TW2U\jh'TPN:9y]hXg߼x9ཻg#;`N.8pF{=%R]JS.45u_ʬ.o\wIիXedJ/q`[שׁki1rg|,q9oij򔜃&NefN;aH's_cC=_,嗘;4a2 vۏo.3mM >>g|Ev Z0}hm8Bݽiae8d|_XI<{_MnTˆu=]Su76< Lo}*0rI;Xz1]}y Qm]H)|kӽyQlA1<YUc2iw* = RpCuŧtªC[X_Qʚt~H11$K:RdVՒ:0 tIwD[chjFQX] :wF6;T|63J#>JzFtw HE[D&Kޣ´ػkĢezKTMQu[&rEQ?|&3vRIOr~eTVSs;j2ȨI 2Ԉ140f%MdT(rq~<%~V¬ V,Eȑ\tt SNA+`;^F帝uЇ:z65j鋃uh#)Jk*0,:gk 74 y>֒\QO/o*I/̰g%ek>GGw:o0u%.g _V)&]G*9 PU9&OzR}YQ ۷>LQsg+V{:ef31'bѢoҿ"EU@Jv)Rg4th ӕPRt'wSnGpr#)x!sNt虱 6֬>5m-v inضz}ӄk6cLL$HO@N2-/.HexO:Vv܂s4'0ظXu"0~psI`giB+4gD-KYLn-oU} X1+ aSYD>|Ktd"6ix{-0i(:HfFJ߻ƽ1([􃁾BÕGÍHujy}sc>{'Z$qYEفn1%%8͞)u%9{(l̘̤:lyOD:)|gOqb;G~%K \I%[Ex.Lp;*ӟ~Nkya6u-emEs`XBH7tp4,<]8}" y9y6Bpƛ& נJ-p J J1e 'cͪ'F("Q fq:} h ORqvaf^;Fea |\Y/DY;G=;qO/+YQNA0|,t2A x{Y u~R-y%kz}劔S0f2W A70;;)Ab56&fw~`/PӇdK[†>l1ڷ &6&| e*^(3QZ$>Ҩ]K ]cRJZԺ꼍?FA^.&2YLh3/ni2:Gɉi8`gӾ9ᱣHK{W ET̝wk&*63ӫ˺gHXMƖW0kܤUVm~/>AqӝG @L^my>mhm""ZIqlv-vl}^ޮWL~4i'jW>eO YN?FەS!"@qk(ӻ9 %|u.}s%MWOԩ&Խ;ҍ7`94760yV}n<:m1 >3';NRR $^U=03`@(&rGJ}7`z#DZ38Lz)sv&#Uƶk|ъ8r?޷co~t^6yLR'I}yA=+< k[Rwo{IQ|zRe~^(~^5]jl'E?f߄yO^wt ScزBMk;+2y+eo6VKsc䦧j~n|{y*&&NKA-&*1s`jz ʭ !>:5e,F />&8g`qv=iw2(0Y{[1 Lj8 o˳$mZMZI|7p/}A)Y-4~tn>}vJ?h%M5%9{YM1I.+YH}W^YGi=La`in/3n<\jٞb=mOH5()+d#vDhv*{t#ۗڀ\ku5׶~ cT\dn5Y%췽}U'KK.C{xcr2Fuo*(|uy9{LSeHbBY\;bǒ`?pϟ^ČyO_O*S&%{Z]=ag4mi(Y*M[M.PmTT]mӖ lk Z<5v%۲om\Y^_{EicP{O&<;uSl]y&HJyL2{;KH`9MEI ZZ̓Gy8gGֽPZnuKKAQF:>>ұtP-=LY٦pw߮|jwZ \I&/Iɽt&Y^ eI7)tyXgV~&gJ0LXmhKZGdm W9gUJ| 섵;S{`j`B}v@9T_"V$8 ]D%wVn4_ =5E2.D(rRCNG*icI߱"Brq JAC#qOED5;~-L4ޑ'EcH߻h,)!'Tem)ipL,ma{b=p6PO,F@SJE(dg1%=L{N10hͪ^74!+ DQƤ |(tˆ9*CH!4BޛbYݵZVE]v@@EM:BK1BDܿ? 7oΝ;/a PJM&c3‡{`e+~f|P6_~#1$r="W,H#[eNvBbj:,_n@7sЅg/eQitPcXoO>Nw{b`xga.A0\E2sATxf\ΖaM=;z@gՍr: ɦ WⱌZk/ i DF0l0͛ #픹ʂ`EXP8+H6O۟/b' C՗=YXs^:ϝf읶8RbB D%c<#q2ۄkYe-_Bye?ԝd{k$T.7nվNvK/ ի=_|-"-]lĝS ـAmF4raF+.>d4;Oޚ,g;'ÞwQ{{QU*NM(s+wDH+ Uj($ӓRgJi.~ ٤H*+?;'4SZG8vqsƍ('@UOzа?%Fv]< ٥F9jrG+ˑ_Z_c'[!bdA,av )f?}>Fcf][TA@%OP S)]]SOܞ+ot o1ؚbOw#ϼJǤW_x\J_Zmوm[HߙK[Pic"8}u,z[U2\sp,E*1◃0yt ;-U]̘6U. V$ ,ɼkغk z1\\aL[u7ڽ{Oʾ}Դ4 D`/LVd%u  k[' w_`_HpTT9|JV'sRjyv"|uvXN՘#rCU*ɼ@hh+#CTyVxʣd:J;p !dnY]%+ !;#'#sJ &jA'^kdğM+PIN]\_G~l皚d0h>OKW!}3n*Q`OocUƌp2da ;o&8{glog8wMÓ<{!c/_|cP8\҄_z mزt *}y[Ox1-!Y y/dk)DN^;яm3/A=лge8iM+p14z^lFM9ٹ} 9t>ݥY %Q5+֬[K`}ʫEmI%*J>$'[Q=/3 ;Z_UV#si(vTKu/V/xiX3hh/{m-V&O jǠH_"D :4v9 pMRԵ\ؔO!|ԮRH;8ϬNOVbﲉ3^N7jT u˗*x+j EOT{w{&w-lmɤ|K3g fwXI`=HS#'-xIj;~l|)ozxc\-,ZU.yGTij7P^H-[>58G^omOb U)e_GW-u{"oVw?0s+si+Y9E\8IuzJUۺim;i}T!Ytrm%`%vnYK[M-CL:Zz6ԭ2\"դO~Y_\ZEqfa >̭-)|NrїNF-SaF)u{ldzi(#~wVoA#*H4]џc+9㵿,6"+797m *gPp>{c Yͺ2A`H8sۄiH튤Zd #ۓ8FMԩ h81<(]DZg@uʔT5=zP} =? j,UⒼx1{d,Zso?qS Lm.<1}X+|VZE(//`/ݴ3= Xxgh-JꑛvqyJ` D6b3(1'S0ǍGJ%!Cv<~2N߿́c+"%9Ͽ ̈),3fݮgǏ61$@?l/+'OR*D$zxa[ z-wpwY36<3J݉G 5V*2^Wş~,jAIʪ7cIUdw,PBZ_%XXR%ߤ)c{w܅*l]#V YPs17+*s&%PPQ{N.RXƤe 2gbCd8QۉXo%ѹcRΨ?Ӵ=we{Eq(_2_u[a$ɉklZUt~|<=QC>톾dKb%CDD>_Cm6ULmu1:T2IK l2({8I|CV粲]"s=)p"{ҳi=@ImmW__dDs21l9tso#]Ø;i aBӔU*W 税^\l+%$ɋl_N:ÐLБBHtb,>]h1qK a\C :Z'* 8 (:FJqoBFR*wcx1xqH*1BC}o0f_/h˰A~E=.u&{3 0`9tBsknE~: 8ueab6jƤA~V t %玒LƝ%,sd'As#%CvxZgЬ . Z?d  t:p X#+;m2. /QAsŃ?9מzB1h8v뇬E,D=M)Wb)Ȟ.ʏqK{U}rw}_,YArϿᔭM5|}BPWm2i}Aՠv%̅)&Ѹe e)xIj"y:>R rڵ&bI,ڷM)_J0uϧyK9bmo@1-YxIjY(f&}_lzH>zGRa(}hg![vGʾyԱu7sP=ѮOwϾOk=HIMFtA+opY%>2^(>N0BUz;?;ީi8jRNS-PYՄ{;lb(/lE9}vE`FrOɎ̥I'z 9?0qBqץ5^4ňA8{&+YY6!9N!6{l9.pmqpC/+ 8 M旁&xїW"BLEm1% TR~+ڿ|7. ׍]0='$۸$?aޭ%˾W@D<.g6oڛѝ@Ɵ! 8x1N+݉^a)Is Rީ$g&'5|9|̗İ̭)Ymoc3sZ\}F (_|!5c 1gOxm{7n[kjbbdx*۴,|f1f]qq*ٟ_Yɨqavnnv ud:$lZ߿>+:1}SDYzpی[t,GAvc.?1 bZ껑lth jlI YI>|<=p)\svt? =wNw;1|Evt&s;;]Mw> KB7*t&Lm. H[?}"z[ftG)p_f3ޣ{{lX ~Z#M#sMtlGAL+*Oחp FNЌEߞǒR†@vYn$I2CW5U*k0 0j\[Jd!>En pw3fk9*)]4h(Ŧ~Dԑ0hIjM񯏾nGowOiT 9 Ͳ =|!ۢR VwH;M:x~Ypb'N(bQΙ$Әh`\Q4AscǙwͦ7c-b9;Yׯ)8vF:Ҿqshǿ}=ZՖO5uDVa R'\,'Zs(Zcx!<4Rs`Oޞ.8`0z#Ǐ`ʌa}|]o+ϣ޷aJ01X0n9-n2YVUX q3tfbE8n>x(2C18_N :xi(}]~2Qo:̑|=);4ũSg qg--D >~(tK`#$%g~Do8ga۾R){esqV&:;RtM0y(eA)0zԘ=F={GYZvh&V1ilΊLupS^dr ;2'Xh .Xߏd  څcOJYӀN෺2tau<{}E2yIˠ i<}a(}nje{<#c( .1|&cŗ)u vE#{i.]:#,dJ{t?Ubͪ}*U`54_1z`=W4ڡ$v &c pIe)㏾XAA Ȫ]O~chJmp<,o@yޭ;C 3||7nؠ8\8}[7*_uA 3,8Vo؈f(3-i(d.*6Ъ2s}_ɇ~i rRa уl"GWO[ՈTJjb `뮼?..Z^LgOvܰ1md߹ tjiշq{c`ǭҢ| I¸*ZÊ e Q 2~o{7=0 b$%1$m,s;XG֭Ɣ;d?9;{QJӼQZ#rՖ݋`\mo޾c~EU]#]Ud&m%z@t/}m)b|?c5q!8+v)20;["\}yؘ`Hh? =)\RRqR>/C ÉL~+.<%4v"aEO8Q˗L^ :ǘBظ BG@73Q8|8]- V(Ǚ\3%V0. {PZ)DIn{# *2FЌ\$ЕIg"9~?#jL3Cs&.5h[rI<Ћ_SsMwzصn?r8'v3">9RFFͽLKB=LLplqkA9Yz\;~}s}+gx:gSd#p0*K`N'^|s0QGi~&8޴΢&tކ{#s* {lfE'}+WXT~p`Yn4ɢA>_Xظ6#e!#eu4:C &*jGV|rq4,hzt3Lֶ@VUUOHsb>(;1^ӄ+Fj3x@fx#ZiqWkwtMH!47V#ާ>Zפ$ǭ Z83Q`[6m[ h&7*ں+.#}O>`UA:)\CGvY-RGIrn$}JWCR\gOߦJ_c)a[GIr5qyWP`9NkߪchÒxΌ5O<8rC ҹvr*֭*5mn.sHcDt'P)ކ~XgӐDv3~ ݙ%9+KKs#xJ$FFuB"k(DŽpI\2k*WC:zyCkkcJ9Ȥo@/w*}lHqO1a~$`Fh!4TÐMـҺ=ca0Q:;akochH?U NvVjC[hU>ct |sFFyx; `sC-mhU4o.wyTf,y]{Ț;g )%\1J|f#7螨-#XC]s,# @z : daA4ltKh?*`xXf ^0>[F/l'Y5~ ew|Eɺ0n/kiI>j) kJv\ػ;pmbQV4Bv^%=b,!lI U0F;5o7lw ql  j1LEU,yZaG_ӄ` Q474"6.'T*ճ P$=0#Qp=(JUq^T^!#!hhxM)S(o `æ0zzOpr84#`oH7NjDѷΦ/K҇y}S㑗>л//^Ψj6I6!cKlV3Av;Ul`. JoIQKϖagQY˂itKHä2RU~rx)Oituh"MkJ'+ (ՆV薯yHapPj mwLXAɱxsn%Vr*6j>kA:haJe&M :O`Կ?[o{GABۧ4C^JοQR&|1[)!%Ȉ3ש=$ދ2),.ϑDX՛>/Ǵq~Ni~inu `@(NJ[ #zt4<{ɺ$%*3 P d F]=A~}ˋfLסBPR߈ lٲˆܵk/ƌg()pB ǽ }(d3 @?M \%gVðϞ cYw\7 CF!9"~\1qRGKb9ٿaa 3,J}ᇈ`\-ٰk= eTՔ:8L#q)B< Cݯ2suvmuQƑml*/UqTV.[BѾRmѧO*IuenFcymۈÇ+$|pGW7O{o!hg/AA!$_EE 7^'00UmI#jVWڛ= 4aέ#1x4NS˖ [0 ZJ|a+`vtu"P$҄#lXk̓Dv})+a+e\X`7 dŧT]p_?Ä?H/Ʌ 0ߧ@E{)-Mox*t*t`tDNVkD?BM_"5QZ;z.x=Ȓg7ar*H!:l+glE؂q<|Mqb\LMðcaK\ 2x0!ICN;u&֬]lgO[SG²5~O||%WUYuz=۬fΙ_K:C9 ={73^H=:J[R鏖̥|&DYMzAu2eҶJطwJaa]z4 /C6y(;+C/=@Aʺ^p=Uwdoټ߲.mURgiq0EZx+yy v+rk x*y?o-k `l<.~q-jaT-K1^zr~zjmR$64,df5ul+9TJ}+rutU&YJֱ]j_Xˑmr{ IZArm]S7S}*OXumؑ򎨠guQRz=>zIl"6#Ğd{ vBu^<[մ!r 6ֆ-sݑdSG2ijCEƤx"4Jcd46sV:W$вW,5<0' D'_:vsVRcH<"^V=é?~+z]@<>[ߍnyэF}$GsŔs}Ib}D3g3kݗ'K̏Id1;b}6}(ud%UJ*8Q~)3{F?ͦ{fJv&3qx$7;cAp!ǥ?.!* 9Eq zyQ֯<$&2Ê<.d7b<WDlM2̿m<P'؈M/mUsZRa0=y%A>H7s4k[NKщ`^mLHT ?0T\D`MP4-]? n>=)+gcq|QkRK+C[&boQ–fB#cɂPxi{q<&T9ɨK0҅ 4ћqrE/eov*͠arUy(1T;v,e MवiEW2j߶} NF&T(e܈-[F0 FX0e064:QBmj;$`i[{>.DЌ X&O-Æc_)2@o!Cux~ذ~vl])!U]BCcaKQ2kQ'=c\־7a(R>,/5=蓜b)A`5sqdWo8;ڡIMOrT#R[f4aؚ #5yEdH,lI-|BQ :d(Jq"b #}4ܴr(o_1I[( _R_ w- k[eۄM'cf^pȪ&tRӹ3'N APN)ɰcQKYvz NNDa-8vׯ]:FCTu`(dߙFpg/?yqeh#\Xe D2{Qm蠡={v 5رHDGS+ k߯XNYKOx`cTNt۲-U}})))tT̗WD!9)* jgqXX(!X}y!lC"(kű!-vP=Raq%WȘdl-LVcΣ2DgC%OoXPcppN깄snxtsMU2À}Y:鉱pm,~َGcP$s~G w|ҟ EGV9mUf{kFob:l ٦Taشf=;۱G' JA#?w-SSR oTѩ CƣO8fpU`$$"J)n"蛙#|3Z>~^RYT 1#x}mRjfh*GGr S]?d& c3;. aJ:M:sJKvCau(F^N ޵YEj=kno&dÇ |6GfnڣOVaz?ށC1c(އ}? E@0Pjؕې/V86C„VϢ/ytJ++o'T汯MX^Pط{`o#{H%ߚ> ?d>jlП},{ scǷSTTULL8[rSv~5Μun%cU`cƏp=ioSaҮ$4&xnjG! M bg{mz.sG@ˎoW]>z4 KiAQ%],]h UP i4TS+M/Tž#a$b *CدCjuL/H;]\:oߕcK.H*&venR)]ͧz9Ze6%HCz:GG{.d"'`\Nu Z#T;✠ 娟rGshaOZ,廔/t'$ɳSUYu>͑7W_pswqxR[g6d;y.jؑo!t6v2aU/l;TCB[LxH% a{264J#2{LQz* ߬6ҩ=*[z(n}Mȏ8l?hx)PXR7css ɦRgx s"|3Ptwܺ&5oNh9I<=d![/f&R1Ԇ 8FÈ`f> ox0| RSOK &$HfȺze]7I>ٿqW(M Ʀ44H/校ɶ2c},., !j@ZdIVvj2>bQ^\@/zjX.mkgK6r|N)t¸!dBYV #POO[{v?Ao_Wo%/7n=Hr#@+[W_Ra+18'O\܅2~ߓr+y(@7+Q=tgN$P Ȫ{uUd* >nH}VrEn{^'w_lٱ}}=Z[^(Yzm1㲁* p lRZ̧~+Lz5~%/ <8OTTHz2]x: #FOTm?o`T[Kr~:SvP]CVe@ &_}EJNQNOVV)"Wy lؖm~,o"0֬Udi oqZUW>_]fFEë=H4:Q6;bCf |VJe *e4BB2d>^9d`QәHå 4d#4S~ #?Nu>:xH˳k>")!}\aXD`== DvFq1lrW/O^gg͖?4%Ky ܱzALGȊDžD`UAw5cnj[6CY~"Ss Pi'q OI&K{ ۮGxhm h*Uwן!?n[s&̞0>[KQΫ9wK_,>=uΛ) Mm2>T+u^dL!NIGԪJOڙq銣!NuSbwuI2>2t&6Izb|O4c6߷^_ Jʻֻs:_ ug*/8'90W5,DaT;. $RG@팕BoMG֤Qh cM)NP◪Pu*1tҙieK1RZ N' ]"UvQae&)N0E 8ЋfbzSӮw?SyW]+`Tq38Z@nˌ;uŔ͡,Ǫd$S5@' jk/CFf,%HH==e]=('&)I}>q1)lfEY{ 0x ʛq($Q"cgbc\&GgVU獛(!؅24,3**q1>ڌ|N ;r9&1F9-ރ'>A"/ 6Pzv&Wb9uBiQt')FQ'Wqrz9.w(7+0XU!;GahSȭC~I5Raٓ`׷7GÄXi˚oe׽@IDATBtG(;tN}?"Ѓ2 |rjY?HN%#7G߁)Rͭ߈zC;pAi<>cGEォvS;i+9R-U7o)w(y4 אh8{SR~hmi3XղyIN6VlھwpU\AlHQ^^v{ݍjbLk4 MD&||P4&o޼7Ν;3|;#`Mc!29I킅?rv&S(FHgL:]:ӣ܏eFҺPF}/ħ~Kg?tgf D:g]e]3 Zb<ͯUuXt &q( _'nxyB?ٔ@7~<@c_5p$0Ȍsgl^0&vŒoCfe+YLlhl#4R8wᄎ#ps'R߃,;lI}%104-> bQ&+ V)Yv K)XsOsonrPhh( )ǟ=sr)ÿ﯌i2,x338р%-kаlɾ t~q-WE} ꮀ5싍7M%KOV%|Z`|y|:% ;@eljGY972b|뒵^r֮XAGҙ,l#7:8Auci=ezd&J3~a!K&I~O_O0] ]){bdaİpdRVVJ#r+xx僯-`i.IqIXIyYl̘v7:(9sVEg2:ˆw+ Gs1ŽC;WG)3=s(6)C73d HOVmzַ$fhoo*0i81^Y6:љj0܏Eؿo7za 3љ L]~#|2,Hgȑ$1!ϝ8b: kvOᡌJ;`[IcN"?*e;F3='1l܌VgJIpkuvUbI2ϔOӹR0aCL'#ɼVφ hkvV-E)cQ**tdg;,4S/P2^]ޝgי`V`9ԧ|(-Xe,$kun+a1G5@%O9jWzV,ŤE E.o& ٢t9zPvCxgޫ8wso](^xmh4#/fџZ:s| &lДOs58W8l 4WORх=}CvIaٞθ9wUME2tS7t>|̹tIB5孫͜0s8ЉIOmqc/ΟQw<%3GAmRݑ[XGZ΍tc8ͥT*`|ntfÚU97Ŧ՛y=J;I:] )6('Xm!<ȤPe~Da1Z.UpBunX|'amTO&+9wԋ'ƾ 3keDS[7@}q$-dz|D5 Ț5e0먴**-PajK;{V #?ONf| ]SUdm7޽( Mwla<ػ3CKi]zhe(wBh`;r8Mrj^yڞKtKMl,o'Iٖ.ti,۬7kiÔ|@EM0e,ꔥ z Lok@@:N^%վGC9;Qr3{jz)N ") ڏRt/}a :R-+iɋNt0є]Uz]!uiG;I^NX{~WfomV)ӾMAּ+KMh̨W6}eFpHEZSLQUqœC5Ϣ6 u;l\5}V6}N0#XlL)\|@励wJYx2T?GK}a㪝ҭGs ui;\m R[3iT]&uw0XVF^#`Z BӁձ4&@FŔOTNSU8mӭ;F}7f1nW Y =em]QFp ~~ʹ$as&NWh@:B)S{6c& )Oi'Sjҹd}Q.`aWJ~%"O%Sq[1dk9yr cU\M<Jxuw^/<4|:ۆ2X@1066dHvŧ ɗq&\(ݚ}GϏ<ͥ Z.ƨB`iXd$z81Pctva2RPɯʟ~AG?dH$1(Ia1%Wma8b_㳷#L O ks [\*=Ws7P!l3B15T*l CmX٢4m8Y|.[dec!#4c7ð<}xJ`jLYjJڲ8sUƉ`eOh xً`۔)SCά,c0s߉La#G4ԥ\p[&%3 V ؔt" Xi%iBkC]PXd%}#_|e}a 4tS;=w]tXpU> ;i iԀ1c<>s#*D&w @_bӦ+Ǒ6q8_d3ؘ02'5O<*k8 ]$Aiq(F#gba@È@FVIy5|Ki4'0ݎ/Ә_-K/hI2Rmh[YY2g_l]Ξ Bc{{#ǟd]e{IiQWJ!J&\L`R*,(^Ebr%:\s8$녨gK\ޝG0|t.>` h֖hPy%S2Q3O;@)P$1'1p8o Ʌ|m<"yĴ eyo%^uqqYb1~qJ(LEg[aԨIz2&&;vZܗ;eN3# z%_K„!憵kxo1k}GWCI8~.z$jS#:Lv/D!=1G߽`~Ʈ>MI`HUX%=jWtD(F^n2f?wTec}OP4)+l>Ёøb8M^2<G燘_1fN tilҳTUөY1k{te\5 S=gPLwgؽs+/ӈ9@:`(]xucpP(,J8J8PaĘ1f% $[SQ#N|4 R Mv >d atJ_R64R 9Ѷ*Gkz}v=9n-߁ k:pdBI TY2X/>k'7!N%C֎ ડ$Hd\32J[4X:sok6&CK(FtDQBzBI[I*Lj*cZj뒹JJ:)>2>S?~Rn߳Ίd[o_©0vx  2b<ץ,b+ש P'IN zrNհ1$ʺZR.9N5j'yɱ2֜h9/&:uEnVfqDnΎd:!f8{{ݱ8c1w1!s+5n<֮^?S{`:91h3p,ga-QI"$wCu+V(-ڦɀE_(n^Ĩ vm\qcc*jN"WTddLݭ?A>:vI)Ͽ_C.ƌΞHN:Tst0'c5A,!c=J@aXjV[Ww*ΔRu)*_$VbX~0"hJpfvfwtc]߈p$*Ls֫CCtbXO`:TR NȎ00O° dg6q tfֆe> [*o>BJqU *87u_T{6W-w{=0:}a߼yC,M̳6׏?ԶVvqze~&RQlH4iQDIXjҶJ)v{|')5j9iCUR)ZRN\D^Q5*$Zؾ2դ2*!6+`AeCNɺdG&fQ9NĉBT>r$qT8 SM.C*Mw'`epNBJ%LEoly~YIʃSxN}>f)όOo @ڑeD6KijG;Fꬥsz)ˡu2FY=ԳSSn) Hɸ@; )!Ͻtz>)޽(ʣsr8Gv~5Y4^mk)^㛚RU*ov-1Ka *)';]q:|G:LE$W44K$vcrR/ܧʩT]UCi 83ӼT;nm:RyTu&dO9 GEpK 9 o?l-,/etGk'cχm`HNߢb_y ~=z-9ṿ?Am@ÔhٵwRrp8~l|?}_s+R&Ì3a5I(sl䋯*ۦ7mr.z!΄*hhמyCP[RI~֭_q);ZY81dd{g3֬?^x[[##ÇܶA)]F.5_8a/'@gG/mPP#Y;q\W?6QsOvHzvg*aOg3;1CDٔ{c./E# KdK$mjeqB%?;%^L󷌬>$%$^"{Łf 8gƏI9ueSc4Haiڪ9YXf-0_WiMSaU#T RY'pHe- ͥ0?@|!VLU<Ƌda$Ci_G$$RUMٙkW%1s>/O#1+jt1﫯bj[Z_ # ɇ1CJ#"I_$UP1d  /8v >3KϿHdѣwu,XzٰGƫH:ZHQ硇]G5-)oiDPӔp%eMW7?>.}:2;b`sY{Fv0'#y×Fz5q!wt怶fj|ts׮>uZ_-ɾ-+r { ܺ󥑑PRsʧa'8wΟFpmj]}䷀ywMN/W·'.Z,2.EԆcߚZj^< u?j ' Ǯ;Xb>l|2=֚=$ uWf^*sG0Vפd?4mȹAUy%ӈSnWlN`th~:ad??5}E? >& =F4t`G0 |7jg23gޭ~YH9]9Glgu͏" 7m^8\:& }?󧎮c5}9> ]$h1A:ʁ1_W'JǠ1P5NBGJ#l k==[CFvtL 7G0t+o%y-I/r^ʟk_u7f/S6֩f :7:l$[-׍V2/vuUkzMV巜c[W1Y[WO{I)7T4i>MenTqM$xki.KCY'׈Dwnz~j(:eMijGQ]R;?ջKG/TJwRVivHI`^:^U&y Vܼ\8J]zN[s06o$=:e2Wbq! IFbb,%(w2RsQVeH>4 #PV<3#.e_@=p,d처Rσ5g>TٸH3|B2d$mݙsCx|@::R†$3맍wwA:zLb歌A!ؽs1UBHvw4i̘1vDK&6gK&\I!1,J_pRRaXn=YMơa<?T}0G-.|VuTwt8m]8Fp#Sr&+6MQ1qC @SzvgLJ|Hqzb 5XpB#m(O&̸aٳbYݑ1n79rbleipX_|o*|siɋ] #)и.\A#v̜roLdEɧiJIMG 6v6%-dLxZ PDO>am~-2Uӏ\#%kTL,?i&_O0 rw@c3K#%`Ǝ dߒ~qJ(Iߒ`66̎:=fM}ie_ŌBɶ|/ !~=C}/Er~5ga5ܽ;@cBT8K?|;Xa=%Ip՘x2NZnf@۲BXzFdp nLV ̐j%hЗu04 v8ЗC0[撌앷ަz45 87?$wZ3MCV=}_m K? hHJ!͆@{78z2GyY |I|O db|h̯(,GFF>%)_v÷r8'a!xWq۪J ^d>6ŝ?4aaJǯ6d@i'6O:eMLL T8IiRV=-a0fR$)9QY7^1@YL|o2iN/o7") (ƌQ@VJQB_ V9TDlͥl%>GBЉ3 28 wl0hP1(e <ŔlϪZYۺV/KsF+ȞWΟG9z7W;%%"/=@*lxrL~PC6cs犏>"[G&dD Nz $Hႃ[7 MٞHE HNcZݴw!ٚKΎn5lj$9>֣a{k~ö⦭ٽa6˼a-Оk|3m,YؘKtkQOvXd[cīo*咩 ,ۭ9S”95jRY%쑹e>e(sU-ͨ.5sҩPx)9[g$R uشeM/1P"H9 7e0G5'(M?o믿72]g3$F729/N %ى!euhd*,XjLC=6X/aiu{477wȘɠS*9Ʈ8H[_DgWSX瑐e7j8$_y! w)6d֚]+-,Gȸ\߾+~[PmuX ;thp Ci%e"=Qdw:HAUq q).C&W/Qu HntQBuqWY»{8Jghݱ}ZFivۚK$f+{S8m7[.$.mk)m:3TF/(iZv׏G%١!ZUq^m0al\[!ۻulF_\RKc/U} ՑO0:P팢(dZq\f[US S8):(q7)R+4yU'!{!3t")}fh^U;^Ou hQwgo%vbQ^ WFՠt*Іl>n]a?d$De2j Zv+&yOeA\)kVϫڴ5uMl:~dؘ*#TVW겹erRNy+Iݦ֋ԯ5js#ekܷ͎Ӿ&r?R>}n9K doSr()6\ұm㇒{ K,DSe"{ni nV&6`YEB:07]J9}VeiZ>)f0d_18}ʎC@YPSxY?L XXk('-S/ d#j,j\iǩ"5~$b)]}{K 2ƒ+/L`1:yXUYI9V63A:XX||v8eܠ4S=(M#ðjwpE\G[X[/7B}U'.lk~QG9=Ƅ${ȔYHɹ!H Hÿ3 1تJp",aH aީQ42O-WI,x 9Tꆫ)-mo7.9Qti+Bo=zwБ臈ġͥ`Gt XbYdt\ufm6 V~t65cw'AVOJt,Y,ԣ,}4iɒ=,lqLұ3z2h|{O]i`3G[xtf!\Ly1/"/Q1[; f^j(%I0B=(hcF# 82ޛ?Dgs degԣ3F@#z[ʉy3H|c#d*ߺD;\rkO mS; kcPk+%lܜ=ke4 oR.m|,wlmN3QjwhhcmT5\4d"{xԨQX|N8: a !Q[l۶[;9֪=bRAn2 K:{XH4>uȒ=/}J5 t'~PIl5;=%Bm|=SJ(ƉxBlr*c\D<cNHxL:f2C](qՊ8pp?u pU`r9gNSr[ Pd&4X(]|D Px c6ơ=e' `>JEhNcPƿHXnNοw^d~yjDG'0g;7_3lC{؞0X_uV)wLU% 9n2=4E3r≻C`\_DPޘᔔL5 Hs?yϩZ#L}}{zhb"^ nvr|<'(dr ¾۟FxޡrNoC׾c40Uf3 n)3|θ ̛o|P|OO@bq,Afٽ2SynR0s` vN*6 bzR`P}G3gAhy22 .)Wu>chyƭ׸TmNeډ Pm[HI^OYy%%gD2UW^ eO=XL3_<`ʐ]Sf%i]ggHZUSڥ cWJ'KG'GO)vR𤋮Sm[}[[l%Sfs=^dMS`^_gF%RSGsnu^If w/E%4//Ui Xz!I-}d&Ust)yIg!9<]t5H_M7‘^7Jj񚦼\+9W~8 ;w~ qler~i`\v.40"L[!^Fk\F_to* G'.TJ2ngAY Ȉ5~YJz(N.})WB/_Cu5c< :-͛_Dn[Nv% ݻXᩙp0qG \JFd |aĖ'h"M}imF)b/ഖ1 i'0o3d {U{YVc?U-WqM%Ꜩ?W㑪ot ٨yȾ2| Ǝ   酌Ɋ"itoC!%t,&\A0'% ~{GgXk&M1d`ƨKm8Niww 1$h!q*<)|Qtw#4pdЖ,~ Fm"^y~6431b5ɵNtw[ ڟ92%7~2T-x`sƌE >ɑV7շ_[= v`J-=ҕ=80cO N~w>ݺcĨ1(1C2J i!<xgyolĺ1hѨзgޗc3Z nO?T>ez5)N1P5ϸ ?vu%ɓq ҀiCc6tk+휷O@ݞT&0b ׎V$Y=,N0e.bWS,")l=f|fL/'`|$qOzdK s> aHx N)w|sP ®қWB#b`Cy(n0^/lOO)՗ts ,wjE5*zd\9o»quEٶ4}Th 3.mYd9%:]w\;Vo/ yE8Jg:]:ءlg5_k,Ϡ#Cn V1XA4cW,ȕ03__ frvZ7aR9~A@ޛcNu_uB ;%&j^}`{}$xTV1h|/{mO@.Ӈs+C˻΀qL3Eksj:NJRQ9Rk7l~vWa$s2':sTq16?'s*a8۵7eh]ڙrW+ 藺B<;+l]Љ7{sz`&}vuqM,ЕT)EdB6k,PΐƦF-=D^[=xēwc1ǭjr'KΩNY+PG΍knwĞ K:30%zS{OeMāmK0ᗩ6Ș׌YjdEšt s^L"Ѝ 5OC5WPMӨ`? 9wp\:⣢&5V|cp}/NT)oI{K,] #amw!j1ЏHsrT SՖp[݌6zSTqgx :m-uzHE@@'֡$l6Ƚ|؎4Ia:qgGq&VƘpϳ|&4]MdYջX~fD`wF. {\7rޏ?I:K?Eh*|ԓOVʎu,WGw+٨`14Lؠ=*`&%iכ{j}ݻ-y /0=MVs/K2dv~HI5Bs`rsE^~9,1 q7kV\M^|BJƥV &}j:aNJ=Y:J*ݮ6戾Q:0S ͏>EM%xeLRI9CakpݲJK{̲R ǪO.L}lMv_Nmyщ9z-r]"'qyz^FI+m5ʡ͊@Od_u[Ӽ%?UR:=~r#UNoDC7->)cr$$˦PKZoCR{W|Bs5.݉ll-k) w\-uHHC, \{ !mi!1.7|' i(SP;iOz+ Opk':iTicR5˹o-vYd]ob\I4;/uveڵ~#NǙPo9 A0lڴ4MM*8Ey'I PBü%m{Y%.P}09bdFUڵ3&N\Pʬ@IDATH*@UK?rhcdQI)!73$U|ABzv>7SDŽISߓqHjėۻ\4#D`2= ܩ.vh:1S j9a;1ߗkVyqfvDg}oeȞ4ed`wMH:X`՘4>Ÿ~ezA?-*'cK}8{v j&MF=5CG1#F7 d?Py"65ڕ+.# cא=ٌ\Se"}Î<<적, 0'm1o-^zѶ=E#̞>cv韛qPBot]+lghE%"h'g=RJØ10 :GJJTOS:G_fv&ؾm;Jh^^|Bo2ckm6'FG|fc|.{Ιm_o'y mة= `|^X"(${8V` +"2.fLdsxE `xQAT͸@@x9M;|$(o8ɿ?Cm ֳE҃hJ^8{ Jſ=bѐGQ]LX޶tF'}0*BrQzkt9hsTYqp޿ee 1=?I:RZ=040ųO=?~cD}>t!x_^\*JcflgZJ򹶠oX%#3KYۢ4*x4еDfpXjWgk0V2|b1TZ'LY(O_d߱ S/5ȍ^M,IM2s'aAG6:}zR<~Y9aT|VuqJ]vnyq{^h&_%éZ):|V߲~ [bi=þ=|xd!C^g13fJV:y0Ā;R9lΏcybc9d ]:fN[APPʳ'(RɔP!ԣ. }Lhx?ӓDSHJuE037_~I<4|˜n:,[-^_!HR?**v~;" qtF*Uq G% t `sХw&4es۠ME>xb8CuFӱҿ9WN!vS!P-ruAq)Up̌џ1cWZ}a9+¿'jjj~6!YXD?S!>0M ECm ˪L:eOs .N=ӶP2zng\]8܈;>JѦ Dz w`XNXq+EG_?GhtPX>ˋp>2퐓ī||t`+ ahY4Rfo\< AxosKth?Z'{Nd :]L|E)%+vvKˆsJpoLV8${q. 2*FlX>KQfpjMB>jzB +.eYGPΤޤ:`>@Ye/zB ]ue鰔Nz>I1(}Tgd&+ !U?\XXI X&NH9erOMܴlX$yk&d_sJژd^ @[Af=zk.Z>U%bg&eYJ.Ql",QTEͻu}?r&h_ԝW%l~k-+`oOSrP(r*- /ڈITrhiZr.MXQQ)15gkl-& uG 6]DUdQA{=VTq",;QBU=5[VCiȘطg78)E\+@Tc")uV )J7a1|/&93&HʥŇ0?av) Es^GE@,^$12çaAYJP@p1 {cԻiúuXEiZ4 oQ*f!  vdr^̠@H|Zy?ڐCA=3Y-K `U+ksɳ<,vfGF R٘QqCaE@nUbHmذieۻaPY;)TU:9dSq܇{/RVtڽO)ΐ5w7l;a/{0P{<؇b찳,՘9_)Lc['O2L so93PEY aBFr{-._A0\ܼ=&S_5"ŽA@{k82KF~Z"dFF_1,CFj"Aaܠ[Xu$m 2$WOv/VWW֗ĶZŒ55-uJ/ϾF\fl`m7ceFQ__vԩf$J;zW-S-Vb ~^d: Brl!l;%%_D!X\t?~V o]z-ۑe϶ܘdy9Ϧ68vx|fw8.oaʴ?ae.%cϬ0}1GKylX.ʳz1&a;X-ƴi~Z:Kyч3f&h:14 Ԥαd|궦KK9K'#yP KLp:y+KQq$0W69ąKQ(ã7ٶQtXLUwprE^>ؖv팗^{ QP*t?W@VA%t8{*}FL~.͹H!gp\@'` %vjӋ8w.@d Xvw@f8Ȱ8~Smi/|;oQttw =&?qdBPZT`xm /#A O{TqZHp Q[MjgӮvSQ%5QOrѓ؆u pA:/W Fҙj2tAgʴJh:!>.7mz󚝐v.$2Afp#"YȕqSYv&X;E"FBGF[Z&N KW/葓r݉+=cj{vXSd.xGؙCvfwźcy޲E!<|QYyH#n|=G .4(EmuxN.aLBH!|FI.NNUI ÿ;r/<=yrO*IZZ_r)`|U`@!r$;GK_Ň FCO{^C}.Ȫ*6J95[@RZ[.7MgUØlvjWj.?u\τ 6nENM2w5͒][LԲКqmsiʫcuuYCi}iC@*|?z>GaD#/Ie *0>ţFFA81wC:xCG<$݋o2{ߚ>7mAtd&)LL KNv˖ɤ V1a`2 Ӗ7KrAx"ɀkf2[UKV(c #S KJ*%J1'ȩb4#@%zv6M{I3xh 8jO/U{ W. ; 8NaKRYÇ2059Ѵ1ܮ._10ěJ[ ANB%>EԳg'2cRozغ\`B$CN;1FYOؚWzg fvLp6%Ԍa8E/SzVpbɸO AlզّL\?Ө#+4^`ʔiәk494{K 7sSN] w,V9VM-h@#4:0m '])S:n=;h0wM }R7Ɗ4ӒrTx7YA&k<ڟ͝ 2s͏>@pd~2O66!S8{FW˽{8nݮog^$WȊ "TEEGC?'BBl/uB\ ~G`)(ٙ?d|UБc0z1th矧ccx`WS0Ti+ pȢ~:Lvcߏ,{C2%}[S?gz0CB>sy"";Dֻz.\xzޔw7^FJvIݎyVΝ;+(Ea q1Q]UD L:ĸs$ C:I2N'xgnN(̬,|#R8pad[}6zA RM!`Y=8Hk֬š1{\,[!V05Fs T}',D e>Amgj5 G' Cnj4o)+a(Q>4/bĒϿPXŋ?\FVJKnŴ9wCm]ڛo`9  7,WMkcʪ?yuV`͇*stﮌkn8=LAQ?S 1;Vrda\:GgXo|FPX05kQaz2>9~H!;-4n{.vϛŐof>Co2XX}vn8)e]{w3T=;O>K  ע4H H.sq[:u`U׫lL1#4Yτ ߥ1 B'QƏ`dX:lw8gQ2N]:|(1׬^/gTsjIXE1}CqOs,~Eci W:Za{o cd%ʚхϾOf؋nypq7+@F*dC O'cR|/(f+# ~زx)MYGy/D5 \:rٓL 3{%9\=gH8i $)g8=Pӏs4vM5n(+)[~fLX2 @5hҷ+Nt~ X?}G(̣ԣfMWΫ*ohE4yV8ztC1DJF6u>o:KRGb#0RƲb, 7$5g.zO(u1g)gO.J+ uz؟UGuOW ֗nW#h#fx:1=PXXy`+:}ytdUӁ>-6Vq0>bCY;{,}%*YÏFі8i%G (hş|cqQ)ڟu~F̕O;~L % ]vj:l>j'ߴxu_Y9 9EM[GǴ|J_`Og.tlh<x`=D5 ٙO' |+HR:EVl?O4כ} <69ʆ\LBnva$/hl'ށ!z+yKg1v)|kRmgxo_~,E+aY/sV%l!a0ɭVatj2~l4+3I$%@FX5 & $rj O.UN\Y{tؿpﺾp#5?3* jV={ 5R:@J!K#,I36_:[ܟ:u 6mBvcQI&CNwy"e?qb.n{(.֧2beReX#i\㄰=ՊZG=8d"(.oA Csp!:Ɣ ˺TCYnV1N٨Ëp$lҘ MÚNјVrgsE@R˒wv{دjl8nNHf:]]8L0W>PJ8p1u /cǍ>2bЛ,mB- U<^~ } Âz!0?J'C{Q7/2BJQ7dW!Ugkq,rȼ_W9t(GS-RDb-p3U}Fcu͏f{+I6l?X01(g)3eF&H=S I9RZM)q`Ifnܷg*ahb]9I6dQߘsەÅU螩8v2 9&4nYj)6:O;Z-~=dլ;,, )ޏơ`%KzVlHIr#hȠR[^|UŔ)k,8`,~zmr8t|2t l?#L `Sz:zM@.4(nQwӀf _?e[ hi|]ܐGi<-:54vY:w1_lٗ?ƜLJ!^ "MdVefȮvW&v%ٛdm&w^1pXn ŋQAqoA7YjB@$=-ßF\ѹ /3lg[ilbw~$ K5!r`TUW#@RCRV9eȝ4\Z`뺵c#<Ȗ b~X=LWO`ӖH|Ǻ/9Y) 3$N߲oD-;񔥶3cPf hDrq !ƀtgKP< q1Xeۉp0c4}1cӉRdHuX3NcWYny0X ڨ2|u;Ԉd ܹw\뭜qmKE#jruvR~tSW+:}G4Ã=L0?cPo2GuEaDxa XU+%'̘E!FK%`6Lȴ ~ݸOՈ`_ kH$K]>0+)vm~F0o;P9A%Z flc]ؗHEy!xT @ zOܵ{=@MS _87Vd9Mjb"Sq{FJ(y??%|=bl  %#[ M~d]J}EUw8x VNIMbOI=x~=ۛt{nY70JuvEF6Y4c鈩k4GhRu⌻'jǍj[ә!)9>L+d(s71pJ޼U`U=c5_9:3~ jTNT6ȪG QF0seGcv}#8q,,9QSU8{d &YQC#gbbM~dqr4B?zX nwG~. ["oc ҨNfKS?00Xaoi3c\Fx 3t9+; _B:3 a;N"jۏ ܞ9ބPN 0ωl"%jf O6 %y pDZ7٫Z+ۄJA.'li~.tf+ġs=lLZ`DKk+"#ˆ؄n=zS⸄9h*Ñ=TI"XvdmK];s5҂?k3\ʕyF)5lj@bQ+p{ w`:1x5|R=V];~y 8} THG,"bȚ@rnF ۉ1Yًmشv#IsW7 [W[چ=aU |wܿrSTʃݍc:6[I0;s)|X8E{T0 .Q/%m޲-\UNj>bJ:xa3=l؈_orX̞4#xq~_EᄆEP[:v8~c㺯u 8k؉ylD-TA.Ts[EOmm*Nu\n|ey*}W?ݴA^dg0i|Ҕ)ӌY-mxXN ֮) VI*c:S|;|oi! =>{7m.t9z- "5%čmb ]\SFFtPnrqxჯ%Qe2[eݡoA&]_K4jcNb< "ȥf\I@( (IQvBQ;F k^Y8+ P"u6qUpNfMS˘C{ߎr긏_vYJY]o_5O*ի!\ۮ.S^}vwQ}@ F!ɸFQP9MͿzCI>zv|VWFiatʜ?ʍde8gjU혴Yzho߬=J.V{ilI"3@UT 'ǶP^f:h d'">1)' ă溄auKSz2,i~nyw-&Fခ5B°@:AZ@grzRܽ=8tQ0isGImT JFkG8͸ן e[q^<{#FookQ6Ʒ2{V2ad͛Qk+H:K99xb`߭z CT2(Ls.WI?SUy !SeZCHKd1GZQ Bymy a(gTl!J:x8A)`~N-uRfj"Q]J6'ᾧ_QPS(j ɚ^2Ȁ8a"ALOJxy"1 S~3AR$/ mMZm %&#j9njG)ḙ$_}?d)֎ z3kZr2KIYpH?ۿmg!*QF"̛;G9cXO_8wEmY#\@C!fhߺPc6_&LӦb1X\n2#+X/j#O4-# t?J#"pk,lcTf$VFm'j41qHCI)4ha k6nEedbt t_2H~Q0?RxUNf?/\Uw @&!7#L;'ͯ'$ĜQV"X]*xeq^:~;; Ɍ[Z|x}L9m7d`ػ{+l]~=FMGNA&=Ŷl6_2wiH2b3 ud rS: qn ᇋaoA&CF^@5DF[; 7?cDD{[;1rUdX |@Zv6m2h܊?LjXC9s [mtJYu3`l9})sEs$ڜiR;il-e~@jHIaX^4 |; TPTl Ko`?}=r\:;1Z>2#?Ndr/RyA;ef}9o&b &0[[[J)33f*B"ƒ~uC ɀ$k?z^緟fl e"J:T=^~U8LKyPtm%Qvٶ;=t](zͭg^} o V~7}{' 鰨(G||*B3bBttw; Și$ ?&Tt*+պe t dE Ʋ=8yZ+AS"<)o;c\[qj[h <+`m&é+3 4KiF={$O?£o\M”N*Ŭ6pȥw݌8:ƗdI1YzYU(m(͊_P^qq艏} s<؄hݱ8}j"y=__{n8~Qcg {RtȢ 1>yjkAd\)IT1ŔuryAhȕg%sT'*>+S÷O;ЪT#^gvՔodaRiibl؍c ! |SFj_]f2r옿zbԔ6NvW'e[N }ÏK:T{U?qY_CAxc8><%.=w ҉\8w2TQށȩlUbVͷ1mr Dp2Yϣ9|(]z t g+Z9h lba~ݵw([|ئX[u2*t@ƚM¯]>4uՅ3>`9%1jl`݊rT>/e|2)ǐFz.*U/w; RhO*-bjy? 3;dP[/fӾkz8O bFaJ^z `uϱuo[<rgU^>Dw2] 9v:SFRIĵm$tÐSy8Frb*9^}^F\L$%!6 9=JKYim _jNk+Dx$=e~k#>-}u:$/+>Ûe)';WS7^ϕҟ H/qQd}q)zi#:K:_r}*Z)I}F}^ l硖}\7Ldy7PUyznNQdJRUMrq]'$?^wx\WtZ0 ~>nD )z=twy|0S2Ą[XvC3lC !QaudDN=]+PAXCaxoZk$Xa݉޳g^o\2fDbGԔBNI7׷ݽ'=@J%C<՗c:49P^2XZz͎Ĕ,E`o7Ƥ ؂p G̱mNwhL!0 -3`x8t_b[h$t`_`}(8xae85r>[>4^'/pR/(i:e]/եsR*mk60^[iÆmXZLojZr˖=t90$"DQ9(-ϥ ԕm}iU-d^&:Th;^O @*Ƶ4̪$C{O vh[Vjֈqe^'ESޮH~7, Br]Qp~q@(ת hrd_Lw'SÆ̠D?AkCgXtQY_vo)pqIr"N`Ϙ= |E'_J9fѓ&C̡;r.|x3lxk9` p{t+M y 2/"7n7(+L`$Ҧ3|ǿcx0Z*j:ypv'qlLǕ:bu5*g>N3GQsgxza杋(˸MtC"54bզHayq!_/eH*d'sN޴\<β"CKWHsnd,@Ȑض099t K̚>.NW&jR-uZ/@c@ޅ; ZEX:#=a0 ]W};MQQ8yߍz>9ނ3?)RUj;մ;'Ap/ŶtN"ZР( (k\[ļ ȑ8io_cOWTI j8ƚqz-cNF!A߹#z):D)W@]_b_ tWs6N%(6E-KT:Wktcʏ_A]HXK2q2z 3(Gϑ;9ƚPL{ٜtDŽP_C bSN׵sq5;Y_1\cyRbs6Gc iP¦Z;ʤw_?G>;d?>~N{I\v2oG\ntݮvK6*e#VXGS A}Our5`)a <>'.?V@WS]٬Ͽ3}j U{zs+ף{|G5>k׭k|DOh kƿ+4 Վ@mFk/(ADidvjg;/_z:qkFZc檬S']4@ 1y+8F,/7щ)r%)ܘ㔥ue'N`&/kkR=ABkS=~lI쮬/k/[sWN|1U0sq®-1cH,|4TO&(0ah  l}Ot!vvc%3ڱ ;W\[iva@࢜&Gc4*,R+ SMF턋d=mSJv3Yh,Kc840-#<ہzhM\ȼDIC1rKeyÙG5A i&(7hc:=.+EmUd'›Ҳ!2&Fb۔UlSQM-[q,0 O<FN-ېvC}ƴ^ksc ^ zN:{18F^4RxKh]qr; ыe=ovihg}WdƲ ?){w WlGǎװyd_ c`UHg aoKo̚=O?NδSE t yK+Mcs6 1w{*tȁd< Rj7ߠ#!Wd ^83cKpI@k%\u チ4>ڵ ~5lA5cCc(2/>"o`ڏ1,P<įNY1Ȥ+3'1{, '!Wv*e<{xНDh#{4S C.[{v'"saNA|]ͩ46NT ?20C ŷ_|867 <&/a=k*sqiH9XV9~0eF'[vp{jfl|X(/D$,#U7~7boOaʕdK^wq!=b㲏ޡ18 `Auk$W;XeqgNh0ߋ,rcr8xキwh)NvËO?u*W.*(.|kYő3.ydv0úue8xտ}|!}{C7# + !tZxͿc0CΨSxp.A×,;ѷo? A':43UB/ɂlLi~+_v\S6j}=A.0_>x7n `l G@_%W￰'`|dO{u_Tmf @$KIbU qpfVKߩl:pd)9Ǚ$JdOf؅J+z\u^AWCk.dZ#50,\Nذ.Ƒ}[H9M'V kw:hZ1qj$ hTe.eiHվ&ߪ]Y{ zYjW1uټH7FEVF8`UIK \{y>bGՌX^Jf/~=rP'_1r *8qҶ/')f>_ EI@]/A'}˫])_PA[EYOB>RVwuGϹ6\܇-+@j<եg>//!Z/d#@bKjk܏7X8g^xsyr ^s7R& ?ujiPTYuҐH'|T邎@*i0`pw-I!w6зZrj&K^$O-7\rA#ig # *ɣ1r2_P'+R`C9!gz؈j ſ,] C1LP 60]Ѱ@4ymH,2=PGvES~2Jo?2-W1G㠡[ QGⓈܻj0p=\g #P߸⮆cma큨Nqm4̘};f~72+ޘ3>bϿNxQ*)ƺgs7pQ o*$ٻJ5:=L4Ggö~=QHrB k7#Ɠ0cU &>HH~ /-y$q-(Y2^ZdB3œxZD/FN7xH*aN߀(/́'hN(UwΛal}wHa7+3x9zC]Vo~$%\à!Õ8H{x9+AMh)@9?%2F+{嚗!Ө=k(=מdMGD9r}= *Ȍ޽{+1Sp 5To(:ϲ;>&9MyXtvPRV̻N^h42fkw,z,lCqxC)o7gw݈<BWٳR fAyu1,h8.%3u {(#:rhc;671QhХ1Jnd&آRdM5eAIg‘)4_ģ>&3+}uD ӧY ևV h-![0)u5D.% ^ hJ|DG/Y,HID 'n^tJF-ߛ[6QMy{; 77B烔r`⎖:M';U&;ԍPVZK(a̮cƓG  (6 bMݻ `B%Go*d@zzW78E;#wP32T%o 9i HNJnwÍ@TL@|<(MHC~ 5a.AèЙG {k嬯[몊k; "㮅wDx~mLi;`ue sI.NBc|h9xsŋ}{+FHW;JZ_|F4Z8-fJwOdgaF?`6C=X{fw-=~Ɍ#chlc) NI 2KjH91A.z^njh 2> :J\+}H`\1SbĐ0\LIklzV7X|;bS] ]Pv1-{ܥ$C3caʥ:y\OQЗq,H0*31{av}dsyl߶ ,8 oG%Kc]~eϾ+( FPn%@gV QɓXGdcENEm8r txgW-IDžGGSy:o|CKa Ta՗J07}?w4.0zeu=ۯh2qL4W*㸸 n*'p}ƳԹ6uOT05]J/k *GQM緞Wsux:攔Ũ¢YN1,x'um8b>2S3}M IJAJ\e`ẓ;A{8cbvVvϧDiS3yjieK=ػk7(k؋IL\#Aa4 &s9d][מcN$B:$#|ӵ>}6oZ?ԕKytZ97!Y2涮Liw.yy ơmò+\%.&X5P-S zdo$,8G m YK`ɜ_mZp2%1()9߮ePJkKa⦺*WH~TkUC_N*mu]AT;M1iҗ6GNw#N`2h X,h;koZs0 wyà*qW=|Ķ~QPRaH#kJD :s^( j$;vA/bog'@(;e` <9z5GS:ga?j\;`xJw]K_%}/]eDUI9ײhх"}db[{\U[ڒљKF\-~_+I:޻6ܿP6^s~[>RRfj)eUaup[2>>oZlod)#94!WK\G%)k*}YrYo:FmsI4ph4h#gUF:bŻFu#'{`~vP$X5r5L7+ϽߎpwS8,e̾x#r:vE+L޼f0aܘt8@Zߺ.a[I`J>Nd nP 3'h+> Uڌ44R@2K/װ􎤱Lcp)яɑ^'Xܿ^$U>Ɖ4f\$I>5Pںv6>"Z1JI{!f`({zھ} ֹev{ʀ1(Eihܽ}7'Bޏ2 : {rpm$e=vwfb\s :b/*1bI4d/}>V傁{bc x=fϚKylBjrJ_PU3ekne-Ϧ"vʆz0f?7>Q+~9F@R%ì{ѿ^!h' .I3oSl(3Ʈ'c,3]Y͞,.1E}g!~eHBfg3шC$eFwd@h("0nދc;awPMUM'N!;p'ƎLFh6nފ^z f'@dΞ9WXϝ]I<,[`q]LYzq]#E~ٞ<0/>$jiec E&/$\ݕNy9K%0cFq^YY 6t+FF* ]7wO2`A krZٍ@[N8əܳfth"VQSnz4gaa8w>zL]]~O=("B{"`8E ƈt_C1.[YTᇋCY%j uD#o*'2u&ֻvYJY+)ztRȅw  k6B3ۊmbIwsa!mv"{_nBeqPPw54Bwt顖@U|pv.*Ư% OMZ WdžΦ Ʋ:i&"bxTH|8U,,!ͣFtCةXb_as2{^:x ؿlomA8SEI=W/D :yۍ oey"?%J9|.7cԺ}~t-֢$#+|edU\"Az@'_P8?4݌\!qD UKC2ж't=u"2Ah9_߫러pU>46i.cCZ8:swsAeM-A*:@MkO :zH:bqwƢNMfUWVQ|ę/MMчŏdƧ'g /(njXƎ4cZy8 +;WX3VwCjطH)BIeLD&gOGaQ.ġ$h\J봧'v1ꦌ}L|tp1#t)ϷiD/?˗*62>ua2᳨"NB%'gW`f5;O3̂7zmg K>-:)<7k xA]~; =: NwMX9DPvS"V 3وa?\oZE{MmZp5Y/#-.x865’wV֧R枟C^jR-DMuQ;.tdqSoGz6 8=%&qnƹ|\~ vs 2X$Y=xs$DbKlj'@,e}o(1B/vU|pyeXk8A:oaD}6<Ǜv >+ӽ+CpUvUH)[*(j:$~t:ֱ]?u!Nq8S558s#a8RGa}Hܐ$6Pd[곴e~Ӏ Nn NZͫh"Q+s{h;)/ iWMmrln*:*'/Mo)Uevv+{HK9ɱYRnGO[>:8B-4JR#{:Ar'Ȫ#K9ҹsN8_3[X3İk'd=Xi8Q@q*J! #t>+y1j. u̫ipӻUgn:;9VI&h+[su`$oQC.yup83F\ itKǟ-⨱5NCyw`(N%1eu)eY%r;0v4r:zQ͏ZЈOén?-d# ^j1ل  8S"FO . @Fѧ4v[3Fn*ë;+z)KN2VAMs;VĤ "}& K4fRF1=3덈!z{b<,1$0qfJc1Cc-಼7JIw*""X v;MruvsZ]EDE$o|@~}^;.[~XYfJ2 a{%N2O1flDzuK5!"67+X3X [[=vdEM˃ s~; TbΒh%@š`3Y"  qie&=\یsl'y_ ~]DH(#X@Z#ޣ}m2R],ŷ?|O y1,zK)1fy}PgH,ol1;/N!h*=$EpќӀc1!Q#j+VԄUUȔfXޟ봯C: D=C˻4U'Xc@ Np(Keq,&blo*F--p:u9( i&ADQm>ΧwߵY J0o6vgf`n:4€J?4-{5A-u|θkFhWbI$&/ߵ"6Tz0 %jyի>:OKwR~{wouVO}*+{ж.רa6~ܿ_p͏ڪ -lK~D # :pp`-"\gv>y: 6ۚm4W,* t Ycɴ Vd-H5517~ZSaW\IVr *m1AvЈ+ 7v›>;c#&L|2пL[LBqukH0kda_jߡў ?Y?M+9): #p "hIXJ umRZR`+pu%K\ML{w[Ʋ+u v41?x,>p|^~ز};CdUU}ox3Щ>KQ|O.Ḻi< КF=?|ѥx衇x_~uJП;kÇQDo+-YY ז2\zffj`nGeނsLYLLWa˶]TY0g!TOs/XزdBOC'^+ߠMzP雂}a?7 hK)h]҂A>R%;s H !VQg^ꢚʖV}p`=C:9GOԋT=eV<3,k%Ygg7:4ZHzs?n憟$ޤOû+-OnHs+mc7F gĴ l;ܦD1\^۲pCĎU2 |H؄$8H)cE C!/l.2(=$,ru̯d,;JIOuzF@ػG_{cԋp5tl($7 S2 ݚ;H f\'ռ*96b`&z`{ia{~ #tY6@l%Z~5+Ne`uˎ7E9.?,j1d "YVɉB(e<=G3cmQ=I=Ǩ|?2TڒSu쫴Hvm=տÇ ov;_0z4|K^Ws{%Ζ×?v~W,5cI}>l߾d}M۶`$nM w췹"xQ*kF-7Q|ȧä )(Jjs[Qah +Z,|qv25%g̎V9.߁!x1n8ikB%a>(%Tb &-w.DUx!=YkfgWrgԵt"=zOfluF zdrs8wt ьOGEVKttA0-lbi{ s CWkCa簀_e-q%{QQ6 {+OY__oU`}gy[-_g{ٻc3"ϭNNR2R~>G@IDATQg=]y i1J[)3g#*1g(xk|*}U_Vڋ*e(j"*0mF Ӂt jiOV~K!u;b2ۥ%5Rle%%0\eP]\i=܆lz3q.$(hs}dT$&e?}|NgHs%Li =5 ?x?zO\MyMѧiwպs;FʠZr_xAN4qr|6b:|{9`̹A۴lضw/_y]9|*N|߲~5j\ )59T#g8P6e$xt_HZ1V[N03e@PנK@! WkB\R2c2+?l2(u#|4N~K a vT#:+X(VԳQXH t;?C q1WL-CGa{F7u+mڋK/C = *ʽMH#;k ^A0m mʲ+C.SƖxgz6tc2w%Xsa?|Zݩeq0oa^M*G]1xBY[7^f`2S"A8(#hE)tacGhHpJr ]A\z0dZnA}M)˴ly}7PuG~m}J:w/Elh[oYFk1B5dsk 5Rqσg` sXGPE_TgMkF.u9*:_&O *B}CMy [4 Y(d?!pP=Wۍ,,J/*=SǏapȆ[mFR,Uzf`,UHط}ǐ _/g0ť%Ƿ%%$WkOdUk.S谞=Eޙy]`$ &}1r\ˆ!ؼ{fQ"RUy( › ]_ok}i z<2I75Z>6>TbM,嗟]n.4s팳b9Lƶژ0lPlvPg 댡lKha˭Ng'0s2ð>0o.;5gI~xh 8XaμTa1[>t0Cr*B@g쎝2M=#uIIqRHeXzHU۔z{[珓R8,M@VIbBN##?Pcll rvHGNIY8c#zy2ڄ3Amq.e&%?\H@7ΣAVcpovmyBc{֐^lmT@gr,8vdZ(kLA f Q6H@K2}}@$ų=ߥXH]!u;v>m*~M\s2^cE k˳$3ײWA0b=2]E >Oj^Kkk I^j I#_C: CIyܶc?Qv`tЙ S%Ϟe,oy8?7d- 40#ulnǙ Kk'wDJN̻˺١DZ_GrQ嚺i/؇vegNא+-Tgw KTk*"mpigęRm{UjιY+k_W19f9^)D+{GEݍ 8iO'Y\Xa*@Wh\TpG}}9Vl]U*WgE6Pl hRKE%TR2 VIB#zynVAjkgezӮnެ½ SFuyp M>H (Ն_UUͅRu1'`z@L;AP_^r`O+ލéƨdLRR\FbX_pJ2ܻɰgBpc̍OY}S2Z- 9Z5j ]!,JBLHIBEM*uNa ɸuݼ} `]"j:eHMe r \| ok{@V\Z:! [66BEG+ )&x۹Dƾ L K1``hǁ}T\bݲ̱מyrPM}Ȉp{ʺ!2VV&J!6UՁغXSN8g>ۨkEj6p o7>#cǎfGو:".X&=J[j=4ATSrڻ>*wnĕ&lݲœO=`:(9VKq~&.D[(NoN.N8Gv]r".\R:x?XړZ hi Q3%$D0ȓ)'p e"`0n ՇuAx5<.3W4~Y Z•viYEe$ nO'38)2\n^x?DUUqQx\ wQetTƎI=m9Box̅ (B:{o YoT^>sILj`~VXbY0ʞ;HҧF_tXGJ,y'ڬ6Òar*>;z<ş66 &qibw:/9*V%Q)224h@_]:r3X@0bU @[K^YظCܛ P}>qտNJ4 bWU5..ŨQ!l\U% ~{{]u?*šD R~j*Fv|I4p/?|(Ͽ*%>T5U8e`=')^mu7RubY ~hW>8YG;C+kb]t "! _2p}ma ۷hځɻlmqMN2$ Κ5`έ[+g|PTK%~#Fƶ=GX{31s*ޝT#<2}8J.E6-WmF}*2?kI|'psa@Z6.߀uS_<|`-K=SJMf/VU](/Z1f$Q2_Tban>o-5_]mp0ܻaml%hrp';-qzu-;PM.EmYW23Փbc9 kJJ6 r I,$u14D@O}'@ȴYEvP9(um;{/ן}’$P9(ٳ/\&37S*5u:nX.$ E]DTUޅ23!3A!_-5TV*!ߪH\u?[r)F9q-䐎7ۖ1uyu?' C).=6P#M0}hoy>(> 4>BVOS[7x4x4 <ķC.3/ޝb]Db ԲঀJٽsI(s(ʪm$sGVLe@x P=H2ˠ}wOɣؿo/*g°ە9N:Bϊ[iANI6/aRn $34b+|71)zxw0rCzVϭ $qTUA\ rPiQU_~^$yq@!hͻ4D܍em+o~%G-ODיXj3C*;%pwxloxg"ifq'+lkF6g2'q1r3J_IӔy\5]=t@tB#u„ |U{VTytRO+X¾…4WIr_ 1'1eU7 *;)62 /GE;  ,g]g.W'}Kw^mmł4 >Ng[mUEo/}/ӡ=]Dz!d0eI+H@%E]M5qK)T 0`6m LMlCy]nZRȫӥp@ {*&=/%1 S܍}Yr i vՊ ŝ EOr4R~$#OFFz[0ѭմS7TscFUM#Ibvy0ΗьN~U$iEQ)1] @pr.Q,#ީkRԓǕADKz9K<]5+mƑd9٧L}[هovHig巬"qNzvkAԨx!lzr~RP:]9< ; X:^}W"N% 8A6> 1jf-ېW2biin7 Ps']J]y1^•e*;*:košlZdġWn"E:1I0V*hXժݨTlU?JGvnñjrL:ѸwT m<o/SZr!'3FXmZs8dܟ`?n״c]oWZ?y ʪ /-21´4F[KFF;<a}(J.EXtT|ko_hL=Bրv\IqZ'az38UK{>nX|YQS&2ԃ;=ٿ-Sbgĺ)/u{g/LG9:yEm1%%_k%`.O*0Vo.Z#aĈ8} NGq0| U.TRuyj>c,[=a' w{ @EEgcؼ**rs6*pxZ .i GY7 $fSk1-gϟ?dvRux7Y¡GEu,Y3Oϗ}K@`P7"X2m$SglF2J'DE #ߊg.ոc y}T;c` oA>BةXߨUkPD:DfͺZᔺ>~* hI'(ˤB1 # &&臯?w_F $ $<>&YEp ?5#7~.̻2j8Οb 'V-_Q9}.N2 M%AF V΁'=1%C00%91],31/[Rm˼h|{U~ң+Fxgfˠh*{V|N}%-ֹmX82ãfB.jLб/^8ꔒpuVUϦ suK؞_`9\,>s](u|9kO$-#YEtٴXe!pUX;1݉a#dbs{`ss/u1Z6 J$_k\:?J]'0眍2_} ]w1WjX5.Sr`R: K]^KX|CFbpʂV %%nT)j+XmȐ:M>%O )T%\a'Isq⹉ZsSƺSqbv\g޶,n;N'kCotOq_gNuek*J0ooT~*ƍiE`_ϰMRXXڃ̫oGVTI)IGcP/_$&ҊU:i3W5AI;'g,l7Q^ۼXj*8RA"sRQDt%*<ғJfZ!*_']MK8Mh#Z|yWY#wCv9ygsQפ K>SMa I#SFHേhKj|9?\JWKxcΦ@'2-24N ƫؿ O(>^`/nͰR?-'iTق<\ {M-N8 sa;] l)MpNDj9I151-({7"96et>cΡ(ĐsT5),WXش4)TN\mQNUE[[)vY/Hŧ# LgC~zT>^EsDY+m.ٟ'2~/E檭EK\^?n/RkUMVy@U[XļU|CmuuVDV *u~4b~1m|?6\dҧ!C[:+VjEAjWDJLUQ* TX7[mlv+I&DԢtfn4nTFi(Ŵ".vG+@^%9]5ݳq5a !ʐHsl/[MoA"|j1W1x+QŜKCf+O='n î)_do倸$8Ce/y>z]IՎʯ0| .*#̃Uu(6 t7A@eO6Y^=^έh.O5h$}VDL5N'0 ֯#(Sz2A֭ܘ@З p>%Qs"u:4CA-://wg=BRf!jSWtrؕ쏕kyB Yzr"Փo#/;1ǯ*~N@QtF3Qr, 6'#3=֓>¤J˴kr<60f̚Kbo$aY*}zc6@Vfϸ NӣJ@{7WLh7:OL" ZrRPJ1^` -'0腰BZam1կTU"PaK)-2 y:}hl{SO`]T5`͆-38 L=֣ ƎTD}#vb]ӵO 9\7]&޾wl2͚ls۶u]>֭^NhA9"ÃQc'bւ, pQڈG#QTY'>`ol \AdjR<>8wM,J( ~q=B(YU$&4$UK}۰f76ك~`DA)4Xƾۙc@6*+cl?7X)l3J1lTe$GVYg0^n_I$tK4)?~꘏3  j&g2qݜJϲ33BwATyg5?|QӐn ÆJ4Td4=$nݖfIk\l{05aѶ\0  `>s j]ꐣv/B9$Hƶ!+9(!q&}mhjG;'g*@XxcϦ($sJI(mQ"SS Tɱ$=A5>l .jݣ̼5$%i_]ī, :eݨC#=sz,*gXG;:QhSFM,IW+aY^ԑ8ةD~ DiF +:F05FmܺŸ Q6 .ny=`Yc0{='O&`!x5ڢ3%Y\hcKE1cE9[7J1sAPEkJJ-]}Z䥷CsB RF QZLDO/Z]ׅX+.DzJ+Rx-ߢ5Aܼy3!-A*Mq|Y v'`үou" 'ˁ6 %a_e~DqۻHN!96yh#x<AD闑C"n%vHՅ>0Wd>$`ص/n/Iinݽ~e_1=XuTl4浟&ZU_&]-hjA щ Tpg acj=^.2 j4ܝx8 'O I0qqxԹS- CZ,"QK^ wb>8I7hSmh9Aѧ Zt!f͚whȴ1 /?}./˫Nn4dx~C0U~w>/CeoYi$D]eJm~ G*J$s v@VY3]|긾QϲKA[c;Xd J}%YQ ($ 5*դ-'T?ۚm2ICzz4_*M;jb*RyMytkXL8n2`K G N1MI4tFĤo5%?qGU78AwD9{?>_V^zc iP1X= *_׬(f`['tHvj7Gs> udZtx7̀c-6@ ]lI4t(@RߨB̶_$'<3U”zP:3 `j^{^+]u;En8y=6;ٞ($5m/ ^%K]۞ھ˧g`R]&}AR%xr!jX݇_~\=fu lOz,ʏxPJYyk?*rjkRc}nhY9&9^yi/CDNF>?s宂 ({Y=u_rDF+3Tz @̓kwvbAʍ*U# )XrQf%Joڀl07;do:"oYBf%o|G\ RVبNMud-E9Eg=~۟Ri - \Xdݏ-;u T6 u0/Q*=3腗2e4jœ/W d2I]~{xJL3g`djߙo_)6&-~}n ,5CG"0x3Flm^Mn,v%Ę,W;G{w780qR 9Rfq< eZS16?8y+/|9.\!9Ka޳ l<ـJׯEtda&Ms:pS!lw"k@Tl/kdaĀ0$2iKY$ۍV ܽkv}e~S$||i5\Q˝|\_SЎh`QTX]b~ֵ[4F 1uFgơ"E2ohϪ Т5|fX^S*58MfCaǯ'1h>f(8t#Gc䙴 53;},AbqC5tdmBY0T"yر{>K"F©CHĺ8^wԳl@9K~j5vT8eby |I  hVXi?!ɯ'X]]ϺW7FQx@+ VXijdX;pxPW?OFѳT NjٓVO>Ri? $+<gI<ħ҆y ~FN~}=iإPCNǾ\ۺ_:oٺpLVE IԖfc91sa%g3hBʛ1c`nޙpdnUQ7 98G 99!T6JN쇺]UQ}u Y^ c6c|$^FGoA\9G4S~]TA/s* d] (>,/5S0LG*.L(JѝA ?WZJYqT1Wx}ZD%lf`IzM+Swc#d|5zn볞,haC)GCY%9hIk_-5<<\Ls*ŁT~Si[NYZYc >39 | 3 KJj`=$٘8`@;u{tV+h@HɓTH^cp__}hؤD|٢`=B"Tٳ_M#"30w\۰<Q`un7u sD>:sЅb+gY];\@\}{`6\{g.P 7 ի<*1 Έk^nڽ{?Coݵ~?0>;͉TJ*!6:~>'.?s@H抉%;HD|NXlG˟[W7Wd>UrNeoϭGժw4bQҢ{7-+L cބ!Te3Vo*jIqوb( 7<ϹW_0MhQ~6NXk .8ebLqq[de!y'+@)Ѯ x:kb`t/h]̜<|6njgwNa a;kɼI H9}<[wm (&om%Bhھ%`D ž*@ZGKYwfb¤i ~_mCI ֛{wSqE96ϋ:xԘD*R ^j͂Ki$> Usݪћڎ@VWUU^ j?N~π*(U^ 7ږԑ:t0E* 1>=*Sg+==>tm %h/~v9Nsơ۩:d8{RAUq =3 Q`EMM`/,t?v짠AϚy/rvF7gGz#$H$f"|LY4GUq> f RK=Dۇa#IQu@J&;mbiO& `O@Fr F eSס=h.͗}DTNcIJa]m?t#ٞy a.O؍$yR!briɜ׹uOVr L@V!cHT).ߞ&)6T fY@kw?[1.u`kfl83z-8j!8nV(\0wЧ} Ο=Eq7G]Gfߝ0$` 0jpUXi?˪KFt_f?Ye'E]ƅĞirBKuG_' kewۑrX(%`=oG*˝p' ɓa@xg*-t64CW 曧')8(ӵ.7_Gl%?:O~Ư;C3}+ jŴ_˫oKe5_Ex̓vHX:Ol0¤~3ˣEߺq+j7T֙Nlj+~(h B9jX&eŽ*朂b2'hnNT#SH$@nL+ v"٫ǸU0kD=wMNGs'b{r/Ř֫C3 ;y f&8q4"#;E5n-$ Ssi};fǒOCFST=z0g|ZŻb`hC]q&rHC4Ï>DіEbEU}iq .>^(.HcxHS;ove ./Tť͘ǂ)Hiw᧟?kzŅ Q AB^TQ^ھ)RZ w#nspI ̙3rgΜkxM 56},~?W^MWY ,VaתeL9N˖&C*Ai.VU+$@UqSuy p%S)qU,"SYp:``ɩf}aSֹd;rI›jZQ۸ #]5@ej/6$ޯ^6#" $)B> lt[H($.ƶM{TLrɽھH=XHU*kir΅ck-ׅoKp[6FZ<f5jYA͕s`mSx٣dT+>42jmQ/em-2vP[וzLyPv9d8d̓ td/Qr{wVļ$b"@5E3;W"de5yN %[4zx|5TXv Feu x\\qb]|2jheӒ 3SF 2Sձpa0ecDZۉ=GÖ#2}v%Kau%/EMB&sMLJ42ڋzwfVj5b,%;{79(OIOnڧU(GHP.Fg)WWMi}XZF1frTdKƭR>K/%y̨q +cko*aIqK1hJ/V{3]xdoہӮ]> K^yE Sr.ٖm)4%ލ]B&MņR)G;]bUvݛMw{foay>;s 38vg{8+ ?}zc,wı}Hϯ t n8A_֞sl[׭ jᢹ3cw0x*~U_Àc%6\([M63)_]Vߎ39ht s|%!%= $fB@Nj.O96vdvFз Y`1ji]cic'# ^ZI9POX⻯`DCk>~ٻw' KKР$xP>USt`YW5^wїN |p-]G:_sV 2CVO/oe$Qn֑I/WV@?ZӞ.b߱`Ufh{1!Zs5!_̼s|Ղd{=֊ J[kʌ#ĒcM]Ge,E@$,ۛJB|7˦<`"ώLj*VT1~^S.TK%']>=f;d ÍM7C؊^5 `'Oؐ gPzfƟFJy蛛!!1q5䘩c7Tҟ4> NL ޾eLr~(FCNtTudɺqfp[UUֱ'cvzAja_k:癷'`[]{#C?K;C{)d~ز_8>L+ ȞV@Y}Y;a ;3-0ӧ ~E ;&L ?>L:N2[)I&@齩WE`=zC,L,~ =6eTS@ʇ|]lRP/,CQ;-W{Z~1<#xѧO/E%޸5>UeA*8^m"pjPN' {XbFyM zZXRĀqu3Im)Pxj~1ޣ9U*3) 2]\GIN53+4lZ4c>M5=pxZ\XeH.U{0B>.x^Oh"V㝜<8ۙ߶7Pαc scD e^#FOfT(IPUR)7 8.Q;#9+Iyw;9>S'_b:%<3qjK,F3 e*=KPҝl2'{+|@1 TD:ׁt^m!<8>q02Y\m `ksE [;7:hzxy~,yNܿqc4Tu*(R-,LwV)`\Pv 6֩$C U|j|O**^O#%'=6U n~A-;:W)u}r+Ä_/?6'qGTU6҇j#mj?q_T쳖=2\m`eRh-&_4q.ndҞ}~xx0_>6696#G 9Σ‘D uQqi2* 9EeAҙLZj-!惚"P)&"hBE ]2mJ rlH 0?n<-*Rw%>PK0c%W`mxJ{e;vBs]VϹJQ*?%Yf^EKTw $G<4t8v”1{?>+(Òo{ L0G'{$ֺÚ|Jefo7/׭åw0 L/vƾsvd RNOll^=R qd9PI>Q'€ruV֔P rOữ~X[|y?NTIDmY:vnxєNfVn3 y0enT""ϑՑ"* [Xo۽?SYW٧#|{H2^Œ&Fvbo`pU>RpYjkXHs T-!dtgR2FML/b"0չI@`]7Nl˶I;yE&[mFS32Wn` mpRk^y x)u޹~#KޛR9 ^)CJt8eݕ-[6mAFPX߅ x79aΞ=YUV~[ޔ1:e_oXg %ʯ0Z\\=Џ^/.g[JJxj9]/?6zqtexJ!Uyk|o.Mt 6-ϼ^d8 +R߳'6|ʾ=Rie󦵐Ϥ `Vs舄j9ȉ2"UBϯ"[v6FE_@{GsT,7™2me"$#tP2{a7 aXؓub8ŀEYK"$yew+r+8d݊B^ǣxtr>L`u(ɹ̱H=^Di?;'AGF@X[dVAPe L-;[IxQˎ}|+%dfT̍dcY !9n0e1 *ȃS'υk|m1Zb.DξTTX[\n~=),;J2F wGh{.?g^RY2FZ1=Yo| [pZq Ӌ<K0A^M c#dm UݘVx)q :Kr7){0PRqsvGlwN@pA19FSf $\dh0tPge9x6·31 *O## akk+z*!T6gap%;sl[xv'{kסz-MSz-wrr#k#&Ħ)$a&B3"r&$Bfh5Ăxc;Q^Cf Rn,~As\Dk2X}|د6)䩳 1P_]@)E*f~=-@םlW{TgId_IQ$~ޑ.,Ffb _#ܫ{3`]CR ʖ3IVwh| CJ-|2`AY†dF .ܙ#vX6"?5hNoΞ-^n]y3!p̈́>x;+ۣ)ySeM|mn\?{ .o:skx{̲g&SV) VXOКA];#s JNU>jd0`?.B|bW8ϤWƾ?zLL_o*,]=:{QȂdJg!2'Ϡ#Y|#xχ7^©GЧo ek;%kٓ{ikK5YGGtСEڹѮ ;a&5kG?u$K kkx2Mp~qIgQ,W>zSj@-/7+fbuyV8(4=FYmΝ3##%|%,|Wsk0lmX+%}وa3Zq{mߵf@W&E6%&$R*(+яJ(ZgNy?`qu\>yr\yyi0ӷR%*0D%6~np#SzB{TUUO$뼐54[E ;ƈNcPUc2U@;&) * 5޸^XoiKG]+)ˤ5V{,znXO)kѕu8ɣw#ERG-#Ӻ-0jigX]r:55V%HJ\1dR _73Ic:7-{xg}3]dwWi[ˉь1lTqm<<9uehx㞆 v h$E[WƴPHjxe2O~W6DUXYqUo> H,u{MT_Xԕ1$0idO7׎>ViS6yr?*2*`+>cӖ&)yIj{^sLmك_Go{u}9^x̝ ןvn^ ll䟶gk!!n'sd`A0[nvs\\lH6QwyhjAX0xBǶt2앟W5f~`cӁWb ~h߲"bpfǜii)%yzR*{9%66x)c̞/e )k(6CkX50laLB2a+o`:_Fe097r. w5&\9_|bFq_ O٧N\zkz0S㿪UxSJo ;GrvC3L:KL"8\z>rJq]ѻ \;'Rr(BiV_ eO̜2Eob`;]|ddad g|ACSk6 PZ42(\`Dtz1=$2}![u$;Vkvpܴss ]^B b{''W + B"IFh_9b0ɭĎm[le/y< t!K@ߑY{㋰z$h$ɔ9|'ɢ> t?o+/z"%+߭Xu? |rt`l;'|' kގHDnY 2ϜK@Qi\p2<dpbЫ+}C״GG_re \څ׫O_|շȍ`{Oy@l93qdX^NAZBNҙ > "<]Fĩ3O-%5b-%UCn_K׮ػc3ayg4zff6ad[ 9I~tS6?itf"o'+D&Xvm(sHNDbAv=ⶳfI*3{x4.ٖ RχcݟDDg_}(jcmrRg1auf>k#: l\՞gMICw飥BEyՕ|~;u:m .IJ'jϵZV됑MԶ`mG9? CU@m?"3=TS4)|b-d%6P7N###8fpd ˰$A'K&x1saϊB;s9kVf`nGc ^aA'%2~WI ȤBʨy Б>{dᳩ_)HHJ |1I?=+gTS"\?lctqE1ԫJ'&ysܭ# љoKs^]Ebm=(5˔<: }or ?QwNRny>D C"y/o޳FN^&HY#%^ZU7=m `!\`.*J>&"lJ'׶RaMX+k@@|aA>FǙC[pax%;#%ca;r*1nH s.EcIۍ_U0sAQ:.o, y53-@LRo<; 9h+1^tL$Xy}ȜA7ݨTUsقU>3xFuHx9&HzLE +M4~&++bu x`cwkJ1M`ۜ ͕nvS9|gq{*41!K-y "ϿW_>X2YV 8]fp 魙A|7-L էWKnu{O_|b!l/~XIbW "_ 9Ɠ`áio0e3ߓi=,)f <2#g=\(avw&κn* k ͗?쭕{z g0ktz"_–`Ic?˱zr)w 8ٓkAuCzE]I )Aw=67tO'/"4 N+|ԩt>x,yia.Y ,N3Llg |)tn6CISw/< Enj@(;;:u[ܽ[cKme~L`t@>Hyɿ`}wb_rJL9x6C;}2ViT$7*לP!nd*aLp-^Uso!|wSQ{*p, ?f)lH{SGu{TT6W|p,ph=Uߣ1ɺM 3j؈pq z3̱M bbi"AJʯo&Pܵp&h VpŜR#%Q6ǐ&9H.Wr#*_ߓqhZ!QH.cZb0 1H)2#+4ףRIDt=ЩCXPqJ5)@8?/E7;l%q U>Z]\|W͵Ͽ*g+rRΖaUj7׳}"u,jsE]րɱtj46RS{ NBA M;*Oyɗf-j!/)a:p -}Бpcڎ/uK5fi%p"kok/"d`` ,D؇^1@HpaL``1 : 23cpҍ B;g̖Ϫ Y:z2kҚYAO W)#[V~#GR^p=QWgk PaJp>)I VN1F0 3Ai]{ X,%3zC0stÁc1̝dz|TzҰ\˕[;pOhOBMu-6oL5G0;PyѾȁSnPdscvEfa9R}%I')\frLM|3S>A_=ud>YȹԁF*TJ&ǧc'8}x;VioƬYdHkw  ;)9slvs}|I ܺQZl9X0`;)<=Zk=; rܹ гWJbHXKkA~ 5 kL^'{ DM$~0aCdXb˕NL4`wk>?W]ixif+ ?_j2)%3'_: "F;R0)?Jyy (Ú//+qNg}wuHSfۛɳê;ob]m?QoTV9&G_>*,erȿLkz>*f~ڵӋH% RyX!2. sQWM5v0l e&R{+;̬m ęxJ h[&z9^65}>ACG¹P s/+ҹ.Oa;7*G1:qw( /s"_XY^KMĔRLG@tL,>{ cRahyg9`|ïHbHEe=v 죶o9J9+'X, `Uuax`t&Jzuar)srv'w ¯[G?ftli9e#u6A g*} wz֣ 5G(mt;@ӐMFhfdjS'QU^|OOSh ֑ ;CC׈ X_~=Hw荁AA8a4Q_erj(ǗB>#T#B<D1i{w{?Z&;pѼyJS#޵'Ɗ$ُLb#嗟V#QHt*e&\E;sSڒ2r1nUM/14R'GWj Z{T;/> |;Gᱧtt.ܽiAn*" CHrH~c)S{|40ui ԒkđFf#ZI}پ6sCBJ@7&A94xl9RPp`͵X&+GK|\~-/]6*ۏGFv"tj΍cNNˀ)j)|FDr1ZTÊެU|G =h&^)¤XT*׆L17z %ӯRg;])D FbYErʘ4Zʸ1`ҟ3GRHoj@DԔihyߦ<[~/^#טyb"{w\sjϓk kȯG_[{h1X) *Vվ?d5`}INPE'SQcj ^BXRρyi|glC;.̓=_g"S\r.3^*oim;񵶽/Y wgm:he\Λ? =Lal!C?_jnտPoO-)5"94a4_YO@gfV6/ǥt'+2iK΃uHHQGؾiD#EEϝߎ0A3J'YK/bav&|B81`&byf"9#& RrJpr aDf4wޅO>.*+`2u v¢J:򉧑Zi %3G'$9H g!YbKG:*F&EÚ/jQ3N<n5^ 7ofݵizVha |YkߴPׯߨԙ3g jW89pٿw2/jZ˯?u_R\@.]JOn@IDAT7?ý-XYo"ka;>[뻥28>S־[8GPv6xq6,d_}x$ӧOe*-y=w;qz "~lY|}lk۽egO/J{veyRBfؚ#[l%u8o{Kė0&/ 2 +AȑÇcҐ56Ǣ/0ӕ@ȓ{xCTlÞHNÌ #{L]:kZS S7ͺzqPzw۾8{ d\LctqEXJL2]L0ep|ᴑ(籗(Q'K}SA[нW,srJ%xx_3ů;,?31c?2TzQ9Pp}8bgODDǗv/5j P ;rTUBÑ|8< K」O5V%+L(ݑ>#ot_4Lt& dȩ?EQx׿Wߦomw~yۚ{ ӣL5?ukлÎ N=qnsl.-gOFlUHrSg}{!C{ecvLhYGgueo='R{I2l'HifA\V}1͐Beާ8m&LE\":V7[d}Z]mSs+0#7Sdu륬=k뎲rŚ'@0[jr7t]܍deÕvh'~ o*2C_(7W֮j$w7Ow9nV\5ꟲ6=%kog1$`ܝz|Ϛ`w?iW,3}Yo4W3o4᧴éG=pc_%혼1q^K_|TqV.3cҊ]sWiE6Uf]L+_H[ncmlWN>zAl.:^ͧh  Ffnyuȗq?҆gŋ׭ӥ ~$ޏP\Z߰oWl\/W'J,FN8-~26I]9տ?N$dWL۹ËKC"c`UMwi?ɵ-HJ6mK}U˻E}O|U~Ԝ8\whͿ V$M=I:m,O~FjZeS䣬jՄ/ 2 W:.d=d/s NuL.Ô˃ /|QTe0r%g^TpǝTV"Wm|R~k_U򿦿:_cEb굡^GMME6#KSӻL#".c+ j{I:U}д^Kտw2_@RZMޯlR/S~w<_7ؠ+=ZnD ߽w8 o.gK6[dkw-YV>mfcf ͬa6w;zTﺟ,-ɐd\ۂtˆQ3@E$H5#[LZT*-٢, ]·Z$kG>^w\6PHlώ^n Zb_؟@_gyQ`Z~4BEFihzL‹ d.Q{dIFV@s1bB5QAVI* e1 ]0 \4c{!f 7bZS_J`#g?_D77&nj3,%BQY_ʎڻ">8̾/W%l1u̸평&1 ]u{x6kXQ'02#.S~/ߧxlQu#/{ =hP_)_mY75p-Ѓ8ux7}9%֧/hiLk>e0ŵMZY&!ϯkF-)S*YZ_(v#iY\9??wi3R nBB^dM2jͳE;D]WQM^WLз/YB?i4n)諬~B,AÇ, )i>xj-/2v}g`{3ok]]g<Ą82ּmwJ=s _5IOB%.9_Fkufkv[`EueL gCqy5Ŷu.$s/ {!ЮCaFCd-fB5+TP Ba}9}`a0ʹ B@#.!΀.PXq AakI$Ƿ+ ƖR¯oni}HNa%6瑘Y7\N6T4b0\(;HR$V ?#=}m5eD{̄gUy3ĶȄ(]9uCo^|& 30Pk,@q$ٿkĀJFF. Ohd6qR YVd;_ ZOUUm#{[2u?XD1G2 _QhXۺĤ8 ?g"uޞXfi5#צZc%S1OFJ2τ5X[2DR)&OK_L|9YHe~j~r|瞎I§߮FnzN0aώ_mIhkV2BŦBcMʏ6qdu7wv46VjC)Z=*Skё!q0;Y }{#+LF98&xDbh^Β+h1Qe1ƌa T8 >_ѱ{_^M.d4qP$v˺v. =g C--> CƐ0E.&0#o=n]0d,?c-J^%OkV7֖E_is j۶rwuxFT L2X3T+|^ey| \߯/|xV ozv߯e;L&( gG{cF߁\J2Փ >MO {2݁.mOs[i\*u*XU٪&FTy򛔞7OR\OK֓ءү "0Lr*8v!mJ 巗>yzfb/nsOڑkPǖؗ:OQq2 ESk  w/ FWGu-Ncm5akb8r9}au Ԧ "Rum6^U#؀w*'N/~Hs[nu]n[^{hⰷX/p64c}nXEkM+) bW#W[ %az\7Jv|ұS/ Pv;#ǩ hdc=pH셔HV:mrPq&a9kf~ig/:"KZC,n38U^HKKלt""6! 鍜 բAVfE)=ZP{Sٿ82e?ߘz<}2˭i@oMƌ:2Az 0ҍ`N#BBNSX$ܜ, ݃XD}ya<~8( f\_ 0jǀPwd$Ae{fd}/d]y(LRS > yFoe) o=M:Õ/`UV95شy+ߏxjE9`{8X _^u@GO?IO} =6`ʗ *Jq;| ;pof4Rk[+80p(k'JMbBRsZPRTpFk[l) w>sT ղݧ)+;'u}raDQB ~ڸw͚ugL2s$s׏\IשGh;8w!CwQRBFزa پ39&¢|F7Ll9 ̑1dAʒ4VRR&VoN?0D!rcQU&k p"<ڥ#*k&Q< (ae+K捂 Y+\i-0#K=46d;i&ܐͱGBqtÛ70yL%ɑd* 7>.x(=:t\}`eeD]8[Tm;΄a٧5 ˀI -A(ɤL\<|m} QĻg`jQpO@"v#u[7dR;.)Yi9~*פz>Gtd¥>Jy]GZ~1 L yRv?r>':1h4ZӟvNœS,D>@f^]W]_!^JmQ=uر{ @^d`AaW^#= P`F.όeE$4n/~|*S()6wŦ-k0>\L$k}ؤ 8~6=}&`emNc긅v=J֊ХSG$]Ns=ǝ2ix~'x7g.GaY a6Yg:xGiC כ$$ /2eO#ҕ8q4}Lѭ)e=6n}Ҝ1xM81i;$p\=}Yb5%;=1rNöL~)-) m U"07} c=Mo=Bquve &5,+3p0ǥ'm&NڸI|RT\xW?rLCYU4HPmG @{&]8v!d|uMhAj\Q]G*@Lդ5 S; 1Ҩ) |)^픩e ^GMȑb 2׌4%TioW_2QSh2H5B1 8}_zhú/ѕ-F혌dJ#}%~8=\JiNa ̜6#IV(<<]kEl#(IRn=94.:|\9l""1.'CRR< 9 |pB4+=Շ}?X9t'PVu7&k!ttZoQ-w ZÕa -+\YJxxW~.` :͓]GbRr׵r_\nYY?aԵr\"/,xYUHoH,~s[<ײ_jDkerȵ"c)hoIBQoz;΀3aQ- rfgKlٲH/ ln!q&{o؉ӔJfO?y/+jVtATa_&E96BߔåU!kuKk ҧ*A#kktETTꌨŭnpY S0@H&HZ8 ,V =6l7BWj)VSd#{djO 2R`caShH(kXe9vow+k=5oBͯ('SגAho'OMi)ozS6x'i_5/j`voDY:t"HгB;^dZ^l@VX f@Kkk3tKƋ/M/$H3W2\ 202fdm\厑#"aCjm3N'3(?@?4$ftؿմuגp4.UǭR2J"];sbsxއnJM(ǩ %9l"Y(.(GEmL)ԯ; Y_ܹsxW+md_avқ#bJAs$̕r;>D_ޢ? M4俏0Myr=:ҩa;rR|;lKGP* @ 1 tiBY غu'.;VUTmLE!n82}{?}9Y(;oDzpú!(3mOֶ#A 2V*0(rNNP |1fvYM QK#626drz%.UA)[t :͹za՚UYˀ\XqwK4;Sg~wL,^N$?>>m%;jrPVY%PH}%vٱʤU_B!cۦ ]RnQٓe:YXYA{u*R}:5>;kW0?U:D ]|pr#Fg'M^ [.Ot>?@~):>Y^@߈(\zd<>j%?2uAU]o֔)HK>_A `~*-1]ӱl~ɴ^{r?ϐf= $I$֪V7eßzⷾr:]%L;HjnFOomV@V9X{G"jbX 6Qwl$O!(eW*qs{Wq-"W`r0%$3QzըR|?]gap4&tɷ4ʨRRG59A`:gf%s`ZgLϝTg:veCEy005yC q?!=jBrEʙurǙPB՘ *U撥nٟSٮu䀒ƥg˂GabM66<£U6ԔAgذ%ۅnB#{ad?-<7|A-DOi5 @~NcW&}"IX/Bo%tyU48 頞Gh.1d~;Us/5TDc&)NL1 (zᣘR d(';nq66֐(HPS\lI>n=gh*>a UhKhrĬIh*օ|(gZ 1ba}=O`ǖ݃>߈=_ttާEs>Cgs@(:ZۻŌ̃:y ᶞO<gZ8]Vg̡H`y9 Bo̦.Accd^ZCmK`]5c<97:u׏>D,Nՠѣ/9/so6?rʑ9OC# A Cک8;Zї`~&՗_Ç0zK]Ecf* Kkj>gxީ?= `):wt'(,TO3&g[Ji v4ĝJ;T4]3F ҈^F@+&hAxztSfXs3+&+T'Os`mf&z9OWx"j­_[٧)$kU?avlqNlLEc1 _|1\m =EYh0!u'o(N'KVcmLOϵf;!@'fJ]IvH2p(wB cxbȭ3Jk7OĐ2 }旗 {^1cg¡ Dعjv>Jk2TZT::G]2zt`Re1ve~v'K~7].ӨMä'6Q䥧R~HKKVTs?2ϔ4NkG.ْZb?.$ѱKވΌp9ރw?S2**+S2NPSRla|4?? ަߴN; )jY+LC| u_qdgzb4%3;.g`ѫ;Y0a[:ܙZ '+V4'Yz;Wr^xlp:Y#SE UEJSsx9PSA՗kvWʎ^$S΀k-G_7GUE--0(2 BtQ eYtG˹"7<e(=an]۷qNyFܾ#.IcJpe?EsmXe\qx'Ұ/y/GH#&l|{#g '=# OơQ8F0Զ{p1.׭]}*f3ntHM2{:.tԖ,Ēћ!WJ-ƃwL"o323;&tZ%;;11OgrK:x恻a"}OF5UT=HHJپ 3'RV~xL~}X:O;›|7]rA,X7<(Cלv?TI$z oqtg+ɄaQwǜ1Dvh[[܆/kg0wquݷ_(bi @5 PvП޴'dJT=( /~3OjT=Cb<У/Ng0|ijn7*LEDCDPpeqw<ۦA%ģ qZ3Oi%Ǵc=ѣfYw_3;#יVXq'ӏ+ VKu݅:sk^sprW!+y77,Ն|!ǘaoFlWxڲրo*A˧eY;}H7o8הumPq?&ݜ1mG Zm)ř#Bk';e^S!ൾIX<7cFcOB{<ԋm61amԅ kpWjB/r,`ij# `Dg:=]Fr?0g| μκFh8-f#MzﹺVOVc-հe,?~x\mqgLy'iO']gѕ6nC/Z4wU}<*ϜQ ٳ/1A\l5gr3>y v:zUǦ\ )U4Eg%{R-εwu*븆/C/7:vXJys3\51YEWd7 ءo칪wal tJFNBdq,c<؉ikG imi=>PC$K$iMif4mYDQѡ5&(X j hAEM+%܌) 8d 0L D11 Jpؙ@a ,(w nǑÌx8_xc&" q|( Փh{M)UKsZlø(ؓ1qI2#;wm> ;_|9{D]n7 QJ>FElB!YPpn~fLCNx(=M;s~*ZTcIzPd@1"֬`{?O߯<s5NS)>L9f0CaG#8rRqL@Y7SၱWIm_ݭa\޽Z J> H3OPrrj=N[ӧގ鏌0o Ћ k{ (jvd0WQRZsdjK'.k-h :wqwLZ=sS)4a@9dKK ;)&퇸};N@锹g^3t@?~.xO hFa2;' EW`& rQ(1Gzf#VSƍE^A>k SV982"jp-v J3۞POf yaG)btԀJA~?Jr=cm\\.ܷa1B+cgAx~ԋieLu\~8$>}킮!JeG&L˂l<⧸c)JtOwrXH:_qK0PdǠ YK%Z#e*x.R' 7ԟCR|"܊Q.CiXQK!vqD9=X^ǏkZ̅}(3$shwT[[ѱ8՞UnZG5Q#ǴzL/&#VxP)0-%ެ$LO]ȅ$99&&^}zcԋ 'Wp$L]yLgf)E쨒XlO8 'ѩ}jX jPTޑQxdV S>!0C$OqSPii-؟x.d:2&+Lp*=i(dsTEq5{tEF/ c.5H}u!Nh_Bҡ 8nh1/GbF)BL >l6\؟5]>U˪]R*[ߕnԽEܗ<5̕sWPXH&nܓl~})ƺ ?-C ΗRF&v NFi9ztʆb+;Gǿc ~Cߢ1.ycO!ciŘ75!! ͺM-F w1!4,m 3(!om)b"+&, CL AHi,f<47þ}'ee<UVWVU?AOOҒ2ʈxԜ)` }#|Rk})Oo&pUϾ,6yGJC} Gݯy1ש<ӛc'zFFJU 1JS+/2zSgN(#ЊqMirX` W;ONgB tZk)gM%na4]piad=w;EZk\$P*[mGL[GK3Qzp.,,j k1]6<Ѓ,2b9%gY\Owo54aL_24(9wEui@3e`c' :ƼloVb `YK^, Bruh֩+!^;pm' a[i|_㏔uqU1$ 3j4r6!d0H'UmrHOOyep Kr~q6>|0ܚWYh q7rts}Wj@5}Qv5h nl:Y3 %B*R-A+̖{xtWkS5Dj.O&l@]cy>a5TY'N:r)u|Ϫ<UYVҐ[R%rt1Z=*~3ckE$J,vR>X]j`Ͷ`wR<`bm 3PS̔2~ɩi(=?Yi'oVɥ95θ7 Fj9e0;ǰncs5wcHf9 $/#=eR!h1=BEFÕQWNFxVBƱ#g_|cơ$Fc)D| 5. @$$2΢#vZH0t){o?y`mx"KV$r(k C֣io061ΰt `Cд쭦QHM;:9}52'N7W'LHػNdm:Zek ئquW`߁hX|el cƋbBRGk8|8228s)e  v&2(ل:P2ՑɊՔy!=}6e-Aed"_OH PY,ױ.+An RtblPkGW,g@+1F>Vc8p TY|^ 3AkϿBנ(%Vߗ|>d kd[<[cxw DȞ-o2ިajjNc3٭P09y {Xd&z> hQE䌑̶=L4;8\dhT s_}1YFvud4csJ'|zz淬w ͭ ۅ FXqt6p/]LwFiqa<ЈN{XWC4Ta[QNqCdt v]fڋZ²c Q: |p"쐓`nm{6P4n= c$L7$+hg Pן1WwxOt%[Jg:!%hc Js'UfM u s3 ;4PGΧdl]F̕ )i* sb!w1$6 Ʌ_ *^ΜuM,~c]Sfe+Ω Di{lڈrN+S74)0A5/I.'MNtRƲPG n>FGc:kzLIEm_B C>B~1fxP;2$Ok..}1.e%yT @^7`%d[Mn8Β _׻EFe9M06t":dP%Xg&f6nB'p\< צ}쑣vtl͍adn/d$(Հ޽?kO 8R("g[*_Q \F+:Fp|x>Q̱`:9hr6Kx7⋗'c\ /pLZRf<&FOOǞ2|e;P9@N=뭫ۧJ0zQl m QWebWKs0)'WܮA~fWcԉorE%hl, IGǸxC}3::``/NfW×Dc1u9n8W[~~^[ RRBg8 -!(q׈;6cU% ab|vۗmu,w<ETeK.hha?a=WZr_1vhlF#lg^K,JGW՟=k$,$ GX5'Ȭ,@cY3V.\r8dD?<Tl0,ӟ2WP{ ¶ƕl)yKm6R=!ݶS:[r, ׮BeiLC%>.ǜ~* CE,_4F s;`uxErfi.;[ۏ{~qnwL;&q8xH8Pp-~OL@ߓ9w@Y;Coux_xfLq]UvpO! y ୗŨK#x6[+WEwBՖ\j̫s/@RaK.{C~ע;~HCs9謝48Wo5c3'ThRXgrՎOŊUkN@-~h39=Vd>Μ<TcVHdTUX@^GSj0*/+߮˯\`Uk2ŠW{b`PJk=srtB$fOЩ" k)X<ؓɧaPTѻsrg2*3"#`Y121v3 =p@$rBahv޽f}Ef6~ vAߒcE(_@őFN}Uf]=`<_]Ym\߇@+"C7W[kңX#AWgaHP_7|<#98[̶([7ÂoMy>%(Fzt6iL)j}qOY|FZּ,ZO bH's4TQH<+6mڀ  >$D% 72ȑCѷD $u=ѳ(D[6ɭ?zBqzڿ/ێ]}<2u4نs*a#e;|t7 fƩDP3`['y>6y%f:"#a3?Ӧ3OKw! K֐-W'LƤqm?iMtjm1 bI␧qAa"ۂϒmh[;QRfONK0p5{6yr[[%+8ij0r|Zp6A v4:DP#E8{fT,qV9=2U Z %Y ,"8 ͚tItgv@g_}ͣ(v K'C)iԶ Z0NٶfOo>x3gecú0:Z9_} D\ZLI {\oI%)v,.O?D __ɷk]ҶU]7j_&Vp!oX;0¢ڀX&U`#3N\vIKز5hnņsC *Ҷ,cra #JIWzH5HkKem/Ё{W;c[)j)LY6KGV_ցTˢl99z*jt:vVt0םUjDWqJP}WPkHTZiL>JT 6Nͼ 3ٿ?Þ`>Ψ98Mňw@ ǏٌiXl9QA^\W[;zX /,@fA?S_-DK|[pLwO&, )?Vy3#42u|Lxy6Td[xt8FM,{&6{p>.dI$$Cvyq=% Q@{yn)jMy;$Y/$F>M[{-#MI\VDP_ ڏX"WX|-e`(G7_c&(f뺟88Z[od cʺO@V)8zvg[UW:FRIskj{ʾ*vT:4S2#~|WAzSbԀ^iñӛR:zx[\H# CZFX@9zN!sahZueCq~C[Q<4z3rQz֍8N2'lhx<.dG"3~}2Ǧؐt:Xa#zk^u 9 /4dhxfҀ`PWX|.';85M`3rS魗Oo3E8WDÍrj5uSzcWtRN)CsCq۔[gkb5tdKIdCV䪑"W+|O{8@OKo@C;d ڣA Ƥ^NI#1k;} /_iS'd'[Fi\`Gic?>CU#e#QYSc9SM̓p޷M< __o8S R %7homiBI^ºc_V#ܼ<W(s]u.6c!7<] v6| ;G)QM#Ҁ~%Y9)0''t[;>}r̉#t%`Fz~5J E(P[Y&i,ЛqAcϾ> -IBqf Ocd,puy1\l xvei{)wM0v |ԣ+L(om^ ACM9%K&x|=ׁ߂>]:?^+"{KlV3hL6t*oc,ƫ/܋]wA|4R5 폆a/hV]ȣ(_=pNWX5yވe+K{7qv{_w>+E댔X6|߽[VN5cE/"k[ۤ?,i4I<y|TJRS!栱socVʲR⵱4zzP8 Xӗ\ZpwȺþl,DvF@$F o#?#]|qM*'ӷB*,_b5xwWb^8p-edV*nDl2#P̺XYqV\v4KJLN!c4cӠ(h=<k$ӼLwNh*l޹'χ(;h6G=8Kw.?4A( kad=[򡳃¦1Y 1ʤR{Ȫ0ۘv.y ^|ٚX+<>#ܺbTq asT+[:2k'ѣ/8fmNR,86us2% 2󳑗W [g[8ѩђN{ Ƥ?*I&N%K (q\{nwaáeW.4u*v}r݌r9xP('qv͑Q7^zϯRiƑ!Z)/g51ֹl.%1Y.1և'0>2lȒھu;ڗݿ>pm{y(GOkV_ߦ6gBiaYro5q >NY/i@~ |Am~5~r^*^~XCxݗRHYzHͿamLō #zSO=kw(Y%ni2fDg_U֩}Po)_vه][VQC\>ˤrwS9(: ;z!Ç00 =cL9el:cx -zC&Z#EPUuQc._vUY)b(=u>Mg¬K?K0#0RvBN4u}Yp,T|^MUN9ңvݏ $yVČXt%U۹xM>dXn=&dQbyN#*1k]f du[@髌R2mta7Ad.[=~f6:7;wGXJ-0"Vyc»K@-E-dzڍ~eac¶=ĆюbܤG~0zY!dAyy9lmm/ɫ1/~jT3 (jf/ܭ”lB9L[gdس^<$X {_~u.4?We kj+ʶ!xq笧rض;b줻 $ڈUkW:4eLo_~Ⱦ_(ݾzvW[V*H"1>et : .#oA~x+i8YoCJ͚7`?x CGW#h@fMTUPc(>t`+R2`6 "e]x:ce [7Y )y[v& Cz6^07tIv@ 34(Qp:_ 4N/ 8m:fc)uk7EvFg^̔FȀ 4%Z2Ř6nwb˖ғ铩{RK?u '*i\,@! ^-|PL` F% й,e-,lmľ/lز;,lL4?R>۶3y ՜u`k]/ضjrsEpԭvB_'o2W^V7=l_x'j³R˨VY'Ndif>dq>*{0hx:p=eڎ.Ͽ,[F:W֬K%?z{˛1ZVD﹀o͙͘ՌyS *ӵ胛Е1;z a+EcH|IKƾ1Jg0Io9i^=:WJ] 2cl%ܤ;QDGQl_}X|F^C Ntv0"h^|:|!8)quȢCȩ x_;p5U% jX%jljҞke iO^9Tn^&uS@ (G}Dɪϯ>J_Ν8 ]iZ3d%˘Çf*L4 ts2Zdj*Ŷ(6[ PD˾yaZŦCnlՓhdTѠ utqt`U qwyEZ {VʴΪh%jUe=L[ ':)vғ' Rs|ePڳ,0?r:f;<6H"8&9i2Ɥ!q6DǷ\54Y*+o*JqND)|eu{Z bOe$*''׹JtJC층4Uq;K;yיtf%b\n(g_`<%ut*#At}ao!!=)?r < B:2;c`Th7ctAH _zƙZG]&NF8c_πCڟWvاWrO4*8Yv[sB\W2}zAVkORYԔdu ,GK_-F{^pLWSUUÑCeίSmߧH#Ϋ~n^)Ӎv?ZYff[ MA$Yx2;q۪db5N(.KK8 ࡝T{U(iߝ҂AXu uzk{! r2q!erU6MEЄi3V􋺝Fe*"{BR2hQӊ;> JyAvk8!%^pk"oċo/\y>|#XWbix2:XLSPwOҳqV#z6֝@5.=렜zM4lj*˔JRH]Wo 5rj;LV?~}Ga#Vw/eG]cs>˜^z[^sI8W c^.Q6?pmDua!8</S4a~;)tC47} 3-x8A۹MxΩT7 MIO?[)ډ* pF A$NnTFH1#PJI5r3u?DS%QT{cJ L@F 'KΕwqG/+Vc{w&Ci u SɩsgI{dxyOCR~aҲƄ Jǡ\,h7oLykPm٨0@MB:~^=D`t_l8tdۻ頎Y h[0|Jjh_~Lf!f Μp.- p7vBpP UhܤA,8}Lc2O?_vbp<_|ȀP;x9O>c;L{ "@n)<}^a8B45,SC9 mHQ>y<>1 K_z'N#}Y\b<0I p}m׻'&MrY'upӪ5*~'g$`xˋAl?X_"M .Lekb6ʶRCu)e߆6#?2@W؆_7IT_~6J~җ0Y`ρ\%<\}:@T1GsbrTv, 9~xשxG|T,߫GيgkhٳܜYݮ qeb5];>ٓ1:C!Fs>aC|zE2WsY;d{aIX+Hx@IDAT[dPm6b,M|V}'(.K#ӽ:V7?W^/7fk{LXuQ(/|') ž>X0i<_I)/nZ,a`: Uƀ63}9ؤc--ʾץ9Wk+Pqb >}xTJ>Sj$=3!cW~6~ [#-ں ڼ[Pq/8" FKi /=gNJ ^A^GL|[x}DŜeoiu0Q_s"w2deo (Q<vzFnL#W@ml3.+/Nw|\uuuB&xV+k`M,mkA&V[ +9{ɋߍB㤥FA5Ѩ6)wz%L!w+|{++tB˖FSl,F=& YD{xb^AtVS./;^"VȐ掾eb "j=}WWL490Lbm62ϼ)ƏGFcԑm(wo1He4~TZڿ0Ä-|I~}(f!IW,%y yjS05tTXTsջ3i-8: 7T^('ҁT\ba_ޔRX/;0Z]eJmvtmXԵz`=s2rRc܊l"VBjJDJJQ|bXU*/ʊȸRbF?oփ RP{|HY+; X1ddw+}w!xxK\|{k FΊzV7"S)DiuGʼVQ~[W<-bЏl 0ՀDw_*ȇF/Z@o77]3֟etk̟Skg D̘8 HMDJN%@DsUv*JG'آ1Rzn1$7n6^ ࡔ҄;S׈QEH?ܖWr(]l ֋a|JM0N23 ؃(gD&>k.g1>V,\|Eo)35 ,ߍ:87߄23% q=]-A0G iY8KYd%j6CGZ<:*A`P_g0!|<́[}z"N5uk8̙Wy$;Uy ba(h*%oD!RR+k\  ʢغK ٳfwWN ?5~ݎ`AK[֧_ [l62⿺󥝨T!"SKs?#`v~Qf<=Eg `%UCAVy=IGS bl`~u-c[: Is 6`V&{C ~d{$زqXY riig(\WSCdA50T'`/FId^@I5gv(%'18$ iC0tUoD&(K}Ya"!q(=`iYyn 4 $ҩ2z6vUTթaiƔ~mFZ '4Q\ڍdkq=1\(4ɜf}KIQ%`a0N1inEHg?<3."88+쯼h#%,}4La^Sa[v줌), thR@6C†L~!aO&.fbXth)C%mݨ>`H6qwE{a֝?{&=ˍ6Ӛ_,+Rt%;LٻGV%E8o-}.\JiJRL?jY}_ltx :  bgH1Qc_m]w67{ZTѨ(`Uʠ(#]X&ءo:2=+QY\,VLurvGlAUrjrRJAy%h/)Ul(\)L5Z}# v |?Ң OWoT,P\R\*g fMs"fS#th9bXi'Kdط2@/*$ZL XV28MM &Ww&C>##]US?^.Q3>ydذ) W|/ qi!'3 G67Tzf65O 6jE_2331eT)ރ93ı]Ka;w{5PԝR=ږ=௹^ $5S|7;9}?x/%uvѹSv6X1H:OJ+77r;* U3ot^bʍ9Q ܎c~~ GGUޯ3^J JA.Qʋd` a1%gj$qGG;NdgGݹ_x`vd+Jzz!??U쬞ML̓`Z:갥׎{X`[;M}l&cՔѶdzC   t14U6Ttq`V "$H س XT۶e6lF2j.5 Rr+.>sND>=Dݚ&+-D1A"}3"2AvlNl"Yμ3Å0N>Gi^Q t P^K%}Y(**ކZoDIJGFap\۫'N$C'/1/\>EIG_ 5& `doe(=nVq *)y.'o?F{WVv q1g1_c?Zpݶ=hXbA1%@u|uu<nvZJ:I^~IL:~6HS6'x3򺟽"zUYt։hbp%8 (Y^BV3>e ȗX 5v7M3F1n fHfQYysŏ_INqgkT^.EVufݻ_| !8 0ՆϹi (o6@P`0ʘGՋ0# HYjե}od"?mYK{C 27HÑJ!>ѧHuD~l㚐zce3 ,>D! G%e6Cf[)lj,¿?}:MŢEHmSq~Xt8~[}(V3d{BUq%蒑#(<`;ΏB<QQaEWpKn0)~7^e-ܧލIGBr 3ウ6M֗  my!HN8ʠiL)x>}q`ɼƬ'{R'emƂRJ$+m@֞ڋob S?Rqɉ9XW@S'B:VƄoJ oL~([T` N*w1 f` P_]Ĉebe2rY1~Ųߒ[*E (OY`R=utS8][F]}p@S=Ŋ0kiRTUUdYJr2YlD`9JJksų"UJζE?KpqVe}r\ ^WW~mafv*jlLr'w[ñk40?`ۺ-L㢎hVU1(τӟj"+H[g_)_˲ji35˵j.޵FU&QmSx4 Ȭ鼮KqXbM3+|YGTdi2&qo`Vxi~퍭@POцbD[6f*9U¼쮙*!z,<^LUTLyM*ˋ9Q[RpSwc w&nϲ?b}ۏOޠ8F-k*"?+Y(#1#7>DvD~׃ -o4URc {u7ݎB4Ed`ݦ#$m|6Dew3q$Q4'N?ry֌íڵk_'6_Lԓs1? '6%I#F:H=i(-F9ԫWo^ߣm΃8cF=;^IˇZK54u`?ʘ>3I^`(?Yt0ئ~dN;m5iSn50֬yF&2 c u5M2k=sHF\t%۴]-XG]#Ι3z~CpΣˤ|*F8g-NDE10`$BkA4Դj0 FpY~m1"#[R<nG{Sxc &d)?,Y[F63ɨ)@{iRvڶNAkXfUm-y{`7$2 @KHE#HbCxEeْ/@vz6(yGH?+TիV  kc+Gh1;paVap}Z!ʔ6J{+X`gPX ֦ sTJL*#9:3 =ֶ:-M3ȢifZ*^xy|ͷXpR `چ JÄ 6a5E{P t`a}l&7=x.prcN]rP=yݹ,y]~@G.ȢO"}tDx=Tbb2gd֮ ={1욭^LGR3D)9)wIe:LgSgj/b9`)CfHʼ<4\(fܿ,.dP ĸA[FiʎN;y5`uw8>_JC;кy\CBlZ|uɴTC-k r A 1Ƽ9!avw`=w*4tZۢK9߈ûzHuJ[=k0v@ Û̞BG2xo}`oe(ϥ"3 P!֍|̀53a"\~& d<2ā O>m-yؼ}7VR]؀1ip;ge㳏[.Uy\0ϟ~yT00q5" LECG1tRxڴzV0(/,8WX Q*^ <^VRyߜװk~dfe0,>G5d`59䒥SK~&)ʈY uL7O>Zhc{u\κnT8 >01gP5y&Uq8'Igd u8mHՀ:2,AaT0%fOp58PPu^.b^M[SmG1M' دc`h(`%$ $Cg@M';EE`U?ym?oy,|d" F[?mS1N 4zΧXr<DZA [`GգV2|lc@5^Oƥ@Kk Ul6 %#`v2E[0AX0Paƚ۸^΂* [}&`R*^bJmA PU<}}2-ExG "Yil6hR#``rא];h43ϳ?J57lڼ<}}LaM7o~-׽g=NNL6b쫾yۮ$ӨfJˮWhw+ 0m_eqs65jQ\^Ļ'JI+<Ã<i3?`ESsmSlph)]sb#' 5]S8Ch[Yfip{("np(?D4F8#1ZJc&aݶd##VCGMa^6)ݡĎ5>#ѷؾΟ\;;n#>q nE  or<Ӂ=-SWYk L%W`5=|)GK@ ܑK@T")^yx +}J4榮ơcLNdf^.)zxg$Ֆu9b8zBGWp(v)(#37_C?GJlo=2S|;岿w,C1pSpw!Ѿ=?b]س F"VX' e2>np5`$hrm-^}|!6;ݾD<%!]L1o}=(Q&?'=,jmTD߯Eg~֠q]s|ϣ\!UI-[ KY`_$j9)Klo:s&`j5yki9liܒ (hKU(9vL 1CJ^V䢸0 ~.m$9EUH%3aBvjLeUwle)ǧE&߾Hf{ ,Dž`$GTl ʼnh?>\M2QO *͛رH~z>Z9R",*(J; zv8k ())>>WYOry/~ ??ʔ(Ͼfڝ`ʵϝ=Ie/6?\:8M@J@%`Ȑ!ʫ3gb$u (-.b%x^ftU0߀i?17$2^Oes˥2:͊g6n QѣI>~*ć܅FµLr1,˥|7?a,>!cS[m{`: h`x/+1߼ N2˜=f^Z~~ZXac#@b=LSUIԋ2ӱ+Y4hByˇzcz1ܶf.Zضz?RE&#n$Шoekn/+iFDA0i$|).[ ywm߈[Nǡ@sv ܄pu2Frßct  xY>!;ۚ@²^z2J4&uJ BN ^!/:5X8sB5l y㵍<},dC\>j"msqĉ! }x< o8d~SX >#Gݏ߇,Wݝ7v,2 Pivsxصys{xsҼ+l[s@bFmﳋ Ex*Xrj}O˪ҥyyd|b`\{N3|}@*Q{.r'j~|C4C*a ?n鰥|H[C9GhKB՚J'~c&nZRK?v6oݎ\{@-J#.1cde2]D;8h b]7KK嫿Rw]U~9M}ۑ%`~eا-Ly4˻ ]8&>.7K>~3672?Sw(+b Z,; ǿfwv؄U v]ToYn-bp3&qCF=F _pAM7DSmX+_ `jh2uq١fy '~.PDJbfpu*;3os__Jy$30':RQ [oj.+=<ԷG2hI?*l621Lu+o3GPV@5F*rD ueW>1 h;kT|ʔiq%oW~'7QW:J'IunGoIjqK$F\(G̹8*`>E ζ /xoez@!l}_ݍ6~G1Y%dKQY [j$w#$ܶoNB]."'*M'>^޿rqy2zDBmydU) NS6QH$e6s?mZ^V,v)ʒkǖG&Tn^WDq#YiiHVEWcMPjс\C2KI§8$ӧc(8 ߺPDǗY(w;yL5;~?qO_vHLZQ*Ў%1(uL);ǂu? 0aҤx衇o >o(h~)ÿV P*q՗o{MӥRMm44"k' LөmBJ*˺ =L`](l}14V߽2uTI3n?Q$PZo>f(xxbBO<پcGQJ5pld$„VzmioJ]]w6dX\hx8,;-?ssMduM-2ʘ [PѢdmΒ3v/Ô.3ޔtDMI!s?{Ґ,-Fʹ ;&ezmkMlc~7F%}6ٓGp6/4Q fc쪂1X=lV!~#qݳ7ƓIن f}{Tou cBL $tn8#AcgC{H6|ZԐ )j mosDlE)Z[^V򊐔Cn]@R?;#jH&[zuL\|YǼT-Eł]'o`Ӽ6~ƔObMc'zU`ޮ+82(!<94 @?0> y @=*/.ȯ)/#Qusƀ)$\!ZZBg LnI<{|+W6KL0}`Q]-;-81g}nb Γ͓ `" h:qR<RZ,#T|,ܜ\H ) ny `c!h`:8K>۶mB>ly+RdHC] ),.M2{ynD }v ܬ,ӥݸRޠ|Z_nWMǟ_38Go7x%t3#kH#?%5s IvJE sGʥfWe&=ĻYuŸ>u[TM Ko,3҄`l]3iơC*yzQib!7 k ȯfg"h@ݽ^q_`޿mf<"p:93fYqp:6 SfŢӧ)0>&q}s,wN\u?KCCnjlFpv 71>.O,ʙ.&Eu~]IpFcc!3U8wq6jt_4{20E@ EW/dj:N=zLU'_톫 ~RڕC!!aKw7``GZjΖ5#8Sކ\@l ePgμv.=I^*G`|y0޺ wXe <0Tq&J Gp"$⓲o`/Zs&m2/řB';4eˆsC3+96*}kdJ7G_ XsCw 2'6\gJT6ztZ:uH<{}fY/6AX1e}~AfmSX"z`axOW-Uԧ@#':*^T `j*weډ Vǽ:ؾ ?x{{p/AhoWaxE^eGC{J\O Aw^z.o~2d 1)ȑ#/0D^|g| JFړ '}yTh"hH<[+.C 0iٟO?j92~OqIő4C0U1899KE}zar0r-.~ bPI`ΘU6tNo?٨" &bGdE'W/⻯A?J>S맸gKw*w܆C $l4y+Y,̣sh7{.DC OV 3OXk*PsBk2ύm=b|;L |KfCM2_|0úiѓ88c/@Cae}} ii 2Xk;ѻi W_GB18Qcsx`{:C6.;=PGf+ZʘmQb:G `nwn$pi;k;yrϱ(lΐm(ꇋTD:!rw;c΍*r[^*L:~a~R4THeV'4ӂXU/AoS\O'\"kZ暨e!œpg?f\\)ԖX9{\L&83C¤~>?/4.7cyIk0f{ٷg T0S|o*ٮsu&zeSmcb3"p| _Ϯ$OG%WbZ /$ GJG+ʷJ/rPBwE;pUc`eUYs`wCEam `b=O* 57c7bJ^| 0O<{h#rqWӧ0E+kL0,l<t cz)]F+Sdc <><}0a =S:gMޞI]:̝m6s>-\w_E<=c1u @Yy=y3-?_G1ٗ ! U#9UТvY-kd ߪB3=.UcP$1κc!fUJCuo2-AU!4]Fvaz:_If9Zc"`5πW_EW B]FlB2u5ÀpvF#SȨlUoA^Q܌K4m՗^[$,3rH~J3pAcC:w5k(+( :8UVdi\ `j؈#Thi0 Q~zM/7W** rSQpTbiu[O?D,3#EuYl姾M}+)w@.DZcmj 0Hy 3EQavćL`Z`P,\w5?ı=e+EA4D4'?*ِ\KۻAalv~6;@IDAT.vM R:'Qʑr,tT=  g[Sʥ須2L~0ԉ*!t몶LjjT]IPmV%dCMmF}Vh1azJRV] CS#Lg[Un A1--a&H$1>e]ʝ}73ݫkvZ2κ>z3\#P*\gz2̵X^YM`LtН}G8yR|d Ŕy-Ffls!ˆzژ33)w3JxGgӕAcF8-Yz~$.~6`$99wWL&:\ukpAl`PÒǞQRf2&0_VQqx܋/]!{NDzR"fNd5v A?FVLOaw&rԽP%I572.3Og Qc/''G:e{>s 6Z>rS2G 6ا"O%A* %I2u < ΒtemHcGŷ?P҄E!#2q$fJiCOCI{Hl󧪥EfIyQݰcJ.r9ˍ pA:S!e.U!3((A.S-ZYW4_$xմaRu T7ӊҍD YO{@ ͭZ015D)3*κdŪIϷy-ABC p d t>k~AP&veʑV1sݺt`'I c}'LB@(N.;@f,?eZL2d5Xu:xW t1rK:扈d4pK['X7cE4Cp݃݁=<<V݌sV {dEVQN\#᜕K_&L,%1g#Pk+`7-DBh顺 rK>dPBuZUR N8wk,UEm*a 5=#Ϫi-/CQM5Ȥpv"\Beb> U8@g @ LODŽ9ǽ?FGؽs3\M^/[P*ؼ&o[6_ڑ,@V`IVI MtڑS<ql>Xݒ_1#*̠U+">(w=uɠ͡jfu!Of9ly, Qy='ce! ŏ-ĎSbެ{pB:Nmɝ$)fKg~|2ZOO>y'lVg]n -wd` q`dGcsMaɁV;aӎ=Oڬ)eG2/,V}*O uP Yם^i+GbD^X7j(+/Jৼ薦W2+ )Nӭ}敉HT'`Ё]_SqIdc^˕ݰ+:OcWc?;ЂY[rf[!66~AHOW J$hba>{ -`?+hD= O>]Le.a|?)I1w|l۴E*o^p0ƽQ>jΐBfKuAcpd5۪㍛Y9Xjkᑨ< )?Wx ZɊ}y،{\[YN? Y,yz":mf(q,BJ/v浳DH`Ԣx)#[q +esLzly([xVGM8믷\ [ M(jA 5j/#Sp>=mL"۫}}zv8ןW918Ҋ8[c-}3&WKl,=M!ϻ3fVknj6|9Ǫ?!Qzz:t\^kV!"Hn.Ak/*/#A]bԬpZPO;,u 7 ~bLWqb%9V_d p l2+̱d,Zh{1Q@u6Ұ} <&̀ua~hRŬc"LFxd$5zAf8vLn>2U à!\}Jnjҁԅ${RRyUC>:|a#l q8"{'~){>>̌ٿzy ztZ`jsh4B'|Z^ uNA>dV 3fj0anRMPh6$dihhf,6#F,S- MmrTK'vةpt3`3c*| =a&5vey L,A8W f Eu԰5QVVN N,DY|އ`_g֊^g82e/PZ, g,dV2bdЉl!Q Y?\ۡOƈ7q22n4{Yjz d KSSkfA֖f((LA1]Ew;%րuaA~v"ȦyG8US,U_ ;I ̩|R5*jUaEwM6D|B* Z](=YM u* @iEA{:LAL #7qQ hd sf;+@9]Nfep(:/Q__|Ս$+dՒև@KgswV1?W19Xi#̚+V:G21WQcЯ?&LFes^/@Q d#z:M (0d/d&'XKzzr=*JJYhrٙg}r[4wLA<c`ՠbq3rJ( :x]ߥY̓sp2yy̵}L+$P{d43A蓏J!co t{BЪ>Jmʱ Ssxw`U\[wA:HH{{L{K/&&FM5ͨ ("V Hk"/{ܩΜ^kת~u/*Ċ~ӛ:|xW0ۓw7m"J `V{N q&5V7}Spٕ4 :mR-vLpcYr@,ΚgW5Vucud|(@sIRynliϘ:YuזLJŻ}’*4tdk1N!d۳6B~V>0iI UC2\C${J<)8S֮_5O>-#>oײǞ|xOmxFK>azvIņUUNw/4$Z ɦZcذ&R ֫ Jj֡jǙq!yOfѧ}0'>8:FKդPY#2];!9>(5s#EL`|[0< Ȕ+2,޼VzԼwCQiP:&=5@xO 1SXxbo|LXu1U sUgcSzy6kIγ}t߿K<+TBkhE7|n8y6&\엻!uŊXrj<) F͗x?df$Kr)u VP)O~OiGeyĘߣo^T6WDYQx^%5.ũ}>8М,835q>]PYƼ+瀕zqy"/\BJR㏢ބc"ҧ*9о@m燹H&~V`ࣴe8(+#:f eKBNn6ы?S(}-ZsD&URL&z **eie]~;mmoU䞺Jiq>%a&`ܡMkХJNd jۆ 9 Лp^U 0ii9w^G%ۤ%ÌSI׀=ԶA!:` O0B/Xs5j u՗!mWi9P{iJ"-  W$SRi%sF 3zPLzw&dKv"_"-.ٗ&#VU[mwԓ x],8o@\a-'G@ `0үo98PN`q8r &^i8 @O\w:sW5Vu^ԩ6(B5$SuK׭N'M?` O ݴ Osմ ?g?Z7 `J{c^@zGХԵ kJa0B!B)m.ٻ. @6ZM'媕_sbۍlNȦжQWGf2+ >xosd# .k';? X̀ {c '>~G7콰L; V]F8HӇbn(Z/8S6hʔq(AN7Ga L>FxY3+ڇ1`($lmSGOӶY6W5V9m`uϝ&C8QB5BE NI!yϳnn71eTѓTۈOl4.SVȰʫiEu9sc ҔYZWE/oJ&ȁ_4d3ǚ-[}4 [N]wKx[B>ط yY@{Y|sx/~1YGYָ@vt =͒"bǯ 5=o'SNXeyi&'*Nk2ARg2#18 ^'}==5w¼&mV2&F}sى=?'(y" uЙ\Ed{3k.Ы6K 3(~BBE\Ġ*,f͞MΛ *ipL5*0;W8er{jTӻ0(Q#}]R6X;ߘLbnJvcsS@y'vt8__J|'>!oЕ=r*tr#GfR{jqv{$u{>G{AZ|rer햻ICXWhߦ0<S2R.U3y~ #UoM;kV%럖♹'ǘg*L>hZĻX}wǎcM%0W[6G4?`.!ctvm]xXeEƣcUcm2lFKΫh֑Eă'L2ϓڋAW! dÊ̩7ZKEQ"QT$fJvF }+k;725N" @tcPK^}Z1䩆g;}R}\ У,( h1aoOW!de\_ov@ mj -$< W|^/5z S؝rH.oi)^ G)+Y_4ē!״!N/y62GSM|u"u)T0٥Kq0]$]ffnSͲv"ڭ= cluޑC Ul2nζ1 21dYI};KCʽ(g_cNkuTKu5cryJ\WmBCϠ>qH Dʌ밝d?Zy۷oF^~% 81@.S]\9ֳ{KUjxc$"#B^.ܱUޥOg?02=akflf獕! 菸X:&0kȬj-QUIKPR>ΣdZa ~ SR8Kllh@)\뭷1P\p1>Q")(9 r:K^0XUU/:0(@9*2цN1l] (;Zhߥ>MW&;o;LС#} ;Qn/VF(&L:r\8ٶCdzەW$6`\BIq>jxH,ARF9 b!!=(.p ܻ܋B)V?9P'BfGs ]K"?r3<2  [)yG&Yw#jK|ᡀc&ƩnKqHY=هعٻl3 8"wj9JO}K--??_ѽ$tvr/M;Y:z_!Ik_fьi3\Gǜ2kBI&T•gL8OI41DJUa'Ƌ%) \IƿR > QG#88X jPUbJҙtzu;ѿ2^Ŝq,:]sxA2쒓Jqd6.mמX$V~ǥphiу KX2h,tZ,!zI4L@ѓ\r(5/Dru0YBM Ӎ~)\_-F}?A)/מa;|||xO\d JϘ4]Q֞|~|ݧʻ8V'?"Uy)+?Cyd}Wa; ]y]zd*dnN\k)uiF=|3̥U!qj&D)I`|DVD'aՑT'D7$WWᘳWB6rsнs~ZJHb]|<<=:~g0wrܙ0Ew< 쩜ƒbJ2KK+<9`- UcϜbX;0d^yꄲՀlaq:Ǜ%ܦl:T]Ɇm+*B;&w˶ReT~,:(Dti/uߝ p3@}u NYdJ"d3ZW*w EO_}rb˽6'MX|KbJwueIe9#N*E= *Ǔ-].+M*A,m4 ʾU桺\T?Qd<2g[ǿyFѰ~fmLU$O"uq%>R9_oi›ּ5~.W!ԗĕcP]Yo`1ZƵp'ezWϾ5;#+_,Z@`}&i$[E0ǎ 3s~m9ϭ {8ۣcފrNT1 Ik#*|vm| jcR%%JDvxpNIq4$<ȳtu{5J[@t)S+kUE*ϕJ>K]4/}Pb ZXRgeޥF漯D6bH{_oL%YKJiMrVNr~ke;?Sz%fΘ '~5 0YKvw> ݃$zՎ㠱0kpf=wY`*2&tVC~d/HNPt d-Iv8@~cVF2BkZd+E slZΧf"<* >nUpYkl='͔ߓ hT1+w,N]6EX}ه89°6C-ZQ,b"{wSԔ^fM,X0Sr_v-;?`Mp꨾e͂俁9$pW9xI;1KpTCpz5毭+;o<zY " fglP]\rdB:}Crđ='RI 5W>v9\NhSU_x1 LGGfl 6w|uGØ`{$'k~ j1vW`^G`U5.t@ZJ&ܹC"39ٗ@IZ2Nشi#龍K_ DjYwnZ@w;~ӻ#A-f_΁) f=yL^8&q4<\ȒwxF-([ {c-<˹ `(2ڼK &Z軨 R42,jP^ܐWETN{~ٔ4D=23A#BjH OoeTQf sy_c2(ʐM$z@db*-?uvt8q]Lx(=j~ N_c 3cP@m=lpL 0kCcќ UAl'Pwx+oJ];ȸ?ݭP>mx!~^@5ϝg+ju*6NY͙R!~à%HN?&33V./qΝŦ˰A'u"f)$8dS\fwAWK0]D ;T+nc ǎGR ~el˵9y~ 椞~/JH&M;h1ܡcZSȈcy 뾖2b%S2"Q-aFбSF B]zOw:A"T4wDAU 1Tnkg:rؙGBaI^IѭɴEX#HL;x-QZ:Xe- N:UpjbnC"%')MI% )Kf}=%{Ɋʪ2;qW֬JJ۹sCQI62ʆF%2 df^j%Ek-L}¬Hww7|<7/^ohabTlR,fZE{vF{/ٞVh=q<"m]ґS1LL(h@hܰpѭñSg:$Ci?kցD0;=OR[}E]Rm*zH#HH`WK/iR4ٶvB5˷˿ z!;1s#sqX8 X- 6YOż'|B l# kJrj| xlqZލv{AYGⴹV=qF*c7๧0*<K F<]~8wn&d1z3?0cɣ,|y5Zˆ L7mBfy<Äj zf|MU$!_v5r9Ĕl[3?Bh6Ǩc`Gه?l^Mϸikn?~\ Uk䳛QpE+Ψa_τ ՖCFP&cT`_1v\OM}}۔^=2$^S߉ϖg { Dni;X+63)ml_T׿3?V9zg&J9b VֹLý>R•Ͻn( =E@)Tc4t xvT So~A:(z7tAna)r K54 2 A1?o؋Ic#e5lQV\=e]2RcPC;V*FllGmjc{)ch#֐T@vW,svąF053w˖s4tAd ?˿}1{EK :Id/}׷ReOiNpӶ h.it2ga9[wn {Tʀ3K }|=bzr~2&L_|@G5dTz w¿LfF5 ETB:y_q`]`&kKUMU 5ɚl[w!,OJa@KM΃Gٌ(… z2(SPPHx!XCv}EZ> g428}{W"LGt!&#Ƒcbά{pG(;i) O<<'qdZ3i@ӿ3PXb'u4"@`Oּ 8SJWhI UϫLЫk\Kuzf9SoJ9bYvG<߶{a1`laYˀ6{y tFЫn598{Ve1uHG'C`oܶu++,à}<n^>8M8u, Yw'M5r.6VF _A;ogd&ȜUGdM[] {n^Tpq [Ad6V]挜{PZ ;O[Yyl傼k/\m PinPrc4ΐr7+^^{-E%L{2PdMs{qp."e)~C׈l1ggD%ϱ5=ɮc:Ŕ =E_Ww9;D`:EٰkZ1ZY9Oؑ^{J`f{-1FEI<y VK/Z 2i4%JqbFpNBAU=&ɤ+}'j8P|{0@冭o`kOI:B+]&jXa{QWES 8P4-.3qłuP~\/uc5 Ud-$g=6(x~%'fQҗ #0 GSNl~ұBC)J* 2iXD|JJʴ1z$gd|qqq:RawD w݆B$O\z23),1p26TOʞ1Gon S6wtr# F%xgnوʊ |G8vJʾ~$VQq6T#B CߣN4ƕj ( 4ǣ׆J#b _=A(l{nxX'&P * kF:mTrz '13g઴m mFYu3%@k~~*vb_ice_Rϝx^C/2<ģxɹʡ{SBX`2dx_);}+&$]={}9̵;УLyUa;9^T{.*E٩!0LXHOwFM*%H;?s1OG=c©YX^_ {nxO*vJo;z uL0l3kKuLjYs(]DhCۀ1f*@psXf` * SG6ҜOޮ "oZ串Lg؞zrkyDPA՗I垕kzJ&U%\?^R]<=er?d}pxEl]omބ֢w2 nZmL_ո>N9S;sJYOou_/A2F &= 9 "B#"Zq>{1==(a \[;W-p1Z 9/*dLȧ(킽U+:QQ Ipf; |0R@IDAT}/"Iya!7ڳ,LpYݲBerr sH)l/rj[,mi VW}_\>TrՆP97n*T3>`_H?}4,fR1vle7t`~-W3~6O]FmK]jYO'{uBc]X`U)jì4bҀA2u0)"һOTe#ZnyfpVX))qa|0`[ʳe]ˉ(ACh׶-^BYΚ]zz#ҥdaR,ϽFv3  ʌ1l d@Fv >;+ -\si@k)|F;9;ׄonS! RS[HjnVJ erEa<4c&PLq_'3,țhD+=c1@|HN>elm;=c/W31!=~gGP1oЉcpg.`f4r8 (HU-m"ly>hhU2`턊<w#Kǐ傌ԋ.' %WJ-2 N XbDIUQD@Ι)#PLt(ϼɴ~^ I`׶8IY:K:x"RewGO1R(ZDcd_"HL%fqۓAYEE4ls 0'/BFd&i!a'M qhC`m~e6\322BF|$S];ʍZъZ29ƀ&a{ؾ&F 0EY3뙒"zL,"xmM0- `8Pmz*< ʆ`ʢS*2&ɂil2,۲'R%r )6YKݰ8l$|a^} 'Bw5fIs|,9|oIV%GkٺmIsæaG(tH]d`ㅋaea];. #+B%{R&\S15@䉓8DZV8t4sh zd$pUXVt(G*J_@iY:'E V݃}L9x~Est^#QrTU< _oOϲ]+e`E_7c89)drĞB&I5,*ӗ>UULVѰ$-\% 'Fuu96OX~Zy>ac؉Lc'-+: jnlMk_zuFXMV+̜:c䈑AoC1(E㱊,fͦ||k+kx,䲯h! jz^>XRImڙ{(}XL,5-y+sNcw@_G?0x^VdWXAc[ ~=-c؄ӈ2o=~3"2cTGLW6us"ؽ?=6ҳUdu= 1i#CYl(N>HHHDA0&e[Sg~x<} ts3E@G/>C )عy5.ЦMLaNFU{ $8-`䉓R q2+i(߽㧔BDC7XDEGNyWKjF%Ŏk'|j#!%ӎxۃV#ZLQӰf9WB`IS1H"jKt*XB%陕lG+0lpDIzs @hƛZKfYT.]Sjy csYW'& P$RKcYUY[yn<*'*dyh ŋ9o`5l'gpy*i^XMϫ6|U XpWdq\g_#9oG|%9wuiDS{; GK,a텧]I{IkAH|/ЪM7'+gT˽&(]k-m7^Tx*,5eqg* =[_`2 =I8O*m-onV a#LB_lz*j>Zo.El狏SA,2W~׫G4xĺ"Ϸ jDH$,wK!pIŇ#vڥMS6I)L(M{٭0~p&6!Rƍ#XernW|SwjV kf%K9s2lOf #sE, bs3TqДH@|=6r,u(C 㘕S]Azޚ"dwm,8\f{xϿ!Hb{fE)J898rʼnly5 ઼s 9ȃjW̔e{ зQ]|:xѣ?EC*]w֒4;KyY Ƞ_Y]]5-3dr|Gfs*/b\T[zJ4"cڶbqm&?C=KSN~VrBVÖ=ӏ=7Mf33WSPLĉ# w+u7σWo;ヌdb{(lkaBd JIIIQc 68砂RwM\"kkhydeKҏ$? zzԧ|tRPu_OL$+B_}  C|+3xp6FG-6'ڀ2>7UJ(<mJRυ}Y֭cfnIdbF:RJ8V|2ve+ܿ^2[g̶0JqA ߋj_ۓzW HMlmkT"̑KJ۹R2wAs/ eSV}5"N}3&^ z*){\LbģՃ`=Yk-m4`N g)\K酘0iR/V"NxbYCLSlIq+B=uQ=cF]oل?]l}CʃI0qh\%V&= +ptWL:)1qJ:(/_UyIٸy2y`FNONc1bhV ܄^!'/q%nUTufI=l:JIt0<<ŧ? ˪ nuCDQħc`uiG虴Fz23 ͊UxlȞ=y @r9=uյ>aRӏP|YT9 /rϠ)heaCI鵴)F4&6VP* EKKK@8s&Uy .6+ۏKZȰ\\%!h5FL೪7tgd9*`byLƶqm[QC{[`쌇5Z[jH"@CKhogD}S˭YR6Ewfc 1ug7m =ڇ}sl_ĴtGG]_QOˍ^yse[^=}ŭ_/٧sy1䴿+vgv \ag:;YLf=~ )wP4R"O$ xKE{\YdYhkg4r`T&>k5W| T`"'BGBcA+3#0`]ݯۦb<;}}ݲØ4r8ҋE^Ni~ $%_m;ѕWzmne&CmC)__'!wymN` f 𶅶iCVs>b7bzmZO6]2H7(3`Njހ被  W?| qg,# kP.؅O@[xӳ}=0dz͸g&ܧD`ؖ{&uko ;S&MlY)~=ǞzZq>sD,ٴ e )rawLRʽ7]d:g`݆ +e_ˉ==q mn9,`B; 6=pFl8,c!0$xGI 6Yqu<] [RL)/%i)`MkgrfDL̍n;wl N+T ̳-*k#2A%Lubt(GP7H9ػg?yxUHouC=v ښIn 0lT^1kZE@8lxmZSnU.q<ȼqYm(`8B,$ENNjN>NGB (\&G#QH=: r* Ҏ$ale'c-t2ac+de'eˮ[$@ i~q0Uqݚ|d=hF,>yN[n{ޣcc7"U}DyJ  3h\G>fg`0jtvjrH,P}ƵdY抭E:~'Oe֘4mԚ clA^StℲܫ|(I90_D'@:xdx@5qp1SSdB9[e_"3 z:}><1@U=ڍٸ{ʯT -emic>Ӹg &#1yd?],ǡTO0bQ%= P2 `(ى_}WkTk0dGeJm74n8 *28UQF"6!Uyi:LrxxbSxjZ:QH43=A\9"[7B`桓_V/k c()lO ΝE۶{f?z{W}x?`DJc} 6AnʺәYP_r.>fiBҪq5:HODMFֆtoL.!4&elԮ B9xo?8y,+r|=1[5 h^B|{.&`W䪗~[U#v – ;qٔӪGO?O_e+W>#]I-aCp#e%E@li~;sTnJp)r}{`YVlv ,Ub-3ET6zd%V[{)DrBANfd|`=PTZKV6Lw9N:E(+K:[7jJ[7ӣ<$-Iwh&%S _NI-#v]IJ8uRTR !!-/S) % Q^qCESg9vb,_ SXX"/ږRyhtH2 wXz}ֻ?x<A:3QLA|ŗrꕫφ'iU':u\<{{#HCb.Roنue{wa7ap\dFxTKIQ(\D67^>^ŭGڧטKKyQW^nj+#5$_ -- B^Zwrcu?|'`êyu>բn')Zբ.h~VP-'.LH$I]vWVUi3MMsO<;\Vlb@4\č_%hn%c 4ulk\\AԢSDC3UU^ygxouL7M|)ڷ:V# ).%(x;YtdN\(U&\/o|Y{F_D@ywʥX2(hA>h }A'|h&fFyU/=1%9c&ĄپPIB:.&;wyȩ 1X|>*+A)}z=|Mewȸ* ~`BƁ \zcc*<8x݁~ہؾw7ƎjwtpR ?~l4߮WeʊJ 9tHɢMԥ#_%+;NJ M6&dFVL> wϔO?z¹Z2{&ELJ c& sU`D;╇PFkĜu@.(~9.& 60/0sߞ䍴PyC0ضU:u4{_E}HHB- ޤ"(DP+VދB F\&\b@';}Μ9gZ'о5?v:}؎ 1(L&@$M4cl޴{mF&nL{?q!e_AZYgu;OhRE&g:Gfm*%8~[~ʱcaK&CAÕA98z%#n]jym1|~\>АP&Lcʤg`/UU:`2XAu:ڶ ([W!K'c9 a9jQiIr]#|ڽ+yZۚ1Xm%߹W<7E hKOJYT+Lyvld}LV}\'BcXr.s|4e3޴s3CSh fN5l UL)]Fe>lݲu])V :L^mږy2]^-1N.l_&ˏw G5ŷgcnlC5Q2 okd>3x¼_2]2{ X#n_,R՘w)ZP7ա:7|DGRࣷw[oo|͏Hs8r3Z+Lƴ;O]ZEaBKfÆeu VAxTfƘ|Z{Wrcp6F6||.܎95gܯO<>)ubch<#c#8Ѫv,H3>r}zdwT;5`⨏GkT5ƀ6)Bg\6t5 r%m&DIBpu1~HL$&OT_\\fVD ռ/)P=)3ܕ\,1wF7:A-2].#uǥLخRoȳ.}m6'u%zHNndVX1cƨWLrJklLwb ;d_=WpT-V:gV:6P($H㳡m[i1;FY4 dlo'f SV hQ~m|RrJk.G{K_;MX_zMl۸na%<wu4P:rH@,_ݦ?yQ~%@ҷ,uw[ԥbcpF21plUl_2*S<.]bm;(`÷`چeZ` YE+F@[~.8lU"J"G[֓/z+yad+:׷|Eb^|~8O\Gٴt2yO%ܻOވ(D8yl/٬a ҇z2q֋3 `=}?uHiߜEO=WwFF*ە-s[! U^V9Tˢo)w[V-q,Q?Vp4ӔPֹ9f3IƜjYd~,}a:z@Oc ٝ6jbŒ꛿:P@G&d>l^'Q=){(Sy5@A}d='dkI'`萁e0ޅ]Ėrw{ j6u6!YG?}N^XM F*GI'#SC݂dE@`i=왣q]MÞ]Axޫ&2m=]%Ȯeٌ@B=W1MW|j`jnSGH-.^n]ۈ~Eܪ + 7AA(ZrN”l6֚绡4{gOls61RH99"d{G"Hy?Ϝ9Sd2~ôG`M޳i С={z2>'[{& ءOl[2]Ҍ/f0=:( + jgXPT,LM@Jy\mL@P?!!(`RIޕDD y?z"LEbT RvMʹ1ݐSBk]"[R(Zx8қ݄rбoC:ALl<TKoQWV E$Ʊ^ҭ/L)Xj꧜a4ra![=L2'Rɴ)M_f Cw_o~?V0i"^NLЖ!X'TZ2 و,0٧ ТU.]{bĄGʕw6H~6\y+1h&LNΣa6*sFbF7sXNH~O{b_|xr@UYŧģx[Mzċ <&ָN?Vz@Q&O~27 o *KR[;1iʒ1n]UWSk{`C$7d7^9]q3kZSCXG萭g_N?i L}yRϠϭ3}B`\]ˉUߣ*g\N(уv+5Z7ӎ:dnڰ ?, 7>l6tqC'ᇮ+fhoŎL)107D)MZ:njO#__٭0kgNr)ep)ǛNY۳K0&˽ΐ۰>"6 jyG?$;ue5ūEz]Cy~.!O;0~v f|9ǻd&xխ)n݋rJPѣ i f|_PB߆`a>97nK0jYT`B󞭘LWOseVnuQe|%g-jH,FiH\eцϰLǕ17|hܜ9 U,I S]=[:{ Kp_?-)qI#5؇I[bi7aC5De3SWkh)rLLv\q+nM=q)*!h>'̅q56nj\Oa /ÍkC&%H(:ʺQ5]OXUlWYFݎ?~W e6ڱ?S{ {jY5K:G 'ۯI$nݻ[]P[TZR`CʰQ4XR8#Bݺiw1k+v#O>Vb:ϼNDG`vvvLVU ,;YK#19>hvr RSnp4' f~N}9Gh!ʢud;>l̍I@߁ґxsJ5Pw_ 6FasOO^LHћ.VAu3I_ѹg\jn_ ѣNSh>? :wےr,߮/uJѮ L~vX]VeԪ 2Rgԯ7dyeoՒ/ql8$_K yJf_ǽKZl}msزe_Qg{WoaX*u{ۑ|J[>GJCreKA{泰CV4H~.o'3ْrAf'3BS 8luѣkkd;jOV>L%[ޟ2uᏮZs [AR ~yYʹ'O6búYzdQ^o'P1#%MpټAtA5{ϻNrr %ӽnapV](!)̞ &u~.('HqpM@Ĥ !UKuue,$R⊢M+!-[Q2%{|a[« SY㋅}0侽E$eGZz}C)'XG)s*XY0^@vC)3Fq;ZM=t J`ag WSOG}xWJ^$Y 6~տ+W?u5ަ~~\\\(hJ$C* ^uB {ahdɠ]xq;F~][XoLy .i=P;8 Ql_)ς /wc8/x8j7!kQxc?e=ġ5dEsO~WÉմl|.r>2$oz浸L6eqzPnc*^@)j4H2d8*6S1zh7ʀ)EaSJ_eC- 8Z $=})aesρTX[p8ls7~p 9MGxX0=k7W_D2;p12ۄ⑇&0QFӃlLxcӗAmaMZctjeX;ټ|[6akZcS#AC[0Cl7Fq-̮\Sm.PRy_JǑc 0NCg|8_7= %t,?'/G^-5EnѿPe<@IDATuOwOH_T[67 i-0aa7n C@﮽њ禖Q;v{o}-<_I sČ_H61-0')$̝3L(|0c `8k"@ah@@ړ0v ,.?Z ̞К-Ha&SjnA),Y(:Gtr23mt"ՔN%NյL5JۓXo@!_|Eǯ+ASpk(6H٧KQe0d.ZRԎ(1 F[=>Uɬ&h ?ԑ+ە(& qI21esqûX2]8C}:&ЮcoϢSvV/[KvsuhFS&1QRf(Ѽ].p 5TAvx!̺R$LK|C[c`ݪ &)*Bc\E/td,UeeU2wwn(\z+o~E}w_ H3jfxXb bcbQk %B_/?p'ND=x0O+ÕM n0Dk}7t߸0QH-[8FIuQdF͚Kxlog)ZN~sV` ۱L>}${XTfGZȳ :`ok/)aȤyTSl-tf&og|iIw=vEc8hF7l6_'egcg>ts>Y10Bt=іzg~e9 lXY^X^Rd$AC砮+FU,r Wi/ho\dž&WD8؏S2W=ݹ6GCϏbwUEwڨ,-l_Q+z/.T "-K ܋mqI2jaj;vIx TٽZ)վ>w[ĎN2>^hAiz:m&Y= Cj`s DX_(,,u%AbJ7lv)5 6S'!ARE]m 2¾!eƌv~I[ՕK}wό6ga)dJ|Qff 띶/g׋[΢}Yu@+LHI)i޿6aPJK~Mfd J) 4j@G4ͻM/XȄe gD2W&Y2V #+& Qۻ7d&9]QG.D+Jlb֜Ѫ?>5eON"{Aa `UE| k.%M3C`f(#;ʥ⮒ɶz*CÅʹ b%hH{.ft~%5Ԩ$|נҾ4菤koIA3"I7;Wls 1mUv.tO.y''"ޫ :#L We$i?&L_l׽; veC1""3!_w9ܮZwʿW_/A.%]ܚ( =bΟq݁lX7&Y$(8 D\3F{оw7?N"h֘ܗPQBJ"BAw6?&yLNBPP pԺ@p AmX(F7,zD:A3cW' ,ʀOˊhnf&\+D YC"zaGv5CaK&43/X 2F4ǡJ}F\E 6 )ŝܫ%Ӭk4=8JCD`LU]$*&2_,9w&&@W޴1Aօc腮=z)~ |vt4,WTқ֓  `A0rc ٔQhleT2sc޽hBU "ax{:;݃fak͆3B€0MOֵdw<PqAV)vl]/'C;~ۃz*#e۴ ~PSX@'v K^S1K;Ý`Y0E>M>Og}Bg%D?BlJ`cpq"[=޾E3ߺѧ CSӲzeSφ!#'W-"ıCaDQב@V6vd(G{YWZ1qr/IF8>2`9y|mMLԻ 2BJ#?0KBS^ØCՇ89nEٗfPz|O/zFźKcjJ .'h3ī;P.7ŀ>oގI=p1&z ={j1;HL{!4t&:]?~_3&iG&Wfr:t֨jݖ+JbͿCғX`\Gg^A29Xz- e{^_abB,Ы[_1Yip4wj}Au=n8$BC<3DM`oW|KT<xoSRЄuT~)ܰ9qg0^KE\}u :ىxf“䬴s[5UY\.˸ 9~WZUK޴%%+o6Puo޻{'&"#n6.sէE.@oFBE!J\`wiQQV:N>Ϥz jT[[-+ ȹ->zwZWiM巒"c24/~חW_KYѣGFO KW+p*E,ceػ3$tpx<=] yxLdujN^0₫ʀ5%)fbZe+iT* 4e*+lқw}6dyT ːK*)9sPG-w̢+ GH!k1V4i5Y'2L%c9Zρ,h!&,(oڠ‡>R6ZE]5*)gT$ =YcűQ:a B aD֨|h,laBaISmA x:HL{yy ^͚6QXdX |ͷZ~@YT ZG6[4fd0]naڿ\zR2 4j5}KJv@}Y6d̹" -k&;;7.*u`OVLteQ"5xn1#P4d!MиcF}()Ah(v.:bL롚rfQbʙޤEeڥ\P `zC 0[Vޭ!gQF=_yRK=0gL~pM6Β!ruog+qc*'hɀAu1fu,q05B[`\ij $2JCWZf'#5lJ 'ف>bA{)͡<,N}4/? _Ro s]Pau.V i TI'5`nzJ੧'PA?ǡ#$>C˜ lN}S2дE|ЛoPB5Ƈ0=MPDFdHw9^x D KR^3Ca2ıa?C1k_F h;<HIBJʬ5BR943fP׹|Љ#A>\P~"'[~*]$)&($HRRjK [95kr&H Q< :TU0,JA3<܋2X$ĝe%ÈKL\xt9yxw/#9- uN?䍙qlОC).Y0׌#M`i{gB9A /ep]$AV)~Ae&wW%TkP]juT/6مy$t +߆l/&=+h҅,]ַ6>ҢM VDacˏ굗av~-?@ϞyoBطW'|Wۓ;wv3Ez=K2_;vEY+DNLY* "ʏNU[b ζnJ#Cޡg[-+g#&)laJtbXQ O¿P(>r=dXEI3g(kǻy5aanwm7d[ з_2l0|gؽs'egCV}Fذy Rҥs[̝?cw[3G*HdXO mUƶJTfRT+eL)N@9Op|k[Vsܸc97 Y=~ Y.ƏčLʵǣ~1&dS-!m2Xs.s|kesM9î|>)0L4fB5EMz*&9 wͩ2#L,Ja2ۗ6Ho%[̶dε2&!z=TJ0`b7U|⨀x׸z%r֞; EET (u8ͨ'j)%EjA̗Yn:KWU"c~VeWW7SǢlk>jyknY{w̉SgY:4+VQ~cɀ2i݂@JM,{Sy^:մxfRN}U6:~Ag~=Gٗ; jd*`}eGqn4aro pl/ VW"oٺ~'7”)Ϡ]۶(/.֫)Etkm[cu0<e]Z*ّ|]IV1:LQ;K`O*wd"ς YC Uz Kof:> Oc ;S=8DSt# iOQh߲ ydod$6" .ɠLbXt2HGϝS((dRɨw¼;U1wdnc p>[N>U b/&Ѓ<|[ho~hߩl |7o<7⊴+WO()%ėp~X^^X ^Nki`Uڷl*Ed#% ^[Z`d顠17F:帮c T_/¼9_`ʍ9Җ 6vJVWJTw 6!ʮ г4w7U%>"9|N5(wJ-yOw)Ͻ9qx3F:uR3Y$ oiݴ?A|='0?_kDK>`_S,$Z<1e< uuП&ͲaAN->R Kr Qb|w/ c&xd%2RY%όzhODŽGBQ#Cy'#ђiM0gWgjkxpڏ4Y`۲/ч>eO:7܋KDG&ТǾx)K.<|C?pTK~ HO.SWe˴SB*li-5k`}u*˪>ˇY=w[f~6YB]s[d+| W?OulOJj)De8;m,b<0qҋ_)<6m/P>ksۻËk/MTGi'{E,YDtЁ}}d_tbQaȶXURQ&0i<1i W^ŋҕӚʸI|o,!9]7$}r3&)Emʽ~W>WlMUmL^{2yl4NG&1$5Ɵ&4@-rZ"/=Iy:3uEsp,,ҦBj][D֮Gߝ Iִ 9Pq'"i`e$>:ܤN#>{!dULp~@HhbM\אyi[򬯍P 'mvaZT^WLHe[S7 \=u_1֮z_VL3E~Om8;&/4DӶ_h)NR?05a li<85vPd< zKʻEm\s՛q/̨>y{wkbbd~ihoT(<:g"gro2ȑcuBCYcي8BRaۭg.aL>G q'}S4.\> g`X\r CrQ"zOQv,P⻱KgH=O ^R3 䙔$q<=ڑD7zeGά[n`u8-+.##Rn|ܩȹI(E*_iO/%c8!n/R6vɍ9ڟYɬo-8C `8] =qck7G׊K/c歘Nur&cm|HY:vοcٗvZx| mW>Z`%CJHBaS,T}>$ ^T D._#5PoHM;j`l&܏φ5rQVۊz2/ ,+Gі,;$22-)Y_㻯>OQ3_]^Hl?^w0vC/u5zTwkj#+J`$:Ni;{sA$R(;ٜ̆xV$ ImJV6:QIekA#y$ح)Y\lF:p8td/:w;=,)ޭifܿts';8qNӳ+ D ;=!߲̅s0p`[?g{ah;-ei EU@0>ܓOS%z`27y*uaPȼr0 {цޗaA+-"J)+N[j9{ ͽИD1F, ە" tDm;/L{31aeJWk2ю`.AndX* n> nrk``Ѥߦ탃(t}{)daՋճS^wn)SLrTW}BPBpXkp\4=9sq:: Z#C7ll(9|!zR'xbӊPjs%Wxw(L`m2Qn-aLnO[n vQ# |̌y,l[֬1ҫ7'؞kFr,NhuC.(Fٔl5~Zʕ)厩"`O_͇\Z2z/\"kЧ,L=<ޢ:VaWw;p&-zĹ?Q=Ҵ"M99*'_΃`t;w-ѷqd { 7>Ʉc)>k#렀Lkԓ"vCC} L' a RZ#w'. h8JBB;*fIo02%pQz60}0HLQvEn2Ȃ5%ɢ&02e%?tB;t$*K`C&ݯo0[UF2޿o%=sPo  +1{XPmպ Lrȃ=~{K(kԞa *ڻC%MʝLŨ4r4RoGs?Yqg}>e9O1pOkNV?[&ODvu6vPbD"vU8uR[f_玟>fL60cضUs8߯ATOHŽ};`GfizR6|&3ž;w;M1%? 1c4ٷw&+Ov;|~ÒvH)цX|*f,2ЈNpػۣ @~TWavA~#ݰzu2Tבe p|˝ȴG9}̛S6!O٦ܗN%C 0s]Y"v bA`W~e&a(-!Ӟ{nȤP,~t]*\hddȵiDi'auS^Q׏U9^D>b(RRSwAx!(3* {NP u {eOg~*.+BbR F 4}XԞ~V14r Β ,E,2O#&]]5lp2zp>]kJl#}e$z玵Ӕ+u[LTW/Vp ~1Cbm2icg~uZܜنɇ!UM6P6j;^~Lvdd!B0¨epjD"+zV ud5|), S$q)”ha+2ֱM`& r EeoV9^Y^ֻwTc);eRuRڮb@IG7yݰ֯%ўq4r/ٚݢ%븒%jfLkCMy|~WBH&&mԟEKT5PI:`룛}H!O2GM;]7Q' (.evҳ].:=0o\L{f뀫hS[TB!A}go S?{OwzϾLk`c玬8kJ6 {ǤnEgàpJI+ XmRB?$=#TjDG7_"g{erʊBYH`kv}[Uݾ%H WS̢#9ҧ3ZOŦC u(D|4swU4Ќia&< 1pn@w;exujEJ1=R(Gz/mc*g!c,0|YE&(C{((~ Jd&A/瞣e A T܏m?#)M\h["LJũDJ'A8]t=7AYLjH"J^j57[(A=~fEVncSgF\XO>Gν}0z8| ⓡ|,K۴x14pWbC9k6Mp | 8LðߜÝ&xS5;-9ٺ%?7OГ!4-%+ =;m({lĠ3gN&]zoJg.Dn0ĉgg1͚8D~(oOI[N6V0'?9m:ϸʊ2tNv+6oD [dՏMU@5ӏ&QJ#%RKPv/Qz56~*.\F6M dRRSzWYeOa1 R~ M}x}+VCCЬeKџv/Gij|G*+DYYX=١nH,l<2e⎬ 3 ?=}5Ϙhk('$;IN>~ٸ.L mג9Vm;Uh6\*dJ:9{ :NڍLz w ל჻XOk> &$gUN Q.X0)e?>[3aпʀعz/ (KyKgUyY̥LN8}, ln_c3[x8*,d=]\V,Eч.YtV5^|Mx]7f|NUStȘV*#~ x,{ѳgNJ>6qQLp|3y ^ ]Be~-ukѦ㲤v wS?>KCІknd/ WKKR*I[C8^C0aDC14Sv®e1,r*EMբFx㟴e0QKqGY\FE*+ǨFelOX}hϻg }}̟|F\%2B[K)G[9tXlFDDhSC;y= Kg~`/,*gV6(v7R{aѬtn 1F8QKxWf`dW5:20~ Z4Cuml *2GD0G`x0N@oHÉLr@Reu N>Ah@YP5IGѦcr*NCw$bg?8Ȗ1m=+ٶk'L.^̪tf"5ٷwweǂ)0{gas^ٸN{! *ۖ}˸!_fmyUIQ=YVאGfR(.~nqo?iĚ$Wwsuyee~%lCf[wku K:Uʮ?A&Kji; Q@cǎӳ hϛq)D'.l^+Pd>zVZS\E vn7@0&.L5kx`\-!ߋ#qoSgխI/nȷfFTFcݒZFkJO})uCd\&C+u,Dp墂\d`jgB#D|iץaUV2LEM#cXSTEGC#/&hL{3xźMdT1N=΀z%i YA2dt ZIQeE)]v+ҁ}3@5A9D۵WܹxWPBa'oa/kQ|1Z1X5ޮ8~8 U0D ]GZQϣ]sthMW(6(z_OQdIdߩ%?ۅ-znd֔Ól^zn[ xrfNpm&mm3q *TgvlG&8MƑ.f^F\Z\mG4MZGPHJL"tu'#H!G&Y>ں+$!B=!D@݊(uګu}-ŝ@A"] ɿ\&\BΝ9sf̑Zm$]N1%$Vmd qI=u]R ,Avа/el&{0;fp3c衣CN2i|hIc=aŒV>6Q4#l2A; 9xtΛG) LeI]D3ûmS^tDͳ1 Sc}$[ )۶5l{:fuu2Xûg؁@{:)#NL|ғ3N}Is ;smjjll ԓ m5嵄pT7K42ncCz@IDAT*p&| v\ #>\MQWֿN@ӍsWMI)m1S㟚\kyϮ!/'qh[( *<ǖNHbD3 Ik ()@mڭQD͓q,o:=?Il<#]s4}j1LV;{Fbw;6h$RJڂ7>H--@` ԄK/lq7 KH00};>yq"+eA2=zڿ{Y ]p2<?&LhA}'/o|yL8Eql8Jץsdڗrl~ 5CwPn PaIeO C@$I& , r28С*/Lff;@GTUe"_Y]^#ƺ뎡uIEee4&GSUV*`llKU" =Ęٔ5v2Ξ N: 8 tp}\XzFeRB0*і,ΌmgI >v0XQb"1HT#h`0 ich75Æᣇ)2/q[Ȑ4ci Up&FU<лg[^qٛBwV:N,LO9M/><*U 3-ϓ<_w3< BﳀȎLB#OE;ЉNMXd:~AEd)ᵫa ^0unAo`+n&>I憆+ :q[ "^ߞv}zE<,%BrӲZD&cuf(3fpS:`SN_'N`֓!]:izPQ joiqt puhV(l!`*9D /0 YO>! F +*ߟ<>[8A6#;scZ0\C2m[;:JI$}m +1*%{mz&U9sf #8FhJsԜ ջ|^w`*c]@IJN98G(SvE-Ac8ˈ=Xڋ۵tX ':QHQJkNT R@Sn~P۔jk d:jdS77NO knI!I3pXh%  $2 yL)\-~)(ɔlj=YL6 TD]D!+uBX'$K" C.ةH 2~V(C:r_]9 p3kG+Oٗ@; |r ȶeDDEqBP&/Z勿Bsc cԕ@Y~nHG7'M-B% gPN7Ϙn\y@p`dD^W$/=6pc<'ȒĈZol$:玝DjB"rԜj]wOV2}W;b%d;ٳ F9PJJ~TYdžuT1(d)it8S8-%dOTʕ ;JGRt)Q_QnYuYdw jwn0%\[^?/Aǥ0.cPm?-AOK= WҼyN8us'H`RC뫜YN-k;=Y4=x4PSK_  ENG4`;/ݙE7@AIB) bc RЦMkYQnrja-?L3(w鏴|0vQcHC~ŷ2erx8l ‰*X02d1ٹeH[V(}]1&xC!FM_>B #RY8{ - D6nn.p4iƺx%eDѱ2yn%d {e'g*,Kaa|[أok2/HMi(J&iIqxqLL{ -ru`(ϊ%w" $6>Յ]ds V=nd:w3dl r[UFAz(/`2^z7-v'"/ɍFvmۣnsJ[1ޭ>~_F&p-sv-+ U G])lt"ZG'Jq2U(-.+F' "ҰkWEFLigŠϨGmxMN]heѫL{&i YWС+يz3B['_|d>b\t߮ 0}%nj^y,v(r- W)G_3T "a+OV3d}y|gU=U%1`u]tR VA5/E9olQ֣8-Ģ$E$Y{Iڵ4h^*S}C;[8_wf/)CG\[:VI ><Θ!dϚ񠒥 XMqRGJѮGO^^:.E)2&1S@NRSl6$ۯΰ.U%S!N?5%7bE3cPNGHBJnFǹ;Z]t}L-&W_y(,YU7= K=eqr͸wZ-_Pb9WaA6YsL.j֌\}f)ƌrOw!" b9,:cv9gU,T93O& <32OgSaǂhMۥ~ ~}0zx|,N/RIt7zwE 3SGpTՎO~YsÆQ|؅^n.9U}^},*R6ڷc;_Tj&.;r- ;h6m<Ze|!ǑcKӰ/.u~;06þ=ǹX u5c| Rm8Mjc 4z5PAN'37xU׺[mdpR?yJM7| $g#Jow:_VdۻŸʛBS"80ǔx];C!b.FKAv*=oA +] 3&!#q(Vֶҁ FK7gYh! j[YذfUoL dI,y :p}n~3ZS!cMjιa⃡}mARYk~p!|?eq05-pu`QcNîdcz *}7ե)k/cvO?]*b F)lP+*=){UW10vX `U7V\EJzNk~d0+AwvF!Gu;2r8v s7SbtJUd^gZ'r~}fGpG z@km.rߍ15 GVmk殸ڳ8ʤ3}pW%^Lˇez$@qH.D;=med;gdQF2{Pz|CdE *Y NPSkM̔ČgSБBr,_=uJJ[#, C#zw±uؽu)!J'7_KV8~cPJ)O+VH AжNmlR[s+kMi4JE9v>E>l̔]6pn#G+˥yJo6)gYtZ2sVz̮Rnc_M;?th悸\=0NHq?QgȺ#1ydFg#m@od&M@mbV(3ݲm:LR؊I"99Q11mCDeof HGnԯd^g'giiB;}oGP}ӯb}=dnk+WpyJf[Ìj;T9py9p0(@na_{%p'GSj,cdhRQWܩgBFd_i/% hoSU^nh|䙺ZZ29֠aWàV=v5ANy%NÒO9iATܿ7?T?_Nhӻ?.wlRS!vOZbPL>9<%IBL1h͊*ԟME>W)d|"8R0梫 ry8*ﭼ4k ddE`_AU<$*VOýD*8ZVN=,ĩXԏj$_y>+Qa {O82^܌D0u#0jd:H‰h8mr,` ;]51:>URy`8Abeg}Hpjۣ`SX[ MOQWZQĻBYLŴ2Oljͭ(Y/2BJ9V1)2f/R6etva\s]V5<BʭWX ؠֹ>˲^Oi'+L/W]pϿ{Yy䷱gJ/=ĵ@gb .?ǦOsZ*U:(QҹMeGݫ/c厝xr,*kztA'_|Op SyHu&ܙ%u C8 נZ E:tgNq錑3LHU3Nq?p߬9>}rXEEƿA z^8PPVefe(I'{2GK`:|z.Dˆ>,qD5K;\4Դt\˗E9p`Q3>l1zz՜vT8?uuu"e9(67 htQHŖᅤTiwdƧG5ƏX{fƒǞk\P{C#ՑCę얶Eo vj[7֖VʘF98=M'cIj,ihj:a5xsk?ZνK~6Y^djjS5kTSvo[tZq_}Mke V1I]dYvX1<m;S6^O6V4 =eybݺ_1i{nXWUJ*Ymx8m/6d#'"PV(E|=iX9|x?쭭}O#E }4ԡ*:t5 ]zS)MMV~KWTdcEfUuE J^xEpͿ ]kg\ӃJb_LFNW'lPB'bcJ}j %xv`Ca^R"Ӱ> gk56i2GKd5E2oE^NIHD_ɒ[-1ß_l,UfL :=5@Ue-z7{K*T64T'29pu`! ;jq4j'a-ڵqr ,"#X*nSΫfzcWdƶSÖF6^.d^AU2udٶgfm׃l2dc$M/=5GSq/=yRN=lMʯjdJr`5ûQgL;[ë#k".6j& cn쨑W[Ny &ظwja4%u#(&t'k3xL5`l; /XHDw̚5WݍM-1}tx:jr;gF={ɚRJ~?~1A*ޠ2$voI >l ;z<-]_(klb:~da` 4uյ0CpVy]ejb؜,Y?N73@CAb$W{j'k:<9Y2R3Z`LT <\,ΝdD] YN禠>.v$,#*,s72ݝgUཷ)Et04|/ u`|uq8ة3(4J2fH}; 9prkW+K)[z8#RX}6~2\3ñ}zpA:J&vUV?$#|$rK=f0u7#+[8{3?.u 5c?/`Ic b>X *r$v酅g1Lb|l(ݧ1Hj:뗓T 0D>>Ɩ2JzV$Re_֗q oqPS ܗ1iX ٸe7f;~T3`t1t 튗Ѽ^-B0JJA҄E&ɉ_)N+aC-CY|V!0:9w1 bS_edC>1섡> ʱ"*c:5^6N fc1"NQ6K@r:lXVtL >kW,CXTڊc;ғk̜9?EQiס#22 |-R]{ŹJO0IέG.صw2:n<%&1B@}oA kW~}9J[vӊ.~p.Zcn^0֧L=>NnT7_kPo\%AO2'L|([tҸ ХzNwu>'jϫڔ o_]~tvs;iG>H-kTV0նNi]T%O1pỮȀ ֭Oߙ%vtEcA#\<ܣ^] 5Aa;B /j9jshEd.n l)d1flkt sqr=:3I|ǼV(\IZ2^-垇?~ f}vX0&P3obT#If-C_];PVRB9bc:*ggbC浍"gdjr$5?ÿ/*J2iwD$eK悑 &*9n Q6 d=#1ZvRl3]*YAӐC;$᧕qԹ/c-~+19߱x6o'F:hNI{Jr6.b?QNqMd}RFu#0(S':Fj۷aLҽ0p8_YYAz# [ׯ޵fD, VSz&9"aiL狊3KsM(I8QctU飮*e)hI}xٰ6mjJ RZ\oZPK哝|֔n}y^Fp_1y"{&?X`<`UV0%%'cO9t3z2GȈdk1:јع VQ{9bxsי! FKc{X"s+ uu37ݡT$HϨXFu1f6lq%WWҾMw>eur+ ɍ=) ~[9(: {%NMvX'H$|6uu1e::cH'-确k/f!=chQQӒ<W{yp&*qiOcϽ.z0P%1NU9kv-jc땈Xgڇ8&+%2`<@e *6/^`P C'x7`511XD3;w >mn٠A&8]$Ygq\5``ki988] >ر4+|yxjޔ~oG CxJ$&F~d &CV;\<>\H|.z_,cGꌧ.A2^W~ "0\Z  G[,_֨БD~~ݶ@ᅹ*&4> g=ptDSBt4Ft S 9#&eЩd VduwѣaTqVE=?Yxeر0r~zY./NDH N2j9R8H -%LTͳ|ϓG9QSnǤ\PUI"1\PK7i`mx~Q%Hݩת^ z">>E~!^G~=]ajdP,sV=sB2f_QOz7Ք@;7]t$o:?]\f`OuǑӴ_S 45PFi+ʉвqBÉdz Ճ=_̇)HoffּlSN`D#/iDIXvez 7(#Ɣ"Z :?w>v׍devVb͹GdEJv -fիi,7@wo#,Xb3u3Buu`aޝF2"ׅ!+'"ђ WZƣ%C ]$x]Z5=4twc=?`'ㄒLe;i DV) x^8nXXZ`1X{LH]QeWK8w+2&C.Zc3zS? xx>صk7E{0|g@zzWstken$G]ɬf}wj6;v(I^p1ȤZ 2Qc98Iȼ@מ,-%J/$ >GZ֧‡rm:96vGQX< ADRcx<Ե7&qsgaAUs[7Td< Ď_ )MLJ2|W*t̘4MFsK0`sh)Xn_صHYB@Ы}m.}#p@[n=X_πэ'; mUdԺ'> ;vAA~x"-<'q1pU+Qs2nc2Vwn1y 6]ٌ\Fiq5&u<Ȱ, H|L>},`;I/62հ32Űw* R1L*N1:uQ۷YyVNg e2p)ꕼn&70#ph g2AQXil,,bXH<^3 ^%SL!T4YOcbha~u"t$Cѫ@`UNLEVFIa[U"lz:ImkdCwvR> G: XJajs{%g#ǏVZx"tuVzY|-`ՆMA 3xyvQ"* }^G?ٙ:::vs_34"*|M[޲+/{$[ڨby k- ɪ:{Fl=d8/]a:{#GoBރ~@mu uZjr -"},]1ftja7XX^KࢗzjWi8IP{NJ/p ,hkոr,ynXM2wtªTce±X8e&E#cLX:qb#ۣp%#X;L`bߕM82,;9 Q3q! jz$L+E$}?a]9'03Ookطf5 .A՟X#s 7~ܱUK^[_TRL>a"<P B콦Tέ<cK/SB -M[Cv|5Zb~0=qو'P]S$#ϳ64`8⼡8hIKh԰!tt)7vJNTD |Tǂb9~ T4p܏z]7Lۯ.GQ~6,nD/ضi3q*PwKl<` G;Sgm-DY"owFj2 % bef %_ռwr/q'T *}j1\~Cp<,S&L hιftt,O V˚r,aN j"EC;~7N=ډu!ש!֓>˕t#CcIG.xIygI s_S}rua0g/[Çc}C:r\EŔ%>AHHOG(e9%T_JWRRSy_+ K{RT`AQI%%.#y'oh8q s>HIJgq2<N8NTڙ _At2>m`Mӷח# }TkY:Ur˲چI$s9:WFiټ*_ rɵCKӞ]Z uCsN?vsgNs#~V8 ҆cM5r<"cPe:BhX;q~jHtMzo5 ,Mg{+p/tny9yk;dߔj?N:EYȱF,Eqdڶi/U[g$FtN6nR3 CGNVW-6!ƧeO_Wn$=K"릭ݳڢ{dU XhQJ:,.Cĩd?ʠ䬃ww4/S9wB/s ?HfFI> BA8vE利d̽xC@/ͅ˥0\xl^O9 4Š]&F2elp2klL0v`2 Ĵh1fW1^76Df^4!~Ј,jpt308VNTXh|Z!5t|JcB[Ϲ*Y}0hɸ"6-SGH<ă|ޜgk-+2O&AL@Jd{jG=]|>蕅J#л]~AJsSRN9dԔ՚@p~xQ ނӟk1 MGuj E]:C QJ2SL~'4rJ$ c}&6,焜wsI=ٳkZ&?t<33/=]8 }}ơ;v @]hID32 (}3[Dl|t #zkn +/.!{Ec￳LMU=gQ*lsѡ0\<,gE2& cƛ;s/*eh=;lhO|%g'h7#="#Q®$΃0nȋvG8x~EKVpԀ}+Cܠˊ)z왓W@Gkܹm:ц,cK;G$ftg-f ᗫض^oV(H3Hm`:DF6B>\oN9~a5Y4b~miZ^GSN韅"J2o^q`~u.,r'vxu(hrbbSPb4Sqʠ<4d`׍ qaN.4!ke\ut%q/!+: iMGW"1oNt3 2+)ٿ cX-G\8mzтxnzHzMW>nR 3Zhh}6'!uy6򏴗j{u_Y}/?W>|/<)RC.dPn۴dxx0ƶEɖ\FJۧYya>F 0' jfeXH[(t5 pN5R_REdGz/#o5~:#l_-L;Sꏱk>:!*ACGSʟ{~y> ,>\䙔aMCKkyA6;¿Ǖbb/4&2|YLC0cst> <1t uྵ [Q52*e ȚCgnTLk[9Iq&b˘NvE?MQp m"J3v(naAG;4Ы#>-_}=1-qig"lڣFȣ:{m{*v;uyA_qv:zlm:Y>N rI]S~Z$]O>x^sQh~W_dfr{T9%K%pմknż)āSϨfa"%]x%0p |b)+I H78ٶ_y_'zDeXOMYe,(+:|N6_Xe6t.(ӰxJ.~ҿH{q$}OsΩFir^g:Jm>-4Ϸ8Z5,SSYW.uu 7Wm_?q13NM?qRק#>5@+:n?q5 깩KV9BS)M5T}9dmzX2R=ěGZOƆ޺W^A0-z.ΌWDfLSj` YONFq_Nĵ:C<'-ʊdOTNhݺ 0X)w'G0Ak k_=&FeU5]PsfdR>3(pY43q'62bvyJiRgR GR&90굪2z(hI;c" UXqFg2+\: Lވ1Dt%k4E Ȋ'zi fd%u1AFd9bVxޕGN⒞:ST|CEP&P @H5oqA\Q^]0h1KEVFu#`> S3|t~44Jz`t?zwCaJl |) HfSi$ӳ.Ġw wA6ِ_t1YixiũXK5I,_5c7} f.wg~&KazoJ y-l!]TPϽF1 @I~xk 4JiݣgCuAuO"e\b :{ݨ HFweiXe4\XJyd,lWߜ8qeFO5iݸi#>n¡;aif }'Wf|Ѕ"z,(2% 8r=8 jm2 *ʔ+hGVKJ):X]zէ'Q6Wsѳ[T̓Q]1N?IF'fA铧hC FG32=1e(*Rh\ߒK@`'cvg@i]}e{oͅw;?W<ڣ] 4Gd<\X*&ڞfdfw[93|Ǝc4 }z1Cv]">?I=|k-jj2UKgdzHKC)5=p.n:ٻ#)Hֶty%d֗qFBu&hp?3 gg:sA6:* 0e40dSӗ(Y,fZ)mhͫ,^>#Eq9| IUK5 4ff\oSd B+QCc8^>o|8}6Y\)>gGRRGΝ= H %1 cC>s"UUAG}==Y<{6Ljȥ×kN ̗\G̺R# ]sZ:$By'Ѧ 髄"2'c$劭XN~c,6R Iڻ)_Ŗ?GJlJҌ-k6Oi#wKG׵ksOgնq N *0d@hkJ՞"m@ci'Ĩ~7K}N?/(Qe1|;bbb9eM['oہh:9xj=/ҁa:{M#rQ7g}j8BN+3nj?_b hZ(pmZWÏS"?ĚHd ::ijA$l+ ;-2VϝIKo/†jxdJ=WJ߆ n OuRHBVeV~>[СTZvJ*kMaG7GlVJ$"f% 2з\ØtpKW>Kip԰ f؋ ¾uM%E~ #Ud(Lch]5a4TJ49$y+d'R-|,h!~,B{ /a?Sh eRrzlC=BaI>y~>7 p=]75GA,k>?CaK ۚ:Mkd{ v k>(2ɸ$x& VG 6=<{8 :x(ڕ{:y_NE,#Oo_ L/W/Lv'.^A &+\T(Ye-}z! upp֣76]/5]>K'`S)eLǠQ)Y xg~HFmZlԑFy fqgWoY{+Ga5^,/c.z3dҶ}fxs"Rxplo->˿E-۵W$_߿ K<~/_"J g$Z﬽IY^t 4WQad-[!Ǖs:SEaʲ0jZ_{)KeheQ@zaϹ6YVː;ᾷ_/[HGnS۔M)yOnƠ)1Nכ42FO3~Y^|:ֵ+klE|f8ZxNM>^.9m廹3?sJ`SH!cZDԾnuPY]>nר]kc٧ϐ/99; Dž J.2") FiռROs^8GpT>Mmb ՜v;rQ4kkPUSA8ato})sŇoϦ׭iU'' m`}/@BtqkH ܣb †m1̾9w;Tz$p\ L{Q|4 Re[Dm|~ނr5H|чԓ##ذn6o_SaUoJ/W֝o&=8R YaDo ?,^xAWmO7 Z< E%xo0㍷ѹ;0MEzF<}EsT!UF˘DgԹQNRGz2Tjѭ1֜oܭ4k.EwǑ6Pڭ+~UKy  >Msևb{NxMxLZuگ[`WGD7ɢFCI.9QKc y:9o,\fmPo/ٍ"씑‹oKL7Q*\x99Y{u' WY)jWĞ{]CW ɰ޾'y0C;F]OD~Owɼ)W)=iL+צes2ǟgCȤSԔd搻߀0L>LhȈF6M+pl[ fn(L&k-y##Жrʃ(yΤD(![!L0VV9i '6QQ¶YX DSQlyCJ@U N]Gi):p/* фn#@ҹlkdh9@qԷGh(_KE~E*Mql, 阛A9=KOad-,CFEo''0?e8q&ǍGf`ОJy"zp|<'!吽,#D;I62ԍ6711Q ݇+IIp:%oʍʢٓ0j$uhvQc鮹L,5Wp:!|~2 $z9t= fti+~E+071^cQ9Vd`ߧC,Z1߄,ո;E61j(lm5/(#S GAAGOqab"+1l'IM>| Jn5 ED D0f)[9+5wPHŝLwHfdl}l81۶]}0e!Y';dQ 5$' Vl\fdkS;cĄGpjYxi!Sgt 3x1PAsn.Th7g|Bo/6)/O0љ==: cޚlDn]v Ԗ!5? BṡyLܜ<6 tym*NW8s쨞vn)OL*h[ԝ?}&5<6zjjXϓl]E2eX$yWe>u{hu \ 9cff P7:kpqeG6uh*gܓ *ٶ}b8c4;ʴD( Ӑ +d\ZM{/~6|(ߪ}/Xx gx\ sZQ_{ũXouV,Ɓͫj> JĉX߽6]9iA݊2GgTaj3K&)Y' Y{Sy< SS>QꈳY{Uֽ6Wʊ[#*R;ǫ=y[)R0DZ놵z'pTucɌ"߂u Hzb1ezilܺ,{dC-Xe3m^hx7~}/[Qd?od}OjHn~߿z6^wub/%`{S{.]K. Z8VO> 3?a.Vy_Ȉ҉kW1bHe=3mCEr)v2'ѨQ;ua|adns8NKfI!}/_1Jex9hpCs!KSw; z"}T* >'Oƈჰyt=[h8!΋8s .&L/7Ak>FN4OyKQAEY13Oňq Cx-(z7\.UG5'r%*.&N1\pO==e<%}Zdl$ϼ'ѨexI[J]d,$e|#vA^e/s8]!Ǘ:eV~6w?IWK69M}d]P@9ڪu۶} ccCju#:au7>W2r4SV}l T c;0\sprt-;0uȾ|E-5FeN:=:GЄ``6UUUh#I;BGSNBڴ T]pT<:tJqv(1Js^ݻ#)9/M K yR[({-)8fg:pd2gJ 'LAVOٻy{1?rkVD~ (Cq\ bc)r/>2#Įe^ΔUHc>cgcD8gܔl30磏 >}?0 < At4/ctA50ٳLPőN(UUNeNWAk"7q9Ȳ 5QY^E96,b ~ 05 JjVjb,-*tu:s&5RiNe-%L^|dz jw]5gwe[СJMJ y.O^ɾLk=mdHEiW\ʄ_\fЩc *(9$4.yR;#Fb^вm{0O n66E#'>`3ar݊[o Mp`v=K:EJGYtK.k `mbuQѧgO4kp*Z _2p~2 yq2H77H3^|aKv∑x/1bvޓz& rB:›LH>ax )`1pde)#&2w!V!sWhcq*+Q3k[TUP8 ,Y3%\(uh˫W]Brqȴs_i2&Su{H,gJE" 5L 2d4khU}PaM0 7n:\gkzuA-0yJ=U 'c_`p>ʈwW$!UUꗗ郘ǨuO}=:t*uEUV)Qg.C@J"jNZvyo+([*o-ɹ() go?tRWe/c !@f4  ЩJeqzDHK_zIZG&\*MyBLUTS=+d11i 4 BL]G~hagkQ{ ӊldT } Ij%k9dUZU ;x٣Za (XTM3}_gZع1/]mkМ6(}$Ғ081 zs?7p<|F7c/[0OuW K'M{ `m ûqI}^cםr\I'`2єzc9J%/0ݭݒ1jc%ĵY"жr,UWǤ\>fL Bƕ9;ѬӮ[* +eΜZ"3[`Ł?u77ҥDݵg?y%kC kpݕgeİϧ1ubp&}9|;h2b0̖.iRb?0| Zl2:;޵۽9+dL/r# QJJE+iO!`ҨmZ5}!f<`sd`olH^ӑ|ҮeK4)ї.\1@픉0h06-7GԑX -A1ɻw.+z+Wo5?112&Q٘"5=ѿüp;zUfPg ΀>Cޤ]RR#Eo`ZS^K2Xl{`UH8HoCLuאPY}m͠@ԴQh!w{~r83]ZOCMFul h[W9RWjf\W4@c -wዹs!7^_}d3b}wiYDUPoN5*umW~u[3>|W``R6npfSJטD0d)e-sqE]0ԪuGXe ev4)N&Ftk6\A⛍Qdaaw䀮=Q:-=]$ڳ B߄YO/\!s5TwK 0S.o zf"RhuHdg_ [mBIt5'톇 g#vmSoY1AS__$]LS*ʣcp`jJPodz6[KԊN{*EՁ--.o}{wekiv*̬{FUUYd>Mv/U@>GmY)N&qJ*2 $&ZWnt3&!@ /^GZ~9ٵ?gqgu3FVpoA)Ǽa. z}p'= ˃%ϞLI*j3\m%- vt'ԦCFsƸ;2/+xgNE!O_tLN.paݘ˵6lA~~1ܛ231v(8\&G,ۉ,AwB4JWPج*Pxwh߶]AVy&U۸\ iGPkU [Vq"j]__931y<\r o 쳝^AGFzRlV0nMtyѩ R:2_hwC2t"+גX8`R .,3t+'r Ο;@cBާΝA=7̩| "=tgZ2f8壍ɚ5/T9|8w [2Ħcʣ Rɼ7>ʱ0О_œvHvi^W)r3(ћ' qrv`r֩is,ѳn_Sw!`=:RŁ fw %1i0?o92B`5 @R) lZZ3m\< nCMe .9onj֔R]TPF0G/AΑ`ln\[#*, 7SȑH(HΪMΓc?d&?p-). J}jCRJ@äs'DW),u#;ack/KƆd^D?~CqR`U V)zyM9RS-{=z&*"ϰF)j=qM8IYc)WjKf.%kB3O]nOuDfKQ5K d8)x$ VX0{Y N :^WYUkbGKIߪ3Um;ZVMʿaЈ7Rq22eiYS2Z 3p j<{aca9hj7!#UyD?jQR:| ,*pF9M-SȾDkj;Uqd W~V_f{[Ǡ.Q\),l{KAco?aJ! 7 dS%3І);ߌF QL o~M+25سgT3/ Ϧ@sVP*_cω&-+oS4A^i8g{Z6^;+[E|T4=Ex=6WLFBVq)\rю \`GfCZt;g_7i~dl3`,T0j"&&y|.5}HސE-%s9;5-4#4]DjG@Y=9\eיCH|xkij>K^oooJ @V7$=0AW2Í~tƱ|,YAxwq&%eLE# za|f=ѤISd(f@F-qzr ߑcwGL T-_Ξ ?C͝;QH걏*94%&?M ݫy/0K=#Ƿⓙs9F{p.E4!c gW#}!ԓ>Cl U(<(,uhqn ~f@vi@+ˑSs׎/SE6`\:]jj),~^Y8v(2xWW)G9DT8 U'Wx >ZL5Ft"ep#| œ ,CFG;eK'!EnUhyTx 7ȢӜ"fGx LTJy2h9hN[,.pnct0Ҕ,4ow'$U#r"ԘBЈ2npy.5ڷk*cgϡSP0nڷ=EbbTu&NnF<*ˏM2c3ƪ  9[iWO~UރZV;'T][)GTOY\<N޷Yux_kEU'n*vB߁ .im=C zF^Чڊ,90ay7䤟rYU0p.-Z/ yy쨀&%3g,芠<)d\:r(.*wDM2? 1eL?|[Ա>U:'cOx3[ Jp3c#*mq"- =hޙ9=I==~_|C"uUa*X'':ٵg}LEVm =Ux2bj)V ,tܡ%XSصiAVcݓE3'>>{Xwl}NWU,`XڵklCYnڻ# ~vP*_- ǚۨC(yraKxo[^{`-(/U@͂Lv)`YRYW}9uRE`oޯ󿜧}|D)*Sq!a٢$z˱%,ތ1RO=<=a3@IDAT8.q+M Sx2ǧiKҮWg(ť%JtxVn`^;f,,._ ֕ak7;4bl<|`B[񵝕鯯OU4Ԝ>2/c_ L?~}QAQ9hq_.*krL2RbUuH stE!amRy7x}}n*xIO=-۷"sgo)ǿw 鈁N֊bNJ]P|brEBr}bS+ 3H}'8b\I8r06^#3 E&̝qbL o]+S#2ꁬ.9'ǁ}a|;O{\/ݱYlK ,wסvd?bc&G%*(5jiyo0FR6tJL%b4@dʡM8p1]"^ "Tr:KaGH&{7muSWJ4Qnz8SX [ͧS16`N\Ǭ2jueI4ρ#k< {G-웞pWi(2V"jees)sZ 7[d͐.̃|W|rGqy̹өSWp3?]Clzf<Ⱥ0 Yh;=LiC 7o$`qtxh*|e)ZY6tk1QNȖa52(sD,5o(H004&/%?z-[) \_=sN:NgeE%N\3۟CqK]CѴTƜQy4ZaCF67;'c57lK02,q *}e;^X}9j¼@s_4X^{EꋸD3ƬY)FfM;\N9tp4-sE;SԃĶ)|x7`lf=yRtA&KQnB0ԛ+ Gف&DQג'%];&MŻb` ֠Sn nV99ZU+<^܉tbsIsF* ;9^?cj`x|$#Kc0ğ?}GS Wxg1q18XNM\K6QNxݛ[/ ~EvkfNO#^ INVGEY%N>L\7q` 6l] _[ kгG$+SPtF7uWrfJۇE0& V<Fa=~!CFׁrw,w+hftys8#d`͊_gi KO3TQjn>. ^q81CJ.Ij#/9J:*kTf KkGw.1&zF|PAs s4,>a{WZٞjp0c6oVyIgҼƪSFaq6Wt7mQJ|W ,S՚E7KXW+_\yHIO:eR\224 R_:}tk_:T C>Ό6^!{n&7sT$ʿn2~7nE`VUYufB2s1﫯O|0c_Ywo{G0pJmr_֗vLq@s_OM\#蛅%ؒ6G650Τ=-;)˱4L@ Ie֥%%`V2Yh 9fP)B:2vkie_@$lmF߶VÞmg%(<;{cË׵+'^Ct>>bV>=:=q ޞcx{/N EpNAd,qX s5<5;`qhݞd2b]-NA HLƛ>ŋ x+ A]޸O@5ޗz9vMVdl[&7+;j'cv)l>EG1kՏ_vV*6Y S{Np5bd<胺_FM&A$mPl,Gh|}fBHwɱqt03!]7YtR:.;ӝ^OJ3/=#f̩~]P0o y'{0s(g O], YF]z1뽷yĮ7Jw`~dz(R럏?l8Wqf($ZÓ@ͺL^3tlNxA>6Mx c/<?8n\m<w%^ ߛH:STy`hКQCƔ_ cifZ}C|1g>rߦ]NH\U+"tC](e5ڏ1K+H6!zGavUeXֵ#}DYOHO1r[5 [/i+ ѓroxIQ` 9ZfI/WY$pN>n*rmM=VC=U 'l|[ygc@Lyvm,ﲓSX?ABcL9y`;~-Ydjjv |XΔzJ0A27OgKz ^ --yR|6Ԡif}&Ō2(%0~l#P h["H;+;Lyp %3kp- t;8Xfmw2wgDP9 `W3=%cI)(Vrq(}H`)zs#\]r3γ1בQѯTroڇ`xaF`;Db"MXFjjDJϧUZZX"Y7p&w83SFF-e.đFg?t^DyyM 60vOekkж?HH#J;}%+ k ZU IiXVOֻq/L*`DEJnCF$9t zaw;^G vW* n2{":)1QJC{|q'r\WBՔX7XX`/fn&&1/X֖W?>=wqrtk?m;"/ES9sӦ73^~YSњ٢;xy#8Cf#M#z:_[Ua.8g:Ӯ\1غc:moMHP:C rщ{lj m AUJ||,%L-TdhNpKʯKixbn⿫H qG S֘$%mjlw.k`h<8k. ij{̚.†CG~ x74C~>x]@Xsdޫ8gJՍYj (IC2Crm(?UЙ_-T]SM$!8(UvdR;H9lc~if}<VmeQdLa>񓈿\,Qĝ=TXJ8ٴdʢ0*89ub [~r?U{]!PRhé[60>h.م1p<3pvBxh6sD.`6 6[tr" kC?r ݌‹ڽ-S}tcbf403甎?˹E"{ gcQS&?CՀ޾en)vAƉRsfuo~Rd5纽wظ|(JK86Ac+/>ԭ ;SƿԶi;5俢AK-  1j 6-AQQa4/vr/_y@5BGNB)+#RV\U?U@ֿPQBIC)i 5?d{;OpyL䁸xk0wv8#"Nި[W8Ղl)5'E2%PUאsGw>N;C} 9A'=fcf2J#3zE%~5+}{ګ60źC`(5+{:'xetlD/:2澋R 3ElgE)Ly+}ʜ`[ϖ)g +2;9˜gǗ`M6,ьS`C^`yY50S0Üv/Z]ƕW̉UN={K)+(o6 Y.T3ɹ*`k E1Q&2U-dRYSt`ZMn&6\'mL1ጬ|@G ۶ADشn91,̇בSh ǎ\3{GlQ!Zt5%ݙ1 1^^N?DAbAl頦Qɏ~w.xАJ?O3Qiu,f5Y#RL"O?ܦe_TXT&8О3`Uv˿|$8߂ b8k&.;tG'XΞ?~w< GbŘ.]qu2Ul gۆu-ӟ{؏<w s`H^xQ`5ׯ ˳~5L0639b t@mk&Ha+މ[_ˌzQSFIkh.\Zlݎ5 de^OEێDۨ]oCAګ*}{`qJfPYׯR=l@ H; ߆֣fn2:a*)FkFvdoHiO',Ϟo} Gᅥ>4Rtwn#D:t Z& P-~6|7JM)(ܷKqN)'?*%*<Wll!C(MX})"mp 2:ڽ%z tkhŖO&xbJ1j29j,(-v>dp @3g,|bi*)V:3wrĻ!%? cduko6dHݾqN͑A Fxo>)F&K#"D:󅳶 M-űiU}~83G<3[}ʟ17S^E `mm]W]CT un9urmC0deYe*w, ,2W̬z?A_YnchtJE8rb]{oXHa[n:g9TֻwJieο\ >gN ?+ G }iGŋ*\Ǽ_XW*$VtdrPyf =iKj*stf? ss%Zb>c޾td?߶#$ʸO^T@59vMu-~VPY9]ۻڭ]Ȇg<~ xkfi 6axSDB-Wr۴lU=oGUe}ۘ=lżϕի37U2 ' ' Дؑ|s ӟ~y}*$/7 >rGEÏJ'ǘGh>b4H؎?e{MY+`JL0̢_=CX(sw9n7RGXfJH}frSֽ%2^㨋n?ZO.D~XSߌϠЉ0+S [;;OaA amʡTugChWzxh9 ~a 4khQ>F>&._ڴ0zhK\B"Jk^M5}՗_~INlB KO!NzxLJ Eʶ%K:kSM=S9~̑CgppJ )Z4nj,\PUtRFLkuo1|G" kұ+S$28;#k2'h,{-}c7?A_ZJ"Tkld 4mZM&3wVnj_^gw ڒ91"BJtU@VZ%)N?~mF;s>$lKq%>1} ,Q@炑1̺lQ x ƚ&&_y$ 9W{Mahr0%dzQ8/-wŰGKzEnhR&,\_t0N2ge5[=T;\{9y j䌼xf}# "(9n!J I Ewv](# :d M.s[ERW^B7(@7r<7>8,tc iJ4eC7c?`Rp4, r*l.ۙy /?y %7 &%n_C|2eZmc-{vVhkoJIF9JU,E&\ykR[_gu=9KlɀRW^ ktv<`sNWU2t$lyi|Ě֓hL@3JӦÚs43TS3"J޸N.d ]ڶiMRLy&wsz/"ҽ)F}ϞbP}L,BtS;&‖I.p!!lQp)O SH1USMUݠoRi|T9* 2m?Ƒs15[i«u[j*|3?\k Z:,`b᠘37RK3E ,ٷZ1)s~\t W+?-Z9Πn6"೜AiT]JaZX0/"IOeҭd\ \puoWڄ6":J=9Mؽf$3;k053pN[Q֒rްtܛ; v/3m^ h"砆6)뎓ͺ\Z2b@JD)2.n^̽qXz (Bi0Qr>1 _~5 7цNjʴX6g1"D'%>!X7aW Δu`J-* :+fLP\YYM}M,KH|ҫ{\u%GÕ_qfOч߭JSmQh'#LbUSq5zlx}xRu:E)92fRMm+KjJ29,Ƿ4y,oG%Yg G[Y0Oo L}O~a$A"mcQx}Zɚp"摵CN{Z" 2^eag\ƥ8y^!#L|H ݁y+@ʺé+66* z@lvesr[_Q0̽~"$Es?r1^z /Pq<}mps'W{o񋣓3`)֫x- Bk}c߈Nr7.j9ɜ7S wD̙̿{3;i jNcMUw{-m*^raTWU0p L]م輑|OtgCuCd̟"/~(l}nhzE9ŀ21UQC QonDEG+1vJ0#Nd#co<#J \Ʀ j1hlwa ;mX#<d a%T)㮩J%~ansɀlNa픵w[og>| ~ ovQǧbq ;;4 andڔJ\NF5̱]L∯㒵`Fyӱ) O GړZJ?eZ62vUdpI ΝgWd oĄRs*ޞjxj |0bشj%h~g$%+ УxcA_TRelsy58e%C݊pe|*(ն{ס]PMb¬/ɧ#%f|B`M\8?NTw"N5ڢJz_+^7'4ғv{((`bo_M`v]Ɔj@ƭb;u@`)}W^{n,z쏋H&0KG7~ScbWdsќ{{Dž#$ĊנCǦFvČli*Bɼw_mhKyZJ+5WP aHڻ4~̖GcQ03&p8eͷk Yx'~D-U9q9QS]$x#t1,w+TN'{8Cͷ" e*|^я| Ҫ4Y-|s=}ζnCYX8"Ҩ;j_ۧI\kt2RMO]¹* =`ٿtzwXRg' AدšWȆn#uP iwQa|!GoTfx>%}KJʳ s._۩17`gs$wt7w3:GhY}&F ?E?ب.шCI+ 8S.k (OIM5_0dۯGd,y46!v![˔pKp|V 8 72nppm]1 5D9 O?}4EhA r9*a=1[-KVCcO~s)Y;@-]*O˯7zKYnC81ᖊnAUBW-Խc>TċX77OBZ%?hν<VGWnBt7}4:LDY|^ieBBHP0"tهXn|`**c†2 ߜ9ޜڥD˷qz8hg9hy0cF]([/:!{%;FW٧{Ͼ;~Jͅ$&Of^){fW{~C͌*ȍOZn|~ .beXi'iǏhF޾C; ](e*SXhiP*:SZxV,C]UAϓcus7U(S͋<h2~}lA*tyZLWx."}x>Kex@T-$t% N.gkX'b-2e2է9߽+ kx+A7AÅzaqx7QRTAŢ6 s2۴d<<j݇ᅨ&93y^T(!s}>V]$:y$Ї hß=&ig +20wv!d́2kơgh`[~(Z,WB{vi2׊S?6Vd}…/O\>'h˩0e˂EpY)LcL$XMTΕtw֠}JTߢ!.n-q~-MMaNww 0Lm /ִZ<ӑV?T`'`a hE^4ՠI0: cLQ.L{\y~8iݚߢ28n>"ABM#ɉȧA `p?ۦ&GxοM'l1D|@-R^_a9YʯDɯ_fS[ge+&\-;W_tӶ-{bׁc\S4a-04v~73EbF `BL3.amw3j!Bʕim@ MY#fbXQӗ|G~fZ枖U /&彐 L0Xι\;#/3e4mfip^vlS8s ADD^dzdX\qDǜ_;~ـ+d*ѲJ8̊tvcZ\NGM'ϦCD)AG"(B["])g`kO/z1{bgbO%#&H"#Ln4M>GNG'bi Ԉ022L'$K1t V .#Ð/f[YG׉l߾=Ӣlu2ȉ`w꡼AC)Ր^ u&'}u.Ą gdٻb TŕFjhff Ea)f׸DƦ&{UQEsjhWD:,V[ VS!B?C@T]\)Rgs%%clO1J`k 芖 - re6*z[ ZNY3gי`tM%hЗ=[)Wt"+:߻`1UW&3h^jFѧ\mhKf _GAP<(؈YQO*5IhQ鈸X gl-;!K l9E|=޹u MpOA;1]9oAW~sIqM_!qĽ98wyiB22J 9IGanQ@Vݬ9-c 9^v?=`ͱ;fҧy~Aj$7эͭk:؛1ݥQQ6i)\wx{ % yyEs "VK$ޫ/'|rΊkeSCvFfQBaW kb[$ Q{hݒD=N/_@ѲO*ͅ*(gKN(wW(2DPч$`dž{wc<AJrPsny =njD]p/D_oW#_=NTY "Ӳ)&QuNYJF'YKQ!uChs3LM7Ž҂E5olgi؋ؽy}}'#%%>ݐ˹չ0Щ<Sߑ ;=Pm1-{s6{*,:Z5zL0u?c&m~cFAS ͛gfL)t},TX˥?{ߣi3cqw>\0)Bc,Uo)-1?zPwʻ}V_(c̉ҝ'I_>Zbq}ej* D|V9 nҲe @:2IiRޅcÇIY3|"/| F= ~n⋕~pglj8x݆M5-[3`M5wA1} Uqc}ǟ!1ЭKY)Y|.@}щ vϣu;,77}"מny _NE%C7Ő0<,8E?mLC7NF|)j9l:æNYHd&D")C~#?Jf}`/NR1ʍ|$pE寇~vDڅ #L*CZjd26(a5ߢ }me)q@N'-I1+SEGcl}cϤz*$bPjƙbAAd/yJ}_>i >HG/c ñ?ÿk5jKF J-3Rf$CNە?󱻔 ؟u?m嵵@[ pqqB9 @ &UZC&)-Bɉ#`zc7hǧ~8x 44L2,_ؑ~1DG6rI%8'rh4p۹)\8b:i\vdpJXrXU8҈y¸5x4b#q\M J $xhO̝v F38Mu Msf,`S̡w-)Nd(zCCx!TmahTb̩EYrJ]5JW`\PRZbjFSTAOK1-8WPnMO!F01 (.F^i >\5d/61劀6k+FRj b"0fkmG6}U!Z6Q,gO@IDATR*!O`# Veqqr&(@XG|ItF|b"#kVA಩*D_u{RXU43TV9^J@5SOWNO<I Q+i؜-.{F^u|h{_~Txz_A9L&d{?%'~ٌqeX̭Y5 o/39gy)&{)R_MX_1&j߁}05| xt>yX\&tȇOFAm3 % {^RJ!#D]goἑ,^v&PCaP.ЗaQ~Ce7b$UΊO!>|k`wAa=wQ7^!G;)}uھi-hٓf<^ԫu,+Uvݵz>uPt`k>zh'`cTQ.QݻA"h >N'X+K45ksڳ4?UaΛh^[9W -HP '~;BSoVW[9:בy(MK'Cx*@ PJ8x~M<1i|4xE<%4+_ca?Y*s^_u3+rC6vU-MS'O`]tRV@֫TJo:;} /[h:cX 1ˉ~Gꅻ3HW6dG 19 xcT2&KUyh@ \#KWaSFR9x54r]HCPϙ2c dJAS K@I/SQYIE+pdȠr(U0ixoDZܯf.X^=gg/`}5nBx7bҨA|n.jq|=X\NKWL9'f}Yʐ^v*#̹\OA=kL5x t}')*430hi)%}$O7%(owY}nd)Ѥ0?HKO  t.֙^5y|绹w;9#O>K?yA̚>somg̙9{k/GnC꽴Ww}:T?'miy9EVe{5+k2QؑDLzR@l*F: u sJysnըJ@i[*c~57)QzVUZ*vtM'~.'';^KeY![f\׊>WU-!(Y #L>J\jK޷Y܉ٳaζ4~\u!(e(ǴE)MMQƲƂMmܗ̤,64ȯ3")W-YڅnFum%bҳ1,[PMՁNNQ`h#;w { '%$Pښn 3еMV5%ڙs441&|/?˸އr^U@VU>*]uVUS3*9Yl;i4u#.9u|?4nᄀ~A 6 t7G+2"ܕZw~>x)^z!4Qa* Ia];}})zA=z/aԄ*%iҪ=A` O4:Ё=Xj 9R #LG,Goy=w-ӧ%mo/?E+g!<4 8|~LXݽsEgb۔8;ٴkX! NoJUۧ'rEaO-U(#瞄ERBzÐe;zbapxDyؿFr3CVtz Uȫ_lLeCUgN*ߧ_Kɾ69W`bZ@Tӧ뮾dFbS.蝘v-C {¤UB \~!Ⱦ ]iN`W~HST/>Izuׯ,-)\KA ;:ı= OMr;SI5 r19+%*V^R5IMFdt8vU=L@Cccur0EƼ+ΒQ'x(4~\Rп/r1u$ %0oej'sQ=Iѫ:4L'5j>h*7YO<;Am!.ҪDO?2 "#p/f.GfA6k?Z:VN^ k{UxvWNЪRF>C m$b߹sdvV =sqm`Uik@"hVhu"EaA}/ kOVg.oN>_~_5}.Fb8ЇՎ㔭#sOp\qC{P3p~ZVYe Y%dTx|p?P Gď6uG9:y*OySFc̠PN쮴f{i։у[Ye|~yhӝZMjd >X E;ZRdp%KRXȣOρ~4_+Ҵܙ4{N\V UghPrҿÇ#)) 8F~].`3tw%Jt^|UxṠsbAM+W^2y]Ȕgߪc:ޟn }zCVƉGGqӻNC.PJOU3h_!ieVh\VB-@|J̘<IpQ``\^HMU`:O}52(k-S X붥d+V$7pTVͯܤ{ tVnn:}ԯ9<~H- Uʫq_T{]ѿK絀XhcmY[-@Ѯ+J+zŲ&}mo9U$f$6k[hk-[/*w% U-}S`ei2fJD7MvN.4qϗB,pC1Є!'M0I˰`abPGq+9(9Ǻ ;r*ٷb+LQޔꆮ+> |+c͸|?FY1duiCTlifۮIGyRV_P=&co@C E-q@Tfh{^Sæea$jJoב2buc ng,]$Ŭ[m]H_Vf=k\Y4B.nHKq͖/ejͧ$^(_s6x7JjGt Y02-+S7Qk۪j%J$-ŖL ).B:海E%jiS=i›f|8%b'j?{M=*wH:6|< JrP#mldc+<7= i{sמCkPKFɅpJ†8zRC OB6L2=07 <`$~~ "h0~9 _X4)1 t$yf"'?v s߀E[;g@Ÿ|) U5SE9Z%xMԠ"NTrU1hk [gixw}0(ƹ#G`>Ə .]8'] ؐY{CNTV T<2REꜪx\)(Z o6~ZfW7E先>߷W˾Ϻ ׳R{F=11dp&SSySå9"P|pW ༠_Q)̞ۢ"ؿ0_p3QlHfS4/%Ӝo'EaT:bڔqH?"L/.JJh~:Nޘ5&l,VN[eab ,x:A@R |P !Z8g$s eVlP\v`b)3ȳi?5sFvE/@f?'΀O)yd/~"џjW%.B앹\C?4Ƕյ Z@ӵ n8gڮ݃gˏGѬ\c\8IڃG⹷ӄ\/D}{ +KHgP9gg,%aUXR,*.1&ߐ/)t}/3O-s+8*d$}Th$K th5ؑhR op! m_ ' E2Y[# 6G'k]QyCf=5EfVZ2W_K֙ڳQYA|\4F2\~|1 6b QHHY֩ UuԶِfϫ;2OJPuTFH)No֩pZU4_NsiԸYvFz+ 82sci a8bدZ$Ǐ2įPajmHPp'r)_7߅aSS U?r~}/Z3k>{hg[ [ dOx3דI}f;fQʤBCh_QU$*ɫ.I 5Hz^M[~~dRw\ЦߢD@( ]W -m5&ݬa_ʚEb /Qg{P : 3M2:*@9-y)ZK/>r MȆu׌U@p+Q9[/eDv\ٔXt uu/̛f,cE" 660$304DqE6J1omJ^KJ(&$..̸a;Ύo$՜_ (Ds[Xٺh C@`O~J ՛V"'M\귪&U'cq?nz꺿LTڵ_g,&"X:^+q\S\oTD)Cȫ搾T4deiywJP&?mZ֟BzY z G;csFҭaWUI6tudVdpKMw+ߌcHςds;E &5dLT/dTה-LCuMU'gSH۫P}i)J%`0% Hۈ {t͙OJ!cS.K%X[1o v4oYTjan.D ΝP cVU{B'OoZ:wĀC)ХP[dT$v؊݆_G~-+yTR%*lߴ^(c#ԧOGğ:ȱ3fߘnYFF]Pj)ӏ0_ I;XM';:x"I~g<\p})Srߓ0۽er߽s(b ع' 3?]cLHS1T1#;vf5@٫qNU2l,-q|tn#,=Y.bIF~FT9qOQ5X60rNd.rﹰ5ZRLªqs姭sѠ e?\*O?]R˳oXqQ@;d1QUP4f5OsmƩ3THҸQ} NAWpC*זrPe49i)Hh=q!$xHKYɲӧ]n]r z&1mWL,ykktLRтߘ6ybLeٽbp!>5U~JK9Oi\M~O6=wo( se53XEDҟ#%oJq%gC`ݳ']d[YAVV|6=L1WW5c^ #+hk̜8WFoM4MO7!`uGw0s1<_|47cOb9 h &]Sb:u .Ǟ=րZSM6TsUΡer2,o DYdͪ72O1@wk%#GaRؼMYJ)seup_oMz@ҋQMCmFUӼ1mlk *> [έ[gLau%1}#zyd K^hPP hm-m-I4]%]$e$VJI$&#">LLY 0!-%AIFvn˸ܽ^ܻv2U"O9&t'·f;ggWx6d3>Xts 7{ RQPk ܕӕ Za+u#-,=fϧ4E'1'>PXb *a=rȴ;"xX{v‡Zs@)@HY7|mȸ%L8w A4n-ko )e/.9+a aIFF(ύk6tIaaSn~yi x8&"i1.]0gXa5bway^]vai ?A43u9r<||i > ~Уg/| {!OO9NY$F0t![@-]GQa=P@Z[oeg|XRG!归smDӧѯV0!a̙K)hX3 S |&ٶx;CMgNÄ"UEL1uPiכ5)wFTnR4խ;quS4#gusѭn:JUːY_Jj0{*Ig(0D…гwI>Q0>rZQzX} {ME0# 441Iɯpr9\x?LV*^Y^J:I OoWJ nJܷQxwA3odiL«1gЭQs-27/ y`Ol#zx5Jb/WTbvvL7WՋl-SIѓUB>J61v<ڳJB3w?NwEگ&'O.} CU;/-~ؽKLͽ 7طcl%N1oC0vLݏ(-Ýphj$&f_nm؉'GxG+:tdV1|y~U }t~e~&@G. Ջ:G1-vw?nI?3ݜq غ+U~y {M?s,ZHhz]1ށo =5۾Bشm KSHA77mnG) ^c+ /,BhQ5FjHj?;+,yoO.yI4dۉ00A0M_4&;/ɺT Ki%H?\?epA&4G(nϗ,>Zu\OF#?ퟛS SnA@ֶPEX1B^m"_x-=SpK6]&|MNÐo G㵍Mes}q$"|_x$%^XIOB$f >]jC( ψγJ͖RoɦJ;I>OeͦirN Nr\'ؤQLI#K-zJZ~-dDZ&sq{y:'% Dh^OFլLWFm;m-Z5CgOY{kY¥h2 $]=/q5;Q 1cLͷߡ*2~`8u|l[`hl!`t\P4"Ȍ녨Ӝ`/Jy +lg^(25XCg}=i *+PTxES7fyi1*Z#8vx*iE} ;YxY*NPqPFfh 0n4>:/4/~B95aXt\v "OWhJGd\Žq~6t^^2I=)Ϯ"q׎4Ym`4hXiktWBҌ^A8ۭc҃"ưs {hR2R2EQӑDpgKXȳ+R 9"66.0&0E&58%sɴ˾V=ML@9zJźs+GϠʹ5_+ft5a%vnS'Wt3 v:_/~6$-l÷_ qS.o]Mr$D!.;Wbii&8܂_wPz96󾳴@x#ӂ~hy{Y*G_~_&WϜ݅olgq9f@SNpJ='hq؈Q0/}cg}Lhio9QHp fTp5}j S.io(YvbWhutQ.y ?.?Ca-YıYbzW65đ)o~ÏoGFM뱰j%.bY咲}czÿfPPpPX>Xu="}\G%a)~ioeqR%,PZe%(otm-[@&#IhFu ?]V/EQRwMCaf4L y:L!N7Tsd[TF6Cc k'Wh@x 6U5b S4))۷z:?\ PXBQNpZȺǶ Ә>F".{q-;c>8 5AYĊ>}4*A՚*/q`nbce!Y4W%ژs=L4ԊYӬk0<2%fPCqZm]rdڀ@c,q89:G^Ic"EF/2*IϾ4AmJX[#IƓmg#S4-u .6  DQNծm{;{ ̃QN|!MLC.cjyźsrBGo|Ūʕ$NSa'Vf],^Wl?W&b=0޷K"4-G-=B(I>\gD(y1'4֝kÔ?Nݻ{-NS,ͷޥč:B*B]tbѷ߼ ^zYU7r![֠H'V |WMuwt긢K٤U4O}NrNڳ9/ߣUZsNE8DA85F߅n{lu9>D RCA:r gOurF䚒&3Zscl>nFEVߛH!Hڱ!&v#U#%}Q;¦5"gc?oEXk~5Xr,z!lua8X^MVX$`U(J&X{m,y,7C,[ފ?l<}vDeM9_??zM?'Op/e7r2 ]C` ;^Gy5tvAqA.^&$ݹF{7XjSnGrLzn#}(5 ;9ƎG`h-y5{Š&$_zuL%$O0tmŹ ~j_&e|+r| Ƌ&ӎ}XoRw랁2ρ2l d.KEX{:9vB]W?t(jèFCH aO@_ׄℌ?x t.*%웏Q!>x^ƈ--zk)V%v\Վ)cZq ?0^G&TVVި޴y-=k5z2#qC0~LtRb<ؽէvpo7G-Ӿ-oԿ^qw&_uaoMp?"cX6yX٧ g<}-47AEdٚ t٫?O{ Wi)m\|F8~框{ᖊhO7"@wήoR:NKYw ?y!! h[@LdS%JlkmLw?t@IDAT݄eT\jE%Mjj99! [B6UZZ Xu`a뵞[ppp:Xw Bp\Jc0f,8ElH ed`Hd}TT=K\E=dga$YEFN ΰ36.-1^,<ʪJJ]lQPX upckL`iXBaͤ$u,XL Ptp" e-ٗ‘z¿[wG" }DƝ;$2ck t% uݦh͞_"5WnOs {2s1l FR诈 e@} i8)7܊76CY+~*de%2MAž fK a(`{ژ js=*vSA1RKOZ,/8^]YqAS];bI3M;j麡}{3dfb0!ִ!'7&d#9ج=kPZm=GaVW@Ug7TU < YaIS݌o{Uģeows ˆxצ6wWڌ4EhQ^KQgtDmZd'M t܃ ]|U=齇H@ HGP)bC]P׮ޤJ F:I yLx s{fܹs߼[K2B&K <ۧ5\lՏOwоuwcXo*[5f>"=̮sI F=mÂV\e߿͛7ouj&[Ě*cL{wOsmuQPD%S}|v0MQ~L<^~WHYro#Yb)JMV!ϗ(6btҒdeO=ASN01Ɍ+4=X _AaH_\.EHHIS C%\/O t}f2J[s(ħtgބόY qODž?xx|=ʍ{'| hӻ gؓdB~5F Wv0C 9,461o M 8.H;ܴ^ia37\Iȸ_ ggؽ|)\'8[+Jon 8D7lK~o~ a%h]U,0cR9vag1,#(:m*+@CǠg[@%ԗVitT>>wutFv;_iHlCn :vv19mgҦ=6gQoz>\:0uZGqP\HDlY\Jˆ}37TODV~z 7O"ޱ ,<82Zpա`iPj]c9ff7ΟAW $~t% Δ ºu__ -uc{ɊVY/e"}Zߖϑ0F5sSQe1CX`밑)=ڴJέo=ӷtsmsTI^s:3Iѧ^) ?K8~=[dۆV,yv)o`MKtp 163&(efLeSa!kAn&Qy[ǎۄ* W^Wl )띊.),L?+ȁ_1i. o8׃[Pޫ?PH'e[H'㣫}Z/f"}]k'|r`[aL5~VPXz'4sa~emQbfjbRF|I <ʂwW!_`9R&!&vݛBG+ V.xa󈋋q~4֝dL_/qltKįϹY! <8z$F=> .n]ǙZX3 fa1ڻ8s"}~2|[sn?,\.'~X&@D!8ːG핾~2WòUyqLQ,Kttr8Yh^D@{*R TUQXjt4m[|x&kֺM  / 14:"R dQ1(;5@S -1lk$LB21Tꅛ ˴$Z9*Yy%Ji ]ȑp z([k{15--)٭v7mЎFgK̘6B j:99)AA~1 uѹCy}/2jWچ 7QYO'$Ά(=D3y)\~6;Ë/{It831QJ;qdHN?x8Y4y(QgȞKER~ * ݫ {~q$!E EFmAG$#G/gNb .54^ƜDɃߺQ+icS`FΞm&p9\ӊsщ0gEE 8tOτ+Mzj|(+N/Mw6-VVM.vw0:'M!c`P}B)c鋳O#;#޾5~hg2T]7 6nXCV|&QN%ئ'ɜb1c'X E)>i4|.B6Nvc FyM5`"Wt$#)π[ KS&AE"%ՇOɪ}9x­ ߕ0v\̃|["pw,LYsbG6q qʓ2o:QZ`lCuQ.x\W_ln{g]$3 \Zy.5]$VT+ߡ*&߫(='f7! F#Ww -:Y8XtEy.պd=?wD΀H3&E ne't\bj5]N(pȫ- zo;᥷{ l^C.1 {qGђx`T{+/ic^J٪F *ۏڷYk/{MG(Y[~¼Vm\C)u#;"*oT9wvզ&1N--IK/aW߼D',#)%W s1  {`pš_~ mOSCg234+p>vYtH=`KE"5?y8qYdAKR8XjKGa}=nGF>2aej=kcFoC ukoag9kPp NiELuKۺ@NY>;59cV±~j5ZyBvE=hߒHxnmab薥tS^wZ簭-lX&',1+Eزm ֭X=ѓv`ԍYiT⨀-I{W#;RZw᷍6ePp]odU{5Z<8X%]Oٷ, '{YfJyw2g8{~!3saNǾƒ TcRj8W8kYe5C Ї}ݦ-D- x 0[[߮*!'u5|-7VWD廯sS h@Diyx<} ر]<5d66-ܐɐ+\74뜔1mpwwEp#cuT&}]iGZ4} >Se.#e)sKNo/b 9ߐu'sf#c{_iFGN!̛y2}j#ד>Atu}!eI抪b|4V^$B`s:ZiIߴ- FCŸSv=iJM P& [,&Ԥ hc&u1#ӧմmjk,TQ,t-T+pz1\{MgY28 +Brex*бO>؟>057.)EWPee kiA62wV}R.Eрj3j iw%r~^8sR]:@Ue5󀷇5\3%3+?'F "H|;D.p l84O;LV#)j<.tй@Lg0<4h31<[FHcÔEΝQQ/SY42n\XzⰓ¢^:6v,';/h|[]hNC_Q0c. FId/8e?1C,CGX@VIVl o g #]E !KfʮJ+3BS15-68r`'-fo8Ѱ|`%Hx{"6$.]7LV(5W*\ç|QXeqhG HG Mp8M w܉~Ύrd8^>).cv`++P]L.&Q,TХY`~Џ

.b΀fy/zV0L_u(imNb`$AP2ZmSun ES0 992T/^2?>a+1Ӗ' {ل$j":nFQ k)3)pMwG|I<oUӾU}/BHVѺ]%Lism\~*8,gŁ~JM>#ˣZ"q|I:kYbm^`'p`@> а *M{$hGpk.}cf Y?]|:gyY@ެOw{ώ^9>sH^z_^H[Q3Fn3_p;ӥkÏ>^uo{.~vVlrT\b06z\oyn;_Y\hUTo#?bkJ&~|ZUSҀ+Fy7:q%>~ %{`sUHn qIΗwu| it B{4ew9>5a ~/ov u,`/|L~MQ?=14J?5|U@wZv lV~:=0ūk{f{8wdE<՛X4Xݑ8(y!8Ex-`ٳгL$اop֘\sاd* Yl}>_h3{>_]žhޚ؀ѢGHBOe9ʑ>8L6$ I'eoQΘu18Q0\- .4|O; ߌ+ݗ_g`%d؏ [o`gljrT-"ȎnG{ WOPb|{f/֗_|>=mܝ@+O׾ ;͖6.xi;I@-U)?{&XT翂,6 /u_q"56FPNDS^G䢾'÷x3ޖ,;{+ Z} VjO`ߏKk ~GV)&ٚl?Y7#76L-#GF̺+# ֊g^ϼ >1z5pl䱣v|㏷77Ur=vZx{o.'ЭDf&a]sGc ?̟tc>įw@{/0rfWG|6ɨ4I ǻ,@y8$ރ`+;\I}?A֊g||,]'MA[wŷ'%^r5Ob4"Iy *{A=~j6Jt zhO{Qlue;+xd$T̿΢a?|=K|/k^_~\w;po~%3%~*xGsJ(XNu6:old]s/Kt$M?l{;v\ |GuYTh^oZaя>=|P_o]R|}Bw~byM脷󁊢dR?^9 x~K˟fUqmٵ޷.~_?*Lч9J;TGklIF>?.k#'B3x΂hMfm 9EJk 4')aߎ:>F/(/+nH﹎B?;se@4o~d/Z8Տ+hLݞ"+ݻ@[:@j{"Z\uu9sn9{);Wq>s*:s~o|i(H O+$'{/U?䙘:M߬V߁h729 Fg2r~U)n]s@̛Ԉ1@m w=kNP&vԒxnN\n$_lo-X!tak'`$M#Eܩ2Q8'͕oωq-X8~TA0F:/C1f_ Y5Iq06Qz~de457ʦC&82> uagB`RҘeQڃ% O0<|\V-rbHj su2>ҦC`TMLS z bYMʇ._#N%?0FlNΌgr[)µ )ӻ^ ]Ū4)p/­=͏b"0 IeWmmoT^^es k<+ h狮e|:h-<3!xҒ^1U]js|k}!ڇ"4m# 8F$= Wa=˨\6Ɓzج@JKڜu?ܴ`~ЗjLO%\{ZLsB2?Y =˜Pf_zΘ/s= eɄmaK 624™mR8,Hmٜp__~pAn7pq km3jg% (lra.#I,@UYyŮ_Cas͑g!#Wv{I0Btg{tGsr>'ѼB^exM|#s~JVjicToۿ';rB'LŔ_{=T\Z6Ύc_Yܸvlc70[7LJ@jJS-e>lN_QZw1ֺ7Z>m~ 5 >5nĭI)5I.ۣ^l&'d3fdT9|:)_p[;&BbvDk_{n;p`5j>pfC67L_k#dߓ{< Ol2G}d 9><<1ЗA&'g($a_ 4]qD$fu.t(p*?li]5|7 '.0zp5vnduZ 7.g.*[6\G%atd_-H}Z Qk%<.y%2^">%;.ag:xp''ɠ$|X $IJRs?.&mx*'i\Ѥ(Z6瘯Nⷳwe86ahqyVs+T \ϑW3GGǜ] ΣsKI=Jy-S͂gާu잻%co..S;`әwپ%Va:pmϒxS_ Bw4o}7,t?Y7ty A7+Rk)y=nA峫7w.?;w}-ny_hoV?*p/_Y,yYWU}ǎ!o+?Zhq~R>۱ǿ1ho,Q{v'ۡp#K«>r%V`Z6{i [._ldB)rwGt o;Hm&;vƧPܜ`f<>6|zS2R}+윕~ =8zǧJnܛy}q݈/ˉl3Ndݣb~ʻ}{b'7+wyǑDu+J3lEt8ޝ8yS>Œք:B/='7uzvbj~/5y^<~:$n㛻dgqi{v`WS jLiO1ą)OK 6ʋ2# H̖ʛ8vJ[ u쁔m-&`V{-38읩Eaxq~{J" {Uo`ص8x}XgLFJϯފ9nO;s}0b+nAx7+`$/s5Z'G`kNNgcԵcc!@4k)n*ܵ:9޹8B%nyyTѵu4RUh1s&Ӽ~OnwZ%zKsd(z/Um 2IQU f-xP .egBQ--:3R,!T꘍)rNWDY T kڞBǀɢZ۶0u'(J[R"l\`תBv3jB8Ds KssT<´;־6 OWP}@궔}t̩oY:9yoJl= Tq\499OgꙜ2N(Y؛ 8@$w6ę%<# LdܳZ3cj)pu/y<@-وSD&2%b\Z=#^<9xD(^69ti|R+,8ësc0~'CK@ 9tAuIHٻ-=*6rsǏ <;uVǝ) V@<.%Jx+ea+<~dqYi;oR/rbvӀ%,OGhQH%mpT1կ>A`|+ˎtuú?o?p1<:g8^=^X}v%K2v[ꌒҥ!D_ltcRWg#C4Ys ^5 DvϺxdz&dᨑ䥛dET9uӞqɓi@Qc q_Bx4N;Zw<%.-ʘq?lIt'dz( V =/]Pg?'/2D ~>ZeXw3Z$OhgS=n[{g=5ź_I\l&72N9d ?lp75t"t/̙ĜE"Y|l͛,Y OgJIFKf!suO9,ݾ^8xeY}jIxwfz;.A7dmF.{V 2RUB.u.FgU1tԽz]\$2FW-ݣ?$ۛ!: oBGuױ`yϣy56hg0쳽lOCͮ#ޒ#/7Цk'+ڱ#mf{i\5B6^8hBs5)8׺|9$\hg v\Cs|>T^YF}3 )x~-uifF\ A XVgݭEn4is𤮵1z* X<ճv\[mEh( ZLp-XgJ|3(|Ƿ|b a~q~ &lxËכ v1g'9 nuwYkX0Nړ|=¦b+p]w]6W{FU+⤃G[;dތp˩lJZJ uQ8ujl[Wrvd9T@}_:j*lGpCGpZڻT&`/qHS1h\ }B$N|v7#\ؿvMG˞`3EgNkmcx7VW5 ]&6 }g{ ؀z U&qpfoū<1Y> =Ҟ$: ޮkÍw>=^ }ex{u|:.pq$Ϧãúȁ <'#C.COѾ@8~ʾa3]WGGjgE1==|=yO\Y+o{N7m2ћUv}Lg6>]Z YG t&utߚ=/~1e|55{"nƧ Jo౾ƴV,c 0g]Z[-g~=Yk8~FO}׸@653,cA;`%e:1|/7Ûy Bh/" "xۉc Q.[KH ׾+Z_m|oM1\I;UjƷxqxXde4ZyfV{c*k0z" C/ckڦО>'&'~ Lab =FtMM WFw6pѸ #%;muܾ(|`vβGi ħevW1nO8l m_cbC*/koS_xs%u.nudA_]N^Ms Xެ5鏖?~rg=uahw_9W%u3dEϊFp$Ԗp'?;usn)XNؙ2FjR'F^Gk<V$1>Zf ASZNܔL' pKǺp@g± ƲOsa qoMkJGtoP޵Au kŧ@p9&Eb{BvG+}o<;i11գ'Ŏ,(M#*k7ŲWg/DgԎ)qX&͸a ֲGxx!V86o/Lsz `K̫G@ܣ⮓ᶽ^mɒ^dy8+:~GϷy ?xFs /(ɹKGHIč:y<ϻwd#8-~KVrt1ۼk[ 1vgJ&(ZbA@ hޜ#^xΜ9^ ;u!G~?(ᴷ .}3L\`.aV*>aC}h 6PpȵefHMA{^f׎>Z8KYMcO{C}霂7f[?ֿ/tE֣T:/3{q/o^&0u47?~*IΤ8zorȔ1@d(](2f2%Nн&^w3ԑU?{r v_hqߧKyϲHC ޑsSQ XQ9Ixc67e$49@{jgԽkϥ5ce{5(}ќ%)LJ_{$*֋z~|^hj%x2d#b KJ6|=Vz"?s1w6C _uXrÿǯ 5#ut͝1كy G5kakBztu5fЭZHkݵ/9&N^p.F@uq€ӒJm:߻(WBD;PdԫP(,(U0t0{2{9%A}&@: ~: ƻVkMx/#־gVZp|X;H 6~\Z|rFN_>|h$ Nxϙg` ZSQosG'*Z O|W Nc׼Ǽg Q;zZu$G)(87z(}q=}J.yώKS[pۙcb.|B?x2xAW-''J Ԓ OָoS*ɺ&כ~;3۝ +3A #m:_Un,-jE=F޽ȳg:?ϧ|NC -M&lO:R Dl>: $>Lq2µhz¾3 /~FLcu|[OO~Q]vtd~Զl-i =/@Al盏k.[ {iխ%f2|yp-r1oYj?oK{%c[6ꨣ{}Su=:b'vDj`:Ov2E}kvUlM;f˵O>?y51?DcǎF_kG_EUwJ佶-)/!9UVHgo遛"pN(F>>($; +"Dy?HI'rN`ގs%B\.֤ Ɵ:vb쪇&IIr')D;vN=w OR S8G+&P寿}|_xߪ˷ȿ호E I-/}rw3*_my?p݋9ӻ-otuG?r7NNϟSg%;2T eY(o,|JPJF $RUs*~[Ƽ)fT^AcSCh;CVYP"DrD9! mUǎ8/jo$AOhk60>qx,i LF<|y4u+Klm7zcp`0.y!|g|'2a?x#7_9ۧh2wo+acˡNk< _4_ucygδ33gSwf+wI.HDɱF>$A#@>[A`ذ@ ōV(*r}g33q )#fS˃xnt MZS^}Wf|. C<> +&gJ y2=mt@l? %2Wy Zlcme/<|OGGOY>BATҔɵ=1Tn:8q=Fnk kY\ fc 4a<8C te&8qNci WmtXttQU:Y{:3BzH#`&$ Nd@U.Wi"P l}WQZѪzFɇV* h仆c8U\} ٵVJ;%mhooRb#!C,US%m:ePpP> l֔{)𫹿\^`& s&/knAO*7"{2/*3lY3*UU]RB+5M c}WƬxt k`!s4r0 %ul:<>du)܎7"JcNo5lO9f$1({*P vׄP1w[w{om(e.?&F+gؠGZA2)*/{mgMg]suoV4 +ZSr 3Flk󲮌Aټɜ1eRbФS8&(l'ሳ1;m FǮ993F EسpIV9S6p G A.# ~ye(NjԣΕ  %woISǘ@4s:gAաqө>t_yuU7+ϿE@x$*LP{pvՎe~Ov\pϳKB7t0\cZ'e~ H&D-ST Z﮻Uh򠶶|#:h˽SFIÇK;Ї0-c,څJFC EP(]i7GkCcf}aH'Mtpכl`0ۗz RϴӃ2^O,eL㐿LyVhOh(` 2qR)2,| sL` ]4pOMu<$YM:ҽ[Љ`z*> e>.? `\8uAkK5P!ۏh$ц1Ƣbpqۮn;sVa Q3>lHySc:!^~5t({…izG Ah|s$0V/Ͽܝ,p]:G PDvh._䯃Up`5>Vj(3s$řm%}JuN߹s'STa]o0PU:ȵ(jq,kJi}:2` 5 < $DifK}mN K?\czӹsgN;"HO\BdT UjG<=Ix*G.k̲=ysiD}vѝ5a [Nȶr_tn4k>.^lt42|bkB"OQ$|z'o~'#i'`v6N0[ZG2D,Xk%t7){b NSH(pݤe@tdRec`|_[R}&pQ9&VGk׫$dl:\XdҐvftĞm>lok;H[[iGd~]0o@ur;}Nb| EB|G}jo {(z$=V=qrk> 7Jaխ\ 67V(qM.[,g'\$x܂ ]hO 1Fm񟾯#t6ńv\QwթVwߖaNf*tKgN$Hasbo>Egv*yD9k{nI/vLmȊܰ%?tfOe2t>W?S@G!8l ;na~զޫJM}ѥzW~y췦q2y,r~'yJI1d_TǗXg|'Y"ϫcu>mtom_i{ {^Rqw5Ώx8Fk1젍M$;e$UA`BW؛{-;)%4@>cM"4X<>^"3g5ʳ.L>Ii-1=ƵK}S[;\ޕߢ&%ױG<F%6r`"*IGQ"f /G4U =vSxp+vC}bf#i$t%vT.;VO7YAnBZ>ZgŃgϧSgwo@u]'dT$`g2I2d|z.\D$֩TN3>q/]|7OPHEK EԘ4c0cYFRCՂ1S_ʳ"ιj3Ep9 hЅq?*c"H:  Ekii67C㣆65G@Oc ea,BB03  6c\9s)= ,8ӳޭƒ!cPj Sb pE( .[M srk>hP:A%gG# YANBԠQNc lkJ Da#ւ-i"Je#SϽ7i`h-ȉd.JZmD*3 nPAZY˂8T1 cL2c粉#IF顾Z~fI%q[6rv~J  ]TE; ]UPfl砃ShV UҔNT=Jw-Z@a rOǹ€DirܕLpR4lq,XCSaueU}fNbta O[9faz<L'x鶁VX%y5UYpЁYOҹQ*+3mہOorUfkFc@FQPPi;38}r~H E{ã`!k$+ <>, (O҄IEQ)1[ ~h8&O'1.}_ҹk +H*lor鋭hBA}r*B+h߳qU,*%%@%VY'G۳̱Knk\PZi2i*c1VD6u>5\=gFwDۨvgV4s4,{'Rd"sxrluw|!I =a6^=6ڬrо2u]]0oUn&OVe9gƬGu>rQit`]d8WR Y%[swָg:FQÑV7kV9\]f0Q] onr"Y;X3FQ ` m'rJae-/rJȃC̗qD2CM`V%S)>9n*#(YQꨈa,S'gB>vQ5XZ0 =1t~ ˜s8?NYWFo ;~~.^\w݉} Cb|ҪuEl%ᨪ_s7L~BeC; Yd:ʳ! G!a>r1<; 6G;`ey+`l-TE$5kK-+ЬXQU30w `@E䥩c 2aj!V53!C$$Afg#'tnV.t=>Ou6TbH?mun1L*lE3K\θ}n,wAg!炭mA0vw`."HG:ˎ!;GN҉(1اq)E% yS 5kYBw m*T~_`FLISru8|9F]7[C=nI y4lO=*8Ö{ўm8 !R\pg{{q/t%o Vf 0QZ>c_ ܫVINF .tQ h˙XyiY2ûFpMVЎPX-ՓΜ%qgkғWJW?x{j"_{l дml꾰qt#ٗ֠#ԉK׀RB1_M`x\w`±^e/yki8wA_Ail$0X"SSk- &Kq[ZcVkdiFsχ(s{w'M9N^|yex#A`Uo~%{_B%h_p XǓwaS6*`K+|3Dn-u h5yi"0oOAxWώMa<yTp|%S_ιx }F36=d{Q{d"#I,3=/m AK1 Bޟ}q&e]n2^&Il}n3kPuWJ}"Q+SZ=?ģ XWƇ.`ۊxOVv^fgoooAЀߩ$٦yMLѾ^:&"Oc]W//\'myG_@γ榲=/1O/ܷa"+=&FxD˖ћ[s*R}.ol `=50ǟi 2PaG(e|[E!*)DZG# o|wGޙo| i [=߱(Nm?0S=MS+ Щ~L&s̏KKˬ>`WO/]vp;ią6ljFӦHr/:D:78dge-ߟ Y}t}vE*{4'.gR}(d sy-s:"u <  T`殃IH娦F뜯ED51QZXo#.8.vFլo13ᛊEleRʑXV.Ij9}2|]qs,{2x0[i ښ{`(l^ 73+lձbo׷&keL7++exQ|[FŮxW x!~B|55$|Gifr6]|4?jNJVgy)_<!,ij+@-נŤ,0ԞubhMv]^ Ѡ} ?f;:e6D0߸q$2K=F#SeIGTڏ;N-mK96RFw Ca\^Sv=U1f&{2(V&-]9&Ub{ MOJ,I;ﭝ% vFqZG}EvL6RMG_"?]ieXceV, 64_UYJSq)N~7;;G@+Wb $ ш,|m^|qFA#! q&6O 7'n)SK S[#!u:!ezHNy&>#jS>dɭ;]~Q :+4: ث* D_Б(`_wҭ}lV|/4~jJv@{wZ[>d+tMY֨yL,ŏNL.Xit -U5hKs1\b(/g*l媉Bݧb[5uI9U.3Fl5Ä63R"6T 2 6D|Ӣ?MPRΞϭX,Ď3#akHf,AbKײ{XyҌv\Q ]#F?럄Ͳ8L"HWjF?,Aq;tX pgCdfb*@lvK \V{ Qwcej_Lж`z MCI^At奯BPֵcEhhI#, ҭbbWڞXc7 ̆"mkj3S`,ym帊J{ ACbu*4rtȹ'"ձ7? ms дl97iV|>0UTFsN@QEU:tnBS | Y ,trDU* [&S{Xå` i`=/Y(φ wuJ b *%xaU!IA5NܶV_Y(Gy~Aj; +fe]*n9cw~;>h 4IK.fhn͚90+jeULFlIUry%MNijU-:X(Up&:3OY{^Yn@p!ԎҪ ޣglLLϧeOeA2\7Źh6VtcvӥÌI!nŒCf< doVӖmhFvػ=[2MC5!C Aa[w]?A E&NɹjT*8]'A5ζܢGg`DQ{k(w-٨IG`"l =_twKĐ]6IAn4wBҮG~fjHqyX~2GkDy쳾+dKW[(O.@ t`pHտؠ#FLfR~:* # [=ލmTޗiA*|y;/Le %m rL3mF#_O4 2ϳG Sr]ݣTϱX ``⊀|.kGg/q]XHZtp\;kfZ1À6P#hgETz2d=Dy.:_KjB7w,˱:##'VW(O4?{(.:sL/̑x-/دT:w1=i3JoߏtA?FmpJlGf[2 8[ ]՞4ox+ޡ4;E+6Qph;ՙ%ض\Z g Ln~&K3ͦ+,txURӪv+Q{H3H sE΄b.w\L 9g&Sx0hT}{גwC*HW9H$e^҆?wHqV GVuB^^ Nf  >07uk@!}cs_*s>S,8kI;nZiz{cVʹdЉ=tC2|⾬!vv13w+cCo1oRxPe `kxs-Lt(D]SIn!W# {|A1Hp}9)/Nx=uBqP}bW_|a7PO@!WoKgڔ0ATV?njc7/{n~ݔcd*e$wtQu Rn&$+Oz_!/O.rAyf4*y4|bh|и 7YTyb3&[\q~ =' ;?'A|@eҧשVIȃ7Ҥiy؀{8܎16ρ.ZF*1फ़x X7+|3b4[F75&WA+t*D1RfxDԡ=J0&dN[P]:W'xg$Ȅ.K&#Ag$2m Yk|3OKeD0Yw1C-G 7vRaVֳn>!҇G鄩*Jkˬ:A }c{=V)&(+|;ԵLk&T#wal>yXz P^jLf m9Gȵ*-/mS+=fH٩%iPH=l[h{4%&ص Dk-Ry1lq;=+u:`=Llmd}=W?OX%s :W9q@4n{谑EРkD*Vr۵UhgQ?9*ʡ)͈pwX@ =Hz"_Vg;:v"_?2$ ؋5 ǝEꂂw6XڲgG;`qd'!OtckE{a軵3.UzT?B#S=m/jC KTςd4(Y[mp8EHYE\{+ ԷR.(Ϝ ȒC'`l>M5Gq b {z:Uڙk]瘜w9]p޵^ZU6T=2"c-.F@{i _e %] 3O'01we$ جmт_6XsLԮHQ\b:2.c&IK?'_P.:˞tsr5OK_hYbN]Ts\I{T ;rw{o]_HdHȳ& Vwއ; IP_l ݉O+[cTv6ݢJv|wЎQv4EpD`[2#i\oZ uݹ;>q5 ,-vX iwx1O[lVKD:Gc#C'֋+7Y֣;*)TEPBE6>1Yv҂55Ę >62b=A/WŸ&^x1t^UG,:zҳ/ߗgI=ՏsʧKmƾ}@g9N ;G*15^f''I8o9$TpƐAWv@01efJs='v\'曆W}U|>OFː=ǡ!=s/7bSΐ 4b3Jk<1[->==nada*s0u %YL+q{~=sVgȷqXw@%UZ"xN#hWTe]H/eb_:ok;TBYad@ul.V+JhG*PX VypV9#|8(5T<ޢ^Wc! Tf *dPMpN0Q44[ȺTcH}>EƸk3>ڴFOUxNңJ6< TX' F5VB P٫>gu5%ggE(7ph;3JPej+{8#d@Lu3ԸաlF|Al3In>!-襖$JCC@ajO(7d[F" Q޵B`uEm\Rz`ce͏psF!2AK8y:5j`ݭfoĽ>k 2+HZXKEitd#moW߈[Md,I )4 [sF*(Jp Eϧ&kΡN2FypP++QD!<샽0({-Щ4"YP\HjF @@ld+5¸dw(**G&#F|>IG` xg~F6* eWE>QRS79Mfg ffY\dI{ʪȠ&ySPWΰM2>VPJ<2A^fu &$B`\VU-(g&d/-c ˪qD*~ = A Uj)3Y ?G6йBUzq*,c]F$S"!c8nX+ǿ<bc$p#3aF[q]S;-Siz0}Ou/ab.};N4z,xDfvz2=K2zpwtiH^ ѪgalgGD%c')Q̬iIsL;±Hʕ0 8Oҿ6 ar3f׾ODmH$LYUVTVB:"|ce.:jTf*ȃ8>_ A̠/t apX[ !Am9|#W٤\1 40GgVHHTǙh'ؤ-``Uq9*bL(LsT+˿|G{ 5r<{γ*vH}IN-Tld ~!GQI)*xgp{o!G P]|󺱛Wk8OQV#8Ak+taʠg>s~>YCrI\@#8u=oqA6va!IYE w-;=gq *}Ŋe} f@cTF&7H'X/u$f^S"qF$? QKܻuzT;4ne@_]T:Y@]Rtv-y{ ]XE~^T`~v]v!LuW J=vBr}mj X>,qgH30[gݦѐ~GyHkxahX5W"I/-msC./XX|,3yGfv} _ ⯿s >d/ [A&@9XQ-^ h)Z@bP!kA@+>C }C/[nRWgƂo.cjԏ&( ,FFd(h?/2' 螱ϸ f[T? ߨǩjN|?+cx6`Hv :Ow 60P ߇-zgyFB<'o74mKm#L{.*8rj<im0'xJi(&[YF֩=T>L `|Gd~*иL@a;"N_,.;QPaO uk}&:ze'^;8~^YG̯ mڂ]`bw&{qtoEA62ȯ?* o-KSSܰ0c8vfJt@IDATgi;p=xD& 3hUNL "e Egv&lm#L!)$ Y":1PapDA33!LXD+z{٧rz8>SZ[$dA%+mZ8$]lD1J]atkߦ&|s|a+ ]JڮToqvtǤ0\k>3`snxʋŸW\V޷={t)Y"JE\9fO2B5G!mh] 8;Z /{}=saCyA\ju0uxi{ti5hXoJC(æʙ+F='kK5]ičLYAx$MhmcU~ý0Z:`sRxkk77 p]'#ɴЂvv6 !Y>4fdP xtGD=R`Eg2젢s|bL@KG0ؒ$'a-Nk{jK^x gvq8vD38Slf Xekkaǔ*$)U0ѧbV8oSYI}Y}= s6#>S-&[H`GiR lN}vI~Z_iH%>vd :Lr6*2gmT {wO>&> a?Qj7&l7 s*X90s  sA\o<nL7+Z ^8d=Ϥ;h(vr`V[mjP"]L'p`rj>o%hMJ#a0 C43i`\KTC]s6U Ͻ<&E6+a{î>)>  \&T ^ Ae,,!$^a]zU٩` ̀3k-? BSgʺF jhu*8g)f!HPEv/K'vh`bk!eWKY\ݣ@/3gt0٤G+ϊ-TfQg'T8f[MYjDV|f;勁."(>"gAtv6%0/8_QBCOh`^W??MQ֘]X$iʣ^J8(YdM4Xh|.٪ YY3c[nb\.8IfX3]_m0z^4N`Όc2FËRy{wxx?M#~8l}x[yBhm--&#(mD0Lآk/R̅5`oKTz۷3 /uhtGLsquZrR;߂zU?ҝ6l;ڙJ _зQQ-Eg1;i}nXZz5Xc7f/Og3ko|{MgSs @Ь@: z DŽ ۘv2D;;=")VDĜme+mA0`}ڥrQ?1_ecx^ $OȄ/=#9̂k{18$ EE"p[I*u tԡUYWw[uDVE \]ٹ-7&NSDjOoCJҳ]MQvJ f Km{srGPv'H]~<]yLv\Gʫtf}hiqxKrbvV X%zڶNܠ] ,~Z^$Qpz2]$]'K~IO+}937xgubnP}B&=ԐVAW]beB61;E $h7Չbut2*KAĀ3`FleIր 3:ZLmbW ^+^CH`:|:X}TVl#\&,!66rw΃i5V.R+kibxf;7aaon03bZMsc~i[סE Zb)sNРYے^PYEC+vv>G @2G#1ɨ.~ы6kj]ZG[0kX oj`Uw[0)kNs vA>?\czKVOX_gJ+G9ЁS? wJC_߸3./cNIW`8o ࠿GrKpX5u=PR[?ϮA +6Zi5+{aKЦg=g- Я d;\*(XOM&5ӉAmmbrR|«,s Gȉ1|mRhk0Z}Nc_Ob.000!{K`$1W|!%m! A4[!sfEyT>@kEFXOVV:siVzĔċͲJP0"r%Me b$ 3!]_kN oٛUX^Xq&; V΁|=o1A;$-9u1%j@lhݦl8ǃE3~53Ԏ7bk2Ӥ!Ȥ@b% <m4?h3P-mhÞԇÀzB^7X'f/*ŌDgG lm\Ȥ׸풫c<:G86 :T]?r=;W_H_`ǩwTZf-5J;ÙcĄHة:m[AsmúI=5sY KǖE~X= o[/ jf|CYKe&!AR) y?,y62D:V<,J2gҵJ4l6yk,G09=HD|L;Y3u}Kֽ2y;{GǾ|,4F;\*gVfZ"{MVU8|yhjI74ՌTvlf Vl\ F#Bz[E3<c`FĪ!llYcY2;]r=xB$?3{VV%՜ۢA8ƾ`c<n{-kfsU`kUqg Fi1 F[&}\' u=0bt?T>A|a!O< 6pw4ڳ;#@9*Wu6h`U^,S߼96')hC}M8 cfyL(ј^ξ TVqlrL83]'x~&s S00i#=M<8HV6&LO dvsaiDlgcz9Zei?|>h:g.̹ _ 'i>3 -,me6Ix2O~&EPNАWi"R<%Ͼ?ŸsȲo@Bvx avzy3sEfQVh# S&/~yj$=3T҇4+pd!Ʊŵ f~vZ*Pk$@&*@'2d- V綀qmEvZZ$Ktez*S/; DHv{Dj]yoIpVI? ʽxD΋#8i5}>־0H3 :O=0zh)>t sWZZRwyq%7p;`ٕC{VSI d =;!`-xbЃ6OLFB u !露‘DZ&[O| ]r={C:5[>73H] ndu+z) IE# 0qslMX_|YOrC=otyAt$>7N}]/'9J%}%9;1+Zk:Ľ^F]VIi(gn&ʗ>z_ AZ* B3f,v&M7k _@H?ȖB&jk`g03]K׸ S;̏uJIiF*$6>Dw Pq޿ pص9g1XgrDHMT3"_k*ĠmaCbG=tW龞5@$VӬ + OD؇C V~^^zD?!Ȭ=垝S!;Dҋ`6kb'!Q _΍7#3/7Em2Lο%g=>Dc= t+c a|3~kKt,QvS*SO_;QH I5> `fJ#K:9^ Zev6-o ] ~8X?E/]# ۑD]d~>݂4F56r\f Ǒٍ}Aު}v#]&Y+YwmWl[2+=Oёiݴ e6N1Doҋ_W>ϵ"X☖+9{}2@uTФ6v#sg_?>3_9v r4t̱ 4~?lC^+( )җ#y|c]3i ;\AlG # TI/\o`UА{L'mg*K4@C8 vRn셗Ov.NoVKsư[Qy,L'MbD][˚> '6raU[h\<^?Z[y mpٕ$2 lOu-K%ǸowއfBOP6hNM*s.:X[ӿе*Ė՝ x@Evf/2 e7O$.c9H&?7{ */m+ %mF`?'$&8נegE͇x3сMrH(Z 3Jvʺ! y6Vw (ql ʬޗQ=q׏v&t8oǰffjk.Pq?L5ؼHkW¡[V b_蒈~{=t~*IGpa؉`UrsD-YbT~ҟ2>PjhérN|џ%:TߩOѰfҏ1B#tz-0h4|PO1}`h,x;ueA[$0c,$"Ok䳺zS,&7!Ԁ/teoLZlCxmϴn5G9ELd[ qE,5$iˌMCLwB]vQvʗ.˗Ϥ4ƑkTWwjd-K) ptQ^_'9;:耆G4/צYAQQ=AЛH$`'gjC$5se]e-s&;ˑjM [k. 5Ɩ{vok(Sw]O|[C‹=&WT먞V6&>Xvi6Q!@S7މ{J-2ZROVTBU?ӷv[od~XZ!+( Bx83O2ƨ1S]E6uv79Hu@>c/棾0ED|E$VOUސA>c<@Ew]>r}jXC*}=+x3n݊٩[m Ǯ[MȧŅEWډ7UK2ͿMb;D׵uof,l{Ϊ"ː2;TTόA0($b,{Ģ| cF;=m]' cׁX\',xDZva"辺Yn`1.EXa1;܋MUB{.I\B6@9ToK%~[ףBKifCغEBah+~63mefˀ@SAZW0NpZ l*vuXt5A y8=NfUhCTZ$۫~p60V0"lw^RԈ8fU ­2uR9( sdŰ>ǀ]NO&E #Nd= zR0J9 y 1ka2@wU RgojZ&dÁB)pI=M{e}^0L⚾lO".cҐI!Qls{P ʜd;g km mii@0@e{ȐTbq>Y446jyHܯΨ?Yڞ7h鱻A*g?7GX gy zNz)-'멀!A+f" ܆LO"’vYU#К.֐oJ)8VC'ࠅ6* ;)mt>4 ]T, 85b۟#[XP F}V(Z5$-xIE%M=u1L=#S({cbp/s n @}2:39"&{d 3(=G *ZUͦ4hd^ 'eYxsfq?^T Z5DS'?# k4gY&%b@ONM2g5X&Six~2/# ςcctz{{ho1`71-dWh0rt MDS-&Bǟ}ӰU!ҰV]L$X"AN<p{l&MR|ɕ/}{DRg^ӟ¯9]yrG_ ZB>H]8Vӓؤ|ici. i>cx}WY=BhZYTCӳd<9.XJvQ3>jLo_OϼFm;@]\G<^/Vc Z:Sՠw$)/!EPK+Mm9 {} ;kycqX;eV2X::Par ec2+uIzʉ%*Of1mma碑qF΂,M󻾿f^]U]]LOhVF BHC`;l6a_7a\7# ^/lC,Bh4f^}_22+<c;$9S]]}볡9ёwr6s-ƛh>Ɔ\a+x~:9tI@{Iw3Øe6hc\(D =T_+Hh59b둑nK'mJx@;ZҍSVx$!J{fބW&'ö+e0{: 8nn?}7f1siX?~lL ٥*f Pw:V3tڒ7ݷ_^0IXgntةgQ+/k6tO?# >zPh>/QOp L+gn|7ԯg9ʺ?\;dFa?.lI@0H`g,_<Lj #5C>Y6ynݚHvƘha혵ǯΪZjl&?WR֬ Y3 g3>zh_d$stgڙV0QO3:0`scow ?[_3h 1hz5 ".gvYB=웳l{dUâ͗l6?u?N 9,ٯk^뜇5ܓ馍]͹#x{sXpK 0hm ;%ԺGMR}|=t.[x\{wߙX>/{a=6]KO_(h':k\fK|/sMSw[v8`NtƷ&,1A2+$F϶VD(1﫝|ۭͳ>0Gς?zPzK]I ͣ-Y6ϺK99_bs ?:{0YWg?_g&CV &P"(q|$n{wju\W|=\A0s%A.hl:$ɇ|G[{YIixό`H|+| xܘ?6]}@T4 "YoOJ#0|Mn*iCC cK{ӡ֬}?k_J#rp676`G*ӱfd *GxĴƍny_\IK}ڏf7b)H=u~>|IJNb'*O[N0W*2zz_||SG;齃G^hzz;q ښʛ#ڐٗު%{5@gZAsm?㎵pΠ;>NPi:O<g>oh/ >$G+6\|btVqMN7o:O:-aq2ٚ9wg~a_gWk)8^[Lhq%zVqnO'9cv[!VfẏS84ҺτNi-6cnP& D=m_צw77f1Zvʽ;u#.6BUوy K>}xPSE'r8رO}~\ڍEB+ >󝄅ۛ鉽uP(բČf=̑~} 0mĒpCX,;A423_0= 46E}t$(3eozof:GyVe $x./64 Qup齷~s: uy{1}Y;+n);@KǏWn:wO\w\m\i6,à R RRp25.gn*P)gL mk{pF.@Ik\ <`Qv]׸dܵv2;؞-XYF1l9BPy̚79s l0ޖºPL:h+Ce&)Ɩiݪk Z>IP_@o*>fTe?;Ԃ|XŘ9,Z{wU*Ɵы(_Pg|]0=pz'C>$PqLH[ONF|ra[o@|]<Z0]%4Iu}5ׇdJD4>Lvs:=%a@?%7*Z霡2v3vZd$ -Չa2Eg:Fmր#כm3c4139_vy9}~,P_0Ϳ ӫ SƇ-V]Mn8iF^&t/,p|y>< G ?k!m̠tGRT$C Ej$`}JX 2zEt$E=u$ʎ!;˛srC#_-Za'.^ fRUɐ}TcpT@#ռ*Re ooV/Lʴ}>>o73s>g7O9/Ɂ/@}ݣZrRGr"RqR9EVx<;ӍNN期adi{} -!Y[A@ n>ͱ[co\@Ʃ20״?e1$ogoۺڋ oiNd#^8RvI!9x9tn@3]վ.w|Z5n^ oE"ǜ`.nxURg.yšEck{c]gk}ЭR %Ahf[Ɂn$S51.b\m-= 6(ҳFe=icLu&훵#mL3ϓcqX ~_7xXkaH;^3*u6v xtzPwɹ\sq/~˯ЯZ|(ŗw;q hVU֢rsqaxm9 GTމvL}xGhF PČ\ü {ՠNkpI  *yWAr,e { .dO$?[U&vlӃYU % xhNِ/oi%eC. f&ݼK [w 9 CK4;ZZ~Zo"{@| *=`{ ._ёp}gwIKUI4SngZW|4+x(cıQ9;|tN` Î5djL};$4Zv?lnsRw[J7S fS7hGd2iF }Gz`rkЇ5"媋 wQo ={G{Wfw#ف!dcqxMscF߬d 9=,I tҵ`-#=w\>b L>3.}wU}qtFѾ+/^8;2tTnOm]LuHU!ߌJmU43M. nY8ڒIpQ0%3wS/eޚ `Z=pZ@jكzO@Лp:RsAAA5!? BTAg|'MTiK;pߙ+1*:\ OW[? _L5CTG*g%U/` (YU~|}x_q6kIvd#QTn_l[Zڹ 3[ԑ$'qz^,Hх*ǾECnˆYw\jV>{ǟIq @o!)_8z: qնKa7+D =ÞƟ`k:ÕͰPY+I ;};o?ovdBUzyKϻ7_t ӗ>UxQRxc&ޟ8:sBIa[FyAհnR50$¹!d$Qpɚddqw,Ggoԝ-{s1;2ÞK b_~ctt6oޚ~WީŹZNH ټ Ҟ7?2hD'`Enm%%h-,O!U`rГ|P;ͽV{=pC86ɺăMH?@,/x*% ?x<Ŋi6v ?ΰ˥1=7w|'0j ~qu] I$랖ވ>iadiǺ8jߍʻ 24"g%TE?t㫠{"q}o y@=amD(;{-=AE]^S ? 8`ֲM "\f0L! 7_nx1Ze ]xnzLf|8BneOiJqBYj3daT ޭk  j$֗%CȦZJY X0on@ΕksslzTLڡ!^:\;v0"uzUYGS@ƥu@  & iF~PL6l}+>rwHNUW zw>CDH@(,F ;w Ȋ*?bt4(c#cO}Ms_o޹6xWujuªGtjў%2ƼznGk={:8}Bߵ/:B ]1Rκp+bžp,48}msԳ+7U dHZu^qwc=}wdn7e'̆B. /.h@aeZlǕve-Y+ |8{Gg: =1X.9\2Hu2y݈NlsZ?$6>]Oq,̗Qk*6VOk%&Ux'pwayS4xuͶ  k1q/!iT T :'ZN7&eiWލk"gQ/^;rzͯ,סpG3[Bqًd#=}qZQ|ϙg@qlePKqo4-㜢Rá{vm]k!x=C1k!{kkxY=)˸o>M'q8}L9ʻd<|}4mHbTYڝHT0o9 2g=omf@ u)J]溳"W['y6;Ň_ELlA̓,A)-OU0?xL C^n=֜\y{ / h?-]Җ<>ak! ^κ; !5s,s:s$DZ0gc@ttֲ3gxZv<8~|U6υd6Ǫ7rn>m[CvJ=*-&f2T.]4?ث9]/Tgi t0rx5uԉ}x3+Pώz9VEJ;r^2Úֽ񪼐k= 9*`ӯ_禿Rݿ~ݝ?,whs2gk.}Aކ.צ9Uxr/Kq3bϻ_zetux/Atь柏$_͜2Z}} xBx<,QUIЖ[`==CS PB{/<> T)~or|IN5<[Gƞ;Z6nm߻ub#NldfWj~Kζr89}2r1ThAJ>I[k3;`̝2&/x٥G{<]nUfU{9l>Giгz#&m FLv?-}oXfut75Ǎd" ;#tԣޭ*$Fȅ_$Üs}c_?zhVtqYi># }b8\{Z2!y>l(5hfCxJ҇uxgZ:֑CM<[cACf=A3?֯Dpk3Qux ۳Y{m|\;h$ž-q<Mt!IVy[4SC@$Z'.3=UΜiq {3M;rkwK:Wg qI8\;:8[^iy)Qڂst^XLNC%G!k,hU%̧>ڶک?AsĽc N`5W%9zxt_ڰ@ёٳӮ'j8,[9_U[7ϞT8s8/w4ZWߝapkMs4e &vSd%a0cϑo %Α$&=a8-{Mwz6|l]Άt]m=7i}PqGr8UpdX>!i]U5݇ƹ/<ԙܻkjlku UzGV?I' 6WB1GssеyHZ>A/x R/I>dX>JX+No],Ʈ!◾8O{٬Ů]9ǩN YuupT_J-$^me2bG] pI2TL hJ^>bRw*X~Cs]MlM),}=p`<2~^0[2w,[q39=0ӷݚ |{'6m,}8nB1-J*"e#9^F%\< emsMq;|]Æ>b(3wT׵+Ys_cl`LZQؒ赟xh6im삜|"$c;r=KGGXNtrmOW}co|ąt%oV}kg|j?[mlVjFSAx\dJc^wYzKLæZq|0K*@jL3./Ƕc5r$ ѥp(*[u]Z?8Ygɝ#?_[]#Yy|q?__(^jr xIbQnzx@z:JaQTdjհ>;)>gO=%@)ܫhÙ_(wޜvg;rS"YO+ڨ*7Z#v|_]>"3%9>Dpqط'ᩛ7gJN޵?'o|{q׭NN~޼n+/+[هs7 ?8֚&{@r+{[okL1.>x8M?;WX+(Mu]uhL/6QJq Wt'wY[2.z%ڡvڈⷶrf@zw/lZ}d(}ҽ>-i+X,~/Vs߃ jU2۶1)`zҒ7S] Q_IJf{R̂ϼק%K"-X"Dsq-m|בBuOMGG0Pқ1Kq11CmG 7_Px)u2v^==eG@e Pq3ōH>()[]o<ܨJLv90B`E VU53fK ހy=ѦoG @FDŽA2},ghh2( gmmڒЂrcՔE릵uX9[Rh%JJX`"\V&Pۚ Qc2ߖf(BcYH2"֊a= ._֘ ^Bncz1j܍/wNSᾮ  {NK*Ly~Lϻ}OSuy 9xD\.Ey('d^K 9xUy~'_X d23{yea|Fa$Z?hM`.k̡Ǩ,@Ksÿ́L`O)i|v^ս\vUq 7g0t!O)_|pEKœsZ)aEjSi?#w%K}o׷0|}C #Ӣ % VU8#3i8oѵs7p}s>(WjvG7Nzׯh= ?]Su޳{֒ _WK'"<L؞aFY/2HbG=&}38~sl9r˞ ~ޞ`oUaǓ .O€kڸXkH7j^;@Hoe qd=t,N6Dk ~K];jCjy @Z7~}?Ӄw9ltn9KWwV:{ZjVݰz~Herlh'Yes=V)_)ږn{9|N{ЎCq[5gN{o4k_R+s6ηrȐ?>"| i߹Y1^d GO_6}7jwx$b1='g=^?3RUi7Z}Nߟq{Υ:mRƲ9a< ̫xښwVj~4bD _[}WkoL~@^~ʺLSQ[G%:SKĪ8Q28`ɄkQ@=}%B̭ʨR[X%g0ax{8/lݐ.pV3v CvyG<*U4vFJGXNIXuEʸ[~Q>yth8$Hu!s\2(QU }t= Akf}*t|G3GeoגyTn`s&ݻwrtU NkWh/?ӭn|?!@"a}ІkJ7ڌfߛa6.]#[*1Q۞8^U̩9= 4Ǥ@ >̑SoZ;(-=tK+J*6{Kǟt$'^<ѬDU#ߕ:IphF}LYcnx`]s3}՛KEdcLwpxឮ[ծki:4/%hֶ}ou#t|8Xp.?4xű莃 ]ԣ=zXw1%mET~_v߳gVf&`*ltM}nkn&< #G@U{ss6Vp=[~ә_wÆ=?6n\ PW{6a0?L ז۵Dg|+\в}ގΆ '={\\78WHSױbV~dN;BiK~a˓$cG]e1FoKLO Go|PݵJu,0>v:~t$E#8U{ l όhTǁ@?xH2eðw/B6W@v̱z`Ʋ5>$$#1f~pU#/ݽ<"ЉIu+x@$1&/nݸ=Z{rSoJx7+ M'ܗ$JP'I _l~'fKo̗2:TO|urFl%l vG~G |޳ B*Gt:zHɚZCB"TZ5|7|ӇWP(-_:*wq\E}dz=KX[\|0W%'ma=gasⷛ/ձ=z:tp?`\{*G^.X ,j$'T-/ŊQ;?׽gGb+ 7u ݻoK&wo3ڒp=Ϸf~w} qA*T2c^#Ӆo/eGsvE;1Pd=֙f]}1VKog _aCHN6fݣtlfegݕ|@6䏀/&M4H g)x`Ywt]s]TVKxp}hǑɎ'% o+ߠn` J k阋wO_{?9 SD bl`x|&_}x"wf>N6\[8*}G Jspcd{>]Ȗ=>(,>4y>z+}G7\tI_-,BO.3.\2hI R]G/fgW%.9S@Z|o@H[ܺ;awHhԽ|C4SI/'>`d[ıO-)0\}kt=$a,>(a^#^V퇵XDxuW!%s!8ڀ X` }sƯ̇AiSZS"zp>i3@7P'_<1lkpY}a@ C-F?~ر1;mbUTni,ݲΞyࡀlMrY~ [nDp,fcM O_#Mff9z;<T/bTL;T[g;j1ѣiӹ3NO[W ay6N;0[DvP푠,2'Ep7j_z䩗 =\sC2W)Xgq )E,Z%'Bzqn6WN"D(k ?˕d$7g`iʳs>?e{C@qB'̰Yn| nj,x sri-Bn͠`U$hߠ)JR5G,2-wgr: :ҾrjR?vCȭ6Vnd4A?$|Mdlt H?(`Dy[kv屧yG2xf|TOޘ (|d ujr톇)d.`&@`*Ycny"Ym.u/y׾^N 5e=8qH6U;*Be o>8{ 1.8Krh Lr{ET~2;ƧO IhYU*zRdUV~ΏpAU|8׺j5O)P1ΠWt/'{_LP' *n?vAa9_)e qd}DƑӪ85w|07Yr'd`~0tA zVryg<1g/;0y΅! *yS10Cz&2*kA+:Nh# ;e,R%T-_QbJNvq>vU>2uDQ]"&;n9#ej~3%g`+\#s{8{&?{YH\/DKsDĝ3@w<wGésܙ3pkUo~[ߊ~[v]t˩'̹.wK(1hh& }=T%h}fZ{lm ƙAKTٹ/}wIٱo|LΟ ~zd z3i ;lDv\ei,HE~/c58tA뾂}%U+1wcFhņя?O}ۿ:pp== ɼ@ɓC}[fw :{O $7q2ܚ7G]#F6F3Y:ӧ?tўU0f[ɤƸ>^Hd {}tAU'1L g% *JbȤYMsz wKsNz^i~N'̙|o:pcI /|Utc|~|nOtfѩr2u zY7tϚt؎g673w $nMIkC[<}c`M0 mD0|U$Y#4y.6#֥9'S cLHf3fB6柞0&@OUsڕƘ 9`ˆj7{#9u懹A(A}ݰ*RӷÕh?ia?gtY?}A! ֛c/zZKZgY]+A2Ɯ9o`Nf`X-%da΃%0m8o^b8MX< LEZ g]n&Q5KZʫsi->WpX7҂RgA }6Ѐ$; 6sL?Ka=6]9єޟ/ }9ZM$R5#t#\4A3ւ=ٰ&YLv$ eiֈ⨅yﵜc *u A;ETv99oCd&ghUpZ ḫ S%Ÿzc"Fk$tA[dq{X>w8( ރx:**n ]"l7|uD{m?1}ǿ.Pyk pFE>)xQX#H zC=niFi7.z#{O1GF:|$JNh: \d8+Y sXVAWdxf9/?I-dT;L"0ލkWM=Vֹ*.օ״;DϜ^gANK˯b{ |ǂQh˲'/{onn/2?5_YUlŜxHq$'.vT`zՔc^2xxt{<#>YNWg8Con^q[,~pX6{qxε^ XAJ/lN28wƎ_lX89#ilƟ?YyV2~ձj7~ קϞ:<*pB*[lLc/;&1O4G 768#L8πNՉnؖ]ǁ'{ t{ѕ $GL] l^?}tZz&];{6KsV4K~X YD:&8uc @Cn l漺ڷ:fy5Mv(4͗9OszSPzg̟PuǦjzeWK>dS|v#M;gu5||&xGr4Dtf~ CF nn\(j Q!oWBWއI!oO4B89T"+MxM"ޗZmfce)mB v1zaGsN MjЖN}KNql]WӁ-:սsoAC*DWA;zWnS%i,,ix|4k؅c18P2 !L*r!!$X/o(QA9Rqh4m~+eV-7ONwm3? :ӵHj%5 x(ހ'1a`x=}[8֚55.:4φ3?,If0-4pvŁpu#!WTCKd$y|G8 \9XMyߐ?w(?~%!V]nޒ9-yS]b-_'<, # ^n>qs|3_j }'[^^Gb33߈7ҕ%a.vNx: #/~a%J uy_z[3BUGg<M B~4 [إ#~KS1_b ۷Z7vtSI\f~PEϭw+6죰ڎ'eK;]0ܖ.ttfA `\$I4Ʈd:Bn>|vʹnmafS42vZ~YDXQpXՎ3kӻ %֗k2}o]o'jkM$J6c 8? du6pc¶޸[r}gd2OW ]FHj%KOm ZgIxq})V4FHD#'͆lm-w *@~ttO*6ޖ5f篯Y,K \8! BF-uZ?^u$`9dgm}G+~mk6*zE ESGГ$sqލ~mYם[#J<F/19NYJ{]kV6R{ڃtYIHr [ hhimp)s2";`GO^~ zy/ Qgo޺ Z1{|0 !Ú{ֵ,It]A A( ޜٟ/ 4lt|ؼey2c@IDATHRi vM gѵ:ZeĻ| jDqˌ!UE=9$|b*@Ӎ`>Ij6b8/U|EH\.f-^A6 ;y{!OBH9ijӏw/* PKQ!>ٓH5y {cFCry)>I#(ȎI)mmT} 7m(c> Bi\Pt̟3^)(|UbH ^+>'(E :v.` ^F'2߮Nhc?~hs ͺ͂O* #t/+p<}Ir5R#g^hY=ݾhyxS;Օ i ӍK燳o}km7\j\  ExŌF4D@:b# 7N{7YBŲ,d ^z|ݮ6h"ZV!:_fm-܊PZ/ [;<]WU:Ax̢> ւ@x.)g) zR笏1֋Id<̪e=>/[Cהv{O!Ix=<zN3X"͞ +ǭ}NHA 0tbx#QQa_);g蒞߼8F:9PkM swF1dXk_F2#`XhX97%(G(g`tѽ,d4~[H>SL:W3,~^^bhtny n$8}?tc@>:WYi>s3G 9ϭ[ZCa0$%qv9g$= H:A# HsWŁr|Nn}cA}u&.`:evɅ'F5G=W#!\+V eh^ׇT5 @΀a (hsnt#.OWkOU8A}ӉuYZf5to}TrkcM_!~Fdxt4wOЋÇB_AˁmOg o_>?6M/׿Ozo+u<4}u:tawUzM"eҶOwĖ\m΢19$af隰e/9͈|"p9G+-9"!,U2Qlfx l1FU. Zg rȼ^ufCڕ3s<֤ &ӿF;?c8qJ]wʕ;9Kjʃ$P-5 AݯoYR ;C3 A`o L?*ך4< /td:6Gt~j.K'yd(ADW rZ^H؝c.Br>q9̜ycKX4$ǒVs; %ʤluȨDxS5FHKvpڦ{Twm GoAk9tw߉[>0I =QHZ%Y-' H,VB:#9TXsm[7EkfgK0Y^Kد_K?N|q=-)g{Qi_ߞ?><ঢ়̧ LD"} -3g [9.ϓf_oǠIJ{+'I^LUhO@׮*}t\GA37ҕ3|}<d{s,HpNx21tvÃऍɹ;p }>vhn/םWzj}~pNiWa^>&tΩv=po # E$ycЧxu9^efwu=KW0tTs~l搹42]1]Ç#ϟ2W׋GLvXߝ Jq:>gOH .d?dk?$p\Lx%PNd^CȠ5jcf L;33Z7֕o73.@/mhzIQm#;8ɞɠ$]ؐ|1y ,]Jij]ɳh90[\.ts޻8]]qZp:+_gG%\QY~Ku XhݕDq%]ӏ#~05KXAi Td'ʾxܧ[%:;o|睂NޞnN8<}ѐͷJ09[;zwAlxU+I)4 g D}haU!gWl񶗀8֣kڂY[>PX'ΝPuhh#U?GOΞgԸ*Wc*_O K%qU5E 3r 50yHb1v_=҅͡+ 2  ϓiȱC7ߋ΋IQ^{饃Usln}sG߯OFڐ Vdz{|޺:0} 29oEddqΐ;_ |g3EUXx :緱}oɦ^Ē۵g"$X;Hnƻ#þG3_9KNT`5P0?U`+a505{0jZv;օgO<~W[@\|z?oN{!S3|{-z^ 3\P61oGUbc-ػjxm'f1"[B_5^kLI ł3A`uX?cf4C3NB3گsW/3iuMï!6kgMKرO#>y1l}#tS1ҽ9*b+[ߝg>󨔎? k IVыޘK ȞYMF uHx98*` =FU~ru}-wIp}GZvjrx`2Tb Z#vrlOuF/ƿp5Xz(%K=g/؋# ]$%0kd<1d p)_MZK~0!dw&m{6WSؠtR^ =(VSOg+*_ Z$YC )=;[ \k0^)ؘ|NޖDڼ=>~L:VSLhdf!SNH#X=qjz5<{[Ժ:8}FQ!Z4^ عw=`t 1b x⅑aӳKE-1y^ܛRcAۂ ^.d=ٮRfͫ,;׹U>* t ޞxyNպy4&sӖ}Ӗ) OJ~oߵ5#ّa ;cϞ l^jYJNJ͋c&Dlw2Rm G-n60W1ujld#[S~\{Mm DU,n[˹d9Ζ<PFExG0S w堋PAd}x [|d;a%T%0~fHdL>|L.3iԚ !;[i׵c9` );3&-0{@lovלƵV u}ǽ AP-t h@}9( F gvTx5LZ]۾}(ת !5SC־LZ+?[~4Zidor`޶z -:ovG㊶r<k޹~~OVd5aƭ^3Jȁ@'=5-lܠ8ӃWs̜OC1?:VKNXoij&뛣5T0h*,5[`Ł>>z4Llgd|qUsƅqQ߳̔uɠ o9 m\ેKX'Ocu$œ}=KUc|·i[8p/+ah8S_, Koךh;Z]+/0@j (R ;t^zNO~\:r⥗ꊲgǘ9~tˣZ[BUE z Szg2_gZ( ݵk0R [nӃ6gsTζV$⇇щFF8P4Cht(Ih#xnU"guSѶ z'zi|jZm#0{,dHQ4O OW?~ok?2OߟLO\vj+z}8IK/Oi - UL40In0 Y xkeGxȿ%vp0>;w7ٴ!nd?=rl8$$=I.g&w6 nOv<9=.̂wniG8M8&'?̉ӎ.OWC>'dON2|H GJZiY8f@$m';ztk3iUwcSj΍`蹍Ʌ932hEF3-xn,u-cqk#.r(@A72 ;{b2tՠ ϶*mfbD2 |X4 pGת2}<=7sv{+ZV?2GwXU"if{t? z=mA?Ж%̧ ftl hs'[qT^b5ڞh'vݞ6^99Gr,]TȐ`l';s_B p8*BIdiBc54X_:I2dRr=ӅKTsn!}*@1,2sXֳO`fϑ%;?ʡ6dl>*9E6=Yad{n9cڑv?|(' [;{$ͅAQM'v۩:=+ATׄds;24~v9x.:0-d!3ȣ+gFgf sLZ^ڐl{K__m+P:6w٨)YQݐ#DDHr˪'ڃ1ud]BcԪ#Gwhzō{-CoV %$#'Iǽٍh_#Ygrܡfφc\8h~U6v\9'q 75Zlm 䏺Vpzόf.^zH K?q<בŔF5xn}mWl={EV_Ё>ٻ{2°0nW;jWYv>vT8T $ۘv @r, r*"ȹh(!fȂaKGcvbҾg5)=kӯ2GI"wj[pnfg3mYϽcaZ.1m<ؚ(g$># fX fIɡ>gK3 $|ta*; .S 0^V~Wls,S=GWOޞKڗ?=j:VP13:U$g3 Nk/62?I,tB&G`9{_ :}˟#1tz.u{ڗ>OnȺm}t[B ur@ kYB }@\r}']c5WR(-B ~o?dݖ[/qjZڵfjawsONr߻yc@=KL楞h P+S|U܌?x2]KuBJ~tKTSǎLW ̱3_<\?mi[7}wOtx/ rK?xΌ[|+Z?SIWgMn\GS0kd|TTrgUvbVIV%$Vs(67o)1୑x WF"ߌ_λ}XpmMrQcɞ?ƌLoi44{C@jϖD+{ 9@F"\0YfID>+ [|>Mxc:wfAGӥ[^~s ӷ|kxmk/Nʴ19L<[asvi?>oG/u^c۞J2¼\a>i44yRGRMת|t.SP +6`EIfzk= `#qD/+G"Vхȱ(?ڐ %3pK̇7tvuN20Z'S0b2c4 /,dxŋӷd")t=F~JF1lANy  O6'uvϞy8yJNJ! E[ju#G@t#ZjE M`;v5ש>{絧5s?}<ꨠ 3Ӄtj]%0?xu<nW|Գ++E˵G&V`Ӻex=كD{l9jĔ]i{g3+W{zpth[J`t-bσW&!!L`χKd\˧/ݳ鎍+jO310-Kv_=|Պ0>TGn3Χc/$<PpG|G" Oi>'<13n|(s2KF ,KX{?O=J'RpK|'$B`-7O{mYoi&,3X zwbmaxE"DGMuƠ>LEkϚse?8b[:+*+ab&}UT_1DϜzFxӥvc^p}Ok2U9\R ^v86\ LS"}7eրQKgqp3Q<#|$kG۫1G6L>lUK@c&Uo*6z(aHp 0QRhZTL3%Fk'OP/R+{g]t0/v HS>?v2ϟy+d[}S=ZΌ|}fUgύA>R]2Y`kWkT[n+k8vrYfĝ?uTbWՙs)kFFœ6SQw§2˻?O8GblU=WA#U$b`(s+z (n93\O+t+=UEbaeg_MFX~'9,??_{˿ߖ_eW~4'DR~yo/'owŭud]<]dm(G0zT*MUE͂XUK"վ>Lm@uvp3?O>K\|/v]:n7aq)'cX <G;xSZbI38Z[__>/ 3_X~﷫{ +"fk?xs*Sxqc> >TṮ]k4 tݨB-ЗȘ1|KPl5<=2\:|o]$^ApzDyz;mij9VgR:ǡx p1y{օ=u FfڎV+:SرmZzLhs_е  p3V2=Y>SCy5:n*m%}h唟>&]?NchD+idzɆ? `t tK T^m=FCM[|Zwcbpg'sRTFc743NIا3~+SM3W|,ٗl:7bOgh48c#փOҞ>gqz܉Hw^o0žYуhaUѧgCH[ H.x/푱8я x & B뺞IޙAmkkUmsGV}t=)c^hċpWEq{Խ9.^oUxw&gݷ^#V42@!'o3^+SUsͱW9ڞ ܁W䚭1;zo_뮄K3 'xnC{CpE:QׁNp,WmF)Q[fOo4ɛmO}D 4ޟ?WbC'LJ`KWso>%_>G_4]ݴڲ?E%B8DҝQ鴥N7>NpB9<I"*< گorr>wdz~2 m=Lg.O_~ݹ7^JZKg'9/z[vsB3S IoF|^[}dDɂ]g]+9fN샞"41 êH-߈v>U^(qmvO,_IQ*y=s{Ėzel]+>NU=͜o2Co 6 nM]J({%9St_cFE)}ڸkJs_/}.xV2DWZL,^#`2f<bm3O8{|;΍9k~_^eZg\Y߽<[|7c?D= fFl2|V-5Al8w| Du7Ư t~>|~[׳SUfko,Ms8ß/.~uT' hF6ε/u_%p/a.$=4 :2[K'}Ըt][ޣu8#gH.0\9T@OߩwOtw m-JBA~/O|$,%^O*3ڛa}tis _1t5 Mek#dKk3<ɰґn_G/: `ϖ#\,7;%>^-IT)n]8gˉyg,f%37KF~%1m[:$M(#=Oʍ!~$u|0w\|e:}ݸ=FYj NO[*HGfV"ˆwzlu&VoNtot{)ZLs/=va鐺 ?~#O|YtJdN<2/aL.ɾe7&7b&ߣ~:A|:hqP­elAq5[3Y%}T+h9}ncYwSqoٟ_B9q4u~r1ޤt>ؔɟ{phw^_Wα"V4q {ݺM`;o.ɂFBW{o% w1ܾ۝Xw9[r;nͫM=_':{vG\_v: 'uS]Om;5]I+5;]dV l3f׊%nJHx4;W9fM1:==ͨs*nY%m~㚳O|0@!'unM#XTK}h—փXgD?1S==J|]+*k[mD6 u' u˱o1lsB4{̓O;QrlKr:%gf [}y<|qt >k]:y|GGpՙ*{ݧc{IB/5|w8p:Dҧ{dD | bZ-:%9 pIs Օ`!!m[ٯe{C)R((_Zlms1 ֦C+&|ΙQX;hl? 6k-|H (r^ m!*xT!6m%3~1j`8v!ABۚ(HooEIqJ"'6Z6u4V.kz&S}OJ wfg9#rܥhak$Y8Sj:ޝUo킮AӞz^L@ Gv%7^JXzzΤOɊt7 9wuKG?nhl|Nj1FMA)pr63Ԋjc19;a' (J=b2΄}WU毥6eҔ g`G;?N0Wt߳He ׾;Nӧ_^x |Ib}͏I銆(iNO2t߸}~(z!ڍrJ23GO z[Ky@c.'Yk)Gg[RB(ÿ#cpТ)|1 RB>/*׎SB L&=@/ mo?+_MypzU+:Kq병9Kw"50^`m?Ȥ/}'⋻q?q_7W/˟T8~~?,]A9H,Gɦ׫kQ>mD_]7 /N%9"?m ETu7VrGuiю%۶1sJoE$ӑ*Ң5t;X;M&yf8~,_? 7FZ5\^?ܓ~1N'ar?nьgKNc,NiNhћP{y^{s2y컮)eεo!C:rsu.=D=O[W!=nd-ꇵn/Au{.U[S]jzs@ v4h=Fwn[s'ǐvop1ozg\ ㏾$>0uwHg&w~tk$'1x-:KBGkݑ'U5y6}:LvN +ǒkB2@RƛȞׯg#g2ϱ~a7ޜ*IOg?@py Б5wV j%Ac R.o=+Xup`>6Gpq1I෿fU/G\ɿcxZ h~0 րM@[߃ĝB/>CPW\o-_^/_ `#=7:bޣc;UN@6tjFўù6gc;a8ő&!B}<?G2 ;>{{s d(߿:0cqX7pM|V(?͖Mk#sKhrdҜMZ;P +ٌsiΫ|^ #gֱ^ܘ. .]ג,|?Cm0QOr\e+[{7쵊$|~GX ɒw]KT} E 9ǛF=9^9w|[>̊lG y};/\O_8' M/luhH3ݧ>G}㵁WznrDb~ǖ}&:=Kv+l->_=eĮS[{'[:X&j.}8v?|.xbU򭪺?'?Zq[\'; ǹ] az]p0n>¾pq^t?b>|H||k6᳟jc~ߵ}[K+Y^>=*"8=mIwړ$~_<٩ۛ/"1o w<{}ܞ "!)aO|"7ۿw[Vm_gN VGl m U|\=w,#@~.۟ޟ/1]9M!yfw=7 *_~'Tw#sdFv՞hj\;ˈ'2:3t#3W*8ꇾ}>Uk )O& au B7&V~E嚞z<~dӋ/^r潋gE7{J#g?.??QSdk)~KjN7_zux \'85hMxc:=?CA@辧O>|2? L%ɞdގ]t+;$ I:x7sg=z$@!3{mb)$B K^ˍ5=m&_6:~Љ}xvxQ+luh]!6OYw7KYW{hxEYzN͸6W|%jwLh'=njnS<[K?FM>.ja -:^4+KO >Sٳw};Jrggjf˰&i=GAsƶ/x;e wgp[9RFYw*vcX{/!,ed% u kQÙQhAg픭;wC<9P_-kU-1Ĵ%P^!j^ю\@Jyvhga*o@zS[돢Xٞmmܭʧ1#IpU\D'[{#FּZʢh #LI4Iqvw3׺Q tɠ)3*B2 HK58[.(b۝owdpJa4Wˊ&m5BP1ks "o˓کbk`VC:qGѶ,som1;ЙY2|/V, 2 hϳ ;{aqjw/ii#Kw[{ayS:ѵuzcqzIȹykaʞpvKI3$@UڙoNnkl{+ab pD镽8f@vJ9piUbIJ80"$F8b`-04{LIH1 Tx2Yqڗ{rH?*8RK' "sG>0s=7o^h< {rbY1vt>`L)IYo_#zgO m J㙼/©#ݳ}yܵe;rtƞHn5W z,/Xj 12쇓% 9 ˒Wy\OIG ]e;.OqX ߙ9v O+^鳜j3_zJSLcip?9+d5,~%ݩu$>ǹg9W^Fk^y7Yў^{mJ7Zg9چ Z%p?-+_YsZ$[bpKth4۶aR8OeS*8{KBoO3h[}!o㦗}cRg?;wrsdO-@ӷQ]}gk&ray0/x8FsșȉNnr7}ő57$kKb$]˖`[ n\GM3X{f[?<&_ӫg.v,?9Orʕ>|SΊ$xI'O`_~eΧXF ]G MK[=n5a^m#fp<3xwk'E =&x*rstU#fϮp}_m w7ϵ*7Z'?J6xHA*qho OYTĻ8 =4[wy0Zrw2i|Z')vvD>ItDV`jK7* (ZRs6:OQb{u&b[$] DwSuz䟋˶%̲ؕVʊ9#_j܂~N uyˑ'az 7֞O)"YI㞊ݞ<61xRc'aV$6Ԓ$(}'aN:0ҽ*ֵD5F֓0~Z ik?C =woݡE:slx=D&Q a,݈L9@Fsf1KcɊ7A^olq &B@s*> &΃u noȚn|ďVw(6p|59h(V>OuԱTO'IL`J6.܅hr=|}LJ I\CFVDW\wgnԾvqkW E%6~rL.p4|=*ߛ6GpLo쉧3,sb lۻ괌Xh`8D?cQböV‹/-◖ϖ?|_}g#Uq,7_+! I x`^xLn^eM6Dmr^2=xmٸ|OHAk 76NBu4{ͩCKP`Ƨ_84JCB{'cw|,Ch~&k#DLk9 J`696↰$ ܸXesI(AwZz|h -&DhΗ'c';/Ռ(!"[>d#C껪`Pb{F^mUjB!Zk2gn`/d]IssSK]+U/л|MЯ\ۛRo#v:`"g©mMq؝BM0)3IlݳWMlZh-RZӈBB_.\)#,^S;XRGO:oPn290>8W jY$'9Ě5A qA(Ι]j\I|>DyD>L#`<1Hؗ QHyd&lvL-]U+QCR( 'A|Y{x xvqپv4*`>ru搼_l s/ż|{jZḊPo(+ +r3p}4 ޿qe{vTFU {#˷W^ j]fӎ`i  Ɣ3O24M;qi^xus htJIe÷$A%ܦw0CqZ兿{"=`uSrLI]%}!Gv} t|NIJZ!95A7&p6<𔵄kMz{ ^Jva,LiqE`=9 εto[NU6'xi?uj\ `0^ Ľ?pl+|GO+k_9]%cVN}#uЀAOJj qKNkČ8_4q4yq{}wwO69N}rRG D緫F!>1o>>L7dfdp?Y.4E\ZwtIɸ=Lȴqfrt D;U¢eJ|-aDg2k}$e~:=@Tvd>#}{y{3f磧?mfwe]8XO0lY۽ 1jSݓbV8R[`qȂiXMn'ۺQlfwgArCRmSm^n/x=^}֠rtnf?Nn };dr$6^$GynkPW ;81AltK+I] alL2.!abգߎѼxom;%Im\ _ 4~ۻ! }QLʏ@) n9']vdYpoͿwL9+4PGct:ry/Ѽ؛sP;[dcL2U1y\C[o*2>o~t8z9vO~l:+85E@M~Oɭ(zz?hk: 47oH9Nr|}I2pӑv_kbĕLTbƑvN݂~/} 2n_Ww,{mAzƾytxN̓[B% :n$ g,0:%<^S8BI!]8kSp^2RS(n ц3,gOބu}=tIt*P;ə%\|7i ~M#Uɖ+TJu(u>CنiwyK4I>IV>Wni8H #<9N&6)6ǂ!5:С|*넵=s$ֳi}_M8 SU`_s{fL&غu$ߚx[ O{컭Ӂ9AK,[cלO?Y'$n5U?mxl-ً%K#U7I}f,eoh՝|ʯBݞ7.;-}L>)~˗_<5{7RSO)`VhkG2{#_ͽ[i.|nR+ɆhZ$[>M(xɨMk6.'NKIGL e9m1; {f}Na _ u? wIE:׺Wڊ^^GJW-ѧ٬4LDҹ̑@hA]5} ۗ'SwJ̄/ֱ;|},l>W<,>;t,Ľx5]n|国no߁r혎V7jm $*]gTM?p5YpCLH4A <t)1h#^IM_%mDH}ރuyחSvo tSblPEoSq|\CK{ ރ޶..9NtIlΆwGK\ + ÛRs/};X [x NG,;puo~Ӈ_;q+Vp]U:9 x+~8\/u]hd!k7 um~~U|=lot{ECx,T#i[8\qlJZvrt`-ӏn wJDZYwS9|~;PcMy4?>SwVh@?/)_{[/ {?"sꔍ'Xbat/,=,jQ`X"0yw~:=\itrBh֚Bd7شk|+Ύwֳ%&u뺗^(oepz%H4E|piэl E"3i%A6m-u`eK|؛| f!>6~oΒڣxT[N|&*ܧ׮boGYsȿ־1/tTԏOo}u~WF0hcTU/c >Ъhosw0Np5tyB6b[cXB&0Om|,wLHj 1 3JIkdmղ$$͢E֐bވ bxvc~e9on2\DUB_),=U>$={rLfG]efjKr}.;: v_us8J!-C^Ys~7ژ3U^/j;?|GTL:?^˽yOZ/p猱[9sa_kw/qP?5OR68bpז'Sio|g9xU\mznyߗ_:=Ŕc^cJ_g26/;>Woe4܉ڌ4+Ѣ9n_ %z۵Å7K Afcm  PvF*={y9Wr Dt3y=ж(C2|NehJZܽ/;\[1U-qpyi|h_A;T кZ D?;3y>`e 03)8v'OJ Ka#9Isl ;Q7 |ڣ9[+k(uuHhψe جgi͜7Ymʒ:hJ)%̽gT1;cȦI@@aTPrv)9 8i[@Id';k+*jqNz1c0|_ͥ7m{lzЬ)Zpq&Nk4i"V.ub. ydn:}1>Hat\mޚ3:b|'~l}__]hc*i?̸_ͳG@S 71RÐog{9:woqMe3A=a6s8#S'goFY*aM:nF,^Ӄ0xƪU5K]r >_1]Aи:oGa9sIsXx)\Iu៤rcF~a2 7[7І~lk@H ̫Z,e|ϼ[׿(il=S;=qL]gkG>ӷs |po8ohL.1Ug~O.=b0۾| _O~f>%K:ւeޞ~M÷yop}5~h3m_ó Վ>$g_OGptlZTƋ%h=nE_!{F8u}Ə7G/VN"߼SD2$hzwN>ƣ.hȗ'P?6l.W]KSRc t)'06'܁pHX@_+-=ǝ}g5IʚONd`iO4_Z)%S~hP"T3~g4]v}p JVPHˆnIUj+\=z_ؾ]ǞVͼs*ܕI,cN9$ W*9m|ˁc5ml n?lk&<u |YrMU28{TaDlm_+Jui~(8tkrN^`o%K6n:#6;DmAWE.'O?`C@BGK\K" ڼes]2 _|U][kd`O똀|ܭxvn0`q- ڴOBg>W=Z!Cm ݸsMѷٞ =T]-\۩;M ʞU`nEkk]p |$϶5U_ ^2-v`v@e_:zd|pOѸBA)V og *өvӍdI?+yڻkَ>t ]ly"-9 \3-MhݑY$ *0B,|ӟ]~?x sd >vKfw(J;O^UQLvou`A=.8C1>% `:]`]0q!_x!{p wz5)}Z>Z6SJ=~|Rg;ibel'6Ο;;| \qxOo\b7}?/Żuto3ZxgF/ؘz]?+]3xb$κ8Qq8xESxtP%#KP>$MD3l׊'q2]kn\$zPGIJ"_KZ9!4,}C_ G~䇽˷CXUڳ$=me3>۫.`>j(Sskk98n)h ϝO@S(>Å%'E>> @aq'-˩ ח{q-izgy//}#{r?y h8QMbw$!E*.66%hA9 7^O w!PZHUƽw/,a91;+˒{5,\?pui3Ʒ2F\sNʨuFǒ]bR)kcO 8Buz`j+}>wJL/Y>E/i%~#Ê.dhl 4?=yX_.#Yg^w;^qyŵro[7֚hs2!? b>|պv<o V "ˆgVP Bٷ#Y;uوF7adVsr2*(|\Uw['ZgT^h_R=]y8WU004eޮ CQד4Ps0zxw[.Ņs!&0%^9Z'7w=՜UkpZA^[έ#)ƞ-y ϞUpAp3f\'O7ZJww㍒yU-?/,GOzJoh*˵7d2rV[/8>ɴFÏɺWI_گδu&]r(dhYF~3tf&?8b}#'[˓(h }z3ϼ{O䏎_Ͽ T?}uCg;nӂK,aG 73dЀך #E 猪 |@6`w.KΘd>#mD;Jd TK軪H|c4#ʉ_=ŪiGMIaXpR2Be [8σ$`GL7sm<:!pOCs?*lW5+' 2V%03N->^`;>.QBH{&XJI o Z3(tuNU0V̙n ʂ*;>ű,G{kcK1.sxe6uJ>{JNN)ٱiTkcx Wu=f鉂!d^wgsW3Sپ7t<̛gmL'c<:[{3Qh`1Л8&e2.3kC^G;pa#>>G`ɜ=w.f_9tմ/?zwy'=#zӖ@O\G`7d_ӏ~=asFA JtԌ`;џȮxtO_-ˆuw(;G(]6X9 x ->4NC%5nt?[=}w%7ۯ0ɁH$ =xso(q@Y>n+\?9AHed .U 'UL>R#F/tv5.?%npmzy ̏Sb{V2kFo+W8ټYk://ݴfNx8z< kM&*#ͱNGw(]0qw'\g3;,G_$ChFwaS"MrP"ihA"ImlhdwlWrJ%Y 9| ty: ے?\q~O=Nv6+6:Fh!XOtc3w{UV#ɈdUsʅZE,h8˯F>]6p#uSX7z{Wf0{ٖm>^$uK&Gfd|⦮!Tɟ{fitVOF {$|l"~d|̓5nF4 d;ykvo XwtpmޏoK"v R'Gm 6čo<?jAx~/M(||&#G +}W6mMdIY^K+s.8>`|CÓuljl^+Zz~ysm[ACH}u(ohlHN!F"n}26ƒvx:Pc)|In{WWv6qh u*ħ~YkȄA炙C|HCU_:/*iWI x||k#w/K˗+u-huBJjo[#+qQ'J"U| vl9ܾ#8N.ϮCY`i*52lxunb?^a%F{qǃ(VUqCԼL<" IvZ9G-~kW]{Izpt9JTB-zpTrG>d{x2Y /zY _|-9=wa'B?wb D:"OB_$~f|qGW혣WU"ȇ@\ƛWNU.u C 茸QE$qroJrr)Ƿ.'8'w5ؾw)xړ ҙƦd3El(( t] F!D2I>n@Zw%)>f|ADqQj]-j}E09}ɧ8ѡS\}οu20cqW/?`$ֆ?Nz8=9Lȓ/{yO;:NX 7^ӧOgn[^s_h=ںoBlAr⥫mlljtء6(Eֺ.iMrrYwe&>EPA2Ŷ6(ަa¥ᕩZ֮"f!VIW`VešZF!@8awN;meGl 8oOA7B#6i_4m֪5R+!#3F5EF!VS&)B9DPﲖ2}6 j~|D$bbAGAi-O6_p^ȗ.u>YlOY8ܵ{kZզ˝;Ov=2bhkәxI )x?<Ýk\=L\`%c^6&ÀvI9V} 1j-7έpFۉ-OcQTjG:ˢqmgo =Yl}^pŌ\u=gQ;r=*MbkbrR<Ց0K!5ʀTc~#Fe@(Rvh~{؇[Adih&H&-acT1vP}K794w//Wn4:g;\6?x?~3TR,]+ڹ{y_Տtw \}˯7sIa GIPLfU{7۟]?oUaKs{~`885lzg?~|gag ª2$ wRcbV;~ڗ3:c?|ܕfxW23Hڹnѣu)Ue-){ IK?e ǫPw#3w,i$^.;ۗeLcE2' 12Uj1LwTmncV#ST҇OA=;^@'MS ~џ W?AJ6e8c}.8~t$]:RHpshMw8 6e68Y_9WƸy pI >'3%we5+@<];Ǔh̉պ0FiV~'negs&sn^[AMj q2z|~D|A}GFN0zVM*zةShߌ^#'R9;(1cm::O8rZyF"e ieƴteչs$BWIWZ>Ko?C 3\xi[6IXg9|Z%}Sr]tYP~( $gh$Dq!wu۠rPq=9}`g+O%XT`=ZEO%;|D ꈚm;+~ph_s4뙽鮜%k;|,eް?лSЇlhoz xn.7á0ox|'9B#:;{vs|fmuÍ&1lٝրķ­m[uIOILI>QjE;g?m,;Νrp0YuWj4gt9o:ˍt`EWiw֥G3'r7h#wy9^|E[s{3}Yu:~ U:Sg<ņKmhdrs)-Ӄ Zs 9,鵜9;]Y ŎؒȃH'S q/a[ =av7 q4<9v%O|# [yF"6wc7кW}VcV?E7?~jM>< ёFpU`Aa$, msNހ5OPe2xiѽ,g WMF|Ԛӳxx?  U{dTs/l-?f6rp"*M>w4ֻ=/ t$P}f6?K֧si{k> uފ;xtt+(?}3cbG-+/]tNoՍ_If/&m]n8({~,?lO~t .|{}v9lz*ۿ8JߡiۧR &>Gc1NJ'#d6b5m7qO8q._#l{l Itdt) %֜C=KaM(2rK🸋>RבO{蘝tĉppr+߰}g 8W̋|sX ݊V]NC%al;}Vr(Ko\\s]k;@"d.z?޾nu Q$pO׹#@7xӖ-:`W͆zpWϥ`$ػdU 7I69"uμoܼqGqDF{> n~ .E_|9ßE|&: q iA'-JU]8Xfk}O?w%+xx*#nx#Q3={ӄQ+?F~D]|wN~ӳ}kϏv3ӣi;[nܨzE`?,޴QR7?~̾gW)v58#F(N*n 7|o|}o-|wU7Gk̮HZW_,Ή=e`[i''Uiҁ#'?3\̧rfpi+ 3Nvpoοa~xL9ڃ#UN FA!ܔQB Aj|57A0@btjgN:mBH}89Le_F1K$&}J-*$,{P?3(y 4]5*͝ڟqLb|zkҊc8%a2+ c mUtS1aչdǴ(Vл`[EFaM+^n} ٓ[O & )K |lӈ<GƣVR*o=U0?L/JɄ U1ݚla 0]npi{x'9I L8Ix\`RZ$apN>\eIq.6-Q UpCo-H2`oZDkGO7)[.Տ  '9 JXQ%lz-E8ⶣ裞q k/a[nӪD_Q[_z{䓏~solywO~o/pYr=p}pvݿVv{2nxiҦ< #}iY.RƮ|t(D1FJ|h &̓j?o%>)!QOV6S<ص{8[/=yc"2PE:|~>rOt?J5锻E;nbxZ;Qy/;7 a*+9UGns@Co]݋fŀ?SϪjhGవ8 '۽;9]wCpf; 9ۄrNz.]ϦsKnS%$4fn𾣒t$s}]<x?Cֈ?l/?9OfϿ@^/oֵޱo_epn:/$8gY69̗V ,p/_}Uuus%ϯ\tw(?^p#љ[W(gu >nfG9Vӿ+sOytMyq47۵;;zKifN&:2ȣ6v q_XBk,=x:o|i?1iv˯d:+;CI#I \ k0aٷ{1h;|>3͗ϮŶ\?|gmJYfm5H+˗{T>0\=t>{Nud@mo\X~t gѾkZ/B+?v*fkG%'} +A7h|7 /t ڕ(87]Sb =CA}FwFv_tŷ:cGKsƻKG<ΧBLз/v&$}W>Ƿy::y3O>QIB֥3K" V>5'x(H9E.1nydG| pw1rve>OJx ˯~ݯ|^tv|?u(r?]BњZ,~=[n.>Ytdi556wh9Ók@֭¦!`wj`(@'8>]2Yb$QY$7J rDlxQ ן'xw;Yȭs3?볒/[g ཯z%s\$l|'kͽ̯.rq'PEd!GSY}Uo[qvj-[t$uW%Dтyړ:~wg=j_LB#2i ߪ}?q,{sd~SȾ]4c4otkWk]%3~dz_PG0^%y[U<fe ?|-(aN|HxJ>z c? 9O[<~=Tݾ%۷\2>®hIq 4M{PL5|ss$ܽur߻>lٷ-ḳKګ' m12^vo[GŭrWſϒF\go:qU.53[467_ţ'gFֈ5SD$eS|Xf_e fqn)VmnDkW >" xКg.xA wq_PnK`h's l)E-) sZ5NzLkמÝnU߉ fCcpm\lBKLl;YB/b`[O1⻝]BHXHDKVq}D~늡7ku^g֚]F˳*w8?O7,oT-lh7u=fQ6 E/N<>, ~Ńy8= E_+ߩfk;c -pKFCpn%&Aճ5ϲFo>ǔ  Q;%/ 7В=Ɖ <ϤVy VA+~ fm [o$?ј6+JPv4I{0~3xϺk ߝLϛ`is'AFA1ctY?`LGP5:a<~\zGАjǭ[d^|vmyB<yKܻ|7k*zZ/$0hh R.(743S%cߎ=e4Kʚ>TVGp0;C/uǑW.,/}G_= MbȀe:og4rG/|6sm49@^ el ^p#{"ޠ5؎h^Pxsbn = tPZia}[p$93 }TR`4gdp2*KfG_^eSu)a,u᳂رIpñhu [A2qU豞'e񷊦CvLYOUk,ɉz׌!W^9Ggss!*{=]goar{%djrm_s"1RsC@xρoςu{Ǚ·9XM}`iݳOLxrR0߇tSKVy,C?vF6Ys[/k~fjXώ]dj(D˶i|}#dZv+߹sC%Hk9~}3lҎ)1$ZǏ+ooF4/c%h`0;?ꟼ?miͿrͿD;JBF-Z;dq+C$2پ7_z%=<립g\-cx0z?&eɣWH# &AIX9m_:jQ0;--e2͏LuѪUxU?d@AUV%6c;W\#|[{=˒ ^txxUKRR:Dk~+#k{& xǣG1x=0o{G9o7i> mxP<8}w732~' {'hc3Zؤ`=_ub`b?tu$#InMѻ nGFOv<ȯ#n^gl{=3Uu;Z{}1сl_f'lFWl݇;YFg9-]3±.?Docv7xG/[ 6aّ$d w,{d(B)V|nSȚ @$w |{/˾D#bWIŋ |أ#58Gz4WrlֆFck8? sӷ?i|PenZ:;BQ&IpGW&/|Vz}]"1CO=U'DZ=ZKuU@7Lzz{аZt魪d0=+`1Q|*l^\\%!'Mex"Y|Kup|0xOt>J^@A=~֑-pgBFy+/_d[{>OIE.ck*.v3Tnl=gJ gc|`=RGhgou\"Zwd/ ]^,1;0s0g#gIMk;zր-o&KaCǦS<+PR(FwS~>{%}N:Qw#3<Kјf-j ;_~_G32E[xk;q'cΑxp?^7ۇ1$ uu w:Ҙ7/C|3AJ-e16$Pq#{ȇ[J76En;$5ţ'u!3pk#לC9hjKSpjZ VXΎq*~ C `!~7&v[y)nconYOJ޺~Ҳ]K$ ĹZp'yCӵCDzCTlI?|2ĸ;7"Tt{ &aP9JJ$ZxBtNi qch&Qyr^@iO*@)2eū[Q31d{;-sV'HO}ns@(?2cdW mu=M+b#ɝ@c6{zTr(vxB\A?65iy7meFҮX3F``r򙳯P6LSSc`@s_>?oD=熽S!޳::Z=2 .Ǎ=xD&ʹ y4W0YEgk,hFI t "BϊpP956C*VP -Iѭcxh#= {CGUY/G0ڦu2G^c}4|lAa%b ,Ce }zJ4W%&2q&/?׵Z\ϑ*;j?Qn$];k~o< 72^HփsnBSм^>[_Z3'kQ{ @췖ieT0ikǛ[z8N=U= O/\-n:@<+S.8hksϙP{?)h8?nqí;({DпQ;ppa|_/0~U8"гYMN#<W}6QG覄17~H{1ΏO;B* eح"U WS C) HM43=T=)&k%'H跊8FL-dèKf5r^OhfhشWw';~L,Ƃu?/wbEoͣxxBhJ«Wʎ7'Kʜo;X0i`=e[ ^ vxXş2);1 *Z7!};/Wpyh$JY L_`ftɎBF|xD߾>ۡx*<|x[p$[pmNhy&z$vrN7 =CjLk[@y<D˾ˡ'F'?' x ?eu#PS0x?( ]ycm-?D1^4%<9 9N &Y< mQ7ܛjRIbG538\pĽmOHTӁ=]ϖ&[왖 HsІ+ڵ# #> uk[/h|?8c6C8#8 /r Ult`Wm:zP:-/XH8=_E3` a0Ѳ;`ed0_χ - ǼT=ypl)/֮^4I}_tz,\L-\ZTwr:q7YZQ/XӮ# qz. fdخt39p]NrfcxaD}Bk^=/b]Ӂ4xMW fAkPa\K-)$ul8銄&tR._njsO= ֪B:.hoϖt踭[o updtKN&>> g?"'m[p/']/8?x@nsGМW=FoUQ챻uk mpz{_L@IDATÞF| B.0ǯx@vd5kLycTz؝[X%4xrUs:< _WA&u*zV JNymF> kmcWCd˷7c p߁ g-y.`Lo+PCN0k4)AS*ӯ^YhgkfnSXbI$[tgE v~d~iO_OzodƑכ g믿5 .7':>0HA/-dz+yյu-~5]&G 9Vl*+r|+Gk[SOq=(!wkW^9A7>$n_.m芤?w;Ӈⓟi/T +~(+ع?Y*88^~t,.'u⧝\߄0…e [뺈O ]Cj OϞLNOQN2I'vh{[hvW=ƦYMZ!M'ux]O9~.gݷaf9KgϜ&WUzF"aXT1F,K*Bx![@8~huLݛ-V感zD1uLN9Zg%Vt_6mIP$Ս3uus=H8w>[]Lᬸʵ2ֽo]b?yOG-|S[If гx t v4#e~eI 7z3ߟIZ~x'~zU СV ~_$"hm%;_F xX(?/$HHّw׋z#.^E$`9Zv@Ntp}Rܳ>yM M"6lJY8$JA9hesV *#nx>գ: |uQxn< UlC*@9iS]_BՎka*6{%sPYo'G y㖭NXۉ1(s%c˦ĺ ][[H0;AcOH`ra 43 P "{%@b3 ID+efjCdh=2 b n(8ZrR)O(a(;g9{* Q%F1NIqv*>USP5&ūLyxy$7I01#X eh5?F7?sc7,(4`HȯJw6 . :|isÝ_XffzЄF娛j-дJY+c#M#DЏ8nz?JYq6ﭪU(%ag_fj% g_\vT8P0lk]!6:r 8y=^[̂ 95ji3ikcj3_.|o,LgABρ{^-[1vf&޳*mo8<=Z8q4XXem엔"3!.FX =o.J 8}8y%8]ڵ)#ڶob( rN_(p4e ; o%";»>?B h_Suh1$ `Hf߭KP#1Ή8~$-&eCo@y3@/.kt-gc2C9ws*evϞ fKϚ=_ 4(GMF>^B t{UE~ D>WEUkvȍ>] u{:aJ3!>A& /=#9G(,m6v&{c d1'5D%Uu29Y7۶)ΝPd\.f}-d07*q`cnfrDvԁFET,NPXyRv)H5 !%g PmF QMLwG  /?𲞦\K~.: o}eZg_?[^KUߝ0ٶ|_J|;˻pa_7}7+ vK \mxgK4q"qa V?*7?xfǎT yZ^zcZoOG ډE:[sl>ًh/856O[ã;Nd+)nMMX܉foZi]]A)7K_:fk 1+h r @35ҏ$G6 Oܙc%tycwujIs֎Q%`Afy>q鄏/B+r#22k{0,{Ys' Jmfrn<팸-'E?d#yyJ‘#K5pr3GPڧJ\JśK c4-A'IY9/08o-N 4>XճcE L\z_8YJ?ƃɂgSnb ?[mթDA[Siα/k[0dFәpt0_+hzۺ֗4|=w&[r+':+'`Se wВ2m u'as6^BWh1 6祹Zzά&}Ozacpdhsv8,u)oZ{J_l#ѻ#ʚ(s^NV9kL<h0.lj y`y]βrLY2U%֒Z+OlڵfKyߩXY. [[v<j~x$÷9pL=3k;pgHdUvkl/Y̢Z?\7>>vرttkvEíI Hp$$ӹ$ eOZOk|toӜSpHh|ͱ|bv4HI*uxү6x-}ߝ8--0dH;(pX=J]{翕n׿.;+,צ[8-/Soo{;yq yo1/׿&FK =<򷽦3a̯Fgk#ȸ(zZw#^3 ~VٳC@~x= rVzӎ`0'zV<&Gj^֊G{9j9,Od'N6&-IaZ)F8Mq{OT6Nb$F莡Q.׬乍׵==fK-Ig˓xN$8MF>vB#x7کCp:\/l˥l݇eCX}]ы| '1^\]y}_E\'TZb?ۯ]\j:x1zصG_˗S T BŶ(fzCnhL~r ޓ-KߑO,#:@6tK͍tޕOxų| Y#l< (t!t3L:ur Yu~#&_FW"/2\$pg`x: kn~OyVu =|m5dGkE 7#Z f+DԺ@ZBST6|rrp4T&z2T@7~xO7kyR?gI&G|3^:ա;g@' d|M=<}XQvA 6Q}/qsWa ju3yp/Lk! wf{=j=|g4i кKIZ}P+g$V㔴?X~ֽuvV񮍊Кީ*}}pMxxL08m_,F {V.et xFl86rtdxPĵVÉ{~IKj<Ҹw~rsAzb hMbp@8zpX0*s3y@4/6jxc nOIW]s]_$԰0Ɔ8Б r زpy^鵛; I dsp^k{sCg <|ILnmBO&;gw6vun\ٓLu%֣Gzk$ɹscKd=F cXUIJ8{ iKX>/> IYsf{S`"AU]";!`M`KNHƷl%xON)bip{+O>WTh٭gX^$DNP'N(h9bIV~m8wnUe'N:}tf_ܫxK/Cyy*{g,N%w}:oJ# '6v``[ ]';Z g[OmM.kI*!ؙ{Dõ:yp9?߹kO Un)L1= ~|pЁ5& @;\~$NQa}p6wXQ<E 8I9M%>kW $\K"=U8,ЕtaR}yg>9~\t{A0<|m[L|8Zª>Wq|olCkPa=&m1kyq0ݳvHr g`n'x!R:#*<n+R"J J?5Yvk)$2yH~,##ȅ"t[\)0 X|kw/+>MNN4J3 B>e=˹B%xy3)sX9! TN-C qHDŽK K1`b{crΝS/"^ ͜=a7Ƽt7Hh i/$pS/(o,N==1(dth1 lGS=;$fN5IG %QlWw!$5)82 oL cgPL$1Bc"y *}&xLE[[(v!7HyeF7`U(1煮OVWRȧ m[ܙ?HX4C$8*q8!fMh Q*"8t $Kk֬) 8$h h)Y/zhHih;CҏJ.wDK CU8$ T20Yx!܄2O'&4Zpo3{ 9m zqڸWv2&axb5|KrO@XLa #`{Ɵ4YʨO)0Ho~e,tEGue: ;68cwSU8{gMo\=ixw_<ٞ!2#woG k5GC;#06^>9gZI3td/olѷǎͶ{DÛ~vW=a[r9^:A`[g [ :l1~ztjY}|~ ? t%{o61Ѩ#{hq.fpͮ9١>S~US+qݷFv^rcw׿6_+_Z\%?7 f"m ~]iz>4{rNoSwXU¡~K/d p?${h/[z=D;M5I=+6< gKjE=>sǟ^X~__zk[˝ 8+^X2[&> Sz2c $nl޹npy//NhNbIΕkw^{,}d[t~7v J8ݽэ峫dCt9I2=Fpzd[K7s}`I'ז ~•WI|ѹ嗾Wmb?z-1?!['뾀0[晧g?\Iӛo_jƧ?xGx勗m%,U0/ uϜ\n=+h.||IbQh΀htptUg\~ʧ[b*o[7 ֫|lw'0WK}NGLc\vgQC[_{syxuIٖ 2yu6F@k_>tΥy|+n,,_ysx| 6?ĵݑ%k]gOWzk5_\λutWu'i O?Z@E"oCػx':6ÖdWpKdT:Dۙxo t!vj6A:0}8t> k\N7: #{L)a*cqO[;?͏g*Y(ԽMObLpеۻoc.<~HsݳywzvWֲ]YgP߳#A:x3yiswwGWCb*W\ήGn:lY5 g)]_c?kJ'ڈCg];j ~UE9pl:7X_BlvWϪU>Ic$gN6:|@?@֞rl.IQbR NaJIƏ^BW 97v*rS&%;xEo+}g9 ڰIWgႮ/^;Yy:Sg5o~z&"a11Cu3}X*ݻ!yj["I2dE#U󱀹X r r SUV:6hll{~v?0t+nM|Lx޹jl  ck^x~xDk|L۷EǏN-h]_cOL`znvP1PU0(OߓWH9Uyt$`v';a }ܹǩn`/h/M(?Ml=xt|fm󎦂HŰZ}&Y+Mst+< o%t5 :ݕ^Un䞷΍sB1y:R21q4A;;\ \h F>6@҇aJVz ]%o+ur{hg'|Z6HsׁB@ym8樹ه5DtVÞ64D(i-H M?7ረ zEM&@F)&pӝG&S:wV+qPe #3v.d5P `j%P 0D 1(Y9WE }|$u$g- ' k#[FPǁ7=IInWak/)Ap&`d/dqXN}uy7?ҹ܇-)\, f|ٿK~2)"e0'~e54)q s &/isq|JLZ`s#/s`^ps]yAЅwko+§xQϬ <ֵ'C!|g|5~lh\YpT7vq/`#iQT}^*t̸:{H8T Qe|O^,g^r̳#!ck0Gq-R@bN: mQ]e2!ZO 1ēe,H\NF*m]qa!;Fᆿ71pUʜ`,KʭlzD=q R 6K:i1Roxo;q" Kr=sF]"yqqx_^8wtÏ?>|=\UoYpj#ye:8tY@;A6Ad`7͜_+ >&xسmJN =zښ$4ҍn {h t]N %]jm#ģ׮xHwqaH1zr畄w{=HY$%C$)޾ aC5.>={&>$3^.+A ܓw3o^Ȕr6;snN-xWkx&Jn@-idϽ^G˅ ?&XA5>mI_pv8Za9X{O@ym oWt`휢qt.A={Φ78lnLs9$7i<27_d}lpu$g^C 9!trp5o_[ |}YRҽ>lsLL߁Z֡bl=MX]9K2/a5 .XDJIX/j{&+I-ve9=$mkm8:({I-gڂ5^%hx.9w~+o~n+9  ȾƞK3f0qV;ꋎ?g_ UG m0kU6ׯ\Mcpf]ogcɼ6odzp`Ý5*V}inN+Vƍ]W=lh{??8n_n?#!H>ռ]泫}8:A›vZuvTЊʖl!,w؅~f3^`9VaNһ{xWcyF2׃)2Z5 'gǟ]^-{7˥,!DO7w]|}6<҈Ӿ=,vOKiIZ`i=vW1~/+η~d?k`5 GŜgKB*a(rTE~;Gt\sZwjs|#->lyX{F 3&W-I5c*lBtMBͮ4+.,~b5N1A`n5d X 8KU(kkjX?;2ؓWk_ [ٕ[6= .ln1_gWy%=]\9-W^-zFE,;,.:RSv)ڝo,K l^t1;w^Tx/&gGp k"]4{۵ pSv-}Kƺ fxof͇mEffQPJvCm4Cp.+ܛ^ml7^H/g5C>ȆU?ڨVp{*3U3?/]Z% ml }@R'1e0kא 3ȩ`Jv*qw4X^y:I:_.:e@}ENh*J'V\7*^%4>.0$90HxT?ڏ"#~GR m:V*<5x ~9:Wp tFu^D3;u#?/OҵT; O zhOd;wkIWz)Op:7:5' kl,hK>6 ļ:Oc|.U{6W]̊YT'cO7._Ml!.?#Ű]Hc_|J/vߕ+ςl9g ]:t٣w;, Y0eb.p~x5vo9PVa6r OnI9jcG.nfC+k7 ŔT:$Qw>Q,{?|3tm;[LW*:/>t=u۵56 ?lx;c\ WH`ט/o:θ!}s{/qp|m8\ EV\wBa;v xyCYb.wgbeֹج9FIwvۃͮu#}dz XP[s^4-?p|k_{},쭃c{'t5?Uan(elM  #Bk QNCB`JDWH_ W/|g0b;_V?Wo!g k_lL9l3e`"@@xs̫Ϳv G,%#y%-ya`S|鎰w7& wgc%`/-jvp4[٘%ng}bS-ښ PBٙ*[B ^9TC0lsNXrUshʸ!0iprR)ݰ=^٘:d9?O(|8jvM[pPb *)}z:/jߑ[e (o!W۝[e3 !rK_\sS ZW8<#ž[4lmq\׵q|9xgdW:9x6;͘H <ٌ'Uyx(S lo=9b?qyj(Zs;o $pP= )7j-dﴣRIjG߾-5g3"QΜ酶n\8qr kΰ{g/ SOܸzS'GM$ign׉S/-eI'˷ߨRt`XKWkdv9tg9*kwHF΁q2@0lM9 F0F!(pC:p,sFo9{Oe ?g֘5'f @ߺq1Q ~r GYpݐA }rW/mmDtFhd(~|q$Ȫfhɼ;k`<>73,-JnCwuWygvwaa ؒ%e[XKO[08`  ;f'LOtsy5BbaTW{|TeD>3d/|N5XA /G`eMm* { (aHZt1t9:l3nnj zWAI~3 T<κmb#8U@g93@T>ʿ;W_ w.gMڑ!C<# ^]%3 +uD9 drB%I]1_K<]9d>cps6g<6Jd-H:w' ?cSuKvL3u|+MPa!9q&0@A ~!t*g}$*=38#僚#ụ9^~3 hqP.F q.=l#v6ߕQ3'ȷkW{BpԍN<]8&1r78+xmeFO;-,js;utiQ2a?ѫ r&?9\QGt?z Wx#,%z?yȡ~>v0i_u6B\Pghik#jUpĦ"p]%#NKOu+a ] x>S<9_aCy@xN91uڋK jy*@IDATڇuL%^acN:؃2*tNh58c{[Aʺx$C4ЩKZѡ Ȳüa,wQ}}Z '*mmB px7K,FvKw]&.[] nzډGȳ[;g,tfevj<\TNMNљz>e#XVƤiPbu2ʻ?5#* WS̪·CȈk:鳤39iD+j0bW6&i]pbSw8<ؽgc\\OWWtf{TJC 񠭫TU ALfwx{o!dd-;0wM~7Sku:0=3?+{f:]?⒬A93YqNLeL "ݍTM9U ]d <8lGD8= ZsOV$VYݻIˍi t4ɱxUFf~<u|t:Z_y6]w?kt=#QL=-v8Jcia ~|T7^ 6PsMuu;iysV]#8yly|}R'Q38_R'#@>2nĆ}tkd1VZ{T6ǀ͑:I?p+ekR/gVS4buw g' >"@#PV:yz>TKM[$΀}"3)$  x3$ ޻AU#k)g[Y!lYcqSۄ&Qo%|qw#SX`&oWy->P\ TZitv<0un g d~&R>Q|6OGn#? CIg-aj{GT(8e.o]s/]zV"ȔY:u 9γ6;PT6nXS+tNg&'6~@lv5 Oy Scvt$J&lw,r'Bs̗H|Nl,e3.ԐWG߾W{eRteJvIN&[LOG!=]t5WT7Lܩ9~6'X :੅ƏPJ Ks'8qLzJMblftxm/ i䈎+HcW6P!'y+MV֛£8G<A0Rృd'?A&f9 kTzY3 iDxEG[Z1.oG]iPhetfgߌZ*R*m%=)c`^0RuØBoB(_["X<@z a9αL0{o)rE93YRjFY@&Og]Z'6 ,VVp$V +s g3Y ?S>z]N^z@*f huF3fvVdA\QT:#Y!*q̄| Cpt.NM7!LO !Q9f:K1%T3pq<<_fJ( IXH/ 6UܥکWsZxP ۞veghVitUNah >I܀t2N_F21Lۘ.*"_"S¹ML9'/nY|k%PhG5hn}Ow[ ʺ*i320>x9fXEP1 6iW rrb,]LSS88 :U803KTeK ; qH5(Tg]lʘgE U+4n2suD6S-?SS/)h<@3ADfL =\X<< ,QqLlY*(-{`s6ÅK F ˃U8‰tTrm9yLY 5+_:m9(PS*Hu0?;\"| G s"ƨyVL˃va@{KW=umѣA9GXFO;QD!(m f9E>|63UU27XKSCIO?̛C#'瓵W[yy?m0aBx8xv^T8zZ'6gm $m:Uq<"x&~X`셰71Ezj q6+$ OV5qLL$PO5J{u_YNk ?_a(G.}\P\mbt:YwMpw :]֞%^WSdݶ~?<ZiV3QO\'A6eaNMr!vG%p:xy$vu3W#C Tq)]8+'_vO9= X-o]k=6cqL\sCC> f=gS'UcUș<׫˽v!{OogfSaK950}wiBqn_4xwi cmp#Ftuu1Fx: `-Ax_O~& ҃ ~(s;"Yn*K:Qgz[㩳Zh)xY!q_}G6/Ԯ\xjNǕin}Ov>5B[9}t!8GLeRЈyd e}m$dVcG/a@YdeQdEu@|)؃G3$p&qC`+O9c& &~aSnm]hF>ݩrt2F)X>u3_||g=ux04I`j\70JgLQLwb&M4z*A{{$q)}^fgRmm5:qJDV%J~gGP|RosߖP2A_B60x.="I|k57e&&9~#7d^:rU=\u_+#6$2 kw捍 =uF /@X{155u &0N?]6C:i //s6LOhB-嵏=,_>y:%Kv[=}b9(~S&pѦR&3Ԑq&,KZ@{{clb+5Y 6 qBcE: ,~vts fHȎ= Ekwh7v؉oB H I@zVXtƼ sYxzo0N$]@{cMM t56m3M(s 'vď +`l)3a٠p!=sߗȋ nBtxbr^ؖ.'1$IGb++oLbo~mυ;dtqW[CG%:ǼT (k[^ 8ǚ[ۣc5ExdVZ֟},N٥KkSmյt;K̃2]ULn v Me#R 6ULj2'\Ħ& ɞlƥƤ ffm` c6Cy0ϓ p7DA$6Wjmy ~p /& g3({`PI Q=Vv"{q/m2 a#@ 2Qߋ2}+{d;b<=f\-meTwH:p}V gkSOR˩g cdsg' 2^}>#T9dgWOh:WetoL{Nʣ I0KFJOuVaG?V?F TCSA* 3biZ%pSߝ~8bG ΰOƙ@W 聏vzp*ohSqP =p;Li.-̅Pcl=<ǩK"w.΄F01 lr13l*vɊzѕe-ߊ.{;[a4R"y TAV,>y#{?]J^8QpTE\LzQɑx fZE, \ /.˿(%0 5 GbʹY9@g[j|r>Z#ڜyoI/WƉI;:F$IRݻY&a; `,h4TleѶ,Ʋ%>(#VŒ[V mF@v<Ҭ6 s2@Z[wa;Zxr?^2c^q,e * ƘZc 8i;rF^+Q!s7X}p t?V4f~KX`^=&h+ 8bYh:wU9$~-t°)WKb=d}+vR ]!0;("M2/ Gx\NˋkWK EZgE{R`(ɲIFbMޯB>Mu.|_Z!õ(*ed0楋=& iSAC)vf$V {d,!6"kՒNϕhF, \$/8jjz%p޽[^ThNO$ĴsO( 驗>_]_1 565'&J3M#@;,C; }腏ylU9ge@i8@Y{~<ఁcrXK Œuh\S1~l"c*ct"'@ڨh#3<' u [ 据$ 3,m=iE)2)d2xH f{uVߢX9c= M=} GQ WP]"}j 4bw|(^lbr$:Q/u֮#Zg : =ҿ5d[kuyx*SRxb/u|D%`+x2pG580 @O,3 ]{hksL0{:t\uL*/X|DRmν6L!x<ϬM^\LT_4 }9TDh;2RKJStҞA^Sݗ]'qґu]|<煡tL8X/nׇ\Oq)Ia`-K,#I{6|uA^D'Te.yu Tc#H,Q'p`84 NyL{]g%4,=Y9Cs1х|?[ƦAR8=qd}B $8]p^Z6R=V:ia3_'3%p=BG ƄU0ToQ&gg9N#h?3܏-`7g^I#.C$-qE& ߡM =Kv$TQE42|uO:SY&xpa 1tGg:d^uk+ԎH,Tf{Ԣ`^[_Wῖf%,M}wl}𳳿Η{ VØp}LͧXagb~3*e|  hkҽ-1IK _qDcc?ŕؔdLM[FؔIuTjLGRJ$PVE}m=OOqS؃=A%w:R$祏QT|MA8 ?"IXA>Bߓ E?]-< 6757<ƮTB0ڈIbk}SjlVѓN8 : g-/\Ư4EW,Y`YSH( -P.[ZjU]Fz#yNv]Am8v:OM 9 !~[͛WPMIݤ}++R@D~zCV_L?l|%g'rznً|4!HMs~=&tjCO÷'$ק?H>d3Fghw:xέvp6o~9~*;רo2pv3QZoܴ;mdVso6We⦅v(>VQY[C6~[QYowv)sߌ0mpcڱqHH&~g ~C׻m(F7s j C/_ yXL i Ft>~IQoz NAy dV1m3$a,WfP0YY<8n}>'.ylEjӗ`7:=G$ {~cjh}nkkX[N)+Xmuo{sMX",n3Bτ kqL^$77cG-_Q|dǐլS?c:(7)[Sk[GaS|gB!l1e\ceqX$eS&er"q1⭵<4Mo:B-V=ɧOr5&4 @7iޜj9u};$P|sf.CQ$QċygJ%V_0TϞ1@|Uq62GXx$[%z< .O41]09OLĖM2MMaW015j)̜ B,?z2%t.}է_0.Fwњ1p: 'ACQ],f xlyhEE,T  met0˸,*5whN0ol&W̟aj[-^\;UU=+{AVs 9w۠ 3$lB' bfOxpgu|,oGo0 KoFd9?z0]r-CyN0vjڪbJ*?3f/_M$X0CD?Os'+1&F)O}/tZMj`W**V;:2s,LY&`=΃9CZ`_M0Y~8PUn<'Ո 4ҖĀ6\e5#Le-0=((')!dy4,C1 1x{v *&m8,! j 5\٘&w-a>: :G$x߹7VQl˝|rp͔Ԉ. kqx:jƗʶ WjlQf30nYm0sD5@ZF<|Z ` v^ !`͜wh~8ΡmojdwuuqNdЧ ]d:PzOd x GG@1; < vrVᙶ6p'1952GxȷUI:f +=-5t xs6h5I& X; g(n'KY[80h+ lVQZ'] 琣`t}fX7ϷV:M}n:Tuv l{榐m0Y@Cx_N$Wc)3|١ʹ*K5uJѱ.ty<$n. sP$/l!ߜc qXǗIe8MJQ%@mc+WnWP%LKXoeY:|/a(/QGtb41 e"l7aGjiWYxTӆ=4Swk&tپ G3pϛ_ζ+6a:k@y=\~HJ+y JS$`++wk D1&^q<-1{|}I iG6հ'7iH#%Y2C.-C ƊJ6X|sPdb![\4a|z::.|P{>Q{k*kb1:usB/oGOgU{@H<[cp\npaDW@ ~ 2Iq~44tuKC6婍$p x§b'%U|\ԁ"\4fxIYywBOʓnZ]rlr0GՈ btMi[!'Do.*`? Ҷ^MvwxNX&FW^KG,~1ݾ>znsР|7'{C:/gSq3ϕHgD}'&.VKԿc\+UD|bR,؎K-=WRJ:"P_Vnfk~FwHJJ] 4ʒlK-M/q]*+Raiu&35[QbRNfq_;1GiKw L'Mh(+p( l} jחHU=A:P&|K<߿GQcW "YC\ )p|θ_]'1?z"jۡ}r;=ޤ'gl ZSUDo`ڭ*hQˊh ]C_>,$PwR;+ڴU.Y|:9Up =VL2vfRmpAjH>l[m;Ԡ.9>mnt^jw-;xQwSL%?H]FxGQZl;zSVwRY]^L8{ɦiׅ/G}=r!{X2ꚚbͬJx=obj6#0xD[}^͢ <*shLw_S"!x5vN}ث~p[d!N;%,;C#J )QאkSVr&E1E}AtgKN( nm@CSKY\9!Ch߲|m[H.] O~Uwv pz/Lws.sjr-NC#O75`gd/Jp Od{; ۂ0 I& 3S 2`3Hnnd-:BsVtV7TXc.ζf>5VlS/%bk36Fc錸z/=ETG=x!3[+8q^:2~a9tJDG+("S9P7k@W/DƖ[E=G#ڲV˧NL:xcw4S%@–bjF,AǧDe% ct,φ=GA2 cy!gb/*؄FMt"aUfdQf(tih٠gdtݯ8XBp^JPyx^oCVH*1qJeJ!3upGr3g|FJ(W̙r-U(~[I (UBpȸ H**]izfR0u⚊<b`qDSQSG!/N5ȺWQUz+Äd@QY' I@iK~qn[-nXe#O89/$D5ȟf胷y;qO(KІ $ h6,O[m=e8 |a-Ƌ5l<2w/1=.#ӷaHCH`F5a󹴲hE.svtx>G9̚WcާTv61kUqNe&m ue9'L)V'c.F\C\Ӂe{8|脰4t? ؂KgN㮾nġ$tW/: TԐ3Q5oXjrg*|L2͎- sYFvYc\}$5o<EL8@ Hfwyenj lG੼Q6 _t( iF~A;:5 ;VVCN2"Msyeؼc "@aB h)fLctJv$) M'C˱E $D׳Uc( PI<$Uyr>`.lYinj[l@0qτC`k`+XAWb#V k$ßqmߧ,0v׬"\^ Dp@ŀ+pjR{&|lqh`:_lpoec昷zHˀ0iSk')"C*>:ȭzWK3Ki@_ 0;!q{FA>O?|x7}|&2!5K@:VvTW z|8yq\עK԰@]\L8Z*L6ah)uH0pQ.._Y7D7}|7?ߗZVM"쫿J* 6p.+u_Mya;* $'Iot>:28hͯ}9;:;']Р=q!?vEe}81N 5kT;@;t]HV@fɋKHFR6{ʷ$\s8ۡy2SoW@&Wֲfh2v|!/mmիOoIp|u@nj$S3yD싏pxLԒ#]{YAg]C+40 9&}fMlbOK5x‡(0V$*Ƭ';vy@w녳kR{TUP]1{2lCmO m<`:|Sӓill4lae93fE5l?AӗcܕQ^} sOg)%}o.xkmިCԀGGÎSiV^476 efhYYV0ܹϗqx|ts}{9=I<338b o;*B`"!c'^Z$,풙Z[4mGx_(G"4V@Q_Wbb1DЃ]3bxd_5LHg$SyvI4e nrj*M;tG{˸ !\,siyeNB?Li#ҋ˝};NtWynVq}+ly c 1ng'&LO7/XĶ7G+c(`nr&.lI*p'2G*3iV)M7Ҧ=:RoM [|@ Ս^: Y&˛ V-L /CQ7vdyp@[DI8Vt}ZŅ01S~f^6+(l=\K+t]OɊ`yWX+k;OP]}#v;FWOw$y7aaK\]x:EG~*mmNo_OٹK<яX& vΔ7IAUfzr(xh5kM~x0*2r5&p٧TfITVc 79⬏=b3=iV9r|Jb ӳ>̥Y*ũjmYCVuFUGӋ !W46I8/Velp NfR&9g:*$&VF&"om"dRk6ʙ G`ͶB򔀌s2`6ٙY /P՝1N ȴED3HBEm:6 h"#SiԷu(eg׫ܪH^LeC&UT(v- 2+(,8}8wG ;kNkG{#@vCԅGdiX!AB2Sr퉂B0`O P9=03V*(O,pgĪc2*u[Kg8sAF\jaDV0ᛱbfQm{s @bīPY*S9E\+U$pfBzV $>s[9AhQ^ ,d*۩Du)8. Ec5  h/hDRAQ9/ R՞cWy6s)z^ԫdigG4;ɱ{)/E \dX5 ]5 z1hpyP]`հdAΑE9[{OdŕI7^^gaIls3 x|;( s%F!A h\'dUF  8J`!SA9T|Ii"2<^(OPH70($<}:x i9ol灋G' h+jlEV'SEǶV?Qn9pd:mpqLI qT>t<{F ;_$?*f/-Y>9kkO4Q du(?g&5sW%~, U58  Y* VԷ:]5:`l2]x*oJq% cF`\:@!pR>>, Т84 }=ͼɜF voo#N q8Ed͍-z# );^uF _PD7=o=3,߼\71Yp:T iDE oBuBְom`j!s =5 |zOQYIÐ%HJ??^:MN8Z x>Wnx*p1oV5& s߅T셹rFG={֜C1>5&~2&[3 ӎq룴15t䥎檱>,g/nu2*twGOkY*ONOcHM39ЦάGKVVtuw'/ԋFSz.z'jci`TС*i2s\wT Dԣè zBɜ"Ng Ex]8+cɳɪ^&37Vtp^:\ aP*VJ6+ .Qf1A. eO.… Νq|j dr*ʺHa h'ßǦB7 ʼ r3Z{Hs)8S/^lEO@ % øY2z\wq"62ld0C{V$9H\w\,hEg:iL61g"tW{i噇[BيusLG_7u7:k:hCcn E*iY9|U85A~9?d/-=8nKt.rT)vgZQi^߅aj6% xy}5yu&WUF}Rg$[hzo+m#?wla+d[GyrK$8׵0>yЅ`yoCb7g,^&<50LHⅼCކ [ߍϔ3yOPA!qx}>7_3dMBZ;#c|YޤGg ?)W1&K٩.,(.dOzZh7<-Y7x>ue*w^"U3:g[o7haPI; a[ol"DzL,){Vd^YaXɄ˳ PuVS6JȭH>[.e-bec[2֠? :/8o~ Sd$ӽVVeVQt)7NuYzy0k"c~ܭVY g)bHtzLhSt_w@7g<ڠXib["Hlu>G巕͝$MNN$\@_c=*Nrڅ_0Jۚy|OA|o= u}$H |CO u̠=[zq2Q:x<8ǥtp$oM.601)-.Ln )a!CNG-vϰ#:݇k6>a= %1U6a|0fN , ~G`<#O{C:3)40_R҂}orDM[=#`?0%%)iqq1wu*ZHvMe4 \eGyT:-%>{i~nF|vXn[UU<8x+g1ձL(6SӜ'ͧ>"+<ZT;T PWƭE C3 T2BuCOd$ko7mpSoc$TgEz(J<_Yy Od5T4B_|64-dPοڴ:(2 r0 BS%ȱL }"sxx'}~VȜӁ0CpHX]=%v9-$,מz>I-T7Xia~8R5-ԞG\gim2v 37c404pE:Ǔ`'xWkFPu@0o=}iA==6A>a.޹Y89`Fws844ZEP1BfԼG&0V4+spV {.2Nxϓ7sP&#78+pfgUxmuMq6 衞KhTwx)kL u{rD,(HG`V%mx[{Q"ٝ! ۝ 0t5&Rd|-bshkjQ*`b*(D Z =*'}tKJa"LldBԃ]]AcFc Dtr+i{fx/j)ԊF|3IW~Zh3ЍCLE>&BВTƧ EK#H!/1"J&L:  c*_!Ptύ@%u}+CAYAYY53eEPLOx`eYXn4!X<lT;ltp`PE> ]gT,483A1dmH "?*aQFHb:LqEC[\NٓůԬAeΞWH3:tUW "St,h ¡/8rOmVU6mFj'[7( v& |dsTX.Naqv\Ȗ[<" to|uc>4 L0Bf S r"8 'JbȎ]pD ' i Th``͚EG<#DY^jqAx#Op%*by(#O\֐ɖ OhAʆY%$>>u616>_ixȧ"3o~ji2L+0?3Qpsiʲy'R?\ԩcv/$|t}ՇU|9[r )Yȴ QG$` r~j TY鋿+ Mu#]y[_#3; t+CY DƳX^!; XnWpDQՃ&y(ͪ,pD+RoO:gbC?c"H# $O1|hu~kkVӍfAl;g|UwʆEa2aҁuo˺ uofIPCalj&S)nLئ&'3Vw!}w2tp0FT7KX˯`gؚ:M?>sؙ[AƇ7Kx]/?dzڳS30H'bvEEby3Gݷ)Һ '{ 3UfOH|PIC2OSϐ3eMDKpG|poMeGa*Y%' +Z r|m(iߗj_GL+)յ~<'ܡ׾dh H#9h8k*'9>3yW#,Noedy,>l3[=!3>n ]R#kE'ܗ,8b# mq1f= 4o9hVx?.B q=YI$822k\;묔m=|E%3M)0;7I<߈:xsge~&xe=CgPe; 3ͺO%,`1nv{ ^cU4> &+BZZ eq%m07\&Z쭟f|iWzᇿMr1M?DKe\&o;:-i\~M?woƮvV~DǾ:\2T <2tLZ>ZvDw%.TQOh/'Xϴ 2=\pF=4\7 {.\LQWt,чtE~g:_ 3MRڗN=joigv8ac4RVZ^n^jR١ǙI)CsއRI:쳕J]E* &xfĀs[#7?$k:q5!%>(kd$nÎC+)(kt-}b]-4u`wr)^.j==b y[!$-umlt<}vz;FM&Hd#@!A $I?0 ð GVlZHS,;ޑwٝ{0D|GKާ|ooZ$^-FK+̢1olUJ{yOOoϷk%P@J_2#Kwjsw-'՗>m9XÙ2vfőv߀´IA5ЭWln0 ߵ}Оv؏sZ.%NJJ_C9:2&9ދxTwݠpm݊ϡWEK,(!\K؎| |[