Skip to content

Optimize calibrateCamera with Schur‑complement LM and parallel Jacobian accumulation#28461

Merged
asmorkalov merged 8 commits intoopencv:4.xfrom
Ron12777:opt-clean
Mar 26, 2026
Merged

Optimize calibrateCamera with Schur‑complement LM and parallel Jacobian accumulation#28461
asmorkalov merged 8 commits intoopencv:4.xfrom
Ron12777:opt-clean

Conversation

@Ron12777
Copy link
Copy Markdown
Contributor

@Ron12777 Ron12777 commented Jan 24, 2026

Summary

  • Optimized calibrateCamera for faster runtime without changing outputs using Schur‑complement LM, Parallel Jacobian accumulation, alongside other optimizations.
  • Reduced time complexity from O(n^3) to O(n)
  • Add a perf test that uses a 500-image chessboard dataset for performance testing.

Performance

base_vs_fast_results fast_vs_ceres_results base_vs_fast_param_deviation

Testing repo

Testing

  • All local tests pass

Related

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
  • The PR is proposed to the proper branch
  • There is a reference to the original bug report and related work
  • There is accuracy test, performance test and test data in opencv_extra repository, if applicable
    Patch to opencv_extra has the same branch name.
  • The feature is well documented and sample code can be built with the project CMake

@Ron12777 Ron12777 marked this pull request as draft January 25, 2026 01:04
@Ron12777 Ron12777 marked this pull request as ready for review January 25, 2026 01:04
@asmorkalov
Copy link
Copy Markdown
Contributor

@Ron12777 Thanks a lot for the contribution. It looks very promising!
Several questions and proposals:

  1. Could you add a reference to the algorithm and/or papers you used for the implementation? It's very important to have human readable description for review and further algorithm maintenance.
  2. Could you reduce the patch and do not touch code not relevant to the new feature? I agree that auto-formatting tools are very useful, but it introduce a lot of irrelevant changes. It's hard to identify important details in review and generates giant amount of conflicts during 4.x->5.x merge as calib3d is refactored there.
  3. The existing implementation and the new one may (and actually do) converge to different solutions. Also users may have some algorithms that are tunned to current behaviour. I propose to introduce a new flag to select solver with API. The new solution may be default, but I want to give the option to people.

@asmorkalov asmorkalov added this to the 4.14.0 milestone Jan 25, 2026
@asmorkalov asmorkalov changed the title Optimize calibrateCamera and tests Optimize calibrateCamera with Schur‑complement LM and parallel Jacobian accumulation Jan 26, 2026
CALIB_USE_LU = (1 << 17), //!< use LU instead of SVD decomposition for solving. much faster but potentially less precise
CALIB_USE_EXTRINSIC_GUESS = (1 << 22) //!< for stereoCalibrate
CALIB_USE_EXTRINSIC_GUESS = (1 << 22), //!< for stereoCalibrate
CALIB_USE_LEGACY = (1 << 23) //!< use legacy calibrateCamera implementation
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.

The name is not clear, because the algorithm evolves in time and we always have some legacy behaviour. I propose to name it CALIB_DISABLE_SCHUR_COMPLEMENT or similar to describe the logic change.

Size imageSize, int iFixedPoint, Mat& cameraMatrix, Mat& distCoeffs,
Mat rvecs, Mat tvecs, Mat newObjPoints, Mat stdDevs,
Mat perViewErr, int flags, const TermCriteria& termCrit )
static double calibrateCameraInternalLegacy( const Mat& objectPoints,
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.

Please move the notes about Matlab calibration engine by Jean-Yves Bouguet here. Also I propose to rename the function with Jean-Yves name or similar as "legacy" is not meaningful name for the algorithm.

return std::sqrt(reprojErr/total);
}

static double calibrateCameraInternal( const Mat& objectPoints,
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.

Similar naming here too.

@Ron12777
Copy link
Copy Markdown
Contributor Author

A note on the subtract(_mp, _mi, _me);
I couldn't use that exact snippet, because err was a view of _mp. If subtracted directly into _me, err would still hold the raw projected points, which would break the norm() calculation, and so you would have to copy into err anyways for the norm calculation. However you were right with the copy was inefficient and so what you can do is use _me directly in the norm calculation, which saves the allocation.

@Ron12777 Ron12777 requested a review from asmorkalov February 5, 2026 17:39
@asmorkalov
Copy link
Copy Markdown
Contributor

@s-trinh @catree Could you join the PR review?

@s-trinh
Copy link
Copy Markdown
Contributor

s-trinh commented Feb 11, 2026

I cannot speak about the maths as I am not an expert on this topic.

Since this method will be the default one, in my opinion it should be verified on multiple datasets that the current and the PR methods give the same results. Could be:

  • well established datasets?
  • acquire some data with different cameras?
  • use a RealSense (or any other cameras with built-in calibrated camera info) camera to acquire some data and to be able to compare the results with factory calibration data?

The main deviation between the current and the PR seems to be about the distortion parameters estimation.
So typically tests with wide-angle lens could be important to check that there is no regression IMO.

The global reprojection error could also be worth to be plotted. Maybe the new solver gives better estimated parameters, maybe not.


For my uses cases, camera calibration is an offline procedure, is performed once, so computation time is not critical wrt. to calibration accuracy. And I would want to be sure that there is no regression.

About the camera calibration procedure, I have bookmarked these links:

In short, and from what I have gathered, tilted views up to 45° are important for depth estimation and to estimate the intrinsic parameters (focal length, principal point). Good coverage of the image plane is important for distortion estimation. Usually, the larger the board appears in the image, the better it is.


Update

Another reference from the DLR CalDe and DLR CalLab calibration tool:

Try to fill the images with corners. It doesn't matter if part of the calibration pattern cannot be seen. A complete coverage of the image area is paramount at as many images as possible. If the pattern is too small (e.g. if the camera is focused to longer distances and your pattern is projected in smaller areas -- even if it is size A2 or A1) you should opt for central projections.

Note that at extremely short ranges the pinhole camera model doesn't hold anymore and the actual light path through the lens unit has to be considered instead. It is conventionally accepted that the pinhole camera model is only valid beyond close distances of approximately 30 times the focal length (Luhmann et al., 2006; Magill, 1955; Brown, 1966; Fryer; Duane C. Brown, 1986) -- I personally find this value too conservative. Bigger aperture sizes may aggravate the problem, calling for more complex camera models like the thin lens or even the thick lens camera models.

image



Take oblique images w.r.t. the calibration pattern. Using orthogonal images it is impossible for the calibration algorithm to tell pattern range from the camera's focal length (i.e. magnification) or the pattern's absolute scale (if released). You may expect that orthonormal images are neither detrimental nor beneficial to calibration (of course, they are beneficial to the estimation of the distortion parameters), but simulations show that they really are detrimental to calibration accuracy in the presence of noise.

image



Use an accurate calibration pattern, e.g. of solid, metal finishing. If you don't use a precision calibration plate, you may want to use the structure estimation options of DLR CalLab ( Settings -> General settings -> Refine object structure ). If you choose not to use the structure estimation options, then make sure that you accurately measure the calibration plate and fill in the data in DLR CalDe, or create a .cfg file. In order to measure the calibration plate, do not measure just one square but as many as you can, and then divide the result by the number of squares -- in both main directions. If your calibration plate is not rigid, you should move the camera around the (static) plate. If your stereo camera is not synchronized, you should move the plate (and keep it in place) around the camera's FOV.

image



  • example of images acquisition using the DLR CalDe calibration pattern
image

@Ron12777
Copy link
Copy Markdown
Contributor Author

Since this method will be the default one, in my opinion it should be verified on multiple datasets that the current and the PR methods give the same results. Could be:

  • well established datasets?
  • acquire some data with different cameras?
  • use a RealSense (or any other cameras with built-in calibrated camera info) camera to acquire some data and to be able to compare the results with factory calibration data?

I will run some testing on some other datasets (there are a lot of collections of images that people have taken for camera calibration) and will update you guys on what the deviation looks like.

@Ron12777
Copy link
Copy Markdown
Contributor Author

Some investigations:
I ran it on some other image sets:
https://github.com/udacity/CarND-Advanced-Lane-Lines/tree/master/camera_cal
https://github.com/TerboucheHacene/camera_calibration/tree/main/data/images
https://huggingface.co/datasets/benchen4395/CCDN_eval_dataset

Dataset N Status RMS % dev fx % dev fy % dev cx % dev cy % dev Dist L2 % dev
GoPro 10 ok 7.082163357e-13 7.671543854e-14 6.39941107e-14 1.143892364e-14 0 9.339346023e-13
GoPro 20 ok 5.49056388e-13 4.152919867e-09 3.50187602e-09 2.914125113e-09 4.608368811e-09 3.862366329e-08
GoPro 30 ok 6.709912491e-13 8.458904186e-09 7.418315817e-09 9.368062656e-09 1.070694308e-08 3.043044856e-08
GoPro 40 ok 4.015641035e-13 1.667859628e-08 1.463883693e-08 4.183623984e-09 4.335449015e-08 2.669347733e-08
GoPro 50 ok 7.38899178e-13 5.806656036e-11 2.820779795e-11 8.024747019e-11 1.793552603e-10 6.411794608e-09
GoPro 60 ok 6.640462128e-13 5.264723543e-10 4.134289559e-10 7.510879623e-10 8.890425334e-10 1.266105997e-08
GoPro 70 ok 4.300526548e-14 1.462191513e-11 1.297987316e-11 6.762242534e-11 1.247636552e-11 8.833305705e-11
uEye 10 ok 6.09369038e-05 0.0005584007923 0.001811565687 0.01221659862 0.002812460634 0.6572029787
uEye 20 ok 1.452550465e-12 1.922475117e-08 1.006212668e-08 3.495474208e-07 2.447556831e-07 6.269145578e-06
uEye 30 ok 1.170619624e-12 1.999703535e-08 2.813983983e-08 2.809415428e-07 2.044903879e-07 6.369793853e-05
uEye 40 ok 1.900662825e-10 1.070139189e-07 9.082686928e-07 7.096904035e-06 4.720722429e-07 0.002809254063
uEye 50 ok 2.09899078e-10 8.604484632e-07 4.19297597e-06 3.349606321e-05 7.46084441e-06 0.01546856722
uEye 60 ok 9.099331999e-13 5.01972062e-12 3.5016893e-12 5.218142841e-12 1.541726851e-12 2.888387621e-07
uEye 70 ok 8.078933644e-13 8.225544312e-12 3.324867353e-11 3.316158781e-10 8.813044366e-11 1.713350186e-09
Udacity 10 ok 3.598155665e-05 5.43740903e-13 7.473245873e-13 1.705032125e-11 9.808990872e-12 2.815432023e-10
Udacity 20 insufficient_valid_images - - - - - -
Udacity 30 insufficient_valid_images - - - - - -
Udacity 40 insufficient_valid_images - - - - - -
Udacity 50 insufficient_valid_images - - - - - -
Udacity 60 insufficient_valid_images - - - - - -
Udacity 70 insufficient_valid_images - - - - - -
Terbouche 10 ok 0.000141104227 9.666099276 9.668402134 0.02504553593 0.03406407801 29.89964368
Terbouche 20 ok 2.333913859e-14 8.522467047e-11 3.458754376e-11 8.783561421e-11 3.199504619e-11 1.403624072e-08
Terbouche 30 insufficient_valid_images - - - - - -
Terbouche 40 insufficient_valid_images - - - - - -
Terbouche 50 insufficient_valid_images - - - - - -
Terbouche 60 insufficient_valid_images - - - - - -
Terbouche 70 insufficient_valid_images - - - - - -

As you can see, it calculates perfectly fine, except for Terbouche 10 images.
I believe this is because those first 10 images are just not good for calibration. Looking at the parameters we get from calibrating on sets of the first 7,...,10 images with the base, we get:

N = 7

Type ret fx fy cx cy k1 k2 p1 p2 k3
base 0.9537496922 1210.3758753801 1210.8513291733 754.2258499431 602.0491512380 -0.2635778555 0.1882995651 -0.0012720238 -0.0026875657 -0.1281668353
fast 0.9537950521 1240.7184908385 1241.2084398298 754.0957204553 601.9533664940 -0.2769423079 0.2079134038 -0.0012861103 -0.0027271841 -0.1487092610

N = 8

Type ret fx fy cx cy k1 k2 p1 p2 k3
base 0.8965446079 1255.9797828513 1256.1027859093 764.0697708327 593.7471514667 -0.2869358776 0.2183852842 -0.0002499512 -0.0044314306 -0.1563686339
fast 0.8965445824 1237.5567863796 1237.6749853647 764.1226609571 593.7454820001 -0.2785716391 0.2057705706 -0.0002456907 -0.0043758441 -0.1429523969

N = 9

Type ret fx fy cx cy k1 k2 p1 p2 k3
base 0.8489801653 1285.4355983531 1285.4926788274 763.6341654053 593.1228942885 -0.2983212405 0.2341196064 -0.0001982631 -0.0048147422 -0.1749021789
fast 0.8489801046 1301.8525440025 1301.9145974547 763.5745292721 593.1157847789 -0.3059635955 0.2462606753 -0.0001998775 -0.0048667546 -0.1886994220

N = 10

Type ret fx fy cx cy k1 k2 p1 p2 k3
base 0.8094301799 1251.2336934045 1251.2728168642 769.3505210423 593.9605889788 -0.2878088833 0.2138092297 -0.0010010293 -0.0061095528 -0.1484923778
fast 0.8094290378 1130.2882975329 1130.2948242546 769.5432087726 593.7582618627 -0.2343123108 0.1399378154 -0.0008698596 -0.0055546220 -0.0767460752

so it is not an issue with the new solver, the old solver also gets erratic with it.
image
Looking at the images, we see that it is just moving it around side to side up and down, with little depth / scale / tilt changes. Once you add those (calibrate on images 0,...,20)
image
We see the deviation between base and fast collapses (in the first table).

uEye brings in the tilt depth right away so we see it has little deviation with base and fast
image
And the same with GoPro
image

Also on where this spike comes from:
image

It appears to come from image 127 of my testing images
usb_calib_0127
Adding this one image just creates massive jumps in one singular parameter (p2)
Base: -0.000725165 -> +0.000022388
Fast: -0.000725215 -> +0.000027811
From N=136 randomized images from the bank
That big jump in p2 results in the total distortion coefficient deviation being very high (the 6%). It makes both solvers freak out, just in slightly different ways.

Conclusion:
The new solver is solid, it's just when you put the solvers in situations that make them freak out and they cant get consistent parameters, they freak out in slightly different ways.

@asmorkalov
Copy link
Copy Markdown
Contributor

Fantastic research! Thanks a lot for the great job!

@s-trinh
Copy link
Copy Markdown
Contributor

s-trinh commented Feb 15, 2026

Thanks for providing results on additional datasets.


For the Terbouche dataset, it looks like they wanted to strictly acquire images at the working distance?
Maybe a better approach would have been to use a larger calibration board, and with bigger squares.
Acquisition of tilted boards seems again very important for the calibration process.

Indeed, a thorough analysis of the calibration results is important to discard problematic images.


When looking at the doc, you may want to remove:

The algorithm is based on [324] and [39]

and if relevant add your references to the bibtex file: opencv.bib and update the description of the calibrateCamera() function with your approach

  • the old references ([324] and [39]) can still be mentionned in the enum doc for CALIB_DISABLE_SCHUR_COMPLEMENT entry

@Ron12777
Copy link
Copy Markdown
Contributor Author

Updated docs. Let me know if there's anything else that should be done before merging.

@s-trinh
Copy link
Copy Markdown
Contributor

s-trinh commented Mar 11, 2026

I have found some good quality datasets:

I have done a quick comparison test for curiosity.

To create the images list:

  • ./example_cpp_imagelist_creator CameraCalibrationDataset/imagelist_canon_6D_35mm.yml "CameraCalibrationDataset/Cannon 6D Lens Cannon 35mm/"*.JPG

To calibrate:

  • ./example_cpp_calibration -w=9 -h=6 -s=0.026 -o=calibration_canon_6D_35mm.yml -op -oe -enable-k3=0 -imshow-scale=4 CameraCalibrationDataset/imagelist_canon_6D_35mm.yml

The reprojection error results outputed by the exe between OpenCV 4.x and the PR:

Dataset 4.x PR
Canon 6D Lens Canon 35mm 0.8806924 0.8806924
Canon 6D Lens Canon Ultrasonic Macro 24-105mm 3.1044864 3.1044864
Canon PowerHot SX130 0.5300340 0.5300340
GoPro Hero 4 0.6697753 0.6697753
Nikon D3200 Lens Nikon DX 18-105mm 2.3525592 2.3525592
OnePlus X 18.9651056 18.9651056
RaspberryPi 0.3602066 0.3602066

Only for the GoPro Hero 4 I have enabled the k3 distortion term. I have also not tweaked the calibration parameters.

@asmorkalov asmorkalov self-assigned this Mar 13, 2026
@asmorkalov asmorkalov merged commit 7e5463b into opencv:4.x Mar 26, 2026
26 checks passed
@asmorkalov
Copy link
Copy Markdown
Contributor

Thanks a lot for the great optimization!

@Ron12777
Copy link
Copy Markdown
Contributor Author

Wahoo !

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants