View Source Evision Example - Warp Perspective

Mix.install(
  [
    {:evision, "~> 0.1.31"},
    {:kino, "~> 0.9.0"},
    {:req, "~> 0.3.0"}
  ],
  system_env: [
    # optional, defaults to `true`
    # set `EVISION_PREFER_PRECOMPILED` to `false`
    # if you prefer `:evision` to be compiled from source
    # note that to compile from source, you may need at least 1GB RAM
    {"EVISION_PREFER_PRECOMPILED", true},

    # optional, defaults to `true`
    # set `EVISION_ENABLE_CONTRIB` to `false`
    # if you don't need modules from `opencv_contrib`
    {"EVISION_ENABLE_CONTRIB", true},

    # optional, defaults to `false`
    # set `EVISION_ENABLE_CUDA` to `true`
    # if you wish to use CUDA related functions
    # note that `EVISION_ENABLE_CONTRIB` also has to be `true`
    # because cuda related modules come from the `opencv_contrib` repo
    {"EVISION_ENABLE_CUDA", false},

    # required when
    # - `EVISION_ENABLE_CUDA` is `true`
    # - and `EVISION_PREFER_PRECOMPILED` is `true`
    #
    # set `EVISION_CUDA_VERSION` to the version that matches
    # your local CUDA runtime version
    #
    # current available versions are
    # - 111
    # - 114
    # - 118
    {"EVISION_CUDA_VERSION", "118"},

    # require for Windows users when
    # - `EVISION_ENABLE_CUDA` is `true`
    # set `EVISION_CUDA_RUNTIME_DIR` to the directory that contains
    # CUDA runtime libraries
    {"EVISION_CUDA_RUNTIME_DIR", "C:/PATH/TO/CUDA/RUNTIME"}
  ]
)

Introduction

This notebook will demonstrate how to perform perspective transformation.

It's useful to alias the module as something shorter when we make extensive use of the functions from certain modules.

alias Evision, as: Cv

Define Some Helper Functions

Let's prepare helper functions for preparing resources.

defmodule Helper do
  def download!(url, overwrite \\ false) do
    save_as = Path.join(System.tmp_dir!(), URI.encode_www_form(url))
    unless File.exists?(save_as) || overwrite, do: Req.get!(url, output: save_as)
    save_as
  end

end

Read the Test Image

# Read the test image
test_img_mat =
  "https://raw.githubusercontent.com/cocoa-xu/evision/main/test/testdata/warp_perspective.png"
  |> Helper.download!()
  |> Cv.imread()

Function hypot: returns the Euclidean norm

This function calcululates the Euclidean norm, which is useful when we want to know the length of a line segment between two points (Euclidean distance).

# hypot.(list(number())) function returns the Euclidean norm
hypot = fn l -> :math.sqrt(Enum.sum(Enum.map(l, fn i -> i * i end))) end

Calculate the Output Coordinates for Corners

# specify input coordinates for corners of red quadrilateral
top_left = [136, 113]
top_right = [206, 130]
bottom_right = [173, 207]
bottom_left = [132, 196]

input_points = Nx.tensor([top_left, top_right, bottom_right, bottom_left], type: :f32)

# get top and left dimensions and set to output dimensions of red rectangle
output_width =
  hypot.([
    Nx.to_number(Nx.subtract(input_points[[0, 0]], input_points[[1, 0]])),
    Nx.to_number(Nx.subtract(input_points[[0, 1]], input_points[[1, 1]]))
  ])
  |> round()

output_height =
  hypot.([
    Nx.to_number(Nx.subtract(input_points[[0, 0]], input_points[[3, 0]])),
    Nx.to_number(Nx.subtract(input_points[[0, 1]], input_points[[3, 1]]))
  ])
  |> round()

IO.puts("width: #{output_width}, height: #{output_height}")

# set upper left coordinates for output rectangle
x = Nx.to_number(input_points[[0, 0]])
y = Nx.to_number(input_points[[0, 1]])

# specify output coordinates for corners of red quadrilateral
top_left = [x, y]
top_right = [x + output_width - 1, y]
bottom_right = [x + output_width - 1, y + output_height - 1]
bottom_left = [x, y + output_height - 1]

output_points = Nx.tensor([top_left, top_right, bottom_right, bottom_left], type: :f32)

Compute Perspective Matrix

perspective_matrix = Cv.getPerspectiveTransform(input_points, output_points)
Kino.Tree.new(perspective_matrix)

Perspective Transformation

{img_height, img_width, _} = Cv.Mat.shape(test_img_mat)

# do perspective transformation setting area outside input to black
# Note that output size is the same as the input image size
output_img_mat =
  Cv.warpPerspective(
    test_img_mat,
    perspective_matrix,
    {img_width, img_height},
    flags: Cv.Constant.cv_INTER_LINEAR(),
    borderMode: Cv.Constant.cv_BORDER_CONSTANT(),
    borderValue: {0, 0, 0}
  )

[
  ["Input image", test_img_mat],
  ["Output image", output_img_mat]
]
|> Enum.map(fn [label, img] ->
  Kino.Layout.grid([img, Kino.Markdown.new("**#{label}**")], boxed: true)
end)
|> Kino.Layout.grid(columns: 2)