Exercise solution
Completion requirements
Code:
import numpy as np
from PIL import Image
def save_image(arr, filename):
"""Save a numpy array as an image file."""
Image.fromarray(arr.astype("uint8")).save(filename)
# ── Exercise 1: Why NumPy? ────────────────────────────────────────────────────
print("=" * 60)
print("Exercise 1: Why NumPy?")
print("=" * 60)
# (a) list * 2 repeats the list, it does NOT multiply each element
l = [1, 2, 3, 4]
print("(a) l * 2 =", l * 2)
# Output: [1, 2, 3, 4, 1, 2, 3, 4] — the list is duplicated, not scaled
# (b) l + 5 raises TypeError: can only concatenate list (not "int") to list
# Python lists only support + with another list, not with a number.
# (uncomment to see the error)
# print(l + 5)
# (c) NumPy element-wise operations
a = np.array([1, 2, 3, 4])
print("(c) a * 2 =", a * 2) # [2, 4, 6, 8]
print("(c) a + 5 =", a + 5) # [6, 7, 8, 9]
# (d) Written answer:
# NumPy arrays support element-wise arithmetic directly, whereas Python lists
# treat * as repetition and + as concatenation, making them unsuitable for
# numerical computations on images.
# ── Exercise 2: Array Shape and Dimensions ────────────────────────────────────
print("\n" + "=" * 60)
print("Exercise 2: Array Shape and Dimensions")
print("=" * 60)
# (a) 1D array
a = np.array([10, 20, 30, 40, 50])
print("(a) shape:", a.shape) # (5,)
print("(a) dtype:", a.dtype) # int64 (platform-dependent)
# (b) 2D array — 2 rows, 3 columns
m = np.array([[1, 2, 3],
[4, 5, 6]])
print("(b) shape:", m.shape) # (2, 3)
# 2 rows, 3 columns
# (c) Pre-filled arrays
z = np.zeros((4, 5))
o = np.ones((3, 3))
f = np.full((3, 3), 128)
print("(c) np.zeros dtype:", z.dtype) # float64 — the default
print("(c) np.full:\n", f)
# (d) int16 grayscale image
gray = np.zeros((4, 6), dtype=np.int16)
print("(d) dtype:", gray.dtype) # int16
print("(d) shape:", gray.shape) # (4, 6)
# int16 is used because blur computations may produce values outside [0, 255]
# and can be negative; uint8 would silently wrap around.
# ── Exercise 3: Indexing and Slicing ─────────────────────────────────────────
print("\n" + "=" * 60)
print("Exercise 3: Indexing and Slicing")
print("=" * 60)
m = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
# (a) Individual element access
print("(a) m[1,1] =", m[1, 1]) # 50
print("(a) m[0,2] =", m[0, 2]) # 30
print("(a) m[2,0] =", m[2, 0]) # 70
# (b) Rows and columns
print("(b) first row: ", m[0, :]) # [10, 20, 30]
print("(b) second column: ", m[:, 1]) # [20, 50, 80]
print("(b) last row: ", m[-1, :]) # [70, 80, 90]
# (c) Rectangular sub-region
sub = m[1:3, 1:3]
print("(c) sub-region:\n", sub)
# [[50, 60],
# [80, 90]]
# (d) Write to a region — top-left 2×2 block becomes 0
m_copy = m.copy()
m_copy[0:2, 0:2] = 0
print("(d) after writing 0 to [0:2, 0:2]:\n", m_copy)
# The four values m[0,0]=10, m[0,1]=20, m[1,0]=40, m[1,1]=50 are now 0.
# (e) Self-check slices on a 5×5 array
a5 = np.arange(25).reshape(5, 5)
print("(e) 5×5 array:\n", a5)
print("(e) second row: ", a5[1, :])
print("(e) last two columns:\n", a5[:, 3:])
print("(e) 3×3 block at [1:4, 1:4]:\n", a5[1:4, 1:4])
# ── Exercise 4: Element-Wise Operations and Comparing Arrays ──────────────────
print("\n" + "=" * 60)
print("Exercise 4: Element-Wise Operations and Comparing Arrays")
print("=" * 60)
# (a) Arithmetic
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
print("(a) a + b =", a + b) # [11, 22, 33]
print("(a) a * b =", a * b) # [10, 40, 90]
print("(a) b - a =", b - a) # [ 9, 18, 27]
# (b) Subtracting two image regions — result can be negative
img1 = np.array([[100, 150],
[200, 50]], dtype=np.int16)
img2 = np.array([[ 80, 160],
[210, 40]], dtype=np.int16)
diff = img1 - img2
print("(b) diff:\n", diff)
# [[ 20, -10],
# [-10, 10]]
# With uint8: 150 - 160 would wrap around to 246 (256 + 150 - 160 = 246)
# because uint8 cannot represent negative values — the result would be wrong.
# (c) Absolute difference
abs_diff = np.abs(img1 - img2)
print("(c) abs_diff:\n", abs_diff)
# [[20, 10],
# [10, 10]]
# (d) Mean
print("(d) mean of abs_diff:", np.mean(abs_diff)) # 12.5
m = np.array([[1, 2, 3],
[4, 5, 6]])
print("(d) mean of all elements:", np.mean(m)) # 3.5
print("(d) mean of first row: ", np.mean(m[0, :])) # 2.0
# (e) Similarity score
zone1 = np.array([[100, 120, 130]], dtype=np.int16)
zone2 = np.array([[ 90, 115, 135]], dtype=np.int16)
score = float(np.mean(np.abs(zone1 - zone2))) / 255.0
print("(e) score (different):", score) # ≈ 0.033
# Verify: identical zones → score = 0.0
zone_same = np.array([[100, 120]], dtype=np.int16)
print("(e) score (identical):", float(np.mean(np.abs(zone_same - zone_same))) / 255.0) # 0.0
# Verify: all-0 vs all-255 → score = 1.0
zeros = np.array([[0, 0]], dtype=np.int16)
maxv = np.array([[255, 255]], dtype=np.int16)
print("(e) score (max diff): ", float(np.mean(np.abs(zeros - maxv))) / 255.0) # 1.0
# ── Exercise 5: Creating and Drawing on Grayscale Images ─────────────────────
print("\n" + "=" * 60)
print("Exercise 5: Creating and Drawing on Grayscale Images")
print("=" * 60)
# (a) Black image
img = np.zeros((10, 10), dtype=np.uint8)
save_image(img, "black.png")
print("(a) Saved black.png — a fully black 10×10 image.")
# (b) Vertical gradient
img = np.zeros((10, 10), dtype=np.uint8)
for row in range(10):
for col in range(10):
img[row, col] = (row + 1) * 20
save_image(img, "gradient.png")
print("(b) Saved gradient.png — gradient goes top (dark) to bottom (bright).")
print(" To make it horizontal: use (col + 1) * 20 instead of (row + 1) * 20.")
# (c) Rectangle using loops
img = np.zeros((10, 10), dtype=np.uint8)
for col in range(2, 8):
img[2, col] = 255
for col in range(2, 8):
img[7, col] = 255
for row in range(3, 7):
img[row, 2] = 255
for row in range(3, 7):
img[row, 7] = 255
save_image(img, "box_loops.png")
print("(c) Saved box_loops.png")
# (d) Same rectangle using slices — shorter and faster
img = np.zeros((10, 10), dtype=np.uint8)
img[2, 2:8] = 255 # top edge
img[7, 2:8] = 255 # bottom edge
img[3:7, 2] = 255 # left edge
img[3:7, 7] = 255 # right edge
save_image(img, "box_slices.png")
print("(d) Saved box_slices.png — identical result, 4 lines vs 8 lines.")
# ── Exercise 6: Colour Images ─────────────────────────────────────────────────
print("\n" + "=" * 60)
print("Exercise 6: Colour Images")
print("=" * 60)
# (a) Black colour image
img = np.zeros((250, 250, 3), dtype=np.uint8)
print("(a) shape:", img.shape) # (250, 250, 3)
print("(a) centre pixel:", img[125, 125]) # [0, 0, 0]
# (b) RGB gradient
for row in range(250):
for col in range(250):
img[row, col] = [row, 0, col]
save_image(img, "gradient_rgb.png")
print("(b) Saved gradient_rgb.png")
print(" top-left (0,0): black [0, 0, 0]")
print(" top-right (0,249): blue [0, 0, 249]")
print(" bot-left (249,0): red [249, 0, 0]")
# (c) Extract a channel
red_channel = img[:, :, 0]
blue_channel = img[:, :, 2] # ← answer: index 2 for blue
print("(c) red channel shape: ", red_channel.shape) # (250, 250)
print("(c) blue channel shape:", blue_channel.shape) # (250, 250)
# (d) Zero out the green channel
img[:, :, 1] = 0
save_image(img, "no_green.png")
print("(d) Saved no_green.png — green channel removed.")
# (e) Modify a single pixel
print("(e) img[0,0] before:", img[0, 0])
img[0, 0] = [255, 255, 0] # pure yellow
print("(e) img[0,0] after: ", img[0, 0]) # [255, 255, 0]
Last modified: Monday, 20 April 2026, 1:37 PM
