Current section
Files
Jump to
Current section
Files
lib/ssimulacra2/vix.ex
if Code.ensure_loaded?(Vix.Vips.Image) do
defmodule Ssimulacra2.Vix do
@moduledoc """
Convenience wrappers that accept `Vix.Vips.Image` structs.
Only compiled when the optional `:vix` dependency is available. 8-bit
sources are coerced to 8-bit sRGB (`:rgb888`); higher-bit-depth sources are
preserved as 16-bit sRGB (`:rgb16`). Alpha is flattened in both cases.
"""
alias Vix.Vips.{Image, Operation}
@doc """
Compare a Vix candidate against a reference.
Given two `Image.t()`s, the reference pyramid is rebuilt every call. Given a
precomputed `Ssimulacra2.Reference` as the first argument, it is reused
against the candidate — coerce the candidate to the reference's format and
compare. Use the precompute form when comparing many candidates against one
original.
"""
@spec compare(Image.t(), Image.t()) :: {:ok, float()} | {:error, term()}
@spec compare(Ssimulacra2.Reference.t(), Image.t()) :: {:ok, float()} | {:error, term()}
def compare(reference, distorted)
def compare(%Image{} = reference, %Image{} = distorted) do
format = pair_format(reference, distorted)
with {:ok, {ref_bin, w, h}} <- coerce(reference, format),
{:ok, {dist_bin, ^w, ^h}} <- coerce(distorted, format) do
Ssimulacra2.compare(ref_bin, dist_bin, w, h, format: format)
else
{:ok, {_bin, _w2, _h2}} -> {:error, :dimension_mismatch}
other -> other
end
end
def compare(%Ssimulacra2.Reference{format: format} = reference, %Image{} = distorted) do
with {:ok, {bin, _w, _h}} <- coerce(distorted, format) do
Ssimulacra2.Reference.compare(reference, bin)
end
end
@doc "Build a `Ssimulacra2.Reference` from a Vix image, preserving bit depth."
@spec reference(Image.t()) :: {:ok, Ssimulacra2.Reference.t()} | {:error, term()}
def reference(%Image{} = image) do
format = image_format(image)
with {:ok, {bin, w, h}} <- coerce(image, format) do
Ssimulacra2.Reference.new(bin, w, h, format: format)
end
end
# 8-bit (UCHAR) sources stay 8-bit; anything else is treated as 16-bit.
defp image_format(%Image{} = image) do
if Image.format(image) == :VIPS_FORMAT_UCHAR, do: :rgb888, else: :rgb16
end
# When comparing a pair, if either side is higher than 8-bit, both go 16-bit.
defp pair_format(a, b) do
if image_format(a) == :rgb888 and image_format(b) == :rgb888,
do: :rgb888,
else: :rgb16
end
# Coerce to the target format: sRGB primaries, alpha flattened, packed binary.
defp coerce(%Image{} = image, :rgb888),
do: do_coerce(image, :VIPS_INTERPRETATION_sRGB, :VIPS_FORMAT_UCHAR)
defp coerce(%Image{} = image, :rgb16),
do: do_coerce(image, :VIPS_INTERPRETATION_RGB16, :VIPS_FORMAT_USHORT)
defp do_coerce(image, interpretation, band_format) do
colour = Operation.colourspace!(image, interpretation)
flat = if Image.has_alpha?(colour), do: Operation.flatten!(colour), else: colour
cast = Operation.cast!(flat, band_format)
case Image.write_to_binary(cast) do
{:ok, bin} -> {:ok, {bin, Image.width(cast), Image.height(cast)}}
{:error, reason} -> {:error, reason}
end
end
end
end