MarcusRiemer
Efficient way to determine whether a Vix.Vips.Image is a single color?
I have the following code to check whether a certain Vix.Vips.Image consists only of a single colour:
defmodule Spritesheet do
def single_color_tile?(%Vix.Vips.Image{} = image) do
initial_colour = Image.get_pixel!(image, 0, 0) |> dbg()
coordinates =
for x <- 0..(Image.width(image) - 1), y <- 0..(Image.height(image) - 1), do: {x, y}
Enum.all?(coordinates, fn {x, y} -> Image.get_pixel!(image, x, y) == initial_colour end)
end
end
It works, but it is slow: Checking a 128x128 px image takes a few seconds. The following testcases need 7 seconds to finish on my machine.
describe "single_color_tile?" do
test "all transparent black" do
assert Spritesheet.single_color_tile?(Image.new!(128, 128, color: [0, 0, 0, 0]))
end
test "all solid green" do
assert Spritesheet.single_color_tile?(Image.new!(128, 128, color: [0, 255, 0, 255]))
end
test "red solid circle on transparent background" do
refute Spritesheet.single_color_tile?(
Image.new!(16, 16, color: [0, 0, 0, 0])
|> Image.Draw.circle!(7, 7, 7, color: [255, 0, 0, 255])
)
end
end
I was initially quite hopeful that I could trick Image.dominant_color into computing this, but I probably missunderstand the purpose of that function: For me it only returns colours that are not part of the given image:
> Image.new!(1, 1, color: [0, 0, 0, 0]) |> Image.dominant_color!(bins: 1)
[128, 128, 128]
> Image.new!(1, 1, color: [0, 0, 0, 0]) |> Image.dominant_color!(bins: 16)
[8, 8, 8]
> Image.new!(1, 1, color: [0, 0, 0, 0]) |> Image.dominant_color!(bins: 255)
[1, 234, 1]
Is there a builtin Image function that I am overlooking? Or a way to access the raw image data for more efficient iteration?
Marked As Solved
akash-akya
Libvips has highly efficient relational operations. Vix has these as normal operation, as well as as operators which are much easier to read and write.
alias Vix.Vips.{Operation, Image}
# Selectively import needed operators for cleaner syntax
use Vix.Operator, only: [==: 2, all?: 2]
{:ok, img} = Image.new_from_file("image.jpg")
reference_pixel = Image.get_pixel!(img, 0, 0)
if all?(img == reference_pixel, true) do
IO.puts("All pixels match reference")
else
IO.puts("Image contains different pixels")
end
Performance: ~50ms for a 5000×5000 JPEG.
These operations short-circuit on first mismatch, making them extremely efficient for images that aren’t uniform. The comparison stops immediately when a different pixel is found rather than scanning the entire image.
Also Liked
kip
Yes, thats definitely one approach. You still need to check each of the other bins to check if there are no values. And you need to select the right number of bins - which would depend on the colourspace of the source image.
Thats one reason why I would use the solution I proposed: its easier to support images of different colourspaces.
I’ll see how I can improve the documentation for Image.histogram/2 as well, thanks for the prompt.
kip
I suspect this is the pragmatic way and probably how I would approach it. Here’s an example:
def single_color?(image) do
target_color = Image.get_pixel!(image, 0, 0)
diff = Image.Math.equal!(image, target_color)
Image.Math.min!(diff) == Image.Math.max!(diff)
end
This takes 2ms on my aging iMac Pro for an image of 128x128 and 4ms for an image of 512x512 despite that being 16 times more pixels. It’s 39ms for a 5000x5000 image.
Per @akash-akya solution below (where his all? operator also uses libvips’s min/1 and max/1 under the covers), you could also write:
def single_color?(image) do
use Image.Math
target_color = Image.get_pixel!(image, 0, 0)
Image.Math.min!(image == target_color) == 255.0
end
kip
I’ll look into this. It may be a side effect of either (a) the underlying histogram used to drive this process or (b) the affect of the alpha band in your source image. Here’s some examples that appear to work correctly so maybe it’s an edge case when the color is 0?
iex> Image.new!(1, 1, color: [1, 1, 1]) |> Image.dominant_color!(bins: 256)
[1, 1, 1]
iex> Image.new!(1, 1, color: [3, 3, 3]) |> Image.dominant_color!(bins: 256)
[3, 3, 3]
iex> Image.new!(1, 1, color: [128, 128, 128]) |> Image.dominant_color!(bins: 256)
[128, 128, 128]
Its slow primarily because each call to Image.get_pixel!/3 results in a NIF call.
Secondly, images in libvips are demand-driven which means that transformation pipelines are executed as required to generate the resulting pixels. This is very time and space efficient overall - but it does mean there is no guarantee that all pixels are rendered and in memory (although that can be forced when required). Thats likely not the issue here thought - the first point will dominant.
It’s useful to think of images as being like lazy tensors. Set operations will win every time. libvips has a lot of optimised code (pipelining, SIMD instructions, …) for these.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance










