Skip to main content

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.
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.

Transforms

Publish each sensor extrinsic as a foxglove.FrameTransform message:
  • 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.

Frames for poses and keypoints

Poses (foxglove.PoseInFrame) and keypoints (nomadic.Skeleton) carry a frame_id. Which frame to use depends on the body:
  • 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 (e.g. from an IMU), publish derived angles (yaw, pitch, roll in rad), not raw quaternion components.