AUTONOMOUS-DRIVING TOOLS
Coordinate frame converter
KITTI, nuScenes, Waymo and ROS all disagree about which axis is forward and where a heading angle starts. Pick two, and see the same physical car described by two different sets of numbers — plus the matrix and the code that carry you between them.
SOURCE FRAME
nuScenes LIDAR_TOP · x=right y=forward z=up
TARGET FRAME
KITTI camera (rect) · x=right y=down z=forward
INPUT ROTATION
yaw (deg)
about +z (up), from +x (right)
SAMPLE POINT IN SOURCE FRAME
x
y
z
SAME CAR · SAME ARROWS · DIFFERENT LABELS
drag to orbit
SOURCE
nuScenes LIDAR_TOP · RFU
TARGET
KITTI camera (rect) · RDF
AXIS MAP — T, WHERE p_target = T · p_source
point (1, 0, 0)
→ (1, 0, 0)
handedness
right-handed, det(T) = +1
This part is never ambiguous. Points, translations and any direction vector go through T and nothing else.
BOX HEADING — WHAT A LABEL FILE MEANS
yaw in nuSc lidar
30°
rotation_y in KITTI cam
-30°
relation
rotation_y = −yaw
Derived by carrying the physical facing direction across, not from a table. A box's geometry does not get relabeled when you change frames.
ROTATION RE-EXPRESSED — R_target = T · R_source · Tᵀ
SAME ROTATION, OTHER REPRESENTATIONS
quaternion in (w,x,y,z)
0.96593 0.00000 0.00000 0.25882
quaternion out (w,x,y,z)
0.96593 0.00000 -0.25882 0.00000
euler out (roll,pitch,yaw)
0° -30° 0°
Quaternions print with w ≥ 0 so the same orientation always shows the same numbers — q and −q are the same rotation.
PYTHON
import numpy as np
# nuScenes LIDAR_TOP (RFU) -> KITTI camera (rect) (RDF)
T = np.array([
[ 1., 0., 0.],
[ 0., 0., -1.],
[ 0., 1., 0.],
])
# 1. points, translations, and any direction vector
p_kitti_cam = T @ p_nuscenes_lidar
# 2. poses / extrinsics — re-express the rotation operator in the new basis
R_kitti_cam = T @ R_nuscenes_lidar @ T.T
# 3. box labels — carry the physical facing direction across
# yaw: about +z (up), from +x (right)
# rotation_y: about +y (down), from +x (right) — the KITTI label field
rotation_y = -yaw
rotation_y = np.arctan2(np.sin(rotation_y), np.cos(rotation_y)) # wrap to [-pi, pi)
WHERE THESE DEFINITIONS COME FROM
nuScenes LIDAR_TOP · RFU
Derived from kitti_to_nu_lidar = Quaternion(axis=(0,0,1), angle=+pi/2) in export_kitti.py
KITTI camera (rect) · RDF
KITTI object devkit — rotation_y is the rotation about the camera Y axis
The KITTI and nuScenes axes were pinned from nuscenes-devkit/export_kitti.py rather than from prose — it asserts the velodyne→camera rotation outright, and defines nuScenes LIDAR_TOP as KITTI rotated +90° about z. Everything on this page is derived from those axis definitions; no pairwise formula is hard-coded.
Runs entirely in your browser — nothing is uploaded, and the URL carries the whole state, so a shared link reproduces exactly what you are looking at. Found a convention that is wrong or missing? tell me.