///|
/// A square (odd-sized) convolution kernel plus its normalization terms.
///
/// `weights` are stored row-major with `size * size` entries. Each output
/// channel is `(sum(weight_i * neighbor_i) / divisor) + bias`, clamped back
/// into `[0, 255]`. Keeping `divisor` and `bias` on the kernel lets a single
/// `convolve` implementation serve blur, sharpen, emboss and edge kernels.
pub(all) struct Kernel {
size : Int
weights : Array[Double]
divisor : Double
bias : Double
}
///|
/// Builds a kernel, aborting if the shape is invalid (even/negative size or a
/// weight count that does not match `size * size`).
pub fn Kernel::new(
size : Int,
weights : Array[Double],
divisor : Double,
bias : Double,
) -> Kernel {
if size <= 0 || size % 2 == 0 {
abort("Kernel::new: size must be a positive odd number")
}
if weights.length() != size * size {
abort("Kernel::new: weights length must equal size * size")
}
{ size, weights, divisor, bias }
}
///|
/// 3x3 box blur: unweighted average of the neighborhood.
pub fn Kernel::box_blur() -> Kernel {
Kernel::new(3, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], 9.0, 0.0)
}
///|
/// 3x3 Gaussian blur approximation with weights summing to 16.
pub fn Kernel::gaussian_blur() -> Kernel {
Kernel::new(3, [1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0], 16.0, 0.0)
}
///|
/// 3x3 sharpen: boosts the center and subtracts its 4-neighbors.
pub fn Kernel::sharpen() -> Kernel {
Kernel::new(3, [0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0], 1.0, 0.0)
}
///|
/// 3x3 Laplacian edge kernel; flat regions collapse to black.
pub fn Kernel::edges() -> Kernel {
Kernel::new(
3,
[-1.0, -1.0, -1.0, -1.0, 8.0, -1.0, -1.0, -1.0, -1.0],
1.0,
0.0,
)
}
///|
/// 3x3 emboss kernel with a +128 bias so flat areas render as mid-gray relief.
pub fn Kernel::emboss() -> Kernel {
Kernel::new(3, [-2.0, -1.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0, 2.0], 1.0, 128.0)
}
///|
/// Clamps a coordinate into `[0, limit)` so kernels sample a repeated edge
/// pixel instead of wrapping or reading out of bounds ("clamp-to-edge").
fn clamp_coord(v : Int, limit : Int) -> Int {
if v < 0 {
0
} else if v >= limit {
limit - 1
} else {
v
}
}
///|
/// Convolves the image with `kernel`, processing R, G and B independently and
/// preserving alpha. Out-of-bounds neighbors are clamped to the edge. This is
/// the compute-heavy core the WebAssembly demo is built to show off.
pub fn Image::convolve(self : Image, kernel : Kernel) -> Image {
let out = Image::new(self.width, self.height)
let radius = kernel.size / 2
for y in 0.. Image {
self.convolve(Kernel::gaussian_blur())
}
///|
/// Sharpen convenience wrapper.
pub fn Image::sharpen(self : Image) -> Image {
self.convolve(Kernel::sharpen())
}
///|
/// Emboss convenience wrapper.
pub fn Image::emboss(self : Image) -> Image {
self.convolve(Kernel::emboss())
}
///|
/// Laplacian edge convenience wrapper.
pub fn Image::edges(self : Image) -> Image {
self.convolve(Kernel::edges())
}
///|
/// Absolute value for `Int`, kept local so the Sobel gradient stays in pure
/// integer arithmetic without pulling in an extra dependency.
fn iabs(v : Int) -> Int {
if v < 0 {
-v
} else {
v
}
}
///|
/// Shared gradient-edge engine: reduces the image to luma, measures the
/// horizontal (Gx) and vertical (Gy) gradients with the given 3x3 operators,
/// and writes the L1 magnitude `|Gx| + |Gy|`, which keeps the whole pass in
/// integer math (fast and reproducible). Output is grayscale; alpha preserved.
fn Image::gradient_edges(
self : Image,
gx : Array[Int],
gy : Array[Int],
) -> Image {
let gray = self.grayscale()
let out = Image::new(self.width, self.height)
for y in 0.. R == G == B
let k = ky * 3 + kx
sx = sx + lum * gx[k]
sy = sy + lum * gy[k]
}
}
let v = clamp_byte(iabs(sx) + iabs(sy))
let base = (y * self.width + x) * 4
out.data[base] = v
out.data[base + 1] = v
out.data[base + 2] = v
out.data[base + 3] = self.data[base + 3]
}
}
out
}
///|
/// Sobel edge detection with the classic 1/2/1 operators.
pub fn Image::sobel(self : Image) -> Image {
self.gradient_edges([-1, 0, 1, -2, 0, 2, -1, 0, 1], [
-1, -2, -1, 0, 0, 0, 1, 2, 1,
])
}
///|
/// Scharr edge detection: same structure as Sobel but with 3/10/3 weights,
/// which are closer to rotational symmetry and give stronger, more uniform
/// responses on diagonal edges.
pub fn Image::scharr(self : Image) -> Image {
self.gradient_edges([-3, 0, 3, -10, 0, 10, -3, 0, 3], [
-3, -10, -3, 0, 0, 0, 3, 10, 3,
])
}