toxi_openframeworks · ColorQuantizer
Median-Cut Color Quantizer
Heckbert's 1982 median-cut color quantization applied to an evolving procedural gradient + low-resolution noise wash. The source image is regenerated with a new noise/gradient seed each step, then its pixels are partitioned into `num_colors` buckets by recursively splitting the longest channel range at the median, and remapped to the nearest palette entry in either RGB or CIE Lab distance.
Original: https://github.com/postspectacular/toxiclibs
▶ Try the live demo (JS port) ▶ Run reference.py via Pyodide 10 formulas have instant JS ports. The Pyodide path runs the actual Python in your browser — first load is ~6 MB, then instant.
Math essence
Median-cut: repeatedly take the bucket with the largest (max - min) along any RGB axis, sort that bucket along that axis, split at the median into two sub-buckets. After `log2(num_colors)` recursive splits, each leaf bucket's mean is a palette color. Mapping each pixel to its nearest palette entry (Euclidean in RGB or Lab) gives a posterized, banded image. The CIE Lab branch uses sRGB->linear->XYZ->Lab gamma correction so perceptual banding (more even luminance steps) is achievable.
Parameters
| Name | Type | Default | Range | Description |
|---|---|---|---|---|
dim | int | 256 | 128–800 | Square canvas side length in pixels. |
num_colors | int | 16 | 2–64 | Palette size after median-cut partitioning. |
color_space | str | rgb | — | Distance metric for nearest-palette assignment (RGB euclidean or perceptual CIE Lab). |
grad_freq | float | 3 | 1–12 | Spatial frequency of the per-channel sinusoidal gradient. |
noise_freq | int | 6 | 2–32 | Side length of the low-resolution noise grid (interpolated bilinearly). |
noise_amount | float | 0.25 | 0–1 | Magnitude of the noise contribution to the source image. |
regenerate_every | int | 1 | 1–30 | How many steps between source-image / noise-grid resampling. |
seed | int | null | 0–999999 | Optional RNG seed for reproducibility. |
Reference
First 60 lines of reference.py. Full source:
library_external/toxi_openframeworks/ColorQuantizer/reference.py
from __future__ import annotations
from typing import ClassVar
import numpy as np
def _median_cut(pixels: np.ndarray, num_colors: int) -> np.ndarray:
"""Recursive median-cut palette extraction (Heckbert 1982).
`pixels` is an (N, 3) float array of RGB values in [0, 1]. The
algorithm repeatedly splits the pixel bucket with the longest
channel range at its median, producing `num_colors` buckets whose
per-bucket mean becomes the palette entry.
This is the same approach Karsten Schmidt uses in the early
`toxiclibs.color` median-cut demos and now codified in
@thi.ng/color. Reference: Heckbert, "Color Image Quantization for
Frame Buffer Display", SIGGRAPH 1982.
"""
buckets: list[np.ndarray] = [pixels]
while len(buckets) < num_colors:
ranges = np.array([b.max(axis=0) - b.min(axis=0) for b in buckets])
idx = int(np.argmax(ranges.max(axis=1)))
if ranges[idx].max() <= 1e-6:
break
bucket = buckets.pop(idx)
channel = int(np.argmax(ranges[idx]))
order = np.argsort(bucket[:, channel])
bucket = bucket[order]
mid = len(bucket) // 2
left = bucket[:mid]
right = bucket[mid:]
if left.size == 0 or right.size == 0:
buckets.append(bucket)
continue
buckets.append(left)
buckets.append(right)
palette = np.stack([b.mean(axis=0) for b in buckets], axis=0)
return np.clip(palette, 0.0, 1.0)
class ColorQuantizer:
"""Median-cut color quantizer (Heckbert / toxiclibs).
A palette is extracted from an evolving procedural image (a
multi-octave gradient + Perlin-ish noise wash) using the
Heckbert 1982 median-cut algorithm. The full image is then
remapped to the closest palette color in CIE Lab space, yielding
the posterized, banded look that Karsten Schmidt pioneered in his
early openFrameworks demos and that the `toxiclibs.color` library
(now `@thi.ng/color`) carries forward. On each `step` the source
image is regenerated with a slightly different seed so the
quantized output drifts through palette space over time.
"""
family: ClassVar[str] = "toxi_openframeworks"
sketch: ClassVar[str] = "colorquantizer"
def __init__( Try running it locally
The reference implementation is self-contained Python with numpy. Install the package and run:
python library_external/toxi_openframeworks/ColorQuantizer/reference.py Need help? See Learn for the standard environment setup, or Playground for an in-browser sketchbook.
More from toxi · openFrameworks
Generative Type Letterforms
toxi_openframeworks · GenerativeTypeLetterforms
Letterforms assembled from a 9×9 stroke grid alphabet (A–Z subset), then modulated by a parametric function (bar widths and stroke positions follow `a * sin(b*x + c)` style curves) and optionally recursively subdivided into smaller marks.
Gray-Scott Reaction-Diffusion
toxi_openframeworks · ReactionDiffusionGS
The canonical two-species Gray-Scott reaction-diffusion model on a periodic square grid: dU/dt = Du*lap(U) - U*V^2 + F*(1-U), dV/dt = Dv*lap(V) + U*V^2 - (F+k)*V.
Pixel Sort (Glitch Art)
toxi_openframeworks · PixelSort
Per-row pixel sorting à la Kim Asendorf's `ASDFPixelSort` and Karsten Schmidt's own glitch-art Processing sketches.
Sound-Reactive Particle Field (Synthetic Audio)
toxi_openframeworks · SoundReactiveField
Particles advected by a force field whose horizontal component is driven by a synthetic audio signal: a chain of N sinusoids at different frequencies/amplitudes (the 'audio'), with the audio's instantaneous value plus its time derivative ('beat onset') driving the field.
Verlet Particle-Spring Network
toxi_openframeworks · ParticleSpring2D
A Verlet-integrated particle network connected to its k-nearest neighbours by Hooke's-law springs.
Voronoi Tessellation with Lloyd Relaxation
toxi_openframeworks · VoronoiRelaxation
Lloyd's algorithm applied to a jittered point cloud: each seed is replaced with the centroid of its Voronoi cell, repeatedly, producing ever-more-uniform polygonal cells.