Current section
Files
Jump to
Current section
Files
lib/ultraviolet/color/_xyz.ex
defmodule Ultraviolet.Color.XYZ do
@moduledoc false
# this module is only used for internal translations between other color
# spaces, and should not be used on its own
alias Ultraviolet.Color
alias Ultraviolet.M3x3
alias Decimal, as: D
alias __MODULE__
defstruct [:x, :y, :z]
# taken from https://www.mathworks.com/help/images/ref/whitepoint.html
@illuminants %{
# STM E308-01
a: {D.new("1.0985"), D.new("0.35585")},
# Wyszecki & Stiles, p. 769
b: {D.new("1.0985"), D.new("0.35585")},
# C (ASTM E308-01)
c: {D.new("0.98074"), D.new("1.18232")},
# D50 (ASTM E308-01)
d50: {D.new("0.96422"), D.new("0.82521")},
# D55 (ASTM E308-01)
d55: {D.new("0.95682"), D.new("0.92419")},
# D65 (ASTM E308-01)
d65: {D.new("0.95047"), D.new("1.08883")},
# E (ASTM E308-01)
e: {D.new("1.0"), D.new("1.0")},
# F2 (ASTM E308-01)
f2: {D.new("0.99186"), D.new("0.67393")},
# F7 (ASTM E308-01)
f7: {D.new("0.95041"), D.new("1.08747")},
# F11 (ASTM E308-01)
f11: {D.new("1.00962"), D.new("0.6435")},
# default white reference illuminant
icc: {D.new("0.96422"), D.new("0.82521")}
}
# conversion matrices
@rgb2xyz M3x3.new([
["0.4124564390896922", "0.21267285140562253", "0.0193338955823293"],
["0.357576077643909", "0.715152155287818", "0.11919202588130297"],
["0.18043748326639894", "0.07217499330655958", "0.9503040785363679"]
])
@xyz2rgb M3x3.new([
["3.2404541621141045", "-0.9692660305051868", "0.055643430959114726"],
["-1.5371385127977166", "1.8760108454466942", "-0.2040259135167538"],
["-0.498531409556016", "0.041556017530349834", "1.0572251882231791"]
])
# other universal matrices
@adapt_ma M3x3.new([
["0.8951", "-0.7502", "0.0389"],
["0.2664", "1.7135", "-0.0685"],
["-0.1614", "0.0367", "1.0296"]
])
@adapt_ma_i M3x3.new([
["0.9869929054667123", "0.43230526972339456", "-0.008528664575177328"],
["-0.14705425642099013", "0.5183602715367776", "0.04004282165408487"],
["0.15996265166373125", "0.0492912282128556", "0.9684866957875502"]
])
# used in rgb2xyz
@abc [D.new("0.9414285350000001"), D.new("1.040417467"), D.new("1.089532651")]
# sRGB x, y, z points
@white [D.new("0.95047"), D.new("1.0"), D.new("1.08883")]
def new(x, y, z) do
{:ok, struct(XYZ, x: x, y: y, z: z)}
end
def new([x, y, z]), do: new(x, y, z)
@doc """
the XYZ color space representation of the given white reference illuminant
"""
def whitepoint(reference) do
case Map.fetch(@illuminants, reference) do
# for now, y is always 1
{:ok, {x, z}} -> {:ok, {x, D.new(1), z}}
:error -> {:error, "undefined reference point"}
end
end
@doc """
Translates a sRGB color into XYZ space. `reference` is the desired
white reference illuminant (default: `:d65`).
"""
def from_rgb(%Color{} = color, reference \\ :d65) do
case whitepoint(reference) do
{:ok, {x_n, y_n, z_n}} -> rgb_to_xyz(color, [x_n, y_n, z_n])
error -> error
end
end
defp rgb_to_xyz(color, reference_point) do
[color.r, color.g, color.b]
# normalize and gamma adjust
|> Enum.map(&gamma_adjust(D.div(D.new(to_string(&1)), 255)))
# convert to xyz (stage 1)
|> M3x3.mult(@rgb2xyz)
# apply adaption matrix
|> M3x3.mult(@adapt_ma)
# account for reference point
|> then(&[&1, M3x3.mult(reference_point, @adapt_ma), @abc])
|> Enum.zip()
|> Enum.map(fn {i, d, s} -> D.mult(i, D.div(d, s)) end)
# apply second adaption matrix
|> M3x3.mult(@adapt_ma_i)
# put in struct
|> new()
end
defp gamma_adjust(channel) do
sign = channel.sign
channel = D.abs(channel)
linear =
cond do
D.gt?(channel, "0.04045") ->
channel
|> D.add("0.055")
|> D.div("1.055")
|> D.to_float()
|> Float.pow(2.4)
true ->
channel
|> D.div("12.92")
|> D.to_float()
end
D.new(to_string(sign * linear))
end
@doc """
Converts from XYZ to [r, g, b] array; `reference` is the desired white
reference illuminant (default: `:d65`)
"""
def to_rgb(%XYZ{} = xyz, reference \\ :d65) do
case whitepoint(reference) do
{:ok, {x_n, y_n, z_n}} -> xyz_to_rgb(xyz, [x_n, y_n, z_n])
error -> error
end
end
defp xyz_to_rgb(xyz, reference_point) do
[xyz.x, xyz.y, xyz.z]
# apply first adaption matrix
|> M3x3.mult(@adapt_ma)
# account for rerence point
|> then(&[&1, M3x3.mult(reference_point, @adapt_ma), M3x3.mult(@white, @adapt_ma)])
|> Enum.zip()
|> Enum.map(fn {i, s, d} -> D.mult(i, D.div(d, s)) end)
# apply second adaption matrix
|> M3x3.mult(@adapt_ma_i)
# convert to rgb
|> M3x3.mult(@xyz2rgb)
# apply companding
|> Enum.map(&compand/1)
end
defp compand(linear) do
sign = linear.sign
linear = D.abs(linear)
cond do
D.gt?(linear, "0.0031308") ->
sign * (1.055 * Float.pow(D.to_float(linear), 1.0 / 2.4) - 0.055)
true ->
sign * (linear |> D.mult("12.92") |> D.to_float())
end
end
end