ssimulacra2

SSIMULACRA2 perceptual image-quality metric for Elixir, backed by the fast-ssim2 Rust crate (BSD-2-Clause) via Rustler. Published with precompiled NIFs, so the Rust toolchain is not required if you're on a covered architecture + platform.

Note: this binding currently pins fast-ssim2 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 dependency as soon as possible.

Installation

def deps do
[{:ssimulacra2, "~> 0.1"}]
end

Usage

Inputs are packed binaries; the layout is selected with the :format option (default :rgb888). Scores are on the native SSIMULACRA2 0–100 scale: 100 = identical, ~90+ ≈ visually lossless, lower (and negative) = larger perceptual difference.

formatelementchannelsbytes/pixelcolor space
:rgb888 (default)u833sRGB (gamma)
:rgb16u1636sRGB (gamma)
:linear_rgbf32312linear RGB
:gray8u811sRGB grayscale
:linear_grayf3214linear grayscale

Convention: integer = sRGB gamma, float = linear. Grayscale is expanded to RGB (R=G=B). Multi-byte elements are native-endian.

{:ok, score} = Ssimulacra2.compare(ref_rgb, dist_rgb, width, height)
{:ok, score} = Ssimulacra2.compare(ref16, dist16, width, height, format: :rgb16)

For a quality-search loop comparing many candidates against one original, reuse the reference (~2× faster per compare):

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

With Vix

If :vix is a dependency, pass images directly:

{:ok, score} = Ssimulacra2.Vix.compare(ref_image, dist_image)

Cancellation & timeouts

Ssimulacra2.compare/5 and Ssimulacra2.Reference.compare/3 can be aborted mid-computation. The metric runs on a dirty scheduler and polls a cancel ref at strip boundaries, so the CPU is freed promptly.

# Wall-clock timeout — returns {:error, :timeout} if it overruns.
Ssimulacra2.compare(ref, dist, w, h, timeout: 3_000)
# External cancellation — the ref is tripped from another process, because the
# calling process is blocked in the NIF until it returns.
tok = Ssimulacra2.CancelRef.new()
task = Task.async(fn -> Ssimulacra2.compare(ref, dist, w, h, cancel: tok) end)
# ... on client disconnect / shutdown:
Ssimulacra2.cancel(tok)
Task.await(task) #=> {:error, :cancelled}

A quality-search loop can share one ref across probes for an overall deadline (or disconnect). Tripping it aborts the in-flight probe and makes every later probe return {:error, :cancelled} at once:

{:ok, ref} = Ssimulacra2.Reference.new(original, w, h)
tok = Ssimulacra2.CancelRef.new()
# Watchdog trips the shared ref on the search deadline (a disconnect monitor
# can call Ssimulacra2.cancel/1 too).
spawn(fn -> Process.sleep(5_000); Ssimulacra2.cancel(tok) end)
case Ssimulacra2.Reference.compare(ref, candidate, cancel: tok) do
{:ok, score} -> score
{:error, :cancelled} -> :deadline_or_disconnect
end

A cancel ref is single-use: once cancelled it stays cancelled.

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 SSIMULACRA2_BUILD=1. In that case fast-ssim2 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-2-Clause, matching fast-ssim2.