Skip to content
Sample of Median-Cut Color Quantizer

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.

Family
toxi_openframeworks
Source
toxi · openFrameworks
Evolution
pde
Render
quantized_image
Complexity
medium
Agents
0
Year
2010
Author
Karsten Schmidt (toxi) / Paul Heckbert (algorithm)

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

Sample of Generative Type Letterforms

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.

View →
Sample of Gray-Scott Reaction-Diffusion

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.

View →
Sample of Pixel Sort (Glitch Art)

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.

View →
Sample of Sound-Reactive Particle Field (Synthetic Audio)

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.

View →
Sample of Verlet Particle-Spring Network

Verlet Particle-Spring Network

toxi_openframeworks · ParticleSpring2D

A Verlet-integrated particle network connected to its k-nearest neighbours by Hooke's-law springs.

View →
Sample of Voronoi Tessellation with Lloyd Relaxation

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.

View →

See all toxi_openframeworks artworks →