Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Image division

image_division_en.livemd

Image division

Mix.install([
  {:nx, "~> 0.9"},
  {:evision, "~> 0.2"},
  {:req, "~> 0.5"},
  {:kino, "~> 0.14"}
])

Modules

Install dependent modules in the setup cell. It’s good to see the dependencies made explicit. Jupyter does not make dependencies explicit.

This notebook installs the following modules.

  • Nx: Perform matrix operations in Elixir
  • evision: An OpenCV wrapper, Process images in Elixir
  • Req: Download the image in this notebook
  • Kino: Powerful UI/UX in notebooks

What to do in this notebook

Divide an image vertically or horizontally and then merge them together.

Get the image

Download the image of me, Ryo Wakabayashi.

img_path = "rwakabay.jpg"

Req.get!(
  "https://www.elixirconf.eu/assets/images/ryo-wakabayashi.jpg",
  output: File.stream!(img_path)
)

img_mat = Evision.imread(img_path)

When you load an image with evision, the image will appear in the notebook. The automatic display of results is much more convenient than with Jupyter

Then prepare for image processing.

# Get the filename, extension, and shape
file_ext = Path.extname(img_path)
file_basename = Path.basename(img_path, file_ext)
img_shape = img_mat.shape

{file_basename, file_ext, img_shape}
# Prepare a function to save a list of divided images
save_img_tensor_list = fn img_tensor_list, kind ->
  img_tensor_list
  |> Enum.with_index()
  |> Enum.map(fn {img_tensor, index} ->
    dst_filename = "#{file_basename}_#{kind}_#{index}#{file_ext}"

    img_mat = Evision.Mat.from_nx_2d(img_tensor)

    Evision.imwrite(dst_filename, img_mat)

    dst_filename
  end)
end

Divide horizontally

First, let’s divide the image horizontally

# chunk_size is number of pixels in width and height of the divided image
chunk_size = 60
img_mat
# Convert evision image data to Nx tensor
|> Evision.Mat.to_nx(Nx.BinaryBackend)
# Nx.to_batched divides the tensor into pieces of the chunk_size
|> Nx.to_batched(chunk_size)
# Evision OpenCV keeps colors in BGR order, so invert to RGB
|> Enum.map(&Nx.reverse(&1, axes: [2]))
# Display divided images
|> Enum.map(&Kino.Image.new(&1))
|> Kino.Layout.grid(columns: 1)
|> dbg()

I’m divided horizontally.

Kino.Layout.grid makes it easy display images side by side.

dbg shows the output of each pipele. I’* Original image

  • Converted to a tensor
  • Batched to a list of tensors
  • Inverted
  • Displayed
# Save divided images with the prepared funtion
img_mat
|> Evision.Mat.to_nx(Nx.BinaryBackend)
|> Nx.to_batched(chunk_size)
|> save_img_tensor_list.("h")

The file name of the saved image is now displayed.

Now let’s concatenate the divided images.

# Get a list of divided images
Stream.unfold(0, fn counter -> {counter, counter + 1} end)
|> Stream.map(&{&1, "#{file_basename}_h_#{&1}#{file_ext}"})
|> Stream.take_while(fn {_, f} -> File.exists?(f) end)
# Load each image
|> Enum.map(fn {index, filename} ->
  new_tensor =
    filename
    |> Evision.imread()
    |> Evision.Mat.to_nx(Nx.BinaryBackend)

  # Invert color for even numbers to show that they were divided
  case rem(index, 2) do
    0 ->
      Nx.reverse(new_tensor, axes: [2])

    _ ->
      new_tensor
  end
end)
# Concatenate the images
|> Nx.concatenate()
# Trim to image size
|> Nx.slice([0, 0, 0], Tuple.to_list(img_shape))
|> Kino.Image.new()

The images were concatenated and horizontal striped.

Divide vertically

Next, let’s divide the image vertically.

Lay the image to divide it vertically.

Bacause, Nx.to_batched can only split horizontally.

img_mat
|> Evision.Mat.to_nx(Nx.BinaryBackend)
# Swap the vertical and horizontal
|> Nx.transpose(axes: [1, 0, 2])
# Divide
|> Nx.to_batched(chunk_size)
# Swap the vertical and horizontal
|> Enum.map(&Nx.transpose(&1, axes: [1, 0, 2]))
# BGR to RGB
|> Enum.map(&Nx.reverse(&1, axes: [2]))
|> Enum.map(&Kino.Image.new(&1))
|> Kino.Layout.grid(columns: 10)
# If not transposed, the image can only be divided horizontally
|> dbg()

I’m divided vertically.

When Nx.transpose is turned off, I fall on its side and divided horizontally.

This is why Livebook and dbg work so well together for image processing.

# Save the divided images
img_mat
|> Evision.Mat.to_nx(Nx.BinaryBackend)
# Swap the vertical and horizontal
|> Nx.transpose(axes: [1, 0, 2])
# Divide
|> Nx.to_batched(chunk_size)
# Swap the vertical and horizontal
|> Enum.map(&Nx.transpose(&1, axes: [1, 0, 2]))
|> save_img_tensor_list.("v")
# Concatenate divided images
Stream.unfold(0, fn counter -> {counter, counter + 1} end)
|> Stream.map(&{&1, "#{file_basename}_v_#{&1}#{file_ext}"})
|> Stream.take_while(fn {_, f} -> File.exists?(f) end)
|> Enum.map(fn {index, filename} ->
  new_tensor =
    filename
    |> Evision.imread()
    |> Evision.Mat.to_nx(Nx.BinaryBackend)

  # Invert color for even numbers
  case rem(index, 2) do
    0 ->
      Nx.reverse(new_tensor, axes: [2])

    _ ->
      new_tensor
  end
end)
|> Nx.concatenate(axis: 1)
|> Nx.slice([0, 0, 0], Tuple.to_list(img_shape))
|> Kino.Image.new()

The images were concatenated and vertical striped.

Divide into tiles

Finally, divide the image into tiles.

To tile, divide both horizontally and vertically.

img_mat
|> Evision.Mat.to_nx(Nx.BinaryBackend)
# Divide horizontally
|> Nx.to_batched(chunk_size)
# Divide vertically
|> Enum.map(&Nx.transpose(&1, axes: [1, 0, 2]))
|> Enum.flat_map(&Nx.to_batched(&1, chunk_size))
|> Enum.map(&Nx.transpose(&1, axes: [1, 0, 2]))
# BGR to RGB
|> Enum.map(&Nx.reverse(&1, axes: [2]))
|> Enum.map(&Kino.Image.new(&1))
|> Kino.Layout.grid(columns: 10)
|> dbg()

I’m divided into tiles.

# Save divided images
img_mat
|> Evision.Mat.to_nx(Nx.BinaryBackend)
# Divide horizontally
|> Nx.to_batched(chunk_size)
# Divide vertically
|> Enum.map(&Nx.transpose(&1, axes: [1, 0, 2]))
|> Enum.flat_map(&Nx.to_batched(&1, chunk_size))
|> Enum.map(&Nx.transpose(&1, axes: [1, 0, 2]))
|> save_img_tensor_list.("t")
{width, _, _} = img_shape
h_size = div(width, chunk_size)

# Concatenate divided images
Stream.unfold(0, fn counter -> {counter, counter + 1} end)
|> Stream.map(&{&1, "#{file_basename}_t_#{&1}#{file_ext}"})
|> Stream.take_while(fn {_, f} -> File.exists?(f) end)
|> Enum.map(fn {t_index, filename} ->
  new_tensor =
    filename
    |> Evision.imread()
    |> Evision.Mat.to_nx(Nx.BinaryBackend)

  {new_tensor, t_index}
end)
|> Enum.chunk_every(h_size)
|> Enum.map(fn new_tensor_list ->
  new_tensor_list
  |> Enum.with_index()
  |> Enum.map(fn {{new_tensor, t_index}, v_index} ->
    # Invert color to check pattern
    cond do
      rem(v_index, 2) == rem(div(t_index, h_size), 2) ->
        Nx.reverse(new_tensor, axes: [2])

      true ->
        new_tensor
    end
  end)
  # Concatenate vertically
  |> Nx.concatenate(axis: 1)
end)
# Concatenate horizontally
|> Nx.concatenate()
# Trim to image size
|> Nx.slice([0, 0, 0], Tuple.to_list(img_shape))
|> Kino.Image.new()

I’m in a check pattern.