JetsonでCSIカメラ(nvcamerasrc)をdockerコンテナ内から使う

JetsonでCSIカメラ(nvcamerasrc)をdockerコンテナ内から使う

ラズパイでもJetsonでも、CSIカメラのストリームを拾うにはgstreamerを使います。
ただこれがdocker内から使いたいとなるとかなり厄介でしたので、試してハマったことや実現方法をご紹介しておきたいと思います。
この記事の内容は、一応これで動いているというだけで一例と思ってください。
他にも良い方法があればコメント等でお知らせいただけますと幸いです。

結論から書いてしまうと、ホスト側でgstreamerを使いv4l2sinkを使って/dev/video1に映像を流します。
dockerコンテナからは、同じくgstreamerを使ってv4l2srcで/dev/video1からデータを読み出します。

コンテナ用のテストプログラム

まずはコンテナ内から使うPythonのテストスクリプトをご紹介しておきます。
OpenCVで映像を取得し、600フレーム分をtest(フレーム数).jpgという画像に出力するというものです。
ファイルを書き出すため、パーミッションは適切に設定する必要があります。

import cv2

video_source = '(この部分をいろいろ変更しつつ試します)'

cap = cv2.VideoCapture(video_source)
i = 0
while True:
    i += 1
    if i > 600:
        break

    ret, img = cap.read()
    if ret:
        cv2.imwrite('test'+str(i)+'.jpg',img)
cap.release()
ハマりポイントその1

そもそもコンテナ内からはnvcamerasrcにアクセスできません

これがわかるまでにかなり時間がかかりました。

まず、もともとCSIカメラを使う予定ではなかったコンテナを使っていたため、コンテナにnvcamera_socketをマウントする必要があるのでコンテナをrunし直します。
/tmpを丸ごとマウントしました。

またコンテナ内にOpenCVをビルドした際、gstreamerを使う予定ではなかったのでOpenCVのビルドし直しを行いました。
この手順は最終的にうまくいったパターンでも必要となります。

gstreamer関連のモジュールをインストールします。

apt update
apt install gstreamer1.0
apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav

続いてOpenCVを -D WITH_GSTREAMER=ON をつけて再ビルドします。
他のオプションは環境によると思いますので、ここでは触れません。
過去記事等を参考にしていただければと思います。

ここで早速gstreamerを使ってみました。
(記事執筆時点ではメモを頼りに書いていますので、もしかしてうまく動かなかったケースの内容は実際に流したものと少し違うかもしれません)

うまく動かなかったコンテナ側のコード

video_source='nvcamerasrc ! video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080, format=(string)I420, framerate=(fraction)30/1 ! nvvidconv ! video/x-raw, width=(int)1920, height=(int)1080, format=(string)BGRx ! videoconvert ! appsink'

gst-launchを使ってパイプラインを単純化したものをコマンド実行してみます。
ディスプレイ出力するので、ホスト側でxhostによるウィンドウの受け入れと、コンテナ側でDISPLAY変数を設定しておきます。

うまく動かなかったコンテナ側のコマンド(ホストマシンでは問題なく動きます)

gst-launch-1.0 nvcamerasrc ! 'video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080, format=(string)I420, framerate=(fraction)30/1' ! nvegltransform ! nveglglessink

コンテナからの実行結果

(python3:20419): GStreamer-CRITICAL **: gst_element_get_state: assertion ‘GST_IS_ELEMENT (element)’ failed
(python3:20419): GLib-GObject-WARNING **: invalid cast from ‘GstNvCameraSrc’ to ‘GstBin’
(python3:20419): GStreamer-CRITICAL **: gst_bin_iterate_elements: assertion ‘GST_IS_BIN (bin)’ failed
(python3:20419): GStreamer-CRITICAL **: gst_iterator_next: assertion ‘it != NULL’ failed
(python3:20419): GStreamer-CRITICAL **: gst_iterator_free: assertion ‘it != NULL’ failed
(python3:20419): GStreamer-CRITICAL **: gst_element_get_state: assertion ‘GST_IS_ELEMENT (element)’ failed

もっとシンプルに入力だけにしてfakesinkに流したのですが、PAUSED…で止まってしまいます。

ホスト側では動くものがコンテナでは動かないので、原因を探るべくgst-inspectでnvcamerasrcの情報を取得したのですが、これは問題なさそう。

確認用コマンド(コンテナ側でも正常終了)

gst-inspect-1.0 nvcamerasrc

同じような事例がないかを調べていると、nvidiaのフォーラムにこんなスレッドがありました。
Accessing the onboard camera from a container

要約すると諦めてUSBカメラ使いなよという話になっています。
ただ、gst-inspectでは情報が拾えているので、この時点ではやりようがあるものと思っていました。

ここでかなりのタイムロスがあったのですが、仕事仲間よりこんな情報が送られてきました。

NVIDIA Container Runtime on Jetson – Supported Devices

In terms of camera input, USB cameras are supported, but CSI cameras are NOT. In order to access USB cameras from inside the container, the user needs to mount the device node that gets dynamically created when a camera is plugged in – eg: /dev/video0.

ざっくり訳すと、USBカメラはサポートされているがCSIカメラはサポートされてないのでUSBカメラをマウントして使えということです。

公式にそう言われると諦めるしかありません。

さて、ではどうするか。

  • ホスト側からTCP系の何らかのプロトコルを使って配信する
  • /dev/video1に丸ごと映像を流す

前者は軽く調べた限りなんとなくできそうですが遅延が大きそうです。
後者のやりようがあるのかを調べ始めました。

ハマりポイントその2

/dev/video1に映像を流すまでのあれこれ

ここはgstreamerに対する知識不足というところが大きかったのですが、この段階でもかなり時間を要しました。
ここから先は映像を拾えているホスト側で/dev/video1に映像を中継しようという試みです。

まずは、なぜ/dev/video1に映像を流そうと思ったかというと、

gst-device-monitor-1.0

を使って調べると、使用しているデバイスにはsinkインターフェースがあるdummy video deviceとして/dev/video1が設定されていたからです。
もしこのデバイスがない場合は、どう作ったら良いのかはまだ調べ切れていません。

ここからが明らかに自分の知識不足だったところです。
appsinkや画面表示に使うxvimagesink以外にどんなsinkが標準で用意されているのかを知らなかったので、プラグインディレクトリを見て一つずつ調べました。
デバイスに出力できそうなのは、xvimagesinkとfilesinkぐらい?
ドキュメントを見てもそれなりに簡素であまりにもわからなかったのでTwitterに投稿してみました。

するとfdsinkとか使えないかなとの意見をいただきました。
結論的にこれはダメでしたが、ファイルディスクリプタであればたしかに行けそうな気がしました。

ダメだったホスト側のコマンド

gst-launch-1.0 nvcamerasrc ! 'video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080, format=(string)I420, framerate=(fraction)30/1' ! nvvidconv ! video/x-raw, width=1920, height=1080, format=BGRx ! videoconvert ! fdsink fd=3 3>/dev/video1

実行結果

ERROR: from element /GstPipeline:pipeline0/GstFdSink:fdsink0: Could not write to resource.
Additional debug info:
gstelements_private.c(317): gst_writev_buffers (): /GstPipeline:pipeline0/GstFdSink:fdsink0:
Error while writing to file descriptor 3: Invalid argument

これはappsinkのコードとかをベースに作るしかないかなと思いつつ、いろいろなサイトを読み漁っていた時、/dev/video*から映像を取得するv4l2srcというものがあることに気づきます。
srcがあるということはsinkもあるのでは?と調べると、ありました!
しかもまさにこれ!というフォーラムのスレッドも発見。
passing gst-launch from nvcamerasrc to v4l2sink

v4l2sinkで/dev/video1に出力することができました。

うまく行ったホスト側のコマンド

gst-launch-1.0 nvcamerasrc ! 'video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080, format=(string)I420, framerate=(fraction)30/1' ! nvtee ! nvvidconv flip-method=2 ! 'video/x-raw, format=(string)I420, framerate=(fraction)30/1' ! tee ! v4l2sink device=/dev/video1

フォーラムに書かれているように、うまく動作していることを確認するためguvcviewをインストールします。

sudo apt install guvcview 
guvcview -d /dev/video1

ホスト側、コンテナ側ともに無事映像を取得することができました。

ハマりポイントその3

OpenCVから単純に/dev/video1の映像を取得できない

これ、まだ原因究明ができていないのですが、OpenCVから単純にvideo_source = 1を取ると1フレーム目は正しく処理されるのですが、次のフレームのcap.read()でエラーにすらならず全く応答が返ってこない状態になってしまいました。

で、とても回りくどい感じになってしまうのですが、コンテナ側での映像取得をv4l2srcで/dev/video1から取ってみました。

うまく動いたコンテナ側のコード

video_source = "v4l2src ! video/x-raw, framerate=30/1, width=1920, height=1080, format=BGRx ! videoconvert ! appsink"

ただし、かなりの処理遅延がありそう。
ということで、現在受け渡しのフォーマットをどうしたら遅延が最小限になるのかを探っています。

JetsonでCSIカメラ(nvcamerasrc)をdockerコンテナ内から使うにコメントする

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です