millow

A zero-FFI, cross-platform image-processing library for MoonBit.

image
image-processing
graphics
pixel
moon add megemini/millow@0.3.1
Download zip
Author
Version
0.3.1
License
Apache-2.0
Last updated
last month
Downloads
26
README

#Millow

MillowM (for MoonBit) + illow (a nod to Python's Pillow).

A zero-FFI, cross-platform image-processing library for MoonBit. millow works entirely on in-memory RGBA8 buffers (Array[Byte], laid out H × W × 4) and builds on every backend: wasm-gc, wasm, js, and native.

中文 | English

#Demo

Inputto_grayscaletint(100,150,200)gaussian_blur(σ=2)

sharpen(1.0)sobelequalize_histogramthreshold_otsu

rotate_any(45°)find_contourspipeline

cmd/main contains a lightweight demo that generates a synthetic gradient image and applies 30+ millow operations — no external dependencies required:

moon run cmd/main

For the full-featured demo (PNG/JPEG I/O, 30 output images), use the standalone examples/ project:

cd examples moon run .

#Features

  • Core image typeImage with construction, pixel access, cloning, channel split/merge, and sub-images.
  • Color — grayscale (flat & weighted), invert, tint, BGR, alpha flatten, HSV/YCbCr conversions, LUT application, and over compositing.
  • Geometry — crop, flips, 90/180/270 rotation, arbitrary rotation, translation, affine transform, shear, resize (nearest / bilinear / bicubic), rescale, fit/cover, thumbnails, and padding.
  • Enhancement — brightness, contrast, gamma, normalize, auto-contrast, standardize, sharpen, and unsharp mask.
  • Threshold & histogram — fixed threshold, Otsu, Sauvola, histogram (gray & color), equalization, CLAHE, and histogram matching.
  • Filters & edges — convolution, box/Gaussian blur, median/min/max, bilateral filter, Sobel, Scharr, Prewitt, Laplacian, and Canny.
  • Morphology — erode, dilate, open, close, gradient, top-hat, black-hat, skeletonize, and hit-or-miss.
  • Feature detection — LBP, HOG, Harris corner, and Shi-Tomasi corner.
  • Measurement — connected components, find contours, moments, Hu moments, region properties, and pixel counting.
  • Data augmentation — random crop, flip, rotate, brightness/contrast/gamma adjustment, Gaussian/salt-pepper noise, color jitter, composable pipelines with weighted random choice.
  • Metrics — MSE, PSNR, SSIM.
  • Drawing — pixels, lines, rectangles, circles, ellipses, polygons, and flood fill.
  • I/O — PPM/PGM serialization plus a pluggable Encoder/Decoder registry.

#Project layout

millow/ ├── src/ # implementation package (megemini/millow/src) ├── millow.mbt # root facade: re-exports the public API (megemini/millow) ├── test/ # blackbox test package exercising the public API ├── test_alignment/ # alignment tests against Python (skimage/Pillow) reference ├── cmd/main/ # lightweight synthetic-image demo (millow-only) └── examples/ # standalone full demo (PNG/JPEG I/O via mizchi/image)

The root package is a thin facade over src, so downstream users just import "megemini/millow" and reach the whole API through @millow.

#Installation

moon add megemini/millow

Then import it in your package's moon.pkg:

import { "megemini/millow" @millow, }

#Quick start

///|
test "build, transform and inspect an image" {
// A 64×64 canvas with a filled rectangle drawn on it.
let base = Image::from_pixel(64, 64, 30, 60, 90, 255)
let canvas = draw_rect(base, 8, 8, 40, 40, 220, 40, 40, 255, true, 1)

// Grayscale → blur → edges.
let gray = to_grayscale(canvas)
let blurred = gaussian_blur(gray, 1.5)
let edges = sobel(blurred)
assert_eq(edges.shape(), (64, 64))

// Otsu adaptive threshold.
let (_, binary) = threshold_otsu(blurred)
assert_eq(binary.shape(), (64, 64))

// Downscale and serialize to PPM.
let thumb = resize(canvas, 16, 16, Nearest)
let ppm = to_ppm(thumb)
assert_eq(ppm[0], 'P'.to_int().to_byte())
}

In the examples above the API is called unqualified because they run inside the millow package itself. From another module, prefix each name with the import alias, e.g. @millow.to_grayscale(img).

#Augmentation pipeline

Compose multiple augmentations into a single pass with augment_pipeline, which applies each Augmentation variant left-to-right:

///|
test "augment_pipeline example" {
let img = Image::from_pixel(64, 64, 30, 60, 90, 255)
let out = augment_pipeline(img, [
FlipHorizontal,
Rotate(15.0),
Brightness(1.2),
Contrast(1.3),
NoiseGaussian(8.0),
])
assert_true(out.h > 0 && out.w > 0)
}

Available Augmentation variants: Crop(y, x, h, w), Resize(dst_h, dst_w), FlipHorizontal, FlipVertical, Rotate(angle), Brightness(factor), Contrast(factor), Gamma(g), NoiseGaussian(std), NoiseSaltPepper(prob), ColorJitter(b, c, s, h). augment_pipeline may raise on invalid crop/resize arguments.

To sample one augmentation from a weighted distribution, use augment_random_choice:

///|
test "augment_random_choice example" {
let img = Image::from_pixel(64, 64, 30, 60, 90, 255)
let out = augment_random_choice(img, [
(0.4, FlipVertical),
(0.4, Gamma(0.8)),
(0.2, ColorJitter(0.2, 0.2, 0.0, 0.0)),
])
assert_eq(out.shape(), img.shape())
}

#API notes

#Coordinate system

millow uses a single uniform coordinate convention across the entire API:

  • Dimension order: (h, w) — height first, width second. Image::new(h, w), resize(img, dst_h, dst_w, interp), crop(img, y, x, h, w), Image::shape() -> (h, w).

  • Coordinate order: (y, x) — row first, column second. y is the vertical axis (increases downward), x is the horizontal axis (increases rightward). The origin (0, 0) is the top-left corner.

  • Drawing centers: draw_circle(img, cy, cx, radius, ...), draw_ellipse(img, cy, cx, ry, rx, ...).

  • Translation: translate(img, dy, dx, interp).

  • Contours: find_contours returns (y, x) tuples.

#Brightness adjustment

adjust_brightness(img, factor) uses a multiplicative factor:

  • factor = 1.0 returns the original image
  • factor = 0.0 returns a black image
  • Values greater than 1.0 brighten the image
  • Values less than 1.0 darken the image

#Contrast adjustment

adjust_contrast(img, factor) adjusts contrast relative to the image's average luma:

  • factor = 1.0 returns the original image
  • factor = 0.0 returns a solid gray image equal to the image's mean
  • Values greater than 1.0 increase contrast
  • Values less than 1.0 decrease contrast

#Alpha compositing

flatten_alpha(img, r, g, b) composites the image over a solid background color, using floating-point blending for smooth results.

#Border handling

Several operations support a mode parameter that controls how border pixels are handled:

  • Replicate — extends the nearest edge pixel outward
  • Reflect — mirrors pixels across the edge
  • Wrap — tiles the image periodically
  • Constant(r, g, b, a) — fills border regions with a constant color

Functions supporting an optional mode parameter include affine_transform and shear. Most other operations use replicate (clamp) border handling internally.

#Bilateral filter

bilateral_filter(img, d, sigma_color, sigma_space) applies edge-preserving smoothing:

  • d is the diameter of the pixel neighborhood (use 0 to auto-compute based on sigma_space)
  • sigma_color controls how similar colors must be to influence each other (larger = more smoothing)
  • sigma_space controls how close pixels must be spatially to influence each other (larger = wider neighborhood)

#Affine transform

affine_transform(img, matrix, dst_h, dst_w, interp, mode) applies a general affine transformation using a 6-element matrix [a, b, c, d, e, f] representing:

x' = a*x + b*y + c y' = d*x + e*y + f

Use rotate_any and translate for common transformations.

#Random noise

random_noise_gaussian(img, std) adds Gaussian noise with the specified standard deviation.

random_noise_salt_pepper(img, amount) adds salt-and-pepper noise with the specified amount (fraction of pixels affected).

#Backends

millow contains no foreign function calls. It is verified to build on wasm-gc, wasm, js, and native, and the test suite passes on each.

#Testing

moon test # run every test moon test --target native # pick a backend moon run cmd/main # run the synthetic demo (no external deps) cd examples && moon run . # run the full demo with JPEG I/O

#Alignment tests

test_alignment/ verifies millow's output against a Python reference (numpy / skimage / Pillow) that implements the same algorithms. The workflow is:

  1. test_alignment/generate_fixtures.py computes expected output bytes for small test images and writes them as Array[Int] literals in fixtures_test.mbt.
  2. The MoonBit tests construct Images from those fixtures, run each millow operation, and compare byte-for-byte (exact for integer ops, ±1 tolerance for floating-point rounding).

Regenerate the fixtures after changing an algorithm:

source $HOME/venv310/bin/activate python test_alignment/generate_fixtures.py moon test

#Roadmap

See docs/roadmap.md for the full version plan and upcoming features.

#License

Apache-2.0. See LICENSE.

#
Augmentation

Data augmentation operations that can be composed into a pipeline.

Each variant wraps the parameters of one transformation; augment_pipeline applies them in order. Variants:

  • Crop(y, x, h, w): extract the sub-image at (y, x) of size h x w.
  • Resize(dst_h, dst_w): resize to dst_h x dst_w using bilinear sampling.
  • FlipHorizontal / FlipVertical: mirror the image.
  • Rotate(angle): rotate by angle degrees (bilinear).
  • Brightness(factor) / Contrast(factor): multiplicative adjustments.
  • Gamma(g): gamma correction with exponent g.
  • NoiseGaussian(std): add Gaussian noise with standard deviation std.
  • NoiseSaltPepper(prob): corrupt pixels with probability prob.
  • ColorJitter(b, c, s, h): brightness/contrast/saturation/hue jitter.

#
BorderMode

Border handling mode for convolution and interpolation operations.

  • Replicate: clamp coordinates to the nearest edge pixel.
  • Reflect: mirror coordinates with edge duplication.
  • Wrap: tile coordinates periodically.
  • Constant(r, g, b, a): use the given RGBA color outside the image.

#
Decoder

Pluggable decoder: turn an encoded byte buffer into an image.

Implementations decode data into an Image, raising ImageError on a malformed or unsupported buffer. Register instances with register_decoder to make them available via decode.

#
Encoder

Pluggable encoder: turn an image into an encoded byte buffer.

Implementations serialize img to a format-specific byte stream, raising ImageError on failure. Register instances with register_encoder to make them available via encode.

#
Image

Core image type. Internal storage is RGBA8, row-major, h * w * 4 bytes. The byte at offset (y * w + x) * 4 is R, then G, B, A.

#
ImageError

Error raised by image operations that receive invalid arguments.

#
Interp

Interpolation method used by geometric resampling operations.

  • Nearest: pick the closest source pixel.
  • Bilinear: weighted average of the 4 nearest pixels.
  • Bicubic: cubic interpolation over a 4x4 neighborhood.

#
Kernel

Structuring element for morphological operations.

  • Cross(n): plus-shaped mask with radius n (arm length n).
  • Square(n): full (2n+1) x (2n+1) box.
  • Custom(mask): arbitrary boolean mask given as rows of booleans.

#
PadMode

Border handling strategy used by padding and neighborhood operations.

  • Constant(r, g, b, a): fill outside pixels with the given RGBA color.
  • Replicate: copy the nearest edge pixel.
  • Reflect: mirror with edge duplication (the boundary pixel is repeated).
  • Wrap: tile the image periodically.

#
PpmEncoder

Built-in encoder that writes binary PPM (P6).

A unit struct implementing Encoder by delegating to to_ppm. Register it with register_encoder("ppm", &PpmEncoder{}) to plug into encode.

#
RegionProps

Region properties for labelled images.

  • label: the 1-based label the props describe.
  • area: number of pixels in the region.
  • centroid: (row, col) mean pixel coordinate.
  • bbox: (min_row, min_col, max_row, max_col) inclusive bounds.

#
adjust_brightness

fn adjust_brightness(img :
Image
, factor : Double) ->
Image

Brightness adjustment using a multiplicative factor. factor = 1.0 returns the original image, factor = 0.0 returns a black image.

#
adjust_contrast

fn adjust_contrast(img :
Image
, factor : Double) ->
Image

Contrast adjustment based on image mean. factor = 1.0 returns the original image, factor = 0.0 returns a solid gray image.

#
adjust_gamma

fn adjust_gamma(img :
Image
, gamma : Double) ->
Image

Gamma correction. gamma > 1 darkens, gamma < 1 brightens.

#
affine_transform

fn affine_transform(img :
Image
, matrix : Array[Double], dst_h : Int, dst_w : Int, interp :
Interp
, mode? :
BorderMode
) ->
Image

Apply a general affine transform to the image.

matrix is [a, b, c, d, e, f] defining the forward mapping:

  • x' = a*x + b*y + c
  • y' = d*x + e*y + f

  • dst_h, dst_w: output image dimensions.
  • interp: interpolation method (Nearest or Bilinear).
  • mode: border handling for samples outside the source (default Constant(0, 0, 0, 255)).

Each output pixel is sampled from the inverse-mapped source location.

#
apply_lut

fn apply_lut(img :
Image
, r_lut : Array[Byte], g_lut : Array[Byte], b_lut : Array[Byte], a_lut : Array[Byte]?) ->
Image

Apply per-channel lookup tables (each 256 entries). When a_lut is None, the alpha channel is copied unchanged.

  • r_lut, g_lut, b_lut: 256-entry tables indexed by the source byte.
  • a_lut: optional 256-entry table for the alpha channel.

#
apply_lut_uniform

fn apply_lut_uniform(img :
Image
, lut : Array[Byte]) ->
Image

Apply a single 256-entry LUT to the RGB channels uniformly; alpha is copied unchanged.

  • lut: 256-entry table indexed by the source byte.

#
augment_pipeline

Apply a sequence of augmentations in order.

Folds pipeline left-to-right over img, dispatching each Augmentation variant to its underlying operation. May raise from crop/resize.

#
augment_random_choice

Randomly choose and apply one augmentation from a weighted list.

choices pairs each Augmentation with a non-negative weight; the chosen entry is sampled proportionally and applied via augment_pipeline. Returns a clone of the input when choices is empty.

#
auto_contrast

fn auto_contrast(img :
Image
, cutoff : Int) ->
Image

Automatic contrast stretch with histogram cutoff.

Builds the image histogram, discards cutoff percent of the pixel count from each end, then linearly stretches the remaining intensity range to the full 0–255 range. The alpha channel is preserved.

#
bilateral_filter

fn bilateral_filter(img :
Image
, d : Int, sigma_color : Double, sigma_space : Double) ->
Image

Bilateral filter: edge-preserving denoising. Uses reflect boundary mode.

  • d: neighbourhood diameter; if <= 0, computed as max(5, 2 * ceil(3 * sigma_space) + 1).
  • sigma_color: tonal weight, in [0, 1] (normalized RGB distance).
  • sigma_space: spatial weight.

#
black_hat

Black-hat: closing minus original.

Isolates small dark structures smaller than kernel that are filled by the closing.

#
box_blur

Box blur using per-channel integral images (O(1) per pixel). Border windows are shrunk to the valid area.

#
canny

fn canny(img :
Image
, low : Double, high : Double) ->
Image

Canny edge detector producing a binary edge image.

The pipeline smooths the image with a Gaussian blur, computes Sobel gradients, applies non-maximum suppression along the gradient direction, and uses dual-threshold hysteresis to trace the final edges.

  • low: lower hysteresis threshold (0–255). Responses below this are discarded.
  • high: upper hysteresis threshold (0–255). Responses at or above this become strong edges; responses between low and high are kept only if connected to a strong edge.

Returns a grayscale image where edge pixels are 255 and all others are 0.

#
clahe

fn clahe(img :
Image
, clip_limit : Double, grid_size : (Int, Int)) ->
Image

CLAHE (Contrast-Limited Adaptive Histogram Equalization).

Divides the image into grid_size tiles, clips each tile histogram at clip_limit (as a fraction of the tile pixel count) and redistributes the excess uniformly, equalizes each tile, and blends neighbouring tile LUTs with bilinear interpolation. Operates on luminance and emits a grayscale result; alpha is preserved.

#
clamp_byte

fn clamp_byte(v : Int) -> Byte

Convert an integer to a byte, clamping into [0, 255].

#
clampd

fn clampd(v : Double, lo : Double, hi : Double) -> Double

Clamp a double into the inclusive range [lo, hi].

#
clampi

fn clampi(v : Int, lo : Int, hi : Int) -> Int

Clamp an integer into the inclusive range [lo, hi].

#
composite_color

fn composite_color(img :
Image
, mask :
Image
, r : Byte, g : Byte, b : Byte) ->
Image

Composite a solid color onto img using mask as the alpha source. Where mask is bright, the color is applied more strongly.

  • mask: its red channel (normalized to [0, 1]) is used as the blend weight.
  • r, g, b: the color to composite.

#
composite_over

Alpha "over" compositing of src onto dst; result matches dst size. Source pixels outside its bounds are treated as fully transparent.

#
connected_components

fn connected_components(img :
Image
, connectivity : Int) -> (Array[Array[Int]], Int)

Connected components labelling (4- or 8-connectivity). Returns (labels, num_labels) where labels[y][x] is the label (1-based).

Non-black pixels (any RGB channel non-zero) are treated as foreground. connectivity should be 4 (orthogonal neighbours) or 8 (also include diagonals). Labels are compacted to a contiguous 1..num_labels range; background pixels keep label 0.

#
convolve

fn convolve(img :
Image
, kernel : Array[Array[Double]], normalize : Bool) ->
Image

General 2D convolution over the RGB channels (border: replicate). When normalize is true, the kernel is divided by its sum (if non-zero).

  • img: input RGBA8 image; alpha is copied unchanged.
  • kernel: 2D weight matrix; the anchor is the centre (kh/2, kw/2).
  • normalize: divide the response by the kernel sum when true, by 1.0 otherwise.

#
corner_harris

fn corner_harris(img :
Image
, k : Double) -> Array[Array[Double]]

Harris corner response map.

Computes the Gaussian-weighted structure tensor of the image's luma gradients and returns the Harris response det(M) - k * trace(M)² per pixel.

  • k: Harris sensitivity parameter, typically in the range 0.04–0.06.

Returns an h × w array of response values; large positive values indicate corners.

#
corner_shi_tomasi

fn corner_shi_tomasi(img :
Image
) -> Array[Array[Double]]

Shi-Tomasi corner response map.

Computes the Gaussian-weighted structure tensor of the image's luma gradients and returns the smaller eigenvalue of the tensor per pixel.

Returns an h × w array of response values; large values indicate corner-like regions.

#
count_nonzero

fn count_nonzero(img :
Image
) -> Int

Count non-zero (non-black) pixels.

A pixel is counted when any of its RGB channels is non-zero; alpha is ignored.

#
count_pixels

fn count_pixels(img :
Image
, threshold : Byte) -> Int

Count pixels whose luma exceeds threshold.

Uses strict greater-than comparison on the BT.601 luma.

#
crop

Crop a rectangular region starting at (y, x) with size h x w.

  • y, x: top-left corner of the region (inclusive).
  • h, w: height and width of the region.

Raises ImageError if the region lies outside the image bounds.

#
decode

Decode a buffer using the decoder registered for ext.

Raises ImageError when no decoder is registered for ext or the decoder rejects the buffer.

#
dilate

Morphological dilation.

Each output pixel is the maximum of the RGB channel values over the structuring element defined by kernel, growing bright regions. Border coordinates are clamped (replicate); alpha is preserved.

#
draw_circle

fn draw_circle(img :
Image
, cy : Int, cx : Int, radius : Int, r : Byte, g : Byte, b : Byte, a : Byte, fill : Bool, width : Int) ->
Image

Draw a circle. When fill is false, width sets the stroke thickness.

Centred at (cy, cx) with the given radius. Filled circles scan each row over the implicit x² + y² <= r² disc; outlines use the midpoint circle algorithm. Uses source-over alpha blending; the input image is not modified.

#
draw_ellipse

fn draw_ellipse(img :
Image
, cy : Int, cx : Int, ry : Int, rx : Int, r : Byte, g : Byte, b : Byte, a : Byte, fill : Bool, width : Int) ->
Image

Draw an ellipse. When fill is false, width sets the stroke thickness.

Centred at (cy, cx) with semi-axes ry (rows) and rx (columns). Outlines use the midpoint ellipse algorithm; filled ellipses scan each row over the implicit disc. Uses source-over alpha blending; the input image is not modified.

#
draw_line

fn draw_line(img :
Image
, y0 : Int, x0 : Int, y1 : Int, x1 : Int, r : Byte, g : Byte, b : Byte, a : Byte, width : Int) ->
Image

Draw a line with the Bresenham algorithm and the given stroke width.

Connects (y0, x0) to (y1, x1) inclusive, stamping a width x width block at each rasterized pixel using source-over alpha blending. The input image is not modified.

#
draw_pixel

fn draw_pixel(img :
Image
, y : Int, x : Int, r : Byte, g : Byte, b : Byte, a : Byte) ->
Image

Draw a single pixel (alpha blended). Returns a new image.

Sets the pixel at (y, x) to (r, g, b) alpha-composited with the existing colour using source-over blending. Out-of-bounds coordinates are ignored. The input image is not modified.

#
draw_polygon

fn draw_polygon(img :
Image
, points : Array[(Int, Int)], r : Byte, g : Byte, b : Byte, a : Byte, fill : Bool, width : Int) ->
Image

Draw a polygon. When fill is false, width sets the stroke thickness.

points is an array of (y, x) vertices connected in order, closing the path from the last back to the first. Filled polygons use a scanline fill; outlines call draw_line for each edge. Returns the input image unchanged when fewer than two vertices are supplied. The input image is not modified.

#
draw_rect

fn draw_rect(img :
Image
, y : Int, x : Int, h : Int, w : Int, r : Byte, g : Byte, b : Byte, a : Byte, fill : Bool, width : Int) ->
Image

Draw a rectangle. When fill is false, width sets the stroke thickness.

The rectangle spans rows [y, y + h) and columns [x, x + w). When fill is true the interior is filled; otherwise a stroked outline of thickness width is drawn. Uses source-over alpha blending; the input image is not modified.

#
encode

Encode an image using the encoder registered for ext.

Raises ImageError when no encoder is registered for ext or the encoder fails.

#
equalize_histogram

Histogram equalization applied to each RGB channel independently.

Each channel is remapped through its normalized cumulative distribution function so the output spans the full [0, 255] range. The alpha channel is copied unchanged.

#
equals

Pixel-exact equality (same dimensions and bytes).

#
erode

Morphological erosion.

Each output pixel is the minimum of the RGB channel values over the structuring element defined by kernel, shrinking bright regions. Border coordinates are clamped (replicate); alpha is preserved.

#
find_contours

fn find_contours(img :
Image
) -> Array[Array[(Int, Int)]]

Find contours as arrays of (y, x) boundary pixels (simple boundary trace).

Labels the image with 4-connectivity, then for each label collects pixels that have at least one 4-neighbour with a different label (or lie on the image edge). Returns one array per connected component.

#
flatten_alpha

fn flatten_alpha(img :
Image
, r : Byte, g : Byte, b : Byte) ->
Image

Composite the image over a solid background color, dropping transparency.

  • r, g, b: the solid background color; output alpha is forced to 255.

#
flip_horizontal

Mirror horizontally (left-right).

#
flip_vertical

Mirror vertically (top-bottom).

#
flood_fill

fn flood_fill(img :
Image
, y : Int, x : Int, r : Byte, g : Byte, b : Byte, a : Byte, tolerance : Byte) ->
Image

Flood fill: replace the colour at (y, x) and all connected pixels of similar colour (within tolerance) with the new colour.

4-connected BFS from the seed pixel; a pixel is replaced when each of its RGBA channels is within tolerance of the seed colour. Out-of-bounds seeds and no-op seeds (fill colour equals seed colour) return a clone of the input. The input image is not modified.

#
from_hsv

fn from_hsv(h : Array[Array[Double]], s : Array[Array[Double]], v : Array[Array[Double]]) ->
Image

HSV → RGBA8. H in [0, 360), S in [0, 1], V in [0, 1].

  • h, s, v: equally-shaped rows × cols arrays; output alpha is 255.

#
from_ycbcr

fn from_ycbcr(y_arr : Array[Array[Double]], cb_arr : Array[Array[Double]], cr_arr : Array[Array[Double]]) ->
Image

YCbCr → RGBA8. Expects Y in [0, 255], Cb/Cr centred at 128. Output alpha is 255.

#
gaussian_blur

fn gaussian_blur(img :
Image
, sigma : Double) ->
Image

Gaussian blur; kernel size is derived from sigma.

  • img: input RGBA8 image; alpha is copied unchanged.
  • sigma: Gaussian standard deviation. The kernel size is clampi(ceil(sigma*3)*2+1, 3, 99); weights are exp(-d²/(2σ²)) normalised, applied as a separable 1D×1D pass with replicate borders.

#
gaussian_blur_kernel

fn gaussian_blur_kernel(img :
Image
, ksize : Int, sigma : Double) ->
Image

Gaussian blur with an explicit (odd) kernel size.

  • img: input RGBA8 image; alpha is copied unchanged.
  • ksize: kernel diameter; an even value is rounded up to the next odd integer.
  • sigma: Gaussian standard deviation; weights are exp(-d²/(2σ²)) normalised, applied as a separable 1D×1D pass with replicate borders.

#
grayscale_to_rgb

Convert a grayscale image (R==G==B) to RGB by forcing alpha to 255.

#
grayscale_to_rgba

Convert a grayscale image to RGBA with fully opaque alpha.

#
histogram

fn histogram(img :
Image
) -> Array[Int]

256-bin luminance histogram.

Returns an array of length 256 where index i holds the number of pixels whose BT.601 luma equals i.

#
histogram_color

fn histogram_color(img :
Image
) -> Array[Array[Int]]

Per-channel histograms for R, G, B (each 256 bins).

Returns [hr, hg, hb] where each entry is a 256-element array of pixel counts for the corresponding channel.

#
histogram_correlation

fn histogram_correlation(img :
Image
, ref_img :
Image
) -> Double

Histogram correlation coefficient between two images' luminance histograms.

Returns the Pearson correlation of the two 256-bin histograms, in [-1.0, 1.0]. Returns 0.0 when either image is empty or the denominator is zero.

#
hit_or_miss

Hit-or-miss transform. The kernel values are: 1 = foreground, 0 = background, -1 = don't care. Only pixels matching the pattern are kept.

Treats non-zero RGB pixels as foreground. A pixel is set to white when its neighbourhood matches kernel exactly (foreground/background), and black otherwise. Output alpha is set to 255.

#
hog

fn hog(img :
Image
, cell_size : Int, block_size : Int, nbins : Int) -> Array[Double]

Histogram of Oriented Gradients (HOG) feature descriptor.

Computes gradients on the luma field, accumulates unsigned gradient orientations (0–180°) into per-cell histograms of nbins bins, then normalizes each overlapping block_size × block_size block of cells with an L2 norm.

  • cell_size: side length (in pixels) of each cell.
  • block_size: side length (in cells) of each normalization block.
  • nbins: number of orientation bins per cell.

Returns a flat array of normalized histogram values.

#
hu_moments

fn hu_moments(img :
Image
) -> Array[Double]

Hu moments (7 translation/scale/rotation invariant moments).

Computed from the normalized central moments of the luma image. Returns a 7-element array; returns all zeros when the image is empty (m00 == 0).

#
info

fn info(img :
Image
) -> String

Human-readable summary of the image.

#
invert

Invert the RGB channels (255 - value), alpha preserved.

#
laplacian

Laplacian edge response as a grayscale image.

Applies the 3×3 Laplacian kernel [[0,1,0],[1,-4,1],[0,1,0]] to the image's luma field, takes the absolute value of the response, and scales it to fill the 0–255 range. The alpha channel is set to 255.

#
lbp

fn lbp(img :
Image
, radius : Int, n_points : Int) -> Array[Int]

Local Binary Pattern (LBP) codes for each pixel.

For every pixel, samples n_points neighbors on a circle of the given radius (using bilinear interpolation) and builds a bitmask of which neighbors are greater than or equal to the center value.

  • radius: circle radius in pixels.
  • n_points: number of sampled neighbors (also the bitmask width).

Returns a flat Array[Int] of length h * w, row-major.

#
lbp_histogram

fn lbp_histogram(img :
Image
, radius : Int, n_points : Int) -> Array[Int]

Histogram of Local Binary Pattern codes over the whole image.

  • radius: circle radius in pixels, forwarded to lbp.
  • n_points: number of sampled neighbors, forwarded to lbp.

Returns an Array[Int] of length 2 ^ n_points containing per-bin counts.

#
match_histogram

Histogram matching: adjust img so its luminance histogram matches ref_img.

Builds the luminance CDFs of both images and remaps each source pixel to the reference value whose CDF is closest. The alpha channel is copied unchanged.

#
max_filter

Maximum (dilation-like) filter with an odd window size. Each output pixel is the max over a (2r+1)² window with replicate borders; alpha is copied unchanged.

#
median_filter

Median filter with an odd window size. Reduces salt-and-pepper noise while preserving edges. Uses replicate borders; alpha is copied unchanged.

#
merge_channels

fn merge_channels(r : Array[Byte], g : Array[Byte], b : Array[Byte], a : Array[Byte]?) ->
Image
raise
ImageError

Merge channel arrays into a 1 × n image (single row). Alpha defaults to fully opaque (255) when None. Raises ImageError on length mismatch.

#
min_filter

Minimum (erosion-like) filter with an odd window size. Each output pixel is the min over a (2r+1)² window with replicate borders; alpha is copied unchanged.

#
moments

fn moments(img :
Image
) -> Array[Double]

Raw image moments (m00, m10, m01, m20, m11, m02, m30, m21, m12, m03).

Uses millow's (y, x) convention where m_pq = Σ y^p · x^q · luma: the first index p is the row (y) exponent, the second q is the column (x) exponent. Each pixel's BT.601 luma is used as the weight. Returns a 10-element array in the order listed above.

#
morph_close

Closing: dilation followed by erosion.

Fills small dark holes and gaps smaller than kernel while preserving the shape of larger regions.

#
morph_gradient

Morphological gradient: dilation minus erosion.

Highlights region edges by computing the per-channel difference between the dilated and eroded images.

#
morph_open

Opening: erosion followed by dilation.

Removes small bright foreground structures smaller than kernel while preserving the shape of larger regions.

#
mse

Mean squared error over all RGBA channels; -1.0 on size mismatch.

#
normalize

fn normalize(img :
Image
, min : Byte, max : Byte) ->
Image

Linearly rescale the RGB value range into [min, max].

Finds the current minimum and maximum across all RGB channels, then maps that range linearly onto [min, max]. If the input range is empty the image is returned unchanged. The alpha channel is preserved.

#
pad

fn pad(img :
Image
, top : Int, right : Int, bottom : Int, left : Int, mode :
PadMode
) ->
Image

Pad the image by adding margins around it.

  • top, right, bottom, left: margin sizes in pixels.
  • mode: PadMode controlling how the new border pixels are filled:
    • Constant(r, g, b, a) — fill with the given color.
    • Replicate — copy the nearest edge pixel.
    • Reflect — mirror with edge duplication.
    • Wrap — tile the source.

The output size is (img.h + top + bottom, img.w + left + right).

#
pad_to_size

Center-pad the image to at least h x w using the chosen border mode.

  • h, w: target minimum dimensions.
  • mode: PadMode for the new border pixels.

Padding is split evenly (left/top-biased on odd sizes). If the image already meets or exceeds a dimension, no padding is added on that axis.

#
prewitt

Prewitt edge magnitude as a grayscale image.

Combines the x and y Prewitt gradients into sqrt(gx² + gy²) per pixel, then scales the result to fill the 0–255 range. The alpha channel is set to 255.

#
process_batch

Apply a function to a batch of images.

Returns a new array where f has been applied to each element of imgs in index order.

#
psnr

Peak signal-to-noise ratio in dB. Returns a large value for identical input.

#
random_brightness

fn random_brightness(img :
Image
, max_factor : Double) ->
Image

Random brightness adjustment within [1-max_factor, 1+max_factor].

Multiplies each RGB channel by a factor sampled uniformly from the symmetric range around 1.0.

#
random_color_jitter

fn random_color_jitter(img :
Image
, brightness : Double, contrast : Double, saturation : Double, hue : Double) ->
Image

Random colour jitter (brightness, contrast, saturation, hue).

Applies brightness and contrast jitter (multiplicative, via random_brightness / random_contrast) when their magnitudes are positive. Saturation jitter scales the HSV S channel by a factor sampled from [1-saturation, 1+saturation] (clamped to [0, 1]). Hue jitter shifts the H channel by a uniform sample in [-hue, hue] degrees (wrapped to [0, 360)). The HSV round-trip is done once when either saturation or hue is requested.

#
random_contrast

fn random_contrast(img :
Image
, max_factor : Double) ->
Image

Random contrast adjustment within [1-max_factor, 1+max_factor].

Scales pixel deviations from the image mean by a factor sampled uniformly from the symmetric range around 1.0.

#
random_crop

fn random_crop(img :
Image
, h : Int, w : Int) ->
Image
raise

Random crop to target size.

Picks a random top-left corner so the h x w window fits inside the image. Returns a clone of the input when it already fits within h x w.

#
random_flip_horizontal

Random horizontal flip (50% probability).

Returns a mirrored copy with probability 0.5, otherwise a clone of the input. Each call draws a fresh random sample.

#
random_gamma

fn random_gamma(img :
Image
, max_gamma : Double) ->
Image

Random gamma within [1/max_gamma, max_gamma].

Samples the exponent log-uniformly so low and high gamma are equally likely, then applies adjust_gamma.

#
random_noise_gaussian

fn random_noise_gaussian(img :
Image
, std : Double) ->
Image

Add Gaussian noise with the given standard deviation.

Adds independent zero-mean Gaussian noise (via Box-Muller) of standard deviation std to each RGB channel; alpha is preserved.

#
random_noise_salt_pepper

fn random_noise_salt_pepper(img :
Image
, prob : Double) ->
Image

Add salt-and-pepper noise with the given probability.

Each pixel is set to black with probability prob / 2 and to white with probability prob / 2; otherwise it is left unchanged. Only RGB channels are affected; alpha is preserved.

#
random_rotate

fn random_rotate(img :
Image
, max_angle : Double) ->
Image

Random rotation within [-max_angle, max_angle] degrees.

Samples an angle uniformly from the symmetric range and rotates with bilinear sampling.

#
regionprops

fn regionprops(labels : Array[Array[Int]], num_labels : Int) -> Array[
RegionProps
]

Compute region properties for each label (1..num_labels).

labels is a 2D label array (e.g. from connected_components) and num_labels the corresponding label count. Returns one RegionProps per label in label order; regions with zero area keep their initial zeroed centroid/bbox.

#
register_decoder

fn register_decoder(ext : String, decoder : &
Decoder
) -> Unit

Register a decoder for a format extension (e.g. "ppm").

Subsequent calls to decode(data, ext) dispatch to decoder. Re-registering an extension replaces the previous entry.

#
register_encoder

fn register_encoder(ext : String, encoder : &
Encoder
) -> Unit

Register an encoder for a format extension (e.g. "ppm").

Subsequent calls to encode(img, ext) dispatch to encoder. Re-registering an extension replaces the previous entry.

#
rescale

Scale both dimensions by a uniform factor.

  • scale: scaling factor (e.g. 0.5 halves, 2.0 doubles).
  • interp: resampling method (Nearest, Bilinear, or Bicubic).

Output dimensions are round(h * scale) x round(w * scale).

#
resize

Resize the image to (dst_h, dst_w) using the given interpolation.

  • dst_h, dst_w: target dimensions in pixels.
  • interp: resampling method (Nearest, Bilinear, or Bicubic).

#
resize_to_cover

Scale to cover min_h x min_w, preserving aspect ratio, cropping overflow.

  • min_h, min_w: target dimensions to fully cover.
  • interp: resampling method (Nearest, Bilinear, or Bicubic).

The image is scaled then center-cropped to (min_h, min_w). Returns a clone if the image is empty.

#
resize_to_fit

fn resize_to_fit(img :
Image
, max_h : Int, max_w : Int, interp :
Interp
) ->
Image

Scale to fit inside max_h x max_w, preserving aspect ratio.

  • max_h, max_w: maximum bounding dimensions.
  • interp: resampling method (Nearest, Bilinear, or Bicubic).

The result fits entirely within the box and at least one dimension equals its bound. Returns a clone if the image is empty.

#
rotate_180

Rotate 180 degrees.

#
rotate_270

Rotate 270 degrees clockwise (90 counter-clockwise).

#
rotate_90

Rotate 90 degrees clockwise.

#
rotate_any

Rotate the image by an arbitrary angle in degrees, clockwise.

  • angle: rotation angle in degrees (positive = clockwise).
  • interp: interpolation method (Nearest or Bilinear).

The output canvas expands to fit the entire rotated image; areas outside the source are filled as opaque black (0, 0, 0, 255).

#
round_byte

fn round_byte(v : Double) -> Byte

Round a double and clamp into a byte range [0, 255].

#
scharr

Scharr edge magnitude as a grayscale image.

Combines the x and y Scharr gradients into sqrt(gx² + gy²) per pixel, then scales the result to fill the 0–255 range. The Scharr kernel is more rotationally symmetric than Sobel, giving more accurate gradients. The alpha channel is set to 255.

#
sharpen

Laplacian-based sharpening. strength scales the high-frequency boost; 0.0 returns the input unchanged.

#
shear

Shear the image.

  • sh_x: horizontal shear factor (x shifts by sh_x * y).
  • sh_y: vertical shear factor (y shifts by sh_y * x).
  • interp: interpolation method (Nearest or Bilinear).
  • mode: border handling for samples outside the source (default Constant(0, 0, 0, 255)).

The output canvas expands to hold the sheared image.

#
skeletonize

Skeletonize via iterative thinning (Zhang-Suen). Works on binary images (non-zero pixels are foreground).

Iteratively removes boundary pixels over two sub-iterations until no further pixels can be thinned, yielding a one-pixel-wide medial axis. Border pixels are left untouched.

#
sobel

Sobel edge magnitude as a grayscale image.

Combines the x and y Sobel gradients into sqrt(gx² + gy²) per pixel, then scales the result to fill the 0–255 range. The alpha channel is set to 255.

#
sobel_x

fn sobel_x(img :
Image
) -> Array[Array[Float]]

Compute the Sobel horizontal (x-direction) gradient response.

Convolves the image's luma field with the 3×3 Sobel-x kernel using Reflect border handling.

Returns an h × w array of per-pixel gradient values as Float.

#
sobel_y

fn sobel_y(img :
Image
) -> Array[Array[Float]]

Compute the Sobel vertical (y-direction) gradient response.

Convolves the image's luma field with the 3×3 Sobel-y kernel using Reflect border handling.

Returns an h × w array of per-pixel gradient values as Float.

#
ssim

Structural Similarity Index (SSIM) between two images. Returns a value in [-1, 1] where 1 means identical.

#
standardize

fn standardize(img :
Image
) -> Array[Array[Double]]

Standardize the image to zero mean and unit standard deviation.

Computes the mean and standard deviation of the per-pixel luma, then returns (luma - mean) / std for each pixel. A zero-variance image uses std = 1.0 to avoid division by zero.

Returns an h × w array of Double values.

#
threshold

Binarize by luminance: pixels with luma >= thresh become white.

Each pixel's BT.601 luma is compared against thresh; foreground pixels are set to white (255, 255, 255) and background pixels to black. The alpha channel is preserved.

#
threshold_inv

fn threshold_inv(img :
Image
, thresh : Byte) ->
Image

Inverse binarization: pixels with luma >= thresh become black.

The inverse of threshold: foreground pixels (luma >= thresh) are set to black and background pixels to white. The alpha channel is preserved.

#
threshold_otsu

Otsu adaptive binarization. Returns the chosen threshold and the result.

Selects the threshold that maximizes the inter-class variance of the luminance histogram and applies threshold. The returned Byte is the applied cut (one above the background class) so values >= it are foreground.

#
threshold_sauvola

fn threshold_sauvola(img :
Image
, window_size : Int, k : Double) ->
Image

Sauvola local thresholding. Computes a threshold per pixel based on the local mean and standard deviation within a window_size × window_size window. Suitable for images with uneven illumination.

The per-pixel threshold is mean * (1 + k * (std / 128 - 1)). Border coordinates are clamped (replicate). window_size should be odd. Returns a binary image; alpha is preserved.

Uses integral images for O(1) window statistics (O(n) total instead of O(n * window_size²)).

#
thumbnail

fn thumbnail(img :
Image
, max_h : Int, max_w : Int) ->
Image

Downscale to fit within max_h x max_w (nearest-neighbor).

#
tint

fn tint(img :
Image
, r : Byte, g : Byte, b : Byte) ->
Image

Multiplicative color tint: each channel scaled by color / 255.

  • r, g, b: tint color; (255, 255, 255) leaves the image unchanged.

#
to_bgr

Swap the red and blue channels.

#
to_display_bytes

fn to_display_bytes(img :
Image
) -> Array[Byte]

RGB-only byte buffer (alpha dropped), length h * w * 3.

#
to_grayscale

ITU-R BT.601 grayscale: (R*77 + G*150 + B*29) >> 8, alpha preserved.

#
to_grayscale_weighted

fn to_grayscale_weighted(img :
Image
, r_w : Double, g_w : Double, b_w : Double) ->
Image

Grayscale with custom linear weights, alpha preserved.

  • r_w, g_w, b_w: per-channel weights (typically summing to 1.0).

#
to_hsv

fn to_hsv(img :
Image
) -> (Array[Array[Double]], Array[Array[Double]], Array[Array[Double]])

RGB → HSV. Returns three h × w arrays: H in [0, 360), S in [0, 1], V in [0, 1].

#
to_pgm

fn to_pgm(img :
Image
) -> Array[Byte]

Encode as a binary PGM (P5) grayscale byte stream.

#
to_ppm

fn to_ppm(img :
Image
) -> Array[Byte]

Encode as a binary PPM (P6) byte stream.

#
to_rgb

Drop the alpha channel, returning an RGB-only image (A=255 forced).

#
to_rgba

Ensure the image has an alpha channel (A=255 if missing). Since all millow images are RGBA8, this is a no-op that forces A=255.

#
to_ycbcr

fn to_ycbcr(img :
Image
) -> (Array[Array[Double]], Array[Array[Double]], Array[Array[Double]])

RGB → YCbCr (ITU-R BT.601). Returns three h × w arrays (Y, Cb, Cr). Y is in [0, 255]; Cb and Cr are centred at 128.

#
top_hat

Top-hat: original minus opening.

Isolates small bright structures smaller than kernel that are removed by the opening.

#
translate

Translate the image by (dy, dx) pixels.

  • dy: vertical shift in pixels (positive = downward).
  • dx: horizontal shift in pixels (positive = rightward).
  • interp: interpolation method (Nearest or Bilinear).

The output keeps the source dimensions; areas uncovered by the source become opaque black (0, 0, 0, 255).

#
unsharp_mask

fn unsharp_mask(img :
Image
, radius : Double, amount : Double, threshold : Byte) ->
Image

Unsharp masking: add a scaled high-pass component above a threshold.

  • radius: Gaussian blur radius used to derive the low-pass component.
  • amount: scaling factor applied to the high-pass residual.
  • threshold: only pixels whose |orig - blurred|threshold are boosted; smaller differences are passed through unchanged.

Source Files