diff --git a/pkgs/by-name/co/colmap/openimageio.patch b/pkgs/by-name/co/colmap/openimageio.patch new file mode 100644 index 000000000000..780a3c4ba256 --- /dev/null +++ b/pkgs/by-name/co/colmap/openimageio.patch @@ -0,0 +1,4927 @@ +diff --git a/.github/workflows/build-mac.yml b/.github/workflows/build-mac.yml +index bb4b844d..dbb9eccd 100644 +--- a/.github/workflows/build-mac.yml ++++ b/.github/workflows/build-mac.yml +@@ -48,29 +48,31 @@ jobs: + - name: Setup Mac + run: | + # Fix `brew link` error. +- find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete ++ find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete + ++ brew uninstall cmake # Workaround for CI failures. + brew install \ + cmake \ + ninja \ + boost \ + eigen \ +- freeimage \ ++ openimageio \ + curl \ + metis \ + glog \ + googletest \ + ceres-solver \ +- qt5 \ ++ qt \ + glew \ + cgal \ + sqlite3 \ +- ccache ++ ccache \ ++ libomp + brew link --force libomp + + - name: Configure and build + run: | +- export PATH="/usr/local/opt/qt@5/bin:$PATH" ++ export PATH="/usr/local/opt/qt/bin:$PATH" + cmake --version + mkdir build + cd build +@@ -78,7 +80,7 @@ jobs: + -GNinja \ + -DCMAKE_BUILD_TYPE=${{ matrix.config.cmakeBuildType }} \ + -DTESTS_ENABLED=ON \ +- -DQt5_DIR="$(brew --prefix qt@5)/lib/cmake/Qt5" ++ -DQt6_DIR="$(brew --prefix qt)/lib/cmake/Qt6" + ninja + + - name: Run tests +diff --git a/.github/workflows/build-pycolmap.yml b/.github/workflows/build-pycolmap.yml +index 03cb18b4..6063c215 100644 +--- a/.github/workflows/build-pycolmap.yml ++++ b/.github/workflows/build-pycolmap.yml +@@ -21,9 +21,9 @@ jobs: + strategy: + matrix: + config: [ +- {os: ubuntu-latest}, +- {os: macos-14, arch: arm64, deploymentTarget: 14.0}, +- {os: windows-latest}, ++ {os: ubuntu-24.04}, ++ # {os: macos-14, arch: arm64, deploymentTarget: 14.0}, ++ {os: windows-2025}, + ] + env: + COMPILER_CACHE_VERSION: 1 +diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml +index 617c6c0a..5f2f7fba 100644 +--- a/.github/workflows/build-ubuntu.yml ++++ b/.github/workflows/build-ubuntu.yml +@@ -23,6 +23,7 @@ jobs: + config: [ + { + os: ubuntu-24.04, ++ qtVersion: 6, + cmakeBuildType: RelWithDebInfo, + asanEnabled: false, + guiEnabled: true, +@@ -33,6 +34,7 @@ jobs: + }, + { + os: ubuntu-22.04, ++ qtVersion: 6, + cmakeBuildType: Release, + asanEnabled: false, + guiEnabled: true, +@@ -43,6 +45,7 @@ jobs: + }, + { + os: ubuntu-22.04, ++ qtVersion: 5, + cmakeBuildType: Release, + asanEnabled: false, + guiEnabled: false, +@@ -53,6 +56,7 @@ jobs: + }, + { + os: ubuntu-24.04, ++ qtVersion: 6, + cmakeBuildType: Release, + asanEnabled: true, + guiEnabled: false, +@@ -63,6 +67,7 @@ jobs: + }, + { + os: ubuntu-24.04, ++ qtVersion: 6, + cmakeBuildType: ClangTidy, + asanEnabled: false, + guiEnabled: false, +@@ -125,6 +130,12 @@ jobs: + + - name: Setup Ubuntu + run: | ++ if [ "${{ matrix.config.qtVersion }}" == "5" ]; then ++ qt_packages="qtbase5-dev libqt5opengl5-dev libcgal-qt5-dev" ++ elif [ "${{ matrix.config.qtVersion }}" == "6" ]; then ++ qt_packages="qt6-base-dev libqt6opengl6-dev libqt6openglwidgets6" ++ fi ++ + sudo apt-get update && sudo apt-get install -y \ + build-essential \ + cmake \ +@@ -134,31 +145,29 @@ jobs: + libboost-system-dev \ + libeigen3-dev \ + libceres-dev \ +- libfreeimage-dev \ ++ libopenimageio-dev \ ++ openimageio-tools \ + libmetis-dev \ + libgoogle-glog-dev \ + libgtest-dev \ + libgmock-dev \ + libsqlite3-dev \ + libglew-dev \ +- qtbase5-dev \ +- libqt5opengl5-dev \ ++ $qt_packages \ + libcgal-dev \ +- libcgal-qt5-dev \ + libgl1-mesa-dri \ + libunwind-dev \ + libcurl4-openssl-dev \ + libmkl-full-dev \ + xvfb + ++ # Fix issue in Ubuntu's openimageio CMake config. ++ # We don't depend on any of openimageio's OpenCV functionality, ++ # but it still requires the OpenCV include directory to exist. ++ sudo mkdir -p /usr/include/opencv4 ++ + if [ "${{ matrix.config.cudaEnabled }}" == "true" ]; then +- if [ "${{ matrix.config.os }}" == "ubuntu-20.04" ]; then +- sudo apt-get install -y \ +- nvidia-cuda-toolkit \ +- nvidia-cuda-toolkit-gcc +- echo "CC=/usr/bin/cuda-gcc" >> $GITHUB_ENV +- echo "CXX=/usr/bin/cuda-g++" >> $GITHUB_ENV +- elif [ "${{ matrix.config.os }}" == "ubuntu-22.04" ]; then ++ if [ "${{ matrix.config.os }}" == "ubuntu-22.04" ]; then + sudo apt-get install -y \ + nvidia-cuda-toolkit \ + nvidia-cuda-toolkit-gcc \ +diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml +index bab35e18..0d2433fe 100644 +--- a/.github/workflows/build-windows.yml ++++ b/.github/workflows/build-windows.yml +@@ -22,14 +22,14 @@ jobs: + matrix: + config: [ + { +- os: windows-2022, ++ os: windows-2025, + cmakeBuildType: Release, + cudaEnabled: true, + testsEnabled: true, + exportPackage: true, + }, + { +- os: windows-2022, ++ os: windows-2025, + cmakeBuildType: Release, + cudaEnabled: false, + testsEnabled: true, +@@ -42,7 +42,6 @@ jobs: + COMPILER_CACHE_DIR: ${{ github.workspace }}/compiler-cache + CCACHE_DIR: ${{ github.workspace }}/compiler-cache/ccache + CCACHE_BASEDIR: ${{ github.workspace }} +- VCPKG_COMMIT_ID: 0cb95c860ea83aafc1b24350510b30dec535989a + GLOG_v: 2 + GLOG_logtostderr: 1 + CUDA_MAJOR_VERSION: 12 +@@ -51,7 +50,7 @@ jobs: + + steps: + - uses: actions/checkout@v4 +- ++ + # We define the vcpkg binary sources using separate variables for read and + # write operations: + # * Read sources are defined as inline. These can be read by anyone and, +@@ -82,7 +81,7 @@ jobs: + key: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }}-${{ github.run_id }}-${{ github.run_number }} + restore-keys: v${{ env.COMPILER_CACHE_VERSION }}-${{ matrix.config.os }}-${{ matrix.config.cmakeBuildType }}-${{ matrix.config.asanEnabled }}--${{ matrix.config.cudaEnabled }} + path: ${{ env.COMPILER_CACHE_DIR }} +- ++ + - name: Install ccache + shell: pwsh + run: | +@@ -109,7 +108,6 @@ jobs: + cd ${{ github.workspace }} + git clone https://github.com/microsoft/vcpkg + cd vcpkg +- git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.bat + + - name: Install CMake and Ninja +@@ -166,7 +164,7 @@ jobs: + ../vcpkg/vcpkg.exe export --raw --output-dir vcpkg_export --output colmap + cp vcpkg_export/colmap/installed/x64-windows/bin/*.dll install/bin + cp vcpkg_export/colmap/installed/x64-windows-release/bin/*.dll install/bin +- cp -r vcpkg_export/colmap/installed/x64-windows/plugins install ++ cp -r vcpkg_export/colmap/installed/x64-windows/Qt6/plugins install + if ($${{ matrix.config.cudaEnabled }}) { + cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/cudart64_*.dll" install/bin + cp "${{ steps.cuda-toolkit.outputs.CUDA_PATH }}/bin/curand64_*.dll" install/bin +diff --git a/cmake/FindDependencies.cmake b/cmake/FindDependencies.cmake +index a0c3ff41..8833f115 100644 +--- a/cmake/FindDependencies.cmake ++++ b/cmake/FindDependencies.cmake +@@ -27,7 +27,7 @@ find_package(Boost ${COLMAP_FIND_TYPE} COMPONENTS + + find_package(Eigen3 ${COLMAP_FIND_TYPE}) + +-find_package(FreeImage ${COLMAP_FIND_TYPE}) ++find_package(OpenImageIO ${COLMAP_FIND_TYPE}) + + find_package(Metis ${COLMAP_FIND_TYPE}) + +@@ -88,7 +88,7 @@ endif() + if(DOWNLOAD_ENABLED) + # The OpenSSL package in vcpkg seems broken under Windows and leads to + # missing certificate verification when connecting to SSL servers. We +- # therefore use curl[schannel] (i.e., native Windows SSL/TLS) under Windows ++ # therefore use curl[sspi] (i.e., native Windows SSL/TLS) under Windows + # and curl[openssl] otherwise. + find_package(CURL QUIET) + set(CRYPTO_FOUND FALSE) +@@ -156,7 +156,7 @@ if(CUDA_ENABLED) + + declare_imported_cuda_target(cudart ${CUDA_LIBRARIES}) + declare_imported_cuda_target(curand ${CUDA_LIBRARIES}) +- ++ + set(CUDAToolkit_VERSION "${CUDA_VERSION_STRING}") + set(CUDAToolkit_BIN_DIR "${CUDA_TOOLKIT_ROOT_DIR}/bin") + else() +@@ -198,11 +198,19 @@ else() + endif() + + if(GUI_ENABLED) +- find_package(Qt5 5.4 ${COLMAP_FIND_TYPE} COMPONENTS Core OpenGL Widgets) ++ find_package(QT NAMES Qt5 Qt6 REQUIRED) ++ set(COLMAP_QT_COMPONENTS Core OpenGL Widgets) ++ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) ++ list(APPEND COLMAP_QT_COMPONENTS OpenGLWidgets) ++ endif() ++ find_package(Qt${QT_VERSION_MAJOR} ${COLMAP_FIND_TYPE} ${COLMAP_QT_COMPONENTS}) + message(STATUS "Found Qt") +- message(STATUS " Module : ${Qt5Core_DIR}") +- message(STATUS " Module : ${Qt5OpenGL_DIR}") +- message(STATUS " Module : ${Qt5Widgets_DIR}") ++ message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}Core_DIR}") ++ message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}OpenGL_DIR}") ++ message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}Widgets_DIR}") ++ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) ++ message(STATUS " Module : ${Qt${QT_VERSION_MAJOR}OpenGLWidgets_DIR}") ++ endif() + if(Qt5_FOUND) + # Qt5 was built with -reduce-relocations. + if(Qt5_POSITION_INDEPENDENT_CODE) +@@ -218,13 +226,15 @@ if(GUI_ENABLED) + endif() + endif() + endif() ++ endif() + ++ if(QT_FOUND) + # Enable automatic compilation of Qt resource files. + set(CMAKE_AUTORCC ON) + endif() + endif() + +-if(GUI_ENABLED AND Qt5_FOUND) ++if(GUI_ENABLED AND Qt${QT_VERSION_MAJOR}_FOUND) + list(APPEND COLMAP_COMPILE_DEFINITIONS COLMAP_GUI_ENABLED) + message(STATUS "Enabling GUI support") + else() +diff --git a/cmake/FindFreeImage.cmake b/cmake/FindFreeImage.cmake +deleted file mode 100644 +index cf213cf2..00000000 +--- a/cmake/FindFreeImage.cmake ++++ /dev/null +@@ -1,104 +0,0 @@ +-# Copyright (c), ETH Zurich and UNC Chapel Hill. +-# All rights reserved. +-# +-# Redistribution and use in source and binary forms, with or without +-# modification, are permitted provided that the following conditions are met: +-# +-# * Redistributions of source code must retain the above copyright +-# notice, this list of conditions and the following disclaimer. +-# +-# * Redistributions in binary form must reproduce the above copyright +-# notice, this list of conditions and the following disclaimer in the +-# documentation and/or other materials provided with the distribution. +-# +-# * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of +-# its contributors may be used to endorse or promote products derived +-# from this software without specific prior written permission. +-# +-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +-# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +-# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +-# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +-# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +-# CONTRACT, STRICT LIABILITY, OR TORT (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() +diff --git a/cmake/colmap-config.cmake.in b/cmake/colmap-config.cmake.in +index 250dfbc6..73ff530c 100644 +--- a/cmake/colmap-config.cmake.in ++++ b/cmake/colmap-config.cmake.in +@@ -43,9 +43,6 @@ set(COLMAP_FOUND FALSE) + + # Set hints for finding dependency packages. + +-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@) + +diff --git a/doc/cli.rst b/doc/cli.rst +index ce6c3068..24ed0861 100644 +--- a/doc/cli.rst ++++ b/doc/cli.rst +@@ -166,7 +166,7 @@ the available options, e.g.:: + Options can either be specified via command-line or by defining + them in a .ini project file passed to ``--project_path``. + +- -h [ --help ] ++ -h [ --help ] + --default_random_seed arg (=0) + --log_to_stderr arg (=1) + --log_level arg (=0) +@@ -187,10 +187,10 @@ the available options, e.g.:: + --ImageReader.default_focal_length_factor arg (=1.2) + --ImageReader.camera_mask_path arg + --FeatureExtraction.type arg (=SIFT) ++ --FeatureExtraction.max_image_size arg (=3200) + --FeatureExtraction.num_threads arg (=-1) + --FeatureExtraction.use_gpu arg (=1) + --FeatureExtraction.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) +diff --git a/doc/faq.rst b/doc/faq.rst +index 4611e63f..f73b05ed 100644 +--- a/doc/faq.rst ++++ b/doc/faq.rst +@@ -121,7 +121,7 @@ Example of images.txt:: + + 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). ++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. +@@ -217,9 +217,9 @@ 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 ++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 +@@ -232,7 +232,7 @@ 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 ++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 +@@ -344,7 +344,7 @@ 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 ++``--FeatureExtraction.max_image_size`` and/or setting + ``--SiftExtraction.first_octave 0`` or by manually limiting the number of + threads using ``--FeatureExtraction.num_threads``. + +diff --git a/doc/install.rst b/doc/install.rst +index 505e2516..1cc3c6cb 100755 +--- a/doc/install.rst ++++ b/doc/install.rst +@@ -81,19 +81,30 @@ Dependencies from the default Ubuntu repositories:: + libboost-graph-dev \ + libboost-system-dev \ + libeigen3-dev \ +- libfreeimage-dev \ ++ libopenimageio-dev \ ++ openimageio-tools \ + libmetis-dev \ + libgoogle-glog-dev \ + libgtest-dev \ + libgmock-dev \ + libsqlite3-dev \ + libglew-dev \ +- qtbase5-dev \ +- libqt5opengl5-dev \ ++ qt6-base-dev \ ++ libqt6opengl6-dev \ ++ libqt6openglwidgets6 \ + libcgal-dev \ + libceres-dev \ + libcurl4-openssl-dev \ ++ libssl-dev \ + libmkl-full-dev ++ # Fix issue in Ubuntu's openimageio CMake config. ++ # We don't depend on any of openimageio's OpenCV functionality, ++ # but it still requires the OpenCV include directory to exist. ++ sudo mkdir -p /usr/include/opencv4 ++ ++Alternatively, you can also build against Qt 5 instead of Qt 6 using:: ++ ++ qtbase5-dev libqt5opengl5-dev + + To compile with **CUDA support**, also install Ubuntu's default CUDA package:: + +@@ -121,11 +132,6 @@ Run COLMAP:: + colmap -h + colmap gui + +-Under **Ubuntu 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:: + +@@ -150,14 +156,14 @@ Dependencies from `Homebrew `__:: + ninja \ + boost \ + eigen \ +- freeimage \ ++ openimageio \ + curl \ + libomp \ + metis \ + glog \ + googletest \ + ceres-solver \ +- qt5 \ ++ qt \ + glew \ + cgal \ + sqlite3 +@@ -169,18 +175,15 @@ Configure and compile COLMAP:: + cd colmap + mkdir build + cd build +- cmake .. \ +- -GNinja \ +- -DQt5_DIR="$(brew --prefix qt@5)/lib/cmake/Qt5" ++ cmake -GNinja + ninja + sudo ninja install + +-If you have Qt 6 installed on your system as well, you might have to temporarily ++If you have Qt 5 installed on your system as well, you might have to temporarily + link your Qt 5 installation while configuring CMake:: + +- brew link qt5 +- cmake ... (from previous code block) +- brew unlink qt5 ++ brew unlink qt && brew link --force qt ++ cmake ... + + Run COLMAP:: + +@@ -329,7 +332,7 @@ with the source code ``hello_world.cc``:: + } + + Then compile and run your code as:: +- ++ + mkdir build + cd build + export colmap_DIR=${CMAKE_INSTALL_PREFIX}/share/colmap +diff --git a/doc/pycolmap/index.rst b/doc/pycolmap/index.rst +index f532102e..2048f78e 100644 +--- a/doc/pycolmap/index.rst ++++ b/doc/pycolmap/index.rst +@@ -27,7 +27,7 @@ from source, follow these steps: + * On Windows, after installing COLMAP via VCPKG, run in powershell:: + + python -m pip install . ` +- --cmake.define.CMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" ` ++ --cmake.define.CMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` + --cmake.define.VCPKG_TARGET_TRIPLET="x64-windows" + + Some features, such as cost functions, require that `PyCeres +diff --git a/doc/tutorial.rst b/doc/tutorial.rst +index 939b462e..70edc085 100755 +--- a/doc/tutorial.rst ++++ b/doc/tutorial.rst +@@ -163,14 +163,13 @@ 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`). ++directory, and it supports various different image formats by OpenImageIO. 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 +diff --git a/docker/Dockerfile b/docker/Dockerfile +index d1a339cc..1c62bccf 100644 +--- a/docker/Dockerfile ++++ b/docker/Dockerfile +@@ -24,20 +24,28 @@ RUN apt-get update && \ + libboost-graph-dev \ + libboost-system-dev \ + libeigen3-dev \ +- libfreeimage-dev \ ++ libopenimageio-dev \ ++ openimageio-tools \ + libmetis-dev \ + libgoogle-glog-dev \ + libgtest-dev \ + libgmock-dev \ + libsqlite3-dev \ + libglew-dev \ +- qtbase5-dev \ +- libqt5opengl5-dev \ ++ qt6-base-dev \ ++ libqt6opengl6-dev \ ++ libqt6openglwidgets6 \ + libcgal-dev \ + libceres-dev \ + libcurl4-openssl-dev \ ++ libssl-dev \ + libmkl-full-dev + ++# Fix issue in Ubuntu's openimageio CMake config. ++# We don't depend on any of openimageio's OpenCV functionality, ++# but it still requires the OpenCV include directory to exist. ++RUN mkdir -p /usr/include/opencv4 ++ + # Build and install COLMAP. + RUN git clone https://github.com/colmap/colmap.git + RUN cd colmap && \ +@@ -68,15 +76,17 @@ RUN apt-get update && \ + libopengl0 \ + libmetis5 \ + libceres4t64 \ +- libfreeimage3 \ ++ libopenimageio2.4t64 \ + libgcc-s1 \ + libgl1 \ + libglew2.2 \ + libgoogle-glog0v6t64 \ +- libqt5core5a \ +- libqt5gui5 \ +- libqt5widgets5 \ ++ libqt6core6 \ ++ libqt6gui6 \ ++ libqt6widgets6 \ ++ libqt6openglwidgets6 \ + libcurl4 \ ++ libssl3t64 \ + libmkl-locale \ + libmkl-intel-lp64 \ + libmkl-intel-thread \ +diff --git a/pyproject.toml b/pyproject.toml +index 0afede3b..9a2922d2 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -1,7 +1,7 @@ + [build-system] + requires = [ + "scikit-build-core>=0.3.3", +- "pybind11==3.0.0", ++ "pybind11==3.0.1", + "pybind11_stubgen @ git+https://github.com/sarlinpe/pybind11-stubgen@sarlinpe/fix-2025-08-20", + "numpy", + "ruff==0.12.7", +@@ -44,9 +44,6 @@ archs = ["auto64"] + test-requires = "pytest mypy==1.17.0 enlighten==1.13.0" + test-command = "python -c \"import pycolmap; print(pycolmap.__version__)\" && python -m mypy --package pycolmap --implicit-optional && pytest {project}/python/examples/custom_incremental_pipeline_test.py" + +-[tool.cibuildwheel.environment] +-VCPKG_COMMIT_ID = "0cb95c860ea83aafc1b24350510b30dec535989a" +- + [tool.cibuildwheel.linux] + before-all = "{project}/python/ci/install-colmap-almalinux.sh" + +diff --git a/python/README.md b/python/README.md +index e0dfda95..6a889454 100644 +--- a/python/README.md ++++ b/python/README.md +@@ -28,7 +28,7 @@ python -m pip install . + - On Windows, after installing COLMAP [via VCPKG](https://colmap.github.io/install.html#id3), run in powershell: + ```powershell + python -m pip install . ` +- --cmake.define.CMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" ` ++ --cmake.define.CMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` + --cmake.define.VCPKG_TARGET_TRIPLET="x64-windows" + ``` + +diff --git a/python/ci/install-colmap-almalinux.sh b/python/ci/install-colmap-almalinux.sh +index a93393b8..4adfb7e0 100755 +--- a/python/ci/install-colmap-almalinux.sh ++++ b/python/ci/install-colmap-almalinux.sh +@@ -38,7 +38,6 @@ export PATH="${COMPILER_TOOLS_DIR}:${PATH}" + # Setup vcpkg + git clone https://github.com/microsoft/vcpkg ${VCPKG_INSTALLATION_ROOT} + cd ${VCPKG_INSTALLATION_ROOT} +-git checkout ${VCPKG_COMMIT_ID} + ./bootstrap-vcpkg.sh + ./vcpkg integrate install + +diff --git a/python/ci/install-colmap-macos.sh b/python/ci/install-colmap-macos.sh +index 7c403ce3..c688f2ba 100755 +--- a/python/ci/install-colmap-macos.sh ++++ b/python/ci/install-colmap-macos.sh +@@ -5,7 +5,7 @@ CURRDIR=$(pwd) + # Fix `brew link` error. + find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete + +-brew update ++brew uninstall cmake # Workaround for CI failures. + brew install git cmake ninja gfortran ccache libomp + brew link --force libomp + +@@ -17,7 +17,6 @@ ln -sf $(which gfortran-14) "$(dirname $(which gfortran-14))/gfortran" + # Setup vcpkg + git clone https://github.com/microsoft/vcpkg ${VCPKG_INSTALLATION_ROOT} + cd ${VCPKG_INSTALLATION_ROOT} +-git checkout ${VCPKG_COMMIT_ID} + ./bootstrap-vcpkg.sh + ./vcpkg integrate install + +diff --git a/python/ci/install-colmap-windows.ps1 b/python/ci/install-colmap-windows.ps1 +index 7ccb763b..5653d177 100755 +--- a/python/ci/install-colmap-windows.ps1 ++++ b/python/ci/install-colmap-windows.ps1 +@@ -12,12 +12,11 @@ If (!(Test-Path -path "${COMPILER_TOOLS_DIR}/ccache.exe" -PathType Leaf)) { + cd ${CURRDIR} + git clone https://github.com/microsoft/vcpkg ${env:VCPKG_INSTALLATION_ROOT} + cd ${env:VCPKG_INSTALLATION_ROOT} +-git checkout "${env:VCPKG_COMMIT_ID}" + ./bootstrap-vcpkg.bat + + cd ${CURRDIR} + & "./scripts/shell/enter_vs_dev_shell.ps1" +-& "${env:VCPKG_INSTALLATION_ROOT}/vcpkg.exe" integrate install ++& "${env:VCPKG_ROOT}/vcpkg.exe" integrate install + + # Build COLMAP + mkdir build +diff --git a/python/ci/test-colmap-windows.ps1 b/python/ci/test-colmap-windows.ps1 +index bf1bfd67..3cfc6eed 100644 +--- a/python/ci/test-colmap-windows.ps1 ++++ b/python/ci/test-colmap-windows.ps1 +@@ -1,4 +1,4 @@ +-& "./scripts/shell/enter_vs_dev_shell.ps1" +-& "${env:VCPKG_INSTALLATION_ROOT}/vcpkg.exe" integrate install ++& "$PSScriptRoot/../../scripts/shell/enter_vs_dev_shell.ps1" ++& "${env:VCPKG_ROOT}/vcpkg.exe" integrate install + + & python -c "import pycolmap; print(pycolmap.__version__)" +diff --git a/python/examples/custom_bundle_adjustment.py b/python/examples/custom_bundle_adjustment.py +index 5e5aaed6..a43935b5 100644 +--- a/python/examples/custom_bundle_adjustment.py ++++ b/python/examples/custom_bundle_adjustment.py +@@ -4,6 +4,7 @@ C++ with equivalent logic. As a result, one can add customized residuals on top + of the exposed ceres problem from conventional bundle adjustment. + """ + ++import collections + import copy + + import pycolmap +@@ -60,6 +61,10 @@ def adjust_global_bundle(mapper, mapper_options, ba_options): + if frame_id in mapper.existing_frame_ids: + ba_config.set_constant_rig_from_world_pose(frame_id) + ++ for rig_id in mapper_options.constant_rigs: ++ for sensor_id in reconstruction.rig(rig_id).sensors: ++ ba_config.set_constant_sensor_from_rig_pose(sensor_id) ++ + for camera_id in mapper_options.constant_cameras: + ba_config.set_constant_cam_intrinsics(camera_id) + +@@ -145,24 +150,22 @@ def adjust_local_bundle( + ba_config.set_constant_rig_from_world_pose(frame_id) + + # Fix rig poses, if not all frames within the local bundle. +- num_frames_per_rig = {} ++ num_frames_per_rig = collections.defaultdict(int) + for frame_id in frame_ids: + frame = reconstruction.frame(frame_id) +- if frame.rig_id not in num_frames_per_rig: +- num_frames_per_rig[frame.rig_id] = 0 + num_frames_per_rig[frame.rig_id] += 1 + for rig_id, num_frames_local in num_frames_per_rig.items(): +- if num_frames_local < mapper.num_reg_frames_per_rig[rig_id]: +- rig = reconstruction.rig(rig_id) +- for sensor_id, _ in rig.sensors.items(): ++ if ( ++ rig_id in mapper_options.constant_rigs ++ or num_frames_local < mapper.num_reg_frames_per_rig[rig_id] ++ ): ++ for sensor_id in reconstruction.rig(rig_id).sensors: + ba_config.set_constant_sensor_from_rig_pose(sensor_id) + + # Fix camera intrinsics, if not all images within local bundle. +- num_images_per_camera = {} ++ num_images_per_camera = collections.defaultdict(int) + for image_id in ba_config.images: + image = reconstruction.images[image_id] +- if image.camera_id not in num_images_per_camera: +- num_images_per_camera[image.camera_id] = 0 + num_images_per_camera[image.camera_id] += 1 + for camera_id, num_images_local in num_images_per_camera.items(): + if ( +diff --git a/scripts/shell/build_mac_app.sh b/scripts/shell/build_mac_app.sh +index 499f5c59..ff01b88b 100755 +--- a/scripts/shell/build_mac_app.sh ++++ b/scripts/shell/build_mac_app.sh +@@ -66,7 +66,11 @@ install_name_tool -change @rpath/libtbb.dylib /usr/local/lib/libtbb.dylib $BASE_ + 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" ++if [ -d "$(brew --prefix)/opt/qt6" ]; then ++ $(brew --prefix)/opt/qt6/bin/macdeployqt "$BASE_PATH/COLMAP.app" ++else ++ $(brew --prefix)/opt/qt5/bin/macdeployqt "$BASE_PATH/COLMAP.app" ++fi + + echo "Wrapping binary" + cat <"$BASE_PATH/COLMAP.app/Contents/MacOS/colmap_gui.sh" +diff --git a/src/colmap/controllers/automatic_reconstruction.cc b/src/colmap/controllers/automatic_reconstruction.cc +index dac39310..ea0eb484 100644 +--- a/src/colmap/controllers/automatic_reconstruction.cc ++++ b/src/colmap/controllers/automatic_reconstruction.cc +@@ -96,6 +96,7 @@ AutomaticReconstructionController::AutomaticReconstructionController( + + ImageReaderOptions& reader_options = *option_manager_.image_reader; + reader_options.image_path = *option_manager_.image_path; ++ reader_options.as_rgb = option_manager_.feature_extraction->RequiresRGB(); + if (!options_.mask_path.empty()) { + reader_options.mask_path = options_.mask_path; + option_manager_.image_reader->mask_path = options_.mask_path; +diff --git a/src/colmap/controllers/feature_extraction.cc b/src/colmap/controllers/feature_extraction.cc +index dc75e29d..6b613a05 100644 +--- a/src/colmap/controllers/feature_extraction.cc ++++ b/src/colmap/controllers/feature_extraction.cc +@@ -204,7 +204,7 @@ class FeatureExtractorThread : public Thread { + &image_data.keypoints, + &image_data.descriptors); + } +- if (image_data.mask.Data()) { ++ if (!image_data.mask.IsEmpty()) { + MaskKeypoints(image_data.mask, + &image_data.keypoints, + &image_data.descriptors); +@@ -214,7 +214,9 @@ class FeatureExtractorThread : public Thread { + } + } + +- image_data.bitmap.Deallocate(); ++ // Release the memory, since it is not used afterwards. ++ image_data.bitmap = Bitmap(); ++ image_data.mask = Bitmap(); + + output_queue_->Push(std::move(image_data)); + } else { +@@ -281,7 +283,7 @@ class FeatureWriterThread : public Thread { + image_data.camera.has_prior_focal_length ? " (Prior)" : ""); + LOG(INFO) << " Features: " << image_data.keypoints.size() + << " (" << extractor_type_str_ << ")"; +- if (image_data.mask.Data()) { ++ if (!image_data.mask.IsEmpty()) { + LOG(INFO) << " Mask: Yes"; + } + +@@ -367,11 +369,12 @@ class FeatureExtractorController : public Thread { + extractor_queue_ = std::make_unique>(kQueueSize); + writer_queue_ = std::make_unique>(kQueueSize); + +- const int max_image_size = extraction_options_.MaxImageSize(); +- if (max_image_size > 0) { ++ if (extraction_options_.max_image_size > 0) { + for (int i = 0; i < num_threads; ++i) { + resizers_.emplace_back(std::make_unique( +- max_image_size, resizer_queue_.get(), extractor_queue_.get())); ++ extraction_options_.max_image_size, ++ resizer_queue_.get(), ++ extractor_queue_.get())); + } + } + +@@ -403,7 +406,8 @@ class FeatureExtractorController : public Thread { + } else { + const static FeatureExtractionOptions kDefaultExtractionOptions; + if (extraction_options_.num_threads == -1 && +- max_image_size == kDefaultExtractionOptions.MaxImageSize() && ++ extraction_options_.max_image_size == ++ kDefaultExtractionOptions.max_image_size && + extraction_options_.sift->first_octave == + kDefaultExtractionOptions.sift->first_octave) { + LOG(WARNING) +@@ -455,7 +459,7 @@ class FeatureExtractorController : public Thread { + } + } + +- const bool should_resize = extraction_options_.MaxImageSize() > 0; ++ const bool should_resize = extraction_options_.max_image_size > 0; + + while (image_reader_.NextIndex() < image_reader_.NumImages()) { + if (IsStopped()) { +@@ -475,7 +479,9 @@ class FeatureExtractorController : public Thread { + &image_data.mask); + + if (image_data.status != ImageReader::Status::SUCCESS) { +- image_data.bitmap.Deallocate(); ++ // Release the memory, since it is not used afterwards. ++ image_data.bitmap = Bitmap(); ++ image_data.mask = Bitmap(); + } + + if (should_resize) { +diff --git a/src/colmap/controllers/image_reader.cc b/src/colmap/controllers/image_reader.cc +index ae38ac93..0c6a3c12 100644 +--- a/src/colmap/controllers/image_reader.cc ++++ b/src/colmap/controllers/image_reader.cc +@@ -135,7 +135,7 @@ ImageReader::Status ImageReader::Next(Rig* rig, + // Read image. + ////////////////////////////////////////////////////////////////////////////// + +- if (!bitmap->Read(image_path, false)) { ++ if (!bitmap->Read(image_path, /*as_rgb=*/options_.as_rgb)) { + return Status::BITMAP_ERROR; + } + +diff --git a/src/colmap/controllers/image_reader.h b/src/colmap/controllers/image_reader.h +index 9527fb40..ebd04313 100644 +--- a/src/colmap/controllers/image_reader.h ++++ b/src/colmap/controllers/image_reader.h +@@ -82,6 +82,9 @@ struct ImageReaderOptions { + // value `default_focal_length_factor * max(width, height)`. + double default_focal_length_factor = 1.2; + ++ // Whether to read images as grayscale or RGB. ++ bool as_rgb = false; ++ + bool Check() const; + }; + +diff --git a/src/colmap/controllers/image_reader_test.cc b/src/colmap/controllers/image_reader_test.cc +index 5ac7c083..6bace906 100644 +--- a/src/colmap/controllers/image_reader_test.cc ++++ b/src/colmap/controllers/image_reader_test.cc +@@ -40,9 +40,8 @@ + namespace colmap { + namespace { + +-Bitmap CreateTestBitmap() { +- Bitmap bitmap; +- bitmap.Allocate(1, 3, false); ++Bitmap CreateTestBitmap(bool as_rgb) { ++ Bitmap bitmap(1, 3, as_rgb); + bitmap.SetPixel(0, 0, BitmapColor(1)); + bitmap.SetPixel(1, 0, BitmapColor(2)); + bitmap.SetPixel(2, 0, BitmapColor(3)); +@@ -50,25 +49,26 @@ Bitmap CreateTestBitmap() { + } + + class ParameterizedImageReaderTests +- : public ::testing::TestWithParam< +- std::tuple> {}; ++ : public ::testing::TestWithParam> {}; + + TEST_P(ParameterizedImageReaderTests, Nominal) { +- const auto [kNumImages, kWithMasks, kWithExistingImages] = GetParam(); ++ const auto [kNumImages, kWithMasks, kWithExistingImages, kAsRGB] = GetParam(); + + auto database = Database::Open(kInMemorySqliteDatabasePath); + + const std::string test_dir = CreateTestDir(); + ImageReaderOptions options; + options.image_path = test_dir + "/images"; ++ options.as_rgb = kAsRGB; + CreateDirIfNotExists(options.image_path); + if (kWithMasks) { + options.mask_path = test_dir + "/masks"; + CreateDirIfNotExists(options.mask_path); + } +- const Bitmap test_bitmap = CreateTestBitmap(); ++ const Bitmap test_bitmap = CreateTestBitmap(kAsRGB); + for (int i = 0; i < kNumImages; ++i) { + const std::string image_name = std::to_string(i) + ".png"; + test_bitmap.Write(options.image_path + "/" + image_name); +@@ -117,8 +117,8 @@ TEST_P(ParameterizedImageReaderTests, Nominal) { + EXPECT_EQ(camera.width, test_bitmap.Width()); + EXPECT_EQ(camera.height, test_bitmap.Height()); + EXPECT_EQ(image.Name(), std::to_string(i) + ".png"); +- EXPECT_EQ(bitmap.ConvertToRowMajorArray(), +- test_bitmap.ConvertToRowMajorArray()); ++ EXPECT_EQ(bitmap.IsRGB(), kAsRGB); ++ EXPECT_EQ(bitmap.RowMajorData(), test_bitmap.RowMajorData()); + if (kWithExistingImages) { + EXPECT_EQ(database->NumRigs(), kNumImages); + EXPECT_EQ(database->NumCameras(), kNumImages); +@@ -135,12 +135,29 @@ TEST_P(ParameterizedImageReaderTests, Nominal) { + EXPECT_EQ(database->NumCameras(), kNumImages); + } + +-INSTANTIATE_TEST_SUITE_P(ImageReaderTests, +- ParameterizedImageReaderTests, +- ::testing::Values(std::make_tuple(0, false, true), +- std::make_tuple(5, false, false), +- std::make_tuple(5, true, false), +- std::make_tuple(5, false, true))); ++INSTANTIATE_TEST_SUITE_P( ++ ImageReaderTests, ++ ParameterizedImageReaderTests, ++ ::testing::Values(std::make_tuple(/*num_images=*/0, ++ /*with_masks=*/false, ++ /*with_existing_images=*/true, ++ /*as_rgb=*/true), ++ std::make_tuple(/*num_images=*/5, ++ /*with_masks=*/false, ++ /*with_existing_images=*/false, ++ /*as_rgb=*/true), ++ std::make_tuple(/*num_images=*/5, ++ /*with_masks=*/true, ++ /*with_existing_images=*/false, ++ /*as_rgb=*/true), ++ std::make_tuple(/*num_images=*/5, ++ /*with_masks=*/true, ++ /*with_existing_images=*/false, ++ /*as_rgb=*/false), ++ std::make_tuple(/*num_images=*/5, ++ /*with_masks=*/false, ++ /*with_existing_images=*/true, ++ /*as_rgb=*/true))); + + } // namespace + } // namespace colmap +diff --git a/src/colmap/controllers/incremental_pipeline.cc b/src/colmap/controllers/incremental_pipeline.cc +index 348bd053..13654b55 100644 +--- a/src/colmap/controllers/incremental_pipeline.cc ++++ b/src/colmap/controllers/incremental_pipeline.cc +@@ -87,6 +87,7 @@ IncrementalMapper::Options IncrementalPipelineOptions::Mapper() const { + options.num_threads = num_threads; + options.local_ba_num_images = ba_local_num_images; + options.fix_existing_frames = fix_existing_frames; ++ options.constant_rigs = constant_rigs; + options.constant_cameras = constant_cameras; + options.use_prior_position = use_prior_position; + options.use_robust_loss_on_prior_position = use_robust_loss_on_prior_position; +diff --git a/src/colmap/controllers/incremental_pipeline.h b/src/colmap/controllers/incremental_pipeline.h +index bdc52e0e..9dd5d742 100644 +--- a/src/colmap/controllers/incremental_pipeline.h ++++ b/src/colmap/controllers/incremental_pipeline.h +@@ -153,6 +153,10 @@ struct IncrementalPipelineOptions { + // If reconstruction is provided as input, fix the existing frame poses. + bool fix_existing_frames = false; + ++ // List of rigs for which to fix the sensor_from_rig transformation, ++ // independent of ba_refine_sensor_from_rig. ++ std::unordered_set constant_rigs; ++ + // List of cameras for which to fix the camera parameters independent + // of refine_focal_length, refine_principal_point, and refine_extra_params. + std::unordered_set constant_cameras; +diff --git a/src/colmap/controllers/incremental_pipeline_test.cc b/src/colmap/controllers/incremental_pipeline_test.cc +index 6e8b2037..a17a1589 100644 +--- a/src/colmap/controllers/incremental_pipeline_test.cc ++++ b/src/colmap/controllers/incremental_pipeline_test.cc +@@ -30,6 +30,7 @@ + #include "colmap/controllers/incremental_pipeline.h" + + #include "colmap/estimators/alignment.h" ++#include "colmap/geometry/rigid3_matchers.h" + #include "colmap/scene/database.h" + #include "colmap/scene/synthetic.h" + #include "colmap/util/testing.h" +@@ -84,6 +85,15 @@ bool AreReconstructionsIdentical(const Reconstruction& gt, + return false; + } + ++ for (const auto& [camera_id, camera] : gt.Cameras()) { ++ if (!computed.ExistsCamera(camera_id)) { ++ return false; ++ } ++ if (camera.params != computed.Camera(camera_id).params) { ++ return false; ++ } ++ } ++ + for (const auto& [image_id, image] : computed.Images()) { + if (!gt.ExistsImage(image_id)) { + return false; +@@ -106,16 +116,6 @@ bool AreReconstructionsIdentical(const Reconstruction& gt, + return true; + } + +-void ExpectReconstructionsIdentical(const Reconstruction& gt, +- const Reconstruction& computed) { +- EXPECT_TRUE(AreReconstructionsIdentical(gt, computed)); +-} +- +-void ExpectReconstructionsDifferent(const Reconstruction& gt, +- const Reconstruction& computed) { +- EXPECT_FALSE(AreReconstructionsIdentical(gt, computed)); +-} +- + TEST(IncrementalPipeline, WithoutNoise) { + const std::string database_path = CreateTestDir() + "/database.db"; + +@@ -185,6 +185,59 @@ TEST(IncrementalPipeline, WithoutNoiseAndWithNonTrivialFrames) { + } + } + ++TEST(IncrementalPipeline, WithNonTrivialFramesAndConstantRigsAndCameras) { ++ const std::string database_path = CreateTestDir() + "/database.db"; ++ ++ auto database = Database::Open(database_path); ++ Reconstruction gt_reconstruction; ++ SyntheticDatasetOptions synthetic_dataset_options; ++ synthetic_dataset_options.num_rigs = 2; ++ synthetic_dataset_options.num_cameras_per_rig = 2; ++ synthetic_dataset_options.num_frames_per_rig = 7; ++ synthetic_dataset_options.num_points3D = 100; ++ synthetic_dataset_options.point2D_stddev = 0; ++ synthetic_dataset_options.camera_has_prior_focal_length = false; ++ synthetic_dataset_options.sensor_from_rig_translation_stddev = 0.05; ++ synthetic_dataset_options.sensor_from_rig_rotation_stddev = 30; ++ SynthesizeDataset( ++ synthetic_dataset_options, >_reconstruction, database.get()); ++ ++ constexpr int kConstantRigId = 1; ++ constexpr int kConstantCameraId = 1; ++ ++ auto reconstruction_manager = std::make_shared(); ++ auto options = std::make_shared(); ++ options->constant_rigs.insert(kConstantRigId); ++ options->constant_cameras.insert(kConstantCameraId); ++ IncrementalPipeline mapper(options, ++ /*image_path=*/"", ++ database_path, ++ reconstruction_manager); ++ mapper.Run(); ++ ++ ASSERT_EQ(reconstruction_manager->Size(), 1); ++ auto& reconstruction = *reconstruction_manager->Get(0); ++ ExpectReconstructionsNear(gt_reconstruction, ++ reconstruction, ++ /*max_rotation_error_deg=*/1e-2, ++ /*max_proj_center_error=*/1e-3, ++ /*num_obs_tolerance=*/0, ++ /*align=*/true, ++ /*check_scale=*/true); ++ ++ for (const auto& [sensor_id, sensor_from_rig] : ++ reconstruction.Rig(kConstantRigId).Sensors()) { ++ EXPECT_THAT( ++ sensor_from_rig.value(), ++ Rigid3dNear( ++ gt_reconstruction.Rig(kConstantRigId).SensorFromRig(sensor_id), ++ /*rtol=*/1e-6, ++ /*ttol=*/1e-6)); ++ } ++ EXPECT_EQ(reconstruction.Camera(kConstantCameraId).params, ++ gt_reconstruction.Camera(kConstantCameraId).params); ++} ++ + TEST(IncrementalPipeline, WithoutNoiseAndWithPanoramicNonTrivialFrames) { + const std::string database_path = CreateTestDir() + "/database.db"; + +@@ -570,6 +623,8 @@ TEST(IncrementalPipeline, GPSPriorBasedSfMWithNoise) { + } + + TEST(IncrementalPipeline, SfMWithRandomSeedStability) { ++ SetPRNGSeed(1); ++ + const std::string database_path = CreateTestDir() + "/database.db"; + + auto database = Database::Open(database_path); +@@ -577,9 +632,9 @@ TEST(IncrementalPipeline, SfMWithRandomSeedStability) { + SyntheticDatasetOptions synthetic_dataset_options; + synthetic_dataset_options.num_rigs = 2; + synthetic_dataset_options.num_cameras_per_rig = 1; +- synthetic_dataset_options.num_frames_per_rig = 7; ++ synthetic_dataset_options.num_frames_per_rig = 5; + synthetic_dataset_options.num_points3D = 100; +- synthetic_dataset_options.point2D_stddev = 2.5; ++ synthetic_dataset_options.point2D_stddev = 1; + synthetic_dataset_options.use_prior_position = false; + SynthesizeDataset( + synthetic_dataset_options, >_reconstruction, database.get()); +@@ -600,48 +655,53 @@ TEST(IncrementalPipeline, SfMWithRandomSeedStability) { + return reconstruction_manager; + }; + +- // Single-thread execution ++ // Single-threaded execution. + { ++ constexpr int kRandomSeed = 42; + auto reconstruction_manager0 = +- run_mapper(/*num_threads=*/1, /*random_seed=*/42); ++ run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); + auto reconstruction_manager1 = +- run_mapper(/*num_threads=*/1, /*random_seed=*/42); +- // Same seed should produce identical reconstructions in single-thread mode +- +- ExpectReconstructionsIdentical(*reconstruction_manager0->Get(0), +- *reconstruction_manager1->Get(0)); +- +- // Different seed should produce different reconstructions +- auto reconstruction_manager2 = +- run_mapper(/*num_threads=*/1, /*random_seed=*/123); +- ExpectReconstructionsDifferent(*reconstruction_manager0->Get(0), +- *reconstruction_manager2->Get(0)); ++ run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); ++ EXPECT_TRUE(AreReconstructionsIdentical(*reconstruction_manager0->Get(0), ++ *reconstruction_manager1->Get(0))); ++ ++ // Different seed should produce different reconstructions. Notice that, for ++ // some seeds, we may still get identical results, so we try a few different ++ // seeds until we get a different result. ++ bool different_result = false; ++ for (int random_seed = kRandomSeed + 1; random_seed < kRandomSeed + 10; ++ ++random_seed) { ++ auto reconstruction_manager2 = ++ run_mapper(/*num_threads=*/1, /*random_seed=*/random_seed); ++ if (!AreReconstructionsIdentical(*reconstruction_manager0->Get(0), ++ *reconstruction_manager2->Get(0))) { ++ different_result = true; ++ break; ++ } ++ } ++ EXPECT_TRUE(different_result); + } + +- // Multi-thread execution ++ // Multi-threaded execution. + { + auto reconstruction_manager0 = + run_mapper(/*num_threads=*/-1, /*random_seed=*/42); + auto reconstruction_manager1 = + run_mapper(/*num_threads=*/-1, /*random_seed=*/42); + // Same seed should produce similar results, up to floating-point variations +- // in optimization ++ // in optimization. + ExpectReconstructionsNear(*reconstruction_manager0->Get(0), + *reconstruction_manager1->Get(0), +- /*max_rotation_error_deg=*/1e-14, +- /*max_proj_center_error=*/1e-14, ++ /*max_rotation_error_deg=*/1e-10, ++ /*max_proj_center_error=*/1e-10, + /*num_obs_tolerance=*/0.01, + /*align=*/false); +- +- auto reconstruction_manager2 = +- run_mapper(/*num_threads=*/-1, /*random_seed=*/123); +- // Different seed may produce different reconstructions +- ExpectReconstructionsDifferent(*reconstruction_manager0->Get(0), +- *reconstruction_manager2->Get(0)); + } + } + + TEST(IncrementalPipeline, PriorBasedSfMWithRandomSeedStability) { ++ SetPRNGSeed(1); ++ + const std::string database_path = CreateTestDir() + "/database.db"; + + auto database = Database::Open(database_path); +@@ -651,11 +711,11 @@ TEST(IncrementalPipeline, PriorBasedSfMWithRandomSeedStability) { + synthetic_dataset_options.num_cameras_per_rig = 1; + synthetic_dataset_options.num_frames_per_rig = 7; + synthetic_dataset_options.num_points3D = 100; +- synthetic_dataset_options.point2D_stddev = 2.5; ++ synthetic_dataset_options.point2D_stddev = 1; + synthetic_dataset_options.use_prior_position = true; + SynthesizeDataset( + synthetic_dataset_options, >_reconstruction, database.get()); +- synthetic_dataset_options.prior_position_stddev = 2.0; ++ synthetic_dataset_options.prior_position_stddev = 1.0; + + auto mapper_options = std::make_shared(); + mapper_options->use_prior_position = false; +@@ -673,43 +733,48 @@ TEST(IncrementalPipeline, PriorBasedSfMWithRandomSeedStability) { + return reconstruction_manager; + }; + +- // Single-thread execution ++ // Single-threaded execution. + { ++ constexpr int kRandomSeed = 42; + auto reconstruction_manager0 = +- run_mapper(/*num_threads=*/1, /*random_seed=*/42); ++ run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); + auto reconstruction_manager1 = +- run_mapper(/*num_threads=*/1, /*random_seed=*/42); +- // Same seed should produce identical reconstructions in single-thread mode +- ExpectReconstructionsIdentical(*reconstruction_manager0->Get(0), +- *reconstruction_manager1->Get(0)); +- +- // Different seed should produce different reconstructions +- auto reconstruction_manager2 = +- run_mapper(/*num_threads=*/1, /*random_seed=*/123); +- ExpectReconstructionsDifferent(*reconstruction_manager0->Get(0), +- *reconstruction_manager2->Get(0)); ++ run_mapper(/*num_threads=*/1, /*random_seed=*/kRandomSeed); ++ EXPECT_TRUE(AreReconstructionsIdentical(*reconstruction_manager0->Get(0), ++ *reconstruction_manager1->Get(0))); ++ ++ // Different seed should produce different reconstructions. Notice that, for ++ // some seeds, we may still get identical results, so we try a few different ++ // seeds until we get a different result. ++ bool different_result = false; ++ for (int random_seed = kRandomSeed + 1; random_seed < kRandomSeed + 10; ++ ++random_seed) { ++ // Different seed should produce different reconstructions. ++ auto reconstruction_manager2 = ++ run_mapper(/*num_threads=*/1, /*random_seed=*/random_seed); ++ if (!AreReconstructionsIdentical(*reconstruction_manager0->Get(0), ++ *reconstruction_manager2->Get(0))) { ++ different_result = true; ++ break; ++ } ++ } ++ EXPECT_TRUE(different_result); + } + +- // Multi-thread execution ++ // Multi-threaded execution. + { + auto reconstruction_manager0 = + run_mapper(/*num_threads=*/-1, /*random_seed=*/42); + auto reconstruction_manager1 = + run_mapper(/*num_threads=*/-1, /*random_seed=*/42); + // Same seed should produce similar results, up to floating-point variations +- // in optimization ++ // in optimization. + ExpectReconstructionsNear(*reconstruction_manager0->Get(0), + *reconstruction_manager1->Get(0), +- /*max_rotation_error_deg=*/1e-13, +- /*max_proj_center_error=*/1e-13, ++ /*max_rotation_error_deg=*/1e-10, ++ /*max_proj_center_error=*/1e-10, + /*num_obs_tolerance=*/0.01, + /*align=*/false); +- +- auto reconstruction_manager2 = +- run_mapper(/*num_threads=*/-1, /*random_seed=*/123); +- // Different seed may produce different reconstructions +- ExpectReconstructionsDifferent(*reconstruction_manager0->Get(0), +- *reconstruction_manager2->Get(0)); + } + } + +diff --git a/src/colmap/controllers/option_manager.cc b/src/colmap/controllers/option_manager.cc +index 06a5498b..14c421d4 100644 +--- a/src/colmap/controllers/option_manager.cc ++++ b/src/colmap/controllers/option_manager.cc +@@ -110,7 +110,7 @@ void OptionManager::ModifyForInternetData() { + } + + void OptionManager::ModifyForLowQuality() { +- feature_extraction->sift->max_image_size = 1000; ++ feature_extraction->max_image_size = 1000; + feature_extraction->sift->max_num_features = 2048; + sequential_pairing->loop_detection_num_images /= 2; + vocab_tree_pairing->max_num_features = 256; +@@ -131,7 +131,7 @@ void OptionManager::ModifyForLowQuality() { + } + + void OptionManager::ModifyForMediumQuality() { +- feature_extraction->sift->max_image_size = 1600; ++ feature_extraction->max_image_size = 1600; + feature_extraction->sift->max_num_features = 4096; + sequential_pairing->loop_detection_num_images /= 1.5; + vocab_tree_pairing->max_num_features = 1024; +@@ -153,7 +153,7 @@ void OptionManager::ModifyForMediumQuality() { + + void OptionManager::ModifyForHighQuality() { + feature_extraction->sift->estimate_affine_shape = true; +- feature_extraction->sift->max_image_size = 2400; ++ feature_extraction->max_image_size = 2400; + feature_extraction->sift->max_num_features = 8192; + feature_matching->guided_matching = true; + vocab_tree_pairing->max_num_features = 4096; +@@ -269,7 +269,7 @@ void OptionManager::AddFeatureExtractionOptions() { + &feature_extraction->gpu_index); + + AddAndRegisterDefaultOption("SiftExtraction.max_image_size", +- &feature_extraction->sift->max_image_size); ++ &feature_extraction->max_image_size); + AddAndRegisterDefaultOption("SiftExtraction.max_num_features", + &feature_extraction->sift->max_num_features); + AddAndRegisterDefaultOption("SiftExtraction.first_octave", +@@ -645,6 +645,8 @@ void OptionManager::AddMapperOptions() { + &mapper->mapper.local_ba_min_tri_angle); + + AddDefaultOption("Mapper.image_list_path", &mapper_image_list_path_); ++ AddDefaultOption("Mapper.constant_rig_list_path", ++ &mapper_constant_rig_list_path_); + AddDefaultOption("Mapper.constant_camera_list_path", + &mapper_constant_camera_list_path_); + +@@ -952,6 +954,12 @@ void OptionManager::Parse(const int argc, char** argv) { + if (!mapper_image_list_path_.empty()) { + mapper->image_names = ReadTextFileLines(mapper_image_list_path_); + } ++ if (!mapper_constant_rig_list_path_.empty()) { ++ for (const std::string& line : ++ ReadTextFileLines(mapper_constant_rig_list_path_)) { ++ mapper->constant_rigs.insert(std::stoi(line)); ++ } ++ } + if (!mapper_constant_camera_list_path_.empty()) { + for (const std::string& line : + ReadTextFileLines(mapper_constant_camera_list_path_)) { +diff --git a/src/colmap/controllers/option_manager.h b/src/colmap/controllers/option_manager.h +index 12bfe68a..3d5bffc6 100644 +--- a/src/colmap/controllers/option_manager.h ++++ b/src/colmap/controllers/option_manager.h +@@ -174,6 +174,7 @@ class OptionManager { + std::string feature_matching_type_; + + std::string mapper_image_list_path_; ++ std::string mapper_constant_rig_list_path_; + std::string mapper_constant_camera_list_path_; + + bool added_log_options_; +diff --git a/src/colmap/exe/feature.cc b/src/colmap/exe/feature.cc +index 59e2d8fa..122c048a 100644 +--- a/src/colmap/exe/feature.cc ++++ b/src/colmap/exe/feature.cc +@@ -106,6 +106,7 @@ int RunFeatureExtractor(int argc, char** argv) { + + ImageReaderOptions reader_options = *options.image_reader; + reader_options.image_path = *options.image_path; ++ reader_options.as_rgb = options.feature_extraction->RequiresRGB(); + + if (camera_mode >= 0) { + UpdateImageReaderOptionsFromCameraMode(reader_options, +diff --git a/src/colmap/exe/gui.cc b/src/colmap/exe/gui.cc +index 040f4427..19a35f02 100644 +--- a/src/colmap/exe/gui.cc ++++ b/src/colmap/exe/gui.cc +@@ -53,13 +53,15 @@ int RunGraphicalUserInterface(int argc, char** argv) { + 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); + ++#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) && \ ++ (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) ++ app.setAttribute(Qt::AA_EnableHighDpiScaling); ++ app.setAttribute(Qt::AA_UseHighDpiPixmaps); ++#endif ++ app.setAttribute(Qt::AA_DontShowIconsInMenus, false); ++ + colmap::MainWindow main_window(options); + main_window.show(); + +diff --git a/src/colmap/feature/CMakeLists.txt b/src/colmap/feature/CMakeLists.txt +index c03e3643..0f3ebea9 100644 +--- a/src/colmap/feature/CMakeLists.txt ++++ b/src/colmap/feature/CMakeLists.txt +@@ -101,5 +101,5 @@ COLMAP_ADD_TEST( + colmap_util + ) + if(TESTS_ENABLED AND GUI_ENABLED) +- target_link_libraries(colmap_feature_sift_test Qt5::Widgets) ++ target_link_libraries(colmap_feature_sift_test Qt${QT_VERSION_MAJOR}::Widgets) + endif() +diff --git a/src/colmap/feature/extractor.cc b/src/colmap/feature/extractor.cc +index 730a3da2..45dfa1d3 100644 +--- a/src/colmap/feature/extractor.cc ++++ b/src/colmap/feature/extractor.cc +@@ -46,17 +46,18 @@ void ThrowUnknownFeatureExtractorType(FeatureExtractorType type) { + FeatureExtractionOptions::FeatureExtractionOptions(FeatureExtractorType type) + : type(type), sift(std::make_shared()) {} + +-int FeatureExtractionOptions::MaxImageSize() const { ++bool FeatureExtractionOptions::RequiresRGB() const { + switch (type) { + case FeatureExtractorType::SIFT: +- return sift->max_image_size; ++ return false; + default: + ThrowUnknownFeatureExtractorType(type); + } +- return -1; ++ return false; + } + + bool FeatureExtractionOptions::Check() const { ++ CHECK_OPTION_GT(max_image_size, 0); + if (use_gpu) { + CHECK_OPTION_GT(CSVToVector(gpu_index).size(), 0); + #ifndef COLMAP_GPU_ENABLED +diff --git a/src/colmap/feature/extractor.h b/src/colmap/feature/extractor.h +index c6808a04..d2bcc7dc 100644 +--- a/src/colmap/feature/extractor.h ++++ b/src/colmap/feature/extractor.h +@@ -47,6 +47,9 @@ struct FeatureExtractionOptions { + + FeatureExtractorType type = FeatureExtractorType::SIFT; + ++ // Maximum image size, otherwise image will be down-scaled. ++ int max_image_size = 3200; ++ + // Number of threads for feature extraction. + int num_threads = -1; + +@@ -59,7 +62,8 @@ struct FeatureExtractionOptions { + + std::shared_ptr sift; + +- int MaxImageSize() const; ++ // Whether the selected extractor requires RGB (or grayscale) images. ++ bool RequiresRGB() const; + + bool Check() const; + }; +diff --git a/src/colmap/feature/sift.cc b/src/colmap/feature/sift.cc +index 9aeee62e..79265d65 100644 +--- a/src/colmap/feature/sift.cc ++++ b/src/colmap/feature/sift.cc +@@ -63,7 +63,6 @@ namespace colmap { + constexpr int kSqSiftDescriptorNorm = 512 * 512; + + bool SiftExtractionOptions::Check() const { +- 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); +@@ -157,7 +156,7 @@ class SiftCPUFeatureExtractor : public FeatureExtractor { + bool first_octave = true; + while (true) { + if (first_octave) { +- const std::vector data_uint8 = bitmap.ConvertToRowMajorArray(); ++ const std::vector& data_uint8 = bitmap.RowMajorData(); + 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; +@@ -346,7 +345,7 @@ class CovariantSiftCPUFeatureExtractor : public FeatureExtractor { + vl_covdet_set_edge_threshold(covdet.get(), options_.sift->edge_threshold); + + { +- const std::vector data_uint8 = bitmap.ConvertToRowMajorArray(); ++ const std::vector& data_uint8 = bitmap.RowMajorData(); + 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; +@@ -573,7 +572,7 @@ class SiftGPUFeatureExtractor : public FeatureExtractor { + << -std::min(0, options.sift->first_octave); + sift_gpu_args.push_back("-maxd"); + sift_gpu_args.push_back( +- std::to_string(options.sift->max_image_size * compensation_factor)); ++ std::to_string(options.max_image_size * compensation_factor)); + + // Keep the highest level features. + sift_gpu_args.push_back("-tc2"); +@@ -647,17 +646,16 @@ class SiftGPUFeatureExtractor : public FeatureExtractor { + // first octave in the pyramid (which is the 'first_octave'). + const int compensation_factor = + 1 << -std::min(0, options_.sift->first_octave); +- THROW_CHECK_EQ(options_.sift->max_image_size * compensation_factor, ++ THROW_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.Pitch(), + bitmap.Height(), +- bitmap_raw_bits.data(), ++ bitmap.RowMajorData().data(), + GL_LUMINANCE, + GL_UNSIGNED_BYTE); + +diff --git a/src/colmap/feature/sift.h b/src/colmap/feature/sift.h +index 017e6c32..7cdb48b6 100644 +--- a/src/colmap/feature/sift.h ++++ b/src/colmap/feature/sift.h +@@ -35,9 +35,6 @@ + namespace colmap { + + struct SiftExtractionOptions { +- // 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; + +diff --git a/src/colmap/feature/sift_test.cc b/src/colmap/feature/sift_test.cc +index 5c89eb11..af493016 100644 +--- a/src/colmap/feature/sift_test.cc ++++ b/src/colmap/feature/sift_test.cc +@@ -46,19 +46,19 @@ + namespace colmap { + namespace { + +-void CreateImageWithSquare(const int size, Bitmap* bitmap) { +- bitmap->Allocate(size, size, false); +- bitmap->Fill(BitmapColor(0, 0, 0)); ++Bitmap CreateImageWithSquare(const int size) { ++ Bitmap bitmap(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)); ++ bitmap.SetPixel(r, c, BitmapColor(255)); + } + } ++ return bitmap; + } + + TEST(ExtractSiftFeaturesCPU, Nominal) { +- Bitmap bitmap; +- CreateImageWithSquare(256, &bitmap); ++ const Bitmap bitmap = CreateImageWithSquare(256); + + FeatureExtractionOptions options(FeatureExtractorType::SIFT); + options.use_gpu = false; +@@ -89,8 +89,7 @@ TEST(ExtractSiftFeaturesCPU, Nominal) { + } + + TEST(ExtractCovariantSiftFeaturesCPU, Nominal) { +- Bitmap bitmap; +- CreateImageWithSquare(256, &bitmap); ++ const Bitmap bitmap = CreateImageWithSquare(256); + + FeatureExtractionOptions options(FeatureExtractorType::SIFT); + options.use_gpu = false; +@@ -121,8 +120,7 @@ TEST(ExtractCovariantSiftFeaturesCPU, Nominal) { + } + + TEST(ExtractCovariantAffineSiftFeaturesCPU, Nominal) { +- Bitmap bitmap; +- CreateImageWithSquare(256, &bitmap); ++ const Bitmap bitmap = CreateImageWithSquare(256); + + FeatureExtractionOptions options(FeatureExtractorType::SIFT); + options.use_gpu = false; +@@ -153,8 +151,7 @@ TEST(ExtractCovariantAffineSiftFeaturesCPU, Nominal) { + } + + TEST(ExtractCovariantAffineSiftFeaturesCPU, Upright) { +- Bitmap bitmap; +- CreateImageWithSquare(256, &bitmap); ++ const Bitmap bitmap = CreateImageWithSquare(256); + + FeatureExtractionOptions options(FeatureExtractorType::SIFT); + options.use_gpu = false; +@@ -186,8 +183,7 @@ TEST(ExtractCovariantAffineSiftFeaturesCPU, Upright) { + } + + TEST(ExtractCovariantDSPSiftFeaturesCPU, Nominal) { +- Bitmap bitmap; +- CreateImageWithSquare(256, &bitmap); ++ const Bitmap bitmap = CreateImageWithSquare(256); + + FeatureExtractionOptions options(FeatureExtractorType::SIFT); + options.use_gpu = false; +@@ -218,8 +214,7 @@ TEST(ExtractCovariantDSPSiftFeaturesCPU, Nominal) { + } + + TEST(ExtractCovariantAffineDSPSiftFeaturesCPU, Nominal) { +- Bitmap bitmap; +- CreateImageWithSquare(256, &bitmap); ++ const Bitmap bitmap = CreateImageWithSquare(256); + + FeatureExtractionOptions options(FeatureExtractorType::SIFT); + options.use_gpu = false; +@@ -260,8 +255,7 @@ TEST(ExtractSiftFeaturesGPU, Nominal) { + void Run() { + opengl_context_.MakeCurrent(); + +- Bitmap bitmap; +- CreateImageWithSquare(256, &bitmap); ++ const Bitmap bitmap = CreateImageWithSquare(256); + + FeatureExtractionOptions options(FeatureExtractorType::SIFT); + options.use_gpu = true; +diff --git a/src/colmap/image/CMakeLists.txt b/src/colmap/image/CMakeLists.txt +index 1e28dc91..2bfe492b 100644 +--- a/src/colmap/image/CMakeLists.txt ++++ b/src/colmap/image/CMakeLists.txt +@@ -42,6 +42,7 @@ COLMAP_ADD_LIBRARY( + PRIVATE_LINK_LIBS + colmap_util + colmap_sensor ++ colmap_vlfeat + ) + if(LSD_ENABLED) + target_link_libraries(colmap_image PRIVATE colmap_lsd) +diff --git a/src/colmap/image/line.cc b/src/colmap/image/line.cc +index e1a13aec..b5036355 100644 +--- a/src/colmap/image/line.cc ++++ b/src/colmap/image/line.cc +@@ -49,21 +49,21 @@ struct RawDeleter { + } // namespace + + #ifdef COLMAP_LSD_ENABLED ++ + std::vector DetectLineSegments(const Bitmap& bitmap, + const double min_length) { + const double min_length_squared = min_length * min_length; + +- std::vector bitmap_data; ++ std::vector bitmap_data_double; + if (bitmap.IsGrey()) { +- bitmap_data = bitmap.ConvertToRowMajorArray(); ++ bitmap_data_double = {bitmap.RowMajorData().begin(), ++ bitmap.RowMajorData().end()}; + } else { + const Bitmap bitmap_gray = bitmap.CloneAsGrey(); +- bitmap_data = bitmap_gray.ConvertToRowMajorArray(); ++ bitmap_data_double = {bitmap_gray.RowMajorData().begin(), ++ bitmap_gray.RowMajorData().end()}; + } + +- std::vector bitmap_data_double(bitmap_data.begin(), +- bitmap_data.end()); +- + int num_segments; + std::unique_ptr segments_data( + lsd(&num_segments, +diff --git a/src/colmap/image/line_test.cc b/src/colmap/image/line_test.cc +index bfd04ada..422291ad 100644 +--- a/src/colmap/image/line_test.cc ++++ b/src/colmap/image/line_test.cc +@@ -35,9 +35,9 @@ namespace colmap { + namespace { + + #ifdef COLMAP_LSD_ENABLED ++ + TEST(DetectLineSegments, Nominal) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, false); ++ Bitmap bitmap(100, 100, false); + for (size_t i = 0; i < 100; ++i) { + bitmap.SetPixel(i, i, BitmapColor(255)); + } +@@ -57,8 +57,7 @@ TEST(DetectLineSegments, Nominal) { + } + + TEST(ClassifyLineSegmentOrientations, Nominal) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, false); ++ Bitmap bitmap(100, 100, false); + for (size_t i = 60; i < 100; ++i) { + bitmap.SetPixel(i, 50, BitmapColor(255)); + bitmap.SetPixel(50, i, BitmapColor(255)); +diff --git a/src/colmap/image/undistortion.cc b/src/colmap/image/undistortion.cc +index ec4b8a7a..839d4e80 100644 +--- a/src/colmap/image/undistortion.cc ++++ b/src/colmap/image/undistortion.cc +@@ -1084,12 +1084,12 @@ void RectifyAndUndistortStereoImages(const UndistortCameraOptions& options, + THROW_CHECK_EQ(distorted_camera2.height, distorted_image2.Height()); + + *undistorted_camera = UndistortCamera(options, distorted_camera1); +- undistorted_image1->Allocate(static_cast(undistorted_camera->width), ++ *undistorted_image1 = Bitmap(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), ++ *undistorted_image2 = Bitmap(static_cast(undistorted_camera->width), + static_cast(undistorted_camera->height), + distorted_image2.IsRGB()); + distorted_image2.CloneMetadata(undistorted_image2); +diff --git a/src/colmap/image/undistortion_test.cc b/src/colmap/image/undistortion_test.cc +index be0fdee5..a7296890 100644 +--- a/src/colmap/image/undistortion_test.cc ++++ b/src/colmap/image/undistortion_test.cc +@@ -110,8 +110,7 @@ TEST(UndistortCamera, BlankPixels) { + Camera::CreateFromModelName(1, "SIMPLE_RADIAL", 100, 100, 100); + distorted_camera.params[3] = 0.5; + +- Bitmap distorted_image; +- distorted_image.Allocate(100, 100, false); ++ Bitmap distorted_image(100, 100, false); + distorted_image.Fill(BitmapColor(255)); + + Bitmap undistorted_image; +@@ -154,8 +153,7 @@ TEST(UndistortCamera, NoBlankPixels) { + Camera::CreateFromModelName(1, "SIMPLE_RADIAL", 100, 100, 100); + distorted_camera.params[3] = 0.5; + +- Bitmap distorted_image; +- distorted_image.Allocate(100, 100, false); ++ Bitmap distorted_image(100, 100, false); + distorted_image.Fill(BitmapColor(255)); + + Bitmap undistorted_image; +@@ -179,9 +177,9 @@ TEST(UndistortCamera, NoBlankPixels) { + 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); ++ ASSERT_NE(color.r, 0); ++ ASSERT_NE(color.g, 0); ++ ASSERT_NE(color.b, 0); + } + } + } +diff --git a/src/colmap/image/warp.cc b/src/colmap/image/warp.cc +index b8a83969..11cb6605 100644 +--- a/src/colmap/image/warp.cc ++++ b/src/colmap/image/warp.cc +@@ -61,7 +61,7 @@ void WarpImageBetweenCameras(const Camera& source_camera, + THROW_CHECK_EQ(source_camera.height, source_image.Height()); + THROW_CHECK_NOTNULL(target_image); + +- target_image->Allocate(static_cast(source_camera.width), ++ *target_image = Bitmap(static_cast(source_camera.width), + static_cast(source_camera.height), + source_image.IsRGB()); + +@@ -143,7 +143,7 @@ void WarpImageWithHomographyBetweenCameras(const Eigen::Matrix3d& H, + THROW_CHECK_EQ(source_camera.height, source_image.Height()); + THROW_CHECK_NOTNULL(target_image); + +- target_image->Allocate(static_cast(source_camera.width), ++ *target_image = Bitmap(static_cast(source_camera.width), + static_cast(source_camera.height), + source_image.IsRGB()); + +diff --git a/src/colmap/image/warp_test.cc b/src/colmap/image/warp_test.cc +index 13c7feb7..0604b17f 100644 +--- a/src/colmap/image/warp_test.cc ++++ b/src/colmap/image/warp_test.cc +@@ -37,20 +37,20 @@ namespace colmap { + namespace { + namespace { + +-void GenerateRandomBitmap(const int width, +- const int height, +- const bool as_rgb, +- Bitmap* bitmap) { +- bitmap->Allocate(width, height, as_rgb); ++const Bitmap GenerateRandomBitmap(const int width, ++ const int height, ++ const bool as_rgb) { ++ Bitmap bitmap(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); ++ bitmap.SetPixel(x, y, color); + } + } ++ return bitmap; + } + + // Check that the two bitmaps are equal, ignoring a 1px boundary. +@@ -91,14 +91,12 @@ void CheckBitmapsTransposed(const Bitmap& bitmap1, const Bitmap& bitmap2) { + + TEST(Warp, IdenticalCameras) { + const Camera camera = Camera::CreateFromModelName(1, "PINHOLE", 1, 100, 100); +- Bitmap source_image_gray; +- GenerateRandomBitmap(100, 100, false, &source_image_gray); ++ const Bitmap source_image_gray = GenerateRandomBitmap(100, 100, false); + 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); ++ const Bitmap source_image_rgb = GenerateRandomBitmap(100, 100, true); + Bitmap target_image_rgb; + WarpImageBetweenCameras(camera, camera, source_image_rgb, &target_image_rgb); + CheckBitmapsEqual(source_image_rgb, target_image_rgb); +@@ -109,8 +107,7 @@ TEST(Warp, ShiftedCameras) { + 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); ++ const Bitmap source_image_gray = GenerateRandomBitmap(100, 100, true); + Bitmap target_image_gray; + WarpImageBetweenCameras( + source_camera, target_camera, source_image_gray, &target_image_gray); +@@ -132,18 +129,14 @@ TEST(Warp, ShiftedCameras) { + } + + 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); ++ const Bitmap source_image_gray = GenerateRandomBitmap(100, 100, false); ++ Bitmap target_image_gray(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); ++ const Bitmap source_image_rgb = GenerateRandomBitmap(100, 100, true); ++ Bitmap target_image_rgb(100, 100, true); + WarpImageWithHomography( + Eigen::Matrix3d::Identity(), source_image_rgb, &target_image_rgb); + CheckBitmapsEqual(source_image_rgb, target_image_rgb); +@@ -153,27 +146,21 @@ 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); ++ const Bitmap source_image_gray = GenerateRandomBitmap(100, 100, false); ++ Bitmap target_image_gray(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); ++ const Bitmap source_image_rgb = GenerateRandomBitmap(100, 100, true); ++ Bitmap target_image_rgb(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); ++ const Bitmap source_image_gray = GenerateRandomBitmap(100, 100, false); + Bitmap target_image_gray; +- target_image_gray.Allocate(100, 100, false); + WarpImageWithHomographyBetweenCameras(Eigen::Matrix3d::Identity(), + camera, + camera, +@@ -181,10 +168,8 @@ TEST(Warp, WarpImageWithHomographyBetweenCamerasIdentity) { + &target_image_gray); + CheckBitmapsEqual(source_image_gray, target_image_gray); + +- Bitmap source_image_rgb; +- GenerateRandomBitmap(100, 100, true, &source_image_rgb); ++ const Bitmap source_image_rgb = GenerateRandomBitmap(100, 100, true); + Bitmap target_image_rgb; +- target_image_rgb.Allocate(100, 100, true); + WarpImageWithHomographyBetweenCameras(Eigen::Matrix3d::Identity(), + camera, + camera, +@@ -199,18 +184,14 @@ TEST(Warp, WarpImageWithHomographyBetweenCamerasTransposed) { + Eigen::Matrix3d H; + H << 0, 1, 0, 1, 0, 0, 0, 0, 1; + +- Bitmap source_image_gray; +- GenerateRandomBitmap(100, 100, false, &source_image_gray); ++ const Bitmap source_image_gray = GenerateRandomBitmap(100, 100, false); + 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); ++ const Bitmap source_image_rgb = GenerateRandomBitmap(100, 100, true); + 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); +diff --git a/src/colmap/mvs/depth_map.cc b/src/colmap/mvs/depth_map.cc +index 1a18cc9d..fa9c6cd7 100644 +--- a/src/colmap/mvs/depth_map.cc ++++ b/src/colmap/mvs/depth_map.cc +@@ -87,8 +87,7 @@ Bitmap DepthMap::ToBitmap(const float min_percentile, + THROW_CHECK_GT(width_, 0); + THROW_CHECK_GT(height_, 0); + +- Bitmap bitmap; +- bitmap.Allocate(width_, height_, true); ++ Bitmap bitmap(width_, height_, true); + + std::vector valid_depths; + valid_depths.reserve(data_.size()); +diff --git a/src/colmap/mvs/image.cc b/src/colmap/mvs/image.cc +index efb6fb3f..952f9f89 100644 +--- a/src/colmap/mvs/image.cc ++++ b/src/colmap/mvs/image.cc +@@ -66,7 +66,7 @@ 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) { ++ if (!bitmap_.IsEmpty()) { + bitmap_.Rescale(new_width, new_height); + } + +diff --git a/src/colmap/mvs/normal_map.cc b/src/colmap/mvs/normal_map.cc +index 47389c10..5014db54 100644 +--- a/src/colmap/mvs/normal_map.cc ++++ b/src/colmap/mvs/normal_map.cc +@@ -101,8 +101,7 @@ Bitmap NormalMap::ToBitmap() const { + THROW_CHECK_GT(height_, 0); + THROW_CHECK_EQ(depth_, 3); + +- Bitmap bitmap; +- bitmap.Allocate(width_, height_, true); ++ Bitmap bitmap(width_, height_, true); + + for (size_t y = 0; y < height_; ++y) { + for (size_t x = 0; x < width_; ++x) { +diff --git a/src/colmap/mvs/patch_match_cuda.cu b/src/colmap/mvs/patch_match_cuda.cu +index b7267318..f3080a3a 100644 +--- a/src/colmap/mvs/patch_match_cuda.cu ++++ b/src/colmap/mvs/patch_match_cuda.cu +@@ -1539,9 +1539,7 @@ void PatchMatchCuda::InitRefImage() { + + // 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(), ++ ref_image_->Filter(ref_image.GetBitmap().RowMajorData().data(), + options_.window_radius, + options_.window_step, + options_.sigma_spatial, +@@ -1576,10 +1574,7 @@ void PatchMatchCuda::InitSourceImages() { + 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; +- } ++ memcpy(dest, bitmap.RowMajorData().data(), bitmap.NumBytes()); + } + + // Create source images texture. +diff --git a/src/colmap/scene/reconstruction.cc b/src/colmap/scene/reconstruction.cc +index e7a26ed9..06e83cba 100644 +--- a/src/colmap/scene/reconstruction.cc ++++ b/src/colmap/scene/reconstruction.cc +@@ -830,7 +830,8 @@ bool Reconstruction::ExtractColorsForImage(const image_t image_id, + const class Image& image = Image(image_id); + + Bitmap bitmap; +- if (!bitmap.Read(JoinPaths(path, image.Name()))) { ++ if (!bitmap.Read(JoinPaths(path, image.Name()), ++ /*as_rgb=*/true)) { + return false; + } + +@@ -862,7 +863,8 @@ void Reconstruction::ExtractColorsForAllImages(const std::string& path) { + const std::string image_path = JoinPaths(path, image.Name()); + + Bitmap bitmap; +- if (!bitmap.Read(image_path)) { ++ if (!bitmap.Read(image_path, ++ /*as_rgb=*/true)) { + LOG(WARNING) << "Could not read image " << image.Name() << " at path " + << image_path; + continue; +diff --git a/src/colmap/sensor/CMakeLists.txt b/src/colmap/sensor/CMakeLists.txt +index d8fd80ad..a79e04b1 100644 +--- a/src/colmap/sensor/CMakeLists.txt ++++ b/src/colmap/sensor/CMakeLists.txt +@@ -44,8 +44,7 @@ COLMAP_ADD_LIBRARY( + PRIVATE_LINK_LIBS + colmap_geometry + colmap_util +- colmap_vlfeat +- freeimage::FreeImage ++ OpenImageIO::OpenImageIO + ) + + COLMAP_ADD_TEST( +@@ -53,7 +52,6 @@ COLMAP_ADD_TEST( + SRCS bitmap_test.cc + LINK_LIBS + colmap_sensor +- freeimage::FreeImage + ) + COLMAP_ADD_TEST( + NAME database_test +diff --git a/src/colmap/sensor/bitmap.cc b/src/colmap/sensor/bitmap.cc +index 7dfcc40f..1fb258c1 100644 +--- a/src/colmap/sensor/bitmap.cc ++++ b/src/colmap/sensor/bitmap.cc +@@ -35,99 +35,173 @@ + #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 ++#include ++#include ++#include + + namespace colmap { + namespace { + +-#ifdef FREEIMAGE_LIB // Only needed for static FreeImage. ++struct OIIOInitializer { ++ OIIOInitializer() { ++ OIIO::attribute("threads", 1); ++ OIIO::attribute("exr_threads", 1); ++ } + +-struct FreeImageInitializer { +- FreeImageInitializer() { FreeImage_Initialise(); } +- ~FreeImageInitializer() { FreeImage_DeInitialise(); } ++#if OIIO_VERSION >= OIIO_MAKE_VERSION(2, 5, 3) ++ ~OIIOInitializer() { OIIO::shutdown(); } ++#endif + }; + +-const static auto initializer = FreeImageInitializer(); ++const static auto initializer = OIIOInitializer(); + +-#endif // FREEIMAGE_LIB ++struct OIIOMetaData : public Bitmap::MetaData { ++ OIIOMetaData() = default; + +-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; ++ OIIO::ImageSpec image_spec; ++ ++ static OIIOMetaData* Upcast(Bitmap::MetaData* meta_data) { ++ return THROW_CHECK_NOTNULL(dynamic_cast(meta_data)); ++ } ++ ++ static std::unique_ptr Clone( ++ const std::unique_ptr& meta_data) { ++ auto cloned = std::make_unique(); ++ *cloned = *Upcast(meta_data.get()); ++ return cloned; ++ } ++}; ++ ++// For backwards compatibility with older OIIO versions without implicit ++// conversion from std::string_view. ++OIIO::string_view OIIOFromStdStringView(std::string_view value) { ++ return {value.data(), value.size()}; ++} ++ ++std::vector ConvertColorSpace(const uint8_t* src_data, ++ int width, ++ int height, ++ int channels, ++ const std::string_view& from, ++ const std::string_view& to) { ++ const OIIO::ImageSpec image_spec( ++ width, height, channels, OIIO::TypeDesc::UINT8); ++ const int pitch = width * channels; ++ const OIIO::ImageBuf src(image_spec, const_cast(src_data)); ++ std::vector tgt_data(height * pitch); ++ OIIO::ImageBuf tgt(image_spec, tgt_data.data()); ++ THROW_CHECK(OIIO::ImageBufAlgo::colorconvert( ++ tgt, src, OIIOFromStdStringView(from), OIIOFromStdStringView(to))); ++ return tgt_data; ++} ++ ++void SetImageSpecColorSpace(OIIO::ImageSpec& image_spec, ++ const OIIO::string_view& colorspace) { ++#if OIIO_VERSION >= OIIO_MAKE_VERSION(3, 0, 0) ++ image_spec.set_colorspace(colorspace); ++#else ++ // Extract logic from 3.0.0 version for backwards compatibility. ++ const OIIO::string_view oldspace = ++ image_spec.get_string_attribute("oiio:ColorSpace"); ++ if (oldspace.size() && colorspace.size() && oldspace == colorspace) { ++ return; ++ } ++ ++ if (colorspace.empty()) { ++ image_spec.erase_attribute("oiio:ColorSpace"); + } 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; ++ image_spec.attribute("oiio:ColorSpace", colorspace); + } +-} + +-bool IsPtrGrey(FIBITMAP* ptr) { +- return FreeImage_GetColorType(ptr) == FIC_MINISBLACK && +- FreeImage_GetBPP(ptr) == 8; +-} ++ if (colorspace != "sRGB") { ++ image_spec.erase_attribute("Exif:ColorSpace"); ++ } + +-bool IsPtrRGB(FIBITMAP* ptr) { +- return FreeImage_GetColorType(ptr) == FIC_RGB && FreeImage_GetBPP(ptr) == 24; ++ image_spec.erase_attribute("tiff:ColorSpace"); ++ image_spec.erase_attribute("tiff:PhotometricInterpretation"); ++ image_spec.erase_attribute("oiio:Gamma"); ++#endif + } + +-bool IsPtrSupported(FIBITMAP* ptr) { return IsPtrGrey(ptr) || IsPtrRGB(ptr); } ++bool IsEquivalentColorSpace(const std::string_view& colorspace1, ++ const std::string_view& colorspace2) { ++#if OIIO_VERSION >= OIIO_MAKE_VERSION(3, 0, 0) ++ return OIIO::equivalent_colorspace(colorspace1, colorspace2); ++#else ++ // Poor (wo)man's version of available functionality in recent OIIO versions. ++ auto is_linear_srgb = [](const std::string_view& colorspace) { ++ return colorspace == "linear" || colorspace == "lin_srgb" || ++ colorspace == "lin_rec709P"; ++ }; ++ if (is_linear_srgb(colorspace1) && is_linear_srgb(colorspace2)) { ++ return true; ++ } else { ++ return colorspace1 == colorspace2; ++ } ++#endif ++} + + } // namespace + +-Bitmap::Bitmap() : width_(0), height_(0), channels_(0) {} ++Bitmap::Bitmap() ++ : width_(0), height_(0), channels_(0), linear_colorspace_(true) {} + +-Bitmap::Bitmap(const Bitmap& other) : Bitmap() { +- if (other.handle_.ptr != nullptr) { +- SetPtr(FreeImage_Clone(other.handle_.ptr)); +- } ++Bitmap::Bitmap(const int width, ++ const int height, ++ const bool as_rgb, ++ const bool linear_colorspace) { ++ width_ = width; ++ height_ = height; ++ channels_ = as_rgb ? 3 : 1; ++ linear_colorspace_ = linear_colorspace; ++ data_.resize(width_ * height_ * channels_); ++ auto meta_data = std::make_unique(); ++ meta_data->image_spec = ++ OIIO::ImageSpec(width_, height_, channels_, OIIO::TypeDesc::UINT8); ++ SetImageSpecColorSpace(meta_data->image_spec, ++ linear_colorspace ? "linear" : "sRGB"); ++ meta_data_ = std::move(meta_data); + } + +-Bitmap::Bitmap(Bitmap&& other) noexcept : Bitmap() { +- handle_ = std::move(other.handle_); ++Bitmap::Bitmap(const Bitmap& other) { + width_ = other.width_; + height_ = other.height_; + channels_ = other.channels_; ++ linear_colorspace_ = other.linear_colorspace_; ++ data_ = other.data_; ++ meta_data_ = OIIOMetaData::Clone(other.meta_data_); ++} ++ ++Bitmap::Bitmap(Bitmap&& other) noexcept { ++ width_ = other.width_; ++ height_ = other.height_; ++ channels_ = other.channels_; ++ linear_colorspace_ = other.linear_colorspace_; ++ data_ = std::move(other.data_); ++ meta_data_ = std::move(other.meta_data_); + 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)); +- } ++ width_ = other.width_; ++ height_ = other.height_; ++ channels_ = other.channels_; ++ linear_colorspace_ = other.linear_colorspace_; ++ data_ = other.data_; ++ meta_data_ = OIIOMetaData::Clone(other.meta_data_); + 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_; ++ linear_colorspace_ = other.linear_colorspace_; ++ data_ = std::move(other.data_); ++ meta_data_ = std::move(other.meta_data_); + other.width_ = 0; + other.height_ = 0; + other.channels_ = 0; +@@ -135,167 +209,16 @@ Bitmap& Bitmap::operator=(Bitmap&& other) noexcept { + 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 Pitch() * height_; +- } else { +- return 0; +- } +-} +- +-unsigned int Bitmap::BitsPerPixel() const { +- return FreeImage_GetBPP(handle_.ptr); +-} +- +-unsigned int Bitmap::Pitch() const { return FreeImage_GetPitch(handle_.ptr); } +- +-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; +-} +- +-std::vector Bitmap::ConvertToRawBits() const { +- const unsigned int pitch = Pitch(); +- const unsigned int bpp = BitsPerPixel(); +- std::vector raw_bits(pitch * height_ * bpp / 8, 0); +- FreeImage_ConvertToRawBits(raw_bits.data(), +- handle_.ptr, +- pitch, +- bpp, +- FI_RGBA_RED_MASK, +- FI_RGBA_GREEN_MASK, +- FI_RGBA_BLUE_MASK, +- /*topdown=*/true); +- return raw_bits; +-} +- +-Bitmap Bitmap::ConvertFromRawBits( +- const uint8_t* data, int pitch, int width, int height, bool rgb) { +- const unsigned bpp = rgb ? 24 : 8; +- return Bitmap(FreeImage_ConvertFromRawBitsEx(/*copy_source=*/true, +- const_cast(data), +- FIT_BITMAP, +- width, +- height, +- pitch, +- bpp, +- FI_RGBA_RED_MASK, +- FI_RGBA_GREEN_MASK, +- FI_RGBA_BLUE_MASK, +- /*topdown=*/true)); +-} +- +-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 { +- THROW_CHECK_GE(y, 0); +- THROW_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; +- } ++ if (IsGrey()) { ++ std::fill(data_.begin(), data_.end(), color.r); ++ } else { ++ THROW_CHECK_EQ(data_.size() % 3, 0); ++ size_t i = 0; ++ while (i < data_.size()) { ++ data_[i++] = color.r; ++ data_[i++] = color.g; ++ data_[i++] = color.b; + } + } + } +@@ -311,12 +234,9 @@ bool Bitmap::InterpolateNearestNeighbor(const double x, + 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 y0 = static_cast(std::floor(y)); + const int y1 = y0 + 1; + + if (x0 < 0 || x1 >= width_ || y0 < 0 || y1 >= height_) { +@@ -324,12 +244,13 @@ bool Bitmap::InterpolateBilinear(const double x, + } + + const double dx = x - x0; +- const double dy = inv_y - y0; ++ const double dy = 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); ++ const int pitch = width_ * channels_; ++ const uint8_t* line0 = &data_[y0 * pitch]; ++ const uint8_t* line1 = &data_[y1 * pitch]; + + if (IsGrey()) { + // Top row, column-wise linear interpolation. +@@ -348,14 +269,14 @@ bool Bitmap::InterpolateBilinear(const double x, + 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]; ++ const double v0_r = dx_1 * p00[0] + dx * p01[0]; ++ const double v0_g = dx_1 * p00[1] + dx * p01[1]; ++ const double v0_b = dx_1 * p00[2] + dx * p01[2]; + + // 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]; ++ const double v1_r = dx_1 * p10[0] + dx * p11[0]; ++ const double v1_g = dx_1 * p10[1] + dx * p11[1]; ++ const double v1_b = dx_1 * p10[2] + dx * p11[2]; + + // Row-wise linear interpolation. + color->r = dy_1 * v0_r + dy * v1_r; +@@ -369,120 +290,94 @@ bool Bitmap::InterpolateBilinear(const double x, + + 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; ++ std::string_view make_str; ++ std::string_view model_str; ++ float focal_length = 0; + *camera_model = ""; +- if (ReadExifTag(handle_.ptr, FIMD_EXIF_MAIN, "Make", &make_str)) { +- *camera_model += (make_str + "-"); ++ if (GetMetaData("Make", &make_str)) { ++ *camera_model += std::string(make_str) + "-"; + } else { + *camera_model = ""; + return false; + } +- if (ReadExifTag(handle_.ptr, FIMD_EXIF_MAIN, "Model", &model_str)) { +- *camera_model += (model_str + "-"); ++ if (GetMetaData("Model", &model_str)) { ++ *camera_model += std::string(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 + "-"); ++ if (GetMetaData("Exif:FocalLengthIn35mmFilm", "float", &focal_length) || ++ GetMetaData("Exif:FocalLength", "float", &focal_length)) { ++ *camera_model += std::to_string(focal_length) + "-"; + } else { + *camera_model = ""; + return false; + } +- *camera_model += (std::to_string(width_) + "x" + std::to_string(height_)); ++ *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)) { +- static 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; +- } ++ float focal_length_35mm = 0; ++ if (GetMetaData("Exif:FocalLengthIn35mmFilm", "float", &focal_length_35mm)) { ++ if (focal_length_35mm > 0) { ++ // Based on https://en.wikipedia.org/wiki/35_mm_equivalent_focal_length ++ // According to CIPA guidelines, 35 mm equivalent focal length is to be ++ // calculated like this: ++ // "focal length in 35 mm camera" = ++ // (Diagonal distance of image area in the 35 mm camera (43.27 mm) / ++ // Diagonal distance of image area on the image sensor of the DSC) ++ // * focal length of the lens of the DSC. ++ const double diagonal = std::sqrt(width_ * width_ + height_ * height_); ++ *focal_length = focal_length_35mm / 43.27 * diagonal; ++ 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; ++ float focal_length_mm = 0.f; ++ if (GetMetaData("Exif:FocalLength", "float", &focal_length_mm)) { ++ float focal_x_res = 0.f; ++ int focal_x_res_unit = 0; ++ if (GetMetaData("Exif:FocalPlaneXResolution", "float", &focal_x_res) && ++ GetMetaData( ++ "Exif:FocalPlaneResolutionUnit", "int", &focal_x_res_unit)) { ++ if (focal_length_mm > 0 && focal_x_res_unit > 1 && ++ focal_x_res_unit <= 5) { ++ double pixels_per_mm = 0; ++ switch (focal_x_res_unit) { ++ case 2: // inches ++ pixels_per_mm = focal_x_res * 25.4; ++ break; ++ case 3: // cm ++ pixels_per_mm = focal_x_res * 10.0; ++ break; ++ case 4: // mm ++ pixels_per_mm = focal_x_res * 1.0; ++ break; ++ case 5: // um ++ pixels_per_mm = focal_x_res * 0.1; ++ break; ++ default: ++ LOG(FATAL) << "Unexpected FocalPlaneXResolution value"; + } ++ *focal_length = focal_length_mm / pixels_per_mm; ++ 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; +- } +- } +- } +- } ++ // Lookup sensor width in database. ++ std::string_view make_str; ++ std::string_view model_str; ++ if (GetMetaData("Make", &make_str) && GetMetaData("Model", &model_str)) { ++ CameraDatabase database; ++ double sensor_width_mm; ++ if (database.QuerySensorWidth(std::string(make_str), ++ std::string(model_str), ++ &sensor_width_mm)) { ++ *focal_length = focal_length_mm / sensor_width_mm * max_size; ++ return true; + } + } + } +@@ -491,199 +386,216 @@ bool Bitmap::ExifFocalLength(double* focal_length) const { + } + + bool Bitmap::ExifLatitude(double* latitude) const { +- std::string str; ++ std::string_view latitude_ref; + double sign = 1.0; +- if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLatitudeRef", &str)) { +- StringTrim(&str); +- StringToLower(&str); +- if (!str.empty() && str[0] == 's') { ++ if (GetMetaData("GPS:LatitudeRef", &latitude_ref)) { ++ if (latitude_ref == "N" || latitude_ref == "n") { ++ sign = 1.0; ++ } else if (latitude_ref == "S" || latitude_ref == "s") { + sign = -1.0; + } + } +- if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLatitude", &str)) { +- static 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; ++ float deg_min_sec[3] = {0.0}; ++ if (GetMetaData("GPS:Latitude", "point", °_min_sec)) { ++ *latitude = ++ deg_min_sec[0] + deg_min_sec[1] / 60.0 + deg_min_sec[2] / 3600.0; ++ if (*latitude > 0 && sign < 0) { ++ *latitude *= sign; + } ++ return true; + } + return false; + } + + bool Bitmap::ExifLongitude(double* longitude) const { +- std::string str; ++ std::string_view longitude_ref; + double sign = 1.0; +- if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLongitudeRef", &str)) { +- StringTrim(&str); +- StringToLower(&str); +- if (!str.empty() && str[0] == 'w') { ++ if (GetMetaData("GPS:LongitudeRef", &longitude_ref)) { ++ if (longitude_ref == "W" || longitude_ref == "w") { ++ sign = 1.0; ++ } else if (longitude_ref == "E" || longitude_ref == "e") { + sign = -1.0; + } + } +- if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSLongitude", &str)) { +- static 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; ++ float deg_min_sec[3] = {0.0}; ++ if (GetMetaData("GPS:Longitude", "point", °_min_sec)) { ++ *longitude = ++ deg_min_sec[0] + deg_min_sec[1] / 60.0 + deg_min_sec[2] / 3600.0; ++ if (*longitude > 0 && sign < 0) { ++ *longitude *= sign; + } ++ return true; + } + return false; + } + + bool Bitmap::ExifAltitude(double* altitude) const { +- std::string str; +- if (ReadExifTag(handle_.ptr, FIMD_EXIF_GPS, "GPSAltitude", &str)) { +- static 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; ++ std::string_view altitude_ref; ++ double sign = 1.0; ++ if (GetMetaData("GPS:AltitudeRef", &altitude_ref)) { ++ if (altitude_ref == "0") { ++ sign = 1.0; ++ } else if (altitude_ref == "1") { ++ sign = -1.0; + } + } ++ float altitude_float = 0.f; ++ if (GetMetaData("GPS:Altitude", "float", &altitude_float)) { ++ *altitude = altitude_float; ++ if (*altitude > 0 && sign < 0) { ++ *altitude *= sign; ++ } ++ return true; ++ } + return false; + } + +-bool Bitmap::Read(const std::string& path, const bool as_rgb) { ++bool Bitmap::Read(const std::string& path, ++ const bool as_rgb, ++ const bool linearize_colorspace) { + if (!ExistsFile(path)) { ++ VLOG(3) << "Failed to read bitmap, because file does not exist"; + return false; + } + +- const FREE_IMAGE_FORMAT format = FreeImage_GetFileType(path.c_str(), 0); ++ OIIO::ImageSpec config; ++ config["oiio:reorient"] = 0; + +- if (format == FIF_UNKNOWN) { ++ const auto input = OIIO::ImageInput::open(path, &config); ++ if (!input) { ++ VLOG(3) << "Failed to read bitmap specs"; + return false; + } + +- handle_ = FreeImageHandle(FreeImage_Load(format, path.c_str())); +- if (handle_.ptr == nullptr) { ++ const OIIO::ImageSpec& image_spec = input->spec(); ++ width_ = image_spec.width; ++ height_ = image_spec.height; ++ channels_ = image_spec.nchannels; ++ if (channels_ != 1 && channels_ != 3) { ++ VLOG(3) << "Bitmap is not grayscale or RGB"; + 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) { +- if (FreeImage_GetBPP(handle_.ptr) != 24) { +- FIBITMAP* converted_bitmap_24 = FreeImage_ConvertTo24Bits(handle_.ptr); +- handle_ = FreeImageHandle(converted_bitmap_24); ++ data_.resize(width_ * height_ * channels_); ++ input->read_image(0, 0, 0, channels_, OIIO::TypeDesc::UINT8, data_.data()); ++ input->close(); ++ ++ auto meta_data = std::make_unique(); ++ meta_data->image_spec = image_spec; ++ meta_data_ = std::move(meta_data); ++ ++ if (linearize_colorspace) { ++ const std::string colorspace = image_spec["oiio:ColorSpace"]; ++ if (IsEquivalentColorSpace(colorspace, "linear")) { ++ data_ = ConvertColorSpace( ++ data_.data(), width_, height_, channels_, colorspace, "linear"); + } +- FIBITMAP* converted_bitmap = FreeImage_ConvertToGreyscale(handle_.ptr); +- handle_ = FreeImageHandle(converted_bitmap); + } + +- if (!IsPtrSupported(handle_.ptr)) { +- handle_ = FreeImageHandle(); +- return false; ++ if (as_rgb && channels_ != 3) { ++ *this = CloneAsRGB(); ++ } else if (!as_rgb && channels_ != 1) { ++ *this = CloneAsGrey(); + } + +- 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; ++bool Bitmap::Write(const std::string& path, ++ const bool delinearize_colorspace) const { ++ const auto output = OIIO::ImageOutput::create(path); ++ if (!output) { ++ std::cerr << "Could not create an ImageOutput for " << path ++ << ", error = " << OIIO::geterror() << "\n"; ++ return false; + } + +- int save_flags = flags; +- if (save_format == FIF_JPEG && flags == 0) { +- // Use superb JPEG quality by default to avoid artifacts. +- save_flags = JPEG_QUALITYSUPERB; ++ auto* meta_data = OIIOMetaData::Upcast(meta_data_.get()); ++ ++ const uint8_t* output_data_ptr = data_.data(); ++ std::vector maybe_linearized_output_data; ++ if (delinearize_colorspace && linear_colorspace_) { ++ std::string_view colorspace; ++ if (!GetMetaData("oiio:ColorSpace", &colorspace)) { ++ // Assume sRGB color space if not specified. ++ colorspace = "sRGB"; ++ SetImageSpecColorSpace(meta_data->image_spec, ++ OIIOFromStdStringView(colorspace)); ++ } ++ ++ maybe_linearized_output_data = ConvertColorSpace( ++ data_.data(), width_, height_, channels_, "linear", colorspace); ++ output_data_ptr = maybe_linearized_output_data.data(); + } + +- 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); ++ if (HasFileExtension(path, ".jpg") || HasFileExtension(path, ".jpeg")) { ++ std::string_view compression; ++ if (!GetMetaData("Compression", &compression)) { ++ // Save JPEG in superb quality by default to reduce compression artifacts. ++ meta_data->image_spec["Compression"] = "jpeg:100"; ++ } + } + +- return success; +-} ++ if (!output->open(path, meta_data->image_spec)) { ++ VLOG(3) << "Could not open " << path << ", error = " << output->geterror() ++ << "\n"; ++ return false; ++ } + +-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; +- } +- } ++ if (!output->write_image(OIIO::TypeDesc::UINT8, output_data_ptr)) { ++ VLOG(3) << "Could not write pixels to " << path ++ << ", error = " << output->geterror() << "\n"; ++ return false; ++ } + +- 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; +- } +- } ++ if (!output->close()) { ++ VLOG(3) << "Error closing " << path << ", error = " << output->geterror() ++ << "\n"; ++ return false; + } ++ ++ return true; + } + + 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_THROW) << "Filter not implemented"; +- } +- SetPtr(FreeImage_Rescale(handle_.ptr, new_width, new_height, fi_filter)); +-} ++ const OIIO::ImageBuf buf( ++ OIIO::ImageSpec(width_, height_, channels_, OIIO::TypeDesc::UINT8), ++ data_.data()); ++ std::vector new_data(new_width * new_height * channels_); ++ OIIO::ImageBuf new_buf( ++ OIIO::ImageSpec(new_width, new_height, channels_, OIIO::TypeDesc::UINT8), ++ new_data.data()); ++ THROW_CHECK(OIIO::ImageBufAlgo::resize(new_buf, buf)); + +-Bitmap Bitmap::Clone() const { +- FIBITMAP* cloned = FreeImage_Clone(handle_.ptr); +- return Bitmap(cloned); ++ width_ = new_width; ++ height_ = new_height; ++ data_ = std::move(new_data); ++ auto* meta_data = OIIOMetaData::Upcast(meta_data_.get()); ++ meta_data->image_spec.width = new_width; ++ meta_data->image_spec.height = new_height; + } + ++Bitmap Bitmap::Clone() const { return *this; } ++ + Bitmap Bitmap::CloneAsGrey() const { + if (IsGrey()) { + return Clone(); + } else { +- return Bitmap(FreeImage_ConvertToGreyscale(handle_.ptr)); ++ Bitmap cloned; ++ cloned.width_ = width_; ++ cloned.height_ = height_; ++ cloned.channels_ = 1; ++ cloned.linear_colorspace_ = linear_colorspace_; ++ cloned.data_.resize(width_ * height_); ++ for (size_t i = 0; i < cloned.data_.size(); ++i) { ++ cloned.data_[i] = ++ std::round(.2126f * data_[3 * i + 0] + .7152f * data_[3 * i + 1] + ++ .0722f * data_[3 * i + 2]); ++ } ++ cloned.meta_data_ = OIIOMetaData::Clone(meta_data_); ++ return cloned; + } + } + +@@ -691,58 +603,72 @@ Bitmap Bitmap::CloneAsRGB() const { + if (IsRGB()) { + return Clone(); + } else { +- return Bitmap(FreeImage_ConvertTo24Bits(handle_.ptr)); ++ THROW_CHECK_EQ(channels_, 1); ++ Bitmap cloned; ++ cloned.width_ = width_; ++ cloned.height_ = height_; ++ cloned.channels_ = 3; ++ cloned.linear_colorspace_ = linear_colorspace_; ++ cloned.data_.resize(width_ * height_ * 3); ++ for (size_t i = 0; i < data_.size(); ++i) { ++ cloned.data_[3 * i + 0] = data_[i]; ++ cloned.data_[3 * i + 1] = data_[i]; ++ cloned.data_[3 * i + 2] = data_[i]; ++ } ++ cloned.meta_data_ = OIIOMetaData::Clone(meta_data_); ++ return cloned; ++ } ++} ++ ++void Bitmap::SetMetaData(const std::string_view& name, ++ const std::string_view& type, ++ const void* value) { ++ THROW_CHECK_NE(type, "string"); ++ auto* meta_data = OIIOMetaData::Upcast(meta_data_.get()); ++ OIIO::TypeDesc type_desc; ++ type_desc.fromstring(OIIOFromStdStringView(type)); ++ THROW_CHECK_NE(type_desc, OIIO::TypeDesc::UNKNOWN); ++ meta_data->image_spec.attribute( ++ OIIOFromStdStringView(name), type_desc, value); ++} ++ ++void Bitmap::SetMetaData(const std::string_view& name, ++ const std::string_view& value) { ++ auto* meta_data = OIIOMetaData::Upcast(meta_data_.get()); ++ meta_data->image_spec.attribute(OIIOFromStdStringView(name), ++ OIIOFromStdStringView(value)); ++} ++ ++bool Bitmap::GetMetaData(const std::string_view& name, ++ const std::string_view& type, ++ void* value) const { ++ THROW_CHECK_NE(type, "string"); ++ auto* meta_data = OIIOMetaData::Upcast(meta_data_.get()); ++ OIIO::TypeDesc type_desc; ++ type_desc.fromstring(OIIOFromStdStringView(type)); ++ THROW_CHECK_NE(type_desc, OIIO::TypeDesc::UNKNOWN); ++ return meta_data->image_spec.getattribute( ++ OIIOFromStdStringView(name), type_desc, value); ++} ++ ++bool Bitmap::GetMetaData(const std::string_view& name, ++ std::string_view* value) const { ++ auto* meta_data = OIIOMetaData::Upcast(meta_data_.get()); ++ OIIO::ustring ustring_value; ++ if (meta_data->image_spec.getattribute( ++ OIIOFromStdStringView(name), OIIO::TypeString, &ustring_value)) { ++ *value = std::string_view(ustring_value.data(), ustring_value.size()); ++ return true; + } ++ return false; + } + + void Bitmap::CloneMetadata(Bitmap* target) const { + THROW_CHECK_NOTNULL(target); +- THROW_CHECK_NOTNULL(target->Data()); +- FreeImage_CloneMetadata(handle_.ptr, target->Data()); +-} +- +-void Bitmap::SetPtr(FIBITMAP* ptr) { +- THROW_CHECK_NOTNULL(ptr); +- +- if (!IsPtrSupported(ptr)) { +- FreeImageHandle temp_handle(ptr); +- ptr = FreeImage_ConvertTo24Bits(temp_handle.ptr); +- THROW_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; ++ target->meta_data_ = OIIOMetaData::Clone(meta_data_); ++ auto* target_meta_data = OIIOMetaData::Upcast(target->meta_data_.get()); ++ target_meta_data->image_spec.width = target->Width(); ++ target_meta_data->image_spec.height = target->Height(); + } + + std::ostream& operator<<(std::ostream& stream, const Bitmap& bitmap) { +diff --git a/src/colmap/sensor/bitmap.h b/src/colmap/sensor/bitmap.h +index 925360ba..e07d6aee 100644 +--- a/src/colmap/sensor/bitmap.h ++++ b/src/colmap/sensor/bitmap.h +@@ -36,11 +36,10 @@ + #include + #include + #include ++#include + #include + #include + +-struct FIBITMAP; +- + namespace colmap { + + // Templated bitmap color class. +@@ -65,70 +64,47 @@ struct BitmapColor { + T b; + }; + +-// Wrapper class around FreeImage bitmaps. ++// Wrapper class around bitmaps. + class Bitmap { + public: + Bitmap(); ++ Bitmap(int width, int height, bool as_rgb, bool linear_colorspace = true); + +- // 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; ++ // Number of bits per pixel. This is 8 for grey and 24 for RGB images. ++ inline int BitsPerPixel() const; ++ ++ // Number of bytes required to store image. ++ inline size_t NumBytes() const; ++ ++ // Scan line size in bytes, also known as stride. ++ inline int Pitch() const; + +- // Scan width of bitmap which differs from the actual image width to achieve +- // 32 bit aligned memory. Also known as stride. +- unsigned int Pitch() const; ++ // Check whether the image is empty (i.e., width/height=0). ++ inline bool IsEmpty() 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 ConvertToRowMajorArray() const; +- std::vector ConvertToColMajorArray() const; +- +- // Convert to/from raw bits. +- std::vector ConvertToRawBits() const; +- static Bitmap ConvertFromRawBits( +- const uint8_t* data, int pitch, int width, int height, bool rgb = true); ++ // Access raw image data array. ++ inline std::vector& RowMajorData(); ++ inline const std::vector& RowMajorData() 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; ++ inline bool GetPixel(int x, int y, BitmapColor* color) const; ++ inline bool SetPixel(int x, int y, const BitmapColor& color); + + // Fill entire bitmap with uniform color. For grayscale images, the first + // element of the vector is used. +@@ -148,15 +124,15 @@ class Bitmap { + 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; ++ // Read bitmap at given path and convert to grey- or colorscale. Defaults to ++ // linearizing the colorspace for image processing. ++ bool Read(const std::string& path, ++ bool as_rgb = true, ++ bool linearize_colorspace = false); + +- // Smooth the image using a Gaussian kernel. +- void Smooth(float sigma_x, float sigma_y); ++ // Write bitmap to file at given path. If the bitmap is linearized, write it ++ // de-linearized to the file in sRGB. ++ bool Write(const std::string& path, bool delinearize_colorspace = true) const; + + // Rescale image to the new dimensions. + enum class RescaleFilter { +@@ -172,27 +148,30 @@ class Bitmap { + Bitmap CloneAsGrey() const; + Bitmap CloneAsRGB() const; + ++ // Access metadata information (EXIF). ++ void SetMetaData(const std::string_view& name, ++ const std::string_view& type, ++ const void* value); ++ void SetMetaData(const std::string_view& name, const std::string_view& value); ++ bool GetMetaData(const std::string_view& name, ++ const std::string_view& type, ++ void* value) const; ++ bool GetMetaData(const std::string_view& name, std::string_view* value) 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; ++ struct MetaData { ++ virtual ~MetaData() = default; + }; + +- void SetPtr(FIBITMAP* ptr); +- +- FreeImageHandle handle_; ++ private: + int width_; + int height_; + int channels_; ++ bool linear_colorspace_; ++ std::vector data_; ++ std::unique_ptr meta_data_; + }; + + std::ostream& operator<<(std::ostream& stream, const Bitmap& bitmap); +@@ -264,15 +243,70 @@ std::ostream& operator<<(std::ostream& output, const BitmapColor& color) { + 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_; } + ++size_t Bitmap::NumBytes() const { return data_.size(); } ++ ++int Bitmap::BitsPerPixel() const { return channels_ * 8; } ++ ++int Bitmap::Pitch() const { return width_ * channels_; } ++ ++bool Bitmap::IsEmpty() const { return NumBytes() == 0; } ++ + bool Bitmap::IsRGB() const { return channels_ == 3; } + + bool Bitmap::IsGrey() const { return channels_ == 1; } + ++std::vector& Bitmap::RowMajorData() { return data_; } ++ ++const std::vector& Bitmap::RowMajorData() const { return data_; } ++ ++bool Bitmap::GetPixel(const int x, ++ const int y, ++ BitmapColor* color) const { ++ if (x < 0 || x >= width_ || y < 0 || y >= height_) { ++ return false; ++ } ++ ++ if (IsGrey()) { ++ color->r = data_[y * width_ + x]; ++ color->g = color->r; ++ color->b = color->r; ++ return true; ++ } else if (IsRGB()) { ++ const uint8_t* pixel = &data_[(y * width_ + x) * channels_]; ++ color->r = pixel[0]; ++ color->g = pixel[1]; ++ color->b = pixel[2]; ++ 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; ++ } ++ ++ if (IsGrey()) { ++ data_[y * width_ + x] = color.r; ++ return true; ++ } else if (IsRGB()) { ++ uint8_t* pixel = &data_[(y * width_ + x) * channels_]; ++ pixel[0] = color.r; ++ pixel[1] = color.g; ++ pixel[2] = color.b; ++ return true; ++ } ++ ++ return false; ++} ++ + } // namespace colmap +diff --git a/src/colmap/sensor/bitmap_test.cc b/src/colmap/sensor/bitmap_test.cc +index 0a1c787a..09998aac 100644 +--- a/src/colmap/sensor/bitmap_test.cc ++++ b/src/colmap/sensor/bitmap_test.cc +@@ -31,7 +31,7 @@ + + #include "colmap/util/testing.h" + +-#include ++#include + #include + + namespace colmap { +@@ -53,6 +53,13 @@ TEST(Bitmap, BitmapGrayColor) { + EXPECT_EQ(color.b, 5); + } + ++TEST(Bitmap, BitmapRGBColor) { ++ BitmapColor color(1, 2, 3); ++ EXPECT_EQ(color.r, 1); ++ EXPECT_EQ(color.g, 2); ++ EXPECT_EQ(color.b, 3); ++} ++ + TEST(Bitmap, BitmapColorCast) { + BitmapColor color1(1.1f, 2.9f, -3.0f); + BitmapColor color2 = color1.Cast(); +@@ -68,285 +75,125 @@ TEST(Bitmap, Empty) { + EXPECT_EQ(bitmap.Channels(), 0); + EXPECT_FALSE(bitmap.IsRGB()); + EXPECT_FALSE(bitmap.IsGrey()); ++ EXPECT_TRUE(bitmap.IsEmpty()); + } + + TEST(Bitmap, Print) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, true); ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); + std::ostringstream stream; + stream << bitmap; +- EXPECT_EQ(stream.str(), "Bitmap(width=100, height=100, channels=3)"); ++ EXPECT_EQ(stream.str(), "Bitmap(width=100, height=80, channels=3)"); + } + + TEST(Bitmap, AllocateRGB) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, true); ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); + EXPECT_EQ(bitmap.Width(), 100); +- EXPECT_EQ(bitmap.Height(), 100); ++ EXPECT_EQ(bitmap.Height(), 80); + EXPECT_EQ(bitmap.Channels(), 3); + EXPECT_TRUE(bitmap.IsRGB()); + EXPECT_FALSE(bitmap.IsGrey()); ++ EXPECT_FALSE(bitmap.IsEmpty()); + } + + TEST(Bitmap, AllocateGrey) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, false); ++ Bitmap bitmap(100, 80, /*as_rgb=*/false); + EXPECT_EQ(bitmap.Width(), 100); +- EXPECT_EQ(bitmap.Height(), 100); ++ EXPECT_EQ(bitmap.Height(), 80); + 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()); ++ EXPECT_FALSE(bitmap.IsEmpty()); + } + + TEST(Bitmap, MoveConstruct) { +- Bitmap bitmap; +- bitmap.Allocate(2, 1, true); +- const auto* data = bitmap.Data(); ++ Bitmap bitmap(2, 1, /*as_rgb=*/true); + 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 bitmap(2, 1, /*as_rgb=*/true); + 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); ++ Bitmap bitmap(1, 1, /*as_rgb=*/true); + EXPECT_EQ(bitmap.BitsPerPixel(), 24); +- bitmap.Allocate(100, 100, false); ++ bitmap = Bitmap(1, 1, /*as_rgb=*/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); ++ bitmap = Bitmap(100, 80, /*as_rgb=*/true); ++ EXPECT_EQ(bitmap.NumBytes(), 3 * 100 * 80); ++ bitmap = Bitmap(100, 80, /*as_rgb=*/false); ++ EXPECT_EQ(bitmap.NumBytes(), 100 * 80); + } + +-TEST(Bitmap, ConvertToRowMajorArrayRGB) { +- Bitmap bitmap; +- bitmap.Allocate(2, 2, true); ++TEST(Bitmap, RowMajorDataRGB) { ++ Bitmap bitmap(2, 3, /*as_rgb=*/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(); +- ASSERT_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(); +- ASSERT_EQ(array.size(), 4); +- EXPECT_EQ(array[0], 0); +- EXPECT_EQ(array[1], 2); +- EXPECT_EQ(array[2], 1); +- EXPECT_EQ(array[3], 3); ++ bitmap.SetPixel(0, 2, BitmapColor(2, 0, 0)); ++ bitmap.SetPixel(1, 0, BitmapColor(3, 0, 0)); ++ bitmap.SetPixel(1, 1, BitmapColor(4, 0, 0)); ++ bitmap.SetPixel(1, 2, BitmapColor(5, 0, 0)); ++ EXPECT_THAT(bitmap.RowMajorData(), ++ testing::ElementsAre( ++ 0, 0, 0, 3, 0, 0, 1, 0, 0, 4, 0, 0, 2, 0, 0, 5, 0, 0)); + } + +-TEST(Bitmap, ConvertToColMajorArrayRGB) { +- Bitmap bitmap; +- bitmap.Allocate(2, 2, true); ++TEST(Bitmap, RowMajorDataGrey) { ++ Bitmap bitmap(2, 3, /*as_rgb=*/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(); +- ASSERT_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(); +- ASSERT_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, ConvertToFromRawBitsGrey) { +- Bitmap bitmap; +- bitmap.Allocate(3, 2, 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)); +- +- std::vector raw_bits = bitmap.ConvertToRawBits(); +- ASSERT_EQ(raw_bits.size(), bitmap.Pitch() * bitmap.Height()); +- +- const std::vector raw_bits_copy = raw_bits; +- Bitmap bitmap_copy = Bitmap::ConvertFromRawBits(raw_bits.data(), +- bitmap.Pitch(), +- bitmap.Width(), +- bitmap.Height(), +- /*rgb=*/false); +- EXPECT_EQ(bitmap.Width(), bitmap_copy.Width()); +- EXPECT_EQ(bitmap.Height(), bitmap_copy.Height()); +- EXPECT_EQ(bitmap.Channels(), bitmap_copy.Channels()); +- bitmap.SetPixel(0, 1, BitmapColor(5)); +- bitmap_copy.SetPixel(0, 1, BitmapColor(5)); +- EXPECT_EQ(raw_bits_copy, raw_bits); +- EXPECT_EQ(bitmap.ConvertToRowMajorArray(), +- bitmap_copy.ConvertToRowMajorArray()); +-} +- +-TEST(Bitmap, ConvertToFromRawBitsRGB) { +- Bitmap bitmap; +- bitmap.Allocate(3, 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)); +- +- std::vector raw_bits = bitmap.ConvertToRawBits(); +- ASSERT_EQ(raw_bits.size(), bitmap.Pitch() * bitmap.Height() * 3); +- +- const std::vector raw_bits_copy = raw_bits; +- Bitmap bitmap_copy = Bitmap::ConvertFromRawBits(raw_bits.data(), +- bitmap.Pitch(), +- bitmap.Width(), +- bitmap.Height(), +- /*rgb=*/true); +- EXPECT_EQ(bitmap.Width(), bitmap_copy.Width()); +- EXPECT_EQ(bitmap.Height(), bitmap_copy.Height()); +- EXPECT_EQ(bitmap.Channels(), bitmap_copy.Channels()); +- bitmap.SetPixel(0, 1, BitmapColor(5, 0, 0)); +- bitmap_copy.SetPixel(0, 1, BitmapColor(5, 0, 0)); +- EXPECT_EQ(raw_bits_copy, raw_bits); +- EXPECT_EQ(bitmap.ConvertToRowMajorArray(), +- bitmap_copy.ConvertToRowMajorArray()); ++ bitmap.SetPixel(0, 2, BitmapColor(2, 0, 0)); ++ bitmap.SetPixel(1, 0, BitmapColor(3, 0, 0)); ++ bitmap.SetPixel(1, 1, BitmapColor(4, 0, 0)); ++ bitmap.SetPixel(1, 2, BitmapColor(5, 0, 0)); ++ EXPECT_THAT(bitmap.RowMajorData(), testing::ElementsAre(0, 3, 1, 4, 2, 5)); + } + + TEST(Bitmap, GetAndSetPixelRGB) { +- Bitmap bitmap; +- bitmap.Allocate(1, 1, true); +- bitmap.SetPixel(0, 0, BitmapColor(1, 2, 3)); ++ Bitmap bitmap(2, 3, /*as_rgb=*/true); ++ bitmap.SetPixel(1, 1, BitmapColor(1, 2, 3)); + BitmapColor color; +- EXPECT_TRUE(bitmap.GetPixel(0, 0, &color)); ++ EXPECT_TRUE(bitmap.GetPixel(1, 1, &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)); ++ Bitmap bitmap(2, 3, /*as_rgb=*/false); ++ bitmap.SetPixel(1, 1, BitmapColor(0, 2, 3)); + BitmapColor color; +- EXPECT_TRUE(bitmap.GetPixel(0, 0, &color)); ++ EXPECT_TRUE(bitmap.GetPixel(1, 1, &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); +- } +- } ++ bitmap.SetPixel(1, 1, BitmapColor(1, 2, 3)); ++ EXPECT_TRUE(bitmap.GetPixel(1, 1, &color)); ++ EXPECT_EQ(color, BitmapColor(1, 1, 1)); + } + + TEST(Bitmap, Fill) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, true); ++ Bitmap bitmap(100, 100, /*as_rgb=*/true); + bitmap.Fill(BitmapColor(1, 2, 3)); + for (int y = 0; y < bitmap.Height(); ++y) { + for (int x = 0; x < bitmap.Width(); ++x) { +@@ -358,76 +205,35 @@ TEST(Bitmap, Fill) { + } + + TEST(Bitmap, InterpolateNearestNeighbor) { +- Bitmap bitmap; +- bitmap.Allocate(11, 11, true); ++ Bitmap bitmap(11, 10, /*as_rgb=*/true); + bitmap.Fill(BitmapColor(0, 0, 0)); +- bitmap.SetPixel(5, 5, BitmapColor(1, 2, 3)); ++ bitmap.SetPixel(5, 4, BitmapColor(1, 2, 3)); + BitmapColor color; +- EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5, 5, &color)); ++ EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5, 4, &color)); + EXPECT_EQ(color, BitmapColor(1, 2, 3)); +- EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5.4999, 5.4999, &color)); ++ EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5.4999, 4.4999, &color)); + EXPECT_EQ(color, BitmapColor(1, 2, 3)); +- EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5.5, 5.5, &color)); ++ EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(5.5, 4.5, &color)); + EXPECT_EQ(color, BitmapColor(0, 0, 0)); +- EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(4.5, 5.4999, &color)); ++ EXPECT_TRUE(bitmap.InterpolateNearestNeighbor(4.5, 4.4999, &color)); + EXPECT_EQ(color, BitmapColor(1, 2, 3)); + } + + TEST(Bitmap, InterpolateBilinear) { +- Bitmap bitmap; +- bitmap.Allocate(11, 11, true); ++ Bitmap bitmap(11, 10, /*as_rgb=*/true); + bitmap.Fill(BitmapColor(0, 0, 0)); +- bitmap.SetPixel(5, 5, BitmapColor(1, 2, 3)); ++ bitmap.SetPixel(5, 4, BitmapColor(1, 2, 3)); + BitmapColor color; +- EXPECT_TRUE(bitmap.InterpolateBilinear(5, 5, &color)); ++ EXPECT_TRUE(bitmap.InterpolateBilinear(5, 4, &color)); + EXPECT_EQ(color, BitmapColor(1, 2, 3)); +- EXPECT_TRUE(bitmap.InterpolateBilinear(5.5, 5, &color)); ++ EXPECT_TRUE(bitmap.InterpolateBilinear(5.5, 4, &color)); + EXPECT_EQ(color, BitmapColor(0.5, 1, 1.5)); +- EXPECT_TRUE(bitmap.InterpolateBilinear(5.5, 5.5, &color)); ++ EXPECT_TRUE(bitmap.InterpolateBilinear(5.5, 4.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 bitmap(100, 80, /*as_rgb=*/true); + Bitmap bitmap1 = bitmap.Clone(); + bitmap1.Rescale(50, 25); + EXPECT_EQ(bitmap1.Width(), 50); +@@ -441,8 +247,7 @@ TEST(Bitmap, RescaleRGB) { + } + + TEST(Bitmap, RescaleGrey) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, false); ++ Bitmap bitmap(100, 80, /*as_rgb=*/false); + Bitmap bitmap1 = bitmap.Clone(); + bitmap1.Rescale(50, 25); + EXPECT_EQ(bitmap1.Width(), 50); +@@ -456,38 +261,191 @@ TEST(Bitmap, RescaleGrey) { + } + + TEST(Bitmap, Clone) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, true); ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ bitmap.Fill(BitmapColor(0, 0, 0)); ++ bitmap.SetPixel(0, 0, BitmapColor(10, 20, 30)); + const Bitmap cloned_bitmap = bitmap.Clone(); + EXPECT_EQ(cloned_bitmap.Width(), 100); +- EXPECT_EQ(cloned_bitmap.Height(), 100); ++ EXPECT_EQ(cloned_bitmap.Height(), 80); + EXPECT_EQ(cloned_bitmap.Channels(), 3); +- EXPECT_NE(bitmap.Data(), cloned_bitmap.Data()); ++ BitmapColor color; ++ EXPECT_TRUE(cloned_bitmap.GetPixel(0, 0, &color)); ++ EXPECT_EQ(color, BitmapColor(10, 20, 30)); + } + + TEST(Bitmap, CloneAsRGB) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, false); ++ Bitmap bitmap(100, 80, /*as_rgb=*/false); ++ bitmap.Fill(BitmapColor(0, 0, 0)); ++ bitmap.SetPixel(0, 0, BitmapColor(10, 0, 0)); + const Bitmap cloned_bitmap = bitmap.CloneAsRGB(); + EXPECT_EQ(cloned_bitmap.Width(), 100); +- EXPECT_EQ(cloned_bitmap.Height(), 100); ++ EXPECT_EQ(cloned_bitmap.Height(), 80); + EXPECT_EQ(cloned_bitmap.Channels(), 3); +- EXPECT_NE(bitmap.Data(), cloned_bitmap.Data()); ++ BitmapColor color; ++ EXPECT_TRUE(cloned_bitmap.GetPixel(0, 0, &color)); ++ EXPECT_EQ(color, BitmapColor(10, 10, 10)); + } + + TEST(Bitmap, CloneAsGrey) { +- Bitmap bitmap; +- bitmap.Allocate(100, 100, true); ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ bitmap.Fill(BitmapColor(0, 0, 0)); ++ bitmap.SetPixel(0, 0, BitmapColor(10, 20, 30)); + const Bitmap cloned_bitmap = bitmap.CloneAsGrey(); + EXPECT_EQ(cloned_bitmap.Width(), 100); +- EXPECT_EQ(cloned_bitmap.Height(), 100); ++ EXPECT_EQ(cloned_bitmap.Height(), 80); + EXPECT_EQ(cloned_bitmap.Channels(), 1); +- EXPECT_NE(bitmap.Data(), cloned_bitmap.Data()); ++ BitmapColor color; ++ EXPECT_TRUE(cloned_bitmap.GetPixel(0, 0, &color)); ++ EXPECT_EQ(color, BitmapColor(19, 19, 19)); ++} ++ ++TEST(Bitmap, SetGetMetaData) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ const float kValue = 1.f; ++ bitmap.SetMetaData("foobar", "float", &kValue); ++ float value = 0.f; ++ EXPECT_TRUE(bitmap.GetMetaData("foobar", "float", &value)); ++ EXPECT_EQ(value, kValue); ++ EXPECT_FALSE(bitmap.GetMetaData("does_not_exist", "float", &value)); ++ EXPECT_FALSE(bitmap.GetMetaData("foobar", "int8", &value)); ++} ++ ++TEST(Bitmap, CloneMetaData) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ const float kValue = 1.f; ++ bitmap.SetMetaData("foobar", "float", &kValue); ++ ++ Bitmap bitmap2(100, 80, /*as_rgb=*/true); ++ float value = 0.f; ++ EXPECT_FALSE(bitmap2.GetMetaData("foobar", "float", &value)); ++ bitmap.CloneMetadata(&bitmap2); ++ EXPECT_TRUE(bitmap2.GetMetaData("foobar", "float", &value)); ++ EXPECT_EQ(value, kValue); ++} ++ ++TEST(Bitmap, ExifCameraModel) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ ++ std::string camera_model; ++ EXPECT_FALSE(bitmap.ExifCameraModel(&camera_model)); ++ ++ bitmap.SetMetaData("Make", "make"); ++ bitmap.SetMetaData("Model", "model"); ++ const float focal_length_in_35mm_film = 50.f; ++ bitmap.SetMetaData( ++ "Exif:FocalLengthIn35mmFilm", "float", &focal_length_in_35mm_film); ++ ++ EXPECT_TRUE(bitmap.ExifCameraModel(&camera_model)); ++ EXPECT_EQ(camera_model, "make-model-50.000000-100x80"); ++} ++ ++TEST(Bitmap, ExifFocalLengthIn35mm) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ ++ double focal_length = 0.0; ++ EXPECT_FALSE(bitmap.ExifFocalLength(&focal_length)); ++ ++ const float focal_length_in_35mm_film = 70.f; ++ bitmap.SetMetaData( ++ "Exif:FocalLengthIn35mmFilm", "float", &focal_length_in_35mm_film); ++ ++ EXPECT_TRUE(bitmap.ExifFocalLength(&focal_length)); ++ EXPECT_NEAR(focal_length, 207.17, 0.1); ++} ++ ++TEST(Bitmap, ExifFocalLengthWithPlane) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ ++ double focal_length = 0.0; ++ EXPECT_FALSE(bitmap.ExifFocalLength(&focal_length)); ++ ++ const float kFocalLengthVal = 72.f; ++ bitmap.SetMetaData("Exif:FocalLength", "float", &kFocalLengthVal); ++ bitmap.SetMetaData("Make", "canon"); ++ bitmap.SetMetaData("Model", "eos1dsmarkiii"); ++ ++ EXPECT_TRUE(bitmap.ExifFocalLength(&focal_length)); ++ EXPECT_EQ(focal_length, 200); ++} ++ ++TEST(Bitmap, ExifFocalLengthWithDatabaseLookup) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ ++ double focal_length = 0.0; ++ EXPECT_FALSE(bitmap.ExifFocalLength(&focal_length)); ++ ++ const float kFocalLengthVal = 120.f; ++ bitmap.SetMetaData("Exif:FocalLength", "float", &kFocalLengthVal); ++ const int kPixelXDim = 100; ++ bitmap.SetMetaData("Exif:PixelXDimension", "int", &kPixelXDim); ++ const float kPlaneXRes = 1.f; ++ bitmap.SetMetaData("Exif:FocalPlaneXResolution", "float", &kPlaneXRes); ++ const int kPlanResUnit = 4; ++ bitmap.SetMetaData("Exif:FocalPlaneResolutionUnit", "int", &kPlanResUnit); ++ ++ EXPECT_TRUE(bitmap.ExifFocalLength(&focal_length)); ++ EXPECT_EQ(focal_length, 120); ++} ++ ++TEST(Bitmap, ExifLatitude) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ ++ double latitude = 0.0; ++ EXPECT_FALSE(bitmap.ExifLatitude(&latitude)); ++ ++ bitmap.SetMetaData("GPS:LatitudeRef", "N"); ++ const float kDegMinSec[3] = {46, 30, 900}; ++ bitmap.SetMetaData("GPS:Latitude", "point", kDegMinSec); ++ ++ EXPECT_TRUE(bitmap.ExifLatitude(&latitude)); ++ EXPECT_EQ(latitude, 46.75); ++ ++ bitmap.SetMetaData("GPS:LatitudeRef", "S"); ++ ++ EXPECT_TRUE(bitmap.ExifLatitude(&latitude)); ++ EXPECT_EQ(latitude, -46.75); ++} ++ ++TEST(Bitmap, ExifLongitude) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ ++ double longitude = 0.0; ++ EXPECT_FALSE(bitmap.ExifLongitude(&longitude)); ++ ++ bitmap.SetMetaData("GPS:LongitudeRef", "W"); ++ const float kDegMinSec[3] = {92, 30, 900}; ++ bitmap.SetMetaData("GPS:Longitude", "point", kDegMinSec); ++ ++ EXPECT_TRUE(bitmap.ExifLongitude(&longitude)); ++ EXPECT_EQ(longitude, 92.75); ++ ++ bitmap.SetMetaData("GPS:LongitudeRef", "E"); ++ ++ EXPECT_TRUE(bitmap.ExifLongitude(&longitude)); ++ EXPECT_EQ(longitude, -92.75); ++} ++ ++TEST(Bitmap, ExifAltitude) { ++ Bitmap bitmap(100, 80, /*as_rgb=*/true); ++ ++ double altitude = 0.0; ++ EXPECT_FALSE(bitmap.ExifAltitude(&altitude)); ++ ++ bitmap.SetMetaData("GPS:AltitudeRef", "0"); ++ const float kAltitudeVal = 123.456; ++ bitmap.SetMetaData("GPS:Altitude", "float", &kAltitudeVal); ++ ++ EXPECT_TRUE(bitmap.ExifAltitude(&altitude)); ++ EXPECT_EQ(altitude, kAltitudeVal); ++ ++ bitmap.SetMetaData("GPS:AltitudeRef", "1"); ++ ++ EXPECT_TRUE(bitmap.ExifAltitude(&altitude)); ++ EXPECT_EQ(altitude, -kAltitudeVal); + } + + TEST(Bitmap, ReadWriteAsRGB) { +- Bitmap bitmap; +- bitmap.Allocate(2, 3, true); ++ Bitmap bitmap(2, 3, /*as_rgb=*/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)); +@@ -501,30 +459,23 @@ TEST(Bitmap, ReadWriteAsRGB) { + 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_EQ(read_bitmap.RowMajorData(), bitmap.RowMajorData()); + + 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()); ++ EXPECT_EQ(read_bitmap.RowMajorData(), bitmap.CloneAsGrey().RowMajorData()); + } + + TEST(Bitmap, ReadWriteAsGrey) { +- Bitmap bitmap; +- bitmap.Allocate(2, 3, false); ++ Bitmap bitmap(2, 3, /*as_rgb=*/false); + bitmap.SetPixel(0, 0, BitmapColor(0)); + bitmap.SetPixel(0, 1, BitmapColor(1)); + bitmap.SetPixel(1, 0, BitmapColor(2)); +@@ -538,73 +489,92 @@ TEST(Bitmap, ReadWriteAsGrey) { + 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_EQ(read_bitmap.RowMajorData(), bitmap.CloneAsRGB().RowMajorData()); + + 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()); ++ EXPECT_EQ(read_bitmap.RowMajorData(), bitmap.RowMajorData()); + } + +-TEST(Bitmap, ReadRGB16AsGrey) { +- 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)); ++TEST(Bitmap, ReadWriteAsGreyNonLinear) { ++ Bitmap bitmap(2, 3, /*as_rgb=*/false, /*linear_colorspace=*/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"; + +- // Bitmap class does not support 16 bit color depth +- FIBITMAP* converted_rgb16 = FreeImage_ConvertToType(bitmap.Data(), FIT_RGB16); +- EXPECT_TRUE(converted_rgb16); +- EXPECT_TRUE(FreeImage_Save(FIF_PNG, converted_rgb16, filename.c_str())); +- FreeImage_Unload(converted_rgb16); +- +- // Assert the file was written correctly with 16 bit color depth +- FIBITMAP* written_image = FreeImage_Load(FIF_PNG, filename.c_str()); +- EXPECT_TRUE(written_image); +- EXPECT_EQ(FreeImage_GetBPP(written_image), 48); +- FreeImage_Unload(written_image); ++ EXPECT_TRUE(bitmap.Write(filename, /*delinearize_colorspace=*/false)); + + 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_TRUE(read_bitmap.Read( ++ filename, /*as_rgb=*/false, /*linearize_colorspace=*/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()); +-} ++ EXPECT_EQ(read_bitmap.RowMajorData(), bitmap.RowMajorData()); ++} ++ ++// TEST(Bitmap, ReadRGB16AsGrey) { ++// Bitmap bitmap; ++// bitmap.Allocate(2, 3, /*as_rgb=*/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"; ++ ++// // Bitmap class does not support 16 bit color depth ++// FIBITMAP* converted_rgb16 = FreeImage_ConvertToType(bitmap.Data(), ++// FIT_RGB16); EXPECT_TRUE(converted_rgb16); ++// EXPECT_TRUE(FreeImage_Save(FIF_PNG, converted_rgb16, filename.c_str())); ++// FreeImage_Unload(converted_rgb16); ++ ++// // Assert the file was written correctly with 16 bit color depth ++// FIBITMAP* written_image = FreeImage_Load(FIF_PNG, filename.c_str()); ++// EXPECT_TRUE(written_image); ++// EXPECT_EQ(FreeImage_GetBPP(written_image), 48); ++// FreeImage_Unload(written_image); ++ ++// Bitmap read_bitmap; ++ ++// // Allocate bitmap with different size to test read overwrites existing ++// data. read_bitmap.Allocate(bitmap.Width() + 1, bitmap.Height() + 2, ++// /*as_rgb=*/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.RowMajorData(), ++// bitmap.RowMajorData()); ++ ++// 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.RowMajorData(), ++// bitmap.CloneAsGrey().RowMajorData()); ++// } + + } // namespace + } // namespace colmap +diff --git a/src/colmap/sensor/database.cc b/src/colmap/sensor/database.cc +index ab83c83e..03de6e9c 100644 +--- a/src/colmap/sensor/database.cc ++++ b/src/colmap/sensor/database.cc +@@ -37,7 +37,7 @@ const camera_specs_t CameraDatabase::specs_ = InitializeCameraSpecs(); + + bool CameraDatabase::QuerySensorWidth(const std::string& make, + const std::string& model, +- double* sensor_width) { ++ double* sensor_width_mm) { + // Clean the strings from all separators. + std::string cleaned_make = make; + std::string cleaned_model = model; +@@ -60,7 +60,7 @@ bool CameraDatabase::QuerySensorWidth(const std::string& 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; ++ *sensor_width_mm = model_elem.second; + if (cleaned_model == model_elem.first) { + // Model exactly matches, return immediately. + return true; +diff --git a/src/colmap/sensor/database.h b/src/colmap/sensor/database.h +index 93af5226..08a86102 100644 +--- a/src/colmap/sensor/database.h ++++ b/src/colmap/sensor/database.h +@@ -45,7 +45,7 @@ struct CameraDatabase { + + bool QuerySensorWidth(const std::string& make, + const std::string& model, +- double* sensor_width); ++ double* sensor_width_mm); + + private: + static const camera_specs_t specs_; +diff --git a/src/colmap/sfm/incremental_mapper.cc b/src/colmap/sfm/incremental_mapper.cc +index 2a054cdd..671f2a8c 100644 +--- a/src/colmap/sfm/incremental_mapper.cc ++++ b/src/colmap/sfm/incremental_mapper.cc +@@ -693,9 +693,8 @@ IncrementalMapper::AdjustLocalBundle( + num_frames_per_rig[frame.RigId()] += 1; + } + for (const auto& [rig_id, num_frames] : num_frames_per_rig) { +- const size_t num_reg_frames_for_rig = +- reg_stats_.num_reg_frames_per_rig.at(rig_id); +- if (num_frames < num_reg_frames_for_rig) { ++ if (options.constant_rigs.count(rig_id) || ++ num_frames < reg_stats_.num_reg_frames_per_rig.at(rig_id)) { + const Rig& rig = reconstruction_->Rig(rig_id); + for (const auto& [sensor_id, _] : rig.Sensors()) { + ba_config.SetConstantSensorFromRigPose(sensor_id); +@@ -708,14 +707,11 @@ IncrementalMapper::AdjustLocalBundle( + num_images_per_camera.reserve(ba_config.NumImages()); + for (const image_t image_id : ba_config.Images()) { + const Image& image = reconstruction_->Image(image_id); +- num_frames_per_rig[image.FramePtr()->RigId()] += 1; + num_images_per_camera[image.CameraId()] += 1; + } + for (const auto& [camera_id, num_images] : num_images_per_camera) { +- const size_t num_reg_images_for_camera = +- reg_stats_.num_reg_images_per_camera.at(camera_id); + if (options.constant_cameras.count(camera_id) || +- num_images < num_reg_images_for_camera) { ++ num_images < reg_stats_.num_reg_images_per_camera.at(camera_id)) { + ba_config.SetConstantCamIntrinsics(camera_id); + } + } +@@ -812,6 +808,13 @@ bool IncrementalMapper::AdjustGlobalBundle( + } + } + ++ for (const auto& rig_id : options.constant_rigs) { ++ const Rig& rig = reconstruction_->Rig(rig_id); ++ for (const auto& [sensor_id, _] : rig.Sensors()) { ++ ba_config.SetConstantSensorFromRigPose(sensor_id); ++ } ++ } ++ + for (const auto& camera_id : options.constant_cameras) { + ba_config.SetConstantCamIntrinsics(camera_id); + } +diff --git a/src/colmap/sfm/incremental_mapper.h b/src/colmap/sfm/incremental_mapper.h +index f56cdc96..20fd0f46 100644 +--- a/src/colmap/sfm/incremental_mapper.h ++++ b/src/colmap/sfm/incremental_mapper.h +@@ -118,6 +118,10 @@ class IncrementalMapper { + // If reconstruction is provided as input, fix the existing image poses. + bool fix_existing_frames = false; + ++ // List of rigs for which to fix the sensor_from_rig transformation, ++ // independent of ba_refine_sensor_from_rig. ++ std::unordered_set constant_rigs; ++ + // List of cameras for which to fix the camera parameters independent + // of refine_focal_length, refine_principal_point, and refine_extra_params. + std::unordered_set constant_cameras; +diff --git a/src/colmap/ui/CMakeLists.txt b/src/colmap/ui/CMakeLists.txt +index 591f3682..38e923ee 100644 +--- a/src/colmap/ui/CMakeLists.txt ++++ b/src/colmap/ui/CMakeLists.txt +@@ -30,6 +30,17 @@ + + set(FOLDER_NAME "ui") + ++set(COLMAP_UI_QT_LIBS ++ Qt${QT_VERSION_MAJOR}::Core ++ Qt${QT_VERSION_MAJOR}::OpenGL ++ Qt${QT_VERSION_MAJOR}::Widgets ++) ++ ++if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) ++ list(APPEND COLMAP_UI_QT_LIBS ++ Qt${QT_VERSION_MAJOR}::OpenGLWidgets) ++endif() ++ + COLMAP_ADD_LIBRARY( + NAME colmap_ui + SRCS +@@ -67,7 +78,5 @@ COLMAP_ADD_LIBRARY( + colmap_image + colmap_scene + colmap_controllers +- Qt5::Core +- Qt5::OpenGL +- Qt5::Widgets ++ ${COLMAP_UI_QT_LIBS} + ) +diff --git a/src/colmap/ui/feature_extraction_widget.cc b/src/colmap/ui/feature_extraction_widget.cc +index 0357a677..b9a6369b 100644 +--- a/src/colmap/ui/feature_extraction_widget.cc ++++ b/src/colmap/ui/feature_extraction_widget.cc +@@ -79,12 +79,12 @@ SIFTExtractionWidget::SIFTExtractionWidget(QWidget* parent, + AddOptionFilePath(&options->image_reader->camera_mask_path, + "camera_mask_path"); + ++ AddOptionInt(&options->feature_extraction->max_image_size, "max_image_size"); + AddOptionInt(&options->feature_extraction->num_threads, "num_threads", -1); + AddOptionBool(&options->feature_extraction->use_gpu, "use_gpu"); + AddOptionText(&options->feature_extraction->gpu_index, "gpu_index"); + + SiftExtractionOptions& sift_options = *options->feature_extraction->sift; +- AddOptionInt(&sift_options.max_image_size, "sift.max_image_size"); + AddOptionInt(&sift_options.max_num_features, "sift.max_num_features"); + AddOptionInt(&sift_options.first_octave, "sift.first_octave", -5); + AddOptionInt(&sift_options.num_octaves, "sift.num_octaves"); +@@ -115,6 +115,7 @@ void SIFTExtractionWidget::Run() { + + ImageReaderOptions reader_options = *options_->image_reader; + reader_options.image_path = *options_->image_path; ++ reader_options.as_rgb = options_->feature_extraction->RequiresRGB(); + + auto extractor = CreateFeatureExtractorController( + *options_->database_path, reader_options, *options_->feature_extraction); +diff --git a/src/colmap/ui/image_viewer_widget.cc b/src/colmap/ui/image_viewer_widget.cc +index c743351c..9b86723a 100644 +--- a/src/colmap/ui/image_viewer_widget.cc ++++ b/src/colmap/ui/image_viewer_widget.cc +@@ -120,8 +120,9 @@ void ImageViewerWidget::ShowPixmap(const QPixmap& pixmap) { + + void ImageViewerWidget::ReadAndShow(const std::string& path) { + Bitmap bitmap; +- if (!bitmap.Read(path, true)) { ++ if (!bitmap.Read(path, /*as_rgb=*/true, /*linearize=*/false)) { + LOG(ERROR) << "Cannot read image at path " << path; ++ return; + } + + ShowBitmap(bitmap); +@@ -167,14 +168,14 @@ FeatureImageViewerWidget::FeatureImageViewerWidget( + 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)) { ++ if (!bitmap.Read(path, /*as_rgb=*/true, /*linearize=*/false)) { + LOG(ERROR) << "Cannot read image at path " << path; ++ return; + } + + image1_ = QPixmap::fromImage(BitmapToQImageRGB(bitmap)); +@@ -215,7 +216,8 @@ void FeatureImageViewerWidget::ReadAndShowWithMatches( + const FeatureMatches& matches) { + Bitmap bitmap1; + Bitmap bitmap2; +- if (!bitmap1.Read(path1, true) || !bitmap2.Read(path2, true)) { ++ if (!bitmap1.Read(path1, /*as_rgb=*/true, /*linearize=*/false) || ++ !bitmap2.Read(path2, /*as_rgb=*/true, /*linearize=*/false)) { + LOG(ERROR) << "Cannot read images at paths " << path1 << " and " << path2; + return; + } +diff --git a/src/colmap/ui/match_matrix_widget.cc b/src/colmap/ui/match_matrix_widget.cc +index 07dcab5f..7298ca60 100644 +--- a/src/colmap/ui/match_matrix_widget.cc ++++ b/src/colmap/ui/match_matrix_widget.cc +@@ -52,8 +52,7 @@ void MatchMatrixWidget::Show() { + }); + + // Allocate the match matrix image. +- Bitmap match_matrix; +- match_matrix.Allocate(images.size(), images.size(), true); ++ Bitmap match_matrix(images.size(), images.size(), true); + match_matrix.Fill(BitmapColor(255)); + + // Map image identifiers to match matrix locations. +diff --git a/src/colmap/ui/model_viewer_widget.cc b/src/colmap/ui/model_viewer_widget.cc +index c7015c91..a51e9d7f 100644 +--- a/src/colmap/ui/model_viewer_widget.cc ++++ b/src/colmap/ui/model_viewer_widget.cc +@@ -734,7 +734,11 @@ QImage ModelViewerWidget::GrabImage() { + + EnableCoordinateGrid(); + ++#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) ++ return image.flipped(Qt::Orientation::Vertical); ++#else + return image.mirrored(); ++#endif + } + + void ModelViewerWidget::GrabMovie() { movie_grabber_widget_->show(); } +diff --git a/src/colmap/ui/model_viewer_widget.h b/src/colmap/ui/model_viewer_widget.h +index e2a64715..b56618cd 100644 +--- a/src/colmap/ui/model_viewer_widget.h ++++ b/src/colmap/ui/model_viewer_widget.h +@@ -44,6 +44,9 @@ + #include + #include + #include ++#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) ++#include ++#endif + + namespace colmap { + +diff --git a/src/colmap/ui/options_widget.cc b/src/colmap/ui/options_widget.cc +index 2ea0ea84..17cd1b6d 100644 +--- a/src/colmap/ui/options_widget.cc ++++ b/src/colmap/ui/options_widget.cc +@@ -93,6 +93,7 @@ QSpinBox* OptionsWidget::AddOptionInt(int* option, + QSpinBox* spinbox = new QSpinBox(this); + spinbox->setMinimum(min); + spinbox->setMaximum(max); ++ spinbox->setValue(*option); + + AddOptionRow(label_text, spinbox, option); + +@@ -112,6 +113,7 @@ QDoubleSpinBox* OptionsWidget::AddOptionDouble(double* option, + spinbox->setMaximum(max); + spinbox->setSingleStep(step); + spinbox->setDecimals(decimals); ++ spinbox->setValue(*option); + + AddOptionRow(label_text, spinbox, option); + +@@ -131,6 +133,7 @@ QDoubleSpinBox* OptionsWidget::AddOptionDoubleLog(double* option, + spinbox->setMaximum(max); + spinbox->setSingleStep(step); + spinbox->setDecimals(decimals); ++ spinbox->setValue(*option); + + AddOptionRow(label_text, spinbox, option); + +@@ -142,6 +145,7 @@ QDoubleSpinBox* OptionsWidget::AddOptionDoubleLog(double* option, + QCheckBox* OptionsWidget::AddOptionBool(bool* option, + const std::string& label_text) { + QCheckBox* checkbox = new QCheckBox(this); ++ checkbox->setChecked(*option); + + AddOptionRow(label_text, checkbox, option); + +@@ -153,6 +157,7 @@ QCheckBox* OptionsWidget::AddOptionBool(bool* option, + QLineEdit* OptionsWidget::AddOptionText(std::string* option, + const std::string& label_text) { + QLineEdit* line_edit = new QLineEdit(this); ++ line_edit->setText(QString::fromStdString(*option)); + + AddOptionRow(label_text, line_edit, option); + +@@ -164,6 +169,7 @@ QLineEdit* OptionsWidget::AddOptionText(std::string* option, + QLineEdit* OptionsWidget::AddOptionFilePath(std::string* option, + const std::string& label_text) { + QLineEdit* line_edit = new QLineEdit(this); ++ line_edit->setText(QString::fromStdString(*option)); + + AddOptionRow(label_text, line_edit, option); + +@@ -184,6 +190,7 @@ QLineEdit* OptionsWidget::AddOptionFilePath(std::string* option, + QLineEdit* OptionsWidget::AddOptionDirPath(std::string* option, + const std::string& label_text) { + QLineEdit* line_edit = new QLineEdit(this); ++ line_edit->setText(QString::fromStdString(*option)); + + AddOptionRow(label_text, line_edit, option); + +diff --git a/src/colmap/ui/point_viewer_widget.cc b/src/colmap/ui/point_viewer_widget.cc +index dbb08051..2ce58fa0 100644 +--- a/src/colmap/ui/point_viewer_widget.cc ++++ b/src/colmap/ui/point_viewer_widget.cc +@@ -192,7 +192,7 @@ void PointViewerWidget::Show(const point3D_t point3D_id) { + + Bitmap bitmap; + const std::string path = JoinPaths(*options_->image_path, image.Name()); +- if (!bitmap.Read(path, true)) { ++ if (!bitmap.Read(path, /*as_rgb=*/true, /*linearize=*/false)) { + LOG(ERROR) << "Cannot read image at path " << path; + continue; + } +diff --git a/src/colmap/ui/qt_utils.cc b/src/colmap/ui/qt_utils.cc +index 340da967..86f2e8e2 100644 +--- a/src/colmap/ui/qt_utils.cc ++++ b/src/colmap/ui/qt_utils.cc +@@ -57,7 +57,7 @@ QMatrix4x4 EigenToQMatrix(const Eigen::Matrix4f& matrix) { + 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); ++ QRgb* image_line = reinterpret_cast(image.scanLine(y)); + for (int x = 0; x < image.width(); ++x) { + BitmapColor color; + if (bitmap.GetPixel(x, y, &color)) { +diff --git a/src/colmap/util/CMakeLists.txt b/src/colmap/util/CMakeLists.txt +index 6399bbbb..49260dba 100644 +--- a/src/colmap/util/CMakeLists.txt ++++ b/src/colmap/util/CMakeLists.txt +@@ -79,7 +79,15 @@ if(DOWNLOAD_ENABLED) + endif() + endif() + if(GUI_ENABLED) +- target_link_libraries(colmap_util PUBLIC Qt5::Core Qt5::OpenGL OpenGL::GL) ++ set(COLMAP_UTIL_QT_LIBS ++ Qt${QT_VERSION_MAJOR}::Core ++ Qt${QT_VERSION_MAJOR}::OpenGL ++ ) ++ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) ++ list(APPEND COLMAP_UTIL_QT_LIBS ++ Qt${QT_VERSION_MAJOR}::OpenGLWidgets) ++ endif() ++ target_link_libraries(colmap_util PUBLIC ${COLMAP_UTIL_QT_LIBS} OpenGL::GL) + endif() + + if(CUDA_ENABLED) +diff --git a/src/pycolmap/feature/extraction.cc b/src/pycolmap/feature/extraction.cc +index c5d228c8..481b6710 100644 +--- a/src/pycolmap/feature/extraction.cc ++++ b/src/pycolmap/feature/extraction.cc +@@ -44,9 +44,9 @@ class Sift { + "settings, explicitly specify them, because the defaults " + "will change in the next major release.", + 1); ++ options_.max_image_size = 7000; + options_.sift->peak_threshold = 0.01; + options_.sift->first_octave = 0; +- options_.sift->max_image_size = 7000; + } + options_.use_gpu = use_gpu_; + THROW_CHECK(options_.Check()); +@@ -54,15 +54,11 @@ class Sift { + } + + sift_output_t Extract(const Eigen::Ref>& image) { +- THROW_CHECK_LE(image.rows(), options_.sift->max_image_size); +- THROW_CHECK_LE(image.cols(), options_.sift->max_image_size); ++ THROW_CHECK_LE(image.rows(), options_.max_image_size); ++ THROW_CHECK_LE(image.cols(), options_.max_image_size); + +- const Bitmap bitmap = +- Bitmap::ConvertFromRawBits(const_cast(image.data()), +- /*pitch=*/image.cols(), +- /*width=*/image.cols(), +- /*height=*/image.rows(), +- /*rgb=*/false); ++ Bitmap bitmap(image.cols(), image.rows(), /*as_rgb=*/false); ++ std::memcpy(bitmap.RowMajorData().data(), image.data(), bitmap.NumBytes()); + + FeatureKeypoints keypoints_; + FeatureDescriptors descriptors_; +@@ -117,10 +113,6 @@ void BindFeatureExtraction(py::module& m) { + auto PySiftExtractionOptions = + py::classh(m, "SiftExtractionOptions") + .def(py::init<>()) +- .def_readwrite( +- "max_image_size", +- &SiftExtractionOptions::max_image_size, +- "Maximum image size, otherwise image will be down-scaled.") + .def_readwrite("max_num_features", + &SiftExtractionOptions::max_num_features, + "Maximum number of features to detect, keeping " +@@ -173,6 +165,10 @@ void BindFeatureExtraction(py::module& m) { + auto PyFeatureExtractionOptions = + py::classh(m, "FeatureExtractionOptions") + .def(py::init<>()) ++ .def_readwrite( ++ "max_image_size", ++ &FeatureExtractionOptions::max_image_size, ++ "Maximum image size, otherwise image will be down-scaled.") + .def_readwrite("num_threads", + &FeatureExtractionOptions::num_threads, + "Number of threads for feature matching and " +diff --git a/src/pycolmap/sensor/bitmap.cc b/src/pycolmap/sensor/bitmap.cc +index 7e28ea9d..42d4873b 100644 +--- a/src/pycolmap/sensor/bitmap.cc ++++ b/src/pycolmap/sensor/bitmap.cc +@@ -36,28 +36,8 @@ void BindBitmap(pybind11::module& m) { + py::buffer_info output_into = output.request(); + uint8_t* output_row_ptr = + reinterpret_cast(output.request().ptr); +- const size_t output_pitch = output_into.shape[1] * channels; +- for (ssize_t y = 0; y < output_into.shape[0]; ++y) { +- if (is_rgb) { +- for (ssize_t x = 0; x < output_into.shape[1]; ++x) { +- // Notice that the underlying FreeImage buffer may order +- // the channels as BGR or in any other format and with +- // different striding, so we have to set each pixel +- // separately. +- // We always return the array in the order R, G, B. +- BitmapColor color; +- THROW_CHECK(self.GetPixel(x, y, &color)); +- output_row_ptr[3 * x] = color.r; +- output_row_ptr[3 * x + 1] = color.g; +- output_row_ptr[3 * x + 2] = color.b; +- } +- } else { +- // Copy (guaranteed contiguous) row memory directly. +- std::memcpy( +- output_row_ptr, self.GetScanline(y), output_into.shape[1]); +- } +- output_row_ptr += output_pitch; +- } ++ std::memcpy( ++ output_row_ptr, self.RowMajorData().data(), self.NumBytes()); + return output; + }) + .def_static( +@@ -75,47 +55,23 @@ void BindBitmap(pybind11::module& m) { + const int height = array.shape(0); + if (width == 0 || height == 0) { + throw std::runtime_error( +- "Input array must have positive width and height"); ++ "Input array must have positive width and height!"); + } + +- if (channels != 1 && channels != 3 && channels != 4) { ++ if (channels != 1 && channels != 3) { + throw std::runtime_error( +- "Input array must have 1, 3, or 4 channels!"); ++ "Input array must have 1 or 3 channels!"); + } + + const bool is_rgb = channels != 1; +- const size_t pitch = width * channels; + +- Bitmap output; +- output.Allocate(width, height, is_rgb); ++ Bitmap output(width, height, is_rgb); + + const uint8_t* input_row_ptr = + static_cast(array.request().ptr); + +- for (int y = 0; y < height; ++y) { +- if (is_rgb) { +- for (int x = 0; x < width; ++x) { +- // We assume that provided array dimensions are R, G, B. +- // Notice that the underlying FreeImage buffer may order +- // the channels as BGR or in any other format and with +- // different striding, so we have to set each pixel +- // separately. +- output.SetPixel( +- x, +- y, +- BitmapColor(input_row_ptr[channels * x], +- input_row_ptr[channels * x + 1], +- input_row_ptr[channels * x + 2])); +- } +- } else { +- // Copy (guaranteed contiguous) row memory directly. +- std::memcpy(const_cast(output.GetScanline(y)), +- input_row_ptr, +- width); +- } +- +- input_row_ptr += pitch; +- } ++ std::memcpy( ++ output.RowMajorData().data(), input_row_ptr, output.NumBytes()); + + return output; + }, +@@ -126,7 +82,7 @@ void BindBitmap(pybind11::module& m) { + .def("write", + &Bitmap::Write, + "path"_a, +- "flags"_a = 0, ++ "delinearize_colorspace"_a, + "Write bitmap to file.") + .def("__repr__", &CreateRepresentation) + .def_static( +diff --git a/src/pycolmap/sfm/incremental_mapper.cc b/src/pycolmap/sfm/incremental_mapper.cc +index e31c9ac8..6b80b1ef 100644 +--- a/src/pycolmap/sfm/incremental_mapper.cc ++++ b/src/pycolmap/sfm/incremental_mapper.cc +@@ -183,6 +183,11 @@ void BindIncrementalPipeline(py::module& m) { + &Opts::fix_existing_frames, + "If reconstruction is provided as input, fix the existing " + "frame poses.") ++ .def_readwrite( ++ "constant_rigs", ++ &Opts::constant_rigs, ++ "List of rigs for which to fix the sensor_from_rig transformation, " ++ "independent of ba_refine_sensor_from_rig.") + .def_readwrite("constant_cameras", + &Opts::constant_cameras, + "List of cameras for which to fix the camera parameters " +@@ -344,6 +349,11 @@ void BindIncrementalMapperOptions(py::module& m) { + &Opts::fix_existing_frames, + "If reconstruction is provided as input, fix the existing " + "frame poses.") ++ .def_readwrite( ++ "constant_rigs", ++ &Opts::constant_rigs, ++ "List of rigs for which to fix the sensor_from_rig transformation, " ++ "independent of ba_refine_sensor_from_rig.") + .def_readwrite("constant_cameras", + &Opts::constant_cameras, + "List of cameras for which to fix the camera parameters " +diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json +new file mode 100644 +index 00000000..ce7d86ba +--- /dev/null ++++ b/vcpkg-configuration.json +@@ -0,0 +1,7 @@ ++{ ++ "default-registry": { ++ "kind": "git", ++ "repository": "https://github.com/Microsoft/vcpkg", ++ "baseline": "912567f6fdd1883e07b070dcc0aa67cec072042a" ++ } ++} +diff --git a/vcpkg.json b/vcpkg.json +index 6adbe3a6..dfc330a2 100644 +--- a/vcpkg.json ++++ b/vcpkg.json +@@ -20,7 +20,6 @@ + ] + }, + "eigen3", +- "freeimage", + "gflags", + "glog", + { +@@ -28,6 +27,7 @@ + "default-features": false + }, + "metis", ++ "openimageio", + "sqlite3", + { + "name": "vcpkg-cmake", +@@ -38,6 +38,16 @@ + "host": true + } + ], ++ "overrides": [ ++ { ++ "name": "metis", ++ "version": "2022-07-27" ++ }, ++ { ++ "name": "gklib", ++ "version": "2023-03-27" ++ } ++ ], + "default-features": [ + "gui", + "download" +@@ -60,7 +70,7 @@ + "description": "Build with GUI support.", + "dependencies": [ + "glew", +- "qt5-base" ++ "qtbase" + ] + }, + "cgal": { +@@ -75,7 +85,7 @@ + { + "name": "curl", + "features": [ +- "schannel" ++ "sspi" + ], + "platform": "windows" + }, +@@ -97,4 +107,4 @@ + ] + } + } +-} +\ No newline at end of file ++} diff --git a/pkgs/by-name/co/colmap/package.nix b/pkgs/by-name/co/colmap/package.nix index 18f17a3ae551..ed97fb1b4e5d 100644 --- a/pkgs/by-name/co/colmap/package.nix +++ b/pkgs/by-name/co/colmap/package.nix @@ -7,7 +7,7 @@ boost, ceres-solver, eigen, - freeimage, + openimageio, glog, libGLU, glew, @@ -26,6 +26,7 @@ cudaCapabilities ? cudaPackages.flags.cudaCapabilities, cudaPackages, faiss, + sqlite, llvmPackages, gtest, }: @@ -35,19 +36,47 @@ assert cudaSupport -> cudaPackages != { }; let stdenv' = if cudaSupport then cudaPackages.backendStdenv else stdenv; + depsAlsoForPycolmap = [ + boost + eigen + ceres-solver + openimageio + glog + libGLU + glew + cgal + poselib + faiss + sqlite + gmp + mpfr + lz4 + qt5.qtbase + ] + ++ lib.optionals cudaSupport [ + cudatoolkit + cudaPackages.cuda_cudart.static + ] + ++ lib.optional stdenv'.cc.isClang llvmPackages.openmp; + # TODO: migrate to redist packages inherit (cudaPackages) cudatoolkit; in stdenv'.mkDerivation rec { - version = "3.12.5"; + version = "unstable-3.12.5-openimageio"; pname = "colmap"; src = fetchFromGitHub { owner = "colmap"; repo = "colmap"; - rev = version; - hash = "sha256-ngmEYCLeCh5pSNmXItV3siY6/DupEHK+dYZ56LWZbhg="; + rev = "f8edccaa36909713b9d3930e1ca65cb364a38b26"; + hash = "sha256-0lD7ywM48ODe11u9D3XSk9btqQ4gs/APBFf9IyiXe6g="; }; + # TODO: remove this when https://github.com/colmap/colmap/pull/3459 is in a release + # This was produced with: + # git diff f8edccaa36909713b9d3930e1ca65cb364a38b26 e40c0730020938587c9d4eb7634cbff93cbc2f81 + patches = [ ./openimageio.patch ]; + cmakeFlags = [ (lib.cmakeBool "DOWNLOAD_ENABLED" false) (lib.cmakeBool "UNINSTALL_ENABLED" false) @@ -66,7 +95,7 @@ stdenv'.mkDerivation rec { boost ceres-solver eigen - freeimage + openimageio glog libGLU glew @@ -77,14 +106,8 @@ stdenv'.mkDerivation rec { gmp mpfr xorg.libSM - poselib - faiss ] - ++ lib.optionals cudaSupport [ - cudatoolkit - cudaPackages.cuda_cudart.static - ] - ++ lib.optional stdenv'.cc.isClang llvmPackages.openmp; + ++ depsAlsoForPycolmap; nativeBuildInputs = [ cmake @@ -95,7 +118,7 @@ stdenv'.mkDerivation rec { autoAddDriverRunpath ]; - passthru.updateScript = gitUpdater { }; + passthru.depsAlsoForPycolmap = depsAlsoForPycolmap; meta = with lib; { description = "Structure-From-Motion and Multi-View Stereo pipeline"; @@ -110,6 +133,7 @@ stdenv'.mkDerivation rec { maintainers = with maintainers; [ lebastr usertam + chpatrick ]; }; }