Nervesで画像処理がしたい


この記事は#NervesJP Advent Calendar 2020、22日目の記事です。
遅刻してすみません。

はじめに

皆さんお疲れ様です。ringoと言います。普段はIT系の会社でIoTの研究開発をしています。この記事ではNervesでどうにかして画像処理がしたい!ということで色々調べつつ手を動かしたまとめになります。

並列処理に強くて処理の流れを書きやすいElixirは積和演算を画像データに対して行う画像処理に向いてそうだなというのは前々から思っていました。
ただ、OpenCVのような便利ライブラリがあるわけでもなくて、自分でElixirでゴリゴリ書くのもやってみたいけど、、、という日々を過ごしていると、実験: レガシーなImage ProcessingをElixirのパイプで書いてみるという記事を発見します。先を越された!ってなったけど、それなら自分はカメラを使って組み合わせて何かしら作ってみようというのがこの記事のスタートになります。
それはそうとしても、IoT向けプラットフォームというならばカメラ使ってかんたんな画像処理ぐらいはできないとねってのは個人的な思いとしてあるので、だったら私がやってみようという気持ちも無きにしもあらずです。

この記事で書くこと

ElixirでRaspberry Pi カメラモジュールを動かす

「elixir image processing」や「elixir opencv」で調べても目的にあった情報は出てこず早々に諦め、Nervesでカメラを使う方向へシフト。
それで調べると、 elixir-vision/picam というライブラリを見つけました。
picamのreadmeを読むと、カメラからのjpegの取得やカメラの設定ができるAPIが一通り揃っているみたいです。これが動けばカメラから画像を取り込むところはかんたんに実現できそうです。

Elixirで動作確認

Nervesで動かす前に、ラズパイ3にインストールされたElixirでpicamを動かしてみます。

iex(1)> Picam.Camera.start_link
{:ok, #PID<0.189.0>}
iex(2)> Picam.set_size(640, 0)
:ok
iex(3)> File.write!("sample.jpg", Picam.next_frame)
:ok


ちゃんと写ってることが確認できます。

他のエフェクトも試してみます。
エフェクトはPicam.set_img_effect(:sketch)で変更可能です。

Picam.set_img_effect(:cartoon)

Picam.set_img_effect(:emboss)

ストリーミングのサンプルを動かす

picam/examples/picam_http/にあるサンプルプログラムを動かしてみます。

Instructions for usage with Nerves
1. TODO

という不穏な文字列が見えますがとりあえずElixirから動かしてみます。
動かしてブラウザから確認すると、ちゃんと動画として表示されていることが確認できます。

画像の上下が逆なのはカメラの設置場所のせいです。
ここでiexでPicam.set_img_effect(:sketch)とかするとエフェクトのかかった動画が表示されます。

Nervesでカメラモジュールを動かす

Nervesにpicamライブラリを追加してビルドする

Nervesのプロジェクトを作ります。
picamの動作確認がしたいだけなので、mix.exs{:picam, "~> 0.4.0"}追記してビルドしたイメージをmicroSDに書き込みNervesのiexへログインします。
ここで問題発生、NervesはRead Onlyなのでさっきと同じようにFile.write!で書き込むことができません。
ただ、データのフォーマット見た感じカメラからデータを受け取ることはできているみたいです。

picam_httpを動かす(失敗)

それならば。。。というわけでexampleのpicamならどうだろうと思いNervesのプロジェクトを作成してみたところ以下のエラーでビルド失敗。

== Compilation error in file lib/camera_test/application.ex ==
** (CompileError) lib/camera_test/application.ex:18: undefined function worker/2
    (elixir 1.11.2) src/elixir_locals.erl:114: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
    (stdlib 3.13.2) erl_eval.erl:680: :erl_eval.do_apply/6

ToDoになっていたのはそういうことか。と納得しつつ、どうにかアドベントカレンダーの記事的な着地点を探すべく別の方法を探ります。

カメラから取得したjpegをブラウザで表示する

streamがダメならひとまず画像だけの表示で動かしてみます。
流れとしてはpicamで取得した画像をcowboyでブラウザに表示します。

Nervesの設定は次の記事を参考に環境構築から書き込みまでを行いました。

mix.exsに以下を追記してmix deps.get

mix.exs
{:nerves_network, "~> 0.5", targets: @all_targets},
{:picam, "~> 0.4.0"},
{:plug_cowboy, "~> 2.0"}

application.exにはcowboyのプロセスを追加

application.ex
children = [
  Plug.Cowboy.child_spec(scheme: :http, plug: CameraTest.Router, options: [port: 4001])
] ++ children(target())

画像の取得とブラウザへの表示部分です。

router.ex
defmodule CameraTest.Router do
  use Plug.Router

  plug Plug.Parsers, parsers: [:urlencoded], pass: []
  plug :match
  plug :dispatch

  get "/image" do
    image = get_frame |> Base.encode64
    markup = """
    <html>
    <head>
      <title>Picam Take Picture</title>
    </head>
    <body>
      <img src="data:image/jpg;base64,#{image}" />
    </body>
    </html>
    """
    conn
    |> put_resp_header("Content-Type", "text/html")
    |> send_resp(200, markup)
  end

  match _ do
      send_resp(conn, 404, "not found\n")
  end

  def get_frame do
    Picam.Camera.start_link
    # Picam.set_img_effect(:sketch)
    Picam.set_size(640, 0) # 0 automatically calculates height
    Picam.next_frame
  end
end

取得したjpegをそのまま貼り付けたら表示されなかったのでbase64にエンコードしてから表示しています。
ここまでできたらhttp://ipAddress:4001/image にアクセスしてみると、、、

表示されました!動画ではなく画像なのでブラウザを更新しないと新しい画像が取得されません。
(カメラの画像に紫がかったラインが見れるのはセンサーを割ってしまったためなので動作とは関係ないです。)
(ラズパイ0上のNervesがwebサーバーになって、ブラウザに画像が表示されてるのをどうやって表すか模索した画像です)

終わりに

やりたかったNervesで画像処理はまだ難しそうですが、第一歩としてNervesからラズパイのカメラモジュールを動かすことができました。ここまででも定点カメラや画像の差分で物体検知ぐらいはできるんじゃないでしょうか?
当初ノリと勢いでNerves勉強会参加して、アドベントカレンダー登録したはいいものの、何を作ろうか考えていましたが、ひとまずここまでで投稿します。
お疲れさまでした。良いお年を!