Calibrating sensors on a homemade autonomous robot means running one specific procedure per sensor type, in a fixed order: camera intrinsics from a printed checkerboard, IMU noise and bias from a long stationary recording, magnetometer hard-iron offsets by rotating the robot, time-of-flight range offsets at a known distance, and wheel odometry from a driven square path. Each procedure produces numbers you store on the robot and load at startup — not a one-time tweak you eyeball.
The order matters because later calibrations assume earlier ones are already correct. Camera-IMU extrinsics, for example, are estimated against an IMU whose own noise model is already known. Get the sequence wrong and you fold one sensor's error into another's parameters, where it is almost impossible to find later.
Table of Contents
- Camera intrinsics: the checkerboard and the number that tells you if it worked
- IMU noise, bias, and why it takes two hours of doing nothing
- Magnetometer calibration, and the limitation that bites homemade robots hardest
- Range sensors: offset and crosstalk, measured at a known distance
- Wheel odometry: drive a square in both directions
- Camera-IMU extrinsics, and the time offset people forget
- Frequently Asked Questions
Camera intrinsics: the checkerboard and the number that tells you if it worked
Camera calibration recovers the intrinsics — focal length, principal point, and lens distortion coefficients — by photographing a pattern whose real geometry you know. A printed checkerboard is the standard target. You move it around the frame, the software finds the corners, and it solves for the camera model that best explains where those corners landed. The quality metric is re-projection error. According to OpenCV's camera calibration tutorial, the closer the re-projection error is to zero, the more accurate the found intrinsics are; it is computed by re-projecting the known object points with `cv.projectPoints()`, taking the L2 norm against the detected corners, dividing by the point count, and averaging across views. The same tutorial notes you need at least ten test patterns.
Treat fewer views as an unvalidated result, not a fast one. The single most common failure is the board dimensions. The ROS 2 `camera_calibration` monocular tutorial calibrates with an 8×6 / 108 mm checkerboard and warns that `–size` counts interior vertices, not squares — so a physically 9×7 board is passed as `–size 8×6 –square 0.108`, with the square size in metres. A mismatch there silently rescales every intrinsic you get back. Nav2's camera calibration tutorial runs the same node with `–size 7×9 –square 0.02`, which confirms the point: any board geometry works, provided the interior-corner count and square size match what you actually printed. Measure your printed board with calipers rather than trusting the PDF. Home printers scale pages, and a 2% scaling error becomes a 2% error in every distance your camera later reports.
IMU noise, bias, and why it takes two hours of doing nothing
An IMU — the combined accelerometer and gyroscope that tells your robot how it is rotating and accelerating — needs two different things calibrated. One is the stochastic noise model that filters consume. The other is the magnetometer, if your board has one, which is a separate problem covered below. For the noise model, the ROS `imu_utils` Allan-variance tool requires the IMU to be recorded completely stationary for roughly two hours — its example launch file sets `max_time_min: 120`.
From that recording it derives white-noise densities and bias random-walk values, which are exactly the parameters that filters like `imu_filter_madgwick` and visual-inertial odometry front-ends expect you to supply. Default values copied from a tutorial describe a different chip than the one on your robot. Plan this as an overnight job on a surface that does not move. Foot traffic, a nearby washing machine, or a desk you lean on will contaminate a two-hour record, and the contamination shows up as noise densities that look plausible but are too large. Keep the robot powered and thermally settled; letting it warm up first and then recording gives a more representative result than starting the record the instant you flip the switch.
Magnetometer calibration, and the limitation that bites homemade robots hardest
Hard-iron error is a fixed offset in the magnetometer's readings caused by ferromagnetic material sitting near the sensor. The ROS `robot_calibration` magnetometer node treats it exactly that way — an offset introduced by nearby ferromagnetic parts such as PCB components — and collects data by rotating the robot at a commanded speed while recording the `/imu/mag` topic to a bag. Its source also logs an error that soft-iron calibration is not implemented, so expect offset correction only from that tool. Here is the part that catches hobby builds. Analog Devices states in its hard- and soft-iron correction guidance that these distortions can only be compensated if the interfering sources stay fixed and rigid relative to the magnetometer.
On a homemade robot, that assumption breaks constantly: moving the battery, swapping a motor, or adding a steel bracket invalidates a stored magnetic calibration. Finish the mechanical build before you calibrate the compass, and recalibrate after any chassis change. Some sensors demand it even more often. Bosch's BNO055 quick-start application note states that magnetometer calibration is mandatory after every power-on reset and recommends polling calibration status periodically. The part exposes that status in its `CALIB_STAT` register at 0x35, where system, gyro, accelerometer and magnetometer each get a 2-bit field running 0 (uncalibrated) to 3 (fully calibrated). Read that register in your startup code and refuse to navigate on a value below 3 rather than discovering the drift on the floor.
Range sensors: offset and crosstalk, measured at a known distance
Time-of-flight distance sensors ship with a factory offset, and the moment you change their optical environment that offset is wrong. ST's VL53L0X datasheet specifies that offset calibration be performed at the factory, recommends doing it at 10 cm, and says it accounts for supply voltage, temperature and cover glass. It also defines a separate crosstalk calibration for the signal returned by that glass.
The practical consequence for a homemade robot: if you mount the sensor behind a window, a 3D-printed bezel, or a sheet of acrylic in your chassis, you must run both calibrations yourself and store the results in the robot's own memory. The factory numbers were taken without your window in the path. Check readings at more than one distance afterwards. An offset calibrated at 10 cm that still reads badly at 1 m points at crosstalk or a dirty window, not at an offset you should re-tune.
- Fix the sensor in its final mount, with the cover glass installed, before measuring anything.
- Place a flat, matte white target at the recommended 10 cm for the offset step.
- Run the crosstalk calibration separately, since it characterises the glass reflection rather than the target distance.
- Save both values in non-volatile storage and reload them on every boot.
- Redo both after any change to the bezel, the glass, or the supply voltage.
Wheel odometry: drive a square in both directions
Wheel odometry on a differential-drive robot depends on two physical constants you cannot measure accurately with a ruler: the effective wheelbase and the effective diameter of each wheel. Tyre compression, mounting slop, and manufacturing tolerance all shift them. The standard procedure is UMBmark. Borenstein and Feng's method, published in IEEE Transactions on Robotics and Automation, calibrates differential-drive odometry by driving a pre-programmed square path in both clockwise and counter-clockwise directions, and reports accuracy as E_max,sys.
The two directions are the point of the test: on a differential-drive robot, a one-direction square can conceal two mutually compensating errors, so a robot that returns neatly to its start clockwise may still have both a wheelbase error and unequal wheel diameters hiding inside it. The method corrects for exactly those two sources. Run it on the floor the robot will actually work on. Carpet, tile and epoxy give different effective wheel diameters, and a calibration taken on one does not transfer to another.
Camera-IMU extrinsics, and the time offset people forget
Once the camera intrinsics and the IMU noise model are done, fusing the two requires knowing how they are physically mounted relative to each other. The Kalibr camera-IMU procedure from ETH Zurich's Autonomous Systems Lab estimates that spatial transform against an intrinsically calibrated IMU, with the target held fixed while you move the sensor rig to excite all IMU axes. It also estimates the time offset between the two streams, and temporal calibration is enabled by default there — you disable it with `–no-time-calibration`.
That offset is usually the thing a homebuilt rig gets wrong, because USB cameras and I2C IMUs are timestamped by different code paths with different latencies. A few tens of milliseconds of unmodelled delay will degrade a visual-inertial estimate in ways that look like a bad extrinsic transform. "Excite all IMU axes" means deliberate motion in all three rotations and all three translations while keeping the target in view — not gentle panning. Smooth, single-axis waving leaves the solver with too little information to separate the parameters, and it will hand back numbers anyway.
Frequently Asked Questions
What re-projection error is good enough for a camera?
OpenCV's tutorial frames it as a continuum rather than a threshold — the closer to zero, the more accurate the intrinsics. Compare runs against each other on your own rig, and always with at least the ten test patterns the tutorial calls for.
Do I need to recalibrate after rewiring the robot?
For the magnetometer, yes. Analog Devices notes that iron distortions are only correctable while the interfering sources stay fixed and rigid relative to the sensor, so moving a battery or motor invalidates the stored correction.
Can I skip the two-hour IMU recording and use the datasheet values?
The `imu_utils` workflow exists because filters need per-unit noise densities and bias random-walk values. Its example configuration sets 120 minutes of stationary recording, so a shorter capture gives you an Allan-variance fit with less data behind it.



