all lessons / computer_vision_3d / 02 · rotations & SE(3) lesson 2 / 13

Rotations and rigid-body motion — SE(3)

Lesson 01 ended on a quiet assumption: to place a camera, align a scan, or move an object, we can just "transform" it. Translation is trivial — vectors add. Rotation is where it gets hard, and getting it wrong silently wrecks real systems: optimizers that drift off the space of valid rotations, controllers that lock up at a pole, interpolations that swing wide. This lesson builds rigid-body motion from first principles and, crucially, the one trick — optimizing on the tangent space — that every pose estimator in this track (registration, bundle adjustment, NeRF pose refinement, SLAM) leans on.

The plan
Four moves. (1) See why rotation is not a vector space — rotations don't commute and the valid ones form a curved surface, so you can't just add or step along them. (2) Meet the four representations — rotation matrix, Euler angles, axis-angle, quaternion — and exactly what each is good and bad at (gimbal lock lives here). (3) Assemble the full rigid transform SE(3) and its composition/inverse rules. (4) The payoff: the Lie algebra so(3)/se(3), the exp/log maps (Rodrigues' formula), and why you optimize a pose as a small unconstrained twist that you exponentiate back onto the manifold.

1 · Why rotation is not a vector space

Translations are a vector space: any two add, they commute (a+b = b+a), scaling is free, and gradient descent can step in any direction and stay valid. Rotations fail every one of these, and the failures are not academic — they are the source of the bugs this lesson exists to prevent.

Rotations don't commute. Take a book. Rotate it 90° about the vertical axis, then 90° about the axis pointing right; note its pose. Start over and do the two in the opposite order. The final poses differ. Formally R1R2 ≠ R2R1 — order matters, so "add up the rotations" is meaningless. (Translations, by contrast, always commute.)

Valid rotations form a curved surface, not a flat space. A 3D rotation is a 3×3 matrix R with two properties: its columns are orthonormal (RᵀR = I) and it preserves handedness (\det R = +1). That is 9 numbers tied down by 6 constraints, leaving 3 degrees of freedom — but those 3 DOF live on a curved 3-dimensional surface (the group SO(3)) embedded in 9-dimensional matrix space. Step off it — as an optimizer nudging all 9 entries surely will — and you get a matrix that shears and scales, no longer a rotation. This single fact ("the valid set is a curved manifold you fall off of") is what makes the naïve approaches break and what section 4 fixes properly.

The through-line
Everything below is a way to parameterize those 3 DOF. Each choice trades off minimality (how many numbers), freedom from singularities (gimbal lock), composition cost, and — the one that matters most for this track — whether an optimizer can move on it without leaving the manifold.

2 · Four ways to write a rotation

Rotation matrix — R ∈ SO(3). Nine numbers, orthonormal columns. It acts directly (v' = Rv) and composes by matrix multiply (R1R2). Perfect for computing; terrible for storing or optimizing, because 9 numbers with 6 constraints are hugely redundant, and any unconstrained update breaks orthonormality, forcing a re-orthonormalization (e.g. via SVD) every step.

Euler angles — (yaw, pitch, roll). Three numbers, one per intuitive axis, applied in sequence. Minimal and human-readable, which is why they show up in UIs and datasheets. But they carry a fatal singularity: gimbal lock. When the middle rotation reaches ±90°, the first and third rotation axes line up — they now do the same thing, and one degree of freedom vanishes. Near that pole, small pose changes demand huge angle changes, interpolation misbehaves, and optimizers stall. Euler angles also don't compose or interpolate cleanly (which order? which axes?). Use them to display a rotation, never to optimize one. The widget below lets you drive a frame into gimbal lock and watch a DOF disappear.

Axis-angle — (θ, \hat{k}). Euler's theorem: every rotation is a single rotation by angle θ about some unit axis \hat{k}. Pack it as the 3-vector ω = θ\hat{k} — exactly 3 numbers, no redundancy. This is the most physically honest parameterization and, we'll see, it is the coordinate on the Lie algebra. Its only awkwardness: composing two axis-angles isn't a simple formula (you convert to matrices or quaternions first), and θ=0 leaves the axis undefined (a removable singularity).

Quaternion — unit q = (w, x, y, z). The practitioner's default for storing and interpolating orientation. A unit quaternion encodes the axis-angle as

q = \big(\cos\tfrac{θ}{2},\ \ \hat{k}\,\sin\tfrac{θ}{2}\big),\qquad ‖q‖ = 1 .

Four numbers, one constraint (unit norm) — far less redundant than a matrix, and no gimbal lock anywhere. Composition is the Hamilton product (cheaper than matmul), rotating a vector is v' = q\,v\,q-1, and interpolation is SLERP (spherical linear interpolation), which walks the shortest arc on the sphere of orientations at constant angular speed. One subtlety to know cold: quaternions double-cover SO(3)q and −q represent the same rotation (note the θ/2: rotating by sends q → −q but returns the object to start). SLERP must pick the shorter of the two to avoid taking the long way around.

RepresentationNumbers / DOFGimbal lock?CompositionBest for
Rotation matrix9 / 3nomatmulacting on vectors, computing
Euler angles3 / 3yes (±90° middle)awkwarddisplay, human input
Axis-angle ω=θ\hat k3 / 3no (only θ=0 degenerate)awkwardoptimization (the so(3) coordinate)
Quaternion4 / 3noHamilton product (cheap)storing + interpolating (SLERP)
Drive a frame into gimbal lock
A right-handed coordinate frame rotated by yaw–pitch–roll Euler angles (intrinsic Z–Y–X). Take pitch toward ±90° and watch the warning fire: the yaw and roll axes align, so the two sliders start doing the same thing and one degree of freedom is lost. The KPIs show the equivalent unit quaternion (which has no such singularity) and the orthonormality error of R — a health check that stays ~0 here but drifts if you optimize matrix entries directly.
Quaternion (w,x,y,z)
Rotation angle θ
‖RᵀR − I‖ (health)
0.00
Gimbal lock?
no
Show the core JS
// Euler (intrinsic Z-Y-X) → rotation matrix, then draw R·e_x, R·e_y, R·e_z
function Rz(a){return [[cos(a),-sin(a),0],[sin(a),cos(a),0],[0,0,1]];}
function Ry(a){return [[cos(a),0,sin(a)],[0,1,0],[-sin(a),0,cos(a)]];}
function Rx(a){return [[1,0,0],[0,cos(a),-sin(a)],[0,sin(a),cos(a)]];}
const R = matmul(matmul(Rz(yaw), Ry(pitch)), Rx(roll));

// matrix → quaternion (no singularity); angle θ = 2·acos(w)
const w = 0.5*Math.sqrt(1 + R[0][0]+R[1][1]+R[2][2]);
// ... x,y,z from off-diagonal terms ...
const theta = 2*Math.acos(Math.min(1, Math.abs(w)));

// GIMBAL LOCK: at pitch → ±90°, the yaw (Z) and roll (X) axes align.
const gimbal = Math.abs(Math.abs(pitch) - Math.PI/2) < 0.12;
// health of R as a rotation: ‖RᵀR − I‖ should be ~0 (drifts if you optimise entries directly)

3 · Rigid-body motion — SE(3)

A camera or object pose is a rotation and a translation: a rigid transform, the group SE(3) (Special Euclidean, 6 DOF = 3 rotation + 3 translation). Write it as a pair (R, t) acting on a point by p' = Rp + t. The clean way to compose and invert these is the homogeneous 4×4 form (the same trick CV lesson 04 used to fold translation into a matrix):

T = [ R  t ; 0  1 ],\qquad [ p' ; 1 ] = T\,[ p ; 1 ] .

Now chaining transforms is just matrix multiply, TAC = TAB\,TBC, which is how you walk a point from one coordinate frame to another (object → world → camera → image is a chain of these). The inverse has a closed form worth memorizing — you invert the rotation and undo the translation in the rotated frame:

T-1 = [ Rᵀ  −Rᵀt ; 0  1 ] .

Note it is not just (−R, −t): the translation must be rotated back by Rᵀ. Getting this inverse wrong (forgetting the Rᵀ on t) is a classic frame-convention bug — the twin of the row/col and BGR bugs from CV lesson 01, and it puts your camera in the wrong place by exactly the rotated translation.

4 · The Lie-algebra trick — optimizing on the manifold

Here is the section the whole lesson was built for. Section 1 warned that SO(3) is a curved surface you fall off of. So how do you run gradient descent on a rotation — as bundle adjustment, ICP, and SLAM all must — without producing invalid matrices?

The tangent space. At the identity, SO(3) has a flat tangent space called so(3) (the Lie algebra), and it turns out to be exactly the skew-symmetric matrices. Any ω = (ω1, ω2, ω3) ∈ ℝ³ maps to one via the hat operator (the same [·]× cross-product matrix from CV lesson 05):

[ω]× = [ 0, −ω₃, ω₂ ; ω₃, 0, −ω₁ ; −ω₂, ω₁, 0 ],\qquad [ω]×\,v = ω × v .

So the tangent space is an ordinary, unconstrained 3-vector ω — exactly the 3 DOF a rotation really has, with no manifold to fall off. (For full poses, se(3) uses a 6-vector twist ξ = (ρ, ω): translation-like part ρ and rotation part ω.)

The exp map: tangent → manifold. Exponentiating a skew matrix lands you back on SO(3), and the series collapses into a closed form — Rodrigues' formula — with θ = ‖ω‖ and \hat k = ω/θ:

R = \exp([ω]×) = I + \sinθ\,[\hat k]× + (1-\cosθ)\,[\hat k]×2 .

This is precisely the axis-angle → matrix conversion from section 2, now revealed as the exponential map. Its inverse, the log map, reads the axis-angle back out of a rotation matrix. Together they give a lossless dictionary between the flat, optimizer-friendly ℝ³ (or ℝ⁶) and the curved group.

The recipe every pose optimizer uses
To refine a pose T, don't perturb its matrix entries (they'd leave the manifold and need re-projection). Instead represent the update as a tiny twist ξ ∈ ℝ⁶ and apply it multiplicatively:
T ← \exp(ξ)\,T
Take derivatives with respect to ξ (a proper minimal, unconstrained coordinate), take a Gauss–Newton / gradient step to get the optimal ξ, exponentiate, compose, repeat. Every iterate is a valid transform by construction. This "retraction onto the manifold" is what bundle adjustment (CV 05), ICP (lesson 04), NeRF pose refinement, and SLAM back-ends are all doing when they "optimize the poses." When an interviewer asks how you'd refine camera poses, this is the answer: optimize twists in the tangent space, retract with exp.

Worked number. A small rotation of θ = 0.02 rad about \hat k = (0,0,1) has ω = (0,0,0.02). Rodrigues gives R ≈ I + 0.02[\hat k]× (since \sin θ ≈ θ, 1-\cos θ ≈ 0), i.e. the top-left block is [[1, -0.02],[0.02, 1]] — a hair of yaw. For small twists \exp([ω]×) ≈ I + [ω]×, which is why the tangent space is the natural place to linearize and why Jacobians in ξ are clean.

Where this points next

We can now represent, compose, invert, and — the part that matters — optimize the placement of any camera or object in 3D, staying on the manifold of valid motions the whole time. That machinery is inert without something to place. So the next question is the most concrete one in the track: where does 3D data actually come from? Lesson 03 covers the sensors — stereo, structured light, time-of-flight, LiDAR — and the operation that turns their output into the rawest 3D representation from lesson 01: back-projecting a depth map through K-1 into a point cloud, expressed in world coordinates via exactly the SE(3) pose we just built.

Takeaway
Rotation is not a vector space: rotations don't commute, and valid ones form a curved 3-DOF manifold SO(3) in 9-D matrix space. Four parameterizations trade off differently — matrices act and compose but drift under optimization; Euler angles are intuitive but hit gimbal lock at ±90°; axis-angle is the honest minimal 3-vector; quaternions store and interpolate (SLERP) with no singularity and double-cover SO(3) (q ≡ -q). Rigid motion is SE(3), best handled as homogeneous 4×4 matrices with inverse (Rᵀ, -Rᵀt). The key operational trick: optimize poses as unconstrained twists in the Lie algebra so(3)/se(3) and retract with the exp map (Rodrigues), so every iterate stays a valid transform — the backbone of ICP, bundle adjustment, and SLAM.

Interview prompts