3D visionTroubleshooting

Camera fx returns 0: fix the null resolution request

Why a calibration lookup returns fx = 0, the two requests that trigger it, and how to fetch intrinsics at the resolution your camera actually runs.

LumaSpec Vision10 min read Share on LinkedIn

On this page 10 sections
  1. 01Symptom: fx prints 0.0 and nothing else is populated
  2. 02Cause 1: the request went out before the camera was opened, at 0 × 0
  3. 03Cause 2: the struct you read was never filled
  4. 04The fix: open first, then read from the camera
  5. 05What fx should look like: a focal length in pixels
  6. 06Worked example: one lens, three resolutions
  7. 07When fx is not 0 but still wrong
  8. 08Keep fx = 0 out of your pipeline
  9. 09The decision, case by case
  10. 10Questions

You ask a stereo or depth camera SDK for its calibration, print the left camera's fx, and get 0. The camera enumerates, the stream runs, yet every intrinsic comes back zero. In nearly every case the lookup is at fault, not the hardware: the parameters were requested through an information object built from an unopened handle and a 0 × 0 resolution, or read from a parameters struct the code constructed itself. Because fx is a focal length expressed in pixels, a request with no valid width and height has nothing to return. This guide gives you the two causes, the call order that fixes both, the arithmetic that predicts what fx should be at any resolution, and the assertions that prove the value is safe to project with.

Symptom: fx prints 0.0 and nothing else is populated

You read calibration_parameters_raw.left_cam.fx and the console shows 0. The tell is rarely fx alone. fy, cx and cy are 0 as well, disto[0] reads 0.0, h_fov is 0, and T.z, the left-to-right baseline term, is 0. When every calibrated quantity returns zero at once, the structure is empty: nothing was looked up.

One zero on its own can be legitimate. A low-distortion lens can carry k1 = 0.0000 while fx is a sane number. Judge by the pattern: all fields zero means the request path is broken, and a single odd value means the calibration itself. That second case gets its own section below.

Cause 1: the request went out before the camera was opened, at 0 × 0

The failing pattern builds the information object by hand: a camera handle that was never opened, plus a resolution of 0 px × 0 px. The constructor forms and field names below are the SDK's documented ones; camera, resolution and info are illustrative handle names.

camera = Camera()                                  # handle created, device not opened
resolution = Resolution(0, 0)                      # null size: 0 px wide, 0 px high
info = CameraInformation(camera, resolution)       # information assembled by hand
fx = info.calibration_parameters_raw.left_cam.fx   # float, comes back 0.0
print(fx)                                          # 0.0

Factory calibration is stored per resolution. The calibration file carries intrinsic parameters for the left and right sensors at each supported resolution, and the SDK selects the entry matching the capture configuration when it opens the device. A request at 0 × 0 matches no stored entry, so the lookup returns an all-zero structure. Building the information object by hand also skips the open step, so there is no live configuration to fall back on. Two faults, one symptom.

Quick test: print the width and height you passed into the request. If either is 0 px, or the handle was never opened, this is your cause. It is the most common one by far, and it produces exactly the all-zero signature above.

Cause 2: the struct you read was never filled

The second path involves no camera call at all. A parameters object the code constructs itself, such as sensors_param = SensorParameters(), starts as an empty placeholder. Its fields keep their initializer values: random_walk reads 0.0, and so does every field beside it. The SDK only fills its counterpart of this struct when it assembles camera information from an opened device.

The rule is the same as in cause 1, through a different door. Any value that describes hardware — intrinsics, IMU noise terms, the stereo baseline — must be read back from the information of an open camera. A struct your code allocated describes nothing until the SDK writes it.

The fix: open first, then read from the camera

The working order has four steps. Create the camera handle. Open it with the capture resolution the job needs. Call the camera's own information accessor, get_camera_information(), rather than constructing anything. Read the calibration fields out of camera_configuration.calibration_parameters.

camera = Camera()
# camera.open(...) goes here, with the working resolution configured

info        = camera.get_camera_information()
calibration = info.camera_configuration.calibration_parameters

fx    = calibration.left_cam.fx         # float, focal length in px (960.0 at 1920 px width)
h_fov = calibration.left_cam.h_fov      # float, horizontal field of view in degrees
k1    = calibration.left_cam.disto[0]   # float, first radial distortion coefficient
tz    = calibration.T.z                 # float, left-to-right offset on the z axis

Then prove the value before it moves downstream:

W = 1920                       # int, px, the capture width set at open
assert fx > 0.0
assert 0.5 * W < fx < 2.0 * W  # sanity band, derived two sections down
assert h_fov > 0.0

What fx should look like: a focal length in pixels

fx is not a focal length in mm. It is the same focal length counted in pixels, so it moves whenever the pixel count moves. The relations are the pinhole model:

fx = f_mm / pixel_pitch_mm pixel_pitch_mm = sensor_width_mm / image_width_px h_fov = 2 × atan(sensor_width_mm / (2 × f_mm))

The third line explains why the field of view does not change with resolution while fx does: FOV is set by the sensor and the lens, fx by how finely the sensor is sliced into pixels. Combine the first two and you get the sizing rule fx = (W / 2) / tan(h_fov / 2), with W the image width in px. Depth cameras run wide lenses, so factory values land in a narrow band: fx ≈ 0.5 × W at a 90° horizontal FOV and fx ≈ 2.1 × W at 27°. That band backs the assertion above; outside it, the intrinsics you hold belong to another resolution or another lens.

How sensor width, working distance and field of view set the focal length Rays from the edges of the sensor pass through the lens and spread to the edges of the field of view. The ratio of sensor width to field of view is the magnification m.CameraField of view (FOV)Working distance (WD)≈ fsensor width slensm = s / FOVf = WD × m / (1 + m)
How sensor width, working distance and field of view set the focal length. Rays from the edges of the sensor pass through the lens and spread to the edges of the field of view. The ratio of sensor width to field of view is the magnification m.

The same convention holds beyond stereo. A ToF camera intrinsic parameters guide reads fx and fy from the same calibration model, and any camera calibrated on a checkerboard — including standard area scan cameras — produces fx in pixels the same way. For what each parameter means once you hold it, see Depth camera intrinsics and distortion parameters: read, don't guess. Note too that a stereo depth image can cover less than the color image on the same optics, as Depth image field of view: why stereo coverage is smaller explains.

Worked example: one lens, three resolutions

Take a depth camera module with a 5.60 mm wide sensor and a 2.80 mm focal length; these are illustrative figures, so substitute your module's values. The horizontal FOV is fixed by the optics: h_fov = 2 × atan(5.60 mm / (2 × 2.80 mm)) = 2 × atan(1.00) = 90.0°. Now open the same module at three widths and compute fx each time:

Capture width Pixel pitch fx
1280 px 5.60 mm / 1280 px = 4.375 µm 2.80 mm / 0.004375 mm/px = 640 px
1920 px 5.60 mm / 1920 px = 2.917 µm 2.80 mm / 0.0029167 mm/px = 960 px
2560 px 5.60 mm / 2560 px = 2.1875 µm 2.80 mm / 0.0021875 mm/px = 1280 px

fx equals W / 2 in every row because tan(90° / 2) = tan(45°) = 1 collapses the sizing rule to half the image width. The optics stayed identical while fx moved from 640 px to 1280 px. This is why a request at the wrong size cannot be nearly right: an fx of 960 px is 50% high for a 1280 px image and 25% low for a 2560 px one. Cross-check the last row by scaling: fx at 2560 px should equal fx at 1280 px × (2560 / 1280) = 640 px × 2 = 1280 px, which the table confirms.

When fx is not 0 but still wrong

A nonzero fx outside the 0.5–2.0 × W band usually means the intrinsics were fetched at one size and applied to images of another. The classic triggers are a crop applied after capture, a decimated or scaled stream, and values copied from a file saved at a different width. Projection then drifts in proportion to the ratio between the two widths; the failure looks exactly like the one in SolvePnP returns wrong results when a crop rescales the intrinsics.

The repair is a proportional rescale: fx_new = fx_old × W_new / W_old, with cx scaled by the same width ratio and fy and cy by the height ratio. Scale the pixel-valued terms only, and leave dimensionless distortion terms out of the multiplication. If the size change came from decimation, Depth image resolution smaller than expected: check decimation first shows how to find the real factor, and Undistortion scaling parameter: cropping versus field of view handles the same question from the undistort side. Once the values are right, Aligned depth point cloud intrinsics: which camera to deproject with settles which camera's intrinsics should deproject the cloud.

Keep fx = 0 out of your pipeline

Four habits prevent this whole class of failure:

  • Open, then ask. Every calibration read goes through the camera information of an opened handle, never through an object assembled from parts.
  • Keep the resolution with the numbers. Read back the running width and height and store them beside fx in logs and saved calibration files, so every value stays traceable to a size.
  • Assert before use. fx > 0 and 0.5 × W < fx < 2.0 × W before the value enters any projection, undistortion or pose call.
  • Refetch or rescale on size change. A crop, a decimation or a scaled stream invalidates pixel-valued intrinsics until they are updated by the width and height ratios.

The decision, case by case

  • All fields zero, and the code built the information object itself or passed Resolution(0, 0): open the camera at the working resolution and read camera information from the handle. This single change fixes the failure in most code showing this symptom.
  • All fields zero, and the values came from a struct the code constructed: discard it and read the calibration and sensors structures out of the opened camera's information instead.
  • fx nonzero but outside the 0.5–2.0 × W band: fetch again at the running resolution, or rescale fx, fy, cx and cy by the width and height ratios.
  • fx inside the band and h_fov > 0: the lookup is healthy, and later projection errors are geometry problems, not calibration problems.

If you are choosing the depth camera this code will drive, the 3D sensors category holds the laser profilers, stereo and structured-light modules, and the camera finder filters them by type and interface; the camera configurator then walks the rest of the chain. More fixes for 3D capture and calibration sit on the 3D vision topic page.

Questions

The camera streams fine, so why does fx still print 0?

Streaming and the calibration lookup are separate steps. Opening selects a working resolution and starts capture; the intrinsics you read must come from the camera information of that same opened handle. If you built an information object yourself before open, or at a 0 × 0 resolution, the lookup has no calibrated entry to return and every field comes back zero.

Can I hard-code fx from the calibration file instead of reading it at runtime?

You can, but the file stores values at specific resolutions only. A value copied for one size is wrong for another: fx scales with image width, so a figure read at 1280 pixels doubles at 2560. Read the parameters through the open camera so they match the running resolution, and hard-code only after you have confirmed the capture size never changes.

Do distortion coefficients like k1 also change with resolution?

No. In the common pinhole model the radial and tangential coefficients act on normalized image coordinates, so k1, k2, p1 and p2 carry over unchanged between resolutions. The resolution-dependent values are fx, fy, cx and cy, which scale with the pixel dimensions. Keep the distortion coefficients in a separate step from the rescaling so nothing gets scaled twice.

How do I convert fx from pixels back to millimetres?

Multiply by the pixel pitch: f_mm = fx × pitch, where pitch equals sensor width divided by image width. With fx = 960 px, a 5.60 mm sensor and a 1920 px image, pitch = 5.60 mm / 1920 px = 2.92 µm, so f = 960 × 2.92 µm ≈ 2.8 mm. You need the sensor width from the module data sheet; fx alone is not enough.