Evision Example - Principal Components Analysis
Mix.install([
{:evision, "~> 0.2"},
{:kino, "~> 0.7"},
{:req, "~> 0.5"}
], 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, into: File.stream!(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
<> =
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)