Skip to content
AI360XpertBeta

Image Transformations and Homography

Affine maps resize, rotate, and shear images while keeping parallel lines parallel, and a homography adds perspective so tilted planes flatten back out.

An affine map turns the square into a parallelogram with parallels preserved, while a homography turns it into a trapezoid whose divide by w models perspective
An affine map turns the square into a parallelogram with parallels preserved, while a homography turns it into a trapezoid whose divide by w models perspective

Why Does This Exist?

Cameras rarely face their subjects squarely. A photo of a document from an angle turns its rectangle into a trapezoid, a rotated photo needs straightening, and a thumbnail needs resizing. The naive fix treats each case separately, and it breaks the moment views combine: rotation plus perspective cannot be undone by shifting pixels around. All of these edits are one family of geometric maps from pixel coordinates to pixel coordinates, which is why panorama stitching and 3D pipelines like structure from motion build on them. Point fitting details live in homography estimation; this page covers what each map can and cannot do.

Think of It Like This

Stretching rubber over a frame

Think of an image as a flexible rubber sheet with a picture drawn on it. Affine moves stretch the sheet uniformly, rotate it, or slide it across the table: parallel lines stay parallel, so a square becomes a parallelogram but never tapers. A homography tilts the sheet into the third dimension before photographing it, so far parts shrink and parallels converge.

Where it stops: real rubber also stretches unevenly and wrinkles, while these maps are globally rigid formulas with 6 and 8 degrees of freedom. They model one flat plane seen from two viewpoints, never curved surfaces or lens bulge.

How It Actually Works

Affine maps with six degrees of freedom

Affine maps cover translation, scaling, rotation, and shear. They preserve straight lines and parallelism: a rectangle becomes a parallelogram, never a trapezoid. In homogeneous coordinates (x,y,1)(x, y, 1) every affine map is one 2×32 \times 3 matrix MM:

[x′y′]=[m11m12txm21m22ty][xy1]\begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} m_{11} & m_{12} & t_x \\ m_{21} & m_{22} & t_y \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}

The mm entries control rotation, scale, and shear while txt_x and tyt_y translate. Six unknowns need exactly three point pairs to solve.

Homography with the perspective divide

A homography maps one plane to another across viewpoints with a 3×33 \times 3 matrix HH and eight degrees of freedom (nine entries minus one overall scale):

[x′y′w′]=[h11h12h13h21h22h23h31h32h33][xy1],xnew=x′w′,ynew=y′w′\begin{bmatrix} x' \\ y' \\ w' \end{bmatrix} = \begin{bmatrix} h_{11} & h_{12} & h_{13} \\ h_{21} & h_{22} & h_{23} \\ h_{31} & h_{32} & h_{33} \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}, \quad x_{new} = \frac{x'}{w'}, \quad y_{new} = \frac{y'}{w'}

Dividing by w′w' is the perspective step: points farther away get larger w′w' and shrink. Four point pairs determine HH, solved in practice with RANSAC over many candidate matches.

Interpolation fills the grid

Mapped coordinates land between pixels, for example (14.3,8.9)(14.3, 8.9), so the warper must estimate values on the integer grid. Nearest-neighbor snaps to the closest pixel (fast, blocky), bilinear averages the 44 neighbors (the standard default), and bicubic fits the 1616 neighbors (smoothest, costliest). Libraries loop over the destination grid and pull from the source (inverse warping) so no holes remain.

Worked example

Translate the point (10,20)(10, 20) by (tx,ty)=(5,−3)(t_x, t_y) = (5, -3) with the affine matrix whose rows are [1,0,5][1, 0, 5] and [0,1,−3][0, 1, -3]. Then x′=1×10+0×20+5=15x' = 1 \times 10 + 0 \times 20 + 5 = 15 and y′=0×10+1×20+(−3)=17y' = 0 \times 10 + 1 \times 20 + (-3) = 17, so (10,20)(10, 20) lands on (15,17)(15, 17). Now apply the uniform scale homography H=2IH = 2I: it sends (60,40,1)(60, 40, 1) to (x′,y′,w′)=(120,80,2)(x', y', w') = (120, 80, 2). Dividing gives xnew=120/2=60x_{new} = 120 / 2 = 60 and ynew=80/2=40y_{new} = 80 / 2 = 40: uniform scale holds the position after the divide, and the lesson is the mechanism, not the motion. Change HH to rows [2,0,0][2, 0, 0], [0,2,0][0, 2, 0], [0.01,0,1][0.01, 0, 1] and the same (60,40,1)(60, 40, 1) yields w′=0.01×60+1=1.6w' = 0.01 \times 60 + 1 = 1.6, so xnew=120/1.6=75x_{new} = 120 / 1.6 = 75 and ynew=80/1.6=50y_{new} = 80 / 1.6 = 50: depth-dependent w′w' shifts the coordinate, which is exactly the perspective effect affine maps cannot produce. The snippet below runs the second case.

Code

The point of this snippet is the worked example's two maps in plain Python: the affine translation and the homography with its divide by w′w'.

# -> Affine translate of (10, 20), then a homography with divide by w.def affine(pt, m):    (x, y) = pt    return (m[0][0] * x + m[0][1] * y + m[0][2],            m[1][0] * x + m[1][1] * y + m[1][2])
def homography(pt, h):    (x, y) = pt    xp = h[0][0] * x + h[0][1] * y + h[0][2]    yp = h[1][0] * x + h[1][1] * y + h[1][2]    w = h[2][0] * x + h[2][1] * y + h[2][2]    return (xp / w, yp / w)
print(affine((10, 20), [[1, 0, 5], [0, 1, -3]]))# -> (15, 17)print(homography((60, 40), [[2, 0, 0], [0, 2, 0], [0.01, 0, 1]]))# -> (75.0, 50.0)

The affine call returns (15,17)(15, 17) and the homography call returns (75.0,50.0)(75.0, 50.0): with w′=1.6w' = 1.6, xnew=120/1.6=75x_{new} = 120 / 1.6 = 75 and ynew=80/1.6=50y_{new} = 80 / 1.6 = 50.

Watch Out For

Collinear points collapsing the solve

Three collinear points never determine an affine map and four near-collinear points produce a wild homography, because the equations stop being independent. The symptom is extreme skew or numeric blowup from innocent-looking clicks. Fix: spread calibration points across the plane, covering corners rather than lining one edge.

Forward warping leaving holes

Looping over source pixels and pushing them forward lands values between destination grid points, leaving speckled holes. The symptom is a warped image with scattered empty pixels. Fix: loop over the destination grid and pull through the inverse map, which is what library warp functions do internally.

The Quick Version

  • Image warps map pixel coordinates to pixel coordinates with matrix multiplication in homogeneous form.
  • Affine maps use a 2×32 \times 3 matrix with 6 degrees of freedom, need 3 point pairs, and keep parallels parallel.
  • Homographies use a 3×33 \times 3 matrix with 8 degrees of freedom, need 4 point pairs, and divide by w′w' for perspective.
  • The worked translate sends (10,20)(10, 20) to (15,17)(15, 17), and the worked homography sends (60,40)(60, 40) to (75.0,50.0)(75.0, 50.0).
  • Non-integer landing spots need interpolation, and inverse warping avoids holes.