Image.Palette (image v0.67.0)

Copy Markdown View Source

Extracts a small representative colour palette from an image.

The pipeline is the one described in Amanda Hinton's palette post, adapted for :image:

  1. Resize the image so its longest side is :longest_dim pixels (default 300). The cost of every later step is linear in pixel count, so this is the single biggest performance knob.
  2. Drop transparent pixels when the image has alpha and convert the rest to sRGB.
  3. Sample up to :max_pixels pixels (default 90 000) uniformly.
  4. Convert the pixel batch to Oklab in one vectorised pass via Image.Color.srgb_tensor_to_oklab/1.
  5. Cluster in Oklab using Scholar.Cluster.KMeans with the chromatic axes weighted twice as much as lightness (a, b columns scaled by √ab_weight before fitting).
  6. Merge near-duplicate clusters via Color.Palette.Cluster.merge_until/3.
  7. Phantom guard: drop low-mass low-chroma clusters (default < 2.5% of total mass and centroid chroma < 0.05) so small pockets of near-grey pixels can't claim a palette slot.
  8. Pick a representative sRGB swatch per surviving cluster via Color.Palette.Cluster.representative/2.
  9. Sort the result with Color.Palette.sort/2 so the output reads as a perceptual rainbow.

The clustering and rep-selection primitives live in Color.Palette.Cluster so the algorithm doesn't drift between this library and other callers (e.g. Color.Palette.Summarize).

Determinism

Pass :key (any Nx.Random key) to make the K-means initialisation deterministic; otherwise different runs may produce slightly different palettes for the same image.

Requires

Image.Palette is only compiled when both scholar and nx are present.

Summary

Clusters

Extracts a representative colour palette from an image.

Same as extract/2 but raises on error.

Clusters

extract(image, options \\ [])

(since 0.67.0)
@spec extract(image :: Vix.Vips.Image.t(), options :: Keyword.t()) ::
  {:ok, [Color.SRGB.t()]} | {:error, term()}

Extracts a representative colour palette from an image.

Arguments

Options

  • :final is the maximum number of swatches in the output. Default 5. The output may be shorter after the phantom guard or if the input image has very few distinct colours.

  • :k is the number of K-means clusters used internally before the merge / phantom-guard passes. Default 14. Must satisfy k >= final.

  • :longest_dim is the pre-clustering resize target. The image is thumbnailed so its longest side is this many pixels. Default 300.

  • :max_pixels caps the post-resize sample size. Default 90000.

  • :ab_weight is the multiplier on the chromatic axes (a, b) in the Oklab distance metric, relative to lightness L. Default 2.0. Used both during K-means (by pre-scaling the input columns) and during merge / rep-selection.

  • :phantom_min_mass is the fraction of total pixel mass below which a cluster is "phantom"-eligible. Default 0.025.

  • :phantom_max_chroma is the Oklch chroma below which a phantom-eligible cluster is dropped. Default 0.05.

  • :rep_chroma_threshold is forwarded to Color.Palette.Cluster.representative/2. Default 0.03.

  • :sort selects the post-extraction sort strategy. One of the strategies accepted by Color.Palette.sort/2, or false to skip sorting and return clusters in K-means order. Default :hue_lightness.

  • :key is an Nx.Random key for deterministic K-means initialisation. Default: a fresh random key per call (so results are stable within a call but may vary between calls).

Returns

  • {:ok, [%Color.SRGB{}, ...]} on success — a list of at most :final representative swatches.

  • {:error, reason} if image conversion or sampling fails.

Examples

iex> {:ok, image} = Image.open("./test/support/images/Hong-Kong-2015-07-1998.jpg")
iex> {:ok, palette} = Image.Palette.extract(image, key: Nx.Random.key(42))
iex> length(palette) <= 5
true
iex> Enum.all?(palette, &match?(%Color.SRGB{}, &1))
true

extract!(image, options \\ [])

(since 0.67.0)
@spec extract!(image :: Vix.Vips.Image.t(), options :: Keyword.t()) :: [
  Color.SRGB.t()
]

Same as extract/2 but raises on error.

Examples

iex> image = Image.open!("./test/support/images/Hong-Kong-2015-07-1998.jpg")
iex> palette = Image.Palette.extract!(image, key: Nx.Random.key(42))
iex> length(palette) <= 5
true