Skip to main content

apairo

PyPI Python versions CI License: MIT Docs Website

Unified Python loader for robotics sensor datasets — one API across synchronous and asynchronous layouts, with built-in preprocessing, filtering, and dataset composition.

All data is returned as numpy.ndarray. Convert to the framework of your choice.


Installation

pip install apairo

Optional extras:

pip install apairo[vision]   # Image loading (Pillow)

Requires Python ≥ 3.11.


Quickstart

import apairo

ds = apairo.SemanticKittiDataset("/data/semantic_kitti", keys=["lidar", "labels"])
sample = ds[0]
# sample.data["lidar"]   -> np.ndarray (N, 4)  float32  [x, y, z, intensity]
# sample.data["labels"]  -> np.ndarray (N,)    int64

Supported datasets

Class Layout Modalities
SemanticKittiDataset synchronous lidar, labels
Rellis3DDataset synchronous lidar, labels, poses
Goose3DDataset synchronous lidar, labels
RawDataset asynchronous any channels — declared in .apairo/channels.yaml
TartanKittiDataset asynchronous any TartanDrive v2 channel

RawDataset is the profile-free loader for the asynchronous layout: it takes its channels — and their format (npy, npys, bin, img, zarr) — entirely from .apairo/channels.yaml, so it loads any such dataset, including the output of apairo_extractor, with no code change.


Bring your own dataset — keys in the filenames

Point apairo at a directory of files and it loads. When each frame's alignment clock is encoded in its filename — a timestamp, an index — declare it in channels.yaml; apairo parses the key in memory at read time and never writes into your data:

# .apairo/channels.yaml
channels:
  camera: {loader: img, key: {name: 'frame\d+-(\d+)_(\d+)'}}   # timestamp from frame000123-1581624652_750.jpg
  labels: {loader: img, key: {name: 'frame\d+-(\d+)_(\d+)'}}   # same clock; a sparse subset of frames
ds   = apairo.RawDataset(seq_dir, keys=["camera", "labels"])
view = ds.synchronize(reference="camera", method="nearest", tolerance=0.0)  # labels attach where they exist
  • key: {name: <regex>} parses the key from the filename stem (scale: [...] for an explicit unit combine); key: {file: <name>} reads it from a sidecar.
  • A separate order: controls enumeration; a subclass can hand in _key_providers / _order_providers callables for anything the DSL can't express.
  • No key → today's behavior (timestamps.txt, or frame position). Fully additive, opt-in, read-only.

This turns the Rellis-3D camera (2847 frames @ 10 Hz, plus 1200 half-rate image-labels) into two lines of channels.yaml — no subclass, no filename→timestamps.txt transcode. See the Bring your own dataset guide.


Command line

Installing apairo provides the apairo command to inspect and initialize datasets from the terminal:

# Write/repair the .apairo sidecars by scanning a directory (root-aware, idempotent)
apairo init /data/my_dataset

# Show sequences, channels (tracked + untracked), event count and any issues
apairo status /data/my_dataset           # add --json for machine output

apairo init reconstructs the .apairo files for data laid out before they existed (e.g. an older extraction) — no re-extraction needed — and the result loads directly with RawDataset. See Command Line for the full reference.


Pipeline

apairo provides a composable set of operations that chain together — each returns a full dataset:

from apairo import Rellis3DDataset, FramePreprocessor
from torch.utils.data import DataLoader
import numpy as np

# 1. Preprocess — run once, persisted in .apairo, reloaded transparently
class TravLabel(FramePreprocessor):
    output_key = "trav_gt";  output_loader = "npys"
    input_keys = ["labels"]; timestamps_from = "lidar"; sources = ["labels"]
    def __call__(self, sample): return (sample.data["labels"] < 10).astype(np.uint8)

ds = Rellis3DDataset(root, keys=["lidar", "labels"])
ds.run_preprocess(TravLabel())

# 2. Cache an expensive derived channel — computed once, served from RAM
ds.transform("lidar", expensive_ground_prior, output="ground_prior")
ds_prior = ds.select(["ground_prior"]).cache()

# 3. Build train split — filter, join cached prior, apply augmentation
valid = np.load("cache/valid_indices.npy")
ds_train = (
    Rellis3DDataset(root, keys=["lidar", "trav_gt"])
    .filter(valid)
    .join(ds_prior)
    .transform("lidar", RangeFilter(max=50.0))
)

# 4. Drop into DataLoader — no adapter needed
loader = DataLoader(ds_train, batch_size=8, shuffle=True, collate_fn=my_collate)

See examples/ for complete runnable pipelines.


Preprocessing

Define a FramePreprocessor or SequencePreprocessor, run it once — apairo persists the output and reloads it transparently on subsequent runs.

from apairo.preprocess import FramePreprocessor

class TravLabel(FramePreprocessor):
    output_key      = "trav_label"
    output_loader   = "npys"
    input_keys      = ["labels"]
    timestamps_from = "labels"
    sources         = ["labels"]

    def __call__(self, sample) -> np.ndarray:
        return (sample.data["labels"] < 10).astype(np.uint8)

ds = apairo.Goose3DDataset("/data/goose", keys=["lidar", "labels"])
ds.run_preprocess(TravLabel())

See apairo_preprocess for a collection of ready-made preprocessors.


Transforms

Apply callables at access time — no disk writes.

# Per-channel
ds.transform("lidar", RangeFilter(max=50.0))

# Sample-level — consistent mask across aligned channels
def sync_filter(sample):
    mask = np.linalg.norm(sample.data["lidar"][:, :3], axis=1) < 50.0
    sample.data["lidar"]  = sample.data["lidar"][mask]
    sample.data["labels"] = sample.data["labels"][mask]
    return sample

ds.transform(sync_filter)

See apairo_transform for a collection of ready-made transforms.


Filtering

filter() returns a dataset view restricted to frames that pass a predicate. Sweep once, persist the indices, reload without I/O cost on subsequent runs:

# Compute and save
view = ds.filter("trav_gt", lambda gt: (gt == 1).sum() >= 50)
np.save("cache/valid.npy", view.indices)

# Reload — no sweep
view = ds.filter(np.load("cache/valid.npy"))

Select & cache

select(keys) narrows a dataset to a subset of channels. cache() materialises it in RAM. Together they let you cache only the channels worth caching:

ds = Rellis3DDataset(root, keys=["lidar"])
ds.transform("lidar", expensive_ground_prior, output="ground_prior")

# Compute once, store in RAM
ds_prior = ds.select(["ground_prior"]).cache()

# Reuse across training runs — prior served from RAM, base channels from disk
base = Rellis3DDataset(root, keys=["lidar", "labels"])
ds_v1 = base.join(ds_prior).transform(augment_v1)
ds_v2 = base.join(ds_prior).transform(augment_v2)

Asynchronous datasets — synchronize()

Asynchronous datasets (multi-rate sensor rigs) expose a timestamp-ordered event timeline: ds[i] is one event from one sensor. To get complete multi-channel frames, resample onto a reference clock:

ds = apairo.TartanKittiDataset(seq_dir, keys=["velodyne_0", "image_left", "cmd"])

ds_sync = ds.synchronize(
    reference="velodyne_0",   # default: lowest-frequency channel
    method="previous",        # "previous" (zero-order hold), "next" or "nearest"
    tolerance=0.05,           # drop frames with no match within ±50 ms
)

ds_sync[0].data   # {"velodyne_0": ..., "image_left": ..., "cmd": ...}

The result is a synchronous view — random access, shuffling, and the whole chaining API (filter, select, cache, join, DataLoader) work unchanged. Matching is a pure index computation; no data is read until access.


Combining datasets

# ConcatDataset — frame axis (different recording sessions)
combined = apairo.ConcatDataset([ds_session1, ds_session2])

# ZipDataset — channel axis (same frames, different modalities)
combined = apairo.ZipDataset(ds_base, ds_prior)
# or: ds_base.join(ds_prior)

# Built-in splits
ds_train = apairo.Rellis3DDataset(root, keys=["lidar", "labels"]).split("train")
ds_val   = apairo.Rellis3DDataset(root, keys=["lidar", "labels"]).split("val")

Extending apairo

Add a new synchronous dataset with a YAML profile and a minimal subclass. See documentation for the full guide.


Contributing

apairo is one repository of a small ecosystem (apairo_transform, apairo_preprocess, apairo_extractor, apairo_rr). Where a change belongs, the design invariants, and the dev workflow are documented in CONTRIBUTING.md.


License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

apairo-0.6.1.tar.gz (115.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

apairo-0.6.1-py3-none-any.whl (134.4 kB view details)

Uploaded Python 3

File details

Details for the file apairo-0.6.1.tar.gz.

File metadata

  • Download URL: apairo-0.6.1.tar.gz
  • Upload date:
  • Size: 115.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for apairo-0.6.1.tar.gz
Algorithm Hash digest
SHA256 badcad41c808f7c998f9e0d39f8e4f473dc4ccccfbdea291224475c60622d7ff
MD5 a774fbc1ce87515a44b904483a88858b
BLAKE2b-256 56f0b8e3e7d7f3c25d0d69632f26f7da5186616168bb3c7751bb07d9f2d533df

See more details on using hashes here.

Provenance

The following attestation bundles were made for apairo-0.6.1.tar.gz:

Publisher: publish.yml on apairo-robotics/apairo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file apairo-0.6.1-py3-none-any.whl.

File metadata

  • Download URL: apairo-0.6.1-py3-none-any.whl
  • Upload date:
  • Size: 134.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for apairo-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b78cb68d4108d5dd25089bcd34d0116cfd85a05700e3fddc91111ce50d511818
MD5 cc9a1ef60a55a9dfb6b9422296158b5d
BLAKE2b-256 901c8e5a17d194b192b4f5cf21dec71c1d874d997df4592f09550270d97c5a52

See more details on using hashes here.

Provenance

The following attestation bundles were made for apairo-0.6.1-py3-none-any.whl:

Publisher: publish.yml on apairo-robotics/apairo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.7.0

2 files

0.6.2

2 files

This release

0.6.1 This release

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page