Project: images are arrays
Why this matters
Every image a vision model sees is an array like the ones you'll build here, and every image tool you've used (filters, crops, thumbnails) is array math underneath. This project makes NumPy concrete: you'll see slicing, broadcasting and masks as pictures. It also gives you a handful of functions you'll actually use, from thumbnails to watermarks to color palettes.
- Treat a picture as a (height, width, 3) array of numbers from 0 to 255
- Draw shapes with boolean masks, then flip, crop and turn an image gray with slicing and math
- Brighten an image without numbers wrapping around past 255
- Use Pillow to add text and save a file, and find an image's most common colors
The brief
The practice shop wants product shots processed before they go on the website: turned gray for a print catalog, brightened, cropped square for thumbnails, stamped with a watermark, and summed up as a color palette. You'll draw a test image in code, then write one function for each job.
A picture is a grid of numbers
A color image is a 3-D array with the shape (height, width, 3). Each pixel holds three numbers: how much red, green and blue light it gives off, from 0 (none) to 255 (full). The type is uint8, an "unsigned 8-bit integer", which holds exactly the whole numbers 0 to 255.
import matplotlib.pyplot as plt import numpy as np tiny = np.array([ [[255, 0, 0], [0, 255, 0], [0, 0, 255]], [[255, 255, 255], [128, 128, 128], [0, 0, 0]], ], dtype=np.uint8) print(tiny.shape, tiny.dtype) print(tiny[0, 2]) # row 0, column 2: the blue pixel fig, ax = plt.subplots(figsize=(3, 2)) ax.imshow(tiny) ax.axis("off")
Row 0 is the top of the picture. Red, green and blue at full strength make white; all three at 0 make black; equal amounts in between make gray. imshow draws an array as an image, and axis("off") hides the tick marks, which mean nothing for a photo.
Drawing a scene with masks
To draw a shape, make a mask: a True/False array with one value per pixel, True inside the shape. Assigning to img[mask] colors every True pixel at once, with no loop over pixels at all. The trick for building masks is a column of row numbers and a row of column numbers. Broadcasting compares them into a full grid:
import matplotlib.pyplot as plt import numpy as np def make_scene(h=120, w=160): img = np.zeros((h, w, 3), dtype=np.uint8) rows = np.arange(h)[:, None] # shape (h, 1): 0, 1, 2 ... down the side cols = np.arange(w)[None, :] # shape (1, w): 0, 1, 2 ... along the top img[:, :] = (150, 200, 235) # sky everywhere img[h * 2 // 3:, :] = (70, 130, 60) # grass on the bottom third sun = (rows - 25) ** 2 + (cols - 130) ** 2 <= 12 ** 2 img[sun] = (250, 215, 80) tent = (rows >= 30) & (rows <= 100) & (abs(cols - 70) <= (rows - 30) * 0.8) img[tent] = (230, 120, 40) return img scene = make_scene() print(scene.shape) fig, ax = plt.subplots(figsize=(4, 3)) ax.imshow(scene) ax.axis("off")
The sun is every pixel within 12 of a center point, the formula for a circle. The tent is a triangle: the further down a row is, the wider the band of columns it covers. Both masks have the shape (120, 160), one value per pixel, so img[sun] picks exactly the pixels to paint.