Repository navigation
Retire glomap_ra GP+RA; route through colmap4-native - #5
Merged
Merged
Conversation
M1 (Hygiene):
- Drop dead glomap-fork option fields use_precomputed_weights,
fix_non_lc_weights, fixed_non_lc_weight from
colmap::RotationEstimatorOptions. Per audit (gp_inventory.md, ra_inventory.md)
these are dead in fork .cc itself or hardcoded false in pipeline. Drop
matching pycolmap def_readwrite + clean up referencing code paths in
glomap_ra::RotationEstimator (kills the obsolete SolveWeightedLS branch
+ the no-op fix_non_lc_weights warning).
- Update comment block at rotation_averaging.h:70-71 — only skip_risky_LC_pairs
+ use_video_constraints + video_*_scale survive as fork-flag stubs.
- Gate native GP first-scale pin (global_positioning.cc:349-355) on
point_constraint_type != SPLIT_METRIC_DEPTH. Preserves existing colmap GP
unit tests under default BATA mode (Q1 / R10).
M2 (Foundation):
- New file colmap/util/loss_config.h: LossFunctionConfig {name, scale,
weight} + CreateLossFromConfig factory. Names: trivial, huber, cauchy,
arctan, softlone. weight!=1 wraps in ScaledLoss.
- Extend colmap::GlobalPositionerOptions with glomap-fork additions:
PointConstraintType enum (BATA / SPLIT_METRIC_DEPTH), use_init,
random_init_scale, depth toggles (use_log_scale_for_depth_map_scales,
use_log_residual_for_depth, zero_residual_behind, smooth_log_linear_transition,
log_linear_threshold), scale_prior_stddev, filter_depth_outliers,
initial_dmap_scales (optional<map<image_t, double>>),
10 LossFunctionConfig fields with names verbatim from existing glomap_ra
port (loss_normal_geometry, loss_normal_depth, loss_lc_geometry,
loss_lc_depth, loss_normal_geometry_inlier, loss_normal_depth_inlier,
loss_normal_depth_outlier, loss_normal_geometry_trackstart,
loss_normal_depth_trackstart, loss_scale_prior).
- Change random_seed default from -1 to 1 (Decision 9, preserves
byte-identity for existing call sites that previously inherited GP_SEED=1).
- Extend GlobalPositioner state: dmap_scales_, dmap_scale_observation_counts_,
depth_outliers_, 10 cached_loss_*_ shared_ptrs, soft_outlier_fallback_loss_.
M3 (Cost functors):
- Add WeightedBATADirectionalError to cost_functions/motion_averaging.h.
AutoDiff <3,3,3,1> on (c_i, X_k, scale). Anisotropic per-axis weighting
(1/sigma_x, 1/sigma_y, 1/sigma_z). Caller computes
sigma_z = (sigma_x + sigma_y) / 2.
- New file cost_functions/metric_depth.h with 3 functors:
MetricDepthError (5-toggle, 12 reachable combinations — smooth_transition
only meaningful in use_log_residual=true branch),
ScalePriorError (linear), LogScalePriorError (log-space, prior=log(1)=0).
All ported verbatim from fork's cost_function.h.
Cluster cleanup (carried over from prior session):
- Strip 175 lines of dead Fetzer focal cost from sfm/cost_function_glomap.h.
- Strip 47 lines of FilterTracksByReprojection (unbound, unused) from
sfm/track_filter_glomap.{h,cc}.
Verified: ATE 37.40 / 66.77 / 93.35 / 96.68 / 99.67 unchanged (M0a baseline)
on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0 + canonical DB
+ GP_SEED=1.
Implement the trivial-rig case of the metric-depth residual structure. Activated by point_constraint_type=SPLIT_METRIC_DEPTH (default still BATA — no behavior change for native callers). Per-observation in AddPoint3DToProblem (image.IsRefInFrame() branch only): - When image.depth_prior_validity[fid] is true and depth_prior > 0, emit a 1-D MetricDepthError residual on (frame_centers_[image.FrameId()], point3D.xyz, dmap_scales_[image_id]). - Lazy-insert dmap_scales_ on first valid observation per image. Initial value: from options_.initial_dmap_scales (caller-supplied GP1→GP2 handoff) if provided, else 1.0 (linear) / 0.0 (log). - Increment dmap_scale_observation_counts_ for the per-image ScalePriorError weight. Per-image emission in AddPointToCameraConstraints (after track loop): - Reset dmap_scales_ + dmap_scale_observation_counts_ at start (lazy filled by AddPoint3DToProblem). - After all observations added: emit one ScalePriorError / LogScalePriorError per image with depth observations. Loss is wrapped in ceres::ScaledLoss(TrivialLoss, obs_count) so dense-depth images get proportionally stronger priors. M5 will replace TrivialLoss with the configurable cached_loss_scale_prior_. Parameter ordering / bound (M4 step 5): - dmap_scales_ ride alongside frame_centers_ in parameter group 2. - Linear-space: SetParameterLowerBound(&dmap_scale, 0, 1e-5) prevents collapse. Log-space: no bound (parameter is log-space, positivity comes from exp() in MetricDepthError). Init flag wiring (M4 steps 6-7): - options_.use_init short-circuits InitializeRandomPositions for both camera centers AND track xyz (collapses fork's split flag). - options_.random_init_scale replaces hardcoded 100.0 cube size in two call sites. Verified: ATE 37.40 / 66.77 / 93.35 / 96.68 / 99.67 unchanged (default BATA path) on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0 + canonical DB + GP_SEED=1.
Implement glomap-fork loss-routing cascade in colmap::GlobalPositioner.
Activated by point_constraint_type=SPLIT_METRIC_DEPTH; default BATA mode
unchanged.
Refactor: extract per-observation logic into AddObservationToProblem
helper (point3D_id, observation, is_lc_observation, random_initialization,
reconstruction). AddPoint3DToProblem becomes a dispatcher iterating
track.Elements() (is_lc=false) then track.lc_elements (is_lc=true) —
mirrors fork's two-parallel-collection structure.
Cache pre-warm in AddPointToCameraConstraints: materialize 10
cached_loss_*_ via CreateLossFromConfig. Default TrivialLoss/weight=1
config is equivalent to no override; non-default Huber/Cauchy/etc take
effect through cascade.
is_excluded skip: top of AddObservationToProblem.
4-way geometry cascade (SPLIT_METRIC_DEPTH only):
is_lc -> loss_lc_geometry, is_track_anchor -> loss_normal_geometry_trackstart,
is_inlier -> loss_normal_geometry_inlier, else -> loss_normal_geometry.
5-way depth cascade for MetricDepth residual:
depth_outliers_ + LC -> SKIP depth residual entirely (deletes
metric_depth_cost to avoid leak),
depth_outliers_ + non-LC -> ScaledLoss(HuberLoss(1), 1) hardcoded
soft fallback,
is_lc -> loss_lc_depth, track_anchor -> loss_normal_depth_trackstart,
is_inlier -> loss_normal_depth_inlier, MDRP-flagged outlier ->
loss_normal_depth_outlier, else -> loss_normal_depth.
ScalePriorError wrapper updated: replaces M4 TrivialLoss(obs_count)
with cached_loss_scale_prior_ wrapped in ScaledLoss(obs_count). Default
config preserves obs_count weighting unchanged.
Verified ATE 37.40/66.77/93.35/96.68/99.67 unchanged (default BATA)
on sample-10 LaMAR HGE.
Implement glomap-fork pre-Solve depth-outlier filter in
colmap::GlobalPositioner. Activated by
options_.filter_depth_outliers + point_constraint_type=SPLIT_METRIC_DEPTH.
In AddPointToCameraConstraints, after cache pre-warm + dmap_scales_
reset:
- Seed dmap_scales_ from options_.initial_dmap_scales when caller
supplied them (typical GP2 path: GP1 hands off solved scales). Linear
scale converted to log if use_log_scale_for_depth_map_scales=true.
Lazy-insert in AddObservationToProblem then preserves these values.
- Clear depth_outliers_; if filter_depth_outliers, call
FilterDepthOutliers(reconstruction).
FilterDepthOutliers method (new):
- Per-point3D: walk track.Elements() and track.lc_elements separately.
- For each observation: skip when depth_prior_validity[fid]=false or
depth_prior <= 1e-6 or stddev_rel <= 1e-9.
- Apply per-image scale (dmap_scales_[image_id]) if available; else
raw prior. Threshold check:
|log(z_est) - log(scale * depth_prior)| >= 3 * log(1 + stddev_rel)
- Insert (image_id, point2D_idx) into depth_outliers_ on flag.
Anonymous-namespace helper DepthOutlierFlag deduplicates the regular vs
LC sweep.
Native uses image.CamFromWorld() * X_world (the canonical cam_from_world
projection) — fork's pre-Solve branch had inadvertently relied on the
in-Solve flipped translation convention via the image.cam_from_world
member directly; native's CamFromWorld() resolves cleanly because we run
this BEFORE the Solve loop temporarily flips frame_centers_.
Verified ATE 37.40/66.77/93.35/96.68/99.67 unchanged (default BATA mode +
filter_depth_outliers=false) on sample-10 LaMAR HGE.
…ETRIC_DEPTH
Pycolmap binding for native colmap::GlobalPositioner now exposes the full
glomap-fork option surface (M2) + dict return shape (Decision 7).
Option struct extension (BindGlobalPositioner):
- New PointConstraintType enum binding (BATA / SPLIT_METRIC_DEPTH).
- Bind use_init, random_init_scale, depth toggles
(use_log_scale_for_depth_map_scales, use_log_residual_for_depth,
zero_residual_behind, smooth_log_linear_transition,
log_linear_threshold), scale_prior_stddev, filter_depth_outliers.
- Bind initial_dmap_scales as Optional[Dict[image_id, float]] with None
short-circuit; Python passes linear-space, C++ stores per
use_log_scale_for_depth_map_scales convention.
- Bind 10 LossFunctionConfig fields as dict-typed properties via
templated BindLossField helper accepting/emitting
{'name', 'scale', 'weight'}. Names verbatim from glomap_ra port:
loss_normal_geometry, loss_normal_depth, loss_lc_geometry,
loss_lc_depth, loss_normal_geometry_inlier, loss_normal_depth_inlier,
loss_normal_depth_outlier, loss_normal_geometry_trackstart,
loss_normal_depth_trackstart, loss_scale_prior.
run_global_positioning return shape:
- Was: bool. Now: dict {success: bool, dmap_scale_map: Dict[image_id,
float] (linear), dmap_scale_map_nested: Dict[image_id, {linear, log}]}.
- Reconstruction is mutated in place; Python reads images/tracks from
rec.images / rec.points3D directly.
- Mirrors glomap_ra binding return shape so videosfm caller migration
(Python side, deferred to next session) is a near-1:1 swap.
GetDmapScales accessor on GlobalPositioner exposes the per-image
dmap_scales_ map for the binding's dict-extraction step. Linear
conversion (exp() when use_log_scale_for_depth_map_scales=true) happens
in the lambda.
Verified: ATE 37.40/66.77/93.35/96.68/99.67 unchanged on sample-10 LaMAR
HGE — caller still on glomap_ra path; native binding extension is
additive and doesn't affect existing flow.
…ons bindings
Bug fixes uncovered during M7 caller migration:
1. SetupProblem scales_.reserve undercount: was only counting
point3D.track.Length(); M5's two-loop iteration also walks
track.lc_elements, each emplace_back-ing a new scale. Without
reserved capacity, vector reallocation invalidates earlier &scale
pointers stored in Ceres residual blocks → "ParameterBlock with
size 1 has at least one invalid value". Fix: include lc_elements
in the reservation count.
2. dmap_scales_ container: switched from unordered_map<image_t, double>
to std::map<image_t, double>. Ceres residuals store
&dmap_scales_[image_id] data pointers; a hash-table rehash during
lazy-insert in AddObservationToProblem invalidates them. std::map
(rb-tree) keeps pointers stable for the entry's lifetime.
3. Parameter group separation: dmap_scales_ now go in their own
parameter group (group_id+1), separate from frame_centers_. Mixing
1-D dmap_scales with 3-D frame_centers in the same Schur-ordering
group breaks the Schur-complement preprocessor — Ceres downgrades
to SPARSE_NORMAL_CHOLESKY then rejects the inhomogeneous block sizes.
Separate group keeps each Schur block size-uniform.
4. Pycolmap binding: added solver_options.{num_threads,
max_num_iterations, function_tolerance, gradient_tolerance,
parameter_tolerance} as def_property pass-throughs on
GlobalPositionerOptions. The solver_options nested struct is not
directly bound in pycolmap, so caller migration would otherwise
fail with "object has no attribute 'solver_options'".
Mirrors the existing pyglomap binding's pattern.
Verified GP runs to CONVERGENCE (no more spurious FAILURE termination)
on sample-10 LaMAR HGE under both BATA and SPLIT_METRIC_DEPTH paths.
ATE differs from glomap_ra baseline (cross-implementation drift expected
per Decision 7); structural integrity restored.
Glomap-fork RA contributed a per-pair IRLS final-weight map that the
videosfm pipeline reads for its consecutive-pair-weight degeneracy
diagnostic (rotation_averaging.py:117-129). Native colmap RA discards
the IRLS weight vector after the final factorize. M8 plumbs it out.
Plumbing (4 layers):
1. RotationAveragingProblem gains final_weights_ member +
FinalWeights() accessor + SetFinalWeightsFromIRLS(weights_irls)
setter. The setter translates the per-row weight vector into a
per-pair map keyed by image_pair_t. Each PairConstraint owns 1
(gravity-aligned) or 3 (full-3DOF) consecutive rows starting at
constraint.row_index — we read weight at row_index (all rows
share the same weight per ComputeIRLSWeights). Gauge-fixing rows
excluded.
2. SolveIRLS captures the last successful iteration's weight vector
in last_weights and calls problem.SetFinalWeightsFromIRLS at the
end (after the convergence loop). Empty if SolveIRLS doesn't run
(e.g. L1-only path).
3. RotationEstimator::EstimateRotations + SolveRotationAveraging gain
std::unordered_map<image_pair_t, double>* final_weights = nullptr
out-param. When non-null, copy problem.FinalWeights() to *out
after the solve.
4. RunRotationAveraging mirrors. Wired through both branches (with /
without rig expansion).
Pycolmap binding:
- Adds extract_final_weights: bool = False kwarg. Default returns
bool (preserves upstream colmap test ABI per Decision 8). When True,
returns {success: bool, final_weights: Dict[image_pair_t, float]}
dict. C5 fix: indexed by constraint.row_index (the row-major index
into weights_irls), NOT by pair_info.index (the inventory's earlier
draft).
Default behavior unchanged. Verified the C++ builds clean.
Glomap-fork RA's skip_risky_LC_pairs drops pairs whose loop-closure
inlier count strictly exceeds non-LC inliers. The filter reads
ImagePair.{inliers, are_lc} fork fields — these live on
CorrespondenceGraph::ImagePair, NOT on PoseGraph::Edge (which is a
strict subset). M9 plumbs an optional CorrespondenceGraph* through 4
layers so RA can apply the filter without bloating the upstream
PoseGraph interface.
Plumbing:
1. CorrespondenceGraph gains const ImagePairsMap() accessor (the
existing MutableImagePairs() can't be called through a const
pointer). Used only by RA M9.
2. RotationAveragingProblem ctor takes optional CorrespondenceGraph*
(default nullptr). Stored as correspondence_graph_ member.
3. BuildPairConstraints reads cg.ImagePairsMap()[pair_id].{inliers,
are_lc} when options_.skip_risky_LC_pairs && correspondence_graph_
non-null. Counts LC inliers; if lc_count > non_lc_count, continue
(pair excluded from constraint matrix entirely).
4. RotationEstimator::EstimateRotations + SolveRotationAveraging +
RunRotationAveraging gain optional CorrespondenceGraph* parameter
(default nullptr; passes through to RotationAveragingProblem ctor).
5. Pycolmap binding adds correspondence_graph: CorrespondenceGraph =
None kwarg. Default behavior unchanged for callers that don't pass
the CG.
Default behavior: with skip_risky_LC_pairs=False (default) or
correspondence_graph=None, no filtering — exactly matches pre-M9 behavior.
Verified C++ builds clean.
Glomap-fork RA's prioritize_tracking spanning-tree initialization
penalises LC-dominated edges so the MST picks tracking-dominated edges
as parents. Activated when use_video_constraints=true (M11 video-Ceres
path). Without the penalty, LC-contaminated initialization undermines
the Ceres video-aware solve.
ComputeMaximumPoseGraphSpanningTree (local helper in
rotation_averaging.cc) now accepts:
- prioritize_tracking (bool, default false): enable LC-penalty.
- correspondence_graph (CorrespondenceGraph*, default nullptr):
source of ImagePair.{inliers, are_lc} fork fields.
When prioritize_tracking + correspondence_graph are both set, the MST
weight for each edge is reduced by 1e9 if the pair has strict LC
majority (lc_count > non_lc_count). Native MST maximises weights, so
fork's +1e9 to weights_boost becomes -1e9 to weight here
(convention inversion). Fallback to plain num_matches weight when
fork fields absent or empty.
InitializeFromMaximumSpanningTree forwards the CG; reads
options_.use_video_constraints to set prioritize_tracking. Wired
through SolveRotationAveraging from EstimateRotations + RunRotationAveraging.
Default behavior unchanged when use_video_constraints=false (no penalty
applied). Verified C++ builds clean.
Glomap-fork RA video-aware Ceres path. Activated when options_.use_video_constraints && !options_.use_gravity. Replaces L1+IRLS with a Ceres optimization over per-frame 3-DOF angle-axis blocks. Each pair contributes a relative-rotation residual wrapped in either HuberLoss (tracking-dominated pair, scale=video_tracking_huber_scale) or CauchyLoss (LC-dominated pair, scale=video_lc_cauchy_scale). Anonymous-namespace helpers: - RelativeRotationError: AutoDiff<3, 3, 3> functor on (r1_aa, r2_aa). Residual = AngleAxis(R2^T * R_rel * R1) where R_rel is precomputed from PairConstraint::Full3DOF::R_cam2_from_cam1. Sign convention matches fork (no leading negative — the resulting AngleAxis is identical sign to the L1+IRLS tangent residual at convergence; Ceres doesn't care about sign anyway since LM minimizes ||residual||^2). - IsTrackingPair: counts LC inliers from CorrespondenceGraph::ImagePair fork fields (inliers + are_lc); returns true when non_lc_count >= lc_count. Reads via problem.CorrespondenceGraphPtr() (M9 plumbing). RotationAveragingProblem accessor additions (M11): - MutableEstimatedRotations() — direct mutable access to per-frame 3-DOF angle-axis storage so Ceres can optimize in place. - FrameIdToParamIdx() — frame_id -> 3-DOF block offset mapping. - ImageIdToFrameId() — for trivial-rig path PairConstraint image_id -> frame_id resolution. - FixedFrameId() — gauge-fixing anchor. - CorrespondenceGraphPtr() — M9 plumbing surface. RotationAveragingSolver::Solve dispatches video-Ceres path before L1+IRLS when use_video_constraints && !use_gravity. Mutually-exclusive warning logged when both are set (use_video_constraints disabled). Cost is Huber+Cauchy with TAKE_OWNERSHIP semantics; no leak. Solver: SPARSE_NORMAL_CHOLESKY, max 100 iters. M0c-equivalent verification: default behavior unchanged when use_video_constraints=false.
Final deletion phase. Pre-deletion grep verified that the deleted files
have no remaining external references after M7 + M12 caller migration.
Submodule pycolmap.glomap_ra stays alive — track-filter,
track-establishment, image-pair-inliers, view-graph-manipulation, and
relative-pose-estimation continue to bind under it.
Files deleted (~3000 LOC):
- src/colmap/sfm/global_positioning_glomap.{h,cc}
- src/colmap/sfm/rotation_averaging_glomap.{h,cc}
- src/colmap/sfm/cost_function_glomap.h
- src/colmap/sfm/l1_solver_glomap.h
- src/colmap/sfm/optimization_base_glomap.h
- src/colmap/sfm/tree_glomap.{h,cc} (only RA used it)
- src/pycolmap/sfm/global_positioning_glomap.cc
- src/pycolmap/sfm/rotation_averaging_glomap.cc
Files KEPT (still used by surviving glomap_ra modules):
- rigid3d_glomap.{h,cc} — track_filter_glomap consumer
- two_view_geometry_glomap.{h,cc} — image_pair_inliers consumer
- track_filter_glomap, track_establishment_glomap, image_pair_inliers,
view_graph_manipulation, view_graph_calibration, relative_pose_estimation
CMakeLists updates:
- src/colmap/sfm/CMakeLists.txt: drop deleted entries from SRCS list.
- src/pycolmap/sfm/bindings.cc: drop BindGlobalPositionerOptions /
BindGlobalPositioningGlomap / BindRotationAveragingGlomap forward
decls + dispatch calls. Other binders (BindViewGraphManipulation,
BindRelativePoseEstimation, BindTrackEstablishmentGlomap,
BindImagePairInliersGlomap, BindTrackFilterGlomap) preserved — those
bind to the pycolmap.glomap_ra submodule which stays alive.
Pycolmap CMake auto-globs sources, so the .cc deletions remove
themselves from the binding build.
Verified: end-to-end pipeline runs on sample-10 LaMAR HGE +
canonical DB + GP_SEED=1 with native pycolmap.run_global_positioning +
pycolmap.run_rotation_averaging only. ATE produced (cross-impl drift
vs glomap_ra-baseline expected per Decision G2).
…ailable Bug uncovered by post-port audit (audit_algorithmic_semantics.md suspect #1): the M3 cost functor WeightedBATADirectionalError was defined and built, but AddObservationToProblem unconditionally created the unweighted BATAPairwiseDirectionCostFunctor — the M5 cascade only swapped the LOSS function, not the COST function. Result: anisotropic per-keypoint weighting was dropped on every geometry residual, causing the SPLIT_METRIC_DEPTH path to trade off geometry vs metric- depth at the wrong relative scale. Fix: dispatch WeightedBATADirectionalError when image.angular_stddevs[fid] is populated (videosfm pipeline always populates). sigma_x, sigma_y from the Vector2d entry; sigma_z = 0.5*(sigma_x+sigma_y). Falls back to BATAPairwiseDirectionCostFunctor when sigmas absent. Mirrors fork's dispatch at glomap/glomap/estimators/global_positioning.cc:592-601, 646-657. Verified ATE recovery on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0 + canonical DB + GP_SEED=1: - Pre-fix: 0.00 / 0.00 / 17.22 / 57.92 / 95.79 - Post-fix: 39.44 / 68.32 / 93.66 / 96.83 / 99.68 - Baseline: 37.40 / 66.77 / 93.35 / 96.68 / 99.67 (glomap_ra) Native port now produces parity-or-slightly-better ATE than the pre-port glomap_ra baseline. Cross-implementation drift is within ~5% (Decision G2 sanity threshold).
When random_seed == -1 (caller explicitly opts out of deterministic seeding) fall back to the GP_SEED env var so the documented Tier-2 byte-identity recipe (CLAUDE.md "ATE byte-identity") works without patching every call site for the transition cycle. Plan-vs-execution audit flagged this: header docstring claimed GP_SEED fallback existed but ctor only honored options_.random_seed >= 0. Adding cstdlib for std::getenv/std::atoi.
…produce-fork TODOs Per user audit ask: any place where M0–M13 changed colmap4 default behavior to reproduce the pyglomap fork should be marked as a TODO not silently hardcoded — the eventual goal is to converge back to native defaults. Two GP-side crutches: - random_seed default 1 (upstream is -1 / random_device) - GP_SEED env-var fallback in ctor when random_seed == -1 Both stay in place for Tier-2 byte-identity but are now marked TODO(reproduce-fork). Companion notes: .claude/notes/glomap_audit/audit_default_overrides.md
Six fork-imported headers in colmap/sfm/ now carry a per-file TODO comment pointing at the dedup audit (.claude/notes/glomap_audit/audit_glomap_files_vs_colmap4.md). Each banner names which symbols in that TU duplicate native colmap4 helpers vs which are unique fork algorithms. grep -rn 'TODO(dedup-glomap-vs-colmap4)' colmap/src now surfaces the ~250-350 LOC dedup follow-up scoped for after the M0-M13 PR merges.
…ault) Per backward-compat study (audit_backward_compat.md): make vanilla upstream colmap4 the default for both GP and RA so non-fork callers get unmodified upstream behavior. Every fork-added feature now requires an explicit opt-in. GlobalPositionerOptions — 3 changes: - New `use_observation_exclusions` (default false): gates the per-observation skip via `image.is_excluded[idx]` in AddObservationToProblem (Gate G-1). - New `use_lc_observations` (default false): gates the `track.lc_elements` second loop in AddPoint3DToProblem and the paired sweep in FilterDepthOutliers. This IS the GP-side LC config — pairs with RA's `skip_risky_LC_pairs` to give two complementary LC controls (Gate G-2). - `random_seed` default restored from 1 -> -1 (upstream non-deterministic random_device). The GP_SEED env-var fallback in the ctor stays as a documented escape for the Tier-2 byte-identity recipe (Gate G-3). RotationEstimatorOptions — 1 change: - New `prioritize_tracking_in_mst` (default false): decouples M10 MST LC-penalty from M11 `use_video_constraints`. Previously `ComputeMaximumPoseGraphSpanningTree(prioritize_tracking=...)` was hardcoded to `use_video_constraints`; now a separate knob (Gate R-1). pycolmap binding: 3 new `def_readwrite` entries on GlobalPositionerOptions (use_observation_exclusions, use_lc_observations) and RotationEstimatorOptions (prioritize_tracking_in_mst). Net effect: - Vanilla pycolmap.run_global_positioning / run_rotation_averaging callers now get upstream colmap4 behavior unmodified. - videosfm flips all four gates true via _to_native_gp_options / _to_pycolmap_ra_options so the existing pipeline is unchanged. Smoke-tested: defaults are False/-1, post-flip True/1.
Three-step dedup pass (audit_glomap_files_vs_colmap4.md). Replace fork-imported helpers that duplicated existing colmap4 native code. #1 union_find_glomap.h (-42 LOC) - DELETED. colmap::UnionFind<T> at colmap/math/union_find.h:40 is a strict superset (Reserve / FindIfExists / Compress / Parents) and the body of Find/Union is byte-equivalent. - Added 1-line Clear() to native colmap::UnionFind so it covers the one method TrackEngine used that wasn't already in native. - track_establishment_glomap.h now includes colmap/math/union_find.h and uses unqualified UnionFind (resolves to colmap::UnionFind via parent-namespace lookup). #2 rigid3d_glomap.{h,cc} (-79 LOC) - DELETED entirely. All 8 symbols turned out to be dead code: - DegToRad / RadToDeg duplicated colmap/math/math.h:59-64 - RotationToAngleAxis / AngleAxisToRotation / Rigid3dToAngleAxis / CalcAngle*2 / CalcTrans / CalcTransAngle were declared and defined but never called from any TU after M0-M13. - Includers (image_pair_inliers_glomap.h, track_filter_glomap.cc) switched to direct includes of colmap/math/math.h + colmap/geometry/rigid3.h. #3 two_view_geometry_glomap.{h,cc} (-83 LOC) - DROPPED EssentialFromMotion (= EssentialMatrixFromPose at colmap/geometry/essential_matrix.cc:97). - DROPPED SampsonError(E, Vec2, Vec2) overload (= ComputeSquaredSampsonError at essential_matrix.cc:168, callers pass .homogeneous()). - DROPPED HomographyError (= ComputeSquaredHomographyError at colmap/geometry/homography_matrix.cc:266, identical formula). - DROPPED FundamentalFromMotionAndCameras (was 1-line wrapper, dead code — never called). - KEPT CheckCheirality (depth-gated single-pair extension, no native equivalent), GetOrientationSignum (vendored from GC-RANSAC), SampsonError(E, Vec3, Vec3) (depth-aware z-divide variant). - image_pair_inliers_glomap.cc updated to call native helpers; added geometry/essential_matrix.h + geometry/homography_matrix.h includes. Net: -204 LOC across 4 files dropped + 6 files updated. Build clean, pycolmap loads, defaults unchanged.
Final dedup step (audit_glomap_files_vs_colmap4.md). Replace the
vendored `glomap_ra::Track` struct with native `colmap::Point3D`
throughout TrackEngine, TrackFilter, and the 2 binding TUs.
The vendored type was almost a verbatim duplicate of Point3D:
- xyz / color / is_initialized -> already on Point3D (M0/glomap-fork)
- observations (vector<pair>) -> Point3D::track.Elements() (vector<TrackElement>)
- lc_observations -> Point3D::track.lc_elements (already added at scene/track.h:93)
- track_id -> redundant with the map key, dropped
Changes:
- track_establishment_glomap.h: drop `Track` struct + `Observation`
typedef. `track_t` now aliases `colmap::point3D_t`. API uses
`unordered_map<track_t, Point3D>`.
- track_establishment_glomap.cc: 4 algorithm methods rewritten to
use Point3D::track.AddElement / .Elements() / .lc_elements / .Length().
- track_filter_glomap.{h,cc}: same.
- pycolmap/sfm/track_establishment_glomap.cc: drop ToPoint3D
conversion (35 LOC). Bindings now pass-through dict<int, Point3D>
with no fork↔native marshalling.
- pycolmap/sfm/track_filter_glomap.cc: drop ToGlomapRaTrack +
FromGlomapRaTrack (40 LOC). Same pass-through pattern.
Net: -115 LOC across 6 files. Build clean, pycolmap loads,
establish_full_tracks / find_tracks_for_problem /
glomap_ra.filter_tracks_by_angle / glomap_ra.filter_track_triangulation_angle
all expose the same dict<int, Point3D> shape they did before — videosfm
caller is unchanged.
Round of post-review polish on the backward-compat + dedup PRs.
Correctness / fail-loud:
- track_establishment_glomap.cc: THROW_CHECK on the two
ProcessLoopClosurePairs tracks.emplace calls (track_id
collision was previously silent).
- track_filter_glomap.cc + track_establishment_glomap.cc: switch
track.Elements().clear() -> track.SetElements({}) so the
call survives a future signature drift on Elements()
(review #7).
Docstring / comment:
- pycolmap motion_averaging.cc: random_seed binding doc now
matches the new -1 default and surfaces the GP_SEED env-var
escape at the Python boundary (review #1, #3).
- global_positioning.h: header note on use_observation_exclusions
clarifies that Image::is_excluded is itself fork-only;
vanilla pycolmap users won't typically populate it (review #2).
- rotation_averaging.h: prioritize_tracking_in_mst comment
rephrased to describe gate semantics rather than videosfm wiring
(the very coupling we just decoupled).
- two_view_geometry_glomap.h: header explains FundamentalFromMotionAndCameras
was a dead-code delete (no replacement migration), unlike the
other 3 dropped helpers which got migrated to native (review #1
generic).
- track_establishment_glomap.h: file-level note explaining why the
glomap_ra namespace alias is kept post-collapse (binding-path
stability — pycolmap submodule + videosfm import paths).
Two cleanups bundled.
D1 (loss_config dedup, audit_dedup.md):
- LossFunctionConfig{name, scale, weight} (string-based) replaced with
LossConfig{type, scale, weight} where type is the native
CeresBundleAdjustmentOptions::LossFunctionType enum.
- Free CreateLossFromConfig factory dropped; LossConfig::CreateLossFunction()
now reuses native colmap::CreateLossFunction(type, scale) plus a
3-line ScaledLoss(weight) wrap.
- Native colmap::CreateLossFunction was previously in an anonymous
namespace inside bundle_adjustment_ceres.cc; promoted to a public
forward declaration in bundle_adjustment_ceres.h so other estimators
can route through it.
- pycolmap binding: dropped the templated string-based BindLossField
helper. Each loss field is now a plain def_readwrite returning a
pycolmap.LossConfig object. videosfm side does the
string -> LossFunctionType conversion at the bridge boundary.
- arctan support deferred — only used by gravity_refinement (separate TU,
uses ceres::ArctanLoss directly), never used by GP cascade.
Comment cleanup pass:
- Strip M0..M13 milestone tags, Gate G-X / Gate R-X labels, audit_*.md
cross-refs, videosfm sets X via Y_options'' caller-knowledge phrases.
- Reduce verbose --- Glomap-fork ... (M5) ---'' banners to short
purpose lines or drop entirely. Keep WHY / hidden-constraint /
workaround comments verbatim.
- Touched: global_positioning.{h,cc}, rotation_averaging.{h,cc},
rotation_averaging_impl.{h,cc}, motion_averaging.cc binding.
Build + smoke clean. videosfm bridge updated in companion commit.
…ning.h Follow-up to D1 dedup. The loss_config.h after the rewrite was just a ~25-line struct definition with one consumer (GlobalPositionerOptions). Inline it into global_positioning.h next to PointConstraintType so the GP-specific helpers live together. - LossConfig struct + CreateLossFunction() method moved to colmap/estimators/global_positioning.h. - loss_config.h deleted; CMakeLists.txt entry removed. - Includers (global_positioning.cc, motion_averaging.cc binding) drop the include — global_positioning.h covers it transitively. Net: 1 fewer header in the tree.
Lift the loss-creation primitive out of bundle_adjustment_ceres.{h,cc}
into a dedicated colmap/estimators/ceres_loss.{h,cc} so it's a true
shared utility — not a private detail of CeresBundleAdjustmentOptions.
Changes:
- New colmap/estimators/ceres_loss.{h,cc}: top-level
colmap::LossFunctionType enum + colmap::CreateLossFunction(type, scale)
factory. Definition moved verbatim from bundle_adjustment_ceres.cc.
- bundle_adjustment_ceres.h: CeresBundleAdjustmentOptions::LossFunctionType
is now a back-compat using alias to colmap::LossFunctionType;
the now-redundant forward declaration of CreateLossFunction is dropped.
- bundle_adjustment_ceres.cc: definition deleted (moved to ceres_loss.cc).
- global_positioning.h: GlobalPositionerOptions gains configurable
loss_function_type + loss_function_weight fields. Default
HUBER preserves the previous hardcoded behavior. CreateLossFunction()
body now mirrors BA — dispatch via the factory + ScaledLoss(weight) wrap.
Drops include of bundle_adjustment_ceres.h in favor of ceres_loss.h.
- pycolmap motion_averaging.cc: expose loss_function_type +
loss_function_weight on GlobalPositionerOptions. The existing
pycolmap.LossFunctionType enum (bound via BA's binding through the
alias) is now the canonical Python-side type.
Net effect: vanilla colmap GP becomes loss-configurable like BA. The
fork's 10-bucket cascade (LossConfig) still uses the same enum — now
referenced via the top-level type rather than nested inside BA options.
Audit catch (parallel agents auditing fork-modified native files for non-fork drift). The pre-existing fork copy-ctor and operator= for colmap::Image carried all the per-feature fork fields (depth_priors, angular_stddevs, is_inlier, is_excluded, is_track_anchor, is_depth_outlier, etc.) but missed the more recently added pixel_cholesky_xy_ private member. Same class of bug as the documented `cam_from_world` regression in memory/glomap_ra_port_lto_fp_nondeterminism.md — any py::cast roundtrip or value-copy of an Image silently zeroed the field, breaking pose-cov covariance plumbing for downstream consumers. Also reorders the mem-init list to match the header declaration order. Cosmetic-only follow-ups in the same audit pass: - estimators/global_positioning.cc: dedent the extracted AddObservationToProblem body by 2 spaces (was inheriting the original loop-body indentation; cosmetic-only, no semantic effect). - scene/track.h: collapse stray double blank line. Build clean; default-constructed Image still works.
…tive primitives Per audit_dedup.md D2: replace the two fork cost functors in cost_functions/metric_depth.h:182-244 with native CovarianceWeightedCostFunctor<NormalPriorCostFunctor<1>> from cost_functions/utils.h. Mathematically equivalent: - ScalePriorError(prior=1, sigma): r = (s - 1) / sigma - LogScalePriorError(sigma_log): r = log_s / sigma_log - Native: CovarianceWeightedCostFunctor<NormalPriorCostFunctor<1>>::Create(cov, prior_vec) with cov = sigma², prior_vec = (1.0) for linear or (0.0) for log. The linear/log selection in global_positioning.cc:226-232 collapses to a single Create() call where the only branch is which constant the prior_vec carries (0.0 vs 1.0). Net: -73 LOC in metric_depth.h (the 2 functor bodies + their factories + docstrings), +9 LOC in global_positioning.cc (vec construction + Create call). Build clean; ScalePriorError/LogScalePriorError no longer exposed.
…WCF<BATA>
Per audit_dedup.md D4: replace the fork's anisotropic-weighted BATA
functor with native CovarianceWeightedCostFunctor<BATAPairwiseDirectionCostFunctor>.
The fork rotated the residual into camera frame inside the functor:
r_fork = diag(1/sigma) * R * (t_obs - s*(c2-c1))
Native CWCF whitens with sqrt(cov^-1) of a passed-in covariance.
Encoding the rotation in the world-frame covariance:
cov_world = R^T * diag(sigma^2) * R
makes ||r_native||^2 == ||r_fork||^2 (since both equal
r_world^T * cov_world^-1 * r_world). Cost contribution is identical;
residual vectors differ but Ceres loss functions act on the squared
norm so the optimization converges to the same minimum.
Why R can be baked at construction time: GP rotations are frozen
post-RA. Ceres parameter blocks are (position1, position2, scale)
only — rotation is never updated by the optimizer.
Made BATAPairwiseDirectionCostFunctor CWCF-compatible by adding the
2-line metadata (kNumResiduals + kParameterDims) the wrapper expects.
This parallels what NormalPriorCostFunctor already does and is an
upstream-friendly addition.
Net: -60 LOC functor body in motion_averaging.h, +14 LOC at the
dispatch site (cov_world construction + CWCF::Create call).
WeightedBATADirectionalError no longer exposed.
Risk: medium — WeightedBATADirectionalError was the M5 critical fix
that recovered ATE 0 -> 39. Math is provably equivalent in cost, but
autodiff Jacobian path differs vector-wise so FP precision may
shift slightly. Needs sample-10 LaMAR HGE ATE check before merge.
Per audit_glomap_files_vs_colmap4.md Phase 5 (Strategy A): both native
GlobalMapper::EstablishTracks and fork TrackEngine::EstablishFullTracks
implemented the same union-find + intra-image consistency + length
filter + greedy subsample algorithm over post-RANSAC corr-graph
inliers. Extract the core into a new shared helper so both consumers
route through the same code; fork's LC second-pass +
2-view-depth-prior gate stay as fork-specific extensions.
New: src/colmap/sfm/track_establishment.{h,cc} (~210 LOC)
- struct TrackEstablishmentOptions{intra_image_consistency_threshold,
min_num_views_per_track, required_tracks_per_view}
- using MatchPredicate = std::function<bool(image_t, point2D_t,
image_t, point2D_t)> — optional, returns true to skip a match in
the UF phase. Used by fork to skip LC inliers in the regular pass.
- EstablishTracksFromCorrGraph(valid_pair_ids, corr_graph,
image_id_to_keypoints, options, ignore_match) — returns
unordered_map<point3D_t, Point3D>.
GlobalMapper::EstablishTracks shrinks from ~135 LOC to ~30 LOC of
orchestration: builds the keypoint map + collects valid pair ids
from pose_graph, calls the helper, commits the result via
AddPoint3D. No behavior change for native callers — predicate
defaults to nullptr (no LC skip), options carry the same defaults
as the previous inline impl.
TrackEngine::EstablishFullTracks now:
- Builds image_id_to_keypoints from images_ (fork's Image.features
rather than reconstruction's points2D — pre-rec state).
- Pre-computes an LC observation set from view_graph.image_pairs[*]
inliers + are_lc, encoded as pair<uint64_t, uint64_t> for std::set
lookup.
- Calls EstablishTracksFromCorrGraph with an ignore_match predicate
that skips matches in the LC set.
- Calls existing ProcessLoopClosurePairs as the post-pass to populate
Track::lc_elements (and mint two new tracks per LC-only observation
pair, with obs-key-derived ids that don't collide with native's
dense [0, N) range since image_id<<32 >= 2^32).
Deleted from track_establishment_glomap.{h,cc}:
- TrackEngine::BlindConcatenation (~55 LOC)
- TrackEngine::TrackCollection (~110 LOC)
- UnionFind<image_pair_t> uf_ member
- Now-unused colmap/math/union_find.h include
Total: ~165 LOC removed from fork; ~210 LOC added in the new shared
helper (most of which is moved-not-new code).
Backward compat:
- Native API gains an optional ignore_match parameter defaulting
to nullptr — vanilla callers see identical behavior.
- Fork API unchanged (TrackEngine::EstablishFullTracks signature +
return shape preserved).
- pycolmap binding unchanged.
Risk: medium — core algorithm is byte-equivalent (verbatim move of
the loop bodies), but unordered_map iteration order may differ
between native's local instance and fork's instance, which can
produce a different greedy-subsample tiebreak. Default options
have required_tracks_per_view=INT_MAX (no-op subsample), so
iteration order doesn't matter for native. Fork's caller passes
require_tracks_per_view=INT_MAX as well at this stage; the actual
greedy subsample lives in FindTracksForProblem (untouched).
Needs sample-10 LaMAR HGE ATE check before merge.
Phase 5 first-run verify revealed that ExtractMatchesBetweenImages reads
from CorrespondenceGraph::flat_corrs which is only populated by
FinalizeAfterMatchingComplete (native pipeline's geom-verify step). The
fork-driven videosfm pipeline writes inliers directly into
ImagePair::matches + ImagePair::inliers without going through
flat_corrs, so the helper saw zero matches and produced zero tracks.
Fix: iterate ImagePair::matches indexed by ImagePair::inliers directly.
Both the native colmap4 pipeline (which populates these fields during
geom-verify) and downstream pipelines that populate them via custom
relative-pose flows (e.g. videosfm's MDRP) work through this path.
Drops ExtractMatchesBetweenImages dependency entirely; drops the
feature/types.h include that came with FeatureMatches.
Verified end-to-end: sample-10 LaMAR HGE produces 25/25 registered
images with AUC 73.69/86.85/97.37 @ {0.05, 0.1, 0.5}m. Tier-1
structural identity holds through GP1+GP2 (n_tracks, total_observations,
total_lc_observations all match m13 baseline byte-for-byte at every
checkpoint).
Spread across 4 files: - src/colmap/estimators/cost_functions/motion_averaging_test.cc (+9 tests) CWCF<BATAPairwiseDirectionCostFunctor> math equivalence with random inputs (rotation, sigma, c1, c2, scale, t_obs); zero-residual edge case; identity-rotation isotropic-sigma reduction. Algebraically proves D4 dedup (cov_world = R^T diag(sigma^2) R) preserves the squared residual norm of the deleted WeightedBATADirectionalError. - src/colmap/sfm/track_establishment_test.cc (+5 tests, NEW file) Phase 5 helper EstablishTracksFromCorrGraph: 3-pair triangle smoke test, length filter drops short tracks, intra-image-consistency drop, ignore_match predicate, empty-input no-crash. - src/colmap/estimators/cost_functions/metric_depth_test.cc (+7 tests, NEW) MetricDepthError: zero residual when prior matches predicted depth; linear vs log residual mode formulas; zero_residual_behind_camera gate; log-scale parameterization; 5-toggle smoke; rejects non-positive sigma. - src/colmap/estimators/global_positioning_test.cc (+4 tests) LossConfig::CreateLossFunction(): TRIVIAL/HUBER/CAUCHY/SOFT_L1 dispatch through native colmap::CreateLossFunction; weight != 1 triggers ScaledLoss wrap (verified by 2x scaling on output rho). CMakeLists entries added for the two new test files; existing TESTS_ENABLED=ON build picks them up. All 4 ctest groups pass.
Phase 5 left ProcessLoopClosurePairs minting new tracks via static_cast<track_t>((image_id<<32)|feature_id). With image_id >= 1 this lands ids >= 2^32, well above native's dense [0, N) range, so collisions are theoretical-only. Switch to sequential ids continuing from native's max — eliminates the magic encoding and makes the THROW_CHECK guard truly defensive rather than load-bearing.
Round 2 of unit-test additions (6 areas, 34 tests). Driven by the 'genuine coverage gaps' list — fork-unique algorithms previously tested only via end-to-end pipeline. ProcessLoopClosurePairs (track_establishment_test.cc, +5): 4 branches of LC second-pass + sequential-id no-collision smoke. FindTracksForProblem (track_establishment_test.cc, +6): Length filter, 2-view depth-prior gate (drop + keep + boundary), greedy quota, min_num_tracks_per_view default-bug enshrined. TrackFilter (track_filter_test.cc, NEW, +7): FilterTracksByAngle aligned/misaligned/uncalib-double-thresh + point-behind-camera; FilterTrackTriangulationAngle parallel/perp + threshold sweep. RelativeRotationError (rotation_averaging_test.cc, +3): Zero residual when consistent, non-zero on injected angular error matches the discrepancy norm, swap+invert symmetry. Required moving the struct from anonymous namespace in rotation_averaging_impl.cc into rotation_averaging_impl.h so tests can construct it directly. ImagePairsInlierCount (image_pair_inliers_test.cc, NEW, +5): 4-gate degeneracy (epipolar, cheirality, epipole-angle, depth-prior vs nodepth threshold). All 4 gates exercised on the essential-matrix path with controlled synthetic inputs. Backward-compat gates G-1/G-2/R-1 (+8): G-1 (use_observation_exclusions) off/on/partial — global_positioning_test.cc G-2 (use_lc_observations) off/on — global_positioning_test.cc R-1 (prioritize_tracking_in_mst) off/on/no-corr-graph-fallback — rotation_averaging_test.cc. Required exposing ComputeMaximumPoseGraphSpanningTree as a public symbol in rotation_averaging.h (was anon-namespace). Net: 65 total unit tests across 8 ctest groups, all passing.
The ``colmap::glomap_ra::`` namespace and pycolmap.glomap_ra submodule
were named after the original rotation-averaging port from glomap, but
their current contents are track establishment, track filtering, and
pair-inlier scoring — none of which are RA. Rename to ``sfm_ext``
(SFM extensions) so the label reflects the actual contents.
Mechanical sweep across 11 C++ files + 4 binding files:
- ``namespace glomap_ra { ... }`` -> ``namespace sfm_ext { ... }``
- ``glomap_ra::Foo`` -> ``sfm_ext::Foo`` everywhere
- ``GetOrCreateGlomapRaModule`` -> ``GetOrCreateSfmExtModule``
- ``m.def_submodule("glomap_ra")`` -> ``m.def_submodule("sfm_ext")``
Python alias kept for back-compat: pycolmap/__init__.py defines
``glomap_ra = sfm_ext`` so existing videosfm callers
(``pycolmap.glomap_ra.filter_tracks_by_angle`` etc.) keep working
unchanged. Filenames keep the ``_glomap`` suffix for now — file rename
is a separate, larger churn.
Verified:
- Full build clean (warm).
- 8/8 ctests pass: track_establishment_test, track_filter_test (7/7),
image_pair_inliers_test, motion_averaging_test, metric_depth_test,
global_positioning_test, rotation_averaging_test (controllers +
estimators).
- pycolmap import + alias intact: pycolmap.glomap_ra is pycolmap.sfm_ext,
filter_tracks_by_angle resolves on both.
- videosfm GlobalPositioningPhase imports OK with the alias.
Cleans up post-rename comment debt: - image_pair_inliers_glomap.h: remove TODO that already shipped (Sampson/H errors now route through native helpers); drop 'vendored from glomap/types.h' framing on InlierThresholdOptions. - track_filter_glomap.cc: replace 'glomap RA pipeline' / 'RA-shadow' wording with neutral 'dict-of-tracks state used by sfm_ext'. - pycolmap/sfm/track_filter_glomap.cc: drop pyglomap.* Python-name mentions (module no longer exists). - pycolmap/sfm/track_establishment_glomap.cc: drop 'Glomap-style' from establish_full_tracks docstring.
Second comment-cleanup pass beyond the sfm_ext-renamed files. Strips 'mirrors pyglomap.X', 'byte-for-byte parity with pyglomap', 'so the videosfm pyglomap shim can be retired' framing now that pyglomap is gone and the videosfm shim is removed. Preserved: design rationale (e.g. issue colmap#40 reference for adopting CalibrateViewGraph wrapper, pybind11 deep-copy gotcha), open algorithmic TODOs unrelated to the rename.
…lishment
Removes the parallel colmap::sfm_ext::TrackEngine path. The fork-specific
behaviors that lived in track_establishment_glomap.{h,cc} now live as
native free functions in track_establishment.{h,cc} and as methods on
GlobalMapper. Net diff -300 LOC; one less namespace; one less file pair.
Native types already carried the data (Track::lc_elements,
ImagePair::are_lc, Image::depth_priors / depth_prior_validity were added
during the M0-M13 port). This commit fills in the missing code paths:
New free functions in colmap::namespace (track_establishment.{h,cc}):
- MakeLoopClosureMatchPredicate(valid_pair_ids, corr_graph) - returns a
MatchPredicate that excludes ImagePair::are_lc[idx]==true inliers from
the union-find pass, pre-computing the LC set in O(num inliers).
- AppendLoopClosureObservations(valid_pair_ids, corr_graph, tracks) -
4-branch second pass that populates Track::lc_elements (no-track /
both-tracks / one-track-each / same-id, mints new tracks at
max(track_id)+1). Operates on a tracks dict so binding + GlobalMapper
share the call site.
- struct TrackSubsampleOptions + SubsampleTracks(...) - depth-aware
greedy length-sorted subsample. Reads depth_priors / depth_prior_validity
/ registered_image_ids passed as separate maps to keep the function
Reconstruction-free. Asymmetric length filter preserved (lower bound
counts Length+lc_elements, upper bound counts Length only).
GlobalMapper extensions (global_mapper.{h,cc}):
- GlobalMapperOptions gains 4 fields (default-off so vanilla colmap
behavior is unchanged): track_lc_second_pass, track_two_view_depth_gate,
track_max_num_views_per_track, track_max_num_tracks.
- EstablishTracks runs MakeLoopClosureMatchPredicate +
AppendLoopClosureObservations when track_lc_second_pass=true; bypasses
the helper-side greedy subsample in that mode.
- New SubsampleTracksForProblem method bridges Reconstruction <-> dict and
calls SubsampleTracks.
Pycolmap binding (track_establishment_glomap.cc - file kept, contents
rewritten to use native API):
- pycolmap.establish_full_tracks(view_graph, images, options,
lc_second_pass=False) - top-level binding (path preserved). Takes the
native pycolmap.TrackEstablishmentOptions; lc_second_pass optional
kwarg toggles the LC second pass.
- pycolmap.find_tracks_for_problem(view_graph, images, tracks_full,
options) - takes pycolmap.TrackSubsampleOptions (new bound struct).
- The old pycolmap.TrackEstablishmentOptions (fork shape) is gone. The
native TrackEstablishmentOptions has 3 fields
(intra_image_consistency_threshold, min_num_views_per_track,
required_tracks_per_view); the 5 subsample knobs (max_num_views_per_track,
max_num_tracks, two_view_depth_gate, etc.) live on the new
TrackSubsampleOptions.
Tests (track_establishment_test.cc):
- All 16 tests preserved (5 native + 6 FindTracksForProblem-now-SubsampleTracks
+ 5 ProcessLoopClosurePairs-now-AppendLoopClosureObservations).
- MinTracksPerViewBugDocumentation rewritten to describe the new int-typed
INT_MAX default (signed comparison never trips) instead of the old
signed/unsigned -1 -> UINT_MAX promotion. Same observable behavior.
Adjacent (track_filter_glomap.{h,cc} + binding + tests):
- Repointed track_t / feature_t typedef include from the deleted
track_establishment_glomap.h to colmap/util/types.h (the canonical
point3D_t / point2D_t aliases). track_t / feature_t aliases are gone.
Files DELETED:
- src/colmap/sfm/track_establishment_glomap.h (-82 LOC)
- src/colmap/sfm/track_establishment_glomap.cc (-311 LOC)
Verified: scripts/build.sh clean (cold + warm). 7 ctest groups pass
(scene/track, scene/image, sfm/track_establishment, sfm/track_filter,
sfm/image_pair_inliers, estimators/global_positioning, estimators/
rotation_averaging) - 100% in 0.3s.
Three-stage cleanup mirroring the TrackEngine fold (commit 77c0574). Net result: ZERO _glomap files in colmap/sfm/, ZERO sfm_ext namespace references. All fork-extension code now lives in plain colmap:: namespace alongside upstream code, with no naming tax. Stage 1 - two_view_geometry_glomap deletion (-79 LOC): - 3 helpers (CheckCheirality single-pair, GetOrientationSignum, SampsonError Vec3 depth-aware) had ONE caller each (image_pair_inliers). Folded as anonymous-namespace statics in the consumer file. Deleted two_view_geometry_glomap.{h,cc} + CMakeLists entry. Stage 2 - track_filter_glomap rename (no LOC delta, +/- ~250): - Renamed src/colmap/sfm/track_filter_glomap.{h,cc} -> track_filter.{h,cc}. - Dropped sfm_ext:: namespace -> plain colmap::. - Converted struct TrackFilter::FilterX static methods to free functions FilterTracksByAngle / FilterTrackTriangulationAngle. - Pycolmap binding: pycolmap.sfm_ext.filter_tracks_by_angle -> pycolmap.filter_tracks_by_angle (top-level). Same for filter_track_triangulation_angle. Test file + downstream callers updated. Stage 3 - image_pair_inliers_glomap rename (no LOC delta): - Renamed src/colmap/sfm/image_pair_inliers_glomap.{h,cc} -> image_pair_inliers.{h,cc}. - Dropped sfm_ext:: namespace -> plain colmap::. - Behavior preserved verbatim (hard-coded thres_epipole_nodepth, disabled Gate-3 angle check, F-branch positive_count==negative_count silent drop). TODOs for later cleanup left in-place. - Pycolmap binding: pycolmap.image_pairs_inlier_count was already top-level; renamed BindImagePairInliersGlomap -> BindImagePairInliers. - Renamed src/pycolmap/sfm/track_establishment_glomap.cc -> track_establishment.cc; renamed BindTrackEstablishmentGlomap -> BindTrackEstablishment. Same fold treatment. Pycolmap top-level surface (no sfm_ext submodule any more): - pycolmap.establish_full_tracks / find_tracks_for_problem - pycolmap.filter_tracks_by_angle / filter_track_triangulation_angle - pycolmap.image_pairs_inlier_count - pycolmap.TrackEstablishmentOptions / TrackSubsampleOptions / InlierThresholdOptions Removed back-compat alias pycolmap.glomap_ra = pycolmap.sfm_ext (no remaining callers; sfm_ext submodule itself is gone). Verified: scripts/build.sh clean. 7 ctest groups pass (scene/track, scene/image, sfm/track_establishment, sfm/track_filter, sfm/image_pair_inliers, estimators/global_positioning, estimators/ rotation_averaging) - 100% in 0.29s.
…ield Audit showed gravity_dir is exposed in the binding (motion_averaging.cc) + declared in the struct but has ZERO consumers in colmap/src/colmap/estimators/. Setting it does nothing — pure footgun for anyone who flips it expecting gravity-aware behavior. Pose-prior data is supplied separately via the run_rotation_averaging `pose_priors` argument; this field never participated in the up-vector math. The ten other dead-flag suspects from the prior audit (use_precomputed_weights, fix_non_lc_weights, fixed_non_lc_weight, consecutive_trivial, use_relative_pose_constraints, relative_pose_default_stddev, loss_relative_pose, debug_only_relative_pose, scale_regularization_sigma, image_ids_to_optimize) are already GHOSTS — not on the struct, not in the binding, not in videosfm. No additional cleanup needed.
… BindImagePairInliers definition Adds GlobalPositionerOptions::apply_uncalibrated_loss_downweight (bool, default true). When true, AddPointToCameraConstraints wraps the BATA loss in ScaledLoss(0.5) for cameras whose focal length lacks an EXIF prior (Camera::has_prior_focal_length == false) - the mainline colmap4 heuristic for downweighting bearings that depend on a VGC-estimated focal. When false, all cameras get full-weight residuals, matching the glomap fork's behavior. Default true preserves upstream colmap behavior; videosfm callers calibrated against fork-flavored full-weight residuals can flip the flag false at the bridge level (companion videosfm commit wires this through GPOptions). Bound in motion_averaging.cc. Also fixes a leftover from the earlier _glomap rename (commit db8a5a0): src/pycolmap/sfm/image_pair_inliers.cc still defined the binder as BindImagePairInliersGlomap while bindings.cc was already calling BindImagePairInliers, producing an undefined-symbol linker error any time the binding got fully relinked. The earlier build succeeded only because the .so was cached. Rename the definition to match.
Three small hardening additions + comment-text cleanup across the GP/RA C++ surface: - rotation_averaging_impl.cc: SolveIRLS now mirrors SolveL1's NaN-guard on the step vector. Without this, a singular pose-graph silently corrupts cams_from_world via UpdateState and the IRLS loop returns true with garbage residuals on the next iteration. L1 already had this guard at line 755; IRLS didn't. - rotation_averaging_impl.cc: RotationAveragingProblem ctor adds a THROW_CHECK that 'skip_risky_LC_pairs=true' requires a CorrespondenceGraph. Previously the LC-skip filter silently never triggered when CG was nullptr - the option appeared to work but did nothing. - image.h: Image::operator== now also compares pixel_cholesky_xy_. This is the field that triggered the earlier copy-ctor field-drop bug fix (commit 985b2ec); including it in equality means a future unit test of the form 'EXPECT_EQ(copy, original)' would catch an analogous regression. Other fork-extension fields (depth_priors, is_inlier, cam_from_world, etc.) are intentionally NOT added - those are mutable pipeline state, not identity-defining. - global_positioning.h: cleaned 'glomap fork' / 'videosfm callers' vocabulary leakage in the apply_uncalibrated_loss_downweight comment; the comment now describes what the bool does without referencing internal nomenclature. Also added a doc note that the 10 cascade LossConfigs only run under SPLIT_METRIC_DEPTH (BATA mode silently ignores them - prevents future caller from misconfiguring these in BATA mode and wondering why nothing changes).
Match the C++ class name (CorrespondenceGraph). Aligns variable names, parameter names, and dict keys with the type so callers see one term.
…g_in_mst The MST initializer's LC-edge penalty branch was a separate flag from the video-aware Ceres solver, but they were always enabled together (the fork wired prioritize_tracking_in_mst directly off use_video_constraints). Collapse: ComputeMaximumPoseGraphSpanningTree now reads options_.use_video_constraints, deleting the redundant flag, its pycolmap binding, and its option-struct field. The function-level prioritize_tracking parameter stays, callable directly by unit tests.
…LcPenaltyMst The flag was removed in 423c6e5; the tests now exercise the function- level prioritize_tracking parameter directly. Rename the test cases and the comment block to match the surviving concept (LC-penalty branch in ComputeMaximumPoseGraphSpanningTree).
- CreateLossFunction: replace silent `return nullptr` on unhandled enum with LOG(FATAL). The previous fall-through would crash with a nullptr deref downstream; failing at the source surfaces an enum extension oversight immediately. - run_global_positioning binding: drop the duplicate ``dmap_scale_map_nested`` result key. It was a literal alias of ``dmap_scale_map`` — same py::dict assigned twice. Caller now reads the single key.
Comment-only sweep across estimators / sfm / scene headers and impls. Rewrites describe what the code does instead of where it came from. No identifier or behavior changes.
ComputeResiduals is called from L1 and IRLS inner loops. The seed call inside it reset the global PRNG to the same value every iteration, replaying the identical jitter sequence in ComputeGravityAligned1DOFResidual. Move the seed call to RotationAveragingSolver::Solve (once per problem solve) so the PRNG advances naturally across iterations — matches the fork's behavior of seeding once at the GP/RA boundary.
Two fork-equivalent fixes to InitializeFromMaximumSpanningTree: 1. Seed cams_from_world[root] from the reconstruction's MDRP-prior pose before BFS-propagating relative rotations. The previous default-constructed identity seed rotated every descendant by R_root^-1 vs the prior gauge, which can shift IRLS weights through the max_rotation_error_deg filter. 2. Edge weight in ComputeMaximumPoseGraphSpanningTree now reads inliers.size() from the CorrespondenceGraph (post-RANSAC survivor count) when CG is plumbed through, falling back to edge.num_matches (raw match count) for vanilla colmap callers without a CG. Matches fork's MST weight semantic. Test fixture updated: pair inliers vectors are now sized to match the edge num_matches values so both weight sources agree on the MST shape.
…m fork Restores the median-z_est/depth_prior seeding the fork ran at GP1 entry. Without this, GP1 lazy-inserts dmap_scales_ at 1.0 (linear) / 0.0 (log) for every image — the optimizer starts in a different basin than the fork. Source: glomap/glomap/estimators/global_positioning.cc:1006-1133. Adapted to colmap4's Reconstruction model: iterates Points3D' track elements (and lc_elements when use_lc_observations is set) to accumulate per-image z_est/depth_prior samples, takes the median per image, and seeds dmap_scales_ with log(median) or linear median per use_log_scale_for_depth_map_scales. Call site in Solve() is gated on SPLIT_METRIC_DEPTH + use_init + initial_dmap_scales empty, so GP2 (which supplies initial_dmap_scales from GP1 output) bypasses this path. Images without observations consumed here still fall through to the lazy-insert path in AddObservationToProblem.
Sweep upstream-bound C++ comments / docstrings / test-file references for "pre-port", "post-port", "M1-M13" milestone tags, "glomap-fork", "fork-only", "videosfm-side", and the videosfm-private issue colmap#40 reference (incl. one runtime warning string). Rewrites describe what the code does now. One identifier rename: fork_sqnorm -> expected_sqnorm in motion_averaging_test.cc. No behavior change.
…m_cam1 is null The original port of glomap's UpdateImagePairsConfig added a THROW_CHECK around the UNCALIBRATED -> CALIBRATED upgrade requiring ``tvg.cam2_from_cam1.has_value()``. Pairs whose decomposition never produced a relative pose (e.g. F-only RANSAC path or decomposition failure) crash the pipeline at this site. The fork's ImagePair carried cam2_from_cam1 as a Rigid3d (always default-constructible to identity) so it never tripped this check; colmap's TwoViewGeometry::cam2_from_cam1 is std::optional<Rigid3d> so absence is real and observable. Replace the THROW_CHECK with `continue`. Pairs without a relative pose stay UNCALIBRATED and are dropped by downstream filters (inlier-num / inlier-ratio) instead of crashing. Verified end-to-end on V2-adaptive sample-10 HGE.
Zador-Pataki
added a commit
that referenced
this pull request
Apr 28, 2026
* M1-M3: drop dead RA options + extend GP options + cost functors
M1 (Hygiene):
- Drop dead glomap-fork option fields use_precomputed_weights,
fix_non_lc_weights, fixed_non_lc_weight from
colmap::RotationEstimatorOptions. Per audit (gp_inventory.md, ra_inventory.md)
these are dead in fork .cc itself or hardcoded false in pipeline. Drop
matching pycolmap def_readwrite + clean up referencing code paths in
glomap_ra::RotationEstimator (kills the obsolete SolveWeightedLS branch
+ the no-op fix_non_lc_weights warning).
- Update comment block at rotation_averaging.h:70-71 — only skip_risky_LC_pairs
+ use_video_constraints + video_*_scale survive as fork-flag stubs.
- Gate native GP first-scale pin (global_positioning.cc:349-355) on
point_constraint_type != SPLIT_METRIC_DEPTH. Preserves existing colmap GP
unit tests under default BATA mode (Q1 / R10).
M2 (Foundation):
- New file colmap/util/loss_config.h: LossFunctionConfig {name, scale,
weight} + CreateLossFromConfig factory. Names: trivial, huber, cauchy,
arctan, softlone. weight!=1 wraps in ScaledLoss.
- Extend colmap::GlobalPositionerOptions with glomap-fork additions:
PointConstraintType enum (BATA / SPLIT_METRIC_DEPTH), use_init,
random_init_scale, depth toggles (use_log_scale_for_depth_map_scales,
use_log_residual_for_depth, zero_residual_behind, smooth_log_linear_transition,
log_linear_threshold), scale_prior_stddev, filter_depth_outliers,
initial_dmap_scales (optional<map<image_t, double>>),
10 LossFunctionConfig fields with names verbatim from existing glomap_ra
port (loss_normal_geometry, loss_normal_depth, loss_lc_geometry,
loss_lc_depth, loss_normal_geometry_inlier, loss_normal_depth_inlier,
loss_normal_depth_outlier, loss_normal_geometry_trackstart,
loss_normal_depth_trackstart, loss_scale_prior).
- Change random_seed default from -1 to 1 (Decision 9, preserves
byte-identity for existing call sites that previously inherited GP_SEED=1).
- Extend GlobalPositioner state: dmap_scales_, dmap_scale_observation_counts_,
depth_outliers_, 10 cached_loss_*_ shared_ptrs, soft_outlier_fallback_loss_.
M3 (Cost functors):
- Add WeightedBATADirectionalError to cost_functions/motion_averaging.h.
AutoDiff <3,3,3,1> on (c_i, X_k, scale). Anisotropic per-axis weighting
(1/sigma_x, 1/sigma_y, 1/sigma_z). Caller computes
sigma_z = (sigma_x + sigma_y) / 2.
- New file cost_functions/metric_depth.h with 3 functors:
MetricDepthError (5-toggle, 12 reachable combinations — smooth_transition
only meaningful in use_log_residual=true branch),
ScalePriorError (linear), LogScalePriorError (log-space, prior=log(1)=0).
All ported verbatim from fork's cost_function.h.
Cluster cleanup (carried over from prior session):
- Strip 175 lines of dead Fetzer focal cost from sfm/cost_function_glomap.h.
- Strip 47 lines of FilterTracksByReprojection (unbound, unused) from
sfm/track_filter_glomap.{h,cc}.
Verified: ATE 37.40 / 66.77 / 93.35 / 96.68 / 99.67 unchanged (M0a baseline)
on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0 + canonical DB
+ GP_SEED=1.
* M4: SPLIT_METRIC_DEPTH path body in colmap::GlobalPositioner
Implement the trivial-rig case of the metric-depth residual structure.
Activated by point_constraint_type=SPLIT_METRIC_DEPTH (default still
BATA — no behavior change for native callers).
Per-observation in AddPoint3DToProblem (image.IsRefInFrame() branch only):
- When image.depth_prior_validity[fid] is true and depth_prior > 0,
emit a 1-D MetricDepthError residual on
(frame_centers_[image.FrameId()], point3D.xyz, dmap_scales_[image_id]).
- Lazy-insert dmap_scales_ on first valid observation per image. Initial
value: from options_.initial_dmap_scales (caller-supplied GP1→GP2
handoff) if provided, else 1.0 (linear) / 0.0 (log).
- Increment dmap_scale_observation_counts_ for the per-image
ScalePriorError weight.
Per-image emission in AddPointToCameraConstraints (after track loop):
- Reset dmap_scales_ + dmap_scale_observation_counts_ at start (lazy
filled by AddPoint3DToProblem).
- After all observations added: emit one ScalePriorError /
LogScalePriorError per image with depth observations. Loss is wrapped
in ceres::ScaledLoss(TrivialLoss, obs_count) so dense-depth images get
proportionally stronger priors. M5 will replace TrivialLoss with the
configurable cached_loss_scale_prior_.
Parameter ordering / bound (M4 step 5):
- dmap_scales_ ride alongside frame_centers_ in parameter group 2.
- Linear-space: SetParameterLowerBound(&dmap_scale, 0, 1e-5) prevents
collapse. Log-space: no bound (parameter is log-space, positivity comes
from exp() in MetricDepthError).
Init flag wiring (M4 steps 6-7):
- options_.use_init short-circuits InitializeRandomPositions for both
camera centers AND track xyz (collapses fork's split flag).
- options_.random_init_scale replaces hardcoded 100.0 cube size in two
call sites.
Verified: ATE 37.40 / 66.77 / 93.35 / 96.68 / 99.67 unchanged (default
BATA path) on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0 +
canonical DB + GP_SEED=1.
* M5: GP body Cluster B — loss routing fabric + two-loop iteration
Implement glomap-fork loss-routing cascade in colmap::GlobalPositioner.
Activated by point_constraint_type=SPLIT_METRIC_DEPTH; default BATA mode
unchanged.
Refactor: extract per-observation logic into AddObservationToProblem
helper (point3D_id, observation, is_lc_observation, random_initialization,
reconstruction). AddPoint3DToProblem becomes a dispatcher iterating
track.Elements() (is_lc=false) then track.lc_elements (is_lc=true) —
mirrors fork's two-parallel-collection structure.
Cache pre-warm in AddPointToCameraConstraints: materialize 10
cached_loss_*_ via CreateLossFromConfig. Default TrivialLoss/weight=1
config is equivalent to no override; non-default Huber/Cauchy/etc take
effect through cascade.
is_excluded skip: top of AddObservationToProblem.
4-way geometry cascade (SPLIT_METRIC_DEPTH only):
is_lc -> loss_lc_geometry, is_track_anchor -> loss_normal_geometry_trackstart,
is_inlier -> loss_normal_geometry_inlier, else -> loss_normal_geometry.
5-way depth cascade for MetricDepth residual:
depth_outliers_ + LC -> SKIP depth residual entirely (deletes
metric_depth_cost to avoid leak),
depth_outliers_ + non-LC -> ScaledLoss(HuberLoss(1), 1) hardcoded
soft fallback,
is_lc -> loss_lc_depth, track_anchor -> loss_normal_depth_trackstart,
is_inlier -> loss_normal_depth_inlier, MDRP-flagged outlier ->
loss_normal_depth_outlier, else -> loss_normal_depth.
ScalePriorError wrapper updated: replaces M4 TrivialLoss(obs_count)
with cached_loss_scale_prior_ wrapped in ScaledLoss(obs_count). Default
config preserves obs_count weighting unchanged.
Verified ATE 37.40/66.77/93.35/96.68/99.67 unchanged (default BATA)
on sample-10 LaMAR HGE.
* M6: GP filter_depth_outliers pre-Solve pass
Implement glomap-fork pre-Solve depth-outlier filter in
colmap::GlobalPositioner. Activated by
options_.filter_depth_outliers + point_constraint_type=SPLIT_METRIC_DEPTH.
In AddPointToCameraConstraints, after cache pre-warm + dmap_scales_
reset:
- Seed dmap_scales_ from options_.initial_dmap_scales when caller
supplied them (typical GP2 path: GP1 hands off solved scales). Linear
scale converted to log if use_log_scale_for_depth_map_scales=true.
Lazy-insert in AddObservationToProblem then preserves these values.
- Clear depth_outliers_; if filter_depth_outliers, call
FilterDepthOutliers(reconstruction).
FilterDepthOutliers method (new):
- Per-point3D: walk track.Elements() and track.lc_elements separately.
- For each observation: skip when depth_prior_validity[fid]=false or
depth_prior <= 1e-6 or stddev_rel <= 1e-9.
- Apply per-image scale (dmap_scales_[image_id]) if available; else
raw prior. Threshold check:
|log(z_est) - log(scale * depth_prior)| >= 3 * log(1 + stddev_rel)
- Insert (image_id, point2D_idx) into depth_outliers_ on flag.
Anonymous-namespace helper DepthOutlierFlag deduplicates the regular vs
LC sweep.
Native uses image.CamFromWorld() * X_world (the canonical cam_from_world
projection) — fork's pre-Solve branch had inadvertently relied on the
in-Solve flipped translation convention via the image.cam_from_world
member directly; native's CamFromWorld() resolves cleanly because we run
this BEFORE the Solve loop temporarily flips frame_centers_.
Verified ATE 37.40/66.77/93.35/96.68/99.67 unchanged (default BATA mode +
filter_depth_outliers=false) on sample-10 LaMAR HGE.
* M7 (binding side): extend pycolmap.run_global_positioning for SPLIT_METRIC_DEPTH
Pycolmap binding for native colmap::GlobalPositioner now exposes the full
glomap-fork option surface (M2) + dict return shape (Decision 7).
Option struct extension (BindGlobalPositioner):
- New PointConstraintType enum binding (BATA / SPLIT_METRIC_DEPTH).
- Bind use_init, random_init_scale, depth toggles
(use_log_scale_for_depth_map_scales, use_log_residual_for_depth,
zero_residual_behind, smooth_log_linear_transition,
log_linear_threshold), scale_prior_stddev, filter_depth_outliers.
- Bind initial_dmap_scales as Optional[Dict[image_id, float]] with None
short-circuit; Python passes linear-space, C++ stores per
use_log_scale_for_depth_map_scales convention.
- Bind 10 LossFunctionConfig fields as dict-typed properties via
templated BindLossField helper accepting/emitting
{'name', 'scale', 'weight'}. Names verbatim from glomap_ra port:
loss_normal_geometry, loss_normal_depth, loss_lc_geometry,
loss_lc_depth, loss_normal_geometry_inlier, loss_normal_depth_inlier,
loss_normal_depth_outlier, loss_normal_geometry_trackstart,
loss_normal_depth_trackstart, loss_scale_prior.
run_global_positioning return shape:
- Was: bool. Now: dict {success: bool, dmap_scale_map: Dict[image_id,
float] (linear), dmap_scale_map_nested: Dict[image_id, {linear, log}]}.
- Reconstruction is mutated in place; Python reads images/tracks from
rec.images / rec.points3D directly.
- Mirrors glomap_ra binding return shape so videosfm caller migration
(Python side, deferred to next session) is a near-1:1 swap.
GetDmapScales accessor on GlobalPositioner exposes the per-image
dmap_scales_ map for the binding's dict-extraction step. Linear
conversion (exp() when use_log_scale_for_depth_map_scales=true) happens
in the lambda.
Verified: ATE 37.40/66.77/93.35/96.68/99.67 unchanged on sample-10 LaMAR
HGE — caller still on glomap_ra path; native binding extension is
additive and doesn't affect existing flow.
* M7 (C++): scales_ reservation fix + dmap_scales std::map + Ceres options bindings
Bug fixes uncovered during M7 caller migration:
1. SetupProblem scales_.reserve undercount: was only counting
point3D.track.Length(); M5's two-loop iteration also walks
track.lc_elements, each emplace_back-ing a new scale. Without
reserved capacity, vector reallocation invalidates earlier &scale
pointers stored in Ceres residual blocks → "ParameterBlock with
size 1 has at least one invalid value". Fix: include lc_elements
in the reservation count.
2. dmap_scales_ container: switched from unordered_map<image_t, double>
to std::map<image_t, double>. Ceres residuals store
&dmap_scales_[image_id] data pointers; a hash-table rehash during
lazy-insert in AddObservationToProblem invalidates them. std::map
(rb-tree) keeps pointers stable for the entry's lifetime.
3. Parameter group separation: dmap_scales_ now go in their own
parameter group (group_id+1), separate from frame_centers_. Mixing
1-D dmap_scales with 3-D frame_centers in the same Schur-ordering
group breaks the Schur-complement preprocessor — Ceres downgrades
to SPARSE_NORMAL_CHOLESKY then rejects the inhomogeneous block sizes.
Separate group keeps each Schur block size-uniform.
4. Pycolmap binding: added solver_options.{num_threads,
max_num_iterations, function_tolerance, gradient_tolerance,
parameter_tolerance} as def_property pass-throughs on
GlobalPositionerOptions. The solver_options nested struct is not
directly bound in pycolmap, so caller migration would otherwise
fail with "object has no attribute 'solver_options'".
Mirrors the existing pyglomap binding's pattern.
Verified GP runs to CONVERGENCE (no more spurious FAILURE termination)
on sample-10 LaMAR HGE under both BATA and SPLIT_METRIC_DEPTH paths.
ATE differs from glomap_ra baseline (cross-implementation drift expected
per Decision 7); structural integrity restored.
* M8: plumb IRLS final weights via out-param (preserves bool return ABI)
Glomap-fork RA contributed a per-pair IRLS final-weight map that the
videosfm pipeline reads for its consecutive-pair-weight degeneracy
diagnostic (rotation_averaging.py:117-129). Native colmap RA discards
the IRLS weight vector after the final factorize. M8 plumbs it out.
Plumbing (4 layers):
1. RotationAveragingProblem gains final_weights_ member +
FinalWeights() accessor + SetFinalWeightsFromIRLS(weights_irls)
setter. The setter translates the per-row weight vector into a
per-pair map keyed by image_pair_t. Each PairConstraint owns 1
(gravity-aligned) or 3 (full-3DOF) consecutive rows starting at
constraint.row_index — we read weight at row_index (all rows
share the same weight per ComputeIRLSWeights). Gauge-fixing rows
excluded.
2. SolveIRLS captures the last successful iteration's weight vector
in last_weights and calls problem.SetFinalWeightsFromIRLS at the
end (after the convergence loop). Empty if SolveIRLS doesn't run
(e.g. L1-only path).
3. RotationEstimator::EstimateRotations + SolveRotationAveraging gain
std::unordered_map<image_pair_t, double>* final_weights = nullptr
out-param. When non-null, copy problem.FinalWeights() to *out
after the solve.
4. RunRotationAveraging mirrors. Wired through both branches (with /
without rig expansion).
Pycolmap binding:
- Adds extract_final_weights: bool = False kwarg. Default returns
bool (preserves upstream colmap test ABI per Decision 8). When True,
returns {success: bool, final_weights: Dict[image_pair_t, float]}
dict. C5 fix: indexed by constraint.row_index (the row-major index
into weights_irls), NOT by pair_info.index (the inventory's earlier
draft).
Default behavior unchanged. Verified the C++ builds clean.
* M9: skip_risky_LC_pairs filter via 4-layer CorrespondenceGraph plumbing
Glomap-fork RA's skip_risky_LC_pairs drops pairs whose loop-closure
inlier count strictly exceeds non-LC inliers. The filter reads
ImagePair.{inliers, are_lc} fork fields — these live on
CorrespondenceGraph::ImagePair, NOT on PoseGraph::Edge (which is a
strict subset). M9 plumbs an optional CorrespondenceGraph* through 4
layers so RA can apply the filter without bloating the upstream
PoseGraph interface.
Plumbing:
1. CorrespondenceGraph gains const ImagePairsMap() accessor (the
existing MutableImagePairs() can't be called through a const
pointer). Used only by RA M9.
2. RotationAveragingProblem ctor takes optional CorrespondenceGraph*
(default nullptr). Stored as correspondence_graph_ member.
3. BuildPairConstraints reads cg.ImagePairsMap()[pair_id].{inliers,
are_lc} when options_.skip_risky_LC_pairs && correspondence_graph_
non-null. Counts LC inliers; if lc_count > non_lc_count, continue
(pair excluded from constraint matrix entirely).
4. RotationEstimator::EstimateRotations + SolveRotationAveraging +
RunRotationAveraging gain optional CorrespondenceGraph* parameter
(default nullptr; passes through to RotationAveragingProblem ctor).
5. Pycolmap binding adds correspondence_graph: CorrespondenceGraph =
None kwarg. Default behavior unchanged for callers that don't pass
the CG.
Default behavior: with skip_risky_LC_pairs=False (default) or
correspondence_graph=None, no filtering — exactly matches pre-M9 behavior.
Verified C++ builds clean.
* M10: prioritize_tracking MST penalty for LC-dominated edges
Glomap-fork RA's prioritize_tracking spanning-tree initialization
penalises LC-dominated edges so the MST picks tracking-dominated edges
as parents. Activated when use_video_constraints=true (M11 video-Ceres
path). Without the penalty, LC-contaminated initialization undermines
the Ceres video-aware solve.
ComputeMaximumPoseGraphSpanningTree (local helper in
rotation_averaging.cc) now accepts:
- prioritize_tracking (bool, default false): enable LC-penalty.
- correspondence_graph (CorrespondenceGraph*, default nullptr):
source of ImagePair.{inliers, are_lc} fork fields.
When prioritize_tracking + correspondence_graph are both set, the MST
weight for each edge is reduced by 1e9 if the pair has strict LC
majority (lc_count > non_lc_count). Native MST maximises weights, so
fork's +1e9 to weights_boost becomes -1e9 to weight here
(convention inversion). Fallback to plain num_matches weight when
fork fields absent or empty.
InitializeFromMaximumSpanningTree forwards the CG; reads
options_.use_video_constraints to set prioritize_tracking. Wired
through SolveRotationAveraging from EstimateRotations + RunRotationAveraging.
Default behavior unchanged when use_video_constraints=false (no penalty
applied). Verified C++ builds clean.
* M11: video-Ceres SolveCeres + IsTrackingPair + RelativeRotationError
Glomap-fork RA video-aware Ceres path. Activated when
options_.use_video_constraints && !options_.use_gravity. Replaces L1+IRLS
with a Ceres optimization over per-frame 3-DOF angle-axis blocks. Each
pair contributes a relative-rotation residual wrapped in either
HuberLoss (tracking-dominated pair, scale=video_tracking_huber_scale)
or CauchyLoss (LC-dominated pair, scale=video_lc_cauchy_scale).
Anonymous-namespace helpers:
- RelativeRotationError: AutoDiff<3, 3, 3> functor on (r1_aa, r2_aa).
Residual = AngleAxis(R2^T * R_rel * R1) where R_rel is precomputed
from PairConstraint::Full3DOF::R_cam2_from_cam1. Sign convention
matches fork (no leading negative — the resulting AngleAxis is
identical sign to the L1+IRLS tangent residual at convergence;
Ceres doesn't care about sign anyway since LM minimizes
||residual||^2).
- IsTrackingPair: counts LC inliers from CorrespondenceGraph::ImagePair
fork fields (inliers + are_lc); returns true when non_lc_count >=
lc_count. Reads via problem.CorrespondenceGraphPtr() (M9 plumbing).
RotationAveragingProblem accessor additions (M11):
- MutableEstimatedRotations() — direct mutable access to per-frame
3-DOF angle-axis storage so Ceres can optimize in place.
- FrameIdToParamIdx() — frame_id -> 3-DOF block offset mapping.
- ImageIdToFrameId() — for trivial-rig path PairConstraint
image_id -> frame_id resolution.
- FixedFrameId() — gauge-fixing anchor.
- CorrespondenceGraphPtr() — M9 plumbing surface.
RotationAveragingSolver::Solve dispatches video-Ceres path before
L1+IRLS when use_video_constraints && !use_gravity. Mutually-exclusive
warning logged when both are set (use_video_constraints disabled).
Cost is Huber+Cauchy with TAKE_OWNERSHIP semantics; no leak. Solver:
SPARSE_NORMAL_CHOLESKY, max 100 iters. M0c-equivalent verification:
default behavior unchanged when use_video_constraints=false.
* M13: delete glomap_ra GP + RA (videosfm now drives both via native)
Final deletion phase. Pre-deletion grep verified that the deleted files
have no remaining external references after M7 + M12 caller migration.
Submodule pycolmap.glomap_ra stays alive — track-filter,
track-establishment, image-pair-inliers, view-graph-manipulation, and
relative-pose-estimation continue to bind under it.
Files deleted (~3000 LOC):
- src/colmap/sfm/global_positioning_glomap.{h,cc}
- src/colmap/sfm/rotation_averaging_glomap.{h,cc}
- src/colmap/sfm/cost_function_glomap.h
- src/colmap/sfm/l1_solver_glomap.h
- src/colmap/sfm/optimization_base_glomap.h
- src/colmap/sfm/tree_glomap.{h,cc} (only RA used it)
- src/pycolmap/sfm/global_positioning_glomap.cc
- src/pycolmap/sfm/rotation_averaging_glomap.cc
Files KEPT (still used by surviving glomap_ra modules):
- rigid3d_glomap.{h,cc} — track_filter_glomap consumer
- two_view_geometry_glomap.{h,cc} — image_pair_inliers consumer
- track_filter_glomap, track_establishment_glomap, image_pair_inliers,
view_graph_manipulation, view_graph_calibration, relative_pose_estimation
CMakeLists updates:
- src/colmap/sfm/CMakeLists.txt: drop deleted entries from SRCS list.
- src/pycolmap/sfm/bindings.cc: drop BindGlobalPositionerOptions /
BindGlobalPositioningGlomap / BindRotationAveragingGlomap forward
decls + dispatch calls. Other binders (BindViewGraphManipulation,
BindRelativePoseEstimation, BindTrackEstablishmentGlomap,
BindImagePairInliersGlomap, BindTrackFilterGlomap) preserved — those
bind to the pycolmap.glomap_ra submodule which stays alive.
Pycolmap CMake auto-globs sources, so the .cc deletions remove
themselves from the binding build.
Verified: end-to-end pipeline runs on sample-10 LaMAR HGE +
canonical DB + GP_SEED=1 with native pycolmap.run_global_positioning +
pycolmap.run_rotation_averaging only. ATE produced (cross-impl drift
vs glomap_ra-baseline expected per Decision G2).
* M5 fix: dispatch WeightedBATADirectionalError when angular_stddevs available
Bug uncovered by post-port audit (audit_algorithmic_semantics.md
suspect #1): the M3 cost functor WeightedBATADirectionalError was
defined and built, but AddObservationToProblem unconditionally created
the unweighted BATAPairwiseDirectionCostFunctor — the M5 cascade only
swapped the LOSS function, not the COST function. Result: anisotropic
per-keypoint weighting was dropped on every geometry residual,
causing the SPLIT_METRIC_DEPTH path to trade off geometry vs metric-
depth at the wrong relative scale.
Fix: dispatch WeightedBATADirectionalError when
image.angular_stddevs[fid] is populated (videosfm pipeline always
populates). sigma_x, sigma_y from the Vector2d entry; sigma_z =
0.5*(sigma_x+sigma_y). Falls back to BATAPairwiseDirectionCostFunctor
when sigmas absent. Mirrors fork's dispatch at
glomap/glomap/estimators/global_positioning.cc:592-601, 646-657.
Verified ATE recovery on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0
+ canonical DB + GP_SEED=1:
- Pre-fix: 0.00 / 0.00 / 17.22 / 57.92 / 95.79
- Post-fix: 39.44 / 68.32 / 93.66 / 96.83 / 99.68
- Baseline: 37.40 / 66.77 / 93.35 / 96.68 / 99.67 (glomap_ra)
Native port now produces parity-or-slightly-better ATE than the
pre-port glomap_ra baseline. Cross-implementation drift is within
~5% (Decision G2 sanity threshold).
* global_positioning: add GP_SEED env-var fallback in ctor
When random_seed == -1 (caller explicitly opts out of deterministic
seeding) fall back to the GP_SEED env var so the documented Tier-2
byte-identity recipe (CLAUDE.md "ATE byte-identity") works without
patching every call site for the transition cycle.
Plan-vs-execution audit flagged this: header docstring claimed
GP_SEED fallback existed but ctor only honored options_.random_seed >= 0.
Adding cstdlib for std::getenv/std::atoi.
* global_positioning: mark random_seed default + GP_SEED fallback as reproduce-fork TODOs
Per user audit ask: any place where M0–M13 changed colmap4 default
behavior to reproduce the pyglomap fork should be marked as a TODO
not silently hardcoded — the eventual goal is to converge back to
native defaults.
Two GP-side crutches:
- random_seed default 1 (upstream is -1 / random_device)
- GP_SEED env-var fallback in ctor when random_seed == -1
Both stay in place for Tier-2 byte-identity but are now marked
TODO(reproduce-fork). Companion notes:
.claude/notes/glomap_audit/audit_default_overrides.md
* sfm/*_glomap*: add TODO(dedup-glomap-vs-colmap4) banners
Six fork-imported headers in colmap/sfm/ now carry a per-file TODO
comment pointing at the dedup audit
(.claude/notes/glomap_audit/audit_glomap_files_vs_colmap4.md). Each
banner names which symbols in that TU duplicate native colmap4
helpers vs which are unique fork algorithms.
grep -rn 'TODO(dedup-glomap-vs-colmap4)' colmap/src now surfaces the
~250-350 LOC dedup follow-up scoped for after the M0-M13 PR merges.
* estimators: gate fork additions behind opt-in options (vanilla-by-default)
Per backward-compat study (audit_backward_compat.md): make vanilla
upstream colmap4 the default for both GP and RA so non-fork callers
get unmodified upstream behavior. Every fork-added feature now
requires an explicit opt-in.
GlobalPositionerOptions — 3 changes:
- New `use_observation_exclusions` (default false): gates the
per-observation skip via `image.is_excluded[idx]` in
AddObservationToProblem (Gate G-1).
- New `use_lc_observations` (default false): gates the
`track.lc_elements` second loop in AddPoint3DToProblem and the
paired sweep in FilterDepthOutliers. This IS the GP-side LC config —
pairs with RA's `skip_risky_LC_pairs` to give two complementary LC
controls (Gate G-2).
- `random_seed` default restored from 1 -> -1 (upstream non-deterministic
random_device). The GP_SEED env-var fallback in the ctor stays as a
documented escape for the Tier-2 byte-identity recipe (Gate G-3).
RotationEstimatorOptions — 1 change:
- New `prioritize_tracking_in_mst` (default false): decouples M10 MST
LC-penalty from M11 `use_video_constraints`. Previously
`ComputeMaximumPoseGraphSpanningTree(prioritize_tracking=...)` was
hardcoded to `use_video_constraints`; now a separate knob (Gate R-1).
pycolmap binding: 3 new `def_readwrite` entries on GlobalPositionerOptions
(use_observation_exclusions, use_lc_observations) and RotationEstimatorOptions
(prioritize_tracking_in_mst).
Net effect:
- Vanilla pycolmap.run_global_positioning / run_rotation_averaging
callers now get upstream colmap4 behavior unmodified.
- videosfm flips all four gates true via _to_native_gp_options /
_to_pycolmap_ra_options so the existing pipeline is unchanged.
Smoke-tested: defaults are False/-1, post-flip True/1.
* sfm: dedup *_glomap helpers against colmap4 native
Three-step dedup pass (audit_glomap_files_vs_colmap4.md). Replace
fork-imported helpers that duplicated existing colmap4 native code.
#1 union_find_glomap.h (-42 LOC)
- DELETED. colmap::UnionFind<T> at colmap/math/union_find.h:40 is a
strict superset (Reserve / FindIfExists / Compress / Parents) and
the body of Find/Union is byte-equivalent.
- Added 1-line Clear() to native colmap::UnionFind so it covers the
one method TrackEngine used that wasn't already in native.
- track_establishment_glomap.h now includes colmap/math/union_find.h
and uses unqualified UnionFind (resolves to colmap::UnionFind via
parent-namespace lookup).
#2 rigid3d_glomap.{h,cc} (-79 LOC)
- DELETED entirely. All 8 symbols turned out to be dead code:
- DegToRad / RadToDeg duplicated colmap/math/math.h:59-64
- RotationToAngleAxis / AngleAxisToRotation / Rigid3dToAngleAxis /
CalcAngle*2 / CalcTrans / CalcTransAngle were declared and defined
but never called from any TU after M0-M13.
- Includers (image_pair_inliers_glomap.h, track_filter_glomap.cc)
switched to direct includes of colmap/math/math.h + colmap/geometry/rigid3.h.
#3 two_view_geometry_glomap.{h,cc} (-83 LOC)
- DROPPED EssentialFromMotion (= EssentialMatrixFromPose at
colmap/geometry/essential_matrix.cc:97).
- DROPPED SampsonError(E, Vec2, Vec2) overload (= ComputeSquaredSampsonError
at essential_matrix.cc:168, callers pass .homogeneous()).
- DROPPED HomographyError (= ComputeSquaredHomographyError at
colmap/geometry/homography_matrix.cc:266, identical formula).
- DROPPED FundamentalFromMotionAndCameras (was 1-line wrapper, dead
code — never called).
- KEPT CheckCheirality (depth-gated single-pair extension, no native
equivalent), GetOrientationSignum (vendored from GC-RANSAC),
SampsonError(E, Vec3, Vec3) (depth-aware z-divide variant).
- image_pair_inliers_glomap.cc updated to call native helpers; added
geometry/essential_matrix.h + geometry/homography_matrix.h includes.
Net: -204 LOC across 4 files dropped + 6 files updated. Build clean,
pycolmap loads, defaults unchanged.
* sfm: collapse glomap_ra::Track onto colmap::Point3D (#4)
Final dedup step (audit_glomap_files_vs_colmap4.md). Replace the
vendored `glomap_ra::Track` struct with native `colmap::Point3D`
throughout TrackEngine, TrackFilter, and the 2 binding TUs.
The vendored type was almost a verbatim duplicate of Point3D:
- xyz / color / is_initialized -> already on Point3D (M0/glomap-fork)
- observations (vector<pair>) -> Point3D::track.Elements() (vector<TrackElement>)
- lc_observations -> Point3D::track.lc_elements (already added at scene/track.h:93)
- track_id -> redundant with the map key, dropped
Changes:
- track_establishment_glomap.h: drop `Track` struct + `Observation`
typedef. `track_t` now aliases `colmap::point3D_t`. API uses
`unordered_map<track_t, Point3D>`.
- track_establishment_glomap.cc: 4 algorithm methods rewritten to
use Point3D::track.AddElement / .Elements() / .lc_elements / .Length().
- track_filter_glomap.{h,cc}: same.
- pycolmap/sfm/track_establishment_glomap.cc: drop ToPoint3D
conversion (35 LOC). Bindings now pass-through dict<int, Point3D>
with no fork↔native marshalling.
- pycolmap/sfm/track_filter_glomap.cc: drop ToGlomapRaTrack +
FromGlomapRaTrack (40 LOC). Same pass-through pattern.
Net: -115 LOC across 6 files. Build clean, pycolmap loads,
establish_full_tracks / find_tracks_for_problem /
glomap_ra.filter_tracks_by_angle / glomap_ra.filter_track_triangulation_angle
all expose the same dict<int, Point3D> shape they did before — videosfm
caller is unchanged.
* review fixes: docstrings, fail-loud emplace, dead-code notes
Round of post-review polish on the backward-compat + dedup PRs.
Correctness / fail-loud:
- track_establishment_glomap.cc: THROW_CHECK on the two
ProcessLoopClosurePairs tracks.emplace calls (track_id
collision was previously silent).
- track_filter_glomap.cc + track_establishment_glomap.cc: switch
track.Elements().clear() -> track.SetElements({}) so the
call survives a future signature drift on Elements()
(review #7).
Docstring / comment:
- pycolmap motion_averaging.cc: random_seed binding doc now
matches the new -1 default and surfaces the GP_SEED env-var
escape at the Python boundary (review #1, #3).
- global_positioning.h: header note on use_observation_exclusions
clarifies that Image::is_excluded is itself fork-only;
vanilla pycolmap users won't typically populate it (review #2).
- rotation_averaging.h: prioritize_tracking_in_mst comment
rephrased to describe gate semantics rather than videosfm wiring
(the very coupling we just decoupled).
- two_view_geometry_glomap.h: header explains FundamentalFromMotionAndCameras
was a dead-code delete (no replacement migration), unlike the
other 3 dropped helpers which got migrated to native (review #1
generic).
- track_establishment_glomap.h: file-level note explaining why the
glomap_ra namespace alias is kept post-collapse (binding-path
stability — pycolmap submodule + videosfm import paths).
* estimators: D1 dedup loss_config + strip rot-prone comment tags
Two cleanups bundled.
D1 (loss_config dedup, audit_dedup.md):
- LossFunctionConfig{name, scale, weight} (string-based) replaced with
LossConfig{type, scale, weight} where type is the native
CeresBundleAdjustmentOptions::LossFunctionType enum.
- Free CreateLossFromConfig factory dropped; LossConfig::CreateLossFunction()
now reuses native colmap::CreateLossFunction(type, scale) plus a
3-line ScaledLoss(weight) wrap.
- Native colmap::CreateLossFunction was previously in an anonymous
namespace inside bundle_adjustment_ceres.cc; promoted to a public
forward declaration in bundle_adjustment_ceres.h so other estimators
can route through it.
- pycolmap binding: dropped the templated string-based BindLossField
helper. Each loss field is now a plain def_readwrite returning a
pycolmap.LossConfig object. videosfm side does the
string -> LossFunctionType conversion at the bridge boundary.
- arctan support deferred — only used by gravity_refinement (separate TU,
uses ceres::ArctanLoss directly), never used by GP cascade.
Comment cleanup pass:
- Strip M0..M13 milestone tags, Gate G-X / Gate R-X labels, audit_*.md
cross-refs, videosfm sets X via Y_options'' caller-knowledge phrases.
- Reduce verbose --- Glomap-fork ... (M5) ---'' banners to short
purpose lines or drop entirely. Keep WHY / hidden-constraint /
workaround comments verbatim.
- Touched: global_positioning.{h,cc}, rotation_averaging.{h,cc},
rotation_averaging_impl.{h,cc}, motion_averaging.cc binding.
Build + smoke clean. videosfm bridge updated in companion commit.
* estimators: delete loss_config.h, fold LossConfig into global_positioning.h
Follow-up to D1 dedup. The loss_config.h after the rewrite was just a
~25-line struct definition with one consumer (GlobalPositionerOptions).
Inline it into global_positioning.h next to PointConstraintType so the
GP-specific helpers live together.
- LossConfig struct + CreateLossFunction() method moved to
colmap/estimators/global_positioning.h.
- loss_config.h deleted; CMakeLists.txt entry removed.
- Includers (global_positioning.cc, motion_averaging.cc binding) drop
the include — global_positioning.h covers it transitively.
Net: 1 fewer header in the tree.
* estimators: extract LossFunctionType + factory into ceres_loss.{h,cc}
Lift the loss-creation primitive out of bundle_adjustment_ceres.{h,cc}
into a dedicated colmap/estimators/ceres_loss.{h,cc} so it's a true
shared utility — not a private detail of CeresBundleAdjustmentOptions.
Changes:
- New colmap/estimators/ceres_loss.{h,cc}: top-level
colmap::LossFunctionType enum + colmap::CreateLossFunction(type, scale)
factory. Definition moved verbatim from bundle_adjustment_ceres.cc.
- bundle_adjustment_ceres.h: CeresBundleAdjustmentOptions::LossFunctionType
is now a back-compat using alias to colmap::LossFunctionType;
the now-redundant forward declaration of CreateLossFunction is dropped.
- bundle_adjustment_ceres.cc: definition deleted (moved to ceres_loss.cc).
- global_positioning.h: GlobalPositionerOptions gains configurable
loss_function_type + loss_function_weight fields. Default
HUBER preserves the previous hardcoded behavior. CreateLossFunction()
body now mirrors BA — dispatch via the factory + ScaledLoss(weight) wrap.
Drops include of bundle_adjustment_ceres.h in favor of ceres_loss.h.
- pycolmap motion_averaging.cc: expose loss_function_type +
loss_function_weight on GlobalPositionerOptions. The existing
pycolmap.LossFunctionType enum (bound via BA's binding through the
alias) is now the canonical Python-side type.
Net effect: vanilla colmap GP becomes loss-configurable like BA. The
fork's 10-bucket cascade (LossConfig) still uses the same enum — now
referenced via the top-level type rather than nested inside BA options.
* scene/image: fix Image copy/operator= dropping pixel_cholesky_xy_
Audit catch (parallel agents auditing fork-modified native files for
non-fork drift). The pre-existing fork copy-ctor and operator= for
colmap::Image carried all the per-feature fork fields (depth_priors,
angular_stddevs, is_inlier, is_excluded, is_track_anchor,
is_depth_outlier, etc.) but missed the more recently added
pixel_cholesky_xy_ private member. Same class of bug as the documented
`cam_from_world` regression in
memory/glomap_ra_port_lto_fp_nondeterminism.md — any py::cast roundtrip
or value-copy of an Image silently zeroed the field, breaking pose-cov
covariance plumbing for downstream consumers.
Also reorders the mem-init list to match the header declaration order.
Cosmetic-only follow-ups in the same audit pass:
- estimators/global_positioning.cc: dedent the extracted
AddObservationToProblem body by 2 spaces (was inheriting the
original loop-body indentation; cosmetic-only, no semantic effect).
- scene/track.h: collapse stray double blank line.
Build clean; default-constructed Image still works.
* estimators: D2 dedup — drop ScalePriorError/LogScalePriorError for native primitives
Per audit_dedup.md D2: replace the two fork cost functors in
cost_functions/metric_depth.h:182-244 with native
CovarianceWeightedCostFunctor<NormalPriorCostFunctor<1>> from
cost_functions/utils.h.
Mathematically equivalent:
- ScalePriorError(prior=1, sigma): r = (s - 1) / sigma
- LogScalePriorError(sigma_log): r = log_s / sigma_log
- Native: CovarianceWeightedCostFunctor<NormalPriorCostFunctor<1>>::Create(cov, prior_vec)
with cov = sigma², prior_vec = (1.0) for linear or (0.0) for log.
The linear/log selection in global_positioning.cc:226-232 collapses to
a single Create() call where the only branch is which constant the
prior_vec carries (0.0 vs 1.0).
Net: -73 LOC in metric_depth.h (the 2 functor bodies + their factories
+ docstrings), +9 LOC in global_positioning.cc (vec construction +
Create call). Build clean; ScalePriorError/LogScalePriorError no
longer exposed.
* estimators: D4 dedup — drop WeightedBATADirectionalError for native CWCF<BATA>
Per audit_dedup.md D4: replace the fork's anisotropic-weighted BATA
functor with native CovarianceWeightedCostFunctor<BATAPairwiseDirectionCostFunctor>.
The fork rotated the residual into camera frame inside the functor:
r_fork = diag(1/sigma) * R * (t_obs - s*(c2-c1))
Native CWCF whitens with sqrt(cov^-1) of a passed-in covariance.
Encoding the rotation in the world-frame covariance:
cov_world = R^T * diag(sigma^2) * R
makes ||r_native||^2 == ||r_fork||^2 (since both equal
r_world^T * cov_world^-1 * r_world). Cost contribution is identical;
residual vectors differ but Ceres loss functions act on the squared
norm so the optimization converges to the same minimum.
Why R can be baked at construction time: GP rotations are frozen
post-RA. Ceres parameter blocks are (position1, position2, scale)
only — rotation is never updated by the optimizer.
Made BATAPairwiseDirectionCostFunctor CWCF-compatible by adding the
2-line metadata (kNumResiduals + kParameterDims) the wrapper expects.
This parallels what NormalPriorCostFunctor already does and is an
upstream-friendly addition.
Net: -60 LOC functor body in motion_averaging.h, +14 LOC at the
dispatch site (cov_world construction + CWCF::Create call).
WeightedBATADirectionalError no longer exposed.
Risk: medium — WeightedBATADirectionalError was the M5 critical fix
that recovered ATE 0 -> 39. Math is provably equivalent in cost, but
autodiff Jacobian path differs vector-wise so FP precision may
shift slightly. Needs sample-10 LaMAR HGE ATE check before merge.
* sfm: Phase 5 dedup — extract track-establishment core into shared helper
Per audit_glomap_files_vs_colmap4.md Phase 5 (Strategy A): both native
GlobalMapper::EstablishTracks and fork TrackEngine::EstablishFullTracks
implemented the same union-find + intra-image consistency + length
filter + greedy subsample algorithm over post-RANSAC corr-graph
inliers. Extract the core into a new shared helper so both consumers
route through the same code; fork's LC second-pass +
2-view-depth-prior gate stay as fork-specific extensions.
New: src/colmap/sfm/track_establishment.{h,cc} (~210 LOC)
- struct TrackEstablishmentOptions{intra_image_consistency_threshold,
min_num_views_per_track, required_tracks_per_view}
- using MatchPredicate = std::function<bool(image_t, point2D_t,
image_t, point2D_t)> — optional, returns true to skip a match in
the UF phase. Used by fork to skip LC inliers in the regular pass.
- EstablishTracksFromCorrGraph(valid_pair_ids, corr_graph,
image_id_to_keypoints, options, ignore_match) — returns
unordered_map<point3D_t, Point3D>.
GlobalMapper::EstablishTracks shrinks from ~135 LOC to ~30 LOC of
orchestration: builds the keypoint map + collects valid pair ids
from pose_graph, calls the helper, commits the result via
AddPoint3D. No behavior change for native callers — predicate
defaults to nullptr (no LC skip), options carry the same defaults
as the previous inline impl.
TrackEngine::EstablishFullTracks now:
- Builds image_id_to_keypoints from images_ (fork's Image.features
rather than reconstruction's points2D — pre-rec state).
- Pre-computes an LC observation set from view_graph.image_pairs[*]
inliers + are_lc, encoded as pair<uint64_t, uint64_t> for std::set
lookup.
- Calls EstablishTracksFromCorrGraph with an ignore_match predicate
that skips matches in the LC set.
- Calls existing ProcessLoopClosurePairs as the post-pass to populate
Track::lc_elements (and mint two new tracks per LC-only observation
pair, with obs-key-derived ids that don't collide with native's
dense [0, N) range since image_id<<32 >= 2^32).
Deleted from track_establishment_glomap.{h,cc}:
- TrackEngine::BlindConcatenation (~55 LOC)
- TrackEngine::TrackCollection (~110 LOC)
- UnionFind<image_pair_t> uf_ member
- Now-unused colmap/math/union_find.h include
Total: ~165 LOC removed from fork; ~210 LOC added in the new shared
helper (most of which is moved-not-new code).
Backward compat:
- Native API gains an optional ignore_match parameter defaulting
to nullptr — vanilla callers see identical behavior.
- Fork API unchanged (TrackEngine::EstablishFullTracks signature +
return shape preserved).
- pycolmap binding unchanged.
Risk: medium — core algorithm is byte-equivalent (verbatim move of
the loop bodies), but unordered_map iteration order may differ
between native's local instance and fork's instance, which can
produce a different greedy-subsample tiebreak. Default options
have required_tracks_per_view=INT_MAX (no-op subsample), so
iteration order doesn't matter for native. Fork's caller passes
require_tracks_per_view=INT_MAX as well at this stage; the actual
greedy subsample lives in FindTracksForProblem (untouched).
Needs sample-10 LaMAR HGE ATE check before merge.
* track_establishment: iterate image_pair.matches/inliers directly
Phase 5 first-run verify revealed that ExtractMatchesBetweenImages reads
from CorrespondenceGraph::flat_corrs which is only populated by
FinalizeAfterMatchingComplete (native pipeline's geom-verify step). The
fork-driven videosfm pipeline writes inliers directly into
ImagePair::matches + ImagePair::inliers without going through
flat_corrs, so the helper saw zero matches and produced zero tracks.
Fix: iterate ImagePair::matches indexed by ImagePair::inliers directly.
Both the native colmap4 pipeline (which populates these fields during
geom-verify) and downstream pipelines that populate them via custom
relative-pose flows (e.g. videosfm's MDRP) work through this path.
Drops ExtractMatchesBetweenImages dependency entirely; drops the
feature/types.h include that came with FeatureMatches.
Verified end-to-end: sample-10 LaMAR HGE produces 25/25 registered
images with AUC 73.69/86.85/97.37 @ {0.05, 0.1, 0.5}m. Tier-1
structural identity holds through GP1+GP2 (n_tracks, total_observations,
total_lc_observations all match m13 baseline byte-for-byte at every
checkpoint).
* tests: 31 new gtest cases for fork additions + dedup correctness
Spread across 4 files:
- src/colmap/estimators/cost_functions/motion_averaging_test.cc (+9 tests)
CWCF<BATAPairwiseDirectionCostFunctor> math equivalence with random
inputs (rotation, sigma, c1, c2, scale, t_obs); zero-residual edge
case; identity-rotation isotropic-sigma reduction. Algebraically
proves D4 dedup (cov_world = R^T diag(sigma^2) R) preserves the
squared residual norm of the deleted WeightedBATADirectionalError.
- src/colmap/sfm/track_establishment_test.cc (+5 tests, NEW file)
Phase 5 helper EstablishTracksFromCorrGraph: 3-pair triangle smoke
test, length filter drops short tracks, intra-image-consistency
drop, ignore_match predicate, empty-input no-crash.
- src/colmap/estimators/cost_functions/metric_depth_test.cc (+7 tests, NEW)
MetricDepthError: zero residual when prior matches predicted depth;
linear vs log residual mode formulas; zero_residual_behind_camera
gate; log-scale parameterization; 5-toggle smoke; rejects
non-positive sigma.
- src/colmap/estimators/global_positioning_test.cc (+4 tests)
LossConfig::CreateLossFunction(): TRIVIAL/HUBER/CAUCHY/SOFT_L1
dispatch through native colmap::CreateLossFunction; weight != 1
triggers ScaledLoss wrap (verified by 2x scaling on output rho).
CMakeLists entries added for the two new test files; existing
TESTS_ENABLED=ON build picks them up. All 4 ctest groups pass.
* track_establishment_glomap: harden LC-track id minting
Phase 5 left ProcessLoopClosurePairs minting new tracks via
static_cast<track_t>((image_id<<32)|feature_id). With image_id >= 1
this lands ids >= 2^32, well above native's dense [0, N) range, so
collisions are theoretical-only. Switch to sequential ids continuing
from native's max — eliminates the magic encoding and makes the
THROW_CHECK guard truly defensive rather than load-bearing.
* tests: 34 more gtest cases for fork-unique paths + backward-compat gates
Round 2 of unit-test additions (6 areas, 34 tests). Driven by the
'genuine coverage gaps' list — fork-unique algorithms previously
tested only via end-to-end pipeline.
ProcessLoopClosurePairs (track_establishment_test.cc, +5):
4 branches of LC second-pass + sequential-id no-collision smoke.
FindTracksForProblem (track_establishment_test.cc, +6):
Length filter, 2-view depth-prior gate (drop + keep + boundary),
greedy quota, min_num_tracks_per_view default-bug enshrined.
TrackFilter (track_filter_test.cc, NEW, +7):
FilterTracksByAngle aligned/misaligned/uncalib-double-thresh +
point-behind-camera; FilterTrackTriangulationAngle parallel/perp +
threshold sweep.
RelativeRotationError (rotation_averaging_test.cc, +3):
Zero residual when consistent, non-zero on injected angular error
matches the discrepancy norm, swap+invert symmetry.
Required moving the struct from anonymous namespace in
rotation_averaging_impl.cc into rotation_averaging_impl.h so tests
can construct it directly.
ImagePairsInlierCount (image_pair_inliers_test.cc, NEW, +5):
4-gate degeneracy (epipolar, cheirality, epipole-angle, depth-prior
vs nodepth threshold). All 4 gates exercised on the essential-matrix
path with controlled synthetic inputs.
Backward-compat gates G-1/G-2/R-1 (+8):
G-1 (use_observation_exclusions) off/on/partial — global_positioning_test.cc
G-2 (use_lc_observations) off/on — global_positioning_test.cc
R-1 (prioritize_tracking_in_mst) off/on/no-corr-graph-fallback — rotation_averaging_test.cc.
Required exposing ComputeMaximumPoseGraphSpanningTree as a public
symbol in rotation_averaging.h (was anon-namespace).
Net: 65 total unit tests across 8 ctest groups, all passing.
* sfm: rename glomap_ra namespace -> sfm_ext
The ``colmap::glomap_ra::`` namespace and pycolmap.glomap_ra submodule
were named after the original rotation-averaging port from glomap, but
their current contents are track establishment, track filtering, and
pair-inlier scoring — none of which are RA. Rename to ``sfm_ext``
(SFM extensions) so the label reflects the actual contents.
Mechanical sweep across 11 C++ files + 4 binding files:
- ``namespace glomap_ra { ... }`` -> ``namespace sfm_ext { ... }``
- ``glomap_ra::Foo`` -> ``sfm_ext::Foo`` everywhere
- ``GetOrCreateGlomapRaModule`` -> ``GetOrCreateSfmExtModule``
- ``m.def_submodule("glomap_ra")`` -> ``m.def_submodule("sfm_ext")``
Python alias kept for back-compat: pycolmap/__init__.py defines
``glomap_ra = sfm_ext`` so existing videosfm callers
(``pycolmap.glomap_ra.filter_tracks_by_angle`` etc.) keep working
unchanged. Filenames keep the ``_glomap`` suffix for now — file rename
is a separate, larger churn.
Verified:
- Full build clean (warm).
- 8/8 ctests pass: track_establishment_test, track_filter_test (7/7),
image_pair_inliers_test, motion_averaging_test, metric_depth_test,
global_positioning_test, rotation_averaging_test (controllers +
estimators).
- pycolmap import + alias intact: pycolmap.glomap_ra is pycolmap.sfm_ext,
filter_tracks_by_angle resolves on both.
- videosfm GlobalPositioningPhase imports OK with the alias.
* comments(sfm_ext): drop stale glomap_ra / pyglomap / vendored references
Cleans up post-rename comment debt:
- image_pair_inliers_glomap.h: remove TODO that already shipped (Sampson/H errors now route through native helpers); drop 'vendored from glomap/types.h' framing on InlierThresholdOptions.
- track_filter_glomap.cc: replace 'glomap RA pipeline' / 'RA-shadow' wording with neutral 'dict-of-tracks state used by sfm_ext'.
- pycolmap/sfm/track_filter_glomap.cc: drop pyglomap.* Python-name mentions (module no longer exists).
- pycolmap/sfm/track_establishment_glomap.cc: drop 'Glomap-style' from establish_full_tracks docstring.
* comments(pycolmap): drop residual pyglomap migration archaeology
Second comment-cleanup pass beyond the sfm_ext-renamed files. Strips
'mirrors pyglomap.X', 'byte-for-byte parity with pyglomap', 'so the
videosfm pyglomap shim can be retired' framing now that pyglomap is
gone and the videosfm shim is removed.
Preserved: design rationale (e.g. issue #40 reference for adopting
CalibrateViewGraph wrapper, pybind11 deep-copy gotcha), open
algorithmic TODOs unrelated to the rename.
* sfm: fold sfm_ext::TrackEngine into native GlobalMapper / track_establishment
Removes the parallel colmap::sfm_ext::TrackEngine path. The fork-specific
behaviors that lived in track_establishment_glomap.{h,cc} now live as
native free functions in track_establishment.{h,cc} and as methods on
GlobalMapper. Net diff -300 LOC; one less namespace; one less file pair.
Native types already carried the data (Track::lc_elements,
ImagePair::are_lc, Image::depth_priors / depth_prior_validity were added
during the M0-M13 port). This commit fills in the missing code paths:
New free functions in colmap::namespace (track_establishment.{h,cc}):
- MakeLoopClosureMatchPredicate(valid_pair_ids, corr_graph) - returns a
MatchPredicate that excludes ImagePair::are_lc[idx]==true inliers from
the union-find pass, pre-computing the LC set in O(num inliers).
- AppendLoopClosureObservations(valid_pair_ids, corr_graph, tracks) -
4-branch second pass that populates Track::lc_elements (no-track /
both-tracks / one-track-each / same-id, mints new tracks at
max(track_id)+1). Operates on a tracks dict so binding + GlobalMapper
share the call site.
- struct TrackSubsampleOptions + SubsampleTracks(...) - depth-aware
greedy length-sorted subsample. Reads depth_priors / depth_prior_validity
/ registered_image_ids passed as separate maps to keep the function
Reconstruction-free. Asymmetric length filter preserved (lower bound
counts Length+lc_elements, upper bound counts Length only).
GlobalMapper extensions (global_mapper.{h,cc}):
- GlobalMapperOptions gains 4 fields (default-off so vanilla colmap
behavior is unchanged): track_lc_second_pass, track_two_view_depth_gate,
track_max_num_views_per_track, track_max_num_tracks.
- EstablishTracks runs MakeLoopClosureMatchPredicate +
AppendLoopClosureObservations when track_lc_second_pass=true; bypasses
the helper-side greedy subsample in that mode.
- New SubsampleTracksForProblem method bridges Reconstruction <-> dict and
calls SubsampleTracks.
Pycolmap binding (track_establishment_glomap.cc - file kept, contents
rewritten to use native API):
- pycolmap.establish_full_tracks(view_graph, images, options,
lc_second_pass=False) - top-level binding (path preserved). Takes the
native pycolmap.TrackEstablishmentOptions; lc_second_pass optional
kwarg toggles the LC second pass.
- pycolmap.find_tracks_for_problem(view_graph, images, tracks_full,
options) - takes pycolmap.TrackSubsampleOptions (new bound struct).
- The old pycolmap.TrackEstablishmentOptions (fork shape) is gone. The
native TrackEstablishmentOptions has 3 fields
(intra_image_consistency_threshold, min_num_views_per_track,
required_tracks_per_view); the 5 subsample knobs (max_num_views_per_track,
max_num_tracks, two_view_depth_gate, etc.) live on the new
TrackSubsampleOptions.
Tests (track_establishment_test.cc):
- All 16 tests preserved (5 native + 6 FindTracksForProblem-now-SubsampleTracks
+ 5 ProcessLoopClosurePairs-now-AppendLoopClosureObservations).
- MinTracksPerViewBugDocumentation rewritten to describe the new int-typed
INT_MAX default (signed comparison never trips) instead of the old
signed/unsigned -1 -> UINT_MAX promotion. Same observable behavior.
Adjacent (track_filter_glomap.{h,cc} + binding + tests):
- Repointed track_t / feature_t typedef include from the deleted
track_establishment_glomap.h to colmap/util/types.h (the canonical
point3D_t / point2D_t aliases). track_t / feature_t aliases are gone.
Files DELETED:
- src/colmap/sfm/track_establishment_glomap.h (-82 LOC)
- src/colmap/sfm/track_establishment_glomap.cc (-311 LOC)
Verified: scripts/build.sh clean (cold + warm). 7 ctest groups pass
(scene/track, scene/image, sfm/track_establishment, sfm/track_filter,
sfm/image_pair_inliers, estimators/global_positioning, estimators/
rotation_averaging) - 100% in 0.3s.
* sfm: drop _glomap suffix + sfm_ext namespace from remaining fork files
Three-stage cleanup mirroring the TrackEngine fold (commit 77c0574b).
Net result: ZERO _glomap files in colmap/sfm/, ZERO sfm_ext namespace
references. All fork-extension code now lives in plain colmap::
namespace alongside upstream code, with no naming tax.
Stage 1 - two_view_geometry_glomap deletion (-79 LOC):
- 3 helpers (CheckCheirality single-pair, GetOrientationSignum,
SampsonError Vec3 depth-aware) had ONE caller each
(image_pair_inliers). Folded as anonymous-namespace statics in the
consumer file. Deleted two_view_geometry_glomap.{h,cc} +
CMakeLists entry.
Stage 2 - track_filter_glomap rename (no LOC delta, +/- ~250):
- Renamed src/colmap/sfm/track_filter_glomap.{h,cc} -> track_filter.{h,cc}.
- Dropped sfm_ext:: namespace -> plain colmap::.
- Converted struct TrackFilter::FilterX static methods to free
functions FilterTracksByAngle / FilterTrackTriangulationAngle.
- Pycolmap binding: pycolmap.sfm_ext.filter_tracks_by_angle ->
pycolmap.filter_tracks_by_angle (top-level). Same for
filter_track_triangulation_angle. Test file + downstream callers
updated.
Stage 3 - image_pair_inliers_glomap rename (no LOC delta):
- Renamed src/colmap/sfm/image_pair_inliers_glomap.{h,cc} ->
image_pair_inliers.{h,cc}.
- Dropped sfm_ext:: namespace -> plain colmap::.
- Behavior preserved verbatim (hard-coded thres_epipole_nodepth,
disabled Gate-3 angle check, F-branch positive_count==negative_count
silent drop). TODOs for later cleanup left in-place.
- Pycolmap binding: pycolmap.image_pairs_inlier_count was already
top-level; renamed BindImagePairInliersGlomap -> BindImagePairInliers.
- Renamed src/pycolmap/sfm/track_establishment_glomap.cc ->
track_establishment.cc; renamed BindTrackEstablishmentGlomap ->
BindTrackEstablishment. Same fold treatment.
Pycolmap top-level surface (no sfm_ext submodule any more):
- pycolmap.establish_full_tracks / find_tracks_for_problem
- pycolmap.filter_tracks_by_angle / filter_track_triangulation_angle
- pycolmap.image_pairs_inlier_count
- pycolmap.TrackEstablishmentOptions / TrackSubsampleOptions / InlierThresholdOptions
Removed back-compat alias pycolmap.glomap_ra = pycolmap.sfm_ext (no
remaining callers; sfm_ext submodule itself is gone).
Verified: scripts/build.sh clean. 7 ctest groups pass (scene/track,
scene/image, sfm/track_establishment, sfm/track_filter,
sfm/image_pair_inliers, estimators/global_positioning, estimators/
rotation_averaging) - 100% in 0.29s.
* comments: drop stale sfm_ext / TrackEngine mentions in pycolmap bindings
* rotation_averaging: drop dead RotationEstimatorOptions::gravity_dir field
Audit showed gravity_dir is exposed in the binding (motion_averaging.cc)
+ declared in the struct but has ZERO consumers in
colmap/src/colmap/estimators/. Setting it does nothing — pure footgun
for anyone who flips it expecting gravity-aware behavior. Pose-prior
data is supplied separately via the run_rotation_averaging
`pose_priors` argument; this field never participated in the up-vector
math.
The ten other dead-flag suspects from the prior audit
(use_precomputed_weights, fix_non_lc_weights, fixed_non_lc_weight,
consecutive_trivial, use_relative_pose_constraints, relative_pose_default_stddev,
loss_relative_pose, debug_only_relative_pose, scale_regularization_sigma,
image_ids_to_optimize) are already GHOSTS — not on the struct, not in
the binding, not in videosfm. No additional cleanup needed.
* global_positioning: option flag for uncalibrated 0.5x ScaledLoss; fix BindImagePairInliers definition
Adds GlobalPositionerOptions::apply_uncalibrated_loss_downweight (bool,
default true). When true, AddPointToCameraConstraints wraps the BATA
loss in ScaledLoss(0.5) for cameras whose focal length lacks an EXIF
prior (Camera::has_prior_focal_length == false) - the mainline colmap4
heuristic for downweighting bearings that depend on a VGC-estimated
focal. When false, all cameras get full-weight residuals, matching the
glomap fork's behavior.
Default true preserves upstream colmap behavior; videosfm callers
calibrated against fork-flavored full-weight residuals can flip the
flag false at the bridge level (companion videosfm commit wires this
through GPOptions).
Bound in motion_averaging.cc.
Also fixes a leftover from the earlier _glomap rename (commit
db8a5a0d): src/pycolmap/sfm/image_pair_inliers.cc still defined the
binder as BindImagePairInliersGlomap while bindings.cc was already
calling BindImagePairInliers, producing an undefined-symbol linker
error any time the binding got fully relinked. The earlier build
succeeded only because the .so was cached. Rename the definition to
match.
* global_positioning + RA: hardening + comment cleanup
Three small hardening additions + comment-text cleanup across the
GP/RA C++ surface:
- rotation_averaging_impl.cc: SolveIRLS now mirrors SolveL1's NaN-guard
on the step vector. Without this, a singular pose-graph silently
corrupts cams_from_world via UpdateState and the IRLS loop returns
true with garbage residuals on the next iteration. L1 already had
this guard at line 755; IRLS didn't.
- rotation_averaging_impl.cc: RotationAveragingProblem ctor adds a
THROW_CHECK that 'skip_risky_LC_pairs=true' requires a
CorrespondenceGraph. Previously the LC-skip filter silently never
triggered when CG was nullptr - the option appeared to work but
did nothing.
- image.h: Image::operator== now also compares pixel_cholesky_xy_.
This is the field that triggered the earlier copy-ctor field-drop
bug fix (commit 985b2ecb); including it in equality means a future
unit test of the form 'EXPECT_EQ(copy, original)' would catch an
analogous regression. Other fork-extension fields (depth_priors,
is_inlier, cam_from_world, etc.) are intentionally NOT added -
those are mutable pipeline state, not identity-defining.
- global_positioning.h: cleaned 'glomap fork' / 'videosfm callers'
vocabulary leakage in the apply_uncalibrated_loss_downweight
comment; the comment now describes what the bool does without
referencing internal nomenclature. Also added a doc note that the
10 cascade LossConfigs only run under SPLIT_METRIC_DEPTH (BATA mode
silently ignores them - prevents future caller from misconfiguring
these in BATA mode and wondering why nothing changes).
* pycolmap: rename view_graph -> correspondence_graph in bindings
Match the C++ class name (CorrespondenceGraph). Aligns variable names,
parameter names, and dict keys with the type so callers see one term.
* rotation_averaging: drop RotationEstimatorOptions::prioritize_tracking_in_mst
The MST initializer's LC-edge penalty branch was a separate flag from
the video-aware Ceres solver, but they were always enabled together
(the fork wired prioritize_tracking_in_mst directly off
use_video_constraints). Collapse: ComputeMaximumPoseGraphSpanningTree
now reads options_.use_video_constraints, deleting the redundant flag,
its pycolmap binding, and its option-struct field. The function-level
prioritize_tracking parameter stays, callable directly by unit tests.
* rotation_averaging_test: rename Gate_PrioritizeTrackingInMst -> Gate_LcPenaltyMst
The flag was removed in 423c6e58; the tests now exercise the function-
level prioritize_tracking parameter directly. Rename the test cases
and the comment block to match the surviving concept (LC-penalty
branch in ComputeMaximumPoseGraphSpanningTree).
* ceres_loss + run_global_positioning: review follow-ups
- CreateLossFunction: replace silent `return nullptr` on unhandled
enum with LOG(FATAL). The previous fall-through would crash with a
nullptr deref downstream; failing at the source surfaces an enum
extension oversight immediately.
- run_global_positioning binding: drop the duplicate
``dmap_scale_map_nested`` result key. It was a literal alias of
``dmap_scale_map`` — same py::dict assigned twice. Caller now reads
the single key.
* comments: drop residual glomap-fork / videosfm vocabulary
Comment-only sweep across estimators / sfm / scene headers and
impls. Rewrites describe what the code does instead of where it
came from. No identifier or behavior changes.
* rotation_averaging: hoist SetPRNGSeed out of ComputeResiduals
ComputeResiduals is called from L1 and IRLS inner loops. The seed
call inside it reset the global PRNG to the same value every
iteration, replaying the identical jitter sequence in
ComputeGravityAligned1DOFResidual. Move the seed call to
RotationAveragingSolver::Solve (once per problem solve) so the
PRNG advances naturally across iterations — matches the fork's
behavior of seeding once at the GP/RA boundary.
* rotation_averaging: seed MST root + use inlier count for edge weight
Two fork-equivalent fixes to InitializeFromMaximumSpanningTree:
1. Seed cams_from_world[root] from the reconstruction's MDRP-prior
pose before BFS-propagating relative rotations. The previous
default-constructed identity seed rotated every descendant by
R_root^-1 vs the prior gauge, which can shift IRLS weights through
the max_rotation_error_deg filter.
2. Edge weight in ComputeMaximumPoseGraphSpanningTree now reads
inliers.size() from the CorrespondenceGraph (post-RANSAC survivor
count) when CG is plumbed through, falling back to edge.num_matches
(raw match count) for vanilla colmap callers without a CG. Matches
fork's MST weight semantic.
Test fixture updated: pair inliers vectors are now sized to match the
edge num_matches values so both weight sources agree on the MST shape.
* global_positioning: port InitializeDepthMapScalesFromObservations from fork
Restores the median-z_est/depth_prior seeding the fork ran at GP1
entry. Without this, GP1 lazy-inserts dmap_scales_ at 1.0 (linear) /
0.0 (log) for every image — the optimizer starts in a different basin
than the fork.
Source: glomap/glomap/estimators/global_positioning.cc:1006-1133.
Adapted to colmap4's Reconstruction model: iterates Points3D' track
elements (and lc_elements when use_lc_observations is set) to
accumulate per-image z_est/depth_prior samples, takes the median per
image, and seeds dmap_scales_ with log(median) or linear median per
use_log_scale_for_depth_map_scales.
Call site in Solve() is gated on SPLIT_METRIC_DEPTH + use_init +
initial_dmap_scales empty, so GP2 (which supplies initial_dmap_scales
from GP1 output) bypasses this path. Images without observations
consumed here still fall through to the lazy-insert path in
AddObservationToProblem.
* comments: drop residual migration-archaeology vocabulary
Sweep upstream-bound C++ comments / docstrings / test-file
references for "pre-port", "post-port", "M1-M13" milestone tags,
"glomap-fork", "fork-only", "videosfm-side", and the
videosfm-private issue #40 reference (incl. one runtime warning
string). Rewrites describe what the code does now. One identifier
rename: fork_sqnorm -> expected_sqnorm in motion_averaging_test.cc.
No behavior change.
* view_graph_manipulation: skip UNCALIBRATED-pair upgrade when cam2_from_cam1 is null
The original port of glomap's UpdateImagePairsConfig added a
THROW_CHECK around the UNCALIBRATED -> CALIBRATED upgrade requiring
``tvg.cam2_from_cam1.has_value()``. Pairs whose decomposition never
produced a relative pose (e.g. F-only RANSAC path or decomposition
failure) crash the pipeline at this site.
The fork's ImagePair carried cam2_from_cam1 as a Rigid3d (always
default-constructible to identity) so it never tripped this check;
colmap's TwoViewGeometry::cam2_from_cam1 is std::optional<Rigid3d>
so absence is real and observable.
Replace the THROW_CHECK with `continue`. Pairs without a relative
pose stay UNCALIBRATED and are dropped by downstream filters
(inlier-num / inlier-ratio) instead of crashing. Verified end-to-end
on V2-adaptive sample-10 HGE.
Zador-Pataki
added a commit
that referenced
this pull request
Apr 28, 2026
* M1-M3: drop dead RA options + extend GP options + cost functors
M1 (Hygiene):
- Drop dead glomap-fork option fields use_precomputed_weights,
fix_non_lc_weights, fixed_non_lc_weight from
colmap::RotationEstimatorOptions. Per audit (gp_inventory.md, ra_inventory.md)
these are dead in fork .cc itself or hardcoded false in pipeline. Drop
matching pycolmap def_readwrite + clean up referencing code paths in
glomap_ra::RotationEstimator (kills the obsolete SolveWeightedLS branch
+ the no-op fix_non_lc_weights warning).
- Update comment block at rotation_averaging.h:70-71 — only skip_risky_LC_pairs
+ use_video_constraints + video_*_scale survive as fork-flag stubs.
- Gate native GP first-scale pin (global_positioning.cc:349-355) on
point_constraint_type != SPLIT_METRIC_DEPTH. Preserves existing colmap GP
unit tests under default BATA mode (Q1 / R10).
M2 (Foundation):
- New file colmap/util/loss_config.h: LossFunctionConfig {name, scale,
weight} + CreateLossFromConfig factory. Names: trivial, huber, cauchy,
arctan, softlone. weight!=1 wraps in ScaledLoss.
- Extend colmap::GlobalPositionerOptions with glomap-fork additions:
PointConstraintType enum (BATA / SPLIT_METRIC_DEPTH), use_init,
random_init_scale, depth toggles (use_log_scale_for_depth_map_scales,
use_log_residual_for_depth, zero_residual_behind, smooth_log_linear_transition,
log_linear_threshold), scale_prior_stddev, filter_depth_outliers,
initial_dmap_scales (optional<map<image_t, double>>),
10 LossFunctionConfig fields with names verbatim from existing glomap_ra
port (loss_normal_geometry, loss_normal_depth, loss_lc_geometry,
loss_lc_depth, loss_normal_geometry_inlier, loss_normal_depth_inlier,
loss_normal_depth_outlier, loss_normal_geometry_trackstart,
loss_normal_depth_trackstart, loss_scale_prior).
- Change random_seed default from -1 to 1 (Decision 9, preserves
byte-identity for existing call sites that previously inherited GP_SEED=1).
- Extend GlobalPositioner state: dmap_scales_, dmap_scale_observation_counts_,
depth_outliers_, 10 cached_loss_*_ shared_ptrs, soft_outlier_fallback_loss_.
M3 (Cost functors):
- Add WeightedBATADirectionalError to cost_functions/motion_averaging.h.
AutoDiff <3,3,3,1> on (c_i, X_k, scale). Anisotropic per-axis weighting
(1/sigma_x, 1/sigma_y, 1/sigma_z). Caller computes
sigma_z = (sigma_x + sigma_y) / 2.
- New file cost_functions/metric_depth.h with 3 functors:
MetricDepthError (5-toggle, 12 reachable combinations — smooth_transition
only meaningful in use_log_residual=true branch),
ScalePriorError (linear), LogScalePriorError (log-space, prior=log(1)=0).
All ported verbatim from fork's cost_function.h.
Cluster cleanup (carried over from prior session):
- Strip 175 lines of dead Fetzer focal cost from sfm/cost_function_glomap.h.
- Strip 47 lines of FilterTracksByReprojection (unbound, unused) from
sfm/track_filter_glomap.{h,cc}.
Verified: ATE 37.40 / 66.77 / 93.35 / 96.68 / 99.67 unchanged (M0a baseline)
on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0 + canonical DB
+ GP_SEED=1.
* M4: SPLIT_METRIC_DEPTH path body in colmap::GlobalPositioner
Implement the trivial-rig case of the metric-depth residual structure.
Activated by point_constraint_type=SPLIT_METRIC_DEPTH (default still
BATA — no behavior change for native callers).
Per-observation in AddPoint3DToProblem (image.IsRefInFrame() branch only):
- When image.depth_prior_validity[fid] is true and depth_prior > 0,
emit a 1-D MetricDepthError residual on
(frame_centers_[image.FrameId()], point3D.xyz, dmap_scales_[image_id]).
- Lazy-insert dmap_scales_ on first valid observation per image. Initial
value: from options_.initial_dmap_scales (caller-supplied GP1→GP2
handoff) if provided, else 1.0 (linear) / 0.0 (log).
- Increment dmap_scale_observation_counts_ for the per-image
ScalePriorError weight.
Per-image emission in AddPointToCameraConstraints (after track loop):
- Reset dmap_scales_ + dmap_scale_observation_counts_ at start (lazy
filled by AddPoint3DToProblem).
- After all observations added: emit one ScalePriorError /
LogScalePriorError per image with depth observations. Loss is wrapped
in ceres::ScaledLoss(TrivialLoss, obs_count) so dense-depth images get
proportionally stronger priors. M5 will replace TrivialLoss with the
configurable cached_loss_scale_prior_.
Parameter ordering / bound (M4 step 5):
- dmap_scales_ ride alongside frame_centers_ in parameter group 2.
- Linear-space: SetParameterLowerBound(&dmap_scale, 0, 1e-5) prevents
collapse. Log-space: no bound (parameter is log-space, positivity comes
from exp() in MetricDepthError).
Init flag wiring (M4 steps 6-7):
- options_.use_init short-circuits InitializeRandomPositions for both
camera centers AND track xyz (collapses fork's split flag).
- options_.random_init_scale replaces hardcoded 100.0 cube size in two
call sites.
Verified: ATE 37.40 / 66.77 / 93.35 / 96.68 / 99.67 unchanged (default
BATA path) on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0 +
canonical DB + GP_SEED=1.
* M5: GP body Cluster B — loss routing fabric + two-loop iteration
Implement glomap-fork loss-routing cascade in colmap::GlobalPositioner.
Activated by point_constraint_type=SPLIT_METRIC_DEPTH; default BATA mode
unchanged.
Refactor: extract per-observation logic into AddObservationToProblem
helper (point3D_id, observation, is_lc_observation, random_initialization,
reconstruction). AddPoint3DToProblem becomes a dispatcher iterating
track.Elements() (is_lc=false) then track.lc_elements (is_lc=true) —
mirrors fork's two-parallel-collection structure.
Cache pre-warm in AddPointToCameraConstraints: materialize 10
cached_loss_*_ via CreateLossFromConfig. Default TrivialLoss/weight=1
config is equivalent to no override; non-default Huber/Cauchy/etc take
effect through cascade.
is_excluded skip: top of AddObservationToProblem.
4-way geometry cascade (SPLIT_METRIC_DEPTH only):
is_lc -> loss_lc_geometry, is_track_anchor -> loss_normal_geometry_trackstart,
is_inlier -> loss_normal_geometry_inlier, else -> loss_normal_geometry.
5-way depth cascade for MetricDepth residual:
depth_outliers_ + LC -> SKIP depth residual entirely (deletes
metric_depth_cost to avoid leak),
depth_outliers_ + non-LC -> ScaledLoss(HuberLoss(1), 1) hardcoded
soft fallback,
is_lc -> loss_lc_depth, track_anchor -> loss_normal_depth_trackstart,
is_inlier -> loss_normal_depth_inlier, MDRP-flagged outlier ->
loss_normal_depth_outlier, else -> loss_normal_depth.
ScalePriorError wrapper updated: replaces M4 TrivialLoss(obs_count)
with cached_loss_scale_prior_ wrapped in ScaledLoss(obs_count). Default
config preserves obs_count weighting unchanged.
Verified ATE 37.40/66.77/93.35/96.68/99.67 unchanged (default BATA)
on sample-10 LaMAR HGE.
* M6: GP filter_depth_outliers pre-Solve pass
Implement glomap-fork pre-Solve depth-outlier filter in
colmap::GlobalPositioner. Activated by
options_.filter_depth_outliers + point_constraint_type=SPLIT_METRIC_DEPTH.
In AddPointToCameraConstraints, after cache pre-warm + dmap_scales_
reset:
- Seed dmap_scales_ from options_.initial_dmap_scales when caller
supplied them (typical GP2 path: GP1 hands off solved scales). Linear
scale converted to log if use_log_scale_for_depth_map_scales=true.
Lazy-insert in AddObservationToProblem then preserves these values.
- Clear depth_outliers_; if filter_depth_outliers, call
FilterDepthOutliers(reconstruction).
FilterDepthOutliers method (new):
- Per-point3D: walk track.Elements() and track.lc_elements separately.
- For each observation: skip when depth_prior_validity[fid]=false or
depth_prior <= 1e-6 or stddev_rel <= 1e-9.
- Apply per-image scale (dmap_scales_[image_id]) if available; else
raw prior. Threshold check:
|log(z_est) - log(scale * depth_prior)| >= 3 * log(1 + stddev_rel)
- Insert (image_id, point2D_idx) into depth_outliers_ on flag.
Anonymous-namespace helper DepthOutlierFlag deduplicates the regular vs
LC sweep.
Native uses image.CamFromWorld() * X_world (the canonical cam_from_world
projection) — fork's pre-Solve branch had inadvertently relied on the
in-Solve flipped translation convention via the image.cam_from_world
member directly; native's CamFromWorld() resolves cleanly because we run
this BEFORE the Solve loop temporarily flips frame_centers_.
Verified ATE 37.40/66.77/93.35/96.68/99.67 unchanged (default BATA mode +
filter_depth_outliers=false) on sample-10 LaMAR HGE.
* M7 (binding side): extend pycolmap.run_global_positioning for SPLIT_METRIC_DEPTH
Pycolmap binding for native colmap::GlobalPositioner now exposes the full
glomap-fork option surface (M2) + dict return shape (Decision 7).
Option struct extension (BindGlobalPositioner):
- New PointConstraintType enum binding (BATA / SPLIT_METRIC_DEPTH).
- Bind use_init, random_init_scale, depth toggles
(use_log_scale_for_depth_map_scales, use_log_residual_for_depth,
zero_residual_behind, smooth_log_linear_transition,
log_linear_threshold), scale_prior_stddev, filter_depth_outliers.
- Bind initial_dmap_scales as Optional[Dict[image_id, float]] with None
short-circuit; Python passes linear-space, C++ stores per
use_log_scale_for_depth_map_scales convention.
- Bind 10 LossFunctionConfig fields as dict-typed properties via
templated BindLossField helper accepting/emitting
{'name', 'scale', 'weight'}. Names verbatim from glomap_ra port:
loss_normal_geometry, loss_normal_depth, loss_lc_geometry,
loss_lc_depth, loss_normal_geometry_inlier, loss_normal_depth_inlier,
loss_normal_depth_outlier, loss_normal_geometry_trackstart,
loss_normal_depth_trackstart, loss_scale_prior.
run_global_positioning return shape:
- Was: bool. Now: dict {success: bool, dmap_scale_map: Dict[image_id,
float] (linear), dmap_scale_map_nested: Dict[image_id, {linear, log}]}.
- Reconstruction is mutated in place; Python reads images/tracks from
rec.images / rec.points3D directly.
- Mirrors glomap_ra binding return shape so videosfm caller migration
(Python side, deferred to next session) is a near-1:1 swap.
GetDmapScales accessor on GlobalPositioner exposes the per-image
dmap_scales_ map for the binding's dict-extraction step. Linear
conversion (exp() when use_log_scale_for_depth_map_scales=true) happens
in the lambda.
Verified: ATE 37.40/66.77/93.35/96.68/99.67 unchanged on sample-10 LaMAR
HGE — caller still on glomap_ra path; native binding extension is
additive and doesn't affect existing flow.
* M7 (C++): scales_ reservation fix + dmap_scales std::map + Ceres options bindings
Bug fixes uncovered during M7 caller migration:
1. SetupProblem scales_.reserve undercount: was only counting
point3D.track.Length(); M5's two-loop iteration also walks
track.lc_elements, each emplace_back-ing a new scale. Without
reserved capacity, vector reallocation invalidates earlier &scale
pointers stored in Ceres residual blocks → "ParameterBlock with
size 1 has at least one invalid value". Fix: include lc_elements
in the reservation count.
2. dmap_scales_ container: switched from unordered_map<image_t, double>
to std::map<image_t, double>. Ceres residuals store
&dmap_scales_[image_id] data pointers; a hash-table rehash during
lazy-insert in AddObservationToProblem invalidates them. std::map
(rb-tree) keeps pointers stable for the entry's lifetime.
3. Parameter group separation: dmap_scales_ now go in their own
parameter group (group_id+1), separate from frame_centers_. Mixing
1-D dmap_scales with 3-D frame_centers in the same Schur-ordering
group breaks the Schur-complement preprocessor — Ceres downgrades
to SPARSE_NORMAL_CHOLESKY then rejects the inhomogeneous block sizes.
Separate group keeps each Schur block size-uniform.
4. Pycolmap binding: added solver_options.{num_threads,
max_num_iterations, function_tolerance, gradient_tolerance,
parameter_tolerance} as def_property pass-throughs on
GlobalPositionerOptions. The solver_options nested struct is not
directly bound in pycolmap, so caller migration would otherwise
fail with "object has no attribute 'solver_options'".
Mirrors the existing pyglomap binding's pattern.
Verified GP runs to CONVERGENCE (no more spurious FAILURE termination)
on sample-10 LaMAR HGE under both BATA and SPLIT_METRIC_DEPTH paths.
ATE differs from glomap_ra baseline (cross-implementation drift expected
per Decision 7); structural integrity restored.
* M8: plumb IRLS final weights via out-param (preserves bool return ABI)
Glomap-fork RA contributed a per-pair IRLS final-weight map that the
videosfm pipeline reads for its consecutive-pair-weight degeneracy
diagnostic (rotation_averaging.py:117-129). Native colmap RA discards
the IRLS weight vector after the final factorize. M8 plumbs it out.
Plumbing (4 layers):
1. RotationAveragingProblem gains final_weights_ member +
FinalWeights() accessor + SetFinalWeightsFromIRLS(weights_irls)
setter. The setter translates the per-row weight vector into a
per-pair map keyed by image_pair_t. Each PairConstraint owns 1
(gravity-aligned) or 3 (full-3DOF) consecutive rows starting at
constraint.row_index — we read weight at row_index (all rows
share the same weight per ComputeIRLSWeights). Gauge-fixing rows
excluded.
2. SolveIRLS captures the last successful iteration's weight vector
in last_weights and calls problem.SetFinalWeightsFromIRLS at the
end (after the convergence loop). Empty if SolveIRLS doesn't run
(e.g. L1-only path).
3. RotationEstimator::EstimateRotations + SolveRotationAveraging gain
std::unordered_map<image_pair_t, double>* final_weights = nullptr
out-param. When non-null, copy problem.FinalWeights() to *out
after the solve.
4. RunRotationAveraging mirrors. Wired through both branches (with /
without rig expansion).
Pycolmap binding:
- Adds extract_final_weights: bool = False kwarg. Default returns
bool (preserves upstream colmap test ABI per Decision 8). When True,
returns {success: bool, final_weights: Dict[image_pair_t, float]}
dict. C5 fix: indexed by constraint.row_index (the row-major index
into weights_irls), NOT by pair_info.index (the inventory's earlier
draft).
Default behavior unchanged. Verified the C++ builds clean.
* M9: skip_risky_LC_pairs filter via 4-layer CorrespondenceGraph plumbing
Glomap-fork RA's skip_risky_LC_pairs drops pairs whose loop-closure
inlier count strictly exceeds non-LC inliers. The filter reads
ImagePair.{inliers, are_lc} fork fields — these live on
CorrespondenceGraph::ImagePair, NOT on PoseGraph::Edge (which is a
strict subset). M9 plumbs an optional CorrespondenceGraph* through 4
layers so RA can apply the filter without bloating the upstream
PoseGraph interface.
Plumbing:
1. CorrespondenceGraph gains const ImagePairsMap() accessor (the
existing MutableImagePairs() can't be called through a const
pointer). Used only by RA M9.
2. RotationAveragingProblem ctor takes optional CorrespondenceGraph*
(default nullptr). Stored as correspondence_graph_ member.
3. BuildPairConstraints reads cg.ImagePairsMap()[pair_id].{inliers,
are_lc} when options_.skip_risky_LC_pairs && correspondence_graph_
non-null. Counts LC inliers; if lc_count > non_lc_count, continue
(pair excluded from constraint matrix entirely).
4. RotationEstimator::EstimateRotations + SolveRotationAveraging +
RunRotationAveraging gain optional CorrespondenceGraph* parameter
(default nullptr; passes through to RotationAveragingProblem ctor).
5. Pycolmap binding adds correspondence_graph: CorrespondenceGraph =
None kwarg. Default behavior unchanged for callers that don't pass
the CG.
Default behavior: with skip_risky_LC_pairs=False (default) or
correspondence_graph=None, no filtering — exactly matches pre-M9 behavior.
Verified C++ builds clean.
* M10: prioritize_tracking MST penalty for LC-dominated edges
Glomap-fork RA's prioritize_tracking spanning-tree initialization
penalises LC-dominated edges so the MST picks tracking-dominated edges
as parents. Activated when use_video_constraints=true (M11 video-Ceres
path). Without the penalty, LC-contaminated initialization undermines
the Ceres video-aware solve.
ComputeMaximumPoseGraphSpanningTree (local helper in
rotation_averaging.cc) now accepts:
- prioritize_tracking (bool, default false): enable LC-penalty.
- correspondence_graph (CorrespondenceGraph*, default nullptr):
source of ImagePair.{inliers, are_lc} fork fields.
When prioritize_tracking + correspondence_graph are both set, the MST
weight for each edge is reduced by 1e9 if the pair has strict LC
majority (lc_count > non_lc_count). Native MST maximises weights, so
fork's +1e9 to weights_boost becomes -1e9 to weight here
(convention inversion). Fallback to plain num_matches weight when
fork fields absent or empty.
InitializeFromMaximumSpanningTree forwards the CG; reads
options_.use_video_constraints to set prioritize_tracking. Wired
through SolveRotationAveraging from EstimateRotations + RunRotationAveraging.
Default behavior unchanged when use_video_constraints=false (no penalty
applied). Verified C++ builds clean.
* M11: video-Ceres SolveCeres + IsTrackingPair + RelativeRotationError
Glomap-fork RA video-aware Ceres path. Activated when
options_.use_video_constraints && !options_.use_gravity. Replaces L1+IRLS
with a Ceres optimization over per-frame 3-DOF angle-axis blocks. Each
pair contributes a relative-rotation residual wrapped in either
HuberLoss (tracking-dominated pair, scale=video_tracking_huber_scale)
or CauchyLoss (LC-dominated pair, scale=video_lc_cauchy_scale).
Anonymous-namespace helpers:
- RelativeRotationError: AutoDiff<3, 3, 3> functor on (r1_aa, r2_aa).
Residual = AngleAxis(R2^T * R_rel * R1) where R_rel is precomputed
from PairConstraint::Full3DOF::R_cam2_from_cam1. Sign convention
matches fork (no leading negative — the resulting AngleAxis is
identical sign to the L1+IRLS tangent residual at convergence;
Ceres doesn't care about sign anyway since LM minimizes
||residual||^2).
- IsTrackingPair: counts LC inliers from CorrespondenceGraph::ImagePair
fork fields (inliers + are_lc); returns true when non_lc_count >=
lc_count. Reads via problem.CorrespondenceGraphPtr() (M9 plumbing).
RotationAveragingProblem accessor additions (M11):
- MutableEstimatedRotations() — direct mutable access to per-frame
3-DOF angle-axis storage so Ceres can optimize in place.
- FrameIdToParamIdx() — frame_id -> 3-DOF block offset mapping.
- ImageIdToFrameId() — for trivial-rig path PairConstraint
image_id -> frame_id resolution.
- FixedFrameId() — gauge-fixing anchor.
- CorrespondenceGraphPtr() — M9 plumbing surface.
RotationAveragingSolver::Solve dispatches video-Ceres path before
L1+IRLS when use_video_constraints && !use_gravity. Mutually-exclusive
warning logged when both are set (use_video_constraints disabled).
Cost is Huber+Cauchy with TAKE_OWNERSHIP semantics; no leak. Solver:
SPARSE_NORMAL_CHOLESKY, max 100 iters. M0c-equivalent verification:
default behavior unchanged when use_video_constraints=false.
* M13: delete glomap_ra GP + RA (videosfm now drives both via native)
Final deletion phase. Pre-deletion grep verified that the deleted files
have no remaining external references after M7 + M12 caller migration.
Submodule pycolmap.glomap_ra stays alive — track-filter,
track-establishment, image-pair-inliers, view-graph-manipulation, and
relative-pose-estimation continue to bind under it.
Files deleted (~3000 LOC):
- src/colmap/sfm/global_positioning_glomap.{h,cc}
- src/colmap/sfm/rotation_averaging_glomap.{h,cc}
- src/colmap/sfm/cost_function_glomap.h
- src/colmap/sfm/l1_solver_glomap.h
- src/colmap/sfm/optimization_base_glomap.h
- src/colmap/sfm/tree_glomap.{h,cc} (only RA used it)
- src/pycolmap/sfm/global_positioning_glomap.cc
- src/pycolmap/sfm/rotation_averaging_glomap.cc
Files KEPT (still used by surviving glomap_ra modules):
- rigid3d_glomap.{h,cc} — track_filter_glomap consumer
- two_view_geometry_glomap.{h,cc} — image_pair_inliers consumer
- track_filter_glomap, track_establishment_glomap, image_pair_inliers,
view_graph_manipulation, view_graph_calibration, relative_pose_estimation
CMakeLists updates:
- src/colmap/sfm/CMakeLists.txt: drop deleted entries from SRCS list.
- src/pycolmap/sfm/bindings.cc: drop BindGlobalPositionerOptions /
BindGlobalPositioningGlomap / BindRotationAveragingGlomap forward
decls + dispatch calls. Other binders (BindViewGraphManipulation,
BindRelativePoseEstimation, BindTrackEstablishmentGlomap,
BindImagePairInliersGlomap, BindTrackFilterGlomap) preserved — those
bind to the pycolmap.glomap_ra submodule which stays alive.
Pycolmap CMake auto-globs sources, so the .cc deletions remove
themselves from the binding build.
Verified: end-to-end pipeline runs on sample-10 LaMAR HGE +
canonical DB + GP_SEED=1 with native pycolmap.run_global_positioning +
pycolmap.run_rotation_averaging only. ATE produced (cross-impl drift
vs glomap_ra-baseline expected per Decision G2).
* M5 fix: dispatch WeightedBATADirectionalError when angular_stddevs available
Bug uncovered by post-port audit (audit_algorithmic_semantics.md
suspect #1): the M3 cost functor WeightedBATADirectionalError was
defined and built, but AddObservationToProblem unconditionally created
the unweighted BATAPairwiseDirectionCostFunctor — the M5 cascade only
swapped the LOSS function, not the COST function. Result: anisotropic
per-keypoint weighting was dropped on every geometry residual,
causing the SPLIT_METRIC_DEPTH path to trade off geometry vs metric-
depth at the wrong relative scale.
Fix: dispatch WeightedBATADirectionalError when
image.angular_stddevs[fid] is populated (videosfm pipeline always
populates). sigma_x, sigma_y from the Vector2d entry; sigma_z =
0.5*(sigma_x+sigma_y). Falls back to BATAPairwiseDirectionCostFunctor
when sigmas absent. Mirrors fork's dispatch at
glomap/glomap/estimators/global_positioning.cc:592-601, 646-657.
Verified ATE recovery on sample-10 LaMAR HGE / ios_2022-01-12_14.59.02_000-0
+ canonical DB + GP_SEED=1:
- Pre-fix: 0.00 / 0.00 / 17.22 / 57.92 / 95.79
- Post-fix: 39.44 / 68.32 / 93.66 / 96.83 / 99.68
- Baseline: 37.40 / 66.77 / 93.35 / 96.68 / 99.67 (glomap_ra)
Native port now produces parity-or-slightly-better ATE than the
pre-port glomap_ra baseline. Cross-implementation drift is within
~5% (Decision G2 sanity threshold).
* global_positioning: add GP_SEED env-var fallback in ctor
When random_seed == -1 (caller explicitly opts out of deterministic
seeding) fall back to the GP_SEED env var so the documented Tier-2
byte-identity recipe (CLAUDE.md "ATE byte-identity") works without
patching every call site for the transition cycle.
Plan-vs-execution audit flagged this: header docstring claimed
GP_SEED fallback existed but ctor only honored options_.random_seed >= 0.
Adding cstdlib for std::getenv/std::atoi.
* global_positioning: mark random_seed default + GP_SEED fallback as reproduce-fork TODOs
Per user audit ask: any place where M0–M13 changed colmap4 default
behavior to reproduce the pyglomap fork should be marked as a TODO
not silently hardcoded — the eventual goal is to converge back to
native defaults.
Two GP-side crutches:
- random_seed default 1 (upstream is -1 / random_device)
- GP_SEED env-var fallback in ctor when random_seed == -1
Both stay in place for Tier-2 byte-identity but are now marked
TODO(reproduce-fork). Companion notes:
.claude/notes/glomap_audit/audit_default_overrides.md
* sfm/*_glomap*: add TODO(dedup-glomap-vs-colmap4) banners
Six fork-imported headers in colmap/sfm/ now carry a per-file TODO
comment pointing at the dedup audit
(.claude/notes/glomap_audit/audit_glomap_files_vs_colmap4.md). Each
banner names which symbols in that TU duplicate native colmap4
helpers vs which are unique fork algorithms.
grep -rn 'TODO(dedup-glomap-vs-colmap4)' colmap/src now surfaces the
~250-350 LOC dedup follow-up scoped for after the M0-M13 PR merges.
* estimators: gate fork additions behind opt-in options (vanilla-by-default)
Per backward-compat study (audit_backward_compat.md): make vanilla
upstream colmap4 the default for both GP and RA so non-fork callers
get unmodified upstream behavior. Every fork-added feature now
requires an explicit opt-in.
GlobalPositionerOptions — 3 changes:
- New `use_observation_exclusions` (default false): gates the
per-observation skip via `image.is_excluded[idx]` in
AddObservationToProblem (Gate G-1).
- New `use_lc_observations` (default false): gates the
`track.lc_elements` second loop in AddPoint3DToProblem and the
paired sweep in FilterDepthOutliers. This IS the GP-side LC config —
pairs with RA's `skip_risky_LC_pairs` to give two complementary LC
controls (Gate G-2).
- `random_seed` default restored from 1 -> -1 (upstream non-deterministic
random_device). The GP_SEED env-var fallback in the ctor stays as a
documented escape for the Tier-2 byte-identity recipe (Gate G-3).
RotationEstimatorOptions — 1 change:
- New `prioritize_tracking_in_mst` (default false): decouples M10 MST
LC-penalty from M11 `use_video_constraints`. Previously
`ComputeMaximumPoseGraphSpanningTree(prioritize_tracking=...)` was
hardcoded to `use_video_constraints`; now a separate knob (Gate R-1).
pycolmap binding: 3 new `def_readwrite` entries on GlobalPositionerOptions
(use_observation_exclusions, use_lc_observations) and RotationEstimatorOptions
(prioritize_tracking_in_mst).
Net effect:
- Vanilla pycolmap.run_global_positioning / run_rotation_averaging
callers now get upstream colmap4 behavior unmodified.
- videosfm flips all four gates true via _to_native_gp_options /
_to_pycolmap_ra_options so the existing pipeline is unchanged.
Smoke-tested: defaults are False/-1, post-flip True/1.
* sfm: dedup *_glomap helpers against colmap4 native
Three-step dedup pass (audit_glomap_files_vs_colmap4.md). Replace
fork-imported helpers that duplicated existing colmap4 native code.
#1 union_find_glomap.h (-42 LOC)
- DELETED. colmap::UnionFind<T> at colmap/math/union_find.h:40 is a
strict superset (Reserve / FindIfExists / Compress / Parents) and
the body of Find/Union is byte-equivalent.
- Added 1-line Clear() to native colmap::UnionFind so it covers the
one method TrackEngine used that wasn't already in native.
- track_establishment_glomap.h now includes colmap/math/union_find.h
and uses unqualified UnionFind (resolves to colmap::UnionFind via
parent-namespace lookup).
#2 rigid3d_glomap.{h,cc} (-79 LOC)
- DELETED entirely. All 8 symbols turned out to be dead code:
- DegToRad / RadToDeg duplicated colmap/math/math.h:59-64
- RotationToAngleAxis / AngleAxisToRotation / Rigid3dToAngleAxis /
CalcAngle*2 / CalcTrans / CalcTransAngle were declared and defined
but never called from any TU after M0-M13.
- Includers (image_pair_inliers_glomap.h, track_filter_glomap.cc)
switched to direct includes of colmap/math/math.h + colmap/geometry/rigid3.h.
#3 two_view_geometry_glomap.{h,cc} (-83 LOC)
- DROPPED EssentialFromMotion (= EssentialMatrixFromPose at
colmap/geometry/essential_matrix.cc:97).
- DROPPED SampsonError(E, Vec2, Vec2) overload (= ComputeSquaredSampsonError
at essential_matrix.cc:168, callers pass .homogeneous()).
- DROPPED HomographyError (= ComputeSquaredHomographyError at
colmap/geometry/homography_matrix.cc:266, identical formula).
- DROPPED FundamentalFromMotionAndCameras (was 1-line wrapper, dead
code — never called).
- KEPT CheckCheirality (depth-gated single-pair extension, no native
equivalent), GetOrientationSignum (vendored from GC-RANSAC),
SampsonError(E, Vec3, Vec3) (depth-aware z-divide variant).
- image_pair_inliers_glomap.cc updated to call native helpers; added
geometry/essential_matrix.h + geometry/homography_matrix.h includes.
Net: -204 LOC across 4 files dropped + 6 files updated. Build clean,
pycolmap loads, defaults unchanged.
* sfm: collapse glomap_ra::Track onto colmap::Point3D (#4)
Final dedup step (audit_glomap_files_vs_colmap4.md). Replace the
vendored `glomap_ra::Track` struct with native `colmap::Point3D`
throughout TrackEngine, TrackFilter, and the 2 binding TUs.
The vendored type was almost a verbatim duplicate of Point3D:
- xyz / color / is_initialized -> already on Point3D (M0/glomap-fork)
- observations (vector<pair>) -> Point3D::track.Elements() (vector<TrackElement>)
- lc_observations -> Point3D::track.lc_elements (already added at scene/track.h:93)
- track_id -> redundant with the map key, dropped
Changes:
- track_establishment_glomap.h: drop `Track` struct + `Observation`
typedef. `track_t` now aliases `colmap::point3D_t`. API uses
`unordered_map<track_t, Point3D>`.
- track_establishment_glomap.cc: 4 algorithm methods rewritten to
use Point3D::track.AddElement / .Elements() / .lc_elements / .Length().
- track_filter_glomap.{h,cc}: same.
- pycolmap/sfm/track_establishment_glomap.cc: drop ToPoint3D
conversion (35 LOC). Bindings now pass-through dict<int, Point3D>
with no fork↔native marshalling.
- pycolmap/sfm/track_filter_glomap.cc: drop ToGlomapRaTrack +
FromGlomapRaTrack (40 LOC). Same pass-through pattern.
Net: -115 LOC across 6 files. Build clean, pycolmap loads,
establish_full_tracks / find_tracks_for_problem /
glomap_ra.filter_tracks_by_angle / glomap_ra.filter_track_triangulation_angle
all expose the same dict<int, Point3D> shape they did before — videosfm
caller is unchanged.
* review fixes: docstrings, fail-loud emplace, dead-code notes
Round of post-review polish on the backward-compat + dedup PRs.
Correctness / fail-loud:
- track_establishment_glomap.cc: THROW_CHECK on the two
ProcessLoopClosurePairs tracks.emplace calls (track_id
collision was previously silent).
- track_filter_glomap.cc + track_establishment_glomap.cc: switch
track.Elements().clear() -> track.SetElements({}) so the
call survives a future signature drift on Elements()
(review #7).
Docstring / comment:
- pycolmap motion_averaging.cc: random_seed binding doc now
matches the new -1 default and surfaces the GP_SEED env-var
escape at the Python boundary (review #1, #3).
- global_positioning.h: header note on use_observation_exclusions
clarifies that Image::is_excluded is itself fork-only;
vanilla pycolmap users won't typically populate it (review #2).
- rotation_averaging.h: prioritize_tracking_in_mst comment
rephrased to describe gate semantics rather than videosfm wiring
(the very coupling we just decoupled).
- two_view_geometry_glomap.h: header explains FundamentalFromMotionAndCameras
was a dead-code delete (no replacement migration), unlike the
other 3 dropped helpers which got migrated to native (review #1
generic).
- track_establishment_glomap.h: file-level note explaining why the
glomap_ra namespace alias is kept post-collapse (binding-path
stability — pycolmap submodule + videosfm import paths).
* estimators: D1 dedup loss_config + strip rot-prone comment tags
Two cleanups bundled.
D1 (loss_config dedup, audit_dedup.md):
- LossFunctionConfig{name, scale, weight} (string-based) replaced with
LossConfig{type, scale, weight} where type is the native
CeresBundleAdjustmentOptions::LossFunctionType enum.
- Free CreateLossFromConfig factory dropped; LossConfig::CreateLossFunction()
now reuses native colmap::CreateLossFunction(type, scale) plus a
3-line ScaledLoss(weight) wrap.
- Native colmap::CreateLossFunction was previously in an anonymous
namespace inside bundle_adjustment_ceres.cc; promoted to a public
forward declaration in bundle_adjustment_ceres.h so other estimators
can route through it.
- pycolmap binding: dropped the templated string-based BindLossField
helper. Each loss field is now a plain def_readwrite returning a
pycolmap.LossConfig object. videosfm side does the
string -> LossFunctionType conversion at the bridge boundary.
- arctan support deferred — only used by gravity_refinement (separate TU,
uses ceres::ArctanLoss directly), never used by GP cascade.
Comment cleanup pass:
- Strip M0..M13 milestone tags, Gate G-X / Gate R-X labels, audit_*.md
cross-refs, videosfm sets X via Y_options'' caller-knowledge phrases.
- Reduce verbose --- Glomap-fork ... (M5) ---'' banners to short
purpose lines or drop entirely. Keep WHY / hidden-constraint /
workaround comments verbatim.
- Touched: global_positioning.{h,cc}, rotation_averaging.{h,cc},
rotation_averaging_impl.{h,cc}, motion_averaging.cc binding.
Build + smoke clean. videosfm bridge updated in companion commit.
* estimators: delete loss_config.h, fold LossConfig into global_positioning.h
Follow-up to D1 dedup. The loss_config.h after the rewrite was just a
~25-line struct definition with one consumer (GlobalPositionerOptions).
Inline it into global_positioning.h next to PointConstraintType so the
GP-specific helpers live together.
- LossConfig struct + CreateLossFunction() method moved to
colmap/estimators/global_positioning.h.
- loss_config.h deleted; CMakeLists.txt entry removed.
- Includers (global_positioning.cc, motion_averaging.cc binding) drop
the include — global_positioning.h covers it transitively.
Net: 1 fewer header in the tree.
* estimators: extract LossFunctionType + factory into ceres_loss.{h,cc}
Lift the loss-creation primitive out of bundle_adjustment_ceres.{h,cc}
into a dedicated colmap/estimators/ceres_loss.{h,cc} so it's a true
shared utility — not a private detail of CeresBundleAdjustmentOptions.
Changes:
- New colmap/estimators/ceres_loss.{h,cc}: top-level
colmap::LossFunctionType enum + colmap::CreateLossFunction(type, scale)
factory. Definition moved verbatim from bundle_adjustment_ceres.cc.
- bundle_adjustment_ceres.h: CeresBundleAdjustmentOptions::LossFunctionType
is now a back-compat using alias to colmap::LossFunctionType;
the now-redundant forward declaration of CreateLossFunction is dropped.
- bundle_adjustment_ceres.cc: definition deleted (moved to ceres_loss.cc).
- global_positioning.h: GlobalPositionerOptions gains configurable
loss_function_type + loss_function_weight fields. Default
HUBER preserves the previous hardcoded behavior. CreateLossFunction()
body now mirrors BA — dispatch via the factory + ScaledLoss(weight) wrap.
Drops include of bundle_adjustment_ceres.h in favor of ceres_loss.h.
- pycolmap motion_averaging.cc: expose loss_function_type +
loss_function_weight on GlobalPositionerOptions. The existing
pycolmap.LossFunctionType enum (bound via BA's binding through the
alias) is now the canonical Python-side type.
Net effect: vanilla colmap GP becomes loss-configurable like BA. The
fork's 10-bucket cascade (LossConfig) still uses the same enum — now
referenced via the top-level type rather than nested inside BA options.
* scene/image: fix Image copy/operator= dropping pixel_cholesky_xy_
Audit catch (parallel agents auditing fork-modified native files for
non-fork drift). The pre-existing fork copy-ctor and operator= for
colmap::Image carried all the per-feature fork fields (depth_priors,
angular_stddevs, is_inlier, is_excluded, is_track_anchor,
is_depth_outlier, etc.) but missed the more recently added
pixel_cholesky_xy_ private member. Same class of bug as the documented
`cam_from_world` regression in
memory/glomap_ra_port_lto_fp_nondeterminism.md — any py::cast roundtrip
or value-copy of an Image silently zeroed the field, breaking pose-cov
covariance plumbing for downstream consumers.
Also reorders the mem-init list to match the header declaration order.
Cosmetic-only follow-ups in the same audit pass:
- estimators/global_positioning.cc: dedent the extracted
AddObservationToProblem body by 2 spaces (was inheriting the
original loop-body indentation; cosmetic-only, no semantic effect).
- scene/track.h: collapse stray double blank line.
Build clean; default-constructed Image still works.
* estimators: D2 dedup — drop ScalePriorError/LogScalePriorError for native primitives
Per audit_dedup.md D2: replace the two fork cost functors in
cost_functions/metric_depth.h:182-244 with native
CovarianceWeightedCostFunctor<NormalPriorCostFunctor<1>> from
cost_functions/utils.h.
Mathematically equivalent:
- ScalePriorError(prior=1, sigma): r = (s - 1) / sigma
- LogScalePriorError(sigma_log): r = log_s / sigma_log
- Native: CovarianceWeightedCostFunctor<NormalPriorCostFunctor<1>>::Create(cov, prior_vec)
with cov = sigma², prior_vec = (1.0) for linear or (0.0) for log.
The linear/log selection in global_positioning.cc:226-232 collapses to
a single Create() call where the only branch is which constant the
prior_vec carries (0.0 vs 1.0).
Net: -73 LOC in metric_depth.h (the 2 functor bodies + their factories
+ docstrings), +9 LOC in global_positioning.cc (vec construction +
Create call). Build clean; ScalePriorError/LogScalePriorError no
longer exposed.
* estimators: D4 dedup — drop WeightedBATADirectionalError for native CWCF<BATA>
Per audit_dedup.md D4: replace the fork's anisotropic-weighted BATA
functor with native CovarianceWeightedCostFunctor<BATAPairwiseDirectionCostFunctor>.
The fork rotated the residual into camera frame inside the functor:
r_fork = diag(1/sigma) * R * (t_obs - s*(c2-c1))
Native CWCF whitens with sqrt(cov^-1) of a passed-in covariance.
Encoding the rotation in the world-frame covariance:
cov_world = R^T * diag(sigma^2) * R
makes ||r_native||^2 == ||r_fork||^2 (since both equal
r_world^T * cov_world^-1 * r_world). Cost contribution is identical;
residual vectors differ but Ceres loss functions act on the squared
norm so the optimization converges to the same minimum.
Why R can be baked at construction time: GP rotations are frozen
post-RA. Ceres parameter blocks are (position1, position2, scale)
only — rotation is never updated by the optimizer.
Made BATAPairwiseDirectionCostFunctor CWCF-compatible by adding the
2-line metadata (kNumResiduals + kParameterDims) the wrapper expects.
This parallels what NormalPriorCostFunctor already does and is an
upstream-friendly addition.
Net: -60 LOC functor body in motion_averaging.h, +14 LOC at the
dispatch site (cov_world construction + CWCF::Create call).
WeightedBATADirectionalError no longer exposed.
Risk: medium — WeightedBATADirectionalError was the M5 critical fix
that recovered ATE 0 -> 39. Math is provably equivalent in cost, but
autodiff Jacobian path differs vector-wise so FP precision may
shift slightly. Needs sample-10 LaMAR HGE ATE check before merge.
* sfm: Phase 5 dedup — extract track-establishment core into shared helper
Per audit_glomap_files_vs_colmap4.md Phase 5 (Strategy A): both native
GlobalMapper::EstablishTracks and fork TrackEngine::EstablishFullTracks
implemented the same union-find + intra-image consistency + length
filter + greedy subsample algorithm over post-RANSAC corr-graph
inliers. Extract the core into a new shared helper so both consumers
route through the same code; fork's LC second-pass +
2-view-depth-prior gate stay as fork-specific extensions.
New: src/colmap/sfm/track_establishment.{h,cc} (~210 LOC)
- struct TrackEstablishmentOptions{intra_image_consistency_threshold,
min_num_views_per_track, required_tracks_per_view}
- using MatchPredicate = std::function<bool(image_t, point2D_t,
image_t, point2D_t)> — optional, returns true to skip a match in
the UF phase. Used by fork to skip LC inliers in the regular pass.
- EstablishTracksFromCorrGraph(valid_pair_ids, corr_graph,
image_id_to_keypoints, options, ignore_match) — returns
unordered_map<point3D_t, Point3D>.
GlobalMapper::EstablishTracks shrinks from ~135 LOC to ~30 LOC of
orchestration: builds the keypoint map + collects valid pair ids
from pose_graph, calls the helper, commits the result via
AddPoint3D. No behavior change for native callers — predicate
defaults to nullptr (no LC skip), options carry the same defaults
as the previous inline impl.
TrackEngine::EstablishFullTracks now:
- Builds image_id_to_keypoints from images_ (fork's Image.features
rather than reconstruction's points2D — pre-rec state).
- Pre-computes an LC observation set from view_graph.image_pairs[*]
inliers + are_lc, encoded as pair<uint64_t, uint64_t> for std::set
lookup.
- Calls EstablishTracksFromCorrGraph with an ignore_match predicate
that skips matches in the LC set.
- Calls existing ProcessLoopClosurePairs as the post-pass to populate
Track::lc_elements (and mint two new tracks per LC-only observation
pair, with obs-key-derived ids that don't collide with native's
dense [0, N) range since image_id<<32 >= 2^32).
Deleted from track_establishment_glomap.{h,cc}:
- TrackEngine::BlindConcatenation (~55 LOC)
- TrackEngine::TrackCollection (~110 LOC)
- UnionFind<image_pair_t> uf_ member
- Now-unused colmap/math/union_find.h include
Total: ~165 LOC removed from fork; ~210 LOC added in the new shared
helper (most of which is moved-not-new code).
Backward compat:
- Native API gains an optional ignore_match parameter defaulting
to nullptr — vanilla callers see identical behavior.
- Fork API unchanged (TrackEngine::EstablishFullTracks signature +
return shape preserved).
- pycolmap binding unchanged.
Risk: medium — core algorithm is byte-equivalent (verbatim move of
the loop bodies), but unordered_map iteration order may differ
between native's local instance and fork's instance, which can
produce a different greedy-subsample tiebreak. Default options
have required_tracks_per_view=INT_MAX (no-op subsample), so
iteration order doesn't matter for native. Fork's caller passes
require_tracks_per_view=INT_MAX as well at this stage; the actual
greedy subsample lives in FindTracksForProblem (untouched).
Needs sample-10 LaMAR HGE ATE check before merge.
* track_establishment: iterate image_pair.matches/inliers directly
Phase 5 first-run verify revealed that ExtractMatchesBetweenImages reads
from CorrespondenceGraph::flat_corrs which is only populated by
FinalizeAfterMatchingComplete (native pipeline's geom-verify step). The
fork-driven videosfm pipeline writes inliers directly into
ImagePair::matches + ImagePair::inliers without going through
flat_corrs, so the helper saw zero matches and produced zero tracks.
Fix: iterate ImagePair::matches indexed by ImagePair::inliers directly.
Both the native colmap4 pipeline (which populates these fields during
geom-verify) and downstream pipelines that populate them via custom
relative-pose flows (e.g. videosfm's MDRP) work through this path.
Drops ExtractMatchesBetweenImages dependency entirely; drops the
feature/types.h include that came with FeatureMatches.
Verified end-to-end: sample-10 LaMAR HGE produces 25/25 registered
images with AUC 73.69/86.85/97.37 @ {0.05, 0.1, 0.5}m. Tier-1
structural identity holds through GP1+GP2 (n_tracks, total_observations,
total_lc_observations all match m13 baseline byte-for-byte at every
checkpoint).
* tests: 31 new gtest cases for fork additions + dedup correctness
Spread across 4 files:
- src/colmap/estimators/cost_functions/motion_averaging_test.cc (+9 tests)
CWCF<BATAPairwiseDirectionCostFunctor> math equivalence with random
inputs (rotation, sigma, c1, c2, scale, t_obs); zero-residual edge
case; identity-rotation isotropic-sigma reduction. Algebraically
proves D4 dedup (cov_world = R^T diag(sigma^2) R) preserves the
squared residual norm of the deleted WeightedBATADirectionalError.
- src/colmap/sfm/track_establishment_test.cc (+5 tests, NEW file)
Phase 5 helper EstablishTracksFromCorrGraph: 3-pair triangle smoke
test, length filter drops short tracks, intra-image-consistency
drop, ignore_match predicate, empty-input no-crash.
- src/colmap/estimators/cost_functions/metric_depth_test.cc (+7 tests, NEW)
MetricDepthError: zero residual when prior matches predicted depth;
linear vs log residual mode formulas; zero_residual_behind_camera
gate; log-scale parameterization; 5-toggle smoke; rejects
non-positive sigma.
- src/colmap/estimators/global_positioning_test.cc (+4 tests)
LossConfig::CreateLossFunction(): TRIVIAL/HUBER/CAUCHY/SOFT_L1
dispatch through native colmap::CreateLossFunction; weight != 1
triggers ScaledLoss wrap (verified by 2x scaling on output rho).
CMakeLists entries added for the two new test files; existing
TESTS_ENABLED=ON build picks them up. All 4 ctest groups pass.
* track_establishment_glomap: harden LC-track id minting
Phase 5 left ProcessLoopClosurePairs minting new tracks via
static_cast<track_t>((image_id<<32)|feature_id). With image_id >= 1
this lands ids >= 2^32, well above native's dense [0, N) range, so
collisions are theoretical-only. Switch to sequential ids continuing
from native's max — eliminates the magic encoding and makes the
THROW_CHECK guard truly defensive rather than load-bearing.
* tests: 34 more gtest cases for fork-unique paths + backward-compat gates
Round 2 of unit-test additions (6 areas, 34 tests). Driven by the
'genuine coverage gaps' list — fork-unique algorithms previously
tested only via end-to-end pipeline.
ProcessLoopClosurePairs (track_establishment_test.cc, +5):
4 branches of LC second-pass + sequential-id no-collision smoke.
FindTracksForProblem (track_establishment_test.cc, +6):
Length filter, 2-view depth-prior gate (drop + keep + boundary),
greedy quota, min_num_tracks_per_view default-bug enshrined.
TrackFilter (track_filter_test.cc, NEW, +7):
FilterTracksByAngle aligned/misaligned/uncalib-double-thresh +
point-behind-camera; FilterTrackTriangulationAngle parallel/perp +
threshold sweep.
RelativeRotationError (rotation_averaging_test.cc, +3):
Zero residual when consistent, non-zero on injected angular error
matches the discrepancy norm, swap+invert symmetry.
Required moving the struct from anonymous namespace in
rotation_averaging_impl.cc into rotation_averaging_impl.h so tests
can construct it directly.
ImagePairsInlierCount (image_pair_inliers_test.cc, NEW, +5):
4-gate degeneracy (epipolar, cheirality, epipole-angle, depth-prior
vs nodepth threshold). All 4 gates exercised on the essential-matrix
path with controlled synthetic inputs.
Backward-compat gates G-1/G-2/R-1 (+8):
G-1 (use_observation_exclusions) off/on/partial — global_positioning_test.cc
G-2 (use_lc_observations) off/on — global_positioning_test.cc
R-1 (prioritize_tracking_in_mst) off/on/no-corr-graph-fallback — rotation_averaging_test.cc.
Required exposing ComputeMaximumPoseGraphSpanningTree as a public
symbol in rotation_averaging.h (was anon-namespace).
Net: 65 total unit tests across 8 ctest groups, all passing.
* sfm: rename glomap_ra namespace -> sfm_ext
The ``colmap::glomap_ra::`` namespace and pycolmap.glomap_ra submodule
were named after the original rotation-averaging port from glomap, but
their current contents are track establishment, track filtering, and
pair-inlier scoring — none of which are RA. Rename to ``sfm_ext``
(SFM extensions) so the label reflects the actual contents.
Mechanical sweep across 11 C++ files + 4 binding files:
- ``namespace glomap_ra { ... }`` -> ``namespace sfm_ext { ... }``
- ``glomap_ra::Foo`` -> ``sfm_ext::Foo`` everywhere
- ``GetOrCreateGlomapRaModule`` -> ``GetOrCreateSfmExtModule``
- ``m.def_submodule("glomap_ra")`` -> ``m.def_submodule("sfm_ext")``
Python alias kept for back-compat: pycolmap/__init__.py defines
``glomap_ra = sfm_ext`` so existing videosfm callers
(``pycolmap.glomap_ra.filter_tracks_by_angle`` etc.) keep working
unchanged. Filenames keep the ``_glomap`` suffix for now — file rename
is a separate, larger churn.
Verified:
- Full build clean (warm).
- 8/8 ctests pass: track_establishment_test, track_filter_test (7/7),
image_pair_inliers_test, motion_averaging_test, metric_depth_test,
global_positioning_test, rotation_averaging_test (controllers +
estimators).
- pycolmap import + alias intact: pycolmap.glomap_ra is pycolmap.sfm_ext,
filter_tracks_by_angle resolves on both.
- videosfm GlobalPositioningPhase imports OK with the alias.
* comments(sfm_ext): drop stale glomap_ra / pyglomap / vendored references
Cleans up post-rename comment debt:
- image_pair_inliers_glomap.h: remove TODO that already shipped (Sampson/H errors now route through native helpers); drop 'vendored from glomap/types.h' framing on InlierThresholdOptions.
- track_filter_glomap.cc: replace 'glomap RA pipeline' / 'RA-shadow' wording with neutral 'dict-of-tracks state used by sfm_ext'.
- pycolmap/sfm/track_filter_glomap.cc: drop pyglomap.* Python-name mentions (module no longer exists).
- pycolmap/sfm/track_establishment_glomap.cc: drop 'Glomap-style' from establish_full_tracks docstring.
* comments(pycolmap): drop residual pyglomap migration archaeology
Second comment-cleanup pass beyond the sfm_ext-renamed files. Strips
'mirrors pyglomap.X', 'byte-for-byte parity with pyglomap', 'so the
videosfm pyglomap shim can be retired' framing now that pyglomap is
gone and the videosfm shim is removed.
Preserved: design rationale (e.g. issue #40 reference for adopting
CalibrateViewGraph wrapper, pybind11 deep-copy gotcha), open
algorithmic TODOs unrelated to the rename.
* sfm: fold sfm_ext::TrackEngine into native GlobalMapper / track_establishment
Removes the parallel colmap::sfm_ext::TrackEngine path. The fork-specific
behaviors that lived in track_establishment_glomap.{h,cc} now live as
native free functions in track_establishment.{h,cc} and as methods on
GlobalMapper. Net diff -300 LOC; one less namespace; one less file pair.
Native types already carried the data (Track::lc_elements,
ImagePair::are_lc, Image::depth_priors / depth_prior_validity were added
during the M0-M13 port). This commit fills in the missing code paths:
New free functions in colmap::namespace (track_establishment.{h,cc}):
- MakeLoopClosureMatchPredicate(valid_pair_ids, corr_graph) - returns a
MatchPredicate that excludes ImagePair::are_lc[idx]==true inliers from
the union-find pass, pre-computing the LC set in O(num inliers).
- AppendLoopClosureObservations(valid_pair_ids, corr_graph, tracks) -
4-branch second pass that populates Track::lc_elements (no-track /
both-tracks / one-track-each / same-id, mints new tracks at
max(track_id)+1). Operates on a tracks dict so binding + GlobalMapper
share the call site.
- struct TrackSubsampleOptions + SubsampleTracks(...) - depth-aware
greedy length-sorted subsample. Reads depth_priors / depth_prior_validity
/ registered_image_ids passed as separate maps to keep the function
Reconstruction-free. Asymmetric length filter preserved (lower bound
counts Length+lc_elements, upper bound counts Length only).
GlobalMapper extensions (global_mapper.{h,cc}):
- GlobalMapperOptions gains 4 fields (default-off so vanilla colmap
behavior is unchanged): track_lc_second_pass, track_two_view_depth_gate,
track_max_num_views_per_track, track_max_num_tracks.
- EstablishTracks runs MakeLoopClosureMatchPredicate +
AppendLoopClosureObservations when track_lc_second_pass=true; bypasses
the helper-side greedy subsample in that mode.
- New SubsampleTracksForProblem method bridges Reconstruction <-> dict and
calls SubsampleTracks.
Pycolmap binding (track_establishment_glomap.cc - file kept, contents
rewritten to use native API):
- pycolmap.establish_full_tracks(view_graph, images, options,
lc_second_pass=False) - top-level binding (path preserved). Takes the
native pycolmap.TrackEstablishmentOptions; lc_second_pass optional
kwarg toggles the LC second pass.
- pycolmap.find_tracks_for_problem(view_graph, images, tracks_full,
options) - takes pycolmap.TrackSubsampleOptions (new bound struct).
- The old pycolmap.TrackEstablishmentOptions (fork shape) is gone. The
native TrackEstablishmentOptions has 3 fields
(intra_image_consistency_threshold, min_num_views_per_track,
required_tracks_per_view); the 5 subsample knobs (max_num_views_per_track,
max_num_tracks, two_view_depth_gate, etc.) live on the new
TrackSubsampleOptions.
Tests (track_establishment_test.cc):
- All 16 tests preserved (5 native + 6 FindTracksForProblem-now-SubsampleTracks
+ 5 ProcessLoopClosurePairs-now-AppendLoopClosureObservations).
- MinTracksPerViewBugDocumentation rewritten to describe the new int-typed
INT_MAX default (signed comparison never trips) instead of the old
signed/unsigned -1 -> UINT_MAX promotion. Same observable behavior.
Adjacent (track_filter_glomap.{h,cc} + binding + tests):
- Repointed track_t / feature_t typedef include from the deleted
track_establishment_glomap.h to colmap/util/types.h (the canonical
point3D_t / point2D_t aliases). track_t / feature_t aliases are gone.
Files DELETED:
- src/colmap/sfm/track_establishment_glomap.h (-82 LOC)
- src/colmap/sfm/track_establishment_glomap.cc (-311 LOC)
Verified: scripts/build.sh clean (cold + warm). 7 ctest groups pass
(scene/track, scene/image, sfm/track_establishment, sfm/track_filter,
sfm/image_pair_inliers, estimators/global_positioning, estimators/
rotation_averaging) - 100% in 0.3s.
* sfm: drop _glomap suffix + sfm_ext namespace from remaining fork files
Three-stage cleanup mirroring the TrackEngine fold (commit 77c0574b).
Net result: ZERO _glomap files in colmap/sfm/, ZERO sfm_ext namespace
references. All fork-extension code now lives in plain colmap::
namespace alongside upstream code, with no naming tax.
Stage 1 - two_view_geometry_glomap deletion (-79 LOC):
- 3 helpers (CheckCheirality single-pair, GetOrientationSignum,
SampsonError Vec3 depth-aware) had ONE caller each
(image_pair_inliers). Folded as anonymous-namespace statics in the
consumer file. Deleted two_view_geometry_glomap.{h,cc} +
CMakeLists entry.
Stage 2 - track_filter_glomap rename (no LOC delta, +/- ~250):
- Renamed src/colmap/sfm/track_filter_glomap.{h,cc} -> track_filter.{h,cc}.
- Dropped sfm_ext:: namespace -> plain colmap::.
- Converted struct TrackFilter::FilterX static methods to free
functions FilterTracksByAngle / FilterTrackTriangulationAngle.
- Pycolmap binding: pycolmap.sfm_ext.filter_tracks_by_angle ->
pycolmap.filter_tracks_by_angle (top-level). Same for
filter_track_triangulation_angle. Test file + downstream callers
updated.
Stage 3 - image_pair_inliers_glomap rename (no LOC delta):
- Renamed src/colmap/sfm/image_pair_inliers_glomap.{h,cc} ->
image_pair_inliers.{h,cc}.
- Dropped sfm_ext:: namespace -> plain colmap::.
- Behavior preserved verbatim (hard-coded thres_epipole_nodepth,
disabled Gate-3 angle check, F-branch positive_count==negative_count
silent drop). TODOs for later cleanup left in-place.
- Pycolmap binding: pycolmap.image_pairs_inlier_count was already
top-level; renamed BindImagePairInliersGlomap -> BindImagePairInliers.
- Renamed src/pycolmap/sfm/track_establishment_glomap.cc ->
track_establishment.cc; renamed BindTrackEstablishmentGlomap ->
BindTrackEstablishment. Same fold treatment.
Pycolmap top-level surface (no sfm_ext submodule any more):
- pycolmap.establish_full_tracks / find_tracks_for_problem
- pycolmap.filter_tracks_by_angle / filter_track_triangulation_angle
- pycolmap.image_pairs_inlier_count
- pycolmap.TrackEstablishmentOptions / TrackSubsampleOptions / InlierThresholdOptions
Removed back-compat alias pycolmap.glomap_ra = pycolmap.sfm_ext (no
remaining callers; sfm_ext submodule itself is gone).
Verified: scripts/build.sh clean. 7 ctest groups pass (scene/track,
scene/image, sfm/track_establishment, sfm/track_filter,
sfm/image_pair_inliers, estimators/global_positioning, estimators/
rotation_averaging) - 100% in 0.29s.
* comments: drop stale sfm_ext / TrackEngine mentions in pycolmap bindings
* rotation_averaging: drop dead RotationEstimatorOptions::gravity_dir field
Audit showed gravity_dir is exposed in the binding (motion_averaging.cc)
+ declared in the struct but has ZERO consumers in
colmap/src/colmap/estimators/. Setting it does nothing — pure footgun
for anyone who flips it expecting gravity-aware behavior. Pose-prior
data is supplied separately via the run_rotation_averaging
`pose_priors` argument; this field never participated in the up-vector
math.
The ten other dead-flag suspects from the prior audit
(use_precomputed_weights, fix_non_lc_weights, fixed_non_lc_weight,
consecutive_trivial, use_relative_pose_constraints, relative_pose_default_stddev,
loss_relative_pose, debug_only_relative_pose, scale_regularization_sigma,
image_ids_to_optimize) are already GHOSTS — not on the struct, not in
the binding, not in videosfm. No additional cleanup needed.
* global_positioning: option flag for uncalibrated 0.5x ScaledLoss; fix BindImagePairInliers definition
Adds GlobalPositionerOptions::apply_uncalibrated_loss_downweight (bool,
default true). When true, AddPointToCameraConstraints wraps the BATA
loss in ScaledLoss(0.5) for cameras whose focal length lacks an EXIF
prior (Camera::has_prior_focal_length == false) - the mainline colmap4
heuristic for downweighting bearings that depend on a VGC-estimated
focal. When false, all cameras get full-weight residuals, matching the
glomap fork's behavior.
Default true preserves upstream colmap behavior; videosfm callers
calibrated against fork-flavored full-weight residuals can flip the
flag false at the bridge level (companion videosfm commit wires this
through GPOptions).
Bound in motion_averaging.cc.
Also fixes a leftover from the earlier _glomap rename (commit
db8a5a0d): src/pycolmap/sfm/image_pair_inliers.cc still defined the
binder as BindImagePairInliersGlomap while bindings.cc was already
calling BindImagePairInliers, producing an undefined-symbol linker
error any time the binding got fully relinked. The earlier build
succeeded only because the .so was cached. Rename the definition to
match.
* global_positioning + RA: hardening + comment cleanup
Three small hardening additions + comment-text cleanup across the
GP/RA C++ surface:
- rotation_averaging_impl.cc: SolveIRLS now mirrors SolveL1's NaN-guard
on the step vector. Without this, a singular pose-graph silently
corrupts cams_from_world via UpdateState and the IRLS loop returns
true with garbage residuals on the next iteration. L1 already had
this guard at line 755; IRLS didn't.
- rotation_averaging_impl.cc: RotationAveragingProblem ctor adds a
THROW_CHECK that 'skip_risky_LC_pairs=true' requires a
CorrespondenceGraph. Previously the LC-skip filter silently never
triggered when CG was nullptr - the option appeared to work but
did nothing.
- image.h: Image::operator== now also compares pixel_cholesky_xy_.
This is the field that triggered the earlier copy-ctor field-drop
bug fix (commit 985b2ecb); including it in equality means a future
unit test of the form 'EXPECT_EQ(copy, original)' would catch an
analogous regression. Other fork-extension fields (depth_priors,
is_inlier, cam_from_world, etc.) are intentionally NOT added -
those are mutable pipeline state, not identity-defining.
- global_positioning.h: cleaned 'glomap fork' / 'videosfm callers'
vocabulary leakage in the apply_uncalibrated_loss_downweight
comment; the comment now describes what the bool does without
referencing internal nomenclature. Also added a doc note that the
10 cascade LossConfigs only run under SPLIT_METRIC_DEPTH (BATA mode
silently ignores them - prevents future caller from misconfiguring
these in BATA mode and wondering why nothing changes).
* pycolmap: rename view_graph -> correspondence_graph in bindings
Match the C++ class name (CorrespondenceGraph). Aligns variable names,
parameter names, and dict keys with the type so callers see one term.
* rotation_averaging: drop RotationEstimatorOptions::prioritize_tracking_in_mst
The MST initializer's LC-edge penalty branch was a separate flag from
the video-aware Ceres solver, but they were always enabled together
(the fork wired prioritize_tracking_in_mst directly off
use_video_constraints). Collapse: ComputeMaximumPoseGraphSpanningTree
now reads options_.use_video_constraints, deleting the redundant flag,
its pycolmap binding, and its option-struct field. The function-level
prioritize_tracking parameter stays, callable directly by unit tests.
* rotation_averaging_test: rename Gate_PrioritizeTrackingInMst -> Gate_LcPenaltyMst
The flag was removed in 423c6e58; the tests now exercise the function-
level prioritize_tracking parameter directly. Rename the test cases
and the comment block to match the surviving concept (LC-penalty
branch in ComputeMaximumPoseGraphSpanningTree).
* ceres_loss + run_global_positioning: review follow-ups
- CreateLossFunction: replace silent `return nullptr` on unhandled
enum with LOG(FATAL). The previous fall-through would crash with a
nullptr deref downstream; failing at the source surfaces an enum
extension oversight immediately.
- run_global_positioning binding: drop the duplicate
``dmap_scale_map_nested`` result key. It was a literal alias of
``dmap_scale_map`` — same py::dict assigned twice. Caller now reads
the single key.
* comments: drop residual glomap-fork / videosfm vocabulary
Comment-only sweep across estimators / sfm / scene headers and
impls. Rewrites describe what the code does instead of where it
came from. No identifier or behavior changes.
* rotation_averaging: hoist SetPRNGSeed out of ComputeResiduals
ComputeResiduals is called from L1 and IRLS inner loops. The seed
call inside it reset the global PRNG to the same value every
iteration, replaying the identical jitter sequence in
ComputeGravityAligned1DOFResidual. Move the seed call to
RotationAveragingSolver::Solve (once per problem solve) so the
PRNG advances naturally across iterations — matches the fork's
behavior of seeding once at the GP/RA boundary.
* rotation_averaging: seed MST root + use inlier count for edge weight
Two fork-equivalent fixes to InitializeFromMaximumSpanningTree:
1. Seed cams_from_world[root] from the reconstruction's MDRP-prior
pose before BFS-propagating relative rotations. The previous
default-constructed identity seed rotated every descendant by
R_root^-1 vs the prior gauge, which can shift IRLS weights through
the max_rotation_error_deg filter.
2. Edge weight in ComputeMaximumPoseGraphSpanningTree now reads
inliers.size() from the CorrespondenceGraph (post-RANSAC survivor
count) when CG is plumbed through, falling back to edge.num_matches
(raw match count) for vanilla colmap callers without a CG. Matches
fork's MST weight semantic.
Test fixture updated: pair inliers vectors are now sized to match the
edge num_matches values so both weight sources agree on the MST shape.
* global_positioning: port InitializeDepthMapScalesFromObservations from fork
Restores the median-z_est/depth_prior seeding the fork ran at GP1
entry. Without this, GP1 lazy-inserts dmap_scales_ at 1.0 (linear) /
0.0 (log) for every image — the optimizer starts in a different basin
than the fork.
Source: glomap/glomap/estimators/global_positioning.cc:1006-1133.
Adapted to colmap4's Reconstruction model: iterates Points3D' track
elements (and lc_elements when use_lc_observations is set) to
accumulate per-image z_est/depth_prior samples, takes the median per
image, and seeds dmap_scales_ with log(median) or linear median per
use_log_scale_for_depth_map_scales.
Call site in Solve() is gated on SPLIT_METRIC_DEPTH + use_init +
initial_dmap_scales empty, so GP2 (which supplies initial_dmap_scales
from GP1 output) bypasses this path. Images without observations
consumed here still fall through to the lazy-insert path in
AddObservationToProblem.
* comments: drop residual migration-archaeology vocabulary
Sweep upstream-bound C++ comments / docstrings / test-file
references for "pre-port", "post-port", "M1-M13" milestone tags,
"glomap-fork", "fork-only", "videosfm-side", and the
videosfm-private issue #40 reference (incl. one runtime warning
string). Rewrites describe what the code does now. One identifier
rename: fork_sqnorm -> expected_sqnorm in motion_averaging_test.cc.
No behavior change.
* view_graph_manipulation: skip UNCALIBRATED-pair upgrade when cam2_from_cam1 is null
The original port of glomap's UpdateImagePairsConfig added a
THROW_CHECK around the UNCALIBRATED -> CALIBRATED upgrade requiring
``tvg.cam2_from_cam1.has_value()``. Pairs whose decomposition never
produced a relative pose (e.g. F-only RANSAC path or decomposition
failure) crash the pipeline at this site.
The fork's ImagePair carried cam2_from_cam1 as a Rigid3d (always
default-constructible to identity) so it never tripped this check;
colmap's TwoViewGeometry::cam2_from_cam1 is std::optional<Rigid3d>
so absence is real and observable.
Replace the THROW_CHECK with `continue`. Pairs without a relative
pose stay UNCALIBRATED and are dropped by downstream filters
(inlier-num / inlier-ratio) instead of crashing. Verified end-to-end
on V2-adaptive sample-10 HGE.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Retires the parallel
colmap::glomap_ra::GlobalPositioner/RotationEstimatorimplementations in favor of the colmap4-nativecolmap::GlobalPositioner/RotationEstimatorextended with ourglomap-fork additions. Net diff: ~−1,500 LOC (added ~1,500 to native;
deleted ~3,000 of glomap_ra GP+RA).
What changed
Native GP (
colmap/estimators/global_positioning.{h,cc}):PointConstraintType::SPLIT_METRIC_DEPTHenum + branch inAddPoint3DToProblem. Emits a 1-DMetricDepthErrorresidualalongside BATA when
image.depth_prior_validity[fid]is true.dmap_scales_parameterblocks (lazy-inserted, std::map for pointer stability) +
ScalePriorError/LogScalePriorError.(4-way geometry × 5-way depth) +
is_excludedskip + 2-loopiteration over
track.Elements()+track.lc_elements.filter_depth_outlierspre-Solve sweep populatesdepth_outliers_.SPLIT_METRIC_DEPTH) soupstream colmap GP unit tests pass unchanged.
LossFunctionConfigdict-typed properties + dict return shape.Native RA (
colmap/estimators/rotation_averaging{,_impl}.{h,cc}):ABI for upstream tests).
skip_risky_LC_pairsfilter inBuildPairConstraintsvia 4-layerCorrespondenceGraph&plumbing (problem ctor → estimator → runner→ binding).
prioritize_trackingMST: 1e9 penalty on LC-dominated edges inComputeMaximumPoseGraphSpanningTree.SolveCerespath activated byuse_video_constraints && !use_gravity. Per-pair Huber (tracking) / Cauchy (LC) loss;RelativeRotationErrorAutoDiff functor;IsTrackingPairhelper.use_precomputed_weights,fix_non_lc_weights,fixed_non_lc_weight) dropped fromRotationEstimatorOptions.Deletion (M13):
colmap/sfm/global_positioning_glomap.{h,cc}colmap/sfm/rotation_averaging_glomap.{h,cc}colmap/sfm/cost_function_glomap.hcolmap/sfm/l1_solver_glomap.hcolmap/sfm/optimization_base_glomap.hcolmap/sfm/tree_glomap.{h,cc}pycolmap.glomap_rasubmodule kept alive — track-filter,track-establishment, image-pair-inliers, view-graph-manipulation
bindings continue to use it.
Companion PR
Videosfm caller migration:
https://github.com/Zador-Pataki/videosfm-private/pulls (branch
colmap4-glomap-refactor-2).Plan + audit
Full design in worktree
.claude/notes/glomap_audit/:port_plan.md(v3) — 16-milestone plangp_inventory.md/ra_inventory.md— pre-port audit of fork-vs-native deltasround2_{cpp,python,verification}_review.md— agent reviewsexecution_log.md— milestone-by-milestone notesTest plan
colmap/scripts/build.sh) + pycolmap test suite.MPSFM_REUSE_DB=1+GP_SEED=1) through native RA + native GPafter M13 deletion.
Decision G2; reviewer should sanity-check that pre-port baseline
ATE (~37.40/66.77/93.35/96.68/99.67) is achievable post-port via
tuning of the new fork option fields if needed.