はじめに
本記事は Qiita AdventCalendar2022 Elixir vol2 7日目の記事です
Evisionシリーズの1つ目の記事になります
このシリーズは技術評論社のOpenCVではじめよう ディープラーニングによる画像認識の3章の内容を参考にElixirとEvisionで書き換えて行っています
- Livebook + Evision基本編
- EvisionのCascadeClassifierで顔認識
- EvisionのDNN.ClassificationModelを使ってクラス分類
- EvisionのDNN.TextDetectionModelDBでテキスト検出
- EvisionのDNN.DetectionModelでYOLOv4を使って物体検知 12/14公開
- EvisionのDNN.SegmentationModelでセグメンテーション 12/14公開
- YOLOv4の結果を切り取ってEfficientnetで更に分類するシステムをEvisionで書く 12/18公開
この記事ではLivebook上で画像処理ライブラリOpenCVのElixirラッパーのEvisionを使うときの基本的な関数の使い方を紹介します
Livebookについて
Livebook is a web application for writing interactive and collaborative code notebooks.
LivebookはコラボレーションもできるElixir対話的実行環境を提供するWebアプリケーションです
Evisionについて
- OpenCVのElixirラッパー
- Port等を使わずに直にElixirから使うことができる
- Nxのバックエンドとして使用でき、行列演算の高速化(CPU,GPU)ができる
- Nxデータに相互に変換できる
- 膨大な画像処理の関数を使用できる
- DNNモジュールでCV分野の多くの学習済みモデルを使用できる
setup
livebookは公式サイトを参考にインストールしてください
livebookを起動してノートブックを作成したらsetupセルに以下を追加して実行してください
Mix.install([
{:evision, "~> 0.1.21"},
{:kino, "~> 0.7.0"},
{:exla, "~> 0.4.0"}
])
黒色画像の作成 -- Mat.zeros
Mat.zerosは第一引数で指定したサイズで0で埋めた行列を作成します
ついでにモジュール名をaliasで短くします
alias Evision, as: Ev
Ev.Mat.zeros({100, 100}, :u8)
RGBの値で作ってMatで表示したい場合は Mat.from_nx_2dを経由する必要があるようです
Ev.Mat.zeros({100, 100, 3}, :u8)
Ev.Mat.zeros({100, 100, 3}, :u8)
|> Ev.Mat.to_nx
|> Ev.Mat.from_nx_2d()
グレー画像の作成 -- Mat.full
Mat.fullは第2引数で指定した値で埋めた第1引数のサイズの行列を作ります
Ev.Mat.full({100, 100}, 200, :u8)
Nxから色画像の作成 -- Mat.from_nx_2d
Mat.fullだと単色画像はできないので、Nx.broadcastを使って作成していきます
Nx.broadcastの例を見ても分かりづらいですが
Nx.tensorで作った行列の要素数が合うようにshapeを指定すると
単色のRGBの行列を作ることができます
Nx.broadcast(200,{100,100})
#Nx.Tensor<
s64[100][100]
[
[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, ...],
...
]
>
shapeが一致しないとエラー
Nx.tensor([0,0,255])
|> Nx.broadcast({100,100,5})
** (ArgumentError) cannot broadcast tensor of dimensions {3} to {100, 100, 1} with axes [2]
NxからEv.Mat.from_nxするときの注意点ですが
Nxのデフォルトのタイプは :f16でそれだと Evisionでは対応していないので
:u8か:f32にタイプを指定してください
今回はRGB値なので:u8を指定します
shape = {250, 250, 3}
red =
Nx.tensor([0, 0, 255], type: :u8)
|> Nx.broadcast(shape)
|> Ev.Mat.from_nx_2d()
green =
Nx.tensor([0, 255, 0], type: :u8)
|> Nx.broadcast(shape)
|> Ev.Mat.from_nx_2d()
blue =
Nx.tensor([255, 0, 0], type: :u8)
|> Nx.broadcast(shape)
|> Ev.Mat.from_nx_2d()
to_img = fn img -> Ev.imencode(".png", img) |> Kino.Image.new(:png) end
Kino.Layout.grid(
[to_img.(red), to_img.(green), to_img.(blue)],
columns: 3
)
複数の画像を表示する際の小技ですが、
Ev.encodeでMatのデータをpng形式に変換し
Kino.Image.new()で png形式のKino.Imageを作成
Kino.Layout.gridの第1引数にリストで渡して表示する
デフォルトだと縦方向なので:columnsオプションを指定すると以下のように表示できます
画像の読み込み -- imread
git cloneしてきたlivebookで起動しているので、cwdはlivebook直下になります
static/imagesにロゴ画像があるのでそちらを読み込みます
img = "static/images/logo.png" |> Ev.imread()
ファイル書き込み -- imwrite
ファイルを書き込む時は適切な拡張子をつけていれば、それに沿って画像ファイルを生成します
Ev.imwrite("images/logo.png", img)
Ev.imwrite("images/logo.jpg", img)
拡張子を指定しなければエラーになります
Ev.imwrite("images/logo", img)
{:error, "OpenCV(4.6.0) /Users/runner/work/evision/evision/3rd_party/opencv/opencv-4.6.0/modules/imgcodecs/src/loadsave.cpp:730: error: (-2:Unspecified error) could not find a writer for the specified extension in function 'imwrite_'\n"}
リサイズ -- resize
第2引数に縦横のタプルを渡します
img = Ev.resize(img, {200, 200})
回転 -- rotate
時計回りに90度
Ev.rotate(img, Ev.cv_ROTATE_90_CLOCKWISE())
反時計回りに90度
Ev.rotate(img, Ev.cv_ROTATE_90_COUNTERCLOCKWISE())
180度回転
Ev.rotate(img, Ev.cv_ROTATE_180())
反転 -- flip
# 縦方向に反転
Ev.flip(img, 0)
# 横方向に反転
Ev.flip(img, 1)
連結 -- hconcat, vconcat
横連結
Ev.hconcat([img, img])
縦連結
Ev.vconcat([img, img])
結合箇所のサイズ(横連結の場合は高さ)が違うとエラーになります
Ev.hconcat([img, Ev.resize(img, {100, 100})])
{:error,"OpenCV(4.6.0) /Users/runner/work/evision/evision/3rd_party/opencv/opencv-4.6.0/modules/core/src/matrix_operations.cpp:67: error: (-215:Assertion failed) src[i].dims <= 2 && src[i].rows == src[0].rows && src[i].type() == src[0].type() in function 'hconcat'\n"}
結合箇所以外はサイズが違っても問題なく結合できます
Ev.hconcat([img, Ev.resize(img, {100, 200})])
短形図形を描く -- rectangle
短形(四角)を画像内に描画します、物体検知(YOLOとか)でよく使うあれです
引数はそれぞれ
- 1 image
- 2 始点(x, y)
- 3 終点(x+w, y+h)
- 4 色
- 5 オプションいろいろ、 今回は線の太さ
{x, y, w, h} = {25, 25, 50, 50}
color = {255, 255, 255}
Ev.rectangle(img, {x, y}, {x + w, y + h}, color, thickness: 3)
thicknessを-1にすると塗りつぶしになります
Ev.rectangle(img, {x, y}, {x + w, y + h}, color, thickness: -1)
円を描く -- circle, ellipse
- 1 画像
- 2 中心座標
- 3 半径
- 4 色
- 5 その他オプション
Ev.circle(img, {100, 100}, 40, color, thickness: 3)
ellipseは楕円を描くときに使います
- 1 画像
- 2 中心座標
- 3 {縦幅、横幅}
- 4 回転
- 5 描画位置始点
- 6 描画位置終点
- 7 色
- 8 その他オプション
Ev.ellipse(img, {100, 100}, {50, 100}, 60, 0, 360, color)
幅を揃えれば円になりますし
90度回転させて、描画位置を0~180に指定すると半円になります
Ev.ellipse(img, {100, 100}, {50, 50}, 90, 0, 180, color, thickness: -1)
文字を入れる -- putText
画像の指定座標に文字を描画します
- 1 画像
- 2 入れる文字
- 3 {x座標,y座標}
- 4 フォント
- 5 文字の大きさ
- 6 文字の色
Ev.putText(img, "LiveViewJP", {30, 50}, Ev.cv_FONT_ITALIC(), 1.0, color)
切り取り -- Mat.roi
roiは関心領域(Region of Interest)の略称だそうです
指定した領域を切り抜きます
- 1 画像
- 2 {x, y, x + w, y + h}
Ev.Mat.roi(img, {50, 50, 100, 100})
アフィン変換 -- warpAffine
最後は名前だけはよく聞くアフィン変換です
使いこなすには行列計算の知識とかいるのでちょっとハードル高そうですね
画像の拡大縮小、回転、平行移動などを行列を使って座標を変換する事をアフィン変換と呼びます。 by https://imagingsolution.net/imaging/affine-transformation/
src_img = img
{width, height} = {200, 200}
# 並行移動
x = 50
y = -10
m_shift = Nx.tensor([[1, 0, x], [0, 1, y]], type: :f32) |> Ev.Mat.from_nx()
sheer_img = Ev.warpAffine(src_img, m_shift, {width, height})
# 回転
angle = 45
m_rotate = Ev.getRotationMatrix2D({width / 2, height / 2}, angle, 1.0)
rotation_img = Ev.warpAffine(src_img, m_rotate, {width, height})
# スキュー(平行四辺形に変形する処理)
a = 0.2
b = 0.0
m_shear = Nx.tensor([[1, a, 0], [b, 1, 0]], type: :f32) |> Ev.Mat.from_nx()
shear_img = Ev.warpAffine(src_img, m_shear, {width, height})