Module 6 · Lesson 5

Capstone: Image Manipulation with NumPy

A grayscale image is nothing more than a 2-D array of brightness values (0-255). Every “image processing” operation below is really just array indexing, broadcasting, and boolean masking — the exact tools from Modules 1-3, no special image library required.

Below we synthesize a small 40×40 image containing a bright square and a bright circle on a dark background, purely from array operations (no file is loaded, so this runs identically anywhere, including Colab).

rng = np.random.default_rng(seed=7)

size = 40
img = rng.integers(0, 40, size=(size, size)).astype(np.uint8)  # dim background noise

# bright square: rows/cols 5..15
img[5:15, 5:15] = 220

# bright circle of radius 10 centered at (28, 28)
yy, xx = np.indices((size, size))
circle_mask = (yy - 28) ** 2 + (xx - 28) ** 2 <= 10 ** 2
img[circle_mask] = 200

plt.imshow(img, cmap="gray", vmin=0, vmax=255)
plt.title("Synthetic grayscale image")
plt.colorbar()
plt.show()

Output:

plot output

Every slice you write, like img[5:15, 5:15] above, is really Python building a slice(5, 15) object per axis and handing a tuple of them to img.__getitem__. Because that’s just an object, you can construct it yourself, store it in a variable, and use it later — row_slice = slice(5, 15) then img[row_slice] is identical to img[5:15]. For multiple axes, a tuple of slices works the same way: (slice(5, 15), slice(0, 10)) is the same index as img[5:15, 0:10]. The exercise below asks you to accept a crop_slice built this way, so the caller can crop to whatever region it wants.

# `a:b` syntax and an explicit slice object are the same thing.
row_slice = slice(5, 15)
print(np.array_equal(img[5:15, 5:15], img[row_slice, row_slice]))

# A tuple of slices works for multi-dimensional indexing too.
region = (slice(5, 15), slice(0, 10))
print(np.array_equal(img[region], img[5:15, 0:10]))

Output:

True
True
5.1

Threshold, crop, and measure brightness

Write analyze_image(img, threshold, crop_slice) that performs three chained steps on a 2-D grayscale array img:

  1. Build a boolean "mask" the same shape as img where pixels are strictly greater than threshold.
  2. Build a "crop": the sub-array img[crop_slice], where crop_slice is a tuple of two slice objects (one per axis) — e.g. (slice(0, 10), slice(0, 10)).
  3. Compute "bright_fraction": the fraction (a Python float between 0 and 1) of pixels within that crop that are strictly greater than threshold.

Return a dict with exactly the keys "mask", "crop", and "bright_fraction".

💡 Hint

mask = img > threshold. crop = img[crop_slice]. For the fraction, reuse the mask but computed on the crop (or slice the mask itself with crop_slice) and take .mean() of the boolean array — the mean of a boolean array IS the fraction of True values.

✅ Solution
def analyze_image(img, threshold, crop_slice):
    mask = img > threshold
    crop = img[crop_slice]
    bright_fraction = float((crop > threshold).mean())
    return {"mask": mask, "crop": crop, "bright_fraction": bright_fraction}