Skip to content

[ROCm] Fix round 2: merge main and maintainer nits (colmap#4635) - #5

Merged
jeffdaily merged 86 commits into
moat-portfrom
moat-fix-4635
Sep 21, 2026
Merged

jeffdaily merged 86 commits into
moat-portfrom
moat-fix-4635

Conversation

@jeffdaily

Copy link
Copy Markdown
Collaborator

Upstream reply

Thanks! Your changes are in, with the resulting tree identical to #4. I merged main into the branch rather than rebasing so the existing review history stays intact, and your commit is included as authored.

Re-tested on AMD GPUs at the new head: MI250X (gfx90a) and Radeon Pro W7800 (gfx1100), full ctest 162/162 on each, with the GPU SIFT tests (32/32) confirmed running on the GPU. The CUDA build still compiles and links cleanly.

Round summary (not posted upstream)

The maintainer reviewed colmap#4635, said it looks good, and opened #4 (base moat-port) rebasing our three commits onto colmap main 7019dcc plus his own nit commit e30393b. moat-port is frozen and never rebased, so this round reproduces his result as a merge.

Review: passed, no findings. Validation at cfe7336: linux-gfx90a 162/162, linux-gfx1100 162/162 (wave64 and wave32 gates); windows remains under its approved waiver. CUDA 12.8 compile-and-link check clean. Details in projects/colmap/notes.md on port/colmap.

After merge, close #4 noting its content landed tree-identical.

ahojnnes and others added 30 commits August 8, 2026 20:28
Summary:
- remove 56 unused includes identified with clangd and the compilation
database
- retain includes needed by conditional builds, platform-specific code,
macro expansion, and compatibility paths

Testing:
- PYENV_VERSION=colmap scripts/format/c++.sh
- cmake --build build --parallel 8
- ctest --test-dir build --output-on-failure --parallel 8 (160 tests
passed)
## Summary

- expose the existing `Mapper.load_all_images` setting through mapper
CLI/config options
- forward the setting to the database cache used by `image_registrator`
- cover command-line parsing and project-file round trips for the option

This keeps the default behavior unchanged while allowing externally
registered images without two-view geometry to be loaded when requested.

Fixes colmap#4602

## Testing

- `scripts/format/c++.sh` with the required clang-format 22.1.5 (3
changed files)
- configured a CPU-only release build with tests enabled
- built `colmap_controllers_option_manager_test` and `colmap_main`
(371/371 build steps)
- ran `option_manager_test` (14/14 passed)
- ran `colmap image_registrator --help` and verified
`--Mapper.load_all_images arg (=0)` is exposed

## AI assistance

AI tools assisted with repository analysis and implementation. I
reviewed the resulting diff, traced the existing database-cache option
flow, and ran the tests and executable checks listed above.

---------

Co-authored-by: Johannes Schönberger <jsch@meta.com>
Bugfix for colmap#4630 

**Summary**

`SpatialPairGenerator::ReadPositionPriorData` calculates the mean
coordinate of the image position priors and subtracts this from the
positions matrix to improve numerical precision. In the case where some
images do not have a pose prior, their corresponding zero-valued rows
were still included in calculation of the mean position, and this could
significantly bias the calculated mean position.

For pose priors using projected Cartesian coordinates in particular,
where coordinate values might be on the order of 1e6, this could result
in large numerical errors and cause subsequent spatial matching to fail.

**Changes**

- `SpatialPairGenerator::ReadPositionPriorData` has been updated to trim
unpopulated position rows prior to calculation of mean coordinate
- Debug logging has been added that prints the calculated offset at log
level 1
- A test `CentersLargeCoordinatesWithMissingPosePrior` has been added to
test for regressions

Hopefully this PR is welcome and follows general expectations; let me
know if there are issues.
…lmap#4620)

## Summary

- **Propagate overlap before re-partitioning.** Overlapping images
between sibling clusters are now added to their direct children *before*
recursing, so the next level sees them directly instead of propagating
overlap to all descendants after partitioning.
- **Let inherited overlap participate in the next graph cut**. An
overlap image inherited from the parent becomes a vertex in the next
graph cut partition.
- **Enforce the documented leaf bound.** A cluster stops partitioning
once it reaches `leaf_max_num_images + image_overlap`, so every leaf
actually respects the documented size bound (previously multi-level
leaves could exceed it).

## Test

The new `HierarchicalTwoLevelsWithOverlap` test uses the 8-image scene
below (`branching=2`, `image_overlap=2`, `leaf_max_num_images=3`):

```
Original scene:
               100          50          100
        (0) -------- (1) -------- (2) -------- (3)
         |                                      |
        10                                      10
         |                                      |
        (7) -------- (6) -------- (5) -------- (4)
              100          50          100
```

Level 1 splits the scene into two clusters, each receiving the two
best-matching images from the sibling cluster as overlap:

```
level 1: C1 = branch {0,1,2,3} ∪ overlap {4,7} = {0, 1, 2, 3, 4, 7}
level 1: C2 = branch {4,5,6,7} ∪ overlap {0,3} = {0, 3, 4, 5, 6, 7}
```

Level 2 splits C1 and C2 again. The inherited overlap images `{4,7}` /
`{0,3}` have no edges in the child subgraphs, so the graph cut cannot
label them —this is where the behaviors diverge.

**Before this PR**, the inherited overlap is copied into every leaf of
the subtree (leaves with 5 images).
- **leaves**:  `{0,1,2,4,7} {1,2,3,4,7} {0,3,4,5,6} {0,3,5,6,7} `
```
level 2 (before): inherited overlap is copied into every leaf of the subtree
	C11 = branch {0,1} ∪ overlap {2} ∪ inherited overlap {4,7} = {0,1,2,4,7}
	C12 = branch {2,3} ∪ overlap {1} ∪ inherited overlap {4,7} = {1,2,3,4,7}
	C13 = branch {4,5} ∪ overlap {6} ∪ inherited overlap {0,3} = {0,3,4,5,6}
	C14 = branch {6,7} ∪ overlap {5} ∪ inherited overlap {0,3} = {0,3,5,6,7}
```

**With this PR**, `7` follows anchor `0` and `4` follows anchor `3` into
distinct sub-clusters, keeping each leaf at 4 images.
- **leaves**:  ` {0,1,2,7} {1,2,3,4} {3,4,5,6} {0,5,6,7} `
```
level 2 (this PR): inherited overlap is assigned through its anchor
	C11 = branch {0,1,7} ∪ overlap {2} = {0,1,2,7}
	C12 = branch {2,3,4} ∪ overlap {1} = {1,2,3,4}
	C13 = branch {3,4,5} ∪ overlap {6} = {3,4,5,6}
	C14 = branch {0,6,7} ∪ overlap {5} = {0,5,6,7}
```

---------

Co-authored-by: Johannes Schönberger <jsch@meta.com>
…egression) (colmap#4634)

Restore the row-by-row copy used up to commit 7d2058b (the parent of
8e014c5).

Correct copy before the regression:

https://github.com/colmap/colmap/blob/7d2058baaae1406160b5e0c7ae9b47582ade1b32/src/colmap/mvs/patch_match_cuda.cu#L1578-L1582
Regressed copy introduced by 8e014c5:

https://github.com/colmap/colmap/blob/8e014c5bc70c7e01506f1d5ef7daaf25dc3190ae/src/colmap/mvs/patch_match_cuda.cu#L1577

Since commit 8e014c5 (colmap#3459) the bitmaps were copied with a single
contiguous memcpy, displacing row r of every image narrower than
max_width by r * (max_width - width) pixels. Example for a 2x2 image in
a 3-wide layer:

```
  bitmap [a b; c d] -> contiguous copy [a b c; d 0 0] (wrong)
                       row-wise copy   [a b 0; c d 0] (correct)
```

Workspaces where all source images share the same dimensions are
unaffected (the two copies coincide there), which is why the standard
single-camera case never showed the problem. Workspaces with mixed image
sizes get silently corrupted photometric costs for every source image
narrower than the widest one.
In accordance with a brief discussion with @ahojnnes at CVPR, here is a
PR for adding the LoMa matcher.

For reference on the LoMa matcher, we refer to the paper, [LoMa: Local
Feature Matching Revisited](https://arxiv.org/abs/2604.04931), and the
code,
[https://github.com/davnords/loma](https://github.com/davnords/loma).

The PR is basically a copy-paste of how the integration with ALIKED+LG
already was made.

**Note:** The ONNX files are currently hosted on my own GitHub storage
project. Feel free to download them and add them as a release to colmap
to align the download links with where the other weights are hosted.

This PR is restricted to LoMa-B. I think this will benefit the community
the most as the matcher is quick (same speed as LG) and lightweight
while the detector / descriptor are a little slower but the full
pipeline is very powerful (see below).

Happy to fix anything that would improve the integration.

## Tests

I used it for reconstruction on South Building (128 images) and it
looked good. I also made sure the feature matches looked decent on a
MegaDepth pair.
<img width="1093" height="399" alt="Skärmavbild 2026-07-09 kl 14 57 30"
src="https://github.com/user-attachments/assets/bae76061-e9e9-4219-b614-cb0385d7e9ba"
/>

To provide some background on the strength of LoMa: In an HLoc
[fork](cvg/Hierarchical-Localization#495) we run
Visual Localization on InLoc and get:
```
DUC1: 55.1 / 80.3 / 91.4 
DUC2: 72.5 / 87.8 / 88.5
```
This is similar to the results reported in the
[paper](https://arxiv.org/abs/2604.04931) and an almost 20-point
improvement on DUC2 (0.25m, 10°) compared to what is reported in the
[LightGlue](https://arxiv.org/abs/2306.13643) paper.

## Acknowledgements

Credit to [aliejabbari](https://github.com/aliejabbari) for providing
the code for the ONNX export.

/ David

---------

Co-authored-by: David Nordström <davnords@arrhenius1.hpc.arrhenius.naiss.se>
Co-authored-by: Ali Jabbari <alijabbari.contact@gmail.com>
Co-authored-by: Johannes Schönberger <jsch@meta.com>
Co-authored-by: Johannes Schönberger <jsch@demuc.de>
Co-authored-by: Shaohui Liu <b1ueber2y@gmail.com>
## Summary

Global SfM keeps only the largest connected component of the view graph,
so everything else in a disconnected capture is discarded. This PR
reconstructs every component instead, producing one model per component.

The process has three steps:

1. Extract the initial connected components of the view graph.
2. Run rotation averaging independently on each of them, on an isolated
copy of the base reconstruction so that rig calibration cannot leak
across components. This is what settles the final components: filtering
edges by relative rotation error breaks apart clusters that were only
held together by outlier edges, so one input component can yield several
genuine ones. Doing the split here rather than on the raw pose graph
matters because no later stage discards images.
3. Run the full global mapper on each final component, over its own
`DatabaseCache` restricted to that component's images.

The resulting models are sorted by registered-frame count, and those
with fewer than `min_num_frames` frames are dropped.

## Options

- `GlobalMapper.reconstruct_all_components` (default `true`):
reconstruct every component versus only the largest one.
- `GlobalMapper.min_num_frames` (default `3`): discard models with fewer
registered frames.

Both are exposed through `OptionManager` and pycolmap's
`GlobalMapperOptions`.

## Behavior changes

- Multi-component reconstruction is on by default, so runs on
disconnected datasets now emit several models instead of one.
- `min_num_frames` also applies to the single-component path, where any
successful model was previously kept.
- The reconstruction is no longer added to the `ReconstructionManager`
before the mapper starts, so the GUI no longer renders intermediate
states during global mapping; models appear once complete.

## Benchmark: IMC2025, all 13 scenes

No covisibility filtering, same feature/match databases, `--random_seed
0 --num_threads 1`, measured at `b647561c`. `global (main)` and `global
(single)` (`--reconstruct_all_components 0`) come out bit-identical,
confirming the largest-component path is unchanged.

Pooled over all scenes:

| method | AUC@0.5° | AUC@1° | AUC@5° | AUC@10° | reg imgs / 1823 |
#comp |

|----------------|----------|--------|--------|---------|-----------------|-------|
| incremental | 8.52 | 17.01 | 36.25 | 41.67 | 1345 | 34 |
| global (main) | 7.45 | 11.53 | 19.22 | 21.14 | 634 | 12 |
| global (multi) | 13.85 | 23.15 | 40.33 | 44.33 | 1544 | 68 |

Per-scene average (mAA):

| method         | AUC@0.5° | AUC@1° | AUC@5° | AUC@10° |
|----------------|----------|--------|--------|---------|
| incremental    | 10.80    | 21.24  | 43.00  | 48.55   |
| global (main)  | 7.71     | 12.59  | 22.78  | 25.04   |
| global (multi) | 14.60    | 25.21  | 44.74  | 48.91   |

Registered images more than double (634 to 1544 of 1823) and overall
AUC@10° goes from 21.14 to 44.33, slightly ahead of the incremental
mapper. Full per-scene tables are in [this
comment](colmap#4589 (comment)).

## Test plan

New end-to-end tests in `controllers/global_pipeline_test.cc`:

- `MultiComponents`: two rigs with all cross-rig matches removed yield
one reconstruction per component, each matching its ground-truth subset.
- `MultiComponentsWithOutlierEdges`: two clusters bridged only by
outlier edges are still recovered separately, since rotation averaging
leaves large residuals on the bridges and filters them out.
- `MultiComponentsWithOutlierEdgesUsingGravity`: even when every
cross-cluster edge is an outlier, which the gravity-free solver cannot
resolve because the inter-cluster gauge is free, gravity priors make the
bridges detectable.
- `MultiComponentsEmptyViewGraph`: a database with no matches produces
no reconstructions.

New tests in `scene/pose_graph_test.cc` cover the component ordering and
the edgeless case.

- [x] `ctest -R
"controllers/global_pipeline_test|scene/pose_graph_test"`

---------

Co-authored-by: Johannes Schönberger <jsch@meta.com>
Co-authored-by: Johannes Schönberger <jsch@demuc.de>
Follow-up to review feedback in colmap#4641.

## Summary
- restore the camera-ray overloads of pycolmap.estimate_relative_pose
and pycolmap.refine_relative_pose
- attach identity Jacobians to preserve the legacy Sampson-error
normalization
- add regression coverage for both compatibility overloads

## Testing
- PYENV_VERSION=colmap scripts/format/c++.sh
- PYENV_VERSION=colmap scripts/format/python.sh
- PYENV_VERSION=colmap python -m pytest -q
src/pycolmap/estimators/pose_test.py
## Summary

- honor the requested Bitmap::Rescale filter
- use an explicit antialiased triangle filter for bilinear resizing
instead of OpenImageIO's expensive default Lanczos filter
- explicitly retain box filtering where requested
- add a regression test verifying that the two filter modes differ

This addresses the severe slowdown when feature extraction downsizes
images reported in colmap#4638. It does not claim to resolve the separate
no-resize CUDA slowdown discussed there.

## Performance

Using 30 copies of the 1920x1345 image from colmap#4638, resized to 960px with
CPU SIFT configured to detect no features:

- before: 8.08 s
- after: 3.11 s
- COLMAP 3.13.0: 2.13 s

A focused resize microbenchmark measured OpenImageIO's default
downsampling at about 234 ms per image and explicit triangle-filtered
downsampling at about 52 ms per image. The triangle filter preserves
low-pass filtering for downsampling, unlike ImageBufAlgo::resample.

## Testing

- PYENV_VERSION=colmap scripts/format/c++.sh
- ctest --test-dir build --output-on-failure -R
sensor/bitmap_test\|controllers/feature_extraction_test
…colmap#4640)

colmap#4420 adds ROCm, HIP support to
colmap. But on an attempt of starting the stereo step, the dense
reconstruction gui gives an error by only checking against the presence
of CUDA, even if the user has built it with HIP.

This commit fixes that. If user has HIP enabled, but not CUDA, the
process can start.
Fixes colmap#4649.

Pose-prior residuals could reintroduce fixed rig transforms as variable
Ceres parameter blocks. Preserve their constant parameterization and add
regression coverage.

Test: bundle_adjustment_ceres_test
## Summary

Consolidates backend-independent bundle adjustment tests into the shared
interface test suite.

- Parameterizes shared tests across Ceres and Caspar when Caspar is
enabled
- Moves common coverage for camera/point constraints, partial tracks,
track-length filtering, intrinsics, and ignored points
- Removes duplicated tests from the Ceres and Caspar implementation
suites
- Keeps backend-specific behavior and solver-internal tests in their
respective suites
- Fixes coverage where the previous Caspar constant-camera test
accidentally constructed a Ceres adjuster

## Verification

- `estimators/bundle_adjustment_test`
- `estimators/bundle_adjustment_ceres_test`
- Caspar-enabled compilation of all three affected test translation
units
- Formatting and `git diff --check`
## Summary

- run the full configured pytest suite for Windows pycolmap wheels
- execute pytest from the repository root so it uses the shared
`pyproject.toml` configuration
- preserve the Windows-specific Visual Studio and vcpkg environment
setup
- make native command failures terminate the PowerShell test script

This closes the coverage gap identified while reviewing colmap#4648: Windows
previously only imported `pycolmap`, so Python regression tests did not
run on the platform affected by colmap#3798.

## Validation

- `PYENV_VERSION=colmap python -m pytest` (948 passed)
- `git diff --check`
## Summary

- isolate CUDA and non-CUDA compiler caches
- make ccache reusable across cibuildwheel temporary build directories
- compile the macOS prerequisite build without sudo so it uses the
configured cache
- cap cache size and report fresh per-run statistics after wheel
compilation

## Motivation

Recent main builds restored compiler caches, but macOS recorded no hits
and the Linux CUDA job could not save its updates because it shared a
key with the non-CUDA job. The existing statistics also ran before the
wheel builds and therefore did not show whether pycolmap compilation
itself benefited.

## Validation

- parsed build-pycolmap.yml with PyYAML
- ran bash syntax checks on the modified Linux and macOS scripts
- verified the new ccache environment settings with ccache --show-config
- ran git diff --check
## Summary

- replace per-run compiler cache keys with weekly immutable generations
- restore caches in every build, but save only from trusted main/release
runs
- centralize weekly restore/save behavior in a reusable `compiler-cache`
composite action
- cap each configuration according to its observed working set
- prune stale PyCOLMAP ccache entries and recompress retained entries at
zstd level 6 before the weekly save
- remove cross-run FetchContent caches, which consumed roughly 4 GB of
the repository quota by themselves
- preserve stable ccache path/compiler hashing and per-run statistics

## Motivation

The repository had 11.06 GB of active GitHub Actions caches, above the
standard 10 GiB limit. Run-specific compiler archives and PR-scoped
copies were evicting recently saved main caches. The latest main build
restored only 1 of 10 native compiler caches even though available
caches produced approximately 99.3–99.9% hits.

Pull requests now restore shared main caches read-only. One trusted
build seeds each configuration for the current UTC week; subsequent
builds restore the exact weekly key without producing more archives. At
the next weekly epoch, the previous generation is used as a fallback and
the refreshed generation is saved once.

Before saving a new PyCOLMAP generation, ccache entries not used during
the current full wheel build are evicted. The remaining entries are
recompressed from ccache's default zstd level 1 to level 6. This work
runs only on the first successful trusted build that creates a weekly
cache.

## Cache budget

Configured compiler-cache limits are based on the observed working sets:

- native Docker/macOS: 100 MB each
- native Ubuntu: 100–150 MB per configuration
- native Windows: 200–300 MB per configuration
- PyCOLMAP: 350 MB–1 GB per configuration

One complete generation is capped at approximately 4.4 GB. During the
seven-day overlap between weekly generations, the projected peak before
save-time compaction is approximately 8.8–9.1 GB including cached
compiler tools, leaving limited headroom below the 10 GiB repository
quota. Existing FetchContent entries will age out because the workflows
no longer restore them.

## Validation

- parsed all workflow and composite-action YAML
- verified only the shared composite action invokes
`actions/cache/restore` and `actions/cache/save`
- verified pull requests and manual PyCOLMAP runs cannot save compiler
caches
- ran an isolated save/restore smoke test:
https://github.com/colmap/colmap/actions/runs/33055245598
  - the reusable action evicted the deliberately stale entry
- the retained entry survived recompression and archive restore as a
cache hit
  - the stale entry produced the expected cache miss
- `git diff --check`

CI exercises restore behavior on all hosted runner platforms. The first
trusted main run will seed the weekly keys; a subsequent run will
demonstrate warm-cache hit rates.
## Summary

- use Bash to compact ccache on Linux and macOS
- retain PowerShell compaction on Windows
- avoid requiring pwsh after the CUDA job frees disk space

## Context

The PyCOLMAP CUDA job in
https://github.com/colmap/colmap/actions/runs/33075656647 built and
archived its wheels successfully, then failed while saving the compiler
cache because pwsh was no longer available.

## Validation

- actionlint passes for all workflows
- all GitHub YAML files parse successfully
- the Bash compaction sequence passes against an isolated ccache
directory
- git diff --check passes
## Summary

- add `SequentialMatching.loop_detection_min_image_distance` to exclude
nearby images from loop retrieval
- apply candidate filtering before top-N selection so nearby frames do
not consume the loop-detection budget
- expose the option through the CLI, GUI, and PyCOLMAP and document its
behavior

The option defaults to zero to preserve existing behavior.

## Verification

- `PYENV_VERSION=colmap scripts/format/c++.sh`
- `ctest --output-on-failure -R
"controllers/pairing_test|retrieval/visual_index_test"`
- built `colmap_ui` and `colmap_main`
- built the PyCOLMAP `_core` extension and verified the new property
## Summary

- Add process-wide cooperative handling for SIGINT and SIGTERM, with a
second signal forcing immediate termination.
- Propagate cancellation through feature extraction/matching,
incremental mapping, bundle adjustment, point triangulation, image
registration, image undistortion/rectification, patch-match stereo, and
stereo fusion.
- Support graceful shutdown in `automatic_reconstructor` when using its
incremental mapper; global and hierarchical mapper modes remain
excluded.
- Preserve and write usable in-progress reconstruction or fusion results
before returning.
- Expose a reusable `pycolmap.CancellationToken` across supported Python
pipeline entry points and raise `InterruptedError` only after cleanup.
- Keep guided geometric-verification database updates consistent across
cancellation.
- Document CLI and Python shutdown behavior.

Global mapping is intentionally not included because its intermediate
state cannot currently be resumed safely.

## Testing

- `PYENV_VERSION=colmap scripts/format/c++.sh`
- `PYENV_VERSION=colmap scripts/format/python.sh`
- Targeted C++ build for the CLI and affected test targets.
- Seven affected C++ test suites pass:
  - `controllers/bundle_adjustment_test`
  - `controllers/undistorters_test`
  - `estimators/global_positioning_test`
  - `exe/sfm_test`
  - `mvs/fusion_test`
  - `sfm/global_mapper_test`
  - `util/cancellation_test`
- pycolmap shared-module build and stub generation.
- `src/pycolmap/util/cancellation_test.py`: 7 passed.

The full `install` build remains blocked by the pre-existing
`util/cache_test` linker error for
`testing::internal::GetWithoutMatchers()`; the successfully built
libraries were installed directly for pycolmap verification.

The implementation and follow-up fixes were independently reviewed with
Claude Code.
## Summary

- add draft release notes for COLMAP 4.2.0
- highlight multi-component global mapping, LoMa, ROCm/HIP PatchMatch,
the browser viewer, and mapper API expansion
- document performance improvements, bug fixes, and migration-relevant
breaking changes

## Testing

- `git diff --check`
- `PYENV_VERSION=colmap make -C doc html SPHINXOPTS="-W --keep-going"`
## Summary

- require complete function annotations and check all typed function
bodies with mypy
- annotate every tracked Python function, including tests, benchmarks,
docs, examples, and the Caspar generator
- include documentation and the Caspar generator in CI type checking
- exempt generated pybind11 stubs from source annotation enforcement and
narrowly suppress generated-binding diagnostics

## Testing

- `PYENV_VERSION=colmap scripts/format/python.sh`
- exact CI mypy commands against an installed wheel
- `PYENV_VERSION=colmap pytest` (955 passed)
- `PYENV_VERSION=colmap make -C doc html SPHINXOPTS="-W --keep-going"`
- AST audit confirming no executable behavior changes beyond annotations
Fixes colmap#4672

FlatHashMap/FlatHashSet and their node variants are data members of
classes in public headers, so COLMAP_HASH_MAP_BACKEND determines the
layout of Reconstruction, CorrespondenceGraph, BundleAdjustmentConfig
and others: sizeof(Reconstruction) is 320 bytes with STD and 280 with
BOOST. Both backends are header-only, so a mismatch changes no mangled
name and produces no link error, only memory corruption.

The backend was auto-selected from the Boost found at configure time,
which made that layout a property of the build machine rather than of
the source and the CMake arguments. The published manylinux pycolmap
wheels are built through vcpkg and got BOOST; an apt-based Ubuntu 24.04
build of the same commit gets STD. An extension linking its own COLMAP
and sharing bound types with pycolmap in one interpreter then reads
those objects at the wrong offsets, since pybind11 keys registration on
type_index, which is identical for both layouts.

Default to STD instead, and keep the old behavior available as an
explicit AUTO that warns. BOOST stays an opt-in that has to be applied
to everything in the process. colmap-config.cmake now pre-sets the value
COLMAP was built with, so find_package() consumers reproduce that choice
instead of re-deriving it from their own Boost, turning a mismatch into
a configure-time error. The choice is also recorded in the binary as
colmap::kHashMapBackend, reported by GetBuildInfo(), and exposed as
pycolmap.__hash_map_backend__ for downstreams to assert on.
ahojnnes and others added 26 commits September 12, 2026 13:55
…colmap#4713)

Replace the 28-line verbose BSD license header in all source files with
a single `SPDX-License-Identifier: BSD-3-Clause` line, using the comment
style of each language (//, #, %, rem), and add it to the ~230 source
files that were missing a header.

Also rename the license text to the conventional LICENSE filename and
update the Qt resource embedding it (resources.qrc +
MainWindow::License).

Third-party code is untouched: src/thirdparty keeps its own licenses,
and the Google-derived headers in util/glog_macros.h,
optim/tiny_solver.h, and util/string.cc retain their attribution.

Net effect: -17k lines of boilerplate.

Stack: PR 1/3, followed by colmap#4714 and colmap#4716.
Stacked on colmap#4713 (PR 2/3, followed by colmap#4716).

List ETH Zurich and UNC Chapel Hill as the 2016 copyright holders and
The COLMAP Contributors for 2016-2026 in LICENSE, README.md, and
doc/license.rst.
…#4716)

Stacked on colmap#4714 (3/3).

Instruct contributors and agents to start every new source file with the
one-line SPDX header, in the file's comment style.
Remove the unmaintained Matlab utilities in `scripts/matlab`
(model/depth-map/normal-map/PLY readers, writers, and plotting helpers)
and update `doc/faq.rst` and `doc/format.rst` to point to pycolmap
instead.

Remaining case-insensitive mentions of "matlab" in the tree are only
provenance comments in C++ sources (e.g. reference values generated with
Matlab) and vendored VLFeat docs; no Matlab code remains.
## Summary

When `os.cpu_count()` returns 1, panorama perspective rendering
subtracts one reserved CPU and passes `max_workers=0` to
`ThreadPoolExecutor`. This raises `ValueError: max_workers must be
greater than 0` before any panorama is processed.

Keep at least one rendering worker. The existing unknown-CPU fallback,
reservation of one CPU on multicore hosts, and 32-worker cap remain
unchanged.

Add a regression test for reported CPU counts of 1, `None`, 4, and 64.
It uses the real executor and verifies that every submitted panorama is
processed, replacing only the image-processing work to avoid optional
dependencies.

## Validation

- Before the fix: the single-CPU regression raises the reported
exception; the three comparison cases pass.
- After the fix: all six tests in `python/pycolmap/panorama_test.py`
pass.
- A separate smoke test exercised actual rendering with `cpu_count=1`,
two synthetic 16x8 panoramas, Pillow/OpenCV, and the native PyCOLMAP
bitmap writer. It produced two rendered images and two masks, all 4x4.
- Ruff check, Ruff format check, and `git diff --check` pass.

The Python tests used this checkout's source with the native PyCOLMAP
4.2.0 Windows wheel (Python 3.13.5). COLMAP's C++ code and the full
reconstruction pipeline were not built or tested.

AI assistance: OpenAI Codex identified the edge case, implemented the
fix, and ran the regression tests.

---------

Co-authored-by: Paul-Edouard Sarlin <15985472+sarlinpe@users.noreply.github.com>
Co-authored-by: Johannes Schönberger <jsch@meta.com>
Bump to the latest pybind11 release (3.1.0) and adopt two of its new
features.

Version bump:
- `pyproject.toml`: build pin `3.0.4` -> `3.1.0`
- `python/CMakeLists.txt`: `find_package` minimum `3.0.2` -> `3.1.0`

Notable pybind11 3.1.0 changes vs 3.0.x: Python 3.8 and MSVC 2017
support dropped (no impact, pycolmap requires >=3.10), PEP 484
strict-mode numeric casts relaxed, plus `stl_bind` slice-deletion,
`enum_` shutdown-crash, and `scoped_ostream_redirect` fixes.

Feature adoption:
- Custom `__str__` on `enum_` (pybind/pybind11#6078): `str()` on enums
using `AddStringToEnumConstructor` now returns the plain member name
(e.g. `str(Device.auto) == 'auto'`) and round-trips through the string
constructor. `repr()` keeps the qualified `Type.MEMBER` form via its own
function object (pybind11 chains overloads by mutating the shared
record, so the old `__repr__ = __str__` alias would have picked up the
custom `__str__` too).
- Explicit `py::mod_gil_used()` module declaration (new 3.1.0 spelling
for the default): documents that `_core` requires the GIL; no behavior
change.

Verified locally: full C++ rebuild + install, pycolmap incremental
rebuild against pybind11 3.1.0 (incl. stub generation), and the full
pytest suite (962 passed, incl. 2 new enum str/repr tests).
…che (colmap#4702)

Stacked on colmap#4696. Makes `min_inlier_ratio` rejections effective end to
end: rejected pairs are stored empty and skipped on load, instead of
flowing into the mappers behind a DEGENERATE label.

## Producer: matcher rejects UNDEFINED estimates; skips are not stored

The ratio recheck rejects pairs as DEGENERATE while keeping their inlier
matches and models, and the verifier stored those rows verbatim (both
`Match` and `Verify` output stages). Since downstream loaders only check
the inlier count, rejected pairs silently fed the mappers. Now:

- The verifier worker throws if a two-view estimator returns UNDEFINED,
so UNDEFINED at the output stage provably means the pair was skipped
(below-minimum matches or missing descriptors), never estimated.
(Audited all estimator exits: uncalibrated, calibrated, spherical,
shared/one-sided-focal, force-H, known-pose, multiple-models, rig — none
can return UNDEFINED.)
- Skipped (UNDEFINED) pairs are not stored at all: absence of a row
means never verified. `Verify` still deletes any pre-existing row first,
so re-verification self-heals stale rows.
- Rejected (DEGENERATE) pairs keep their diagnosis with the payload
(inlier matches, E/F/H, pose, cameras) cleared wholesale. The old
inlier-count reset is removed: every estimator exit labels below-minimum
results DEGENERATE, so clearing on the label alone is equivalent.

The database interface itself stays permissive (UNDEFINED rows remain
writable and legible for fixtures and legacy DBs); the contract is
enforced at the matcher and at load.

## Consumer: cache skips UNDEFINED/DEGENERATE pairs and warns

`DatabaseCache` rejects UNDEFINED/DEGENERATE pairs at load regardless of
stored inlier matches (single `UseInlierMatchesCheck` choke point),
which also repairs databases written before the producer-side fix.
Additionally:

- Load counts UNDEFINED rows and `LOG(WARNING)`s the count
(outdated-producer rows; clear and re-verify to remove them). Note:
truly-empty rows never reach the cache — `ReadTwoViewGeometries` already
filters them — so the warning covers payload-carrying rows.
- `CreateFromCache` now applies the same check when copying graph edges
(previously verbatim), not just for image connectivity.

## Behavior-change notes

- Incremental mapper: rejected pairs (previously loaded) are now
excluded. Courtyard @ 0.25 re-verified with the final binary: 691 rows,
474 live pairs bit-identical to before, 217 DEGENERATE (all empty, incl.
210 previously stored as UNDEFINED), 0 UNDEFINED rows.
- `skip_geometric_verification` and skipped pairs no longer leave
UNDEFINED marker rows, so re-runs re-attempt them instead of skipping
via the marker.
- Global pipeline: view-graph calibration already marks inconsistent
pairs DEGENERATE; those rows were previously still loaded and are now
excluded too. No dedicated global-pipeline benchmark was run.
- Rig estimator (returns no entries on rejection), guided verifier
(funnels through the fixed output stage), synthetic fixtures
(CALIBRATED/UNCALIBRATED only) are unaffected.

## Follow-ups (not in this PR)

- `FeaturePairs` matcher and rig verification write estimator output
verbatim without the worker assertion; the silent-drop hazard doesn't
apply there (no skip logic), but the same assertion could be added for
uniformity.
- View-graph calibration writes re-estimated output and DEGENERATE
markings verbatim; the consumer filter covers mapper ingestion, but
stored rows keep stale inliers.

## Test plan

- Existing `MatchClearsDegenerateGeometry` /
`VerifyClearsDegenerateGeometry` (DEGENERATE-empty, raw matches
preserved) still pass.
- New `MatchSkippedPairsStoreNoTwoViewGeometry` /
`VerifySkippedPairsStoreNoTwoViewGeometry`: skipped pairs store no TVG
row.
- `MatchSkipGeometricVerification` updated to the new contract (no row
stored).
- New `DatabaseCache.CreateFromCacheSkipsUndefinedGeometries`: UNDEFINED
edges excluded (image dropped when isolated, edge dropped when endpoints
survive); existing `SkipsUndefinedAndDegenerateGeometries` passes
unchanged.
- `controllers/` + `scene/`: 37/38 pass; the single failure
(`scene/reconstruction_test` AddFrame/AddImage exception-text mismatch)
is pre-existing on the base and unrelated (untouched `reconstruction.cc`
code path; this branch only touches `database_cache.cc` in `scene/`).
- E2E: legacy DB with 3 payload-carrying UNDEFINED rows makes the mapper
log `3 two-view geometries with UNDEFINED configuration ...`; courtyard
re-verify (above) shows identical live pairs with no estimator check
firing.
- `clang-format` clean on all touched files.
## Summary

- Change the `compiler-cache` epoch from ISO week (`%G-%V`) to UTC day
(`%F`), so changed objects are incorporated within a day instead of a
week.
- Add a post-save prune step that deletes superseded generations per
family on the current ref, keeping the newest two (new
`retain-generations` input, default `"2"`). Quota footprint stays at ~2
generations alive, same as the weekly scheme.
- Restrict cache saves to `main` pushes and `release` events;
`release/*` pushes, PRs, and manual runs restore read-only. This
preserves the Docker `docker-release_true` family, which is only seeded
by releases.
- Grant the five calling build jobs `contents: read` + `actions: write`
(pruning uses `gh cache list`/`delete` via `GITHUB_TOKEN`).

## Motivation

After large refactors land mid-week, every CI run restores the
pre-change weekly snapshot and recompiles the same new objects until the
next weekly rotation. Daily rotation bounds that staleness to a day.
Explicit pruning (rather than relying on GitHub's 7-day idle expiry)
keeps the faster rotation within the 10 GiB quota: without it, ~7
overlapping daily generations would exceed quota and trigger
cross-family LRU eviction.

## Test Plan

- E2E self-test of the new action on this branch against an isolated
`colmap-compiler-selftest-*` family (temporary workflow, removed before
opening this PR; all test archives deleted afterwards):
- run
[34756730823](https://github.com/colmap/colmap/actions/runs/34756730823):
confirmed save+prune correctly skip on a non-`main` push (Option B
gate).
- run
[34756772404](https://github.com/colmap/colmap/actions/runs/34756772404)
(with a temporarily widened gate): `Cache saved with key:
...-2000-01-03`, `Deleting superseded cache id ...`, verify confirmed
exactly the two newest generations remain — success.
- Prune-selection `jq` program tested against fixtures (keep-2, keep-1,
single/empty/foreign-only listings) — 5/5 pass.
- Same selection pipeline dry-run read-only against live production
families: correct no-op on a 2-generation family.
- All touched workflow/action YAML parsed; extracted prune script passes
`bash -n`; `git diff --check` clean.
- This PR's own CI exercises the restore path (daily key miss with
fallback to the previous weekly archives) on all platforms.
This updates poselib to the latest commit improving build compatibility
with newer versions of eigen.

This was tested on Ubuntu 24.04 with Eigen 5.0.1.

Co-authored-by: jmackay2 <jmackay2@gmail.com>
Co-authored-by: Shaohui Liu <b1ueber2y@gmail.com>
…ap#4721)

Add `SolveEpipolarConstraintMatrix` and `RaysFromCamRaysWithJac` to a
new solvers/utils library. The former replaces two verbatim copies of
the QR/SVD nullspace plus rank-2 enforcement in the eight-point
fundamental and essential estimators; the latter replaces the file-local
`UnpackCamRaysWithJac` helper.

Part 2/5 of the solver-simplify stack (stacked on colmap#4720).

Test Plan:
- estimators_solvers/utils_test (new)
- estimators_solvers/essential_matrix_test
- estimators_solvers/fundamental_matrix_test
colmap#4722)

Add `SolveHomographyFromConstraintMatrix` covering the minimal LU path
and the over-determined SVD path, pinning the shared 1e-8 singularity
threshold. Covered by the existing parameterized estimator tests.

Part 3/5 of the solver-simplify stack (stacked on colmap#4721).

Test Plan:
- estimators_solvers/homography_matrix_test
…4723)

Extend solvers/utils with `CalibratedRays` (replacing two verbatim
copies), plus `PoseParamsFromRigid3d` and `Rigid3dFromPoseParams` for
the 7-parameter TinySolver packing used by the essential and focal
refine drivers.

Part 4/5 of the solver-simplify stack (stacked on colmap#4722).

Test Plan:
- estimators_solvers/utils_test (extended)
- estimators_cost_functions/sampson_error_test (incl. new round-trip
test)
- estimators_solvers/essential_matrix_test
- estimators_solvers/relpose_one_sided_focal_test
- estimators_solvers/relpose_shared_focal_test
Introduces shared HWC->CHW image conversion in feature/utils and uses it
for the ALIKED input tensor and the LoMa detector and descriptor inputs.
No behavior change.

Independent PR (no dependencies).

Test plan: `ctest -R 'feature/(utils_test|aliked_test|loma_test)'`
passes.
…ors (colmap#4727)

Add `ComputeImgReprojError` and use it in the four autodiff reprojection
functors, replacing four verbatim copies of the project /
subtract-observation / seam-wrap / zero-on-failure epilogue. Adds an
analytical behind-camera zeroing test.

Part 5/5 of the solver-simplify stack (stacked on colmap#4723).

Test Plan:
- estimators_cost_functions/reprojection_error_test
Removes `colmap::Clamp` from `math.h` (including its unit test) and
converts all call sites plus all other nested `std::min`/`std::max`
clamping patterns across the codebase to `std::clamp`.

Converted (16 sites in 11 files): `image/undistortion`,
`mvs/mesh_simplification`, `mvs/depth_map`, `mvs/texture_mapping`,
`scene/reconstruction_pruning`, `scene/visibility_pyramid`,
`retrieval/vote_and_verify`, `estimators/gravity_refinement`,
`ui/colormaps`, `ui/model_viewer_widget`, and `math.h::TruncateCast`.

Deliberately left alone:
- `estimators/fundamental_matrix_degensac.cc`: mixes signed `int` and
`size_t`; the inner `max(..., 1)` runs in the signed domain, so a naive
`std::clamp<size_t>` would change behavior for negative inputs.
- `estimators/covariance.cc`: `max(max(...), ...)` is two lower bounds,
not a clamp.

No behavior change; `std::clamp` generates identical codegen to the
handcrafted nesting. Verified all converted sites have ordered bounds
(`lo <= hi` is a `std::clamp` precondition).

Test plan: built `colmap_ui`, `colmap_retrieval`, `colmap_estimators`,
`colmap_mvs`, `colmap_math`; `math/math_test` and
`retrieval/vote_and_verify_test` pass; files formatted with
`scripts/format/c++.sh`.
Stack 1/2. Introduces CreateONNXTensor/CreateONNXScalarTensor in
onnx_utils and uses them in the ALIKED extractor, LoMa
extractor/matcher, and ONNX brute-force/LightGlue matchers. No behavior
change.

Next: colmap#4750

Test plan: `ctest -R
'feature/(onnx_utils_test|onnx_matchers_test|aliked_test|loma_test)'`
passes.
Stack 2/2. Depends on colmap#4745. Introduces ImageFeatureCache in
feature/utils, a per-image cache retaining the two most recently used
entries, and uses it in the SIFT CPU matcher and the ONNX
brute-force/LightGlue matchers, replacing hand-rolled per-image caching.
Also drops the unused descriptor index prefetch in guided SIFT matching.
No behavior change.

Test plan: `ctest -R
'feature/(utils_test|sift_test|onnx_utils_test|onnx_matchers_test|aliked_test|loma_test)'`
passes.
## Motivation

Adding constraints to COLMAP's global-positioning Ceres problem
currently requires hacking its implementation because
`RunGlobalPositioning(...)` constructs, solves, and publishes the
problem in one call.

Following `CreateDefaultBundleAdjuster(...)`, this PR adds
`GlobalPositioner::CreateDefault(...)`, which returns the prepared
standard problem so callers can extend it before solving.
`RunGlobalPositioning(...)` remains the stock one-shot path with
unchanged behavior.

## Changes

- Add `GlobalPositioner::CreateDefault(...)` and a prepared
`GlobalPositioner` lifecycle.
- Expose the prepared Ceres problem, temporary frame centers, and
parameter-ordering extension.
- Add an optional caller-provided loss.
- Expose the construction and lifecycle workflow through PyCOLMAP.

## Why global positioning needs additional seams

The bundle-adjustment workflow can expose a prepared problem through
parameter blocks that are already externally addressable. Global
positioning also owns temporary centers and observation scales
internally, so its prepared problem needs additional seams.

<details>
<summary>[Click to expand for details]</summary>

- **Temporary frame centers.** The solver does not optimize the pose
translations stored in `Reconstruction` directly. It copies poses into
temporary world-frame centers and converts the optimized centers back
during finalization. `FrameCenterParameterBlock(frame_id)` exposes this
temporary parameter to code adding constraints to the prepared Ceres
problem. `Finalize()` performs the required publication back to
`Reconstruction`.
- **Internal observation scales.** BATA observation scales are stored
inside `GlobalPositioner` rather than `Reconstruction`.
- **Late parameter blocks.** Stock parameter ordering is created during
construction. `ExtendParameterBlockOrdering()` adds caller-created
blocks without rebuilding existing assignments and supports explicit
group overrides.

</details>

<details>
<summary>[Click to expand for a possible future refactor]</summary>

A deeper refactor could eliminate the two global-positioning-specific
state seams:

- Optimize the translation blocks in reconstruction-owned `Rigid3d`
poses instead of copied frame centers.
- Make observation scales explicit input and output.
`GlobalPositioner::CreateDefault(...)` could consume a scale map keyed
by observation, allowing callers to provide initial values and inspect
the optimized results instead of relying on hidden owner storage.

</details>

---------

Co-authored-by: Johannes Schönberger <jsch@demuc.de>
Follow-up to the investigation in colmap#4691: PR colmap#4304 made the inner
refinement BAs inherit the full outer solver recipe (robust loss, tight
tolerances), diverging from the cheap legacy/incremental recipe for the
same shared refinement routine.

This change:
- Derives inner refinement BA options from the outer options, inheriting
only variable-selection scope while restoring the cheap legacy recipe
(TRIVIAL loss, gradient-only tolerance, capped iterations, no minimum
track length). Solver/GPU selection stays inherited.
- Restores the legacy track establishment cap
(`track_max_num_views_per_track=100`, exposed via CLI/GUI/pycolmap).
- Restores legacy outer BA tolerances (gradient 1e-10, parameter 1e-8).
- Runs a second outer BA after re-filtering in retriangulation, as
legacy did.

Deliberately not restored: BA rig refinement stays enabled by default,
since current rotation averaging preserves calibrated rigs (unlike
legacy, which overwrote them) and fixing the rig in BA demonstrably
breaks rig recovery (`GlobalMapper.WithoutNoiseWithNonTrivialKnownRig`).

Test plan:
- `global_mapper_test` 5/5, `global_pipeline_test` 13/13,
`option_manager_test` 14/14 pass.
- Formatted with `scripts/format/c++.sh`.
…map#4762)

Fixes colmap#4761.

pybind11 enables LTO for the `_core` module by default, but the Boost
that COLMAP builds from source (when the system Boost is older than
1.84, e.g. Ubuntu 24.04) is installed as non-LTO static archives, and
with GCC that mix fails at link time with `undefined reference to
boost::bad_weak_ptr::what() const` from `libboost_thread.a`. This sets
`CMAKE_INTERPROCEDURAL_OPTIMIZATION` to `OFF` for pycolmap only when
compiling with GCC against a COLMAP that uses the pinned Boost, which
matches the root build.

Tested on Ubuntu 24.04 with `python -m pip install .` against an
installed COLMAP.

---------

Co-authored-by: Johannes Schönberger <jsch@meta.com>
Bring the branch up to current main (7019dcc) with a merge rather than a rebase, so the commits already published on this pull request keep their hashes and the review history stays attached to them. The resulting tree is identical to the maintainer's rebase of the same commits onto that main.

Two files conflicted and both take the maintainer's resolution:

- src/pycolmap/pipeline/mvs.cc: main restructured this file after the branch was cut and already carries the CUDA-or-HIP guard, so main's version is taken unchanged.
- src/colmap/util/opengl_utils_test.cc: main's version, with the QApplication include and the existing tests kept under COLMAP_GUI_ENABLED and the RunsThreadBody test that checks RunThreadWithOpenGLContext runs the thread body in builds without an OpenGL context.

This merge was prepared with assistance from an AI coding agent.

Test Plan:

```
git diff HEAD 02bece1   # empty: same tree as the maintainer's rebase
cmake -S . -B build-hip-gui -GNinja -DCUDA_ENABLED=OFF -DHIP_ENABLED=ON \
  -DCMAKE_HIP_ARCHITECTURES=gfx90a -DCMAKE_BUILD_TYPE=Release \
  -DTESTS_ENABLED=ON -DGUI_ENABLED=ON -DCGAL_ENABLED=OFF \
  -DDOWNLOAD_ENABLED=OFF -DONNX_ENABLED=OFF
cmake --build build-hip-gui
xvfb-run -a build-hip-gui/src/colmap/feature/sift_test
xvfb-run -a build-hip-gui/src/colmap/util/opengl_utils_test
build-hip-gui/src/colmap/mvs/gpu_mat_test
```
- Mention HIP in the GPU matching Check() error and fix use_gpu typo.
- Update stale CUDA-version comments to compute-backend wording.
- Drop unused cuda_to_hip.h include from feature_matching_utils.cc.
- Clarify ROCM_PATH cache-over-environment precedence in installed config.
- Rename CreateSiftGPUMatcherCUDA test to CreateSiftGPUMatcherCompute.
- Report missing PBO interop from the CuTexImage ctor on HIP builds.
@jeffdaily

Copy link
Copy Markdown
Collaborator Author

To approve this fix round, leave a comment containing this line by itself:

/moat approve

To send it back to the porter instead:

/moat changes-requested

Approving covers the commits on this branch and the section under '## Upstream reply' in the body, which is posted verbatim as a comment on the upstream pull request after the merge, behind a standing line disclosing that it was drafted by an AI assistant. utils/upstream.py --merge-fix --apply then fast-forwards the open upstream PR's branch to exactly the approved tip. Anything pushed afterwards, or any edit to the body, voids the approval and needs a fresh one.

@jeffdaily

Copy link
Copy Markdown
Collaborator Author

/moat approve

@jeffdaily
jeffdaily merged commit cfe7336 into moat-port Sep 21, 2026
13 of 14 checks passed
@jeffdaily
jeffdaily deleted the moat-fix-4635 branch September 21, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.