Packages
image
0.62.0
0.72.0
0.71.0
0.70.0
0.69.0
0.68.0
0.67.0
0.67.0-dev
retired
0.66.0
0.65.0
0.64.0
0.63.0
0.62.1
0.62.0
0.61.1
0.61.0
0.60.0
0.59.3
0.59.2
0.59.1
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.2
0.55.1
retired
0.55.0
0.54.4
0.54.3
0.54.2
0.54.1
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
retired
0.51.0
0.50.0
0.49.0
0.48.1
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.2
0.43.1
0.43.0
0.42.0
0.41.0
0.40.0
0.39.3
0.39.2
0.39.1
0.39.0
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.0
0.36.2
0.36.1
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.0
0.29.0
0.28.2
0.28.1
0.28.0
0.27.0
0.26.0
0.25.1
0.25.0
0.24.1
0.24.0
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
retired
0.19.0
0.18.1
0.18.0
0.17.0
0.16.0
0.15.0
0.14.4
0.14.2
0.14.1
0.14.0
retired
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.10.0-rc.0
retired
0.9.0
retired
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
An approachable image processing library primarily based upon Vix and libvips that is NIF-based, fast, multi-threaded, pipelined and has a low memory footprint.
Current section
Files
Jump to
Current section
Files
lib/image/yuv.ex
defmodule Image.YUV do
@moduledoc """
Functions to convert from/to YUV (YCbCr) encoding and BT.601/BT.709
colorspaces and sRGB images.
The following YUV ([YCbCr](https://en.wikipedia.org/wiki/YCbCr)) binary
formats are supported:
* Planar frame types only (not packed frames).
* `4:4:4`, `4:2:2` and `4:2:0` encodings.
* [BT.601](https://en.wikipedia.org/wiki/Rec._601) and
[BT.709](https://en.wikipedia.org/wiki/Rec._709) colorspaces.
Performance profiling indicates this implementation is not suitable
for real time frame processing of YUV images.
"""
@typedoc "YUV encoding"
@type yuv_encoding :: :C444 | :C422 | :C420
@typedoc "YUV colorspace"
@type yuv_colorspace :: :bt601 | :bt709
@typedoc "YUV data as a three-element list of binaries"
@type yuv_list :: [binary()]
# See:
# https://support.medialooks.com/hc/en-us/articles/360030737152-Color-correction-with-matrix-transformation
# https://mymusing.co/bt-709-yuv-to-rgb-conversion-color/
# https://mymusing.co/bt601-yuv-to-rgb-conversion-color/
# https://github.com/libvips/libvips/discussions/2561
# https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.2020_conversion
# https://wiki.multimedia.cx/index.php/YUV4MPEG2#Frame_data
alias Vix.Vips.Image, as: Vimage
alias Vix.Vips.Operation
@bt601_to_rgb [
[1.0, 0.0, 1.402],
[1.0, -0.344136, -0.714136],
[1.0, 1.772, 0.0]
]
# This are the "Computer RGB to YCbCr"
# coefficients
@rgb_to_bt601 [
[65.738, 129.057, 25.064],
[-37.945, -74.494, 112.439],
[112.439, -94.154, -18.285]
]
# See https://mymusing.co/bt-709-yuv-to-rgb-conversion-color/
@bt709_to_rgb [
[1.0, 0.0, 1.5748],
[1.0, -0.187324, -0.468124],
[1.0, 1.8556, 0.0]
]
# This are the "Computer RGB to YCbCr"
# coefficients
@rgb_to_bt709 [
[46.7428, 157.243, 15.873],
[-25.765, -86.674, 112.439],
[112.439, -102.129, -10.31]
]
# Lookup maps
@to_yuv %{
bt601: @rgb_to_bt601,
bt709: @rgb_to_bt709
}
@to_rgb %{
bt601: @bt601_to_rgb,
bt709: @bt709_to_rgb
}
@yuv_to_rgb_offsets [16.0, 128.0, 128.0]
@valid_encodings [:C444, :C422, :C420]
@valid_colorspace [:bt601, :bt709]
@doc """
Converts the raw YUV data in a `.yuv` file
into an RGB image.
The data is assumed, and required to be in:
* Planar format
* 8-bit color depth
### Arguments
* `path` is any accessible file system path.
* `width` is the width of the image encoded in
the YUV data.
* `height` is the height of the image encoded in
the YUV data.
* `encoding` is one of `:C444`, `:C422` or
`:C420`.
* `colorspace` is one of `:bt601` (the default) or
`:bt709`.
### Returns
* `{:ok, rgb_image}` or
* `{:error, reason}`.
"""
@doc since: "0.41.0"
@spec new_from_file(
path :: Path.t(),
width :: pos_integer(),
height :: pos_integer(),
encoding :: yuv_encoding(),
colorspace :: yuv_colorspace()
) ::
{:ok, Vimage.t()} | {:error, Image.error_message()}
def new_from_file(path, width, height, encoding, colorspace \\ :bt601)
when encoding in @valid_encodings and colorspace in @valid_colorspace do
with {:ok, binary} <- File.read(path) do
new_from_binary(binary, width, height, encoding, colorspace)
end
end
@doc """
Converts raw YUV data into an RGB image.
The data is assumed, and required to be in:
* Planar format
* 8-bit color depth
### Arguments
* `binary` is raw YUV data as a binary.
* `width` is the width of the image encoded in
the YUV data.
* `height` is the height of the image encoded in
the YUV data.
* `encoding` is one of `:C444`, `:C422` or
`:C420`.
* `colorspace` is one of `:bt601` (the default) or
`:bt709`.
### Returns
* `{:ok, rgb_image}` or
* `{:error, reason}`.
"""
@doc since: "0.41.0"
@spec new_from_binary(
binary :: binary(),
width :: pos_integer(),
height :: pos_integer(),
encoding :: yuv_encoding(),
colorspace :: yuv_colorspace()
) ::
{:ok, Vimage.t()} | {:error, Image.error_message()}
def new_from_binary(binary, width, height, encoding, colorspace \\ :bt601)
when encoding in @valid_encodings and colorspace in @valid_colorspace do
with {:ok, decoded} = decode(binary, width, height, encoding) do
to_rgb(decoded, width, height, encoding, colorspace)
end
end
@doc """
Writes an image to a YUV file as raw YUV data.
It is recommeneded, but not required, that the path
name use a `.yuv` suffix.
### Arguments
* `path` is any accessible file system path.
* `encoding` is one of `:C444`, `:C422` or
`:C420`.
* `colorspace` is one of `:bt601` (the default) or
`:bt709`.
### Returns
* `:ok` or
* `{:error, reason}`.
"""
@doc since: "0.41.0"
@spec write_to_file(
image :: Vimage.t(),
path :: Path.t(),
encoding :: yuv_encoding(),
colorspace :: yuv_colorspace()
) ::
:ok | {:error, Image.error_message()}
def write_to_file(%Vimage{} = image, path, encoding, colorspace \\ :bt601) do
with {:ok, binary} <- write_to_binary(image, encoding, colorspace) do
File.write(path, binary)
end
end
@doc """
Writes an image to a YUV raw binary.
### Arguments
* `image` is any `t:Vimage.t/0`.
* `encoding` is one of `:C444`, `:C422` or
`:C420`.
* `colorspace` is one of `:bt601` (the default) or
`:bt709`.
### Returns
* `{:ok, yuv_binary}` or
* `{:error, reason}`.
"""
@doc since: "0.41.0"
@spec write_to_binary(
image :: Vimage.t(),
encoding :: yuv_encoding(),
colorspace :: yuv_colorspace()
) ::
{:ok, binary()} | {:error, Image.error_message()}
def write_to_binary(%Vimage{} = image, encoding, colorspace \\ :bt601) do
with {:ok, [y, u, v]} <- to_yuv(image, encoding, colorspace) do
{:ok, :erlang.iolist_to_binary([y, u, v])}
end
end
@doc """
Convert an image in an YUV colorspace and convert it to RGB
colorspace.
### Arguments
* `image` is any `t:Vimage.t/0` that is in a YUV
colorspace such as that returned from `Image.YUV.new_from_file/5`
or `Image.YUV.new_from_binary/5`.
* `colorspace` is one of `:bt601` (the default) or
`:bt709` that represents the colorspace of `image` before
conversion.
"""
# See https://github.com/libvips/libvips/discussions/2561
@doc since: "0.41.0"
@spec to_rgb(image :: Vimage.t(), colorspace :: yuv_colorspace()) ::
{:ok, Vimage.t()} | {:error, Image.error_message()}
def to_rgb(%Vimage{} = image, colorspace) when colorspace in @valid_colorspace do
with {:ok, transform} <- Vimage.new_from_list(@to_rgb[colorspace]),
{:ok, recombed} <- Operation.recomb(image, transform),
{:ok, rgb} <- Image.cast(recombed, {:u, 8}) do
Operation.copy(rgb, interpretation: :VIPS_INTERPRETATION_sRGB)
end
end
@doc """
Takes the `[y, u, v]` planes and converts them to
an RGB image.
### Arguments
* `yuv` is a list of three binaries representing the `Y`,
`U` and `V` planes. Such a list is returned from
`Image.YUV.to_yuv/3` and from `Image.YUV.encode/2`.
* `width` is the width of the image encoded in `yuv`.
* `height` is the height of the image encoded in `yuv`.
* `encoding` is one of `:C444`, `:C422` or
`:C420` representing how `yuv` is encoded.
* `colorspace` is one of `:bt601` (the default) or
`:bt709` that represents the colorspace of `image` before
conversion.
### Returns
* `{:ok, image}` or
* `{:error, reason}`.
"""
@doc since: "0.41.0"
@spec to_rgb(
yuv :: yuv_list(),
width :: pos_integer(),
height :: pos_integer(),
encoding :: yuv_encoding,
colorspace :: yuv_colorspace()
) ::
{:ok, Vimage.t()} | {:error, Image.error_message()}
def to_rgb([y, u, v], width, height, :C444, colorspace) do
use Image.Math
with {:ok, y} <- new_scaled_image(y, width, height, 1.0, 1.0),
{:ok, u} <- new_scaled_image(u, width, height, 1.0, 1.0),
{:ok, v} <- new_scaled_image(v, width, height, 1.0, 1.0),
{:ok, image_444} <- Operation.bandjoin([y, u, v]) do
to_rgb(image_444 - @yuv_to_rgb_offsets, colorspace)
end
end
def to_rgb([y, u, v], width, height, :C422, colorspace) do
use Image.Math
with {:ok, y} <- new_scaled_image(y, width, height, 1.0, 1.0),
{:ok, u} <- new_scaled_image(u, width, height, 2.0, 1.0),
{:ok, v} <- new_scaled_image(v, width, height, 2.0, 1.0),
{:ok, image_444} <- Operation.bandjoin([y, u, v]) do
to_rgb(image_444 - @yuv_to_rgb_offsets, colorspace)
end
end
def to_rgb([y, u, v], width, height, :C420, colorspace) do
use Image.Math
with {:ok, y} <- new_scaled_image(y, width, height, 1.0, 1.0),
{:ok, u} <- new_scaled_image(u, width, height, 2.0, 2.0),
{:ok, v} <- new_scaled_image(v, width, height, 2.0, 2.0),
{:ok, image_444} <- Operation.bandjoin([y, u, v]) do
to_rgb(image_444 - @yuv_to_rgb_offsets, colorspace)
end
end
@doc """
Converts an image to raw YUV data as
a binary.
### Arguments
* `image` is any `t:Vimage.t/0`.
* `encoding` is one of `:C444`, `:C422` or
`:C420`.
* `colorspace` is one of `:bt601` (the default) or
`:bt709`.
### Returns
* `{:ok, [y, u, v]}` or
* `{:error, reason}`.
"""
@doc since: "0.41.0"
@spec to_yuv(image :: Vimage.t(), encoding :: yuv_encoding(), colorspace :: yuv_colorspace()) ::
{:ok, yuv_list()} | {:error, Image.error_message()}
def to_yuv(image, encoding, colorspace \\ :bt601)
def to_yuv(%Vimage{} = image, encoding, colorspace)
when encoding in @valid_encodings and colorspace in @valid_colorspace do
with {:ok, image} <- Image.flatten(image),
{:ok, transform} <- Vimage.new_from_list(@to_yuv[colorspace]),
{:ok, divided} <- Image.Math.divide(transform, 256.0),
{:ok, recombed} <- Operation.recomb(image, divided),
{:ok, offset} <- Image.Math.add(recombed, @yuv_to_rgb_offsets),
{:ok, yuv} <- Image.cast(offset, {:u, 8}) do
encode(yuv, encoding)
end
end
@doc """
Encodes an image that is in a YUV colorspace to
raw YUV data that is a list of the three planes, each a
binary.
The data is always written in a planar format.
### Arguments
* `image` is any `t:Vimage.t/0` that is in a YUV
colorspace such as that returned from `Image.YUV.new_from_file/5`
or `Image.YUV.new_from_binary/5`.
* `encoding` is one of `:C444`, `:C422` or
`:C420` representing how `yuv` is to be encoded.
### Returns
* `{:ok, [y, u, v]}` or
* `{:error, Image.error_message()}`.
"""
@doc since: "0.41.0"
@spec encode(image :: Vimage.t(), encoding :: yuv_encoding()) ::
{:ok, yuv_list()} | {:errpr, Image.error_message()}
def encode(%Vimage{} = image, :C444) do
with {:ok, y} = Vimage.write_to_binary(image[0]),
{:ok, u} = Vimage.write_to_binary(image[1]),
{:ok, v} = Vimage.write_to_binary(image[2]) do
{:ok, [y, u, v]}
end
end
def encode(%Vimage{} = image, :C422) do
with {:ok, subsampled} <- Operation.subsample(image, 2, 1) do
{:ok, y} = Vimage.write_to_binary(image[0])
{:ok, u} = Vimage.write_to_binary(subsampled[1])
{:ok, v} = Vimage.write_to_binary(subsampled[2])
{:ok, [y, u, v]}
end
end
def encode(%Vimage{} = image, :C420) do
with {:ok, subsampled} <- Operation.subsample(image, 2, 2) do
{:ok, y} = Vimage.write_to_binary(image[0])
{:ok, u} = Vimage.write_to_binary(subsampled[1])
{:ok, v} = Vimage.write_to_binary(subsampled[2])
{:ok, [y, u, v]}
end
end
@doc """
Deocdes a raw YUV binary into `[y, u, v]` planes
where each plane is a binary.
### Arguments
* `binary` is a binary representation of a YUV image.
* `width` is the width of the image encoded in `yuv`.
* `height` is the height of the image encoded in `yuv`.
* `encoding` is one of `:C444`, `:C422` or
`:C420` representing how `yuv` is encoded.
### Returns
* `{:ok, [y, u, v]}` or
* `{:error, reason}`.
"""
@doc since: "0.41.0"
def decode(binary, width, height, :C444) do
y_bytes = width * height
case binary do
<<y::bytes-size(y_bytes), u::bytes-size(y_bytes), v::bytes-size(y_bytes)>> ->
{:ok, [y, u, v]}
_other ->
{:error, "Could not decode raw YUV data as 4:4:4"}
end
end
def decode(binary, width, height, :C422) do
y_bytes = width * height
uv_bytes = div(y_bytes, 2)
case binary do
<<y::bytes-size(y_bytes), u::bytes-size(uv_bytes), v::bytes-size(uv_bytes)>> ->
{:ok, [y, u, v]}
_other ->
{:error, "Could not decode raw YUV data as 4:2:2"}
end
end
def decode(binary, width, height, :C420) do
y_bytes = width * height
uv_bytes = div(y_bytes, 4)
case binary do
<<y::bytes-size(y_bytes), u::bytes-size(uv_bytes), v::bytes-size(uv_bytes)>> ->
{:ok, [y, u, v]}
_other ->
{:error, "Could not decode raw YUV data as 4:2:0"}
end
end
# Scales the U and V planes to be the same size as the Y
# plane.
defp new_scaled_image(data, width, height, x_scale, y_scale)
when x_scale == 1.0 and y_scale == 1.0 do
Vimage.new_from_binary(data, width, height, 1, :VIPS_FORMAT_UCHAR)
end
defp new_scaled_image(data, width, height, x_scale, y_scale) do
width = round(width / x_scale)
height = round(height / y_scale)
with {:ok, image} <- Vimage.new_from_binary(data, width, height, 1, :VIPS_FORMAT_UCHAR) do
Operation.resize(image, x_scale, vscale: y_scale, kernel: :VIPS_KERNEL_LINEAR)
end
end
end