System designExplainer

New camera matrix after undistortion: how the principal point moves

How to write the new camera matrix after undistortion: the principal point shifts by the returned view origin; fx and fy stay. Worked 4000 × 3000 example.

LumaSpec Vision10 min read Share on LinkedIn

On this page 8 sections
  1. 01The rule: only the principal point moves
  2. 02Why fx and fy stay put
  3. 03Worked example: a 4000 × 3000 frame under a valid view
  4. 04The three view cases in code
  5. 05What breaks when you keep the old matrix
  6. 06When translation is not the whole answer
  7. 07Checks before you ship the new matrix
  8. 08Questions

You undistorted the image, and the calibration file no longer describes it. Every pixel-to-world calculation downstream — gauging, robot guidance, plane fitting — inherits whatever matrix you feed it, so the real question is which intrinsics belong to the new frames. The rule is shorter than the question: undistortion changes exactly one matrix entry, the principal point, and only when you change the view. Keep the default view and the exported matrix is already correct; choose full or valid and the principal point translates by the origin the call returns. This page gives the rule, the three view cases, worked arithmetic on a 4000 × 3000 frame, and the checks that catch a skipped translation before it costs millimeters on the line.

The rule: only the principal point moves

Call undistortImage and leave OutputView at its default "same", and nothing moves. The undistorted frame lines up pixel for pixel with the input, so the exported cameraParameters matrix already is the new camera matrix. Copy it and move on.

Set OutputView to "full" or "valid" and exactly one entry changes. The call then returns a second output, newOrigin: the input-image pixel that lands at output pixel (0, 0). It is (0, 0) px for "same", positive for "valid" because the all-valid rectangle starts inside the original frame, and negative for "full" because the canvas grows past the frame edges. The principal point is measured from that origin, so the whole correction is one line:

newPrincipalPoint = cameraParams.PrincipalPoint - newOrigin

fx, fy and the zero skew stay as calibrated. Assemble the new matrix around the translated point:

cx_new = cx - origin_x cy_new = cy - origin_y fx_new = fx, fy_new = fy K_new = [fx 0 cx_new; 0 fy cy_new; 0 0 1]
OutputView Returned newOrigin New camera matrix
"same" (default) (0, 0) px identical to the exported one
"valid" positive px, crop starts inside the frame principal point translated, fx and fy unchanged
"full" negative px, canvas extends past the frame principal point translated, fx and fy unchanged

One subtraction, three cases. There is no extra call to hunt for, because the matrix never moves by more than this.

Why fx and fy stay put

Undistortion is a resampling. Each output pixel receives the value measured at the matching distortion-free location on the image plane, at the sensor's original pixel pitch. fx and fy do one job: they convert normalized image-plane coordinates to pixels. The pixel did not change size, so they do not change value.

What changes is where the canvas begins. A full view adds margin around the frame, shifting every original pixel — including the projection of the optical axis — right and down by that margin. A valid view crops the borders, shifting everything left and up. The principal point is the optical center counted in canvas pixels, so it rides along by exactly the origin offset. Nothing zooms and nothing rotates; only the counting origin moves.

Worked example: a 4000 × 3000 frame under a valid view

The inputs, and where each comes from:

  • Frame size: 4000 × 3000 px, the printed output size of the USB3 camera featured below.
  • Calibration output: fx = fy = 3200.0 px, principal point (1997.6, 1506.8) px. These are illustrative values of the kind a healthy calibration returns; yours will differ. They sit 2.4 px left of and 6.8 px below the frame center (2000, 1500).
  • newOrigin for OutputView="valid": (56, 42) px, illustrative. The all-valid rectangle starts 56 px right and 42 px below the original top-left corner.
  • Standoff to the measured surface: 160 mm, from the application.

The rule in words: subtract the origin from the principal point, keep fx and fy, and read the new frame size from the returned image.

The arithmetic:

  • cx_new = 1997.6 - 56 = 1941.6 px
  • cy_new = 1506.8 - 42 = 1464.8 px
  • K_new = [3200.0 0 1941.6; 0 3200.0 1464.8; 0 0 1]
  • New canvas: 4000 - (56 + 40) = 3904 px wide, 3000 - (42 + 34) = 2924 px tall. Sanity check: the canvas center is (1952, 1462), and the translated principal point sits 10.4 px left and 2.8 px below it, still a small, believable offset.

The cost of skipping the translation, if you keep cx = 1997.6 on the cropped frame:

  • Pixel scale on the surface: 160 mm / 3200 px = 0.05 mm/px.
  • Shift in every x measurement: 56 px × 0.05 mm/px = 2.80 mm. That is 28 times a ±0.1 mm tolerance, constant in one direction, so it masquerades as a mounting offset.

The decision for each case: with OutputView="same", reuse the exported matrix unchanged. With "valid", ship the translated matrix with your own calibration numbers. With "full", run the same subtraction; the origin comes back negative. At this standoff, a ±0.1 mm tolerance survives a 2 px ignored shift, because 0.1 mm / 0.05 mm/px = 2 px. The example's 56 px is far past that.

The frame size in this example is the native output of the USB3 model. The GigE model carries the same 12 MP IMX226 sensor over GigE.

Area scan cameras12 MP GigE area scan camera, IMX226, 1/1.7" rolling shutter, 9 fps (LS-CMR-0053)LS-CMR-0053
Sensor format
1/1.7"
Lens mount
C-mount
$560.00View part
Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
$560.00View part

The three view cases in code

% view is "same" (default), "full" or "valid"
[J, newOrigin] = undistortImage(I, cameraParams, OutputView=view);
newPrincipalPoint = cameraParams.PrincipalPoint - newOrigin;   % (0, 0) for "same"
newImageSize = size(J, 1:2);

Build the new matrix from fx, fy and newPrincipalPoint, and store it together with newImageSize. A valid view is smaller than the sensor output and a full view is larger. For the default view you can skip the arithmetic entirely, because newOrigin is (0, 0) and the exported matrix is already correct.

OpenCV splits the same job differently. getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, alpha, ...) returns the new matrix directly, with alpha interpolating between a cropped view at alpha = 0 (no black borders, some source pixels lost) and a full canvas at alpha = 1 (all source pixels kept, black borders — the black borders after undistortion page untangles that trade-off). Pass the result to undistort(), or precompute pixel maps once with initUndistortRectifyMap() when the calibration is fixed and the line runs all shift. Leaving newCameraMatrix empty makes undistort() reuse your calibrated matrix, which reproduces the same-view case.

One portability detail: OpenCV counts pixels from (0, 0) and MATLAB from (1, 1), so cx and cy differ by exactly 1 px when you carry a calibration between the two.

What breaks when you keep the old matrix

Any pixel-to-world conversion reads cx and cy directly: X = (u - cx) × Z / fx, where Z is the standoff and Z / fx is the surface scale in mm/px. Use the old matrix on a translated canvas and every measurement shifts by Δcx × Z / fx. In the worked example that was 56 px × 0.05 mm/px = 2.80 mm at a 160 mm standoff. Pixel-to-millimeter gauging is exactly the workflow the coin size sorting walkthrough runs end to end.

The shift is constant per axis, so it hides well. The robot grips 2.80 mm from where vision says it should, every cycle, in one direction; it reads as a mechanical offset, not a software one. Deprojection inherits it too: a point cloud built from an undistorted frame leans sideways by the principal-point error, which is why the guide to deprojecting aligned depth with the right intrinsics puts the matrix choice first.

Overlays drift as well. Boxes drawn in undistorted coordinates and pasted onto the original frame land off-target by exactly (Δcx, Δcy) in pixels, which at 1:1 scale is a plain translation you can add back.

Two places genuinely do not care. A blob centroid used only to sort parts left and right inside one fixed view needs no matrix. And a gauge you re-teach on undistorted frames absorbs the shift during teaching — the mismatch only bites when the taught frames and the production frames come from different views.

When translation is not the whole answer

Three cases need more than the subtraction:

  • You resize the output. Undistort at native size, halve the frame for a classifier, and fx, fy, cx and cy all multiply by 0.5. Translate first, scale second, because the returned origin refers to the native canvas.
  • The lens is a fisheye. Equidistant models do not pass through the same pinhole math, and a model mismatch fails loudly before any matrix arithmetic matters. The guide to the fisheye undistortImage assertion error walks that failure, and fisheye undistortion pixel mapping shows how the remap bends pixels toward the edge.
  • The image is rectified for stereo. initUndistortRectifyMap() combines undistortion with a rectifying rotation, and the matrices that come out of rectification form a new projection. Use them as a set, not as a translated K.

Checks before you ship the new matrix

  • Same-view check: with OutputView="same", the new matrix must equal the exported one to the last decimal. Any difference is a transcription bug, not calibration physics.
  • Center check: the translated principal point should land within a few to a few tens of pixels of the new canvas center; the example sat 10.4 px and 2.8 px away. A point far outside the frame can still be legitimate, and principal point outside the image explains when to distrust one.
  • Residual check: run your corner or circle detector on the undistorted board image again. Healthy calibrations reproject within roughly 0.1–0.3 px. If undistortion made the image look worse instead of better, that is a separate fault — start at undistortion looks worse after calibration.
  • Gauge check: measure a known dimension on an undistorted frame with the new pixel scale, and confirm the error sits inside the part tolerance.

If the camera behind the calibration is due for a re-spec anyway, shortlist sensors in the area scan cameras category, filter by resolution and interface in the camera finder, or start the camera configurator at the camera step. The systems topic page collects the rest of the integration guides, and the team answers calibration questions through the contact page.

Questions

Does undistortion change the focal length in the camera matrix?

Not at native resolution. The remap keeps one output pixel per input pixel's worth of image-plane scale, so fx and fy stay as calibrated and only the principal point translates. The focal length changes in two cases: you resize the undistorted image, which scales fx, fy and the principal point together, or you rectify for stereo, which applies a rotation and returns a new projection matrix instead of a translated one.

How do I get the same result as getOptimalNewCameraMatrix?

OpenCV returns the matrix directly: alpha interpolates between a cropped view with no black borders (alpha = 0) and a full canvas that keeps every source pixel (alpha = 1). The nearest equivalent to the same-view case is passing your calibrated matrix as newCameraMatrix to undistort(), which is its default. For full or valid views, do the arithmetic yourself: subtract the returned origin from cx and cy.

What happens if I crop or resize the undistorted image afterwards?

Treat a crop as a new origin: subtract the top-left crop offset from cx and cy, exactly as for a valid view. A resize multiplies fx, fy, cx and cy by the scale factor, so a 2× downsample halves all four. Run the view translation first, then the crop and resize, and the matrix keeps describing the frames your code actually sees.

How close to the image center should the principal point be?

On a 4000 × 3000 frame the center is (2000, 1500) px, and a healthy calibration typically lands within a few to a few tens of pixels of it. Offsets beyond roughly 50 px more often mean too few board images or poor corner coverage than a broken sensor, so read the reported reprojection error before you touch the matrix. Principal points outside the image can still be legitimate.

Which matrix goes with pose estimation on an undistorted image?

The translated one, with zero distortion coefficients: the undistorted image has no lens distortion left to model, so its pixels project through the plain pinhole matrix K_new. Use the frame size returned by the undistortion call together with the matrix, because valid and full views differ in size from the sensor output. The original matrix belongs only to the same view.