Worked rotation / Attitude math

A drone camera is pointing one way. Its target requires a 45-degree yaw correction. What exactly should the controller command?

This companion to Quaternions, Axis-Angle, and Vector Rotation takes the inverse trip: from a current and target orientation to a relative quaternion, then to a human-readable axis and angle. We will test the answer against the C implementation in attitudeMathLibrary.

Animated isometric plot of a camera rotating 45 degrees around its positive Z axis
Our one concrete problem: turn the camera about +Z until its heading reaches the target. The red vector moves; the dotted vectors mark start and finish. Open the full-size diagram.

1. What the controller actually knows

Imagine a camera mounted on a level drone. We choose a right-handed frame, with +Z vertical, and define positive yaw by the right-hand rule. The camera starts at the identity orientation and needs a positive 45-degree yaw correction. This is a deliberately simple case: it isolates the conversion before we add roll, pitch, moving targets, or uncertain state estimates.

Current poseqcurrent = [1, 0, 0, 0]

Target poseqtarget = [cos(22.5°), 0, 0, sin(22.5°)]

Relative rotationqerror = qtarget ⊗ inverse(qcurrent)

Commandaxis = [0, 0, 1], angle = 45°

Quaternion order is not decoration. In this library's convention, applying q_error after q_current reconstructs q_target. If your frame convention or multiplication order differs, the formula must be re-derived, not copied blindly.

2. Why the quaternion contains 22.5 degrees

A unit quaternion for an axis u and angle θ stores the half-angle:

q = [cos(θ/2), ux sin(θ/2), uy sin(θ/2), uz sin(θ/2)]

Here u = [0, 0, 1] and θ = 45°. Therefore θ/2 = 22.5° and:

PartExpressionValueMeaning
Scalar wcos(22.5°)0.92387953Encodes the half-angle
Vector x, y0 · sin(22.5°)0, 0No X or Y axis component
Vector z1 · sin(22.5°)0.38268343Positive Z axis component

So qtarget = [0.92387953, 0, 0, 0.38268343]. Because the current pose is identity, its inverse is also identity and qerror = qtarget.

Animated plots comparing a 45-degree physical camera turn with the quaternion's 22.5-degree half-angle
Two coordinate spaces, one rotation. The camera heading turns 45 degrees in its XY plane; the unit quaternion moves 22.5 degrees in the [w, vz] plane. Open full size.

3. Recover the axis and angle

Write a normalized relative quaternion as q = [w, vx, vy, vz]. Let s = ||v|| = sqrt(vx2 + vy2 + vz2). For the shortest rotation, choose the equivalent sign with w ≥ 0. Then, away from zero rotation:

axis = v / s

angle = 2 atan2(s, w)

In our example, s = 0.38268343. Dividing [0, 0, 0.38268343] by s gives [0, 0, 1]. Twice the angle returned by atan2(0.38268343, 0.92387953) gives 45°. We have recovered the command from the stored rotation without guessing the axis from the picture.

4. What happens at zero and 180 degrees?

CaseQuaternionWhat extraction should say
Zero rotation[1, 0, 0, 0]Angle is zero. The axis is mathematically undefined; a program may return [0, 0, 0] as a sentinel.
180 degrees about +Z[0, 0, 0, 1]Angle is 180 degrees. At exactly 180 degrees, +Z and -Z with a positive 180-degree angle describe the same spatial rotation.
Negated quaternion[-w, -v]Same rotation as [w, v]. Canonicalizing the sign avoids an unnecessary long-turn representation.

Near zero, division by s becomes ill-conditioned, so extraction needs a tolerance and a documented axis sentinel. At 180 degrees, w is near zero; floating-point noise can flip the reported axis sign. A controller should compare physical rotations, not exact quaternion signs.

Side-by-side visual of zero, 45-degree, and 180-degree rotations and their axis-angle meanings
The zero case has no unique axis. At 180 degrees, +Z and -Z paired with a positive 180-degree angle produce the same final rotation. Open full size.

5. Follow the C code, not just the diagram

The library already computes the relative rotation and has a camera tracking test for this 45-degree case. Its current quaternion_relative implementation multiplies target by inverse(current), normalizes the result, and flips the quaternion sign when the scalar is negative. Its quaternion_to_axis_angle uses 2 * acos(w) and divides the vector part by its norm. For a near-zero vector part it returns angle zero and axis [0, 0, 0].

You can reproduce the result with the public API, without borrowing a helper from a test file:

camera_axis_angle.c / public C APIC
#include <attitude/quaternion.h>
#include <math.h>
#include <stdio.h>

int main(void) {
    const double pi = acos(-1.0);
    const double current[4] = {1.0, 0.0, 0.0, 0.0};
    const double target[4] = {cos(pi / 8.0), 0.0, 0.0, sin(pi / 8.0)};
    double axis[3], angle;

    if (!quaternion_orientation_error_axis_angle(
            current, target, axis, &angle)) return 1;
    printf("axis=[%.1f, %.1f, %.1f], angle=%.1f degrees\n",
           axis[0], axis[1], axis[2], angle * 180.0 / pi);
    return 0;
}

Clone attitudeMathLibrary, run make, save the code above as camera_axis_angle.c in the repository root, then compile and run:

Build and runshell
cc -std=c11 -Iinclude camera_axis_angle.c build/libattitude.a -lm -o camera_axis_angle
./camera_axis_angle
# axis=[0.0, 0.0, 1.0], angle=45.0 degrees

The repository test independently checks this camera case and reconstruction of a target pose. The atan2 expression earlier is the stable teaching form for normalized inputs; it is not a claim that the current C function uses it. The implementation uses acos(w) without clamping, so applications should validate finite, normalized inputs before calling it. There is also a small API-doc mismatch: the header says quaternion_to_axis_angle returns zero when the identity axis is undefined, but the current source returns success with angle zero and axis [0, 0, 0]. Treat that sentinel as the observed behavior, not as a mathematical axis.

Implementation trail / code review

Follow the rotation into the repository

1 commit + 2 PRs

The camera-error helper was introduced in a direct commit. The two merged pull requests then provide independent quaternion parity and broader conversion validation. They are related evidence, not a claim that the helper itself arrived in those PRs.

Implementation and validationOpen the source, the tests, and the reviewed changes.3 links
  1. CommitAdd relative orientation and camera axis-angle helpers23a9768
  2. PR #1Verify compiled quaternion operations against SciPy70c71d2
  3. PR #2Harden conversions and publish validation docs8d78d29

Start with quaternion.c, then read test_quaternion_relative.c and the compiled API documentation.

6. Questions to explore next

The worked example is a starting point. These are the questions worth exploring in a deeper question-and-answer follow-up:

  1. Why does a quaternion store half the physical rotation angle?
  2. Why can q and -q turn the same camera pose?
  3. Why does the zero-rotation axis disappear, and what should a controller do with that case?
  4. When the drone is already rolled or pitched, what frame does the recovered error axis live in?

For the broader picture, read the original quaternion article first or next. This post is its worked inverse example: from the desired camera correction back to the axis and angle the controller can reason about.