Skip to content

technitute/transforms-cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

transforms

A command-line tool for resolving rigid body transformation chains. Define named coordinate frames and their relationships in a YAML file; the tool walks the frame graph, composes the transforms, and prints the result in whatever rotation representation you choose.


Contents


Building

Requirements: CMake ≥ 3.21, a C++20 compiler (GCC 12+, Clang 15+, MSVC 19.34+).

Dependencies are fetched automatically by CMake:

Library Version Purpose
Eigen 5.0.1 Matrix and quaternion arithmetic
yaml-cpp 0.9.0 YAML parsing
GoogleTest 1.17.0 Unit tests
cmake -S . -B build
cmake --build build

The binary is at build/app/transforms_cli.

To skip building the test suite:

cmake -S . -B build -DTRANSFORMS_BUILD_TESTS=OFF

Quick start

./build/app/transforms_cli example.yaml

Output:

result:
  parent: world
  child: body
  rotation:
    type: dcm
    convention: passive
    matrix:
      - [1.00000000, 0.00000000, 0.00000000]
      - [0.00000000, 0.00000000, 1.00000000]
      - [0.00000000, -1.00000000, 0.00000000]
  translation:
    frame: parent
    direction: parent_to_child
    vector: [1.05000000, 0.00000000, 2.12000000]

Concepts

Canonical internal form

Internally all transforms are stored in a single canonical representation:

  • Rotation: passive direction-cosine matrix (DCM) C such that v_child = C · v_parent
  • Translation: position of the child frame origin expressed in the parent frame

Every input rotation and translation is converted to this form on load, and every output is converted from it on write. This means you can freely mix representations across frames and outputs.

Passive vs active convention

Convention Meaning Relationship
passive The frame rotates; vectors are re-expressed C (canonical)
active The vector rotates; the frame is fixed R = Cᵀ

For a 90° rotation about Z: the passive DCM maps a world +X vector to the child frame as +Y. The active matrix rotates the +X vector to +Y in the same frame. They are transposes of each other.

Quaternion component order

Order Layout Used by
wxyz [w, x, y, z] Eigen, most mathematics texts, Hamilton convention
xyzw [x, y, z, w] ROS, JPL, many robotics frameworks

Both orders represent the same mathematical quaternion; only the storage layout differs.

Euler angles

All 12 standard sequences are supported — six Tait-Bryan orders (all three axes distinct) and six proper Euler orders (first and last axis the same):

Tait-Bryan: XYZ XZY YXZ YZX ZXY ZYX

Proper Euler: XYX XZX YXY YZY ZXZ ZYZ

ZYX intrinsic is the aerospace yaw–pitch–roll convention.

Intrinsic vs extrinsic:

Frame Axes Equivalent
intrinsic Body-fixed (moving) axes Standard body-frame composition
extrinsic World-fixed axes Intrinsic with reversed order and angles

Extrinsic ZYX [α, β, γ] equals intrinsic XYZ [γ, β, α].

Singularities (gimbal lock)

At singular configurations the decomposition into individual angles is not unique. The tool handles all 12 singularity branches:

  • Tait-Bryan: middle angle β = ±90° — the tool sets α = 0 and absorbs the constraint into γ
  • Proper Euler: middle angle β = 0° or 180° — the tool sets α = 0 and absorbs into γ

The extracted angles will reproduce the correct DCM even though they may differ from the input angles.

Frame graph resolution

Frames are defined relative to a named parent. The tool walks from the requested child frame up to the requested parent, collecting each transform along the path, then composes them with chain():

world → A → B → C      (frame definitions)
request: world → C      (output)
path: [T_A, T_B, T_C]  (collected bottom-up, reversed)
result: T_C ∘ T_B ∘ T_A

Errors are raised for cycles, missing frames, or unreachable paths.


YAML schema

transforms

A top-level sequence of named frames, each defined relative to a parent:

transforms:
  - transform:
      name: my_frame        # child frame name (required)
      parent: world         # parent frame name (required)
      rotation: ...         # see Rotation section
      translation: ...      # see Translation section

Frame names are arbitrary strings. The special frame world (or any name not defined in the transforms list) acts as the root.


Rotation

The rotation block always requires a type field. All other fields have defaults.

DCM — Direction Cosine Matrix

rotation:
  type: dcm
  convention: passive        # passive | active  (default: passive)
  direction: parent_to_child # parent_to_child | child_to_parent  (default: parent_to_child)
  matrix:
    - [r00, r01, r02]
    - [r10, r11, r12]
    - [r20, r21, r22]

The matrix must be orthonormal with determinant +1 (validated to 1 × 10⁻⁶).

direction: child_to_parent flips the effective convention: a passive child→parent matrix is treated as active parent→child, and vice versa.

Quaternion

rotation:
  type: quaternion
  convention: passive        # passive | active  (default: passive)
  direction: parent_to_child # parent_to_child | child_to_parent  (default: parent_to_child)
  order: wxyz                # wxyz | xyzw  (default: wxyz)
  components: [a, b, c, d]  # four components in the order specified above

The quaternion must be unit norm (validated to 1 × 10⁻⁶). Use at least 9 significant figures to satisfy the tolerance, e.g. [0.984807753, 0.0, 0.0, 0.173648178] for 20° about Z.

Euler angles

rotation:
  type: euler
  convention: passive        # passive | active  (default: passive)
  direction: parent_to_child # parent_to_child | child_to_parent  (default: parent_to_child)
  order: ZYX                 # any of the 12 standard sequences
  frame: intrinsic           # intrinsic | extrinsic  (default: intrinsic)
  unit: degrees              # degrees | radians  (default: degrees)
  angles: [α, β, γ]         # three angles in the axis sequence order

Angle-axis

rotation:
  type: angle_axis
  convention: passive        # passive | active  (default: passive)
  direction: parent_to_child # parent_to_child | child_to_parent  (default: parent_to_child)
  unit: degrees              # degrees | radians  (default: degrees)
  angle: 90.0
  axis: [0.0, 0.0, 1.0]    # need not be unit length — normalised on load

Translation

translation:
  frame: parent              # parent | child  (default: parent)
  direction: parent_to_child # parent_to_child | child_to_parent  (default: parent_to_child)
  vector: [x, y, z]         # defaults to [0, 0, 0]

frame specifies the coordinate frame in which the vector components are expressed. direction specifies which origin the vector points away from.

Common combinations:

frame direction Meaning
parent parent_to_child Child origin in parent coordinates (most common)
child parent_to_child Child origin in child coordinates
child child_to_parent Lever-arm from child back to parent, in child coordinates
parent child_to_parent Lever-arm from child to parent, in parent coordinates

output / outputs

Use output: for a single result or outputs: for multiple results in one run.

Single output:

output:
  parent: world              # root of the chain to resolve
  child: my_frame            # leaf of the chain to resolve
  rotation:
    type: dcm
    convention: passive
    # order, frame, unit — only needed for euler output
  translation:
    frame: parent

Multiple outputs:

outputs:
  - output:
      parent: world
      child: link2
      rotation:
        type: euler
        order: ZYX
        unit: degrees
      translation:
        frame: parent
  - output:
      parent: world
      child: end_effector
      rotation:
        type: quaternion
        order: xyzw
      translation:
        frame: parent

The output rotation block describes only the desired format — no matrix or component data, just type, convention, order, frame, and unit.


Output format

Single output — top-level result: key:

result:
  parent: world
  child: camera
  rotation:
    type: quaternion
    convention: passive
    order: xyzw
    xyzw: [0.70109644, 0.22667071, 0.31691998, 0.59719838]
  translation:
    frame: parent
    direction: parent_to_child
    vector: [100.06660182, 50.06043898, 30.06641783]

Multiple outputs — top-level results: sequence:

results:
  - result:
      parent: world
      child: link2
      rotation:
        ...
      translation:
        ...
  - result:
      parent: world
      child: end_effector
      rotation:
        ...
      translation:
        ...

The translation is always output as direction: parent_to_child (the canonical direction) in the requested frame.


Examples

Three annotated examples are provided in the examples/ directory.

examples/drone_camera.yaml

A drone with a nadir camera. The vehicle attitude is given as ZYX Euler angles in degrees (aerospace yaw–pitch–roll). The camera mount rotation is given as an angle-axis. The output is requested as a passive quaternion in xyzw order (ROS convention).

./build/app/transforms_cli examples/drone_camera.yaml

examples/robot_arm.yaml

A three-DOF robot arm. Each link joint uses a deliberately different rotation representation (DCM → angle-axis → Euler → quaternion) to demonstrate that they are interchangeable. Uses outputs: to return both an intermediate joint pose and the final end-effector pose in a single run.

./build/app/transforms_cli examples/robot_arm.yaml

examples/spacecraft.yaml

A spacecraft in low Earth orbit with a star tracker mounted on its +Z panel. Demonstrates the active rotation convention and child_to_parent direction on both rotation and translation — the style common in optical instrument calibration files. The spacecraft attitude is given as a passive quaternion in xyzw (ROS/JPL) order. Output is requested as an active angle-axis.

./build/app/transforms_cli examples/spacecraft.yaml

Running the tests

cmake --build build --target transforms_tests
./build/tests/transforms_tests

The test suite covers:

  • Euler ↔ DCM round-trips for all 12 rotation orders, both intrinsic and extrinsic frames, radians and degrees
  • Gimbal-lock (singularity) branches for all 12 orders at both β = +90° and β = −90° (Tait-Bryan) and β = 0° and β = 180° (proper Euler)
  • Quaternion ↔ DCM and angle-axis ↔ DCM round-trips
  • Cross-representation consistency — the same rotation expressed as a DCM, quaternion, angle-axis, and Euler angles all produce the same canonical transform
  • Active vs passive duality across all four representations
  • to_canonical and from_canonical for every input/output type
  • chain() composition including the inverse identity, rotation-into-translation, and multi-hop ordering
  • resolve_path graph traversal — single hop, multi-hop, partial chains, same-frame identity, cycle detection, and missing-frame errors
  • Physical frame chain scenarios — aircraft sensor, three-link robot arm, mixed-representation inputs

Project layout

transforms/
├── app/
│   ├── main.cpp                  # Entry point: load → resolve → write
│   ├── spec.hpp                  # Data types for the parsed YAML file
│   ├── loader.hpp                # YAML → spec structs
│   ├── resolver.hpp              # Frame graph path resolution
│   ├── writer.hpp                # Canonical transform → YAML output
│   └── transforms/
│       ├── types.hpp             # Scalar, Vector3, DCM, Quaternion, AngleAxis
│       ├── conventions.hpp       # Enums: Convention, EulerOrder, QuaternionOrder, …
│       ├── rigid_transform.hpp   # RigidTransform struct (passive DCM + translation)
│       └── conversions.hpp       # All rotation conversions + to_canonical / from_canonical / chain
├── tests/
│   └── transforms_tests.cpp      # GoogleTest suite (66 tests across 12 suites)
├── examples/
│   ├── drone_camera.yaml
│   ├── robot_arm.yaml
│   └── spacecraft.yaml
├── cmake/
│   ├── Dependencies.cmake        # FetchContent for Eigen, yaml-cpp, GoogleTest
│   └── ClangTools.cmake          # clang-format / clang-tidy targets
├── example.yaml                  # Minimal two-frame example
└── CMakeLists.txt

Key invariants

  • Single canonical form. All rotation math operates on the passive DCM. Conversions in and out happen only at the I/O boundary (to_canonical / from_canonical).
  • No runtime allocation in the math layer. conversions.hpp is entirely inline with fixed-size Eigen types; no heap allocation.
  • All 12 Euler singularity branches are correct. Each order's gimbal-lock path is independently covered by tests at both +90° and −90° (Tait-Bryan) and 0° and 180° (proper Euler).

About

Command-line tool for resolving rigid body transformation chains

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors