DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

Elixir OpenCV: StbImageとNx.Tensor と Evision.Mat で相互変換

Elixir で OpenCV を使うときにはどのような素材をどこから取ってきてどう加工するかによって求められる関数が異なり、場合により複数のパッケージが連携する必要があるかと思います。

@ryowakabayashi さんのElixir Image と Nx と evision で相互変換 が大変参考になりました。

ここでは elixir-image/image よりシンプルな elixir-nx/stb_imagecocoa-xu/evision で相互変換をしてみようと思います。

https://qiita.com/RyoWakabayashi/items/34de2207bcf8d745a01a

セットアップ

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"}
])
Enter fullscreen mode Exit fullscreen mode

cocoa-xu/evisionパッチバージョンでも破壊的な変更があるようのでバージョンをロックしておいた方が無難そうです。

https://qiita.com/RyoWakabayashi/items/2846133bc7014319d172

cocoa-xu/evision の作者は好んで Cv エイリアスを使用しているのでそれに則ります。

https://github.com/cocoa-xu/evision/tree/main/examples

alias Evision, as: Cv
Enter fullscreen mode Exit fullscreen mode

画像加工の一例

一例として画像をこのように加工することができます。ここで StbImageNx.TensorEvision.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)
Enter fullscreen mode Exit fullscreen mode

こういう書き方もできそうです。

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)
Enter fullscreen mode Exit fullscreen mode

アントニオ猪木

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)
Enter fullscreen mode Exit fullscreen mode

また、画像をファイルとしてローカルに保存したい場合があるかもしれません。

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)
Enter fullscreen mode Exit fullscreen mode

Binary -> StbImage

画像バイナリの読み込みには StbImage.read_binary!/1 が手軽で便利です。StbImage 構造体が帰ります。

img_stb = StbImage.read_binary!(img_data)
Enter fullscreen mode Exit fullscreen mode

File -> Evision.Mat

ローカルに保存された画像ファイルを読み込む場合は Cv.imread/1 が使えます。

img_mat = Cv.imread(img_file)
Enter fullscreen mode Exit fullscreen mode

StbImage <-> Nx.Tensor

StbImage は簡単に Nx.Tensor へ変換できます。

img_nx = StbImage.to_nx(img_stb)
Enter fullscreen mode Exit fullscreen mode

逆も簡単です。

img_stb = StbImage.from_nx(img_nx)
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

Evision.Mat <-> StbImage

Evision.MatStbImage との間では直接の変換はできませんが、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())
Enter fullscreen mode Exit fullscreen mode

アルファチャネルを取り除く変換

場合によりアルファチャネルを取り除きたい場合があると思いますが、現時点では 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).
Enter fullscreen mode Exit fullscreen mode

Evison.MatNx.Tensor に変換するときにバックエンドをしていするとうまくいきます。

Cv.Mat.to_nx(img_mat, Nx.BinaryBackend)[[.., .., 0..2]]
Enter fullscreen mode Exit fullscreen mode

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs