はじめに
これまで 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 で画像処理が簡単に実装できるため、バックエンドの実装の幅が広がりますね




















