You bolted a depth camera to a bracket and you need to know where it actually points before any downstream measurement is trusted. If the camera carries an inertial module, the answer arrives in under a second and needs no images at all: the accelerometer tracks gravity, and the direction of that vector in the camera's own frame is the camera's tilt. This guide walks through reading the motion stream, normalizing one vector and converting it into an inclination angle with pitch and roll. The arithmetic is shown on paper and in short code you can drop into your capture program. It applies to any camera whose SDK exposes an accelerometer stream: stereo structured-light depth cameras, time-of-flight cameras, and area scan cameras fitted with a companion IMU.
What the accelerometer gives you
Yes, the inclination is available immediately, and it sits one trigonometry call away from a raw frame. At rest, the accelerometer inside the camera measures the reaction to gravity: one body axis reads the full 9.81 m/s² and the other two sit near zero. Tilt the camera and the reading shifts between the axes. The orientation of the gravity vector, expressed in the camera's own coordinate frame, is the camera's attitude with exactly one part missing — its rotation about the vertical, which gravity cannot see.
The convention in your question — a camera aimed parallel to the ground reads 90 degrees — is the angle between the optical axis and the vertical. That is what the formula below returns directly. No depth frame, calibration plate or feature detection is involved; you can log the tilt with the lens cap still on.
Two output conventions exist and both are common. Some SDKs report specific force, which at rest points up; others report the gravity direction, which points down. A level camera reads 90 degrees either way, because the forward component of the vector is zero under both signs. The sign only sets the direction of pitch, and a single test pose fixes it in Step 5.
One vector, three angles
Work in the camera's optical frame: x to the right in the image, y down, z forward along the lens axis. Average the accelerometer over a static window, then normalize it to the unit vector d = (dx, dy, dz):
Inclination answers the question exactly as posed: 90 degrees when the optical axis is parallel to the ground, 0 degrees aimed straight down, 180 degrees straight up. If your drawing calls for a depression angle below horizontal instead, subtract: depression = 90° − inclination. A camera looking 30 degrees down at a conveyor reads 60 degrees of inclination and −30 degrees of pitch.
Pitch uses atan2 with the horizontal magnitude rather than the arcsine of dz, so it keeps full precision at small tilts. Roll is the tilt of the horizon line in the image, the angle you would otherwise take with a spirit level against the mount face.
Reading the motion stream, step by step
You need a camera whose SDK exposes a motion or IMU device, your capture program, and about a minute of stationary time.
-
Enable the motion stream. Depth and infrared frames are not required for tilt. Enable the accelerometer device the way your SDK lists it, commonly alongside the image streams at its native rate of 200 Hz or faster on MEMS modules of this class. Gyroscope frames can stay off unless you fuse them later.
-
Check units and magnitude. Log 100 raw frames with the camera sitting still and compute the magnitude of each. Expect about 9.81 m/s², or about 1.00 if the output is in g. If the reading is near 1.00, multiply by 9.81 for the gate below, or simply note the unit and carry on — normalization removes it.
-
Map the IMU axes onto the camera frame. The inertial module has its own axis convention, and most SDKs publish the rotation from IMU frame to camera frame. Where yours does not, probe it with two poses: camera level and upright, then camera pointed straight down at the floor. In the level pose the full reading lands on the axis that maps to camera y, which points down; in the straight-down pose it lands on camera z, forward. The IMU optical frame transform guide works through this mapping and its failure modes.
-
Average, normalize and convert. One hundred frames averaged is half a second of data at 200 Hz:
import math
# Illustrative names; your SDK's motion stream supplies the same three floats.
ax, ay, az = 0.0, 0.0, 0.0 # m/s^2, camera frame: x right, y down, z forward
n = 0 # frames accumulated, target 100
for frame in motion_frames(): # stand-in for your SDK's motion frame iterator
ax += frame[0]; ay += frame[1]; az += frame[2]; n += 1
ax, ay, az = ax / n, ay / n, az / n
mag = math.sqrt(ax*ax + ay*ay + az*az)
dx, dy, dz = ax/mag, ay/mag, az/mag
inclination = math.degrees(math.acos(dz))
pitch = math.degrees(math.atan2(-dz, math.hypot(dx, dy)))
roll = math.degrees(math.atan2(dx, dy))
# Test, level camera feeding (0.0, 9.81, 0.0):
# mag = 9.81, dz = 0.0 -> inclination 90.0, pitch 0.0, roll 0.0
- Validate at two known poses. Level and upright must read inclination 90.0°, pitch 0.0°, roll 0.0°. Pointed straight down at the floor must read 0.0°. If level reads 0° instead of 90°, the mapping put forward where down belongs — swap those two axes in Step 3 and repeat.
Motion frames carry their own timestamps on their own clock, separate from the image stream. If you later pair each tilt value with a captured frame, camera IMU timestamp clock domains explains how to align the two clocks.
Worked example: a 30-degree conveyor bracket
A camera is bolted to a bracket whose drawing holds the optical axis 30 degrees below horizontal, and you want the as-built number. The averaged window over 100 static frames reads a = (0.05, 8.50, 4.91) m/s² in the camera frame.
First the gate: |a| = sqrt(0.05² + 8.50² + 4.91²) = sqrt(0.0025 + 72.2500 + 24.1081) = sqrt(96.3606) = 9.82 m/s². That sits 0.01 m/s² from 9.81, far inside the 0.20 m/s² acceptance band, so the window is genuinely static.
Normalize: dx = 0.05 / 9.82 = 0.005, dy = 8.50 / 9.82 = 0.866, dz = 4.91 / 9.82 = 0.500.
Angles: inclination = acos(0.500) = 60.0°, so the depression below horizontal is 90.0 − 60.0 = 30.0 degrees, and the bracket matches its drawing. Pitch = atan2(−0.500, sqrt(0.005² + 0.866²)) = atan2(−0.500, 0.866) = −30.0°, the same 30 degrees under the formula's sign convention. Roll = atan2(0.005, 0.866) = 0.3°, a slight twist of the bracket about the lens axis — harmless here, worth a shim if your mount tolerance is tighter than 0.5°.
The cameras you read this on
Stereo structured-light and time-of-flight depth cameras are the families most likely to carry a built-in accelerometer, because knowing gravity helps align and register their point clouds. Laser profilers on a fixed gantry usually take their angle from the fixture instead. Three examples from our 3D sensors range span those classes:
- Type
- Stereo / structured light
- Type
- Stereo / structured light
- Type
- Depth / ToF camera
The MV-DLS300P-04 is a laser galvanometer stereo camera, the MV-DPS200P-02 a projected structured-light stereo camera, and the MV-DT01WMU a time-of-flight sensing camera. Whatever the model, the entry to look for in its feature list is a motion or IMU stream — that entry is exactly what the steps above consume.
If your camera has no built-in module, the same math runs on a MEMS accelerometer rigidly fixed to the mount: map its axes once and the formulas are unchanged. Cameras built for fast motion work are often paired with exactly such modules, as global shutter camera IMU modules for fast motion tracking covers, and a 2D camera from the area scan cameras range on a bracket gets the same two angles from the same code.
Once the tilt checks out, the mount's height and footprint follow next — the 3D camera working distance and positioning guide takes the angle you just measured into coverage planning.
Checks before you trust the number
- Static gate. Accept a window only when the averaged magnitude sits within 0.20 m/s² of 9.81, which is 1.00 ± 0.02 in g. Outside that band the camera is accelerating or vibrating, and the vector no longer points along gravity.
- Averaging window. 100 frames at 200 Hz is half a second; the random part of the angle falls with the square root of the frame count, so doubling to 200 frames buys a further 30 percent noise cut.
- Two-pose validation. Level reads 90.0°, straight down reads 0.0°. Run this once at commissioning and again after any remount.
- Cross-check against mechanics. Compare pitch with the bracket drawing or a digital level on the mount face; a gap larger than 1° means an axis mapping or sign error, not sensor noise.
- Stream health. If the motion stream delivers nothing at all, camera IMU stream freezes walks the isolation steps before you suspect the math.
When the reading is wrong
- Pitch and roll trade places. A level camera that reads 0° instead of 90° has forward and down swapped in the axis mapping. This is the most common error, and the two-pose test catches it immediately.
- Sign flip. A bracket 30 degrees below horizontal that reports 120° inclination and a positive pitch is giving specific force rather than the gravity direction. Negate the vector; the level pose hides this, because 90° is its own mirror.
- Yaw is asked of the accelerometer. Rotation about the vertical leaves gravity unchanged, so heading is invisible to it. Gyroscope-only yaw drifts by degrees per minute uncorrected; absolute heading needs a magnetometer, which depth camera modules rarely carry, or vision.
- The mount vibrates. A bracket on a running conveyor frame can pass the static gate between shocks yet still jitter by a degree or two. Mount the sensor on the dampened side of the frame, or sample the average during a line stop.
- The sensor has no accelerometer. Gantry-mounted laser profilers publish no motion stream at all; there the tilt is a property of the fixture, not a measurement, and nothing in this guide applies.
Next steps
Pick the camera with the camera finder or run the system configurator from the camera step, and confirm the motion stream entry before finalizing the mount. More 3D set-up decisions sit under the 3D vision topic page, and if you want your tilt logging reviewed against the mount drawings, send them through our contact page.
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
Why does a camera parallel to the ground read 90 degrees?
Inclination is measured from the vertical, the direction of gravity, not from the ground plane. An optical axis parallel to the ground is perpendicular to gravity, and the arccosine of zero is 90 degrees. If your drawing works in depression below horizontal instead, subtract the reading from 90: the same level camera has 0 degrees of depression, and one aimed 30 degrees down reads 60 degrees of inclination and 30 degrees of depression.
Can the IMU give me the camera's yaw angle too?
Not from the accelerometer. Gravity looks identical from every heading, so rotation about the vertical is invisible to it. Integrating the gyroscope gives yaw that drifts by degrees per minute without correction. Absolute heading needs a magnetometer, which depth camera modules rarely carry, or a vision reference such as a known landmark in the scene.
My accelerometer reads about 1.00 at rest instead of 9.81. Is it faulty?
No, the output is in g, the unit where 1.00 g equals 9.81 m/s². Multiply by 9.81 if you want SI values for the magnitude gate, or leave it as it is: the normalize step divides the scale out, so dx, dy, dz and every angle come out identical either way. Only the acceptance band needs the unit, and 1.00 ± 0.02 in g matches 9.81 ± 0.20 m/s².
How many frames should I average before converting?
100 frames is a good default: half a second at a 200 Hz accelerometer, with the random angle noise falling as the square root of the count. Going to 400 frames cuts the remaining noise by half and still takes two seconds. Below about 25 frames the angle jitters visibly on a rigid mount; above a few thousand you are waiting for nothing, because slow drift and temperature, not averaging, set the floor.
The SDK publishes no IMU-to-camera rotation. How do I map the axes?
Use two poses. Hold the camera level and upright: whichever axis carries the full ±9.81 m/s² maps to camera y, which points down. Then point the camera straight at the floor: the full reading moves to the axis that maps to camera z, forward. The sign tells you whether the output is gravity-down or specific-force-up; if pitch comes out inverted against the real mount, negate the whole vector.


