# butteraugli
[Butteraugli](https://github.com/imazen/butteraugli) perceptual
image-difference metric for Elixir, backed by the
[`butteraugli`](https://crates.io/crates/butteraugli) Rust crate via Rustler.
The crate is a port of Google's butteraugli implementation from
[libjxl](https://github.com/libjxl/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
```elixir
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:
- **Opsin dynamics**: Photosensitive chemical responses in the retina
- **XYB color space**: Hybrid opponent/trichromatic representation
- **Visual masking**: How image features hide or reveal differences
- **Multi-scale analysis**: UHF, HF, MF, LF frequency bands
## Quality Thresholds
| Score | Interpretation |
| --------- | --------------------------------------- |
| < 1.0 | Images appear identical to most viewers |
| 1.0 - 2.0 | Subtle differences may be noticeable |
| > 2.0 | Visible differences between images |
## Usage
Inputs are packed binaries; the layout is selected with the `:format` option
(default `:rgb888`).
| format | element | color space | use case |
| ------------------- | ------- | ------------ | ---------------------------- |
| `:rgb888` (default) | `u8` | sRGB (gamma) | Standard 8-bit images |
| `:linear_rgb` | `f32` | linear RGB | HDR, 16-bit, float pipelines |
```elixir
{: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:
```elixir
{:ok, %Butteraugli.Result{diffmap: diffmap}} =
Butteraugli.compare(ref, dist, width, height, compute_diffmap: true)
```
Two tuning parameters adjust the perceptual model:
- `intensity_target` — display brightness in nits the images are assumed to be
viewed at (crate default `80.0`).
- `hf_asymmetry` — multiplier weighting how added vs. removed high-frequency
detail is penalized (crate default `1.0`). Values above `1.0` penalize new
high-frequency artifacts (ringing, blocking) more than blurring; values below
`1.0` do the reverse.
```elixir
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:
```elixir
{: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).
### Cancellation
`Butteraugli.compare/5` and `Butteraugli.Reference.compare/3` accept `cancel:`
(a `Butteraugli.CancelRef`) and `timeout:` (milliseconds):
```elixir
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):
```elixir
{: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](https://github.com/hlindset/butteraugli/blob/main/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`.