Skip to main content

Tramage

Tramage is palette-structure-aware error diffusion dithering. Give it an image and a palette, and it dithers as ordinary vector Floyd-Steinberg would, except that the error carried between pixels is first reshaped along the palette’s own geometry in Oklab, computed once per palette, so error is passed through where the palette has real extent and damped where it does not. If your palette is a family of related tones rather than a broad spread of color, and dithered output has always looked grainier or noisier than it should, this is built for that case.

Near-degenerate palette: baseline shows visible noise in the center crop that the shaped variants damp

What it does not do

Tramage does not invent colors the palette lacks. Given a color the palette cannot represent exactly, both this method and ordinary error diffusion produce the same kind of approximation; what changes is where the resulting, unavoidable noise is allowed to sit. Directed away from directions the palette has no room for, it tends to show up as finer, quieter noise rather than the slow, low-frequency drift that unshaped diffusion leaves behind.

Installation

pip install tramage

See docs/quickstart.md for a minimal working example, and docs/for-designers.md for a plain-language explanation of why this matters for palette-based work in particular.

All three variants produce visually identical output on an exactly rank-deficient palette

The image above is a null result, shown honestly: on a palette that is exactly confined to a single plane in Oklab, vector Floyd-Steinberg and shaped diffusion produce identical output. This is the inertness lemma (see docs/mathematical-appendix.md): error orthogonal to a palette’s affine hull can never change a nearest-neighbor decision, so there is nothing for shaping to protect against. See gallery/README.md for the full set of comparisons, including error spectra and the numbers behind the noise reduction shown above.

How it works

Standard vector error diffusion propagates the full quantization error vector to neighboring pixels. This package instead propagates $e’ = Me$, where $M$ is derived from the palette’s own covariance in Oklab: eigendecompose the covariance, shrink each eigenvalue to a gain of $\lambda / (\lambda + \varepsilon)$, and reassemble. Axes where the palette has real extent keep a gain near 1; near-degenerate axes are damped toward 0. The result is a single global matrix, closed-form and computed once per palette; dithering itself costs one 3×3 matrix multiply per pixel beyond ordinary error diffusion.

A per-cell variant — one matrix per palette color, computed from its nearest neighbors — is also implemented and exported, but is not part of the recommended method. On the 124-image corpus run (see experiments/dithering_quality/) it holds up on noise reduction but loses on both tone accuracy and the low-frequency worm metric from K=16 onward; natural-image k-means palettes are globally degenerate enough that the local variant's added complexity does not pay off. It remains in the package as the historical record of an explored extension, not as a second contribution.

For reference, the package also ships a matrix-valued Damera–Venkata filter (dv_filters, dv_diffuse), the exact prior art in this space. On the 124-image corpus it is the strongest unshaped baseline tested, and shaped Floyd–Steinberg matches or beats it at the small-to-moderate palette sizes the method targets (see experiments/kb_baseline/).

Limitations

This is an honest account of where the method is weak, not just where it is strong.

  • It offers nothing on palettes with real extent in every direction. For a broad, roughly isotropic palette, the shaping matrix approaches the identity and output is indistinguishable from ordinary vector Floyd-Steinberg.
  • On palettes that are close to, but not exactly, degenerate, shaping is a trade-off rather than a free improvement: it reduces noise at some cost to fidelity in directions the palette can barely represent. The epsilon parameter controls this trade, and choosing it well requires some judgment about the palette at hand. On the Kodak subset this trade is free for K ≤ 64 and appears at K ≥ 128, where the palette has enough colors to start representing the formerly-flat direction; on CQ100 no tone cost appears at any K.
  • At K = 8 on the Kodak subset, shaping raises Floyd-Steinberg's low-frequency ("worm") error by 52% (median of per-image ratios; pooled means give +41%), while on CQ100 it lowers it by 32%; the full-corpus median is −23%. The sign reversal is the convergence mechanism, not a caveat: shaping pulls low-frequency error toward a common level, so images beginning below that level rise and those beginning above it fall. Per-K detail: experiments/kb_baseline/.
  • The evaluation spans 124 images across two stock-photo corpora (Kodak, CQ100), but stock photography is still not designer practice. A broader run (real designer palettes; other test sets) is the natural next step; see experiments/README.md.
  • Nothing here addresses palette choice itself. A palette that is a poor fit for an image will still dither poorly; this method only changes what happens to the error once quantization has already committed to that palette.

Repository structure

  • src/tramage/: the package. colour.py (sRGB and Oklab conversion), shaping.py (matrix construction), diffusion.py (serpentine error diffusion), metrics.py (fidelity and noise measures), palette.py (palette extraction and eigenspectrum reporting).
  • prototypes/: the original research scripts, kept verbatim as the historical record of the work.
  • experiments/: the evaluation harness and results — 124 images (Kodak + CQ100): dithering quality, filter baselines (KB/DV), palette geometry, color space.
  • gallery/: before-and-after comparisons, reproducible using only the installed package (no prototype code).
  • docs/: a quickstart, a plain-language explanation for designers, and the mathematical appendix.
  • PREREGISTRATION.md: the analysis plan as written. The numbers it cites (e.g. the 45% worm cost) predate the 124-image re-run and are kept unchanged as the historical record; treat any discrepancy with experiments/ results as provenance, not error.
  • tests/: round-trip color conversion, the isotropic limit, the inertness lemma, and energy conservation of the diffusion kernel.

Citing

See CITATION.cff.

Licence

MIT. See LICENSE.

Metadata

Release files for tramage 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tramage 0.2.0
File Size Uploaded Provenance Actions
tramage-0.2.0.tar.gz 13.2 MB With provenance Details for tramage-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tramage 0.2.0
File Interpreter ABI Platform Actions
tramage-0.2.0-py3-none-any.whl Python 3 none any Details for tramage-0.2.0-py3-none-any.whl

Total release size: 13.2 MB

Release files / tramage-0.2.0.tar.gz

Download URL tramage-0.2.0.tar.gz
Size 13.2 MB
Tags Source
SHA-256 checksum
How to use checksums
39f46e76f99e0398a961e5c9d83752fedfff08a4a09957874d2d2b3651a78257
BLAKE2b-256 checksum
How to use checksums
2fb323db7857ee28285af5021951b2fc9ad90b257fa83df69bd11b1cb65bcad6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / tramage-0.2.0-py3-none-any.whl

Download URL tramage-0.2.0-py3-none-any.whl
Size 42.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
75287666e64ee1ec58a86d9ad74d48bd96a9326f6b4187a3908f2c39b6a26f43
BLAKE2b-256 checksum
How to use checksums
a52300e1f7701e04b97b5c0123aa2822ac6b93f63bc2201a0efa3ac7af1487e1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Security for tramage 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for tramage-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page