Image Transformations: Affine, Rotation, Homography
Every photo warp is a small matrix times a point. Translation slides it, rotation turns it, and only a homography can tilt a square into a trapezoid.
Why Does This Exist?
Photos never line up by themselves. A phone tilts, a drone drifts, and a scanned page skews, so pixels from two views sit in different coordinate frames. Without a warp model there is no panorama, no stabilization, and no measurement from images.
All warps share one language. Points become homogeneous triples , warps become matrices, and the output is the matrix times the point, divided by the last coordinate. That language feeds homogeneous coordinates and camera models, and resampling the warped grid needs interpolation.
The one idea you need first
Append a to every point. Translation is not linear in , but it is linear in because the extra column carries the shift. The last row decides the family: keeps parallels parallel, anything else lets them meet.
Think of It Like This
Sliding tracing paper versus tilting a projector
Copy a square onto tracing paper and slide it: sides stay parallel and lengths stay equal. Spin the paper and the square rotates. Those are affine moves. Now project the same slide onto a tilted wall: the near edge grows and parallels meet. That tilt is a homography.
It stops at depth. Paper and projector assume one flat sheet. Real scenes have multiple depths, where one global matrix cannot align foreground and background at once.
How It Actually Works
Write a point as and a warp as , then divide by the third entry. Translation uses ones on the diagonal and shifts in the last column. Rotation by angle uses and in the top left block. Scale multiplies axes by and . Affine adds shear with six free numbers and keeps its last row . Homography frees the last row, so division by bends lines.
In plain words: shifts right, shifts down, and turn the axes, and and control perspective tilt. Every symbol is a knob with units in pixels except the rotation entries, which are unitless cosines and sines.
Worked example
Translate by and : , . The point lands at .
Rotate by degrees counterclockwise. Here and , so and . The point lands at , exactly a quarter turn.
Apply perspective with rows , , to . In homogeneous form maps to . Divide: , . An affine matrix would leave the denominator at and keep parallels parallel. The is the whole difference, and the diagram draws this beside the trapezoid.
Code
The point is the divide. Forgetting the homogeneous normalization applies the tilt to the numerator and skips it in the denominator.
import numpy as np
def warp_point(H: np.ndarray, pt: np.ndarray) -> np.ndarray: p = np.array([pt[0], pt[1], 1.0]) q = H @ p return q[:2] / q[2] # divide by w
H_affine = np.array([[0.0, -1.0, 5.0], [1.0, 0.0, -3.0], [0.0, 0.0, 1.0]])print(warp_point(H_affine, np.array([3.0, 1.0]))) # -> [ 4. -0.]H_persp = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.1, 0.0, 1.0]])print(np.round(warp_point(H_persp, np.array([2.0, 1.0])), 2)) # -> [1.67 0.83]The first line chains the worked rotation with the worked translation: turns to then shifts to . The second line reproduces and from the hand computation.
Watch Out For
Multiplying by H and forgetting to divide by w
Affine habits carry over: apply a homography and use the first two outputs directly. The symptom is a warp that looks almost right near the origin but stretches wildly at the image edge, because versus changes the answer by a factor of two. Always normalize by the third coordinate before comparing against pixel positions.
Sampling warped coordinates with nearest neighbour by default
A warp lands at fractional pixels such as . Rounding to is nearest sampling, and it jitters straight lines into staircases, especially after downscaling. The symptom is shimmering edges in video stabilization. Use bilinear interpolation for photos, and budget the extra four taps per pixel, reserving nearest for label masks where blending classes would invent new ones.
The Quick Version
- Points are , warps are , output is divided by .
- Translation adds: with gives .
- Rotation by 90 degrees sends to minus .
- Affine keeps last row and preserves parallels.
- Homography frees the last row: with tilt gives and bends parallels.
- Divide by every time, and interpolate rather than round fractional positions.