Search code examples
elixirvips

Chroma Key Removal using Elixir / Vix / libvips


This is just some handler code copied from the Vix examples

defmodule VixExt do
  alias Vix.Vips.Image
  alias Vix.Vips.Operation

  @max_height 500

  def show(%Image{} = image) do
    height = Image.height(image)

    # scale down if image height is larger than 500px
    image =
      if height > @max_height do
        Operation.resize!(image, @max_height / height)
      else
        image
      end

    # write vips-image as png image to memory
    {:ok, image_bin} = Image.write_to_buffer(image, ".png")
    Kino.render(Kino.Image.new(image_bin, "image/png"))

    :ok
  end
end

The code that does stuff:

import VixExt

{:ok, fore} = Image.open("/home/user/Downloads/greenscreen.jpg")

{:ok, back} = Image.open("/home/user/Downloads/background.jpg")

# Lower bound green
{:ok, l_green} = Image.Math.greater_than(fore, [0.0, 100.0, 0.0])
# Upper bound green
{:ok, u_green} = Image.Math.less_than(fore, [100.0, 255.0, 95.0])

{:ok, color_fore_mask} = Image.Math.boolean_and(l_green, u_green)

{:ok, fore_mask} = Vix.Vips.Operation.bandbool(color_fore_mask, :VIPS_OPERATION_BOOLEAN_AND)

{:ok, masked} = Image.Math.subtract(fore, fore_mask)

{:ok, inverted_fore_mask} = Vix.Vips.Operation.invert(fore_mask)

{:ok, masked_back} = Image.Math.subtract(back, inverted_fore_mask)

{:ok, masked_bin} = Vix.Vips.Image.write_to_buffer(masked, ".jpg")
{:ok, masked_clone} = Vix.Vips.Image.new_from_buffer(masked_bin)


{:ok, masked_back_bin} = Vix.Vips.Image.write_to_buffer(masked_back, ".jpg")
{:ok, masked_back_clone} = Image.from_binary(masked_back_bin)

{:ok, composite} = Vix.Vips.Operation.add(masked_back, masked_clone)

show(composite)

The problem that I encountered at first is that the clipped images don't get added together properly. Elixir is immutable so I was kind of expecting that behaviour. However, they're all pointed to the same image so when I tried to add them together it was kind of a mashup of the originals.

I can force it to the right behaviour by loading preclipped images which then behaves as I expect but I don't want to have to save to disk for obvious reasons. So now I'm trying to force a copy so that I get the clipped version not the original.

The image shown is the result I want. The person starts out on a green background which gets removed and composited onto the scene.

The current error I'm getting is

** (MatchError) no match of right hand side value: {:error, "Failed to write VipsImage to memory"}

which happens anytime I try to write to the buffer twice.

I would have thought that green screen removal was a rather routine use case which would be easier to do. I'm perfectly happy to take a better approach.


Solution

  • The Image Elixir library now provides Image.chroma_key/2 in response to this challenge from @GeneticJam. Used like this which is basically:

    Image.chroma_key!(foreground) 
    |> then(&Image.compose!(background, &1))
    

    The default chroma covers the likely gamut of chroma green.