A Rust coordinate transform library inspired by ROS2 tf2, for robotics and
computer vision. It provides familiar frame trees, timestamped buffers,
transform chaining, and interpolation, with a Rust API and no middleware
dependency. It forbids unsafe code and supports no_std with alloc.
API reference · Migration guide · Changelog
Requires Rust 1.85 or later.
[dependencies]
transforms = "2.2.0"For no_std, disable default features and provide a heap allocator:
[dependencies]
transforms = { version = "2.2.0", default-features = false }| Feature | Default | Effect |
|---|---|---|
std |
Yes | Adds Timestamp::now(), Timestamp::try_now(), and SystemTime support |
serde |
No | Adds serialization and deserialization for geometry and time types |
use core::time::Duration;
use transforms::{
Registry, Transformable,
geometry::{Point, Quaternion, Transform, Vector3},
time::{Stamp, Timestamp},
};
let mut registry = Registry::with_max_age(Duration::from_secs(60));
let timestamp = Timestamp::from_nanos(1_000_000_000);
// The sensor origin is one unit along the base frame's X axis.
registry.add_transform(Transform::new(
"base", "sensor",
Vector3::new(1.0, 0.0, 0.0),
Quaternion::identity(),
Stamp::At(timestamp),
).unwrap()).unwrap();
let mut point = Point::new(
Vector3::new(2.0, 0.0, 0.0),
Quaternion::identity(),
timestamp,
"sensor",
);
// Resolve sensor coordinates into base, then apply the transform.
let transform = registry.get_transform_for(&point, "base").unwrap();
point.transform(&transform).unwrap();
assert_eq!(point.frame, "base");
assert_eq!(point.position, Vector3::new(3.0, 0.0, 0.0));Transform::new(parent, child, ...)maps child coordinates into parent.get_transform(target, source, time)returns that mapping from source to target. Positions are rotated first, then translated.- Coordinates and quaternion components are
f64. Use consistent distance units and frame axes; the crate does not assign them. Quaternions use(w, x, y, z)order and Hamilton multiplication;q1 * q2appliesq2first. See Quaternion. Stamp::At(t)is a dynamic sample;Stamp::Staticis valid for all time. UseTransform::static_betweenfor sensor mounts. Zero is an ordinary dynamic timestamp.- Each child has one parent and one kind (static or dynamic), set at its first insertion. Cycles are rejected. Re-publishing a sample at the same timestamp replaces it.
Registry::reparent_framemoves a child under a new parent atomically, dropping that child's stored history; descendants keep theirs. Useremove_frameand re-insertion to change a kind or keep history.Registry::with_max_ageevicts on insertion relative to that child's newest inserted timestamp, never wall-clock time. It limits history duration, not sample or frame count.Registry::newretains data until manual removal. Interpolation spans any retained gap; no extrapolation is performed.latest_common_timereturns the newest commonly covered instant, orStamp::Staticfor an all-static chain. Use that instant for a subsequent lookup. For shared registries, keep both calls under one read guard. Geometry errors can still occur during the lookup.get_transform_atrelates two instants but stores only the target stamp. Keep both instants separately and apply its geometry explicitly. See its application restrictions.
Constructors and deserialization reject non-finite transform components and
non-unit rotations. Derived transforms are not re-validated: composition can
accumulate rotation drift or overflow. Call Transform::validate before
applying or serializing a derived result when numeric validity is required;
registry insertion always validates again.
Geometry equality and approx comparisons compare components; opposite
quaternion signs can represent the same rotation without comparing equal.
approx 0.5 is part of the public API. Error variants and payloads are stable;
Display and Debug text are not.
See Registry for lookup errors, removal semantics, custom clocks, and concurrency.
Like tf2,
transforms resolves coordinates through a tree of buffered frame
relationships, including static transforms and interpolation. The lookup
argument order is also target first, source second.
In ROS2, tf2 provides the transform core and tf2_ros adds communication
and waiting facilities. This crate supplies local computation and storage;
applications provide transport, synchronization, and waiting. It is a Rust
implementation, not a binding to tf2, and does not aim for API parity.
| Aspect | ROS2 tf2 / tf2_ros | transforms |
|---|---|---|
| API | C++ and Python APIs | Rust types, traits, and Result |
| Lookup | lookupTransform(target, source, time) |
get_transform(target, source, time) |
| Latest data | A zero lookup time requests the latest transform | Zero is an ordinary instant; use latest_common_time |
| Retention | 10-second default; dynamic cache pruned on insert | new retains indefinitely; with_max_age evicts on insert |
| Communication | tf2_ros broadcasts and listens through ROS2 |
No ROS2, DDS, or other transport dependency |
| Waiting | ROS2 buffer supports waiting for transforms | Synchronous queries; caller supplies waiting |
| Embedded use | ROS2 integration uses a hosted runtime | no_std + alloc, with f64 geometry |
| Re-parenting | Parent stored per sample; a new parent silently re-parents | One parent per frame; explicit reparent_frame drops that frame's history |
The tf2 cache implementation details zero-time lookup and pruning. For ROS integration in Rust, see roslibrust_transforms and transforms_io; check their supported versions and middleware separately.
Time lookup is O(log n) in samples per frame; multi-hop lookup also depends
on chain depth. Failed lookups may scan all frames to diagnose the cause.
Lookups allocate owned results. no_std requires enough heap for retained
samples, frame names, and temporary lookup allocations.
CI builds for Cortex-M4F/M7, Cortex-M0+, Cortex-M33, and RISC-V targets.
These builds establish compilation, not board latency or memory bounds.
Measure the intended allocator, board, and workload before setting a budget.
libm is used in both feature modes; tests pin selected interpolation results
on the test host, without guaranteeing cross-device replay.
The v2 feasibility report records host
measurements and reproduction commands. Run cargo bench for benchmarks.
2.2.0 adds Registry::reparent_frame, atomic re-parenting at the price of the
moved frame's stored history, with the NoParentToReplace and
ParentUnchanged error variants. Its lookups allocate frame names once per
result instead of per hop, name a failing edge on the connecting chain, and
report why frames cannot be connected instead of a numeric overflow. 2.1.3 reduced dynamic-history memory use and
lookup allocations, and shortened and corrected the documentation, changing no
public API, serialization, or numerical behavior. See the
changelog for earlier releases.
All examples are hosted programs. The no_std_* examples disable this
library's std feature; their executable and logging still use std.
| Example | Demonstrates |
|---|---|
| std_minimal | Point transformation and interpolation |
| std_full | Tokio readers and a writer sharing an RwLock |
| std_advanced | Coordinates expressed between different instants |
| no_std_minimal | Insertion and lookup without the library's std feature |
| no_std_full | Point transformation, interpolation, and manual cleanup |
| no_std_advanced | Cross-time lookup and manual cleanup |
cargo run --example std_full
cargo run --example no_std_minimal --no-default-featuresThe following are outside this crate's scope:
- Scaling transformations
- Skew transformations
- Perspective transformations
- Non-rigid transformations
- Affine transformations beyond rigid body motion
- API parity with ROS2 tf2
- Non-linear interpolation
- Extrapolation
- f32 or mixed-precision arithmetic (every coordinate and rotation is f64)
This project uses AI assistance. Since 2.0.0, assisted commits require an
Assisted-by: <Agent>:<model> trailer. The maintainer reviews contributions
and takes responsibility for the code. See AGENTS.md.
See CONTRIBUTING.md for the workflow and verification gate, and SECURITY.md for vulnerability reporting.
MIT.