Distinguish between color extraction strategies

By default, use fast color sampling. Run heavy edge weighted extraction
only on demand.
This commit is contained in:
Claudio Ortolina
2025-06-08 07:43:07 +01:00
parent 0768176e70
commit 569f8b5340
4 changed files with 142 additions and 6 deletions
@@ -1,121 +0,0 @@
defmodule MusicLibrary.Records.DominantColors do
@moduledoc """
Simple edge-weighted color extraction using Vix.
This approach weights colors based on their proximity to edges,
giving more importance to colors from visually significant regions
without requiring complex algorithms.
Generated by Claude, using Sonnet 4.
"""
alias Vix.Vips.{Image, Operation}
@doc """
Extracts dominant colors weighted by edge proximity.
Colors near edges (important features) are given higher importance.
"""
def extract_dominant_colors(image_data, num_colors \\ 5) do
with {:ok, image} <- Image.new_from_buffer(image_data),
{:ok, resized} <- Operation.thumbnail_image(image, 200),
{:ok, rgb_image} <- ensure_rgb_channels(resized),
{:ok, edge_map} <- create_edge_map(rgb_image),
{:ok, colors} <- extract_edge_weighted_colors(rgb_image, edge_map, num_colors) do
hex_colors = Enum.map(colors, &rgb_to_hex/1)
{:ok, hex_colors}
end
end
@doc """
Same as `extract_dominant_colors/2`, but raises an error on failure.
"""
@spec extract_dominant_colors!(binary(), integer()) :: [String.t()] | no_return
def extract_dominant_colors!(image_data, num_colors \\ 5) do
case extract_dominant_colors(image_data, num_colors) do
{:ok, colors} -> colors
{:error, reason} -> raise "Failed to extract dominant colors: #{reason}"
end
end
defp create_edge_map(image) do
# Convert to grayscale for edge detection
# Apply Sobel edge detection - built into Vix!
with {:ok, gray} <- Operation.colourspace(image, :VIPS_INTERPRETATION_B_W) do
Operation.sobel(gray)
end
end
defp ensure_rgb_channels(image) do
bands = Image.bands(image)
cond do
bands >= 3 -> {:ok, image}
bands == 1 -> Operation.bandjoin([image, image, image])
true -> {:error, "Unsupported image format"}
end
end
defp extract_edge_weighted_colors(rgb_image, edge_map, num_colors) do
width = Image.width(rgb_image)
height = Image.height(rgb_image)
# Sample pixels with edge-based weighting
# Sample every 3rd pixel for performance
weighted_pixels =
for y <- 0..(height - 1)//3,
x <- 0..(width - 1)//3 do
with {:ok, [r, g, b | _]} <- Operation.getpoint(rgb_image, x, y),
{:ok, [edge_strength]} <- Operation.getpoint(edge_map, x, y) do
# Weight this pixel by edge strength
# Normalize edge strength
weight = max(1, trunc(edge_strength / 50))
color = {trunc(r), trunc(g), trunc(b)}
# Return the color multiple times based on edge weight
List.duplicate(color, weight)
else
_ -> []
end
end
|> List.flatten()
|> Enum.reject(fn {r, g, b} ->
# Filter out very dark/light colors
brightness = (r + g + b) / 3
brightness < 15 || brightness > 240
end)
if length(weighted_pixels) > 0 do
colors = cluster_weighted_colors(weighted_pixels, num_colors)
{:ok, colors}
else
{:error, "No valid edge-weighted pixels found"}
end
end
defp cluster_weighted_colors(pixels, num_colors) do
pixels
# Slightly larger buckets for better clustering
|> group_similar_colors(40)
|> Enum.frequencies()
|> Enum.sort_by(fn {_color, count} -> count end, :desc)
|> Enum.take(num_colors)
|> Enum.map(fn {{r, g, b}, _count} -> {r, g, b} end)
end
defp group_similar_colors(pixels, bucket_size) do
Enum.map(pixels, fn {r, g, b} ->
{
div(r, bucket_size) * bucket_size,
div(g, bucket_size) * bucket_size,
div(b, bucket_size) * bucket_size
}
end)
end
defp rgb_to_hex({r, g, b}) do
"#" <>
(r |> Integer.to_string(16) |> String.pad_leading(2, "0") |> String.upcase()) <>
(g |> Integer.to_string(16) |> String.pad_leading(2, "0") |> String.upcase()) <>
(b |> Integer.to_string(16) |> String.pad_leading(2, "0") |> String.upcase())
end
end
+4 -3
View File
@@ -5,7 +5,8 @@ defmodule MusicLibrary.Records.Record do
alias MusicBrainz.{Release, ReleaseGroup}
alias MusicLibrary.Artists.Artist
alias MusicLibrary.Records.{Cover, DominantColors}
alias MusicLibrary.Colors.ColorFrequencyExtractor
alias MusicLibrary.Records.Cover
@formats [:cd, :backup, :vinyl, :blu_ray, :dvd, :multi]
@types [:album, :ep, :live, :compilation, :single, :other]
@@ -170,7 +171,7 @@ defmodule MusicLibrary.Records.Record do
end
def generate_dominant_colors(%__MODULE__{cover_data: cover_data} = record) do
change(record, dominant_colors: DominantColors.extract_dominant_colors!(cover_data))
change(record, dominant_colors: ColorFrequencyExtractor.extract_dominant_colors!(cover_data))
end
def generate_dominant_colors(changeset) do
@@ -182,7 +183,7 @@ defmodule MusicLibrary.Records.Record do
put_change(
changeset,
:dominant_colors,
DominantColors.extract_dominant_colors!(cover_data)
ColorFrequencyExtractor.extract_dominant_colors!(cover_data)
)
end
end