Elixir で OpenCV を使うときにはどのような素材をどこから取ってきてどう加工するかによって求められる関数が異なり、場合により複数のパッケージが連携する必要があるかと思います。
@RyoWakabayashi さんのElixir Image と Nx と evision で相互変換 が大変参考になりました。
ここでは elixir-image/image よりシンプルな elixir-nx/stb_image と cocoa-xu/evision で相互変換をしてみようと思います。
セットアップ
Mix.install([
{:nx, "~> 0.5.0"},
{:evision, "0.1.31"},
{:stb_image, "~> 0.6.0"},
# 画像を Web からダウンロードするため
{:req, "~> 0.3.0"},
# Livebook上で画像を表示するため
# {:kino, "~> 0.9.0"}
])
cocoa-xu/evision は パッチバージョンでも破壊的な変更があるようのでバージョンをロックしておいた方が無難そうです。
cocoa-xu/evision の作者は好んで Cv
エイリアスを使用しているのでそれに則ります。
alias Evision, as: Cv
画像加工の一例
一例として画像をこのように加工することができます。ここで StbImage
と Nx.Tensor
と Evision.Mat
で相互変換がされています。
img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Antonio_Inoki_IMG_0398-2_20121224.JPG/330px-Antonio_Inoki_IMG_0398-2_20121224.JPG"
{x, y, w, h} = {40, 30, 250, 250}
rect_start_point = {x, y}
rect_end_point = {x + w, y + h}
rect_color = {0, 255, 0}
rect_options = [thickness: 5]
Req.get!(img_url)
|> then(& &1.body)
|> StbImage.read_binary!()
|> StbImage.to_nx()
|> Cv.Mat.from_nx_2d()
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
|> Cv.rectangle(rect_start_point, rect_end_point, rect_color, rect_options)
こういう書き方もできそうです。
img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Antonio_Inoki_IMG_0398-2_20121224.JPG/330px-Antonio_Inoki_IMG_0398-2_20121224.JPG"
save_as = Path.join(System.tmp_dir!(), URI.encode_www_form(img_url))
{x, y, w, h} = {40, 30, 250, 250}
rect_start_point = {x, y}
rect_end_point = {x + w, y + h}
rect_color = {0, 255, 0}
rect_options = [thickness: 5]
Req.get!(img_url, output: save_as)
|> then(fn _ -> Cv.imread(save_as) end)
|> Cv.rectangle(rect_start_point, rect_end_point, rect_color, rect_options)
image creadit: https://ja.wikipedia.org/wiki/アントニオ猪木
画像の読込
画像はバイナリとして読み込む場合もあれば、ファイルを読み込む場合もあると思います。
Req.get!/2
を用いて Web から画像バイナリをダウンロードすることができます。
img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Antonio_Inoki_IMG_0398-2_20121224.JPG/330px-Antonio_Inoki_IMG_0398-2_20121224.JPG"
img_data = Req.get!(img_url) |> then(& &1.body)
また、画像をファイルとしてローカルに保存したい場合があるかもしれません。
download = fn url ->
save_as = Path.join(System.tmp_dir!(), URI.encode_www_form(url))
Req.get!(url, output: save_as)
save_as
end
img_file = download.(img_url)
Binary -> StbImage
画像バイナリの読み込みには StbImage.read_binary!/1
が手軽で便利です。StbImage
構造体が帰ります。
img_stb = StbImage.read_binary!(img_data)
File -> Evision.Mat
ローカルに保存された画像ファイルを読み込む場合は Cv.imread/1
が使えます。
img_mat = Cv.imread(img_file)
StbImage <-> Nx.Tensor
StbImage
は簡単に Nx.Tensor
へ変換できます。
img_nx = StbImage.to_nx(img_stb)
逆も簡単です。
img_stb = StbImage.from_nx(img_nx)
Nx.Tensor <-> Evision.Mat
Nx.Tensor
から Evision.Mat
への変換も基本的には同様にできるのですが、ここでは注意が必要です。2点あります。
RGB vs BGR
- OpenCVは色チャネルをB(青)・G(緑)・R(赤)の順に保持しています。ですので、適切な場所で B と R 入れ替える必要があります。
Evision.Mat.from_nx vs Evision.Mat.from_nx_2d
- これは正直いうとよくわかりませんが、2Dの画像を取り扱う場合は
from_nx_2d
を使わないとうまく処理ができないようです。OpenCV の関数を使って画像加工しても何も起こらない場合はここを見落としている可能性があります。エラーがでないので厄介です。
img_nx =
img_mat
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
|> Cv.Mat.to_nx()
img_mat =
img_nx
|> Cv.Mat.from_nx_2d()
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
Evision.Mat <-> StbImage
Evision.Mat
と StbImage
との間では直接の変換はできませんが、Nx.Tensor
を介在させることにより実現可能です。
img_mat
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
|> Cv.Mat.to_nx()
|> StbImage.from_nx()
img_stb
|> StbImage.to_nx()
|> Cv.Mat.from_nx_2d()
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
アルファチャネルを取り除く変換
場合によりアルファチャネルを取り除きたい場合があると思いますが、現時点では Evision.Backend
はスライス構文に対応していないようです。
Cv.Mat.to_nx(img_mat)[[.., .., 0..2]]
# ** (RuntimeError) operation slice is not yet supported on Evision.Backend.
# Please use another backend like Nx.BinaryBackend or Torchx.Backend.
# To use Torchx.Backend, :torchx should be added to your app's deps.
# Please see <https://github.com/elixir-nx/nx/tree/main/torchx> for more information on how to install and use it.
# To convert the tensor to another backend, please use Evision.Mat.to_nx(tensor, Backend.ModuleName)
# for example, Evision.Mat.to_nx(tensor, Nx.BinaryBackend) or Evision.Mat.to_nx(tensor, Torchx.Backend).
Evison.Mat
を Nx.Tensor
に変換するときにバックエンドをしていするとうまくいきます。
Cv.Mat.to_nx(img_mat, Nx.BinaryBackend)[[.., .., 0..2]]