MarcusRiemer

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

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

kip

ex_cldr Core Team

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

kip

ex_cldr Core Team

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

kip

ex_cldr Core Team

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.

LostKobrakai

LostKobrakai

Returning early has no benefit if operations are faster batched than iterated. The overhead of doing many NIF calls is one you want to avoid.

The tensor option might be interesting though as Nx might have means of doing the “check if all are the same” code you currently have in elixir for a tensor, again to apply as a single operation.

al2o3cr

al2o3cr

Image.histogram sounds promising; you’d compute it and then check that only one “bin” of the result in each band is nonzero.

I say “promising” because the docs say it returns a 255x255 image but don’t give an example of how that’s computed…

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement