diff --git a/doc/release-notes/rl-2605.section.md b/doc/release-notes/rl-2605.section.md
index d12b075f7791..54c1f3d750c5 100644
--- a/doc/release-notes/rl-2605.section.md
+++ b/doc/release-notes/rl-2605.section.md
@@ -165,6 +165,10 @@
- The `programs.captive-browser` module no longer falls back on a setcap wrapper around udhcpc to discover your network's DNS server due to [GHSA-wc3r-c66x-8xmc](https://github.com/NixOS/nixpkgs/security/advisories/GHSA-wc3r-c66x-8xmc) (CVE-2026-25740). If you're using this module, you must either configure `programs.captive-browser.dhcp-dns` manually or enable one of NetworkManager, dhcpcd, or systemd-networkd.
+- NetBox was updated to `>= 4.5.5`. Have a look at the breaking changes
+ of the [4.5 release](https://github.com/netbox-community/netbox/releases/tag/v4.5.0),
+ make the required changes to your database, if needed, then upgrade by setting `services.netbox.package = pkgs.netbox_4_5;` in your configuration.
+
- The `services.yggdrasil` module has been refactored with the following breaking changes:
- The `services.yggdrasil.configFile` option has been removed. Configuration should now be specified directly via `services.yggdrasil.settings`.
- The `services.yggdrasil.persistentKeys` option has been removed. To maintain persistent keys and IPv6 addresses across reboots, use `services.yggdrasil.settings.PrivateKeyPath` to securely load your private key from a file via systemd credentials. The private key must be in PEM format (PKCS #8).
@@ -219,6 +223,8 @@
- `services.taskchampion-sync-server` module have been added an option `services.taskchampion-sync-server.dynamicUser` to use systemd's DynamicUser feature. This is enabled by default when stateVersion is at least 26.05, and disabled otherwise. If you need this feature, you need to set `services.taskchampion-sync-server.dynamicUser` to `true` and migrate `/var/lib/taskchampion-sync-server` to `/var/lib/private/taskchampion-sync-server`.
+- The default packages in `services.jenkins.packages` have been dropped, since not every Jenkins installation needs any package at all. It's more reasonable to leave it empty and let users configure what they need.
+
## Other Notable Changes {#sec-nixpkgs-release-26.05-notable-changes}
diff --git a/nixos/doc/manual/configuration/profiles.chapter.md b/nixos/doc/manual/configuration/profiles.chapter.md
index b9131978947b..dda65add46d9 100644
--- a/nixos/doc/manual/configuration/profiles.chapter.md
+++ b/nixos/doc/manual/configuration/profiles.chapter.md
@@ -24,7 +24,6 @@ profiles/clone-config.section.md
profiles/demo.section.md
profiles/docker-container.section.md
profiles/graphical.section.md
-profiles/hardened.section.md
profiles/headless.section.md
profiles/installation-device.section.md
profiles/perlless.section.md
diff --git a/nixos/doc/manual/configuration/profiles/hardened.section.md b/nixos/doc/manual/configuration/profiles/hardened.section.md
deleted file mode 100644
index 2e9bb196c054..000000000000
--- a/nixos/doc/manual/configuration/profiles/hardened.section.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Hardened {#sec-profile-hardened}
-
-A profile with most (vanilla) hardening options enabled by default,
-potentially at the cost of stability, features and performance.
-
-This includes a hardened kernel, and limiting the system information
-available to processes through the `/sys` and
-`/proc` filesystems. It also disables the User Namespaces
-feature of the kernel, which stops Nix from being able to build anything
-(this particular setting can be overridden via
-[](#opt-security.allowUserNamespaces)). See the
-[profile source](https://github.com/nixos/nixpkgs/tree/master/nixos/modules/profiles/hardened.nix)
-for further detail on which settings are altered.
-
-::: {.warning}
-This profile enables options that are known to affect system
-stability. If you experience any stability issues when using the
-profile, try disabling it. If you report an issue and use this
-profile, always mention that you do.
-:::
diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json
index 7d8c3382175d..44ec155fa438 100644
--- a/nixos/doc/manual/redirects.json
+++ b/nixos/doc/manual/redirects.json
@@ -1728,7 +1728,8 @@
"sec-profile-graphical": [
"index.html#sec-profile-graphical"
],
- "sec-profile-hardened": [
+ "sec-release-26.05-incompatibilities-profiles-hardened-removed": [
+ "release-notes.html#sec-release-26.05-incompatibilities-profiles-hardened-removed",
"index.html#sec-profile-hardened"
],
"sec-profile-headless": [
diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md
index b984a0cc8378..8f678afb58d6 100644
--- a/nixos/doc/manual/release-notes/rl-2605.section.md
+++ b/nixos/doc/manual/release-notes/rl-2605.section.md
@@ -88,6 +88,12 @@
- `opentrack`, `slushload`, `synthesia`, `vtfedit`, `winbox`, `wineasio`, and `yabridge` use wineWow64Packages instead of wineWowPackages as wine versions >= 11.0 have deprecated wineWowPackages. As such, the prefixes for these packages are NOT backwards compatible and need to be regenerated with potential for data loss.
+- []{#sec-release-26.05-incompatibilities-profiles-hardened-removed} `profiles/hardened` has been removed, because:
+ - It lacks a consistent and transparent baseline or standard,
+ - It may introduce unexpected breakage or degrade performance without clear benefit,
+ - It is difficult to manage user expectations, especially since the implications of enabling it are not always obvious,
+ - and as multiple contributors have noted, it is often more of a “grab bag” of settings than a cohesive security policy.
+
- `services.crabfit` was removed because its upstream packages are unmaintained and insecure.
- `sing-box` has been updated to 1.13.0, which has removed some deprecated options. See [upstream documentation](https://sing-box.sagernet.org/configuration/) for details and migration options.
diff --git a/nixos/modules/profiles/hardened.nix b/nixos/modules/profiles/hardened.nix
index 8a7e98781717..070fac9a3465 100644
--- a/nixos/modules/profiles/hardened.nix
+++ b/nixos/modules/profiles/hardened.nix
@@ -1,136 +1,11 @@
-# A profile with most (vanilla) hardening options enabled by default,
-# potentially at the cost of stability, features and performance.
-#
-# This profile enables options that are known to affect system
-# stability. If you experience any stability issues when using the
-# profile, try disabling it. If you report an issue and use this
-# profile, always mention that you do.
+# This profile included most standard hardening options enabled by default,
+# which may have impacted system stability, feature availability, and performance.
+{ lib, ... }:
{
- config,
- lib,
- pkgs,
- ...
-}:
-let
- inherit (lib)
- mkDefault
- mkOverride
- mkEnableOption
- mkIf
- maintainers
- ;
-in
-{
- options.profiles.hardened = mkEnableOption "hardened" // {
- default = true;
- example = false;
- };
- config = mkIf config.profiles.hardened {
- meta = {
- maintainers = [
- maintainers.emily
- ];
- };
-
- boot.kernelPackages = mkDefault pkgs.linuxKernel.packages.linux_hardened;
-
- nix.settings.allowed-users = mkDefault [ "@users" ];
-
- environment.memoryAllocator.provider = mkDefault "scudo";
- environment.variables.SCUDO_OPTIONS = mkDefault "zero_contents=true";
-
- security.lockKernelModules = mkDefault true;
-
- security.protectKernelImage = mkDefault true;
-
- security.allowSimultaneousMultithreading = mkDefault false;
-
- security.forcePageTableIsolation = mkDefault true;
-
- # This is required by podman to run containers in rootless mode.
- security.unprivilegedUsernsClone = mkDefault config.virtualisation.containers.enable;
-
- security.virtualisation.flushL1DataCache = mkDefault "always";
-
- security.apparmor.enable = mkDefault true;
- security.apparmor.killUnconfinedConfinables = mkDefault true;
-
- boot.kernelParams = [
- # Don't merge slabs
- "slab_nomerge"
-
- # Overwrite free'd pages
- "page_poison=1"
-
- # Enable page allocator randomization
- "page_alloc.shuffle=1"
-
- # Disable debugfs
- "debugfs=off"
- ];
-
- boot.blacklistedKernelModules = [
- # Obscure network protocols
- "ax25"
- "netrom"
- "rose"
-
- # Old or rare or insufficiently audited filesystems
- "adfs"
- "affs"
- "bfs"
- "befs"
- "cramfs"
- "efs"
- "erofs"
- "exofs"
- "freevxfs"
- "f2fs"
- "hfs"
- "hpfs"
- "jfs"
- "minix"
- "nilfs2"
- "ntfs"
- "omfs"
- "qnx4"
- "qnx6"
- "sysv"
- "ufs"
- ];
-
- # Hide kptrs even for processes with CAP_SYSLOG
- boot.kernel.sysctl."kernel.kptr_restrict" = mkOverride 500 2;
-
- # Disable bpf() JIT (to eliminate spray attacks)
- boot.kernel.sysctl."net.core.bpf_jit_enable" = mkDefault false;
-
- # Disable ftrace debugging
- boot.kernel.sysctl."kernel.ftrace_enabled" = mkDefault false;
-
- # Enable strict reverse path filtering (that is, do not attempt to route
- # packets that "obviously" do not belong to the iface's network; dropped
- # packets are logged as martians).
- boot.kernel.sysctl."net.ipv4.conf.all.log_martians" = mkDefault true;
- boot.kernel.sysctl."net.ipv4.conf.all.rp_filter" = mkDefault "1";
- boot.kernel.sysctl."net.ipv4.conf.default.log_martians" = mkDefault true;
- boot.kernel.sysctl."net.ipv4.conf.default.rp_filter" = mkDefault "1";
-
- # Ignore broadcast ICMP (mitigate SMURF)
- boot.kernel.sysctl."net.ipv4.icmp_echo_ignore_broadcasts" = mkDefault true;
-
- # Ignore incoming ICMP redirects (note: default is needed to ensure that the
- # setting is applied to interfaces added after the sysctls are set)
- boot.kernel.sysctl."net.ipv4.conf.all.accept_redirects" = mkDefault false;
- boot.kernel.sysctl."net.ipv4.conf.all.secure_redirects" = mkDefault false;
- boot.kernel.sysctl."net.ipv4.conf.default.accept_redirects" = mkDefault false;
- boot.kernel.sysctl."net.ipv4.conf.default.secure_redirects" = mkDefault false;
- boot.kernel.sysctl."net.ipv6.conf.all.accept_redirects" = mkDefault false;
- boot.kernel.sysctl."net.ipv6.conf.default.accept_redirects" = mkDefault false;
-
- # Ignore outgoing ICMP redirects (this is ipv4 only)
- boot.kernel.sysctl."net.ipv4.conf.all.send_redirects" = mkDefault false;
- boot.kernel.sysctl."net.ipv4.conf.default.send_redirects" = mkDefault false;
- };
+ imports = [
+ (lib.mkRemovedOptionModule [ "profiles" "hardened" ] ''
+ The hardened profile has been removed, see the backward incompatibilities section of the 26.05 release notes for more information.
+ '')
+ ];
}
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix
index 15421ef2f532..d4657fcab8aa 100644
--- a/nixos/modules/services/continuous-integration/jenkins/default.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/default.nix
@@ -86,14 +86,16 @@ in
javaPackage = lib.mkPackageOption pkgs "jdk25" { };
packages = lib.mkOption {
- default = [
- pkgs.stdenv
- pkgs.git
- pkgs.jdk25
- config.programs.ssh.package
- pkgs.nix
- ];
- defaultText = lib.literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ]";
+ default = [ ];
+ example = lib.literalExpression ''
+ [
+ pkgs.stdenv
+ pkgs.git
+ pkgs.jdk25
+ config.programs.ssh.package
+ pkgs.nix
+ ]
+ '';
type = lib.types.listOf lib.types.package;
description = ''
Packages to add to PATH for the jenkins process.
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
index f49b8271928d..d380ab60bc41 100644
--- a/nixos/modules/services/web-apps/netbox.nix
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -119,11 +119,11 @@ in
package = lib.mkOption {
type = lib.types.package;
default =
- if lib.versionAtLeast config.system.stateVersion "25.11" then pkgs.netbox_4_4 else pkgs.netbox_4_2;
+ if lib.versionAtLeast config.system.stateVersion "26.05" then pkgs.netbox_4_5 else pkgs.netbox_4_4;
defaultText = lib.literalExpression ''
- if lib.versionAtLeast config.system.stateVersion "25.11"
- then pkgs.netbox_4_4
- else pkgs.netbox_4_2;
+ if lib.versionAtLeast config.system.stateVersion "26.05"
+ then pkgs.netbox_4_5
+ else pkgs.netbox_4_4;
'';
description = ''
NetBox package to use.
@@ -165,6 +165,15 @@ in
'';
};
+ apiTokenPeppersFile = lib.mkOption {
+ type = with lib.types; nullOr path;
+ description = ''
+ Path to a file containing the API_TOKEN_PEPPER that will be configured for the pepper_id with the id 1.
+ This is required for netbox >= 4.5. For generating this pepper,
+ [consider using `$INSTALL_ROOT/netbox/generate_secret_key.py`](https://netboxlabs.com/docs/netbox/configuration/required-parameters/#api_token_peppers)
+ '';
+ };
+
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
@@ -237,10 +246,12 @@ in
GIT_PATH = "${pkgs.gitMinimal}/bin/git";
- DATABASE = {
- NAME = "netbox";
- USER = "netbox";
- HOST = "/run/postgresql";
+ DATABASES = {
+ "default" = {
+ NAME = "netbox";
+ USER = "netbox";
+ HOST = "/run/postgresql";
+ };
};
# Redis database settings. Redis is used for caching and for queuing
@@ -283,6 +294,12 @@ in
with open("${cfg.secretKeyFile}", "r") as file:
SECRET_KEY = file.readline()
''
+ + (lib.optionalString (cfg.apiTokenPeppersFile != null) ''
+ with open("${cfg.apiTokenPeppersFile}", "r") as pepper_file:
+ API_TOKEN_PEPPERS = {
+ 1: pepper_file.readline(),
+ };
+ '')
+ (lib.optionalString (cfg.keycloakClientSecret != null) ''
with open("${cfg.keycloakClientSecret}", "r") as file:
SOCIAL_AUTH_KEYCLOAK_SECRET = file.readline()
@@ -446,5 +463,12 @@ in
};
users.groups.netbox = { };
users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ];
+
+ assertions = [
+ {
+ assertion = (lib.versionAtLeast cfg.package.version "4.5") -> (cfg.apiTokenPeppersFile != null);
+ message = "NetBox 4.5 or newer require setting `services.netbox.apiTokenPeppersFile`";
+ }
+ ];
};
}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 32c49bf3a0e9..01533d4ea935 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -701,7 +701,6 @@ in
package = pkgs.hadoop_3_3;
};
haproxy = runTest ./haproxy.nix;
- hardened = runTest ./hardened.nix;
harmonia = runTest ./harmonia.nix;
haste-server = runTest ./haste-server.nix;
hbase2 = runTest {
@@ -1054,9 +1053,8 @@ in
neo4j = runTest ./neo4j.nix;
netbird = runTest ./netbird.nix;
netbox-upgrade = runTest ./web-apps/netbox-upgrade.nix;
- netbox_4_2 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_2; };
- netbox_4_3 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_3; };
netbox_4_4 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_4; };
+ netbox_4_5 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_5; };
netdata = runTest ./netdata.nix;
networking.networkd = handleTest ./networking/networkd-and-scripted.nix { networkd = true; };
networking.networkmanager = handleTest ./networking/networkmanager.nix { };
diff --git a/nixos/tests/hardened.nix b/nixos/tests/hardened.nix
deleted file mode 100644
index ac0cedc98dd2..000000000000
--- a/nixos/tests/hardened.nix
+++ /dev/null
@@ -1,113 +0,0 @@
-{ pkgs, ... }:
-{
- name = "hardened";
- meta = {
- maintainers = [ ];
- };
-
- nodes.machine =
- {
- lib,
- pkgs,
- config,
- ...
- }:
- {
- users.users.alice = {
- isNormalUser = true;
- extraGroups = [ "proc" ];
- };
- users.users.sybil = {
- isNormalUser = true;
- group = "wheel";
- };
- imports = [ ../modules/profiles/hardened.nix ];
- environment.memoryAllocator.provider = "graphene-hardened";
- nix.settings.sandbox = false;
- virtualisation.emptyDiskImages = [
- {
- size = 4096;
- driveConfig.deviceExtraOpts.serial = "deferred";
- }
- ];
- virtualisation.fileSystems = {
- "/deferred" = {
- device = "/dev/disk/by-id/virtio-deferred";
- fsType = "vfat";
- autoFormat = true;
- options = [ "noauto" ];
- };
- };
- boot.extraModulePackages = pkgs.lib.optional (pkgs.lib.versionOlder config.boot.kernelPackages.kernel.version "5.6") config.boot.kernelPackages.wireguard;
- boot.kernelModules = [ "wireguard" ];
- };
-
- testScript =
- let
- hardened-malloc-tests = pkgs.graphene-hardened-malloc.ld-preload-tests;
- in
- ''
- machine.wait_for_unit("multi-user.target")
-
-
- with subtest("AppArmor profiles are loaded"):
- machine.succeed("systemctl status apparmor.service")
-
-
- # AppArmor securityfs
- with subtest("AppArmor securityfs is mounted"):
- machine.succeed("mountpoint -q /sys/kernel/security")
- machine.succeed("cat /sys/kernel/security/apparmor/profiles")
-
-
- # Test loading out-of-tree modules
- with subtest("Out-of-tree modules can be loaded"):
- machine.succeed("grep -Fq wireguard /proc/modules")
-
-
- # Test kernel module hardening
- with subtest("No more kernel modules can be loaded"):
- # note: this better a be module we normally wouldn't load ...
- machine.wait_for_unit("disable-kernel-module-loading.service")
- machine.fail("modprobe dccp")
-
-
- # Test userns
- with subtest("User namespaces are restricted"):
- machine.succeed("unshare --user true")
- machine.fail("su -l alice -c 'unshare --user true'")
-
-
- # Test dmesg restriction
- with subtest("Regular users cannot access dmesg"):
- machine.fail("su -l alice -c dmesg")
-
-
- # Test access to kcore
- with subtest("Kcore is inaccessible as root"):
- machine.fail("cat /proc/kcore")
-
-
- # Test deferred mount
- with subtest("Deferred mounts work"):
- machine.fail("mountpoint -q /deferred") # was deferred
- machine.systemctl("start deferred.mount")
- machine.succeed("mountpoint -q /deferred") # now mounted
-
-
- # Test Nix dæmon usage
- with subtest("nix-daemon cannot be used by all users"):
- machine.fail("su -l nobody -s /bin/sh -c 'nix --extra-experimental-features nix-command ping-store'")
- machine.succeed("su -l alice -c 'nix --extra-experimental-features nix-command ping-store'")
-
-
- # Test kernel image protection
- with subtest("The kernel image is protected"):
- machine.fail("systemctl hibernate")
- machine.fail("systemctl kexec")
-
-
- with subtest("The hardened memory allocator works"):
- machine.succeed("${hardened-malloc-tests}/bin/run-tests")
- '';
-}
diff --git a/nixos/tests/initrd-secrets-changing.nix b/nixos/tests/initrd-secrets-changing.nix
index 1fa71a28872f..f0d6bc4c28b4 100644
--- a/nixos/tests/initrd-secrets-changing.nix
+++ b/nixos/tests/initrd-secrets-changing.nix
@@ -13,6 +13,7 @@ in
testing.makeTest {
name = "initrd-secrets-changing";
+ meta.broken = pkgs.stdenv.hostPlatform.isAarch64;
nodes.machine =
{ ... }:
diff --git a/nixos/tests/optee.nix b/nixos/tests/optee.nix
index f9e0bdd2f0f2..2d7c80bda78c 100644
--- a/nixos/tests/optee.nix
+++ b/nixos/tests/optee.nix
@@ -2,8 +2,9 @@
{
name = "optee";
- meta = with pkgs.lib.maintainers; {
- maintainers = [ jmbaur ];
+ meta = {
+ maintainers = with lib.maintainers; [ jmbaur ];
+ broken = pkgs.stdenv.hostPlatform.isAarch64;
};
nodes.machine =
diff --git a/nixos/tests/pulseaudio.nix b/nixos/tests/pulseaudio.nix
index b3b0837b239e..cb19a3436dc5 100644
--- a/nixos/tests/pulseaudio.nix
+++ b/nixos/tests/pulseaudio.nix
@@ -31,6 +31,7 @@ let
name = "pulseaudio${lib.optionalString fullVersion "Full"}${lib.optionalString systemWide "-systemWide"}";
meta = {
maintainers = pkgs.pulseaudio.meta.maintainers;
+ broken = pkgs.stdenv.hostPlatform.isAarch64;
};
nodes.machine =
diff --git a/nixos/tests/systemd-initrd-luks-unl0kr.nix b/nixos/tests/systemd-initrd-luks-unl0kr.nix
index f9dbce1ccdd8..74e74cd95be1 100644
--- a/nixos/tests/systemd-initrd-luks-unl0kr.nix
+++ b/nixos/tests/systemd-initrd-luks-unl0kr.nix
@@ -14,6 +14,7 @@ in
name = "systemd-initrd-luks-unl0kr";
meta = {
maintainers = [ ];
+ broken = pkgs.stdenv.hostPlatform.isAarch64;
};
# TODO: Fix OCR: #302965
diff --git a/nixos/tests/web-apps/netbox-upgrade.nix b/nixos/tests/web-apps/netbox-upgrade.nix
index 5b60e8939ccb..ee84a6b34fd8 100644
--- a/nixos/tests/web-apps/netbox-upgrade.nix
+++ b/nixos/tests/web-apps/netbox-upgrade.nix
@@ -1,7 +1,7 @@
{ lib, pkgs, ... }:
let
- oldNetbox = "netbox_4_3";
- newNetbox = "netbox_4_4";
+ oldNetbox = "netbox_4_4";
+ newNetbox = "netbox_4_5";
apiVersion =
version:
@@ -38,6 +38,9 @@ in
secretKeyFile = pkgs.writeText "secret" ''
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
'';
+ apiTokenPeppersFile = pkgs.writeText "pepper" ''
+ kp7ht*76fiQAhUi5dHfASLlYUE_S^gI^(7J^K5M!LfoH@vl&b_
+ '';
};
services.nginx = {
diff --git a/nixos/tests/web-apps/netbox/default.nix b/nixos/tests/web-apps/netbox/default.nix
index b614b965ae6d..803cf5c25ed3 100644
--- a/nixos/tests/web-apps/netbox/default.nix
+++ b/nixos/tests/web-apps/netbox/default.nix
@@ -37,6 +37,10 @@ import ../../make-test-python.nix (
secretKeyFile = pkgs.writeText "secret" ''
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
'';
+ # Value from https://netbox.readthedocs.io/en/feature/configuration/required-parameters/#api_token_peppers
+ apiTokenPeppersFile = pkgs.writeText "pepper" ''
+ kp7ht*76fiQAhUi5dHfASLlYUE_S^gI^(7J^K5M!LfoH@vl&b_
+ '';
enableLdap = true;
ldapConfigPath = pkgs.writeText "ldap_config.py" ''
@@ -155,10 +159,32 @@ import ../../make-test-python.nix (
u.set_password('netbox')
u.save()
'';
+ createToken = pkgs.writeText "create-token-v2.py" ''
+ from users.models import Token, User
+ from users.choices import TokenVersionChoices
+ u = User.objects.first()
+ t = Token.objects.create(user=u, token="0123456789abcdef0123456789abcdef01234567", version=TokenVersionChoices.V2)
+ print(t.get_auth_header_prefix())
+ '';
+ netboxVersion = netbox.version;
in
builtins.replaceStrings
- [ "\${changePassword}" "\${testUser}" "\${testPassword}" "\${testGroup}" ]
- [ "${changePassword}" "${testUser}" "${testPassword}" "${testGroup}" ]
+ [
+ "\${changePassword}"
+ "\${testUser}"
+ "\${testPassword}"
+ "\${testGroup}"
+ "\${createToken}"
+ "\${netboxVersion}"
+ ]
+ [
+ "${changePassword}"
+ "${testUser}"
+ "${testPassword}"
+ "${testGroup}"
+ "${createToken}"
+ "${netboxVersion}"
+ ]
(lib.readFile "${./testScript.py}");
}
)
diff --git a/nixos/tests/web-apps/netbox/testScript.py b/nixos/tests/web-apps/netbox/testScript.py
index f6c2c9821e12..4ff035a1f0cf 100644
--- a/nixos/tests/web-apps/netbox/testScript.py
+++ b/nixos/tests/web-apps/netbox/testScript.py
@@ -73,18 +73,7 @@ with subtest("Superuser can be created"):
"netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
)
# Django doesn't have a "clean" way of inputting the password from the command line
- machine.succeed("cat '${changePassword}' | netbox-manage shell")
-
-machine.wait_for_unit("network.target")
-
-with subtest("Home screen loads from nginx"):
- machine.succeed(
- "curl -sSfL http://localhost | grep '
Home | NetBox'"
- )
-
-with subtest("Staticfiles can be fetched"):
- machine.succeed("curl -sSfL http://localhost/static/netbox.js")
- machine.succeed("curl -sSfL http://localhost/static/docs/")
+ machine.succeed("cat '${changePassword}' | netbox-manage shell --interface python")
def login(username: str, password: str):
encoded_data = json.dumps({"username": username, "password": password})
@@ -101,8 +90,29 @@ def login(username: str, password: str):
)
return result["key"]
-with subtest("Can login"):
- auth_token = login("netbox", "netbox")
+
+netbox_version = "${netboxVersion}"
+if compare(netbox_version, '4.5.2') < 0:
+ with subtest("Can login"):
+ auth_token = login("netbox", "netbox")
+
+else:
+ with subtest("Superusertoken can be created"):
+ full_token = "0123456789abcdef0123456789abcdef01234567"
+ stdout = machine.succeed("cat ${createToken} | netbox-manage shell")
+ token_prefix = stdout.split(r"Bearer ")[-1].lstrip().rstrip()
+ auth_token = f"{token_prefix}{full_token}"
+
+machine.wait_for_unit("network.target")
+
+with subtest("Home screen loads from nginx"):
+ machine.succeed(
+ "curl -sSfL http://localhost | grep 'Home | NetBox'"
+ )
+
+with subtest("Staticfiles can be fetched"):
+ machine.succeed("curl -sSfL http://localhost/static/netbox.js")
+ machine.succeed("curl -sSfL http://localhost/static/docs/")
def get(uri: str):
return json.loads(
@@ -144,8 +154,8 @@ def post(uri: str, data: Dict[str, Any]):
def patch(uri: str, data: Dict[str, Any]):
return data_request(uri, "PATCH", data)
-# Retrieve netbox version
-netbox_version = get("/status/")["netbox-version"]
+with subtest("Can retrieve netbox version"):
+ assert netbox_version == get("/status/")["netbox-version"]
with subtest("Can create objects"):
result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
@@ -263,12 +273,15 @@ if compare(netbox_version, '4.2.0') < 0:
assert result["data"]["prefix_list"][0]["prefix"] == test_objects["prefixes"]["v4-with-updated-desc"]["prefix"]
assert int(result["data"]["prefix_list"][0]["site"]["id"]) == int(test_objects["prefixes"]["v4-with-updated-desc"]["scope"]["id"])
-with subtest("Can login with LDAP"):
- machine.wait_for_unit("openldap.service")
- login("alice", "${testPassword}")
+# With 4.5.2 and higher, obtaining a session cookie or token without supplying
+# proper CSRF tokens on the frontend /login/ endpoint is no longer possible
+if compare(netbox_version, '4.5.2') < 0:
+ with subtest("Can login with LDAP"):
+ machine.wait_for_unit("openldap.service")
+ login("alice", "${testPassword}")
-with subtest("Can associate LDAP groups"):
- result = get("/users/users/?username=${testUser}")
+ with subtest("Can associate LDAP groups"):
+ result = get("/users/users/?username=${testUser}")
- assert result["count"] == 1
- assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])
+ assert result["count"] == 1
+ assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])
diff --git a/pkgs/applications/editors/vim/plugins/non-generated/fff-nvim/default.nix b/pkgs/applications/editors/vim/plugins/non-generated/fff-nvim/default.nix
index c32464bdb4d4..244f3bf6cf96 100644
--- a/pkgs/applications/editors/vim/plugins/non-generated/fff-nvim/default.nix
+++ b/pkgs/applications/editors/vim/plugins/non-generated/fff-nvim/default.nix
@@ -11,18 +11,18 @@
vimUtils,
}:
let
- version = "1001eb8-unstable-2026-03-13";
+ version = "0.4.3-nightly.dd56a3a-unstable-2026-03-20";
src = fetchFromGitHub {
owner = "dmtrKovalenko";
repo = "fff.nvim";
- rev = "fcdf4a9172fba824ca6834731b93b74eba51d1c3";
- hash = "sha256-AYxWrqru0/HCrfTXRqTExiOT6mNzNMk+pT6APRs0BUM=";
+ rev = "dd56a3a8a8a5a85522badaf6485f28c8f7a7c840";
+ hash = "sha256-F9fnjCwBEJfuK0TuRr7XjMcacMep5K0SuzGft2IFXtQ=";
};
fff-nvim-lib = rustPlatform.buildRustPackage {
pname = "fff-nvim-lib";
inherit version src;
- cargoHash = "sha256-nXtJPE6HNZx5Ra4CwYi/f4EWww//1XwsiwtRG77RPJk=";
+ cargoHash = "sha256-Xhn+EpVF7XQOgHQpmoHHrZ/swi2xgdEFYh8D6mYJMSc=";
nativeBuildInputs = [
pkg-config
@@ -40,8 +40,6 @@ let
];
env = {
- RUSTC_BOOTSTRAP = 1; # We need rust unstable features
-
OPENSSL_NO_VENDOR = true;
# Allow undefined symbols on Darwin - they will be provided by Neovim's LuaJIT runtime
diff --git a/pkgs/applications/emulators/box64/default.nix b/pkgs/applications/emulators/box64/default.nix
index 35f9ffba24fc..683dbf2567cf 100644
--- a/pkgs/applications/emulators/box64/default.nix
+++ b/pkgs/applications/emulators/box64/default.nix
@@ -88,7 +88,10 @@ stdenv.mkDerivation (finalAttrs: {
'';
passthru = {
- updateScript = gitUpdater { rev-prefix = "v"; };
+ updateScript = gitUpdater {
+ rev-prefix = "v";
+ allowedVersions = "\\.[02468]$";
+ };
tests.hello = runCommand "box64-test-hello" { nativeBuildInputs = [ finalAttrs.finalPackage ]; } ''
BOX64_LOG=1 box64 ${lib.getExe hello-x86_64} --version 2>&1 | tee $out
'';
diff --git a/pkgs/applications/networking/cluster/terraform-providers/providers.json b/pkgs/applications/networking/cluster/terraform-providers/providers.json
index da8f4b9fb4c3..f2c1f470ce2e 100644
--- a/pkgs/applications/networking/cluster/terraform-providers/providers.json
+++ b/pkgs/applications/networking/cluster/terraform-providers/providers.json
@@ -391,11 +391,11 @@
"vendorHash": null
},
"f5networks_bigip": {
- "hash": "sha256-aUf9eRpfxzdpz6Lv36xWxSW++tSMxYvqy9xtkko/bfc=",
+ "hash": "sha256-L/qV8Ff5UhRs6r+jadBUkofLcxggBq96gCpkk0Tv3k8=",
"homepage": "https://registry.terraform.io/providers/F5Networks/bigip",
"owner": "F5Networks",
"repo": "terraform-provider-bigip",
- "rev": "v1.25.0",
+ "rev": "v1.25.1",
"spdx": "MPL-2.0",
"vendorHash": null
},
diff --git a/pkgs/by-name/as/asciijump/package.nix b/pkgs/by-name/as/asciijump/package.nix
index cb9659bbec29..06c2e34306b7 100644
--- a/pkgs/by-name/as/asciijump/package.nix
+++ b/pkgs/by-name/as/asciijump/package.nix
@@ -28,7 +28,9 @@ stdenv.mkDerivation (finalAttrs: {
strictDeps = true;
enableParallelBuilding = true;
- NIX_CFLAGS_COMPILE = [ "-fsigned-char" ];
+ env.NIX_CFLAGS_COMPILE = toString [
+ "-fsigned-char"
+ ];
nativeBuildInputs = [ ctags ];
buildInputs = [ slang ];
diff --git a/pkgs/by-name/aw/awsbck/package.nix b/pkgs/by-name/aw/awsbck/package.nix
index 9201a3508c95..90194cfb5643 100644
--- a/pkgs/by-name/aw/awsbck/package.nix
+++ b/pkgs/by-name/aw/awsbck/package.nix
@@ -6,16 +6,16 @@
rustPlatform.buildRustPackage (finalAttrs: {
pname = "awsbck";
- version = "1.0.2";
+ version = "1.0.3";
src = fetchFromGitHub {
owner = "beeb";
repo = "awsbck";
rev = "v${finalAttrs.version}";
- hash = "sha256-DU806u9MsZHB+7LmjMgrkaHG+Hi4BJ9OGKkIdDGOhz8=";
+ hash = "sha256-DZ2k0fwEyfeP24maTuxG2vmQaZuQ4LJo0KaKFTUDSYM=";
};
- cargoHash = "sha256-kgPpT79XSMGtnPkl4xrX3sddJQHcNSZlNmCcrxKqzuk=";
+ cargoHash = "sha256-wydSzfb8GAJYJPNtSyYBUHtN3+9jsGINSex2F3ILQKI=";
# tests run in CI on the source repo
doCheck = false;
diff --git a/pkgs/by-name/ca/cargo-bundle/package.nix b/pkgs/by-name/ca/cargo-bundle/package.nix
index 54eb345f941b..40ef02df4a62 100644
--- a/pkgs/by-name/ca/cargo-bundle/package.nix
+++ b/pkgs/by-name/ca/cargo-bundle/package.nix
@@ -8,7 +8,7 @@
wayland,
openssl,
squashfsTools,
- makeWrapper,
+ makeBinaryWrapper,
versionCheckHook,
nix-update-script,
}:
@@ -28,7 +28,9 @@ rustPlatform.buildRustPackage (finalAttrs: {
nativeBuildInputs = [
pkg-config
- makeWrapper
+ ]
+ ++ lib.optionals stdenv.hostPlatform.isLinux [
+ makeBinaryWrapper
];
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
@@ -37,7 +39,8 @@ rustPlatform.buildRustPackage (finalAttrs: {
openssl
];
- postFixup = ''
+ # squashfs tools are needed to build appimages for Linux
+ postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
wrapProgram $out/bin/cargo-bundle \
--prefix PATH : ${lib.makeBinPath [ squashfsTools ]}
'';
diff --git a/pkgs/by-name/cl/clairvoyant/package.nix b/pkgs/by-name/cl/clairvoyant/package.nix
index 5b77c89e452c..4a1f59df74fb 100644
--- a/pkgs/by-name/cl/clairvoyant/package.nix
+++ b/pkgs/by-name/cl/clairvoyant/package.nix
@@ -15,13 +15,13 @@
stdenv.mkDerivation (finalAttrs: {
pname = "clairvoyant";
- version = "3.1.10";
+ version = "3.1.12";
src = fetchFromGitHub {
owner = "cassidyjames";
repo = "clairvoyant";
rev = finalAttrs.version;
- hash = "sha256-CSORiNPqzliIpslV28NRPs/+bc9iblsLTPOm1WxxTjc=";
+ hash = "sha256-3TdtSa8RQkeUvw+oesHruyy7S/WnsX7FXWnDLowbEkg=";
};
nativeBuildInputs = [
@@ -47,7 +47,7 @@ stdenv.mkDerivation (finalAttrs: {
description = "Ask questions, get psychic answers";
homepage = "https://github.com/cassidyjames/clairvoyant";
license = lib.licenses.gpl3Plus;
- mainProgram = "com.github.cassidyjames.clairvoyant";
+ mainProgram = "com.cassidyjames.clairvoyant";
teams = [ lib.teams.gnome-circle ];
};
})
diff --git a/pkgs/by-name/cl/claude-code-acp/package.nix b/pkgs/by-name/cl/claude-agent-acp/package.nix
similarity index 51%
rename from pkgs/by-name/cl/claude-code-acp/package.nix
rename to pkgs/by-name/cl/claude-agent-acp/package.nix
index 92c4bec7ade0..d8633b21a70a 100644
--- a/pkgs/by-name/cl/claude-code-acp/package.nix
+++ b/pkgs/by-name/cl/claude-agent-acp/package.nix
@@ -5,23 +5,23 @@
}:
buildNpmPackage (finalAttrs: {
- pname = "claude-code-acp";
- version = "0.16.1";
+ pname = "claude-agent-acp";
+ version = "0.21.0";
src = fetchFromGitHub {
owner = "zed-industries";
- repo = "claude-code-acp";
+ repo = "claude-agent-acp";
tag = "v${finalAttrs.version}";
- hash = "sha256-/HeAz0jdXhLhYGcwTgthrE7cGjKjro30GQUmAn4egXs=";
+ hash = "sha256-6c6bHuso3diW5ZfHiM2xcxGDTNG0LIL0TZd0MFVpW/E=";
};
- npmDepsHash = "sha256-poTtwIIPHcgQ2uyIUIWVOpHbdDIzVgympa7aHtuSMok=";
+ npmDepsHash = "sha256-UtiIcjgNCYMFrRpO5AlUbOyutJ3ipwIbcpMi2BqawEk=";
meta = {
- description = "ACP-compatible coding agent powered by the Claude Code SDK";
- homepage = "https://github.com/zed-industries/claude-code-acp";
+ description = "ACP-compatible coding agent powered by the Claude Agent SDK";
+ homepage = "https://github.com/zed-industries/claude-agent-acp";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ storopoli ];
- mainProgram = "claude-code-acp";
+ mainProgram = "claude-agent-acp";
};
})
diff --git a/pkgs/by-name/co/colima/package.nix b/pkgs/by-name/co/colima/package.nix
index 45e4eb62a74f..c03ab54dd7df 100644
--- a/pkgs/by-name/co/colima/package.nix
+++ b/pkgs/by-name/co/colima/package.nix
@@ -5,6 +5,7 @@
buildGoModule,
fetchFromGitHub,
installShellFiles,
+ krunkit,
lima-full,
makeWrapper,
procps,
@@ -60,11 +61,14 @@ buildGoModule (finalAttrs: {
postInstall = ''
wrapProgram $out/bin/colima \
--prefix PATH : ${
- lib.makeBinPath [
- # Suppress warning on `colima start`: https://github.com/abiosoft/colima/issues/1333
- lima-full
- qemu
- ]
+ lib.makeBinPath (
+ [
+ # Suppress warning on `colima start`: https://github.com/abiosoft/colima/issues/1333
+ lima-full
+ qemu
+ ]
+ ++ lib.optional (lib.meta.availableOn stdenv.hostPlatform krunkit) krunkit
+ )
}
installShellCompletion --cmd colima \
diff --git a/pkgs/by-name/co/colmap/disambiguate-gradientchecker.patch b/pkgs/by-name/co/colmap/disambiguate-gradientchecker.patch
new file mode 100644
index 000000000000..f0ec5ea2d147
--- /dev/null
+++ b/pkgs/by-name/co/colmap/disambiguate-gradientchecker.patch
@@ -0,0 +1,19 @@
+commit a1f0a46b4932ed1c98ceb872e6b346b370f1b638
+Author: Patrick Chilton
+Date: Wed Mar 18 15:59:05 2026 +0100
+
+ Disambiguate ceres::GradientChecker constructor
+
+diff --git a/src/colmap/estimators/cost_functions/reprojection_error_test.cc b/src/colmap/estimators/cost_functions/reprojection_error_test.cc
+index 25e6ffdc..7a1e4777 100644
+--- a/src/colmap/estimators/cost_functions/reprojection_error_test.cc
++++ b/src/colmap/estimators/cost_functions/reprojection_error_test.cc
+@@ -106,7 +106,7 @@ TEST(ReprojErrorCostFunctor, AnalyticalVersusAutoDiff) {
+
+ ceres::NumericDiffOptions numeric_diff_options;
+ ceres::GradientChecker gradient_checker(
+- analytical_cost_function.get(), nullptr, numeric_diff_options);
++ analytical_cost_function.get(), static_cast*>(nullptr), numeric_diff_options);
+ ceres::GradientChecker::ProbeResults results;
+ EXPECT_TRUE(
+ gradient_checker.Probe(parameter_blocks.data(), kEps, &results));
diff --git a/pkgs/by-name/co/colmap/openimageio.patch b/pkgs/by-name/co/colmap/openimageio.patch
deleted file mode 100644
index 780a3c4ba256..000000000000
--- a/pkgs/by-name/co/colmap/openimageio.patch
+++ /dev/null
@@ -1,4927 +0,0 @@
-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