Skip to content

Add camera intrinsics priors and parameter bounds to bundle adjustment - #4683

Draft
lpanaf wants to merge 7 commits into
mainfrom
lpanaf/add_focal_prior
Draft

Add camera intrinsics priors and parameter bounds to bundle adjustment#4683
lpanaf wants to merge 7 commits into
mainfrom
lpanaf/add_focal_prior

Conversation

@lpanaf

@lpanaf lpanaf commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Adds soft priors on camera intrinsics (focal length, principal point, extra params) with camera-model-aware bounds and prior targets. Focal length and principal point weights are direct pixel-space multipliers; the extra parameter weight is scaled by the mean focal length to produce pixel-like residuals.

Results based on the current draft PR (

with global_mapper

Undistorted Images

All-25 Pooled Results

model mode AUC@0.5 / 1 / 5 / 10 recall@0.5 / 1 / 5 / 10 registered successful scenes components
PINHOLE bounds off / no prior 66.08 / 75.47 / 85.20 / 87.10 83.39 / 85.77 / 88.54 / 89.41 897/898 25/25 26
PINHOLE bounds on / no prior 66.04 / 75.52 / 85.23 / 87.35 83.46 / 85.87 / 88.71 / 90.02 897/898 25/25 26
PINHOLE bounds on / weight-1 prior 66.24 / 75.97 / 86.81 / 89.11 83.88 / 86.87 / 90.77 / 91.97 897/898 25/25 26
FULL_OPENCV bounds off / no prior 40.44 / 48.05 / 59.13 / 62.96 53.08 / 57.43 / 64.90 / 67.97 674/898 17/25 17
FULL_OPENCV bounds on / no prior 61.24 / 71.28 / 82.56 / 85.00 79.16 / 82.78 / 86.50 / 88.21 897/898 25/25 26
FULL_OPENCV bounds on / weight-1 prior 65.35 / 75.53 / 86.91 / 89.33 83.71 / 86.92 / 91.16 / 92.25 897/898 25/25 26
EUCM bounds off / no prior 35.77 / 44.40 / 56.47 / 59.41 50.47 / 54.89 / 61.60 / 62.88 531/898 13/25 13
EUCM bounds on / no prior 54.20 / 63.87 / 77.11 / 80.53 70.53 / 75.68 / 82.58 / 85.08 897/898 25/25 26
EUCM bounds on / weight-1 prior 60.04 / 69.28 / 80.78 / 83.63 76.31 / 80.03 / 85.41 / 87.46 897/898 25/25 26

All-25 Mode Deltas

model transition ΔAUC@0.5 / 1 / 5 / 10 Δ registered Δ components
PINHOLE bounds_on-minus-off -0.04 / 0.05 / 0.02 / 0.24 +0 +0
PINHOLE prior-minus-bounds_on 0.19 / 0.45 / 1.58 / 1.76 +0 +0
FULL_OPENCV bounds_on-minus-off 20.80 / 23.23 / 23.43 / 22.03 +223 +9
FULL_OPENCV prior-minus-bounds_on 4.11 / 4.25 / 4.35 / 4.33 +0 +0
EUCM bounds_on-minus-off 18.43 / 19.47 / 20.64 / 21.12 +366 +13
EUCM prior-minus-bounds_on 5.84 / 5.41 / 3.68 / 3.11 +0 +0

Distorted Images

The distorted panel evaluates the same 25 scenes and 898 images with SIMPLE_RADIAL and THIN_PRISM_FISHEYE. Each all-25 row comes from one complete report; the original-13 and test-12 rows below are raw-error-pooled subsets of those reports.

All-25 Pooled Results

model mode AUC@0.5 / 1 / 5 / 10 recall@0.5 / 1 / 5 / 10 registered successful scenes components
SIMPLE_RADIAL bounds off / no prior 12.67 / 31.47 / 74.59 / 85.34 33.85 / 61.73 / 93.47 / 97.47 898/898 25/25 27
SIMPLE_RADIAL bounds on / no prior 12.67 / 31.47 / 74.58 / 85.34 33.87 / 61.71 / 93.47 / 97.46 898/898 25/25 27
SIMPLE_RADIAL bounds on / weight-1 prior 12.67 / 31.46 / 74.53 / 85.30 33.74 / 61.69 / 93.46 / 97.45 898/898 25/25 27
THIN_PRISM_FISHEYE bounds off / no prior 45.83 / 67.68 / 90.19 / 93.59 84.99 / 92.32 / 96.73 / 97.11 892/898 25/25 26
THIN_PRISM_FISHEYE bounds on / no prior 46.14 / 68.12 / 90.62 / 93.92 85.55 / 92.88 / 97.04 / 97.28 897/898 25/25 27
THIN_PRISM_FISHEYE bounds on / weight-1 prior 45.90 / 67.75 / 90.16 / 93.52 85.15 / 92.27 / 96.63 / 97.00 897/898 25/25 27
FULL_OPENCV bounds off / no prior 31.57 / 44.65 / 56.80 / 58.52 55.78 / 58.78 / 60.17 / 60.26 427/898 9/25 9
FULL_OPENCV bounds on / no prior 44.64 / 66.02 / 87.97 / 91.25 81.83 / 90.50 / 94.25 / 94.74 897/898 25/25 27
FULL_OPENCV bounds on / weight-1 prior 45.11 / 67.21 / 90.29 / 93.70 83.09 / 92.79 / 96.92 / 97.23 897/898 25/25 27
EUCM bounds off / no prior 0.00 / 0.00 / 0.00 / 0.00 0.00 / 0.00 / 0.00 / 0.00 0/898 0/25 0
EUCM bounds on / no prior 7.75 / 22.08 / 58.69 / 71.86 23.42 / 44.22 / 80.04 / 88.62 897/898 25/25 27
EUCM bounds on / weight-1 prior 9.63 / 25.89 / 65.70 / 77.59 29.84 / 50.66 / 86.31 / 91.55 897/898 25/25 27

All-25 Mode Deltas

model transition ΔAUC@0.5 / 1 / 5 / 10 Δ registered Δ components
SIMPLE_RADIAL bounds_on-minus-off 0.00 / 0.00 / -0.01 / -0.01 +0 +0
SIMPLE_RADIAL prior-minus-bounds_on -0.00 / -0.01 / -0.06 / -0.04 +0 +0
THIN_PRISM_FISHEYE bounds_on-minus-off 0.31 / 0.44 / 0.44 / 0.33 +5 +1
THIN_PRISM_FISHEYE prior-minus-bounds_on -0.24 / -0.37 / -0.47 / -0.40 +0 +0
FULL_OPENCV bounds_on-minus-off 13.07 / 21.37 / 31.17 / 32.73 +470 +18
FULL_OPENCV prior-minus-bounds_on 0.46 / 1.19 / 2.32 / 2.46 +0 +0
EUCM bounds_on-minus-off 7.75 / 22.08 / 58.69 / 71.86 +897 +27
EUCM prior-minus-bounds_on 1.88 / 3.81 / 7.01 / 5.73 +0 +0

With incremental mapper

Undistorted

model mode AUC@0.5 / 1 / 5 / 10 registered components largest
PINHOLE bounds off 36.44 / 56.30 / 84.16 / 89.22 892/898 30 872
PINHOLE bounds on 35.21 / 54.18 / 79.95 / 84.56 888/898 30 867
PINHOLE bounds on + prior w=1 35.21 / 54.18 / 79.95 / 84.56 888/898 30 867
FULL_OPENCV bounds off 0.29 / 0.64 / 1.60 / 2.05 268/898 43 207
FULL_OPENCV bounds on 19.10 / 29.38 / 44.07 / 50.35 887/898 34 855
FULL_OPENCV bounds on + prior w=1 43.22 / 62.27 / 82.42 / 85.72 892/898 31 860
EUCM bounds off 0.04 / 0.11 / 0.24 / 0.31 112/898 31 104
EUCM bounds on 14.96 / 26.21 / 48.40 / 54.93 882/898 32 847
EUCM bounds on + prior w=1 32.56 / 49.64 / 75.08 / 80.48 895/898 31 875

Undistorted mode deltas

model transition dAUC@0.5 / 1 / 5 / 10 d registered
PINHOLE bounds off -> bounds on -1.24 / -2.13 / -4.21 / -4.66 -4
PINHOLE bounds on -> bounds on + prior w=1 +0.00 / +0.00 / +0.00 / +0.00 +0
FULL_OPENCV bounds off -> bounds on +18.80 / +28.74 / +42.47 / +48.30 +619
FULL_OPENCV bounds on -> bounds on + prior w=1 +24.12 / +32.89 / +38.35 / +35.38 +5
EUCM bounds off -> bounds on +14.91 / +26.10 / +48.17 / +54.63 +770
EUCM bounds on -> bounds on + prior w=1 +17.60 / +23.43 / +26.68 / +25.55 +13

Where the regression comes from

Pooled over all 25 scenes / 898 images:

subset bounds off bounds on delta
all 25 scenes 36.44 / 56.30 / 84.16 / 89.22 35.21 / 54.18 / 79.95 / 84.56 -1.24 / -2.13 / -4.21 / -4.66
the other 22 scenes 37.83 / 57.98 / 85.33 / 90.08 37.92 / 58.29 / 85.91 / 90.73 +0.09 / +0.32 / +0.58 / +0.65
lecture_room, old_computer, playground 24.83 / 42.30 / 74.37 / 82.09 12.38 / 19.54 / 29.82 / 32.67 -12.46 / -22.76 / -44.55 / -49.42

Seed sweep: AUC@10, bounds_off -> bounds_on

scene seed 0 seed 1 seed 2 seed 3 mean d median d off range on range
lecture_room 91.7 -> 0.4 (-91.3) 94.5 -> 94.9 (+0.5) 83.3 -> 85.4 (+2.1) 77.3 -> 93.5 (+16.2) -18.1 +1.3 77.3-94.5 0.4-94.9
old_computer 78.0 -> 17.8 (-60.2) 51.5 -> 64.5 (+13.0) 90.4 -> 43.3 (-47.1) 72.6 -> 90.4 (+17.8) -19.1 -17.1 51.5-90.4 17.8-90.4
playground 86.7 -> 73.8 (-12.9) 37.3 -> 96.5 (+59.3) 96.6 -> 96.5 (-0.1) 91.4 -> 96.7 (+5.3) +12.9 +2.6 37.3-96.6 73.8-96.7

Seeds where bounds_on >= bounds_off on AUC@10: lecture_room 3/4,
old_computer 2/4, playground 2/4. Overall 7 of 12 seed-scene pairs
favour bounds on.

Distorted

model mode AUC@0.5 / 1 / 5 / 10 registered components largest
SIMPLE_RADIAL bounds off 10.58 / 28.02 / 69.14 / 79.43 855/898 31 822
SIMPLE_RADIAL bounds on 10.59 / 27.91 / 69.34 / 79.64 855/898 30 822
SIMPLE_RADIAL bounds on + prior w=1 10.30 / 27.12 / 68.70 / 79.15 855/898 30 822
THIN_PRISM_FISHEYE bounds off 43.77 / 64.60 / 85.86 / 88.96 846/898 40 823
THIN_PRISM_FISHEYE bounds on 44.09 / 64.86 / 85.93 / 88.99 847/898 28 826
THIN_PRISM_FISHEYE bounds on + prior w=1 42.64 / 63.64 / 85.44 / 88.64 842/898 30 819
FULL_OPENCV bounds off 40.91 / 61.17 / 82.22 / 85.23 757/898 44 747
FULL_OPENCV bounds on 42.95 / 63.60 / 84.84 / 87.91 856/898 32 817
FULL_OPENCV bounds on + prior w=1 43.33 / 64.18 / 85.64 / 88.76 856/898 32 817
EUCM bounds off 0.01 / 0.02 / 0.07 / 0.08 51/898 25 51
EUCM bounds on 14.90 / 31.38 / 67.57 / 76.92 854/898 30 824
EUCM bounds on + prior w=1 12.24 / 30.39 / 68.64 / 77.99 858/898 30 826

Distorted mode deltas

model transition dAUC@0.5 / 1 / 5 / 10 d registered
SIMPLE_RADIAL bounds off -> bounds on +0.01 / -0.11 / +0.19 / +0.22 +0
SIMPLE_RADIAL bounds on -> bounds on + prior w=1 -0.29 / -0.79 / -0.64 / -0.49 +0
THIN_PRISM_FISHEYE bounds off -> bounds on +0.32 / +0.26 / +0.07 / +0.04 +1
THIN_PRISM_FISHEYE bounds on -> bounds on + prior w=1 -1.44 / -1.22 / -0.49 / -0.36 -5
FULL_OPENCV bounds off -> bounds on +2.04 / +2.43 / +2.63 / +2.68 +99
FULL_OPENCV bounds on -> bounds on + prior w=1 +0.38 / +0.59 / +0.79 / +0.84 +0
EUCM bounds off -> bounds on +14.89 / +31.36 / +67.50 / +76.83 +803
EUCM bounds on -> bounds on + prior w=1 -2.66 / -0.99 / +1.07 / +1.07 +4

lpanaf added 5 commits August 11, 2026 08:49
Bundle adjustment had no prior on the camera intrinsics: focal length,
principal point and distortion parameters were free to drift arbitrarily,
and the only guardrail was the post-hoc Camera::HasBogusParams filter that
discards frames whose intrinsics have already degenerated.

This adds two complementary mechanisms to BundleAdjustmentOptions:

* Soft priors, enabled per parameter group by a weight. Focal length and
  principal point deviations are normalized by the maximum image dimension
  so the weights are resolution independent; extra parameters are already
  unitless. The focal length prior only applies to cameras with
  has_prior_focal_length, the principal point and extra parameter priors
  pull towards the default initialization values (image center and zero).
  All priors of a camera share one residual block with a Huber loss.
* Hard caps (bound_camera_params, on by default except for local BA and
  the single-image absolute pose refinement) that register Ceres box
  constraints using exactly the bounds HasBogusParams tests, so the
  intrinsics cannot leave the region the filter considers valid.
  Parameters that already violate the bounds are clamped first, since
  Ceres rejects an infeasible starting point.

Known limitation: there is no dedicated storage for the prior focal
length (prior_focal_length in the database is a boolean flag), so the
focal prior is snapshotted from camera.params when the problem is built.
Across successive bundle adjustment problems it therefore acts as a
damping term rather than an anchor to the original EXIF value. Pinning it
properly would require a new Camera field and a schema migration.
The bounds and prior targets added in the previous commit assumed the
generic perspective parameter layout, which does not hold for every camera
model:

* EUCMCameraModel::HasBogusExtraParams additionally requires alpha in
  [0, 1] and beta > 0, so the symmetric [-max_extra_param, max_extra_param]
  box admitted parameters that the bogus parameter filter rejects.
* EUCM initializes beta to 1, so pulling all extra parameters towards zero
  dragged beta towards the degenerate beta <= 0 region. FOV initializes
  omega to 1e-2 rather than zero.

Introduces a per-model ParamsBounds hook next to the HasBogus* checks, with
a generic implementation for perspective models, an unbounded one for
spherical models, an EUCM override, and a CameraModelParamsBounds dispatcher.
Bundle adjustment now derives the bounds from it and takes the principal
point and extra parameter prior targets from CameraModelInitializeParams,
clamped into those bounds so a prior never pulls towards a rejected value.
Also skips a parameter whose bound interval is empty, which is now
reachable and would be undefined behavior in std::clamp.

A parameterized test asserts for every camera model that clamping into the
bounds yields parameters the bogus parameter filter accepts; it is built
from the CAMERA_MODEL_CASES macro so future models are covered
automatically. Verified that it fails without the EUCM override, and that
the new EUCM bundle adjustment test fails with the previous prior targets.

Audit of the other 16 models found no further inconsistencies: focal length
and principal point bounds already matched the filter for all of them, all
perspective models have exactly two principal point parameters defaulting
to the image center, the remaining distortion parameters default to zero,
and EQUIRECTANGULAR is excluded because it has no such parameters.
# Conflicts:
#	src/colmap/estimators/bundle_adjustment_ceres.cc
Focal length and principal point prior weights now directly multiply
pixel deviations instead of being normalized by the maximum image
dimension. The extra parameter weight is scaled by the mean focal length
to convert dimensionless deviations into pixel-like residuals.
# Conflicts:
#	src/colmap/estimators/bundle_adjustment_ceres.cc
// length. The principal point and
// extra parameter priors pull towards the values the camera model
// initializes them to, i.e. the image center and, for most models, zero.
double focal_length_prior_weight = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about use variance std as parameters and CovarianceWeighted wrapper where std = 1/weight

// residuals = weights .* (params - priors), such that parameters with a zero
// weight are unconstrained.
template <typename CameraModel>
class CameraParamsPriorCostFunctor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahojnnes ahojnnes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks very much. Left some comments. Once we go through a first round of reviews, I suggest we also run some benchmark numbers.

Comment on lines +209 to +211
// Local bundle adjustment only optimizes a small subset of the problem, so
// the intrinsics are not bounded here but only during global refinement.
options.bound_camera_params = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the motivation for this. If anything, I would expect that bounding the parameters in local BA is even more important than during global BA, because there are fewer constraints?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial motivation is to still allow the model to reject bad results in the local stage, as we discussed before. We can change to enable it as well

// residuals = weights .* (params - priors), such that parameters with a zero
// weight are unconstrained.
template <typename CameraModel>
class CameraParamsPriorCostFunctor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we generalize this to "SoftPriorCostFunctor" or so? There is nothing specific to cameras in this function and it could be reused for other purposes?

Comment on lines +72 to +73
VectorN weights_;
VectorN priors_;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

// with Camera::HasBogusParams, such that the intrinsics cannot leave the
// region that the bogus parameter filter considers valid. Parameters that
// already violate the bounds are clamped into them.
bool bound_camera_params = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe set this to false by default?

Comment on lines +591 to +595
if (upper == lower) {
// Ceres rejects an empty bound interval as infeasible. The clamp
// above already pins the parameter to the only feasible value.
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be combined with upper < lower as upper <= lower ?

*problem_);

// Bound the camera parameters before adding the priors, so that the priors
// are anchored at feasible values.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// are anchored at feasible values.
// are anchored at feasible values. Notice that BoundCameraParams() clamps the input camera parameters to the valid range.

Comment thread src/colmap/sensor/models.h Outdated

// A box constraint cannot express the strict beta > 0 that HasBogusExtraParams
// requires, so beta is bounded from below by this small positive value.
constexpr double kMinEUCMBeta = 1e-6;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest moving this inside the ParamsBounds method.

// initializes them to, i.e. the image center and, for most models, zero.
double focal_length_prior_weight = 0;
double principal_point_prior_weight = 0;
double extra_params_prior_weight = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going with "weight" is probably fine here. The alternative would be "stddev". Maybe add a biref note about how these weights relate to expected standard deviations of the priors?

Comment on lines +208 to +210
// direct multipliers on deviations in pixels. The extra parameter weight is
// multiplied by the mean focal length at the time the problem is constructed
// to convert dimensionless deviations to pixel-like residuals.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit torn here on whether to keep the extra weight just defined on the original scale vs. this rescaling approach vs. an alternative rescaling approach.

Small FOV cameras with equal resolution will have a higher weight on the extra parameters as compared to large FOV cameras. Wondering whether it instead makes more sense to rescale with max(width, height) ? cc: @B1ueber2y @sarlinpe

@lpanaf

lpanaf commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks very much. Left some comments. Once we go through a first round of reviews, I suggest we also run some benchmark numbers.

I just put the results based on the current pr to the description.

@ahojnnes

ahojnnes commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Thanks very much. Left some comments. Once we go through a first round of reviews, I suggest we also run some benchmark numbers.

I just put the results based on the current pr to the description.

For more complex models, the difference is huge. Am I interpreting this right?

@ahojnnes

ahojnnes commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

For the distorted/undistorted results, are these both with --calibrated or --uncalibrated?

@lpanaf

lpanaf commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

For the distorted/undistorted results, are these both with --calibrated or --uncalibrated?

uncalibrated

@lpanaf

lpanaf commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks very much. Left some comments. Once we go through a first round of reviews, I suggest we also run some benchmark numbers.

I just put the results based on the current pr to the description.

For more complex models, the difference is huge. Am I interpreting this right?

Yes, this is also what I see: for the undistorted images, the difference is pretty large for complex camera models, while for the distorted images, adding the prior hurts a bit, but not too much.

@ahojnnes

ahojnnes commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Isn't it a bit surprising that the difference is much larger for undistorted than distorted? I would have expected the vice versa?

@lpanaf

lpanaf commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Isn't it a bit surprising that the difference is much larger for undistorted than distorted? I would have expected the vice versa?

This is not surprising to me: for the undistorted images, the extra parameters can be very much under-determined, while for the the distorted images, distortion are strong enough to give more reasonable values for the extra parameters. Additionally, for the undisorted cases, I tested OPENCV_FULL and EUCM, while I tested other models for distorted images. I will run these two models there as well to draw further conclusions.

Replace CameraParamsPriorCostFunctor with the upstream
ScaleWeightedCostFunctor composed with NormalPriorCostFunctor. Parameters
without a prior get an infinite standard deviation, which the wrapper
inverts to a zero multiplier and thus leaves unconstrained.

Make the intrinsics bounds opt-in everywhere: default
BundleAdjustmentOptions::bound_camera_params to false and drive both
incremental bundle adjustment paths from the new, user-settable
Mapper.ba_bound_camera_params instead of hardcoding them.

Also simplify the degenerate bound interval handling, scope kMinBeta to
EUCMCameraModel::ParamsBounds, and document that the prior weights are
inverse standard deviations.
@lpanaf

lpanaf commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Added incremental_mapper results to the description

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants