A depth camera hung over a robot cell rarely hangs level. You tilt it down a few degrees to reach the target, and the IMU numbers stop meaning what they say: pitch and roll still read fine from gravity, but yaw about the robot's vertical axis comes out split across two gyro channels, and integrating the wrong one walks your pick positions across the floor. This guide fixes that in four moves: confirm the sensor's axis convention on the bench, build the small rotation that cancels the mounting tilt, recover the true vertical yaw rate, and plan the reset that MEMS drift always demands. The worked example uses a 10° tilt and a 30 °/s turn, both easy to reproduce at your bench.
Why can't the accelerometer give me yaw?
It can, in the sense that gravity pins down pitch and roll directly, but yaw is rotation about the gravity vector itself, and an accelerometer only senses gravity — every yaw angle looks identical to it, so yaw has to come from the gyro instead.
Gravity defines the vertical, so its direction fixes two attitude angles outright: pitch and roll, tilted mount or not. Yaw is a rotation whose axis is the gravity vector, so spinning about the vertical changes nothing the accelerometer can sense. No amount of tilt compensation turns gravity into a yaw sensor. The gyro sees yaw clearly as rotation rate about the world vertical, but in its own tilted frame that rate spreads across two channels — exactly the mixing the next sections undo. Reading pitch and roll from the same stream is its own procedure, covered in Camera tilt from IMU data: read inclination as Euler angles.
Which axes are my IMU readings in?
Almost certainly in the camera's own frame, not the robot's: the angular-rate channels follow the sensor's printed axes, so once you tilt the camera a few degrees down, none of them points at the world vertical any more and the raw values mix yaw with pitch.
Depth-camera IMU drivers report in a sensor-fixed frame; a common one has x to the camera's right, y down through the image and z along the optical axis. Whichever convention your SDK claims, prove it on the bench before doing any math.
Park the camera still at its working tilt. In the x-right, y-down, z-forward convention, a downward pitch of θ puts g·cosθ on the down-image axis and g·sinθ on the optical axis. At θ = 10° with g = 9.81 m/s², that is 9.66 m/s² on y and 1.70 m/s² on z. If your readings pair differently, relabel the axes to match reality; the test costs one minute and saves an afternoon of wrong matrices. Turn the camera by hand about the vertical at a steady rate and the gyro channels split by the same cosθ and sinθ, confirming the convention dynamically.
Still choosing hardware? The 3D sensors pages and the camera finder list the depth and stereo cameras this workflow pairs with; the method needs raw gyro and accelerometer channels, so confirm the model exposes both.
How do I rotate the IMU data into the robot frame?
Build the small fixed rotation matrix R from the known mounting tilt θ and multiply both sensor vectors by it before use; with a single downward pitch in the usual camera axes, the world-vertical yaw rate falls out as ω_vert = −(ω_y·cos θ + ω_z·sin θ).
The symbols are plain: ω_y and ω_z are the gyro channels perpendicular to the tilt axis, in °/s; θ is the mounting tilt in radians inside the trig; the minus sign encodes a convention in which positive yaw turns right-handed about vertical pointing up. If the sum runs opposite to the robot's actual turn, drop the minus — the physics is unchanged. The general case is the same idea with the full 3×3: R is built once from the mechanical drawing and multiplies the whole gyro vector, and mounts with roll or yaw offsets as well as pitch simply stack rotations in the order the drawing specifies.
If the camera pose already lives in your robot description, the same rotation belongs there exactly once; Vertically mounted depth camera: put the rotation in the URDF joint shows where it goes.
What does the compensation change at a 10° tilt?
At θ = 10° the yaw rate no longer lives in one channel: a 30 °/s robot yaw appears as 29.5 °/s on the y gyro and 5.2 °/s on the z gyro, and only the weighted sum of the two returns the true 30 °/s.
| Quantity | Value | How it arises |
|---|---|---|
| Mounting tilt θ | 10° | measured from the mount |
| sin θ, cos θ | 0.1736, 0.9848 | standard trig |
| True yaw rate Ω | 30 °/s | the robot's turn |
| y gyro channel | −29.54 °/s | −Ω·cos θ |
| z gyro channel | −5.21 °/s | −Ω·sin θ |
| Compensated ω_vert | +29.99 °/s | −(−29.54 × 0.9848 − 5.21 × 0.1736) |
Follow the arithmetic once by hand: 29.54 × 0.9848 = 29.09 and 5.21 × 0.1736 = 0.90, so ω_vert = 29.09 + 0.90 = 29.99 °/s — the true 30 °/s to within rounding.
Skipping the compensation is what costs. Reading z alone reports 5.2 °/s, almost six times too slow. Reading y alone reports 29.5 °/s, low by 1.5%, which quietly turns a 90° cell turn into 88.6° and leaves 1.4° of error per quarter turn in every pick pose downstream. At rest, the gravity split closes the loop: 9.81 × 0.9848 = 9.66 m/s² and 9.81 × 0.1736 = 1.70 m/s², matching the tilt you measured.
How do I integrate the compensated rate into yaw?
You integrate, you do not smooth or threshold it: each cycle takes ω_vert, multiplies by the time since the last gyro sample, and adds the result to the running yaw, with Δt taken from the IMU's own timestamps so a busy CPU cannot stretch or shrink it.
Six steps cover the whole estimator.
- Measure the tilt once, mechanically, from the mount drawing or a digital level, and record θ.
- Confirm the axis convention with the gravity split; at 10° the 9.66 m/s² and 1.70 m/s² readings must land on the axes you expect.
- Average about one second of gyro samples with the camera still and store the per-channel result as the bias.
- Every sample: subtract the bias, rotate by R, take the vertical component ω_vert.
- Integrate yaw ← yaw + ω_vert·Δt with Δt from the IMU timestamps, not the host loop clock; timestamp resets and rollovers are their own failure mode, covered in IMU timestamp jumps: diagnose resets in long recordings.
- Reset yaw deliberately: when every gyro channel sits under 0.5 °/s for 0.5 s, treat the robot as stationary and pull in a heading you trust, such as odometry or a fiducial.
The shape of the loop, with generic names you can map onto your driver's channels:
theta = math.radians(10.0) # mounting tilt, measured: 10 degrees
c, s = math.cos(theta), math.sin(theta)
bias_y, bias_z = 0.012, -0.008 # deg/s, averaged at startup
yaw_deg = 0.0 # running heading, degrees
for gyro_y, gyro_z, dt in samples: # deg/s, deg/s, s from IMU timestamps
w_vert = -((gyro_y - bias_y) * c + (gyro_z - bias_z) * s)
yaw_deg += w_vert * dt
How fast does gyro-only yaw drift?
Expect roughly 1–5 degrees per minute of yaw drift from a consumer MEMS gyro run open-loop: a residual bias of just 0.03 °/s integrates to 1.8° in the first minute and 108° over an hour, so yaw needs an external correction rather than faith.
The range comes from consumer-grade MEMS bias figures and published yaw-drift studies, and it moves. A startup bias subtraction removes the constant part; a temperature swing across a shift re-introduces bias at the new temperature; vibration on a moving arm rectifies into extra bias. None of it averages out, because integration accumulates whatever bias survives, indefinitely.
Three resets work in practice. The zero-rate hold from step 6 above costs nothing and applies every time the robot stops. Odometry heading trims the drift between stops. A magnetometer gives absolute heading where the site allows one.
Does the tilt corrupt the IMU data itself?
No — the IMU measures the same physical rotation whichever way it faces, and a fixed tilt only changes which axis carries which part of the motion; what corrupts data is a mount that moves: vibration, loose brackets and thermal shifts turn a constant θ into a wandering one.
A fixed tilt is geometry, and the matrix cancels geometry exactly. Two second-order effects remain. Per-channel noise and bias reach the vertical axis with different weights, so a sensor whose noisier channel picks up more cosθ weight drifts faster as θ grows. And a non-rigid mount masquerades as rotation: loose brackets, cable pull and vibration all produce phantom rates. Filtering those out is its own subject, covered in Unstable IMU readings from a depth camera: calibrate and filter. After any bump, re-run the two gravity readings; if θ has moved, your matrix inputs moved with it.
How do I set a tilt angle I can trust?
Set the angle so it is written down and repeatable, not guessed: derive θ from the gravity split at commissioning, then hold the mount mechanically so that number survives bumps, cable pulls and shift changes.
Worked end to end: 9.66 m/s² on the down-image axis and 1.70 m/s² on the optical axis give θ = atan2(1.70, 9.66) = 10.0°. That number already includes every shim, sag and bracket in the stack, because gravity did the measuring. Re-run the two readings after any collision, and if θ moved, move it into the matrix the same day.
A graduated tilt stage dials the angle in and repeats it after every bump:
- Type
- CCD camera stand and positioning stage (LS-MNT-S01)
The LS-MNT-0001 slope-angle slide holds the camera at the θ you set, so commissioning and the production floor agree on one number.
On the machine side, rigidity is what keeps θ from wandering between shifts. The camera stands pages and the accessories finder list slide tables, tilt stages and brackets; for a fixed cell mount, the LS-MNT-0040 professional slide-table stand carries the camera on a 700 mm column over a 400 x 450 mm base.
- Type
- Camera stands
The bench proof is a full turn: put the mount on a rotary stage, rotate exactly 360°, and require the estimator to land within a degree or two of 360°. A rotary stage makes the check repeatable:
- Type
- Rotary stages
The LS-MNT-0220 manual XY + rotation stage turns the mounted camera through that known angle on its 60 mm base.
Sizing camera, mount and tilt together goes fastest in the stand configurator step. The decision, stated once: if the optics let you mount level, mount level — the yaw channel falls straight out and the matrix disappears. If the cell needs the tilt, apply R to every sample, integrate on sensor time, subtract the startup bias, and plan the reset, because open-loop MEMS yaw gives up 1–5° per minute no matter how carefully you integrate. Placement, the other half of the problem, is covered in Conveyor tracking camera placement relative to the robot, and the 3D topic page collects the frame-chain and calibration guides around it.
Questions
My SDK already publishes a fused orientation quaternion — do I still need the tilt matrix?
No. The fusion already mixes the gyro, accelerometer and any magnetometer into one orientation, so the tilt handling is done. Read the quaternion, convert it to a rotation matrix, and chain it once with the camera-to-robot rotation: R_robot = R_mount · R_quat. Heading comes from the matrix rows around the vertical axis, the classic atan2 form. Keep the timestamp discipline you would use for raw gyro data, because the fusion rate is still finite.
Can a magnetometer replace the yaw reset?
Outdoors, yes: magnetic heading is absolute and bounds drift indefinitely. Indoors it usually cannot: steel racking, machine frames and power cables distort the local field by tens of degrees, and the distortion changes from spot to spot on the floor. Map the distortion along the robot's actual path first, and if you use magnetic yaw at all, use it as a slow trim on the gyro estimate.
What gyro sample rate should I integrate at?
Integrate at the IMU's native rate, typically 100–400 Hz on depth-camera IMUs, and take Δt from the sensor's own timestamps rather than the host loop clock. Below about 100 Hz you start missing the peaks of real robot acceleration and integration error grows; above the native rate you are only resampling. Trapezoidal integration is plenty at these rates.
Can I avoid all of this by mounting the camera level?
Often yes, and it is the better answer when optics allow. With the optical axis horizontal, the camera's forward gyro axis is the vertical, the yaw-rate channel reads directly, and the tilt matrix disappears from the code. Choose a tilted mount only when you must — clearance, glare, or reaching into a fixture — and then measure the angle from gravity so the compensation is exact.
How do I test the whole chain before it runs on the robot?
Put the mounted camera on a rotary stage or turntable, rotate exactly 360°, and require the estimator to land within a degree or two of 360°. Then stop, leave everything still for one minute, and read how far yaw crept. That single session proves the scale factor, the sign and the tilt, and hands you the drift number to plan resets around.


