はじめに
これまで Python での画像処理や AI の学習・推論は実務でも扱ってきました
また、 Elixir は Phoenix を使った REST API に数年使っています
しかし、 Elixir で画像処理、 AI というのは未経験です
というわけで、 Nx と evision で Elixir での画像処理を実装してみました
実装したもの(Docker 上に環境構築):
2022/12/13 更新
最新のモジュールを使うように更新しました
参考記事:
Nx とは
Elixir で多次元配列(テンソル)を使うためのライブラリ
Python の numpy のような感覚で使えるため、画像処理に向いています
[
[1, 2],
[3, 4]
]
|> Nx.tensor()
|> Nx.divide(3)
上記のコードを Livebook で実行するとこんな感じ
テンソルの各要素が 3 で割られているのが分かりますね
evision とは
Elixir 用の OpenCV ラッパー
Python の OpenCV と同じことが Elixir 上で実行できます
Nx とも連携できるため、 Python と同じ感覚で画像処理できます
実行環境
- MacBook Pro 13 inchi
- 2.4 GHz クアッドコアIntel Core i5
- 16 GB 2133 MHz LPDDR3
- macOS Ventura 13.0.1
- Rancher Desktop 1.6.2
- メモリ割り当て 12 GB
- CPU 割り当て 6 コア
Livebook 0.8.0 の Docker イメージを元にしたコンテナで動かしました
コンテナ定義はこちらを参照
実行方法
リポジトリーをクローンして docker-compose up
するだけです
git clone https://github.com/RyoWakabayashi/elixir-learning.git
cd elixir-learning
docker-compose up
ビルドが終わると以下のように localhost の URL が表示されるので、ブラウザで開いてください
...
Attaching to elixir-learning-livebook-1
elixir-learning-livebook-1 | [Livebook] Application running at http://localhost:8080/?token=xxxxx
こんな感じで Livebook が開きます
Elixir 学習用のリポジトリーなので他のファイルもありますが、
evision/image_processing.livemd
が本記事で紹介しているコードです
evision の導入
evision インストールのための Dockerfile
Docker 上では evision インストールに必要なもの + 他で使うので Phoenix を入れています
RUN mix local.hex --force \
&& mix archive.install hex phx_new --force \
&& mix local.rebar --force
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install --no-install-recommends -y \
libopencv-dev \
build-essential \
erlang-dev
しかし、 Livebook 上から evision をインストールすると10分以上ビルドに時間がかかってしまったため、
以下のようにフラグを立て、ビルド済のものを使うようにしました
ENV EVISION_PREFER_PRECOMPILED=true
evision のインストール実行
他に使うものも併せてインストールします
Mix.install([
{:httpoison, "~> 1.8"},
{:evision, "~> 0.1"},
{:kino, "~> 0.8"},
{:nx, "~> 0.4"}
])
画像生成
Nx を使ってテンソルから画像生成できます
[
[
[255, 0, 0],
[255, 128, 0],
[255, 255, 0]
],
[
[0, 255, 0],
[0, 255, 128],
[0, 255, 255]
],
[
[0, 0, 255],
[128, 0, 255],
[255, 0, 255]
]
]
|> Nx.tensor(type: {:u, 8})
# Evision のマトリックスに変換
|> Evision.Mat.from_nx_2d()
# 見やすいように拡大
|> Evision.resize({300, 300}, interpolation: Evision.cv_INTER_AREA())
画像のダウンロード
HTTPoison を使って画像のバイナリデータを取得します
img_binary =
"https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png"
|> HTTPoison.get!()
|> then(&(&1.body))
バイナリデータをデコードします
img =
img_binary
|> Evision.imdecode(Evision.cv_IMREAD_COLOR())
画像の書込
Evision.imwrite
で画像をファイルに保存します
lenna_path = "Lenna.png"
Evision.imwrite(lenna_path, img)
画像の読込
Python の OpenCV と同じように画像ファイルを読み込みます
img = Evision.imread(lenna_path)
画像サイズも以下のようにして取得できます
size = Evision.Mat.shape(img)
Evision.Mat.to_nx
でテンソル化することもできます
Evision.Mat.to_nx(img)
リサイズ
リサイズも Python と同じように記述可能です
Evision.resize(img, {256, 128})
グレースケール
グレースケールでの読込もシンプルに記述できます
Evision.imread(lenna_path, flags: Evision.cv_IMREAD_GRAYSCALE())
読込済のマトリックスをグレースケールに変換する場合は Evision.cvtColor
を使います
定数は cv_
で evision に定義されています
Evision.cvtColor(img, Evision.cv_COLOR_BGR2GRAY())
二値化
閾値を指定しての二値化も同様です
{threshold, mono_img} =
lenna_path
|> Evision.imread(flags: Evision.cv_IMREAD_GRAYSCALE())
|> Evision.threshold(127, 255, Evision.cv_THRESH_BINARY())
IO.inspect(threshold)
mono_img
平行移動
平行移動にはアフィン変換を利用します
アフィン変換用の変換行列は以下のように定義します
まず2次元リストで定義した後、
それを Nx で tensor に変換し、
evision で更に matrix に変換します
affine =
[
[1, 0, 100],
[0, 1, 50]
]
|> Nx.tensor(type: {:f, 32})
|> Evision.Mat.from_nx()
あとは Python と同じように wrapAffine
に変換行列と出力サイズを与えるだけです
Evision.warpAffine(img, affine, {512, 512})
回転
回転の場合は getRotationMatrix2D
に回転の中心座標、角度、スケールを指定して変換行列を取得します
affine = Evision.getRotationMatrix2D({512 / 2, 512 / 2}, 70, 1)
Evision.warpAffine(img, affine, {512, 512})
ぼかし
その他、各種フィルター系も Python と同じように記述できます
通常のブラー
Evision.blur(img, {9, 9})
中央値ブラー
Evision.medianBlur(img, 9)
ガウシアンブラー
Evision.gaussianBlur(img, {9, 9}, 5)
図形描画
四角形や楕円などの図形も描画できます
線
img
# 直線
|> Evision.line(
# 始点{x, y}
{200, 400},
# 終点{x, y}
{300, 450},
# 色{R, G, B}
{0, 255, 255},
# 線の太さ
thickness: 5
)
# 矢印
|> Evision.arrowedLine(
# 始点{x, y}
{300, 200},
# 終点{x, y}
{400, 150},
# 色{R, G, B}
{255, 255, 0},
# 線の太さ
thickness: 3,
# 頭の大きさ
tipLength: 0.3
)
四角形
img
# 四角形
|> Evision.rectangle(
# 左上{x, y}
{150, 120},
# 右下{x, y}
{225, 320},
# 色{R, G, B}
{0, 0, 255},
# 線の太さ
thickness: 12,
# 線の引き方(角がギザギザになる)
lineType: Evision.cv_LINE_4()
)
|> Evision.rectangle(
# 左上{x, y}
{50, 120},
# 右下{x, y}
{125, 320},
# 色{R, G, B}
{0, 0, 255},
# 線の太さ
thickness: 12,
# 線の引き方(角が滑らかになる)
lineType: Evision.cv_LINE_AA()
)
|> Evision.rectangle(
# 左上{x, y}
{250, 60},
# 右下{x, y}
{325, 110},
# 色{R, G, B}
{0, 255, 0},
# 塗りつぶし
thickness: -1
)
楕円、扇形
img
# 円
|> Evision.circle(
# 中心{x, y}
{100, 100},
# 半径
50,
# 色{R, G, B}
{255, 0, 0},
# 塗りつぶし
thickness: -1
)
# 楕円
|> Evision.ellipse(
# 中心{x, y}
{300, 300},
# {長径, 短径}
{100, 200},
# 回転角度
30,
# 弧の開始角度
0,
# 弧の終了角度
360,
# 色{R, G, B}
{255, 255, 0},
# 線の太さ
thickness: 3
)
# 扇形
|> Evision.ellipse(
# 中心{x, y}
{400, 200},
# {長径, 短径}
{100, 100},
# 回転角度
0,
# 弧の開始角度
100,
# 弧の終了角度
200,
# 色{R, G, B}
{0, 255, 0},
# 塗りつぶし
thickness: -1
)
文字描画
文字も書けます
img
|> Evision.putText(
# 文字列
"Lenna",
# 左下{x, y}
{150, 200},
# フォント種類
Evision.cv_FONT_HERSHEY_SIMPLEX(),
# フォントサイズ
2.5,
# 文字色
{0, 0, 255},
# 文字太さ
thickness: 5
)
Nx との連携
OpenCV と numpy のように、 Evision と Nx も連携できます
Evision.Mat.to_nx
と Evision.Mat.from_nx_2d
で Nx テンソルと相互変換できます
img
|> Evision.Mat.to_nx(Nx.BinaryBackend)
|> Nx.transpose(axes: [1, 0, 2])
|> Evision.Mat.from_nx_2d()
|> dbg
img
|> Evision.Mat.to_nx(Nx.BinaryBackend)
|> Nx.mean(axes: [2])
|> Nx.broadcast({512, 512, 3}, axes: [0, 1])
|> Evision.Mat.from_nx_2d()
|> dbg
おわりに
Evision を使うことで、 Python と同じことが同じように Elixir で実装できました
Elixir で画像処理が簡単に実装できるため、バックエンドの実装の幅が広がりますね