"Roll, pitch and yaw" sounds unambiguous until you compare two libraries and get two different answers. Euler angles depend on which axes the robot uses, the order the rotations are applied in, and whether they turn about fixed or moving axes. ROS pins all of these down in two short standards, REP 103 and REP 105. This guide explains them with examples you can check.
The axes: REP 103
For a robot's body, ROS uses a right-handed frame:
- X forward
- Y left
- Z up
Positive rotations follow the right-hand rule: point your right thumb along the axis, and your fingers curl in the positive direction. That gives these everyday meanings:
| Angle | Axis | Positive means |
|---|---|---|
| Roll | X (forward) | left side up, right side down |
| Pitch | Y (left) | nose down |
| Yaw | Z (up) | turn left (counter-clockwise seen from above) |
The pitch sign surprises people used to aircraft, where pitching the nose up is positive. Aircraft conventions use X forward, Y right, Z down (often called NED or FRD); ROS uses X forward, Y left, Z up (FLU). Data from flight controllers, many IMUs and some GPS devices arrive in the other convention and must be converted at the driver.
REP 103 also fixes units (metres, radians, seconds) and defines a second convention for cameras: in a frame whose name ends in _optical or _optical_frame, Z points forward out of the lens, X right and Y down, matching image coordinates.
The order: fixed X, then Y, then Z
ROS applies roll, pitch and yaw as rotations about the fixed (parent) axes in the order X, then Y, then Z. The equivalent rotation matrix is:
Reading the matrix product from right to left, roll is applied first. The same orientation can be described as yaw first, then pitch about the new Y axis, then roll about the newest X axis: rotations about moving axes in the reverse order give the same result. Both descriptions are correct and describe one convention; what matters is that every tool you use agrees on it.
These all use the ROS convention:
- URDF
<origin rpy="r p y"/> tf2::Quaternion::setRPYandtf2::Matrix3x3::getRPYstatic_transform_publisher --roll --pitch --yawtf_transformations.quaternion_from_euler(r, p, y)(its default axes,'sxyz', mean static X-Y-Z)- SciPy's
Rotation.from_euler('xyz', …): lowercase letters mean fixed axes
We checked these against each other: for roll 0.1, pitch 0.2 and yaw π/2 radians, tf2, Eigen, SciPy and tf_transformations all produced the same quaternion, (−0.035341, 0.105669, 0.699167, 0.706223).
In SciPy, uppercase 'XYZ' means rotations about moving axes, which is a different convention. from_euler('XYZ', …) and from_euler('xyz', …) give different orientations for the same three numbers.
The frames: REP 105
REP 105 names the standard coordinate frames of a mobile robot and who publishes them:
- map: a fixed world frame. Global localisation (AMCL, SLAM) publishes the
map→odomtransform. The robot's pose in it is accurate long-term but can jump when localisation corrects itself. - odom: a smooth, continuous frame from wheel odometry and IMU. The odometry source publishes
odom→base_link. It never jumps but drifts over time. - base_link: rigidly attached to the robot, with the axes above.
Sensors hang off base_link through static transforms; our guide to static transforms shows how to set them up.
Worked example: a tilted camera
A camera mounted 10 cm forward and 20 cm up, tilted 15° to look down at the floor ahead. Tilting the nose down is positive pitch, so:
ros2 run tf2_ros static_transform_publisher --x 0.10 --y 0 --z 0.20 \
--roll 0 --pitch 0.2618 --yaw 0 --frame-id base_link --child-frame-id camera_link
Run on ROS 2 Jazzy, the publisher reported exactly that rotation, (0, 0.130526, 0, 0.991445). Adding the standard optical frame (roll −90°, yaw −90° from camera_link) and asking tf2_echo for base_link → camera_optical_frame showed the optical Z axis pointing forward and 15° down, as expected.
Sign and order mistakes, and how to catch them
- Degrees instead of radians. Everything in ROS is radians. 90 entered as radians is about 5157°.
- Nose-up pitch assumed positive. In ROS it is negative.
- Uppercase vs lowercase axis strings in SciPy and similar libraries.
- NED data treated as FLU. Convert IMU and flight-controller data at the driver.
The quickest check is visual: enter the angles in the Quaternion ↔ Euler Converter and look at the drawn axes, or view the frames in RViz with the TF display. If the camera's blue Z arrow (in an optical frame) does not point where the lens points, something above is wrong.