Where you are. You know that every position is a reading taken against something, and that the something has to be named. This lesson turns that into arithmetic: two frames, two dimensions, one point, and the conversion between them.
Two tape measures
There is a mounting hole in a large steel fabrication bench. You measure it yourself, from the front-left corner of the bench: 3.05 metres along the front edge, 2.18 metres back.
The drawing pinned above the bench says the same hole is at 1.50 and 0.50.
The drawing is not wrong and neither are you. It was dimensioned from the corner of a sub-plate, and the sub-plate is bolted to the bench 2.00 along and 1.00 back, set 30 degrees off square because that is the only way the fixture clears the column.
Two tape measures, one hole, two honest answers, and no reconciling them by eye. There is exactly one piece of arithmetic that turns either into the other. It is four multiplications and four additions long, your robot will run it a few thousand times a second, and doing its two steps in the wrong order is the most common mistake in this entire subject.
The idea in one paragraph
Give every frame a letter. Put the letter on every number. Then converting a point from one 2D frame to another is two steps in a fixed order: re-express the point in the other frame’s directions, then shift it by where the origin sits. Written out, that is , and it is the workhorse of this module. None of it is hard mathematics. All of the difficulty is bookkeeping: which frame is each number in, which way is the conversion running, and are the two steps the right way round? Get the order backwards and you do not get an exception. You get every point in the scene displaced by the same fixed amount, which is exactly the sort of bug that passes tests.
Naming, before arithmetic
Before any numbers, fix the vocabulary, because the vocabulary is doing more work here than the algebra.
Two conventions to adopt now and never revisit. In 2D, points right, points up, and angles are measured counterclockwise from , so a positive angle turns toward . That choice is called right-handed, and in three dimensions it becomes the right-hand rule you will meet in rotations in 3D. Everybody uses it. So do we.
The point of the braces and the subscripts is that a frame is a decision rather than a physical object. Nothing enforces it but notation.
One point, two frames
Back to the bench. Call the bench corner and the sub-plate corner . Frame sits at in and is turned by .
Wider than the screen; scroll it sideways.
The drawing’s numbers are . Yours are . One hole. The hole did not move when we changed frames; only the description did.
Where the axes land
Now derive the rule, because the derivation is three lines and worth owning outright.
Ask one question: where do ‘s axis directions point, described in ?
’s x-axis is a unit arrow turned by away from ‘s x-axis. Its description in is , straight from the definition of cosine and sine as the coordinates of a point on the unit circle. ’s y-axis is that same arrow turned a further 90 degrees, which lands it on .
Wider than the screen; scroll it sideways.
At those come out as and . Now read for what it literally says: start at ‘s origin, walk along ‘s x-axis, then along ‘s y-axis. Write that walk in ‘s language and you are done:
Stack the two axis directions side by side as columns and you have written a rotation matrix:
Run the bench numbers through it:
Which is 3.05 and 2.18: your tape measure, recovered from the drawing’s numbers.
The order is not negotiable
The two steps do not swap. Shift first and turn second and you land somewhere else entirely.
Wider than the screen; scroll it sideways.
That is 1.16 metres apart on a bench, which would be obvious if you ever saw both answers side by side. You will not. You will see one number, and it will look completely reasonable.
Worse, look at why the two differ. Expand the wrong version, using the fact that a rotation distributes over a sum:
The cancels. The error does not depend on the point at all.
The size of that shift has a tidy closed form, and the intuition is a picture rather than an identity. and are two points the same distance from the origin, separated by the angle , so the straight-line gap between them is the chord of that angle: . Here and , giving 1.157. The error grows with how far apart the two origins are and with how much the frames are turned; it vanishes when either is zero, which is precisely why testing on a scene where the frames happen to be aligned proves nothing.
Going back the other way
Converting to is the same two operations undone, in reverse order. To undo “turn, then shift”, you un-shift and then un-turn:
Check it on the bench: , and turning that by −30° gives back exactly .
There is a small gift hiding in . Cosine is even and sine is odd, so and , and therefore:
The inverse of a rotation is its transpose. Two lines of trigonometry buy you an operation that costs nothing at runtime and never suffers a numerical failure, in place of a matrix inversion that can do both. Rotations in 2D shows why this holds for every rotation matrix, in any number of dimensions, and not by coincidence.
The subscripts are a type system you enforce by hand
Here is the notation that makes those mistakes visible before you run anything. Write for the rotation that takes ‘s directions into ‘s, and for ‘s origin expressed in . The conversion becomes:
Read the subscripts left to right and check that the ones that touch match. applied to something in leaves you with something in : the inner s meet and cancel, the outer letters survive. If the inner letters do not match, the line is wrong, and you can see that without knowing a single number.
| Expression | Reads as | Legal? |
|---|---|---|
| W from B, applied to a B quantity | yes; the result is in W | |
| W from B, applied to a C quantity | no; B and C do not match | |
| W from B, then B from C | yes; the result is | |
| B from W, applied to a B quantity | no; the conversion runs the wrong way |
In code that means the frame lives in the variable name and never anywhere else: cup_B, cup_W, R_WB, t_WB. Two rules keep it working. Every array that holds a geometric quantity gets a frame suffix, with no exceptions for “obvious” cases, because obvious is exactly where this fails. And any function that converts is named for both ends, so w_to_b rather than convert.
What this buys you
You now have one frame conversion, done properly, in two dimensions. That sounds small. It is most of the subject.
Everything ahead is this operation with more structure bolted on. Homogeneous transforms package the rotation and the translation into one object so the order can no longer be got wrong. Composing transform chains strings several of them together to walk the tree from the last lesson. Forward kinematics is that walk applied to a robot arm, with one frame per joint. The arithmetic gets no harder than what you just did. Only the bookkeeping grows, which is why the notation came first.
Check yourself
1. Frame sits at in , turned 30 degrees. A point reads . Write the correct conversion, then say what translating first would have produced and how far off it is.
Correct: . Rotating first gives , and adding gives .
Translating first gives , and rotating that gives . The two answers are 1.16 apart. Note that neither looks wrong on its own: both are plausible points on the same bench.
2. Show in two lines that the wrong-order error is the same for every point, and say why that makes it hard to find.
A rotation distributes over a sum, so . The correct answer is . Subtracting, the error is , which contains no . Every point in the scene is displaced by that same constant vector.
It is hard to find because a uniform displacement leaves the scene self-consistent. Distances between objects are unchanged, nothing looks noisy, and the symptom reads as a mis-calibrated camera or a mis-measured mount rather than a line of code with two operations in the wrong order.
3. Write the conversion from {W} back to {B}, and explain why the subtraction comes first.
.
The forward direction was “rotate, then translate”. Undoing a sequence means undoing each step in reverse order, so the last operation applied is the first one undone: subtract the translation, then rotate back. Doing it the other way, , gives a point 1.16 off in this example, which is the same constant-offset bug wearing a different hat.
4. Why is the inverse of a rotation matrix simply its transpose, and why should you care?
Because and , so writing out gives , which is with rows and columns swapped.
You care for two reasons. A transpose is free and exact, while a general matrix inversion costs time and can be numerically poor. And it gives you a cheap sanity check: if is not the identity, whatever you are holding is not a rotation, which catches a surprising number of bugs before they reach the robot.
5. Someone writes p_world = R_WB @ p_C. Nothing raises an error. What is wrong, and what is the smallest fix?
The subscripts do not match. expects a quantity expressed in and this one is expressed in , so the multiplication is meaningless even though the shapes line up and NumPy is perfectly happy. The result is a well-formed array of garbage.
The smallest correct fix is to convert into first, or to compose the rotations, , and use that instead. The reason the mistake is visible at all is the naming; with and it would have been invisible.
6. Give a case where the order genuinely does not matter, and explain why that is a trap rather than a relief.
The error is , so it vanishes when (the frames are aligned) or when (the origins coincide). In either case both orders give the same answer.
It is a trap because those are exactly the cases people build test fixtures from. A test written against two aligned frames passes under both orders and proves nothing about the code, and the intuition it builds fails the first time a real frame arrives at an angle. Any test that is meant to catch this needs a non-zero rotation and a non-zero offset.
Do this
Open code/frames_warmup.py. It sets up inside with THETA = 30° and T = (1.0, 2.0), and leaves three TODO(you) markers: rotate, b_to_w, and w_to_b. Fill them in using only math.cos and math.sin, no NumPy. The assertions at the bottom tell you when you are right, and the last one deliberately checks that translate-then-rotate is not the same as rotate-then-translate. Ten minutes. The solution is in solutions/frames_warmup.py once yours passes or you have genuinely stalled.
Then add ten lines of your own to see the constant-offset result with your hands rather than on the page:
offset = None
for p in [(1.0, 0.0), (0.0, 1.0), (-3.2, 4.7), (100.0, -50.0)]:
right = b_to_w(p)
wrong = rotate((p[0] + T[0], p[1] + T[1]), THETA)
err = (wrong[0] - right[0], wrong[1] - right[1])
print(f"p={p} error={err[0]:+.6f}, {err[1]:+.6f}")
if offset is None:
offset = err
assert close(err, offset), "the error should not depend on the point"
Every line prints the same error, , whether the point is next door or a hundred units away. Its length is 1.157, the same figure as the bench example in the lesson, because happens to be in both. Change THETA to 0.0 and run it again: the error disappears entirely, which is the test-fixture trap from question 6, reproduced in four lines.
What you can now do
You can name two frames, express one point in both, and convert in either direction with the rotate-then-translate rule and its inverse. You can derive the 2D rotation matrix from nothing but the definitions of sine and cosine, and explain why its columns are the images of the axes. You can show that reversing the two steps displaces the entire scene by a constant, and say why that makes it a bug people misdiagnose as calibration. And you can look at a line like and reject it on sight.