Objective: Implement 2D convolution with configurable padding and stride from raw NumPy, then apply a small bank of hand-designed filters to a synthetic image and inspect what each one detects. All in runnable, in-browser Python.
Task 1: Build a Synthetic Test Image
A simple synthetic shape (a square on a flat background) makes it easy to predict, and then verify, what an edge detector should produce.
import numpy as np
import matplotlib.pyplot as plt
image = np.zeros((20, 20))
image[6:14, 6:14] = 1.0
plt.imshow(image, cmap="gray")
plt.title("Synthetic test image: a bright square")
show_plot()
Task 2: Implement Convolution With Padding and Stride
import numpy as np
def convolve2d(img, kernel, stride=1, padding=0):
if padding > 0:
img = np.pad(img, pad_width=padding, mode="constant", constant_values=0)
kh, kw = kernel.shape
out_h = (img.shape[0] - kh) // stride + 1
out_w = (img.shape[1] - kw) // stride + 1
out = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
r, c = i * stride, j * stride
out[i, j] = np.sum(img[r:r+kh, c:c+kw] * kernel)
return out
image = np.zeros((20, 20))
image[6:14, 6:14] = 1.0
kernel = np.ones((3, 3)) / 9
no_pad = convolve2d(image, kernel, stride=1, padding=0)
same_pad = convolve2d(image, kernel, stride=1, padding=1)
strided = convolve2d(image, kernel, stride=2, padding=0)
print("No padding: ", image.shape, "->", no_pad.shape)
print("Same padding: ", image.shape, "->", same_pad.shape)
print("Stride 2: ", image.shape, "->", strided.shape)
Task 3: A Bank of Hand-Designed Filters
Apply a vertical-edge filter, a horizontal-edge filter, and a blur filter to the same image, and compare the results.
import numpy as np
import matplotlib.pyplot as plt
def convolve2d(img, kernel, stride=1, padding=0):
if padding > 0:
img = np.pad(img, pad_width=padding, mode="constant", constant_values=0)
kh, kw = kernel.shape
out_h = (img.shape[0] - kh) // stride + 1
out_w = (img.shape[1] - kw) // stride + 1
out = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
r, c = i * stride, j * stride
out[i, j] = np.sum(img[r:r+kh, c:c+kw] * kernel)
return out
image = np.zeros((20, 20))
image[6:14, 6:14] = 1.0
vertical_edge = np.array([[1, 0, -1], [1, 0, -1], [1, 0, -1]], dtype=float)
horizontal_edge = vertical_edge.T
blur = np.ones((3, 3)) / 9
fig, axes = plt.subplots(1, 4, figsize=(13, 3.5))
axes[0].imshow(image, cmap="gray"); axes[0].set_title("Original")
for ax, kernel, title in zip(
axes[1:], [vertical_edge, horizontal_edge, blur],
["Vertical edges", "Horizontal edges", "Blur"],
):
out = convolve2d(image, kernel, padding=1)
ax.imshow(out, cmap="gray"); ax.set_title(title)
for ax in axes:
ax.axis("off")
plt.tight_layout()
show_plot()
The vertical-edge filter should light up strongly along the square's left and right borders (where brightness changes left-to-right), the horizontal-edge filter along the top and bottom borders, and the blur filter should produce a softened, smoothed-out version of the square with no sharp edges at all.
Task 4: Verify Against a Known Case by Hand
Pick a single output position and confirm the convolution arithmetic by computing it manually, a useful habit whenever you're not fully sure your implementation is correct.
import numpy as np
def convolve2d(img, kernel, stride=1, padding=0):
if padding > 0:
img = np.pad(img, pad_width=padding, mode="constant", constant_values=0)
kh, kw = kernel.shape
out_h = (img.shape[0] - kh) // stride + 1
out_w = (img.shape[1] - kw) // stride + 1
out = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
r, c = i * stride, j * stride
out[i, j] = np.sum(img[r:r+kh, c:c+kw] * kernel)
return out
small_image = np.array([
[0, 0, 1, 1],
[0, 0, 1, 1],
[0, 0, 1, 1],
[0, 0, 1, 1],
], dtype=float)
vertical_edge = np.array([[1, 0, -1], [1, 0, -1], [1, 0, -1]], dtype=float)
result = convolve2d(small_image, vertical_edge)
# Manual check at position (0, 0): patch is the top-left 3x3 block
patch = small_image[0:3, 0:3]
manual_value = np.sum(patch * vertical_edge)
print("Full result:\n", result)
print("\nPatch at (0,0):\n", patch)
print("Manual computation at (0,0):", manual_value)
print("Function's result at (0,0): ", result[0, 0])
print("Match:", np.isclose(manual_value, result[0, 0]))