Your depth camera sits powered and perfectly still, and the linear-acceleration output holds a flat offset of 2 m/s² on the x axis. You re-ran the SDK's IMU extrinsics update, rebuilt the driver, and the offset did not move. That outcome is expected: extrinsic calibration rewrites the rotation and translation between the IMU and camera frames, so it rotates an offset but cannot shrink it. This guide gives you the five-minute stationary test that separates a healthy sensor from a biased one, the pass numbers to hold the log to (gravity magnitude 9.81 m/s², gyro under 0.1 °/s), the double-integration arithmetic that prices that bias in metres, and the compensation that belongs in your own fusion. It is written for anyone wiring a depth camera's IMU into odometry, robot guidance or condition monitoring.
Why does linear acceleration read 2 m/s² while the camera sits still?
Because the value you are reading is not motion: with the camera static, every axis should sit near zero, so a flat offset on one axis is an accelerometer bias or a residue left by the on-chip fusion that strips gravity from the raw signal. Real acceleration changes from sample to sample; a value pinned at 2 m/s² for minutes is an offset, and an offset has exactly two sources here.
Either the accelerometer itself carries a residual bias that factory calibration missed or temperature re-introduced, or the linear-acceleration stream leaves part of gravity behind. The fusion that removes gravity estimates its direction from the gyro and accelerometer, and a direction error of θ leaves a gravity component of g·sin θ on the horizontal axes. Solve g·sin θ = 2 m/s² with g = 9.81 m/s²: θ = arcsin(2/9.81) = arcsin(0.204) ≈ 11.8°. A mere 12° of attitude error in the fusion reproduces your exact symptom, and both causes live downstream of extrinsic calibration — which is why re-running it changed nothing.
What does IMU extrinsic calibration actually change?
Extrinsic calibration rewrites only the rotation and translation between the IMU frame and the camera frame, so it can rotate an offset onto different axes but can never change its magnitude — which is why the x-axis offset survives an extrinsics update unchanged. An IMU-to-camera extrinsic is one rotation matrix and one translation: it re-expresses each IMU sample in camera coordinates. Rotation preserves vector length, so the offset keeps its size in every frame; translation shifts where the measurement is taken, not what it reads. That is the whole effect of the tool, and it is why your offset survived bit for bit. The same logic separates the three operations people reach for when this symptom appears:
| Operation | What it changes | Effect on the offset |
|---|---|---|
| Re-run IMU-camera extrinsics | Axis directions and lever arm between IMU and camera | None — magnitude is rotation-invariant |
| Switch the IMU stream mode | Which fusion product the sensor ships: raw, calibrated or gravity-removed | Removes it only if the artifact is a fusion product |
| Subtract a measured bias | The signal itself, per axis, in your code | Removes it in every stream |
Extrinsic calibration earns its keep elsewhere: it is the fix when IMU data points the wrong way, as covered in camera-IMU extrinsics: factory defaults versus your own calibration and in the walk-through of the IMU optical frame transform. Both sit with the rest of the calibration guides on the 3D vision topic page.
How do you measure the bias with a stationary log?
Bolt the camera to a rigid stand, let it soak for 15 minutes, record 30 seconds of the calibrated accelerometer and gyro, and pass the sensor if the acceleration magnitude averages 9.81 m/s² within ±0.05 m/s² while every gyro axis stays under 0.1 °/s. Then run these steps in order:
- Bolt the camera to a rigid stand with its cable strain-relieved. A slide-table stand such as the LS-MNT-0040 (height 700 mm, 400 × 450 mm base) holds a camera dead-still through the log.
- Type
- Camera stands
- Level the camera so gravity loads one axis. A small rotation stage such as the LS-MNT-0220 (60 mm stage, 60 mm diameter base) turns the level adjustment into a one-minute job and makes the per-axis read-out trivial.
- Type
- Rotary stages
- Power up and soak the camera for 15 minutes so the sensor reaches a steady temperature.
- Record 30 s of the calibrated accelerometer and the calibrated gyro — 12,000 samples at 400 Hz.
- Average the magnitude |a| = sqrt(ax² + ay² + az²) across the window: pass at 9.81 ± 0.05 m/s². That band is about ±5 mg, comfortably inside what a factory-calibrated consumer MEMS part holds at fixed temperature.
- Check that each gyro axis reads under 0.1 °/s.
- Record the linear-acceleration stream for another 30 s under the same conditions.
- Write down the per-axis means of the calibrated stream. That vector is your bias.
A healthy log reads ax = +0.04, ay = −0.02, az = +9.79 m/s². Its magnitude is sqrt(0.04² + 0.02² + 9.79²) = sqrt(0.0016 + 0.0004 + 95.8441) = sqrt(95.8461) = 9.79 m/s², which sits 0.02 m/s² from gravity: pass. The same camera's linear-acceleration stream reading ax = +2.01, ay = −0.03, az = +0.02 while bolted still convicts the stream — your bench symptom, reproduced on demand — while the sensor itself leaves the log clean.
Which IMU stream should you read, and why does the field still say linear_acceleration?
Read the calibrated accelerometer stream and remove gravity yourself in code; the fused linear-acceleration stream is the one carrying the artifact, and the message field labelled linear_acceleration only names the data slot, not the stream behind it. Stack the three choices side by side and the decision is mechanical:
| Stream | What the sensor ships | Stationary and level, healthy sensor |
|---|---|---|
| RAW / UNCALIBRATED | Output as measured, without factory bias and gain correction | Magnitude near g only after your own calibration |
| CALIBRATED | Factory scale and bias applied | Magnitude 9.81 ± 0.05 m/s², each gyro axis under 0.1 °/s |
| LINEAR_ACCELERATION | On-chip fusion subtracts an estimated gravity vector | Every axis ≈ 0 m/s² |
In ROS drivers the choice is a launch parameter, not a source edit: one common driver names it i_acc_mode under its imu: section, with values such as ACCELEROMETER and LINEAR_ACCELERATION, and some versions default to the uncalibrated stream. Two traps account for most wasted hours. First, the message field prints as linear_acceleration in every configuration, so the label proves nothing about which stream feeds it. Second, an edited example YAML is easy to shadow with another config file that the launch actually loads. After changing the parameter, rebuild, confirm the loaded file path in the launch output, and re-run the stationary log above to see which stream you truly receive.
How much position error does that bias cost?
A constant bias b grows into velocity error at b·t and into position error at ½·b·t², so with b = 2 m/s² you drift 1 m after one second, 4 m after two and 100 m after ten seconds of open-loop integration. The arithmetic:
| Time t | Velocity error b·t | Position error ½·b·t² |
|---|---|---|
| 1 s | 2 × 1 = 2 m/s | ½ × 2 × 1² = 1 m |
| 2 s | 2 × 2 = 4 m/s | ½ × 2 × 2² = 4 m |
| 5 s | 2 × 5 = 10 m/s | ½ × 2 × 5² = 25 m |
| 10 s | 2 × 10 = 20 m/s | ½ × 2 × 10² = 100 m |
Open-loop dead reckoning is therefore unusable beyond about a second — that is the deciding number for whether raw integration may bridge any gap, and it is why an IMU always rides inside a filter that observes and bounds the drift. A visual-inertial filter or the robot's own odometry can absorb a constant bias, but it converges faster and stays tighter when it receives a pre-compensated signal; the compensation methods for IMU attitude errors during acceleration show what the filter is fighting, and the dead-reckoning drift resolution on a consumer IMU works the same t² arithmetic through to metres. Subtract the measured bias even when a filter sits downstream.
How do you remove the bias in your own fusion?
Average the calibrated accelerometer over a 30-second stationary window, take the per-axis mean in the level x and y directions as the bias, confirm the z mean is within ±0.05 m/s² of 9.81 m/s², then subtract the bias vector from every live sample before integration. With the camera level, the horizontal means are the biases directly: from the healthy log above, b = (+0.04, −0.02, 0) m/s², and every live sample becomes a_corr = a − b.
If your convention reads −9.81 m/s² on the vertical axis, the vertical bias is mean(az) + 9.81 m/s²; once the camera tilts, rotate the gravity vector with your attitude estimate before subtracting, and the same one-line correction holds. Averaging does the precision work: 12,000 samples over 30 s cut random noise by sqrt(12,000) ≈ 110× against a single reading, so the mean you subtract is far steadier than any one sample. Re-zero after warm-up and after any firmware change — the checklist below keeps both on the calendar.
When should you actually re-run extrinsic calibration?
Re-run it when the direction of IMU data is wrong — gravity showing up along a tilted camera axis, unit vectors inconsistent after a restart, or the camera remounted, re-housed or serviced — never to fix a magnitude offset that a stationary log has already measured. The tell is direction, not magnitude. Bolt the camera level and read one set of calibrated samples: if gravity arrives almost entirely on the vertical axis and the horizontal axes each carry under 0.3 m/s², the frames agree, since 0.3 m/s² corresponds to arcsin(0.3/9.81) ≈ 1.8° of misalignment. If a horizontal axis carries several m/s², or gravity swings to a different axis after a restart, the IMU-to-camera transform is the fault — re-run the extrinsics update and confirm with the unit-vector check for IMU values that change across restarts. Direction fault: extrinsics. Magnitude fault: bias compensation. Your bench offset is the second kind.
What keeps the bias from coming back?
Bias moves with temperature and with firmware updates that change the fusion defaults, so re-run the 30-second stationary zero after the camera reaches operating temperature, after any driver or firmware update, and after any mechanical shock to the mount. Work through this list before you trust the corrected stream in production:
- Warm up, then zero: capture the 30-second stationary zero only after 15 minutes at operating temperature — the bias you record cold will not match the bias you ship hot, and the same thermal story that drags lens focus through the day moves MEMS bias too.
- Re-zero after updates: stream modes and fusion defaults change between driver releases, so confirm with one stationary log which stream you now receive.
- Log both streams in production: keep the calibrated accelerometer beside the fused output, so a future artifact is one query away from a conviction.
- Re-check after any shock: a knocked mount shifts the gravity direction first and your bias subtraction second; the recovery sequence in vision calibration recovery after camera or robot changes covers the re-verification order.
If the stationary log fails on the calibrated stream itself — magnitude off by more than a few tenths of a m/s² at any temperature — the sensor needs attention before it anchors any estimator. Choose a replacement in the 3D sensors range through the 3D sensor finder, and mount it on hardware from the camera stands category via the accessories finder. For a full station, the configurator below starts at the stand step, and the contact page reaches the applications team.
The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.
Questions
Which axis should carry gravity when the camera is level?
The vertical axis of the IMU frame carries the full 9.81 m/s² of specific force, with a sign that follows the sensor's convention: many parts report +9.81 m/s² on z when z points up, others report −9.81 m/s² with z pointing down. Judge the sensor on the magnitude sqrt(ax² + ay² + az²), which must read 9.81 m/s² either way, never on the sign of one axis.
Does temperature move the bias, and by how much?
Yes. Consumer MEMS accelerometers typically shift a few hundredths of a m/s² across a 20-30 °C warm-up — small next to a 2 m/s² fault, but large next to the ±0.05 m/s² pass band. Run the stationary zero after 15 minutes of warm-up, and again if the camera moves from a cold store onto a warm production line.
Does the same drift hit the gyroscope?
The gyro has its own bias, but it costs angle, not position: a 0.1 °/s gyro bias turns the attitude estimate by 6° per minute, while a 2 m/s² accelerometer bias quadruples distance error with time. Check both in the same stationary log — each gyro axis must stay under 0.1 °/s while the accelerometer magnitude holds 9.81 m/s².
How long should the stationary averaging window be?
Thirty seconds is enough: at 400 Hz that is 12,000 samples, and averaging cuts random noise by the square root of the sample count, roughly a hundred-fold against a single reading. Shorter windows work for a quick check, but keep at least ten seconds so vibration and single-sample spikes average out of the bias estimate.

