自由課題

学んだり、考えたり、試したりしたこと。

Jetson Nanoで自律走行ラジコンを作る(1)

「JetsonでFPVラジコンを作る」というシリーズ記事で、ラジコンにカメラを取り付けてbluetoothゲームパッドで操作できるようにした。
記事一覧はここ

今回からは上記の環境をベースにラジコンの自律走行にチャレンジしようと思う。現状システム(LunchJetと命名した)は以下のような感じである。

f:id:kimito_k:20210420072705p:plain

JetsonではDeep Leaningのフレームワークが使用できるので、DNNベースのモデルを実行する。
具体的には、車載カメラの映像と人間が操縦している際の制御値(ステアリングの方向やアクセル強度)をそれぞれ入出力とした教師あり学習のモデルを作成するつもりでシステムを組組んでみるつもりである。

f:id:kimito_k:20210505052059p:plain

汎化性能次第ではあるが、まずはライントレーサーのラインなし版ができれば上出来といったレベルではないだろうかと思っている。何をするにしてもまずは訓練データが必要なので、カメラ映像と対応する制御値をJetson内に収集する機能から作り込んでいくことにする。

現時点のソフトウェア構成は以下。矢印はデータの流れを表している。
<<gst-element>>はGStreamerのエレメントである。

f:id:kimito_k:20210505111508p:plain

現時点では、momoによりカメラ映像を配信する処理パスと、ゲームパッドの入力を元にモーターを制御するパスの2系統が存在する。

これを以下のようにする。

f:id:kimito_k:20210513053008p:plain

図が少しややこしくなったように見えるが、要は右下のRC car serverにカメラ映像を入力して、訓練データとしてラジコンの制御値と画像を所定のディレクトリに保存するようにしただけである。

めでたくモデルが出来上がって推論できるようになったら、ゲームパッドの代わりにモデルから出力された制御値をつかってラジコンを制御する。

これを実現するために、カメラ画像のストリーム上流(左側)にteeを挿入し、カメラ映像のストリームを複製して解像度やフレームレートを落としつつRC car serverに映像を入力する。その上で、RC car server上に実装を加えれば訓練データをJetsonのSDカードに保存できる(はずである)。

なお、teeの直後のqueueは分枝した2つのストリームを別スレッドで並列に動作させるために必要となる(Gsteamerのチュートリアルを参照)。

上記のデータフローを実現するGStreamerのコマンドは以下となる。かなり長くなった。

$ /usr/bin/gst-launch-1.0 -v nvarguscamerasrc ! 'video/x-raw(memory:NVMM), format=NV12, width=1280, height=720, framerate=30/1' ! tee name=t
 ! queue ! nvvidconv flip-method=2 ! 'video/x-raw, format=I420, width=1280, height=720' ! identity drop-allocation=1 ! v4l2sink device=/dev/video1
 t. ! queue ! nvvidconv flip-method=2 ! videorate ! 'video/x-raw, format=I420, width=360, height=240, framerate=15/1' ! identity drop-allocation=1 ! v4l2sink device=/dev/video2

teeをtという名前付きエレメントとして定義し、このエレメントにqueueを2つ連結している。

RC car serverで/dev/video2側のカメラ映像を取得・保存するのにOpenCVを使用した。
映像の取得にはcv::VideoCaptureクラス(ドキュメントはここ)を、各フレームの画像の保存にはcv::imwrite()(ドキュメントはここ)を使用した。

LunchJetでは、映像の取得はこのあたり

github.com

に、保存はこのあたり

github.com

に実装がある。いずれも非常に簡単である。

訓練データとして、

  • カメラ画像(JPEG)
  • カメラ画像のファイル名と制御値のセット(テキストファイル)

をJetsonのSDカードに保存することにする。
テキストファイルは以下のような簡単なフォーマットにした。

/var/log/lunchjet$ cat control/annotations/2021_05_12_08_22_17_434.log
2021_05_12_08_22_17_434.jpg 1.5259e-05 0.135875

一番目がカメラ画像のファイル名で、二番目・三番目がそれぞれステアリングと駆動モーターの制御値である。 ちなみにカメラ画像はミリ秒単位で時刻をファイル名としている。

ということで、ラジコンを適当に走らせていれば秒間15枚ずつ訓練データが蓄積されていくようになった。訓練データを5000枚収集するとすると、5〜6分遊んでいればよい。

次回以降で、Google Colabを使ってこの訓練データをネタにぼちぼちモデルを作ってみる。

「JetsonでFPVラジコンを作る」記事一覧

Jetson NanoでFPVラジコンを作る(1) - 自由課題
Jetson NanoでFPVラジコンを作る(2) - 自由課題
Jetson NanoでFPVラジコンを作る(3) - 自由課題
Jetson NanoでFPVラジコンを作る(4) - 自由課題
Jetson NanoでFPVラジコンを作る(5) - 自由課題
Jetson NanoでFPVラジコンを作る(6) - 自由課題
Jetson NanoでFPVラジコンを作る(7) - 自由課題
Jetson NanoでFPVラジコンを作る(8) - 自由課題
Jetson NanoでFPVラジコンを作る(9) - 自由課題
Jetson NanoでFPVラジコンを作る(10) - 自由課題

Jetson NanoでFPVラジコンを作る(10)

前回までの記事

この記事のコードは以下のGitリポジトリに公開している。(リポジトリ名変更済)

github.com

前回まででJetsonへの入出力に関するコードは書いたので、あとはこのように結合するだけである。

class RCCarServer : public RCCarControllerListener {
    public:
    RCCarServer() : controller("/dev/input/event2", *this, stop_controller_thread){}
    ~RCCarServer() = default;

    int start() {
        return controller.listen();
    }

    void on_connect() override {
        std::cout << "controller connected" << std::endl;
    };

    void on_change_steering(float value) override {
        driver.steer(value);
    }

    void on_change_accel(float value) override {
        if(is_going_back) {
            driver.back(value * 0.3f);
        }
        else {
            driver.go(value * 0.2f);
        }
    }

    void on_change_back(int value) override {
        if(value) {
            is_going_back = true;
        }
        else {
            is_going_back = false;
        }
    }

    void on_close() override {
        std::cout << "controller disconnected" << std::endl;
    }

    private:
    RCCarDriver driver;
    RCCarController controller;
    bool is_going_back = false;
};

のようなクラスを作って以下のように使用するだけ。

int main(int argc, const char *argv[])
{
    //snip

    RCCarServer server;

    std::cout << "starting server..." << std::endl;

    server.start();
    std::cout << "server started" << std::endl;

    pause();
    std::cout << "terminating server..." << std::endl;

    return 0;
}

ちなみにコントローラの入力値をそのまま駆動モーターに伝えてしまうと速度が出過ぎるので、ゲインは適当に下げてある。

ということでめでたくタミヤのラジコンがFPVで操作できるようになった。
のだが、実際に操作してみるとやはり画角が狭い。(下の動画は以前撮影したもの)

実はmomoで以下の広角ラズパイカメラを認識しなかったため、今までは仕方なくWebカメラを使用していた。

改めて調べてみたところ以下のツイートが見つかった。

mobile.twitter.com

情報を辿っていくとやり方がここに書いてあった。
ラズパイカメラはRG10というピクセルフォーマットを使用しているが、momoはこのピクセルフォーマットに対応していない。

そこでv4l2loopbackというkernel moduleを使用して仮想ビデオデバイスを作成し、このデバイスにRG10から変換したI420の映像を流し込むことでmomoが認識するらしい。

まず、ラズパイカメラをラジコンに取り付ける。
以前使用した1.7mmの透明プラバンと、東急ハンズで調達したL字のアクリル棒を加工してカメラ固定用のプレートを作成し、このプレート経由でカメラをベースプレートに固定した。車体にきちんと収めるための位置決めと、カメラ固定用プレートにフレキケーブルを通すための長方形の穴を加工するのに難儀した。

f:id:kimito_k:20210418154959j:plain

ボディを取り付けてもきちんと収まっている。

f:id:kimito_k:20210419081403j:plain

次にソフトウェア側の作業をする。
まずv4l2loopbackのビルド・インストール・有効化を行う。

$ git clone https://github.com/umlaeute/v4l2loopback.git
$ cd v4l2loopback
$ make && sudo make install
$ sudo depmod -a
$ sudo modprobe v4l2loopback exclusive_caps=1

これでまず仮想ビデオデバイスが見えるようになる。

$ ls /dev/video*
/dev/video0  /dev/video1

video0というのがラズパイカメラで、v4l2loopbackによりvideo1という仮想ビデオデバイスが追加されている。

ここで上記の情報の通りgstreamerのコマンドを実行し、ラズパイカメラの映像をRG10->I420に変換し、/dev/video1に流し込む*1。ついでに解像度を落として画面を180°回転*2させている。

$ sudo /usr/bin/gst-launch-1.0 -v nvarguscamerasrc ! 'video/x-raw(memory:NVMM), format=NV12, width=1280, height=720, framerate=30/1' ! nvvidconv flip-method=2 ! identity drop-allocation=1 ! 'video/x-raw, width=1280, height=720, format=I420, framerate=30/1' ! v4l2sink device=/dev/video1

こうすると、/dev/video1がI420(YU12)の入力デバイスとして見えるようになる。

$ v4l2-ctl -d 1 --all
Driver Info (not using libv4l2):
        Driver name   : v4l2 loopback
        Card type     : Dummy video device (0x0000)
        Bus info      : platform:v4l2loopback-000
        Driver version: 4.9.201
        Capabilities  : 0x85208003
                Video Capture
                Video Output
                Video Memory-to-Memory
                Read/Write
                Streaming
                Extended Pix Format
                Device Capabilities
        Device Caps   : 0x05208003
                Video Capture
                Video Output
                Video Memory-to-Memory
                Read/Write
                Streaming
                Extended Pix Format
Priority: 2
Video input : 0 (loopback: ok)
Video output: 0 (loopback in)
Format Video Capture:
        Width/Height      : 1280/720
        Pixel Format      : 'YU12'
        Field             : None
        Bytes per Line    : 1280
        Size Image        : 1382400
        Colorspace        : sRGB
        Transfer Function : sRGB
        YCbCr/HSV Encoding: ITU-R 601
        Quantization      : Limited Range
        Flags             : 
Format Video Output:
        Width/Height      : 1280/720
        Pixel Format      : 'YU12'
        Field             : None
        Bytes per Line    : 1280
        Size Image        : 1382400
        Colorspace        : sRGB
        Transfer Function : sRGB
        YCbCr/HSV Encoding: ITU-R 601
        Quantization      : Limited Range
        Flags             : 
Streaming Parameters Video Capture:
        Frames per second: 30.000 (30/1)
        Read buffers     : 2
Streaming Parameters Video Output:
        Frames per second: 30.000 (30/1)
        Write buffers    : 2

User Controls

                    keep_format 0x0098f900 (bool)   : default=0 value=0
              sustain_framerate 0x0098f901 (bool)   : default=0 value=0
                        timeout 0x0098f902 (int)    : min=0 max=100000 step=1 default=0 value=0
               timeout_image_io 0x0098f903 (bool)   : default=0 value=0

ここでmomoを以下のパラメーターで起動すると、めでたくmomo経由で映像が見えるようになる。

$ sudo /opt/lunchjet/momo/momo --hw-mjpeg-decoder=false --video-device /dev/video1 --no-audio-device test

ハマったのは--hw-mjpeg-decoder=falseという部分で、これがないとエラーでmomoが立ち上がらない。
原因がわからずに結局momoのソースまで確認したのだが、最近のmomoのJetson用ビルドではデフォルトで--hw-mjpeg-decoder=trueとなっており、この場合には対応ピクセルフォーマットがJPEGかMJPEGのみとなるようだ。

これでめでたくシステムの全機能が開通した。 システム構成は以下になった。

f:id:kimito_k:20210420072705p:plain

ゲームパッドでラジコンを操作している様子は以下。
少しアクセルの遊びが大きい気もするが、特に問題はない。

実際に走行させてみた。

しばらく運転して遊んでみたが、やはり視野が広い(視野角160°)ので運転しやすい。
レーサータイプのラジコンに比べおそらく視点がだいぶ高いせいか、思ったよりスピード感はない気もした。

というわけで、「Jetson NanoでFPVラジコンを作る」シリーズはこれでおしまい。
次回以降は改題して自動運転にぼちぼち取り組むつもりである。

前回までの記事

Jetson NanoでFPVラジコンを作る(1) - 自由課題
Jetson NanoでFPVラジコンを作る(2) - 自由課題
Jetson NanoでFPVラジコンを作る(3) - 自由課題
Jetson Nanoを使ってFPVラジコンを作る(4) - 自由課題
Jetson Nanoを使ってFPVラジコンを作る(5) - 自由課題
Jetson Nanoを使ってFPVラジコンを作る(6) - 自由課題
Jetson Nanoを使ってFPVラジコンを作る(7) - 自由課題
Jetson NanoでFPVラジコンを作る(8) - 自由課題
Jetson NanoでFPVラジコンを作る(9) - 自由課題

*1:オリジナルの記事のコマンドはもっと長いが、冗長に思える部分があったので改変して短くしている。ただ、性能としてはほとんど変わらないようだ

*2:Jetsonだとフレキケーブルのコネクタが上に来るようにカメラを配置すべきだったらしい。ラズパイとは上下逆?

Jetson NanoでFPVラジコンを作る(9)

前回までの記事

前回記事を書いたあとステアリングモーターと駆動モーターを制御するライブラリを書いた。

github.com

RCCar::steer(float)RCCar::go(float)を使ってこんな感じで制御できる。
両関数とも引数の値域は0.0〜1.0である。

int main(int argc, const char *argv[])
{
    RCCar car;

    //swing the steering
    for(float s = 0.f; s <= 3.f; s+=0.05f) {
        car.steer(std::sin(2*M_PI*s));
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }

    //stop and go
    for(float s = 0.f; s <= 2.f; s+=0.01f) {
        auto speed = 0.2/*max speed cap*/ * ((-std::cos(2*M_PI*s)+1)/2);
        car.go(speed);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    return 0;
}

今回はゲームパッドの入力を受け取るためのライブラリを書く。
現在使用しているゲームパッドは以下。

続きを読む

Jetson NanoでFPVラジコンを作る(8)

前回までの記事

相変わらずタミヤのランチボックスをJetson Nanoで制御しようとしている。

今のところシステム構成は以下。

f:id:kimito_k:20210403113108p:plain

前回まででハードの基本的な動作確認が終わったので、今回から本格的にコーディングを行う。
(あまりよく見ていないが)JetRacerDonkyCarPythonっぽいが、今回はC++を使うことにする(f:id:kimito_k:20210403113108p:plain)。

まだまともに何か動くレベルではないが*1、ぼちぼちgithubソースコードをpushしている。

github.com

今日はステアリングモーターと駆動モーターを制御するためのI2C通信の基本部分をコーディングする。

*1:momoを自動起動にしているだけ

続きを読む

Jetson NanoでFPVラジコンを作る(7)

前回までの記事

今回はラジコンを操縦するための入力機器をJetson Nanoに接続してみる。 もちろん入力機器を無線接続したいので、bluetooth接続のゲームパッドを使用することにする。

選定理由は、動作環境としてラズパイ(Linux)が入っていたこと、比較的品質が高そうなコメントが多かったこと、それからデザインがシンプルだったからである。

続きを読む

Jetson NanoでFPVラジコンを作る(6)

前回はJetson Nano単体でラジコンを制御するのに失敗したので、今回は PCA9685というモータードライバーのチップを使用して制御を試みる。
実績はたくさんあるので、できないということはないはずだ。

続きを読む