Android: IMU data¶
The sample¶
Each motion sample carries:
| Field | Unit |
|---|---|
timestampNs |
Nanoseconds on the camera clock (the same clock as the frame timestamps) |
accel |
m/s², x/y/z, gravity included |
gyro |
rad/s, x/y/z |
mag |
µT, x/y/z — live on cameras with a magnetometer |
tempC |
Sensor temperature, °C |
The trailing per-sample value means different things by generation (a frame-sync delay on V1/V2, the
magnetometer reading's age on newer cameras); use SeiImuParser.deriveSofNs(sample, version) rather
than interpreting it yourself.
Rate¶
400 Hz on current cameras, about 562 Hz on V1/V2. Each video frame carries the samples captured since the previous frame — about 13 per frame at 400 Hz. Read the rate from the data rather than assuming it.
Live samples from the stream¶
session.frames
.onEach { frame ->
for (payload in SeiImuParser.parse(frame.annexB)) {
// payload.header: version, sample count, sensor ranges
for (sample in payload.samples) {
// sample.timestampNs, sample.accel, sample.gyro, sample.mag, …
}
}
}
.launchIn(scope)
Orientation (Madgwick fusion)¶
val madgwick = Madgwick(beta = 0.1f)
var lastNs = 0L
fun onSample(s: ImuSample) {
if (lastNs == 0L) {
madgwick.seedFromAccel(s.accel[0], s.accel[1], s.accel[2])
} else {
val dt = (s.timestampNs - lastNs).coerceAtLeast(0L) / 1e9f
madgwick.updateIMU(s.gyro[0], s.gyro[1], s.gyro[2], s.accel[0], s.accel[1], s.accel[2], dt)
}
lastNs = s.timestampNs
val quatXyzw = madgwick.asXyzw()
}
UI widgets (Compose)¶
ImuOverlayPanel (live values), OrientationCube (quaternion-driven 3D cube) and
TimeSeriesPlot / TripleAxisPlot (scrolling plots).
For aligning samples to frames, see Timing and sync. More: IMU guide on GitHub.