The .apairo schema (version 1)
apairo records what it knows about a dataset in a small .apairo/ directory next
to the data. This page is the contract: the on-disk format is version: 1
and stable. Do not confuse it with a dataset profile
(rellis.yaml …), which describes a dataset class shipped inside apairo; the
.apairo/ sidecars describe one dataset on disk.
<root>/.apairo/
channels.yaml # the channel registry (per sequence directory)
dataset.yaml # root manifest -- optional
calibration.yaml # static extrinsics -- optional
Only channels.yaml is required for a directory to be a loadable apairo
sequence. dataset.yaml and calibration.yaml are optional — a dataset with
no extrinsics simply has no calibration.yaml.
Compatibility policy
Validation is tolerant. An unknown field is reported as a warning and
otherwise ignored, so a sidecar written by a newer apairo still loads on an older
one. Every file carries a version (currently 1); a different version is
flagged. Validate with verify_config, verify_manifest, verify_calibration
(or, from the shell, apairo status surfaces channel issues).
channels.yaml
version: 1
channels:
ouster_points: # key == the on-disk directory name
kind: raw # "raw" | "preprocess" (required)
loader: bin # npy | npys | bin | img | zarr (required)
alias: lidar # public name exposed at load time (optional)
frame: ego # coordinate frame, descriptive only (optional)
trav_gt:
kind: preprocess
loader: npys
timestamps_from: lidar # channel whose timestamps this one shares (optional)
sources: [labels] # provenance: channels it was derived from (optional)
gicp_odom:
kind: raw
loader: npy
transform: # this channel *is* a coordinate transform (optional)
parent: odom # (required inside transform)
child: base_link # (required inside transform)
static: false # (optional)
format: t_xyz_q_xyzw # (optional)
gicp_poses: # two stacked arrays colocated in one directory
kind: raw
loader: npy
array_file: poses.npy # the exact .npy this channel loads (optional)
gicp_valid_mask:
kind: raw
loader: npy
directory: gicp_poses # share gicp_poses/ rather than own a dir (optional)
array_file: valid_mask.npy
| Field | Required | Meaning |
|---|---|---|
kind |
yes | raw (on-disk modality) or preprocess (derived/persisted). |
loader |
yes | Storage format: npy (one stacked file, row per frame), npys (one file per frame), bin, img, zarr. |
timestamps_from |
no | The channel whose timestamps this one shares (provenance). |
sources |
no | Channels this one was derived from (provenance). |
frame |
no | Coordinate frame the data is expressed in. Descriptive only — apairo never applies transforms. |
transform |
no | Declares the channel is a transform stream: {parent, child, [static], [format]}. Descriptive only. |
alias |
no | Public name the channel loads under (the directory keeps its real name). Must be unique and must not shadow a real channel directory. |
directory |
no | On-disk subdirectory the channel's files live in, when different from its key — lets a channel share another channel's directory. Defaults to the key. |
suffix |
no | Per-frame colocation: load only <frame_stem>_<suffix>.npy from directory (e.g. velodyne_0/000000_intensity.npy beside 000000.npy). npys only. |
array_file |
no | Whole-array colocation: the exact stacked .npy this channel loads from directory, when it holds more than one (e.g. valid_mask.npy beside poses.npy). npy only. |
dataset.yaml (root manifest, optional)
Identity for a dataset root (the parent of several sequence directories).
version: 1
class: Rellis3DDataset # the profiled class that produced the layout (profiled roots)
name: my_dataset # optional display name
sequences: [00000, 00001] # generic roots: sequence order
channels: {lidar: {kind: raw}} # generic roots: channel roll-up
class is written by a profiled init --as <Class> so apairo status can
dispatch through that profile; name / sequences / channels describe a
generic (RawDataset) root.
calibration.yaml (static rig configuration, optional)
Time-independent rig configuration: extrinsic transforms and camera
intrinsics. apairo exposes them (via dataset.calibration) and
resolves any pair of connected frames with
dataset.calibration.get_tf(source, target); it never applies the result to
data — that is apairo_transform's job.
version: 1
transforms:
lidar_to_camera:
parent: lidar
child: camera
matrix: [[...4x4...]]
cameras:
multisense_left: # the camera's frame (CameraInfo frame_id)
K: [[...3x3...]]
D: [k1, k2, p1, p2, k3] # optional -- omit for a rectified image
distortion_model: plumb_bob
width: 1024
height: 544
R: [[...3x3...]] # optional (stereo-rectified rigs)
P: [[...3x4...]] # optional (stereo-rectified rigs)
Each transform needs parent, child, and a 4×4 matrix; write them with
register_static_transform(root, parent, child, matrix). Each camera needs a
3×3 K (field names mirror ROS CameraInfo); write them with
register_intrinsics(root, camera, K=..., ...) and read them back with
dataset.calibration.get_intrinsics(camera).