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*num_images=*/int,
+- /*with_masks=*/bool,
+- /*with_existing_images=*/bool>> {};
++ : 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