> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nomadicml.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Frames, poses and units

> Coordinate frames, sensor transforms, the body pose, units and rotation conventions.

## Units

| Quantity | Unit |
| - | - |
| Distance, position | **metres** |
| Angle | **radians** |
| Orientation in messages | **unit quaternion**, Foxglove order `{x, y, z, w}` (Hamilton, right-handed) |
| Timestamps | **nanoseconds**, integer |
| Everything else | declared per signal in its `unit` field (see [Signals](/mcap-spec/signals#units)) |

All frames are **right-handed**. Convert your source's units and handedness once, in your
converter. Nothing is converted at read time.

## The `vehicle` frame

Point clouds and sensor extrinsics are expressed relative to one frame named **`vehicle`**:

* **x forward, y left, z up**, right-handed;
* **origin** = the body's reference point **projected onto the ground plane**, so z = 0 is the
  ground and a roof-mounted sensor has a **positive** z.

For a sensor-only recording (no body, e.g. a roadside site), `vehicle` is the site's frame with the
same axes and its origin on the ground.

<Warning>
  An inverted or ground-offset z is the most common conversion error. If your roof LiDAR comes out
  at z = −1.8 m, your z axis is inverted; if it comes out at 0, your origin is at the sensor rather
  than on the ground. Ingest checks the LiDAR height distribution and warns when it is inconsistent
  with the sensor's mounting height. ROS users: REP 105 `base_footprint` matches this frame;
  `base_link` usually needs its height above the ground added.
</Warning>

## Transforms

Publish each sensor extrinsic as a **`foxglove.FrameTransform`** message:

| Field | Value |
| - | - |
| `parent_frame_id` | `"vehicle"` |
| `child_frame_id` | the sensor's frame, matching the `frame_id` in that sensor's messages |
| `translation` | metres |
| `rotation` | unit quaternion `{x, y, z, w}` |

* Transforms are **static**: publish each one **once, at `t0_ns`**.
* Every camera and point cloud carries a `frame_id` in its messages; each SHOULD have a
  `FrameTransform` whose `child_frame_id` matches.
* There is deliberately no matrix or Euler-angle alternative. Convert your extrinsic to translation
  plus quaternion in your converter with whatever your toolkit provides
  (e.g. `scipy.spatial.transform.Rotation.from_matrix(R).as_quat()`, which returns `x, y, z, w`).

A LiDAR published in its own sensor frame MUST have a transform; see
[Point clouds](/mcap-spec/point-clouds#frames).

## Frames for poses and keypoints

Poses (`foxglove.PoseInFrame`) and keypoints (`nomadic.Skeleton`) carry a `frame_id`. Which frame
to use depends on the body:

| Body | Frame for part poses and keypoints |
| - | - |
| `manipulator` (fixed base) | the **robot base frame** |
| `mobile_robot`, `humanoid`, `human` | **one static world frame** (e.g. `world`, `map`, `odom`), the same frame the [body pose](#body-pose) is given in |

* All part poses and keypoints in a recording MUST use the **same `frame_id`** for the whole
  recording. A part in another frame is left out, and a message whose `frame_id` differs from the
  first one on its channel is skipped.
* World frames MUST be right-handed with **z up**. Convert y-up sources in your converter. For a
  right-handed y-up world (ARKit, OpenXR), rotate +90° about x: a position `(x, y, z)` becomes
  `(x, −z, y)`, and the same rotation is applied to every orientation.

## Body pose

The body's own pose over time (the "ego pose": a car's pose, a mobile robot's base, a humanoid's
pelvis, a person's head) is a **`foxglove.PoseInFrame`** channel that is **not** declared in the
overlay. It is found by its schema.

* Every `PoseInFrame` channel that no part's `end_pose` references is read as the body pose, so a
  recording SHOULD have **at most one** such channel.
* A part's `end_pose` (a gripper, a wrist, a foot) is that part's pose and is never used as the
  body pose. Do not reuse one channel for both.
* Position in metres, orientation a unit quaternion, in the static world frame above.
* A fixed-base `manipulator` has no body pose.

## Orientation in signals

Orientation in messages is always a quaternion. When you also publish orientation as
[signals](/mcap-spec/signals#orientation) (e.g. from an IMU), publish derived angles (`yaw`,
`pitch`, `roll` in `rad`), not raw quaternion components.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.