Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Chapter 114 — Affine Transformations

Prerequisites: ch109–113 (All Transforms)

Outcomes: Define affine transformations as linear + translation; Express all 2D transforms in homogeneous coordinates; Understand the 6 degrees of freedom in 2D affine maps; Apply to image warping and coordinate changes

The Affine Framework

An affine transformation is: x’ = A·x + t where A is a 2×2 matrix (linear part) and t is a translation vector.

Combined in homogeneous coordinates: [x’; y’; 1] = [[A₁₁, A₁₂, tx], [A₂₁, A₂₂, ty], [0, 0, 1]] · [x; y; 1]

This 3×3 matrix encodes all 2D transformations:

  • A = R(θ): rotation

  • A = S(sx,sy): scaling

  • A = F(θ): reflection

  • t = (dx,dy): translation

  • A = shear: A₁₂ or A₂₁ nonzero

6 degrees of freedom: 4 entries in A + 2 in t = 6 parameters.

Affine maps preserve:

  • Straight lines → straight lines

  • Parallel lines → parallel lines (ratio of lengths preserved along parallel lines)

  • NOT necessarily: angles, distances, or shapes

# --- Affine transformation framework ---
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-v0_8-whitegrid')

class AffineTransform:
    """2D affine transformation using 3×3 homogeneous matrix."""
    
    def __init__(self, matrix=None):
        self.M = matrix if matrix is not None else np.eye(3)
    
    @classmethod
    def rotation(cls, theta):
        c,s = np.cos(theta),np.sin(theta)
        return cls(np.array([[c,-s,0],[s,c,0],[0,0,1]]))
    
    @classmethod
    def translation(cls, dx, dy):
        return cls(np.array([[1,0,dx],[0,1,dy],[0,0,1]],dtype=float))
    
    @classmethod
    def scale(cls, sx, sy):
        return cls(np.array([[sx,0,0],[0,sy,0],[0,0,1]],dtype=float))
    
    @classmethod
    def shear(cls, shx=0, shy=0):
        return cls(np.array([[1,shx,0],[shy,1,0],[0,0,1]],dtype=float))
    
    def __matmul__(self, other):
        """Compose: self THEN other."""
        return AffineTransform(other.M @ self.M)
    
    def apply(self, points):
        pts = np.asarray(points, dtype=float)
        h = np.hstack([pts, np.ones((len(pts),1))])
        out = (self.M @ h.T).T
        return out[:,:2]
    
    def inverse(self):
        return AffineTransform(np.linalg.inv(self.M))

# Apply to the motivating problem from Part IV intro
theta = np.linspace(0, 2*np.pi, 6)[:-1]
pentagon = np.column_stack([np.cos(theta), np.sin(theta)])

# Chain: rotate 45° → scale x by 2 → translate (3,-1) → reflect across y=x
T_chain = (AffineTransform.rotation(np.radians(45)) @
           AffineTransform.scale(2, 1) @
           AffineTransform.translation(3, -1) @
           AffineTransform(np.array([[0,1,0],[1,0,0],[0,0,1]])))  # reflect y=x

result = T_chain.apply(pentagon)

fig, ax = plt.subplots(figsize=(10, 7))
closed = lambda p: np.vstack([p,p[0]])
ax.fill(pentagon[:,0],pentagon[:,1],alpha=0.2,color='steelblue')
ax.plot(closed(pentagon)[:,0],closed(pentagon)[:,1],'steelblue',lw=2.5,label='Original')
ax.fill(result[:,0],result[:,1],alpha=0.2,color='crimson')
ax.plot(closed(result)[:,0],closed(result)[:,1],'crimson',lw=2.5,label='After all 4 transforms')
ax.set_aspect('equal'); ax.legend(fontsize=10)
ax.set_title('Solving the Part IV Motivating Problem\n(4 transforms in ONE matrix product)')
ax.axhline(0, color='k', linewidth=0.4); ax.axvline(0, color='k', linewidth=0.4)
plt.tight_layout(); plt.show()
print("Composite matrix:")
print(T_chain.M.round(4))
<Figure size 1000x700 with 1 Axes>
Composite matrix:
[[ 0.7071  0.7071 -1.    ]
 [ 1.4142 -1.4142  3.    ]
 [ 0.      0.      1.    ]]

Image Warping

# --- Affine image warp demo ---
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-v0_8-whitegrid')

# Create synthetic 'image' (checkerboard)
N = 60
img = np.zeros((N, N))
for i in range(N):
    for j in range(N):
        img[i,j] = (i//10 + j//10) % 2

# Affine warp: rotation 15° + slight shear
theta = np.radians(15)
c, s = np.cos(theta), np.sin(theta)
# Map output pixel → input pixel (inverse warp)
A_inv = np.linalg.inv(np.array([[c,-s],[s,c]])) @ np.array([[1,0.2],[0,1]])

cx, cy = N/2, N/2
warped = np.zeros_like(img)
for oy in range(N):
    for ox in range(N):
        src = A_inv @ np.array([ox-cx, oy-cy]) + np.array([cx, cy])
        sx, sy = int(round(src[0])), int(round(src[1]))
        if 0 <= sx < N and 0 <= sy < N:
            warped[oy, ox] = img[sy, sx]

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(img, cmap='gray', origin='lower'); axes[0].set_title('Original')
axes[1].imshow(warped, cmap='gray', origin='lower'); axes[1].set_title('Affine Warped (rotate+shear)')
plt.suptitle('Affine Image Warping', fontsize=12, fontweight='bold')
plt.tight_layout(); plt.show()
<Figure size 1000x400 with 2 Axes>

Summary

  • Affine transform: x’ = Ax + t — combines any linear map with translation

  • 3×3 homogeneous matrix unifies all 2D transforms in one product

  • 6 DOF: 4 matrix entries + 2 translation values

  • Affine preserves lines and parallelism; projective transforms (beyond this book) add perspective

This chapter resolves the Part IV motivating problem completely.

Forward: ch164 (Linear Transformations) generalizes to N dimensions; ch175 (PCA) uses orthogonal transformations (rotations) to find principal axes.