Image Processing with Maxima¶
This notebook demonstrates image processing using numerics and numerics-image. We load images as ndarrays, apply 2D convolution kernels for blurring, edge detection, and sharpening, and display the results inline.
The numerics-image module provides:
np_read_image(path)/np_write_image(A, path)— image I/O via opticlnp_imshow(A)— inline display of an ndarray as a PNG imagenp_mandrill()— bundled 512×512 test imagenp_to_image(A)/np_from_image(img)— convert between ndarrays and opticl arrays
Convolution uses np_convolve2d(A, kernel) from the core numerics library, which implements "valid" mode: the output shape is $(h - k_h + 1) \times (w - k_w + 1)$.
load("numerics")$
load("numerics-image")$
Loading the Test Image¶
The bundled mandrill (baboon) image is a classic 512×512 RGB test image from the USC-SIPI database. np_mandrill() returns it as an ndarray with shape $[512, 512, 3]$, where pixel values range from 0 to 255.
img : np_mandrill()$
np_shape(img);
WARNING: Extension chunk type not implemented: "eXIf" (101 88 73 102) in stream: NIL.
np_imshow(img)$
Converting to Grayscale¶
For convolution we need a single-channel 2D array. We extract the R, G, and B channels using np_slice, average them, and reshape to a flat 2D matrix.
The standard luminance formula is $Y = 0.299R + 0.587G + 0.114B$, but a simple average $(R + G + B) / 3$ is sufficient for demonstration.
/* Extract channels: img has shape [512, 512, 3] */
red : np_slice(img, all, all, [0, 1])$
green : np_slice(img, all, all, [1, 2])$
blue : np_slice(img, all, all, [2, 3])$
/* Average the three channels */
gray3d : np_scale(1/3, np_add(np_add(red, green), blue))$
/* Reshape from [512, 512, 1] to [512, 512] for convolution */
gray : np_reshape(gray3d, [512, 512])$
np_shape(gray);
np_imshow(gray)$
Gaussian-like Blur¶
A box filter (averaging kernel) replaces each pixel with the mean of its neighbourhood. A $5 \times 5$ box filter is:
$$K_{\text{blur}} = \frac{1}{25} \begin{pmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{pmatrix}$$
For a closer Gaussian approximation we could weight the centre more heavily, but the box filter demonstrates the principle clearly.
/* 5x5 box blur kernel */
blur_kernel : np_scale(1/25, ndarray(
matrix([1,1,1,1,1],
[1,1,1,1,1],
[1,1,1,1,1],
[1,1,1,1,1],
[1,1,1,1,1])))$
blurred : np_convolve2d(gray, blur_kernel)$
print("Blurred shape:", np_shape(blurred))$
np_imshow(blurred)$
Blurred shape: [508,508]
Edge Detection with the Sobel Operator¶
The Sobel operator detects edges by computing approximate horizontal and vertical gradients. The two $3 \times 3$ kernels are:
$$G_x = \begin{pmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{pmatrix}, \qquad G_y = \begin{pmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{pmatrix}$$
The edge magnitude is $|G| = \sqrt{G_x^2 + G_y^2}$.
/* Sobel kernels */
sobel_x : ndarray(matrix([-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]))$
sobel_y : ndarray(matrix([-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]))$
/* Convolve */
gx : np_convolve2d(gray, sobel_x)$
gy : np_convolve2d(gray, sobel_y)$
/* Edge magnitude: sqrt(gx^2 + gy^2) */
edges : np_sqrt(np_add(np_mul(gx, gx), np_mul(gy, gy)))$
print("Edges shape:", np_shape(edges))$
Edges shape: [510,510]
np_imshow(edges)$
We can also look at just the horizontal or vertical gradient components separately:
/* Show horizontal edges (absolute value for visibility) */
np_imshow(np_abs(gx))$
/* Show vertical edges */
np_imshow(np_abs(gy))$
Sharpening¶
A sharpening filter enhances edges by subtracting a blurred version from the original (unsharp masking). An equivalent single-pass kernel is:
$$K_{\text{sharpen}} = \begin{pmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{pmatrix}$$
This is the identity plus a scaled Laplacian. The centre weight of 5 preserves the original pixel value while the negative neighbours subtract the local average.
sharpen_kernel : ndarray(matrix([ 0, -1, 0],
[-1, 5, -1],
[ 0, -1, 0]))$
sharpened : np_convolve2d(gray, sharpen_kernel)$
/* Clamp to [0, 255] for display */
sharpened : np_clip(sharpened, 0, 255)$
np_imshow(sharpened)$
Emboss Effect¶
An emboss kernel creates a 3D relief effect by highlighting transitions in one direction:
$$K_{\text{emboss}} = \begin{pmatrix} -2 & -1 & 0 \\ -1 & 1 & 1 \\ 0 & 1 & 2 \end{pmatrix}$$
Adding 128 to the result shifts the neutral grey to mid-range, making the relief visible.
emboss_kernel : ndarray(matrix([-2, -1, 0],
[-1, 1, 1],
[ 0, 1, 2]))$
embossed : np_convolve2d(gray, emboss_kernel)$
/* Shift to mid-grey and clamp */
embossed : np_clip(np_add(embossed, ndarray([128])), 0, 255)$
np_imshow(embossed)$
Combining Operations: Edge Overlay¶
We can combine the blurred image with edge detection to create an artistic effect. Here we threshold the Sobel edge magnitude and overlay it on the blurred image.
/* Normalise edges to 0-255 range */
edge_max : np_max(edges)$
edges_norm : np_scale(255.0 / edge_max, edges)$
/* Trim edges (510x510) to match blurred (508x508) */
edges_trimmed : np_slice(edges_norm, [1, 509], [1, 509])$
/* Blend: darken where edges are strong */
blend : np_clip(np_sub(blurred, np_scale(0.5, edges_trimmed)), 0, 255)$
np_imshow(blend)$
Frequency-Domain Filtering with 2D FFT¶
The 2D discrete Fourier transform decomposes an image into spatial frequency components. Low frequencies encode smooth gradients; high frequencies encode edges and fine detail.
By zeroing out frequency bins we can build simple low-pass and high-pass filters entirely in the frequency domain.
/* 2D FFT of the grayscale mandrill */
F : np_fft2d(gray)$
/* Log-magnitude spectrum for visualisation */
mag : np_abs(F)$
log_mag : np_log(np_add(mag, 1.0))$
/* Scale to 0-255 for display */
lm_max : np_max(log_mag)$
log_mag_img : np_scale(255.0 / lm_max, log_mag)$
np_imshow(log_mag_img)$
Low-Pass Filter (Blur)¶
A circular low-pass filter keeps only frequencies within radius $r$ of the DC component at index $(0,0)$. Since np_fft2d does not shift the DC to the centre, low frequencies are near the corners of the output array.
/* Build a circular low-pass mask (unshifted layout) */
nr : first(np_shape(gray))$
nc : second(np_shape(gray))$
cutoff : 30$ /* keep frequencies within radius 30 */
/* Frequency indices: wrap around at N/2 for unshifted FFT layout */
row_idx : np_arange(nr)$
col_idx : np_arange(nc)$
fi : np_sub(row_idx, np_scale(float(nr), np_greater_equal(row_idx, nr/2)))$
fj : np_sub(col_idx, np_scale(float(nc), np_greater_equal(col_idx, nc/2)))$
/* Squared distance grid via broadcasting */
fi_sq : np_pow(np_reshape(fi, [nr, 1]), 2)$
fj_sq : np_pow(np_reshape(fj, [1, nc]), 2)$
mask_lp : np_less(np_add(fi_sq, fj_sq), float(cutoff * cutoff))$
print("Low-pass mask built:", np_shape(mask_lp))$
Low-pass mask built: [512,512]
/* Apply low-pass: multiply spectrum by mask, then inverse FFT */
F_lp : np_mul(F, ndarray(np_to_matrix(mask_lp), complex))$
img_lp : np_real(np_ifft2d(F_lp))$
img_lp_clamped : np_clip(img_lp, 0, 255)$
np_imshow(img_lp_clamped)$
High-Pass Filter (Edge Enhancement)¶
Inverting the mask keeps only high frequencies — the edges and fine textures.
/* High-pass = 1 - low-pass mask */
mask_hp : np_sub(np_ones([nr, nc]), mask_lp)$
F_hp : np_mul(F, ndarray(np_to_matrix(mask_hp), complex))$
img_hp : np_real(np_ifft2d(F_hp))$
/* Rescale for visibility: shift so mean is 128 */
hp_min : np_min(img_hp)$
hp_max : np_max(img_hp)$
img_hp_vis : np_scale(255.0 / (hp_max - hp_min), np_sub(img_hp, hp_min))$
np_imshow(img_hp_vis)$
Round-Trip Verification¶
The inverse FFT should perfectly reconstruct the original image.
/* Verify: ifft2d(fft2d(gray)) ≈ gray */
recon : np_real(np_ifft2d(F))$
err : np_max(np_abs(np_sub(recon, gray)))$
print("Max reconstruction error:", err)$
Max reconstruction error: 1.7053025658242404e-13
Summary¶
This notebook demonstrated:
- Image loading —
np_mandrill()returns a bundled test image as an ndarray - Colour to greyscale — channel extraction with
np_sliceand averaging - 2D convolution —
np_convolve2d(A, kernel)with various kernels:- Box blur (averaging)
- Sobel edge detection (horizontal and vertical gradients)
- Sharpening (identity + Laplacian)
- Emboss (directional relief)
- 2D FFT —
np_fft2d/np_ifft2dfor frequency-domain filtering:- Log-magnitude spectrum visualisation
- Circular low-pass filter (blur)
- High-pass filter (edge enhancement)
- Perfect round-trip reconstruction
- Inline display —
np_imshow()renders arrays as PNG images