Packages
image
0.72.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/scholar.ex
if match?({:module, _module}, Code.ensure_compiled(Scholar.Cluster.KMeans)) and
match?({:module, _module}, Code.ensure_compiled(Nx)) do
defmodule Image.Scholar do
@moduledoc """
Functions that analyse images using
[Scholar](https://hex.pm/packages/scholar) machine-learning
primitives.
The primary public API is `unique_colors/1` and `k_means/2`
which underpin `Image.k_means/2` and `Image.reduce_colors/2`.
"""
import Nx
alias Vix.Vips.Image, as: Vimage
@square_256 256 ** 2
@doc """
Returns the unique colors in an image and the count of
each color.
### Arguments
* `image` is any 3- or 4-band `t:Vix.Vips.Image.t/0` with
`{:u, 8}` band format.
### Returns
* `{:ok, {color_count_tensor, unique_colors_tensor}}` or
* `{:error, reason}`.
"""
def unique_colors(%Vimage{} = image) do
bands = Image.bands(image)
cond do
bands not in [3, 4] ->
{:error,
scholar_error("unique_colors/1 requires a 3- or 4-band image. Found #{bands} bands")}
Image.band_format(image) != {:u, 8} ->
{:error,
scholar_error(
"unique_colors/1 requires an 8-bit unsigned image. " <>
"Found #{inspect(Image.band_format(image))}"
)}
true ->
do_unique_colors(image, bands)
end
end
defp do_unique_colors(image, bands) do
with {:ok, tensor} <- Image.to_nx(image) do
colors_base256 =
tensor
|> encode_colors(bands)
|> Nx.flatten()
|> Nx.sort()
diff =
diff(colors_base256)
unique_indices_selector =
Nx.concatenate([Nx.tensor([1]), Nx.not_equal(diff, 0)])
marked_unique_indices =
Nx.select(unique_indices_selector, Nx.iota(colors_base256.shape), -1)
repeated_count =
Nx.to_number(Nx.sum(Nx.logical_not(unique_indices_selector)))
unique_indices =
marked_unique_indices
|> Nx.sort()
|> Nx.slice_along_axis(repeated_count, Nx.size(marked_unique_indices) - repeated_count,
axis: 0
)
unique_colors =
Nx.take(colors_base256, unique_indices)
|> decode_colors(bands)
# colors_base256 holds one encoded value per pixel so its size
# is the pixel count.
count = Nx.size(colors_base256)
# Nx.diff/1 requires at least two elements so a single unique
# color (a solid-color image) is handled directly.
color_count =
if Nx.size(unique_indices) == 1 do
Nx.tensor([count])
else
max = Nx.to_number(Nx.reduce_max(unique_indices))
Nx.concatenate([diff(unique_indices), Nx.tensor([count - max])])
end
{:ok, {color_count, unique_colors}}
end
end
defp scholar_error(message) do
%Image.Error{message: message, reason: message}
end
@doc """
Clusters the unique colors of an image using the K-means
algorithm.
### Arguments
* `image` is any 3- or 4-band `t:Vix.Vips.Image.t/0` with
`{:u, 8}` band format.
* `options` is a keyword list of options passed to
`Scholar.Cluster.KMeans.fit/2`.
### Returns
* A fitted `Scholar.Cluster.KMeans` model or
* `{:error, reason}`.
"""
def k_means(%Vimage{} = image, options \\ []) do
with {:ok, {_count, colors}} <- unique_colors(image) do
# K-means requires at least as many samples as clusters, so
# the cluster count is clamped to the number of unique colors.
# A single unique color (solid image) is duplicated because
# the random centroid initialisation needs at least 2 samples.
unique_count = Nx.axis_size(colors, 0)
colors =
if unique_count == 1, do: Nx.concatenate([colors, colors]), else: colors
options =
case Keyword.fetch(options, :num_clusters) do
{:ok, num_clusters} when is_integer(num_clusters) ->
Keyword.put(options, :num_clusters, Kernel.min(num_clusters, unique_count))
_other ->
options
end
Scholar.Cluster.KMeans.fit(colors, options)
end
end
# The multipliers are 64-bit so that 4-band encoding
# (255 * 256 ** 3) does not overflow the default 32-bit
# integer tensor type.
defp encode_colors(colors, 3) do
colors
|> Nx.multiply(Nx.tensor([[1, 256, @square_256]], type: :s64))
|> Nx.sum(axes: [2])
end
defp encode_colors(colors, 4) do
colors
|> Nx.multiply(Nx.tensor([[1, 256, @square_256, 256 * @square_256]], type: :s64))
|> Nx.sum(axes: [2])
end
defp decode_colors(encoded_colors, 3) do
b = Nx.quotient(encoded_colors, @square_256)
rem = Nx.remainder(encoded_colors, @square_256)
g = Nx.quotient(rem, 256)
r = Nx.remainder(rem, 256)
Nx.stack([r, g, b], axis: 1)
end
defp decode_colors(encoded_colors, 4) do
a = Nx.quotient(encoded_colors, 256 * @square_256)
rem = Nx.remainder(encoded_colors, 256 * @square_256)
b = Nx.quotient(rem, @square_256)
rem = Nx.remainder(rem, @square_256)
g = Nx.quotient(rem, 256)
r = Nx.remainder(rem, 256)
Nx.stack([r, g, b, a], axis: 1)
end
end
end