View Source Evision Example - Principal Components Analysis

Mix.install([
  {:evision, "~> 0.2"},
  {:kino, "~> 0.7"},
  {:req, "~> 0.3"}
], 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
  # - 118
  # - 121
  {"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"}
])
:ok

Helper Function

defmodule Helper do
  def download!(url, save_as, overwrite? \\ false) do
    unless File.exists?(save_as) do
      Req.get!(url, http_errors: :raise, output: save_as, cache: not overwrite?)
    end

    :ok
  end
end
{:module, Helper, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:download!, 3}}

alias

alias Evision, as: Cv
Evision

Download the test image

Helper.download!("https://docs.opencv.org/4.x/pca_test1.jpg", "pca_test.jpg")
:ok
import Bitwise

# Load image in grayscale
gray = Cv.imread("pca_test.jpg", flags: Cv.Constant.cv_IMREAD_GRAYSCALE())

# Convert image to binary
{_, bw} = Cv.threshold(gray, 50, 255, Cv.Constant.cv_THRESH_BINARY() ||| Cv.Constant.cv_THRESH_OTSU())

# Find all the contours in the thresholded image
{contours, _} = Cv.findContours(bw, Cv.Constant.cv_RETR_LIST(), Cv.Constant.cv_CHAIN_APPROX_NONE())

contours =
  contours
  # Calculate the area of each contour
  |> Enum.map(&{Cv.contourArea(&1), &1})
  # Ignore contours that are too small or too large
  |> Enum.reject(fn {area, _c} -> area < 100 or area > 100_000 end)

# area
Enum.map(contours, &elem(&1, 0))
[17192.0, 16830.0, 16150.5, 15367.5, 15571.0, 14842.0]

PCA analysis

contours = Enum.map(contours, &elem(&1, 1))

pca_analysis =
  for c <- contours, reduce: [] do
    acc ->
      # Construct a buffer used by the pca analysis
      %Evision.Mat{shape: shape, type: type} = c
      sz = elem(shape, 0)
      pts_binary = Cv.Mat.to_binary(c)
      data_pts = Cv.Mat.from_binary(pts_binary, type, sz, 2, 1)
      data_pts = Cv.Mat.as_type(data_pts, {:f, 64})

      # Perform PCA analysis
      {mean, eigenvectors, eigenvalues} = Cv.pcaCompute2(data_pts, Cv.Mat.empty())
      eigenvectors = Cv.Mat.to_nx(eigenvectors, Nx.BinaryBackend)
      eigenvalues = Cv.Mat.to_nx(eigenvalues, Nx.BinaryBackend)

      # Store the center of the object
      <<centre_x::float-size(64)-little, centre_y::float-size(64)-little, _::binary>> =
        Cv.Mat.to_binary(mean)

      centre_x = trunc(centre_x)
      centre_y = trunc(centre_y)

      # Store the eigenvalues and eigenvectors
      eval00 = Nx.slice(eigenvalues, [0, 0], [1, 1]) |> Nx.to_flat_list() |> Enum.at(0)
      eval10 = Nx.slice(eigenvalues, [1, 0], [1, 1]) |> Nx.to_flat_list() |> Enum.at(0)

      evec00 = Nx.slice(eigenvectors, [0, 0], [1, 1]) |> Nx.to_flat_list() |> Enum.at(0)
      evec01 = Nx.slice(eigenvectors, [0, 1], [1, 1]) |> Nx.to_flat_list() |> Enum.at(0)
      evec10 = Nx.slice(eigenvectors, [1, 0], [1, 1]) |> Nx.to_flat_list() |> Enum.at(0)
      evec11 = Nx.slice(eigenvectors, [1, 1], [1, 1]) |> Nx.to_flat_list() |> Enum.at(0)

      # Calculate the principal components
      p1 =
        {trunc(Float.round(centre_x + 0.02 * evec00 * eval00)),
         trunc(Float.round(centre_y + 0.02 * evec01 * eval00))}

      p2 =
        {trunc(Float.round(centre_x - 0.02 * evec10 * eval10)),
         trunc(Float.round(centre_y - 0.02 * evec11 * eval10))}

      cntr = {centre_x, centre_y}
      [{cntr, p1, p2} | acc]
  end

pca_analysis = Enum.reverse(pca_analysis)
[
  {{430, 407}, {691, 338}, {427, 397}},
  {{439, 326}, {697, 264}, {437, 317}},
  {{433, 239}, {683, 182}, {431, 230}},
  {{420, 169}, {666, 127}, {419, 161}},
  {{191, 291}, {176, 52}, {200, 290}},
  {{407, 90}, {645, 49}, {406, 82}}
]

visualisation

src = Cv.imread("pca_test.jpg")

# Draw each contour
src =
  for index <- 0..(Enum.count(contours) - 1), reduce: src do
    src ->
      Cv.drawContours(src, contours, index, {0, 0, 255}, thickness: 2)
  end
%Evision.Mat{
  channels: 3,
  dims: 2,
  type: {:u, 8},
  raw_type: 16,
  shape: {600, 800, 3},
  ref: #Reference<0.1624340523.638451732.91783>
}

A helper function

defmodule PACHelper do
  def drawAxis(src, {px, py}, {qx, qy}, colour, scale) do
    angle = :math.atan2(py - qy, px - qx)
    hypotenuse = :math.sqrt((py - qy) * (py - qy) + (px - qx) * (px - qx))
    qx = trunc(px - scale * hypotenuse * :math.cos(angle))
    qy = trunc(py - scale * hypotenuse * :math.sin(angle))
    src = Cv.line(src, {px, py}, {qx, qy}, colour, thickness: 1, style: Cv.Constant.cv_LINE_AA())

    px = trunc(qx + 9 * :math.cos(angle + :math.pi() / 4))
    py = trunc(qy + 9 * :math.sin(angle + :math.pi() / 4))
    src = Cv.line(src, {px, py}, {qx, qy}, colour, thickness: 1, style: Cv.Constant.cv_LINE_AA())

    px = trunc(qx + 9 * :math.cos(angle - :math.pi() / 4))
    py = trunc(qy + 9 * :math.sin(angle - :math.pi() / 4))
    Cv.line(src, {px, py}, {qx, qy}, colour, thickness: 1, style: Cv.Constant.cv_LINE_AA())
  end
end
{:module, PACHelper, <<70, 79, 82, 49, 0, 0, 13, ...>>, {:drawAxis, 5}}

Draw the principal components

src =
  for {cntr, p1, p2} <- pca_analysis, reduce: src do
    src ->
      src = Cv.circle(src, cntr, 3, {255, 0, 255}, thickness: 2)
      src = PACHelper.drawAxis(src, cntr, p1, {0, 255, 0}, 1)
      PACHelper.drawAxis(src, cntr, p2, {255, 255, 0}, 5)
  end

result = Cv.imencode(".png", src)

Kino.Image.new(result, :png)