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.

Where Next?

Popular in Questions Top

_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
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
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
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
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
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43487 311
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement