CVML Coding & Visualisation Practice¶
Consolidated, runnable code snippets for every chapter, with
Purpose / Input / Code / Output / Visualization / Explanation / Real-life meaning / Exam relevance.
Imports assumed throughout:
Chapter 01 — Image Acquisition¶
1.1 BGR↔RGB and channel swap¶
Purpose:
Demonstrate BGR (OpenCV) ↔ RGB (matplotlib) conversion and red/blue channel swap (Sheet 2 Task 4).
Input:
Color image (any size, 3 channels, uint8).
Code:
img = cv2.imread('hummingbird.png') # BGR
print(img.shape, img.dtype) # (H, W, 3), uint8
def show(img_bgr, title=''):
plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
plt.title(title); plt.axis('off'); plt.show()
show(img, 'original')
swapped = img.copy(); swapped[..., [0, 2]] = swapped[..., [2, 0]]
cv2.imwrite('blue_hummingbird.png', swapped)
show(swapped, 'red↔blue swap')
Output:
Two figures — original and red/blue-swapped image.
Visualization:
side-by-side via plt.subplot(1, 2, ...).
Explanation:
OpenCV stores BGR; matplotlib expects RGB. Channel swap turns red into blue.
Real-life meaning:
Every CV pipeline starts with this conversion; ignoring it produces "wrong-coloured" images.
Exam relevance:
Conceptual: "Why does an OpenCV image look 'blue' in matplotlib?".
1.2 HSV hue offset¶
def hue_shift(img_bgr, deg):
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.int16)
hsv[..., 0] = (hsv[..., 0] + deg // 2) % 180 # OpenCV H ∈ [0,180]
return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
for deg in [0, 60, 120, 180]:
show(hue_shift(img, deg), f'hue {deg}°')
Output: Sequence of recoloured images.
Visualization: Row of four images.
Explanation: OpenCV stores hue as [0,180] (half-degree); we shift modulo 180.
Real-life meaning: Photoshop's Hue/Saturation slider works exactly this way.
Exam relevance: Trial-exam Q1a — visually identify "hue-shift".
1.3 Pinhole projection (matches trial-exam Q4a)¶
def project(P, f):
x, y, z = P
return f * x / z, f * y / z
print(project((10, 5, 3), 2)) # (6.667, 3.333)
Output: tuple (p'_x, p'_y).
Visualization: Plot the 3-D point and its image projection on a sketch.
Real-life meaning: All 3-D-to-2-D rendering uses this.
Exam relevance: Trial-exam Q4a.
1.4 Add Gaussian noise & visualise¶
def add_gauss(img, sigma=0.03):
f = img.astype(np.float32) / 255.
noisy = f + np.random.normal(0, sigma, f.shape)
return np.clip(noisy, 0, 1)
plt.subplot(1, 3, 1); plt.imshow(img[..., ::-1]); plt.title('clean')
plt.subplot(1, 3, 2); plt.imshow(add_gauss(img, 0.03)[..., ::-1]); plt.title('σ=0.03')
plt.subplot(1, 3, 3); plt.imshow(add_gauss(img, 0.10)[..., ::-1]); plt.title('σ=0.10')
plt.show()
Output: Three side-by-side images.
Visualization: Noise becomes more visible as σ increases.
Real-life meaning: Common noise model for image-acquisition simulation.
Exam relevance: Conceptual — Q on noise types.
Chapter 02 — Filtering, Edges, Thresholding¶
2.1 Box / Gaussian / Median filters (Sheet 3 Task 1)¶
Purpose: Compare three smoothing filters on a noisy image.
Input: Noisy image (uint8), kernel sizes.
noisy = cv2.imread('noisy.png')
box = cv2.boxFilter(noisy, -1, (5, 5))
gauss = cv2.GaussianBlur(noisy, (5, 5), 1.0)
median = cv2.medianBlur(noisy, 5)
for i, (name, im) in enumerate(zip(['noisy','box','gauss','median'], [noisy, box, gauss, median])):
plt.subplot(1, 4, i+1); plt.imshow(im[..., ::-1]); plt.title(name); plt.axis('off')
plt.show()
Output: 4 side-by-side images.
Explanation: Box is fast but causes ringing; Gaussian is smooth without ringing; median kills salt-and-pepper.
Real-life meaning: First step in many vision pipelines.
Exam relevance: Trial-exam Q2b-i.
2.2 Sobel from scratch (Sheet 3 Task 4)¶
def sobel_manual(gray):
gx = np.array([[-1,0,1],[-2,0,2],[-1,0,1]], np.float32)
gy = gx.T
Ix = cv2.filter2D(gray.astype(np.float32), -1, gx)
Iy = cv2.filter2D(gray.astype(np.float32), -1, gy)
G = np.sqrt(Ix**2 + Iy**2)
G = (G / G.max() * 255).astype(np.uint8)
return G, np.abs(Ix), np.abs(Iy)
gray = cv2.imread('lena.png', cv2.IMREAD_GRAYSCALE)
G, gx, gy = sobel_manual(gray)
plt.subplot(1,3,1); plt.imshow(gx, cmap='gray'); plt.title('|Ix|')
plt.subplot(1,3,2); plt.imshow(gy, cmap='gray'); plt.title('|Iy|')
plt.subplot(1,3,3); plt.imshow(G, cmap='gray'); plt.title('|∇I|')
plt.show()
Output: Three grayscale gradient images.
Real-life meaning: Almost every classical edge detector uses Sobel.
Exam relevance: Trial-exam Q2c, Q2d.
2.3 Convolution by hand (matches Q2c)¶
import scipy.signal as sg
I = np.array([
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,1,1,1,0,0,0],
[0,1,1,1,1,1,0,0],
[0,1,1,1,1,1,0,0],
[0,1,1,1,1,0,0,0],
[0,0,1,1,0,0,0,0],
[0,0,0,0,0,0,0,0]])
K = np.array([[-1, 0, 1]])
print(sg.convolve2d(I, K, mode='same', boundary='fill', fillvalue=0))
Output: 8×8 matrix matching Chapter 02 §4.2.
Real-life meaning: Demonstrates convolution semantics (kernel mirroring).
Exam relevance: Trial-exam Q2c.
2.4 Otsu vs adaptive threshold¶
gray = cv2.imread('text.png', cv2.IMREAD_GRAYSCALE)
_, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
adapt = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 25, 10)
plt.subplot(1,3,1); plt.imshow(gray, cmap='gray'); plt.title('input')
plt.subplot(1,3,2); plt.imshow(otsu, cmap='gray'); plt.title('Otsu')
plt.subplot(1,3,3); plt.imshow(adapt, cmap='gray'); plt.title('adaptive')
plt.show()
Real-life meaning: Document scanning — adaptive recovers text under uneven illumination.
Exam relevance: Trial-exam Q2b-iii.
2.5 DoG visualisation¶
gauss1 = cv2.GaussianBlur(gray.astype(np.float32), (0,0), 1.0)
gauss2 = cv2.GaussianBlur(gray.astype(np.float32), (0,0), 1.6)
DoG = gauss1 - gauss2
plt.imshow(DoG, cmap='gray'); plt.title('Difference of Gaussians'); plt.show()
2.6 Remove a linear gradient (matches Q2b-iv)¶
def remove_gradient(img):
blur = cv2.GaussianBlur(img, (0, 0), sigmaX=51)
return cv2.subtract(img, blur) + 128
Chapter 04 — Machine Learning¶
4.1 PyTorch MNIST classifier (Sheet 4 Task 3)¶
Purpose: Build a simple MLP for MNIST handwritten digits.
Input: torchvision MNIST dataset, normalised to [0, 1].
import torch.nn as nn, torch.nn.functional as F, torch.optim as optim
class Net(nn.Module):
def __init__(self):
super().__init__()
self.flat = nn.Flatten()
self.fc1 = nn.Linear(28*28, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.flat(x); x = F.relu(self.fc1(x))
return F.log_softmax(self.fc2(x), dim=1)
# Training loop assumed (DataLoader, Adam, NLLLoss, 5 epochs)
Output: Test accuracy ~ 0.97 on MNIST.
Visualization: 4×4 grid of predicted vs true labels.
Real-life meaning: Foundation for any classifier.
Exam relevance: Sheet 4 task; conceptual ML questions.
4.2 Confusion-matrix metrics¶
def confusion(y_true, y_pred, K):
M = np.zeros((K, K), int)
for t, p in zip(y_true, y_pred): M[t, p] += 1
return M
def metrics(M):
total = M.sum(); diag = np.trace(M)
acc = diag / total
sens = M.diagonal() / M.sum(axis=1)
K = M.shape[0]
spec = []
for c in range(K):
TP = M[c, c]
FN = M[c, :].sum() - TP
FP = M[:, c].sum() - TP
TN = total - TP - FN - FP
spec.append(TN / (TN + FP))
return acc, sens, np.array(spec)
4.3 Decision boundary visualisation¶
xx, yy = np.meshgrid(np.linspace(-3, 3, 200), np.linspace(-3, 3, 200))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.3)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', edgecolors='k')
plt.show()
Chapter 05 — Features¶
5.1 Harris corners from scratch (Sheet 4 Task 1)¶
def harris(gray, sigma=2.0, k=0.04, tau=0.01):
g = gray.astype(np.float32) / 255.
Ix = cv2.Sobel(g, cv2.CV_32F, 1, 0, ksize=3)
Iy = cv2.Sobel(g, cv2.CV_32F, 0, 1, ksize=3)
Sxx = cv2.GaussianBlur(Ix*Ix, (0,0), sigma)
Syy = cv2.GaussianBlur(Iy*Iy, (0,0), sigma)
Sxy = cv2.GaussianBlur(Ix*Iy, (0,0), sigma)
R = (Sxx*Syy - Sxy**2) - k * (Sxx + Syy)**2
keep = (R > tau * R.max()) & (R == cv2.dilate(R, np.ones((3,3))))
return np.argwhere(keep), R
5.2 SIFT matching with ratio test (Sheet 4 Task 2)¶
sift = cv2.SIFT_create()
kp1, d1 = sift.detectAndCompute(g1, None)
kp2, d2 = sift.detectAndCompute(g2, None)
bf = cv2.BFMatcher(cv2.NORM_L2)
matches = bf.knnMatch(d1, d2, k=2)
good = [m for m, n in matches if m.distance < 0.75 * n.distance]
out = cv2.drawMatches(img1, kp1, img2, kp2, good, None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
5.3 ZMNCC (zero-mean normalised cross correlation)¶
def zmncc(W, I):
Wm, Im = W - W.mean(), I - I.mean()
num = (Wm * Im).sum()
den = np.sqrt((Wm**2).sum() * (Im**2).sum()) + 1e-9
return num / den
Chapter 06 — Optical Flow¶
6.1 Sparse Lucas-Kanade¶
prev = cv2.imread('a.png', cv2.IMREAD_GRAYSCALE)
curr = cv2.imread('b.png', cv2.IMREAD_GRAYSCALE)
pts0 = cv2.goodFeaturesToTrack(prev, 200, 0.01, 8)
pts1, st, _ = cv2.calcOpticalFlowPyrLK(prev, curr, pts0, None)
6.2 Dense Horn-Schunck¶
(See Chapter 6 §6.2.)
6.3 Backward warping with bilinear interpolation¶
def backward_warp(img, u, v):
h, w = img.shape[:2]
yy, xx = np.mgrid[0:h, 0:w].astype(np.float32)
return cv2.remap(img, (xx + u).astype(np.float32),
(yy + v).astype(np.float32),
cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
6.4 HSV-coded flow visualisation¶
mag, ang = cv2.cartToPolar(u, v)
hsv = np.zeros((*u.shape, 3), np.uint8)
hsv[..., 0] = (ang * 90 / np.pi) % 180
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
flow_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
plt.imshow(flow_rgb); plt.show()
Chapter 07 — Parametric Transformations¶
7.1 Apply T·R·S to a unit square¶
def to_h(p): return np.hstack([p, np.ones((len(p), 1))])
def from_h(p): return p[:, :2] / p[:, 2:]
def T(tx, ty): return np.array([[1,0,tx],[0,1,ty],[0,0,1]])
def S(sx, sy): return np.array([[sx,0,0],[0,sy,0],[0,0,1]])
def R(theta):
c, s = np.cos(theta), np.sin(theta)
return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
square = np.array([[-1,-1],[1,-1],[1,1],[-1,1]], float)
M = T(20, 0) @ R(np.pi/4) @ S(2, 1)
warped = from_h((M @ to_h(square).T).T)
7.2 Pivot rotation (Q6b)¶
def rotate_around(pts, P, theta):
px, py = P
M = T(px, py) @ R(theta) @ T(-px, -py)
return from_h((M @ to_h(pts).T).T)
7.3 Homography DLT (Q7-style)¶
def homography_dlt(src, dst):
A = []
for (x, y), (xp, yp) in zip(src, dst):
A.append([-x, -y, -1, 0, 0, 0, x*xp, y*xp, xp])
A.append([0, 0, 0, -x, -y, -1, x*yp, y*yp, yp])
_, _, Vt = np.linalg.svd(np.array(A))
return (Vt[-1] / Vt[-1, -1]).reshape(3, 3)
7.4 Cubic Bézier and Catmull-Rom¶
def cubic_bezier(P0, P1, P2, P3, n=100):
t = np.linspace(0, 1, n)[:, None]
return ((1-t)**3)*P0 + 3*((1-t)**2)*t*P1 + 3*(1-t)*t**2*P2 + t**3*P3
def catmull(P0, P1, P2, P3, n=50):
t = np.linspace(0, 1, n)[:, None]
return 0.5 * ((2*P1) + (-P0 + P2)*t +
(2*P0 - 5*P1 + 4*P2 - P3)*t**2 +
(-P0 + 3*P1 - 3*P2 + P3)*t**3)
7.5 Thin-plate spline¶
(See Chapter 7 §6.6.)
Chapter 08 — Epipolar Geometry & Depth¶
8.1 Fundamental matrix + RANSAC¶
8.2 Stereo block matching¶
stereo = cv2.StereoSGBM_create(0, 64, 5, 8*3*5**2, 32*3*5**2)
disp = stereo.compute(imgL, imgR).astype(np.float32) / 16.0
Z = (b * f_pix) / np.maximum(disp, 1e-3)
8.3 Trial-exam Q7b stereo to 3-D in code¶
def stereo_to_3d(xL, yL, xR, b, f_pix, ox, oy):
d = xL - xR
Z = (b * f_pix) / d
return np.array([(xL - ox)*Z/f_pix, (yL - oy)*Z/f_pix, Z]), d
8.4 Census transform¶
(See Chapter 8 §6.6.)
Chapter 09 — Morphing / View Synthesis¶
9.1 Cross-dissolve¶
def crossfade(A, B, t):
return ((1-t)*A.astype(np.float32) + t*B.astype(np.float32)).astype(np.uint8)
9.2 View morph formula in code (matches Q8a)¶
def view_morph_pixel(x, x_prime, H1, H2, t):
rect_x = H1 @ x; rect_x /= rect_x[2]
rect_x_prime = H2 @ x_prime; rect_x_prime /= rect_x_prime[2]
rect_xhat = rect_x + t * (rect_x_prime - rect_x)
return np.linalg.inv(H1) @ rect_xhat / (np.linalg.inv(H1) @ rect_xhat)[2]
9.3 Triangulation morph (high-level)¶
(See Chapter 9 §6.2.)
Chapter 10 — Camera Calibration¶
10.1 Calibrate with checkerboard¶
ret, K, dist, rvecs, tvecs = cv2.calibrateCamera(
obj_points, img_points, gray.shape[::-1], None, None)
10.2 Project a 3-D point¶
10.3 Triangulate a 3-D point from 2 views¶
def triangulate(P1, P2, x1, x2):
A = np.array([
x1[0]*P1[2] - P1[0],
x1[1]*P1[2] - P1[1],
x2[0]*P2[2] - P2[0],
x2[1]*P2[2] - P2[1]])
_, _, Vt = np.linalg.svd(A)
X = Vt[-1]
return X[:3] / X[3]
10.4 RQ factorisation to extract K and R¶
from scipy.linalg import rq
def decompose_P(P):
M = P[:, :3]
K, R = rq(M)
sgn = np.diag(np.sign(np.diag(K)))
K = K @ sgn; R = sgn @ R
K = K / K[2, 2]
T = np.linalg.inv(K) @ P[:, 3]
return K, R, T
Chapter 11 — NeRFs¶
11.1 Positional encoding¶
def positional_encoding(p, L=10):
out = [p]
for k in range(L):
out += [np.sin(2**k * np.pi * p), np.cos(2**k * np.pi * p)]
return np.concatenate(out, axis=-1)
11.2 Volume rendering of one ray¶
(See Chapter 11 §6.2.)
11.3 Tiny image-regression toy¶
(See Chapter 11 §6.4.)
Common Coding Mistakes & Debug Tips¶
- Forgetting BGR↔RGB.
- Off-by-one in convolution mirroring.
- Forgetting
model.train()/model.eval()in PyTorch (changes BatchNorm/Dropout behaviour). - Mixing pixel and metric units in stereo formulas.
- Forgetting
model.zero_grad()before.backward(). - Using uint8 arithmetic and overflowing.
- Drawing keypoints with the wrong colour space (BGR vs RGB).
- Wrong indexing: NumPy is
(y, x), OpenCV is(x, y).
How to Test Code¶
- Synthetic input: build a 16×16 image with known properties (e.g. one bright square) and verify your filter / detector output by hand.
- Edge cases: zero-padding vs reflect; corner pixels.
- Numeric stability: use
float32everywhere when implementing filtering by hand. - Test on a single image first, then a batch.