butteraugli

Butteraugli perceptual image-difference metric for Elixir, backed by the butteraugli Rust crate via Rustler. The crate is a port of Google's butteraugli implementation from libjxl.

Note: this binding currently pins butteraugli to a git revision because the cooperative-cancellation API (*_with_stop) has not yet landed in a crates.io release. We will switch to a proper versioned as soon as possible.

Installation

def deps do
[{:butteraugli, github: "hlindset/butteraugli"}]
end

What is Butteraugli?

Butteraugli estimates the perceived difference between two images using a model of human vision. Unlike simple pixel-wise metrics (PSNR, MSE), butteraugli accounts for:

Quality Thresholds

ScoreInterpretation
< 1.0Images appear identical to most viewers
1.0 - 2.0Subtle differences may be noticeable
> 2.0Visible differences between images

Usage

Inputs are packed binaries; the layout is selected with the :format option (default :rgb888).

formatelementcolor spaceuse case
:rgb888 (default)u8sRGB (gamma)Standard 8-bit images
:linear_rgbf32linear RGBHDR, 16-bit, float pipelines
{:ok, %Butteraugli.Result{score: score}} =
Butteraugli.compare(ref_rgb, dist_rgb, width, height)

Butteraugli.Result carries score (max-norm distance), pnorm_3 (libjxl 3-norm aggregation), and diffmap. Images smaller than 8x8 are padded up to butteraugli's floor (8x8) and scored. Diffmaps are cropped back to the input size before being returned.

The diffmap is nil unless you opt in:

{:ok, %Butteraugli.Result{diffmap: diffmap}} =
Butteraugli.compare(ref, dist, width, height, compute_diffmap: true)

Two tuning parameters adjust the perceptual model:

Butteraugli.compare(ref, dist, w, h, intensity_target: 250.0, hf_asymmetry: 1.5)

Both fall back to crate defaults when omitted.

For a quality-search loop comparing many candidates against one original, reuse the reference. Tuning parameters are baked into the reference at build time:

{:ok, ref} = Butteraugli.Reference.new(original_rgb, width, height)
{:ok, s1} = Butteraugli.Reference.compare(ref, candidate1_rgb)
{:ok, s2} = Butteraugli.Reference.compare(ref, candidate2_rgb)

Butteraugli.Reference.compare/3 takes prefer: :speed | :memory (default :speed). :speed reuses the precomputed reference (~2x faster, cancellation checked only at the start); :memory runs a strip-bounded walker with bounded peak memory and per-strip mid-flight cancellation, giving up the speedup. See Cancellation.

Cancellation

Butteraugli.compare/5 and Butteraugli.Reference.compare/3 accept cancel: (a Butteraugli.CancelRef) and timeout: (milliseconds):

cancel_ref = Butteraugli.CancelRef.new()
# ... from another process, on client disconnect / deadline:
Butteraugli.cancel(cancel_ref)
# aborted calls return {:error, :cancelled} or {:error, :timeout}
Butteraugli.compare(ref, dist, w, h, cancel: cancel_ref, timeout: 5_000)

A cancel ref is single-use and can cover a whole batch.

Granularity

Butteraugli.compare/5 on images >= 8x8 (either format) checks the ref between strips, so it aborts mid-computation. Two paths check the ref once at the start instead: sub-8x8 images (padded onto the non-strip path) and Butteraugli.Reference.compare/3 with the default prefer: :speed (which reuses the precomputed reference for the ~2x speedup). These abort a ref that is already cancelled when the call begins (so batch cancellation works — cancel once, every subsequent compare aborts), but do not interrupt a compare already underway. Butteraugli.Reference.compare/3 with prefer: :memory opts into the strip-bounded walker, which aborts mid-computation (per strip) at the cost of the speedup.

So to let a cancel:/timeout: interrupt one long compare partway through (bounding its wall-clock), use Butteraugli.compare/5 (which only does strip processing) on a >= 8x8 image or Reference.compare(ref, dist, prefer: :memory).

With Vix

If :vix is a dependency, you can pass images directly (coerced to 8-bit sRGB):

{:ok, %Butteraugli.Result{}} = Butteraugli.Vix.compare(ref_image, dist_image)

Releasing

Precompiled NIFs are built by the GitHub release workflow on a v* tag. See RELEASING.md for the full publish checklist.

Building from source

A Rust toolchain is only needed if you build the NIF locally instead of using a precompiled artifact — i.e. on a target not covered by the release matrix, or when forcing a build with BUTTERAUGLI_BUILD=1. In that case butteraugli requires Rust ≥ 1.89 (the crate pins that MSRV).

LLM Development Notice

This library was developed with help from LLMs.

License

This wrapper is released under BSD-3-Clause, matching butteraugli.