import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from matplotlib.transforms import Affine2D
plt.style.use("../figure_style.mplstyle")
A_scale = np.array([[2.0, 0.0],
[0.0, 0.5]])
ticks = np.linspace(-1.5, 1.5, 13)
points = np.array([[x, y] for x in ticks for y in ticks]).T
# Each column holds one point, so A @ points moves the whole grid.
def show_map(mapped, name):
fig, ax = plt.subplots(figsize=(7, 4.2))
ax.scatter(points[0], points[1], s=9, marker="o", edgecolors="none", label="Original grid")
ax.scatter(mapped[0], mapped[1], s=9, marker="s", edgecolors="none", label=f"After {name.lower()}")
ax.set_xlim(-3.4, 3.4)
ax.set_ylim(-2.4, 2.4)
ax.set_aspect("equal")
ax.set_title(name)
ax.legend(frameon=False, loc="upper center", bbox_to_anchor=(0.5, -0.05), ncol=2)
plt.show()Demo: A Matrix Moves the Plane
A matrix applies the same coordinate rule to every point. We will compare scaling, shear, and rotation on a grid, then apply those three maps to an image.
Question 1: What Does Scaling Change?
The matrix \(\mathbf A^{\mathrm{scale}}\) doubles the first coordinate and halves the second. Predict how the square grid will change.
The plotting helper only draws two sets of points. This matrix multiplication computes the transformed points.
scaled_points = A_scale @ points
show_map(scaled_points, "Scaling")
Answer. The grid stretches horizontally by \(2\) and compresses vertically by \(1/2\). Points on the coordinate axes stay on those axes.
Question 2: How Does a Shear Mix Coordinates?
The matrix \(\mathbf A^{\mathrm{shear}}\) sends \((x_1,x_2)^\top\) to \((x_1+x_2,x_2)^\top\). Predict which grid lines remain fixed and which ones slant.
A_shear = np.array([[1.0, 1.0],
[0.0, 1.0]])
sheared_points = A_shear @ points
show_map(sheared_points, "Shear")
Answer. Horizontal grid lines stay horizontal. A point moves sideways by its second coordinate, so vertical grid lines slant while their heights stay fixed.
Question 3: What Does a 45-Degree Rotation Preserve?
Every entry of \(\mathbf A^{\mathrm{rot}}\) has magnitude \(1/\sqrt{2}\). Predict how the grid will move and whether its lengths and angles will change.
A_rot = (1 / np.sqrt(2)) * np.array([[1.0, -1.0],
[1.0, 1.0]])
rotated_points = A_rot @ points
show_map(rotated_points, "45-Degree Rotation")
Answer. Every point rotates counterclockwise by \(45^\circ\). The grid turns while preserving lengths, angles, and spacing.
Question 4: How Do the Three Maps Change an Image?
Every pixel has a coordinate. Matplotlib can apply the same matrix to a displayed image. Its Affine2D display transform uses a \(3\times3\) matrix: we place our \(2\times2\) map in the upper-left corner and leave translation at zero. The image is cropped to the original viewing window, so pixels moved outside that window disappear. Predict which visual features will reveal scaling, shear, and rotation most clearly.
image = plt.imread(cbook.get_sample_data("grace_hopper.jpg"))[::2, ::2]
height, width = image.shape[:2]
# An alpha channel leaves empty space around the transformed image transparent.
alpha = np.full((height, width), 255, dtype=np.uint8)
image = np.dstack([image, alpha])
extent = (-width / 2, width / 2, -height / 2, height / 2)
maps = [np.eye(2), A_scale, A_shear, A_rot]
names = ["Original", "Scaling", "Shear", "45-Degree Rotation"]
fig, axes = plt.subplots(1, 4, figsize=(9, 3.2))
for ax, A, name in zip(axes, maps, names):
image_matrix = np.eye(3)
image_matrix[:2, :2] = A
transform = Affine2D(image_matrix) + ax.transData
ax.imshow(image, extent=extent, transform=transform)
ax.set_xlim(-width / 2, width / 2)
ax.set_ylim(-height / 2, height / 2)
ax.set_title(name)
ax.axis("off")
plt.show()
Answer. Scaling changes width and height separately, shear shifts each row sideways, and rotation turns the entire image by \(45^\circ\) while preserving its shape. The same coordinate rules that moved the grid now move every pixel.
The next topic studies special directions that a matrix stretches without turning.