System designTroubleshooting

camera_info_url ignored: resolution mismatch with the camera mode

The driver ignores camera_info_url and publishes all-zero calibration: the YAML resolution mismatches the camera mode. Match width, height and binning.

LumaSpec Vision9 min read Share on LinkedIn

On this page 10 sections
  1. 01The zeros are the driver's default, not your file
  2. 02The rule: match the mode pixel for pixel
  3. 03The portrait trap: 1200 × 1600 is not 1600 × 1200
  4. 04Worked example: what binning does to the numbers
  5. 05Ranked causes when the numbers already match
  6. 06Fix it in order
  7. 07Prevention: calibrate in the mode you run
  8. 08When matching still leaves you with garbage
  9. 09Where this leaves the build
  10. 10Questions

Your launch file sets camera_info_url, the driver starts, images arrive — and every camera_info message still carries an empty D and all-zero K, R and P. Rectification has nothing valid to work from, so undistorted topics stay broken and every pixel-to-3D projection built on those numbers is wrong. The file exists, the path is readable, and the driver never explains itself beyond a quiet startup warning. This failure bites every ROS camera driver, from FireWire nodes like camera1394 to the drivers behind industrial GigE area scan cameras and USB3 board-level cameras. The cause is nearly always one thing: the resolution written in the YAML does not equal the resolution the camera is delivering right now. Below is the matching rule, the binning arithmetic behind it, the ranked causes for stubborn cases, and the five-step fix.

The zeros are the driver's default, not your file

A camera_info message that has never seen your file has a fixed shape: distortion_model is an empty string, D is an empty list, and K, R and P are all zeros — nine entries each for K and R, twelve for P — with binning_x and binning_y at 0. The failing run printed exactly that, together with height: 1200 and width: 1600, and those two numbers are the useful clue. They come from the driver's current mode, not from the file, so they tell you the resolution the camera is actually delivering.

Whatever else you plan to check, print the camera_info stream first and read its width and height. Those are the two numbers image_width and image_height must carry, pixel for pixel. If D and K are instead non-zero but wrong, the file loaded and you have a different problem — calibration quality rather than loading.

The rule: match the mode pixel for pixel

ROS camera drivers share a camera_info manager, and its acceptance test is one comparison: the file's image_width and image_height must equal the width and height of the frames the driver delivers in its current video mode. Fail it and the manager keeps the built-in default — legal camera_info with empty content — usually with a single warning line that a busy console scrolls away. That is the whole mechanism behind an ignored camera_info_url.

yaml.image_width = delivered frame width (pixels) yaml.image_height = delivered frame height (pixels) delivered width = sensor width / binning_x delivered height = sensor height / binning_y

The delivered size is the sensor size after whatever binning, subsampling or cropping the mode applies. A 2 × 2 binned mode delivers half the sensor's columns and rows, and K is expressed in those delivered pixels, so the file must describe them. binning_x and binning_y in the file should record the binning the calibration frames carried — 1 where there is none — so downstream consumers can reason about pixel size.

The portrait trap: 1200 × 1600 is not 1600 × 1200

The file behind the failing run declared image_width: 1200 and image_height: 1600 while the camera ran video_mode "1600x1200_mono8". The mode fixes the geometry: 1600 columns and 1200 rows arrive in every frame, whatever the mounting or the application would prefer. A portrait-shaped calibration can never pass the comparison against a landscape mode, so the zeros keep coming however many times you restart.

The corrected pair for that mode is image_width: 1600, image_height: 1200, with binning_x: 1 and binning_y: 1 because the mode does not bin. Do not instead swap the mode to match a portrait file: a calibration describes the geometry it was computed on, and a file written for 1200 × 1600 frames does not become valid by calling the frames 1600 × 1200. If the screen needs portrait, rotate the rectified image downstream; the driver and the file keep describing what the sensor actually delivers.

Worked example: what binning does to the numbers

Take a 12 MP sensor delivering 4000 × 3000 in its full mode. The matching file reads image_width: 4000, image_height: 3000, binning_x: 1, binning_y: 1. Switch the driver to 2 × 2 binning and the delivered frame shrinks to 4000 / 2 = 2000 columns by 3000 / 2 = 1500 rows, so the file must read 2000 × 1500 and record binning 2. A 4 × 4 mode delivers 4000 / 4 = 1000 by 3000 / 4 = 750.

Mode binning_x, binning_y image_width to write image_height to write
1600 × 1200, full 1, 1 1600 1200
4000 × 3000, full 1, 1 4000 3000
4000 × 3000, 2 × 2 binned 2, 2 2000 1500
4000 × 3000, 4 × 4 binned 4, 4 1000 750

The same arithmetic explains the failing case: mode 1600 × 1200 against a file written 1200 × 1600 fails both comparisons, so the driver refuses the file and republishes defaults. One wrong digit is enough — image_width: 1599 against a 1600-wide mode is rejected just as flatly. You can rescale a full-resolution K into a binned mode by dividing fx, fy, cx and cy by the binning factor, but a calibration taken in each mode you deploy is the reliable route.

Ranked causes when the numbers already match

Work down this list in order; the first three cover most stubborn cases.

  1. The file was edited after the driver started. The manager reads camera_info_url once, at node start; edits on disk change nothing until a restart. Test: restart the node and print camera_info again. This is the usual reason a corrected file "does nothing".
  2. The URL never resolved. A package:// URL needs the package findable in the workspace and the path spelled exactly; a file:// URL needs an absolute path that exists and is readable. Test: the startup log names the URL it attempted — open that exact path yourself. If the log says the file was not found or opened, the problem is the path, not the pixels; camera_info not published: fix the calibration file path and name is the dedicated write-up.
  3. The YAML does not parse cleanly. The posted file declared distortion_model twice, the first time as plump_bob instead of plumb_bob. Duplicate keys are undefined in YAML, and consumers such as image_proc expect plumb_bob (five coefficients in D) or rational_polynomial (eight) and reject names they do not recognize. Test: read the file with any YAML parser and count the keys; keep one distortion_model line, spelled plumb_bob.
  4. The mode changed after the load. A runtime video_mode or ROI switch moves the delivered resolution away from the file, and nothing re-reads it. Test: print camera_info again and compare width and height with the file. Buffer-level height mismatches from mode switches have their own write-up at GenICam buffer height mismatch: TDI camera in area mode.
  5. camera_name confusion. The file's camera_name builds default file:// URLs; it does not gate a package:// load, so renaming it neither breaks nor fixes loading.

Fix it in order

  1. Print the camera_info messages while the driver streams in its normal continuous camera acquisition mode, so frames and info flow together. Read width, height and the binning fields.
  2. Write image_width and image_height exactly as printed, and binning_x / binning_y as the mode's binning — 1 where there is none.
  3. Clean the file: one distortion_model line reading plumb_bob, five values in D, K with a non-zero focal length in pixels, and P consistent with K.
  4. Restart the driver node so the manager reads the file once more.
  5. Print camera_info again and check the proof: distortion_model is plumb_bob, D holds five values, and K[0] is non-zero — in the corrected version of the failing file, K[0] is 390.0, the fx from that calibration. Zeros persisting means a ranked cause above still applies.

Prevention: calibrate in the mode you run

  • Run the calibration in the exact video mode you will deploy. The tool writes image_width, image_height and binning from the frames it actually received, so the file matches by construction. If your source has no driver manager at all — a custom or simulated camera — you calibrate and serve the YAML yourself; publish camera_info without a driver: calibrate and serve the YAML walks through it.
  • Pin video_mode in the launch file so nothing downstream silently changes the resolution the calibration describes.
  • Keep one file per mode and name it with the mode, for example calibration_1600x1200.yaml. Switching modes means switching the URL — and restarting the node.
  • Crop with roi only when you must; offsets shift the principal point in K. ROI cropping carries ordering rules of its own, covered in camera ROI offsets reset to zero: set width and height first.
  • Assert at bring-up that K[0] is non-zero in your own node; one line catches a silently ignored file before it reaches the plant.

When matching still leaves you with garbage

Three cases outlive the fix. First, the file loads — K non-zero — but rectified output still bows: the distortion_model string or the length of D is wrong; plumb_bob needs exactly five coefficients, rational_polynomial eight. Second, rescaled calibrations drift: dividing K by the binning factor is a workable estimate, but measurements wander toward the image edges, so recalibrate per mode when the application measures. Third, the same declared-versus-delivered trap exists in simulation, where the resolution you declare must match the scale you render at — simulated camera resolution and pixel scale: set the FOV right covers that side of it.

Where this leaves the build

Once camera_info publishes non-zero K in the mode you run, the driver side is done and the rest is optics and application. If you are specifying the camera itself for a ROS cell, the camera finder filters GigE, USB3 and CoaXPress models by resolution and shutter, and the camera configurator pairs the camera with lens and lighting. More driver-side fixes are collected on the systems topic page, and if a driver still refuses a file you have checked digit by digit, bring the startup log to the contact page.

Questions

Do binning_x and binning_y matter if width and height already match?

Width and height are the fields that gate the load: match them and the file is accepted. Binning must still describe the truth. Write the binning the calibration frames carried — 1 for a full-resolution mode, 2 for 2 × 2 — because consumers use it to reason about pixel size, and because a binned frame's dimensions will not pass an unbinned mode's comparison anyway. Change the binning and you need a file to match the new resolution.

Can one calibration file serve several video modes?

No. The comparison runs against the resolution the driver delivers at load time, so a file fits exactly one mode. Keep one YAML per mode, named with the mode, and switch camera_info_url together with video_mode, followed by a node restart. Dividing fx, fy, cx and cy by the binning factor gives a workable estimate for a binned mode, but a calibration taken in each deployed mode is the reliable route when the application measures.

Why must distortion_model be exactly plumb_bob?

plumb_bob names the five-coefficient Brown–Conrady model that OpenCV-style calibrations produce, and image consumers dispatch on that exact string; rational_polynomial is the eight-coefficient alternative. A misspelled name — the failing file's first line read plump_bob — either arrives as a duplicate key, which YAML leaves undefined, or loads and then fails rectification downstream. Keep one line, spelled plumb_bob, with exactly five values in D.

Which URL forms does camera_info_url accept?

Two are common: file:///absolute/path/calibration.yaml and package://package_name/path/within/package. The package form needs the package findable in the workspace before the node starts; on machines where it is not installed, an absolute file:// path is the sturdier choice. Either way the driver reads the URL once at startup, so the target must exist and be readable by then, and the startup log names the exact URL it attempted when the load fails.