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

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
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
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
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
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
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
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
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 27727 240
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New

We're in Beta

About us Mission Statement