Skip to content

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:

import numpy as np
import cv2
import matplotlib.pyplot as plt
import torch

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

Purpose: Rotate image hue (Sheet 2 Task 5).
Input: Color image, scalar hue_offset_deg.
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)

Purpose: Project a 3-D point onto the image plane.
Input: 3-D point P = (x, y, z), focal length f.
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

Purpose: Demonstrate Gaussian noise model.
Input: Image, sigma.
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)

Purpose: Detect edges with first derivative.
Input: Grayscale image (uint8 or float).
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)

Purpose: Reproduce the trial-exam convolution.
Input: 8×8 diamond image, kernel [-1, 0, 1].
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

Purpose: Compare global and local thresholding.
Input: Vignetted text image.
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

Purpose: Show that DoG ≈ LoG.
Input: Grayscale image, two sigmas.
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()
Real-life meaning: SIFT scale-space detector.
Exam relevance: Trial-exam Q2b-ii.

2.6 Remove a linear gradient (matches Q2b-iv)

Purpose: High-pass filter to detrend illumination.
Input: Concrete-wall image.
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

F, mask = cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC, 3.0)
inliers = mask.ravel() == 1

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

def project3D(K, R, T, X):
    Xc = R @ X + T
    return (K @ Xc)[:2] / Xc[2]

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 float32 everywhere when implementing filtering by hand.
  • Test on a single image first, then a batch.