自由課題

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

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

「Jetson Nanoで自律走行ラジコンを作る」シリーズのまとめ記事は以下。

kimitok.hateblo.jp

モデルの訓練に必要なデータをGoogle Driveにアップロードできたので、ブラウザ上からPyhonが使える実行環境であるGoogle Colabを利用してモデルを作成しようと思う。

colab.research.google.com

今回は前回機体から収集したデータの内容を確認しつつTensorFlow上でデータセットを作成する。
やり方はTensorFlowのチュートリアルを参考にした。

続きを読む

「Jetson Nanoで自律走行ラジコンを作る」シリーズ 記事一覧

「Jetson Nanoで自律走行ラジコンを作る」の各記事のリンク・簡単な内容のまとめです。
記事を追加するたびに更新予定です。

続きを読む

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

前回記事は以下。

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

今回は、機内で収集したデータをGoogle Driveにアップロードするスクリプトを作成してみようと思う。今回はPythonで書く。

Google DriveにアクセスするためのAPIはDrive APIというらしく、APIの説明は以下にある。

developers.google.com

APIを使用したい場合はGCPのプロジェクトを作成していろいろな設定をする必要があるので、心を無にして言われた通りにする。クレデンシャル作成時にアプリケーションの種類を何となく"TVs and limited input device"にした*1

簡単な動作確認を行うためのスクリプトが用意されているので、これを実行する。

developers.google.com

上記を実行するには以下が必要だった。

$ sudo apt-get install python3-pip
$ pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib

スクリプトを実行すると、Google Drive内のファイルが10個表示される。ここまで確認できたら、最終的には何かデータをアップロードするので、いろいろ調べながら適当なファイルをアップロードするスクリプトを書いてみる。以下の公式ドキュメントが参考になった*2

Upload file data  |  Google Drive API  |  Google Developers
google-api-python-client/media.md at master · googleapis/google-api-python-client · GitHub
API Documentation
https://googleapis.github.io/google-api-python-client/docs/dyn/drive_v3.files.html

ちなみに、Google DriveAPIを使用するためには単に自分のGoogleアカウントのパスワードを用意するだけではダメで、GCP上にアプリケーションを作成する必要があるのでちょっと面倒ではある。

いろいろドキュメントを読んだり実際の動作を確認したところによると、少なくともこの記事執筆時点では、ファイルのアップロード速度はそれほど速くなく複数ファイルをバッチアップロードする機能もないように見える。今回のケースでは秒間30個のファイル*3が生成されるので、訓練データの生成時にリアルタイムで各ファイルをGoogle Driveにアップロードするのは無理そうだ。何となく走行中リアルタイムにデータをアップロードするのを想像してテンションを上げていたので残念である*4

仕方がないので、訓練データを収集し終わった時点で一旦データをtarballにして、このファイルをアップロードすることにする。Google Drive側でフックを掛けるか学習スクリプト内で前処理するかしてデータを展開する予定である。

tarballを作成するのは標準のtarfileパッケージを使えばよい。Pythonにはこのような基本的なものはバンドルされているのでちょっとしたものを作りやすい。

スクリプトは以下にpushしてある。

github.com

今回の実装追加でデータフローは以下のようになった。

f:id:kimito_k:20210523141018p:plain

Jetson NanoにSSHで接続してスクリプトを実行してもよいのだが、実行するコマンドはいつも同じなのでゲームパッドのSELECTボタンを押すと実行されるようにした。
あまり考えていないが、おそらく手動走行と自律走行の切り替えは何らか必要なので無駄になることはない気がする。

ということで、数分間自宅のリビングを適当に走行させて5500セットくらいの訓練データを収集・アップロードした。画像(360x240)は1枚20〜30KB程度で、展開後のデータサイズは総計200MB程度のようだった。

うまくいくか分からないが、次回はいよいよモデルを作り始めてみる。

*1:合っているかは不明

*2:ドキュメントがあまり整理されていないようで情報を見つけるのに正直かなり苦労した

*3:15FPSで1フレームごとにJPEGと制御データのファイルを作成する

*4:おそらくIoT関連のクラウドサービスを使用するようにすれば実現できそうだが、それはそれで多少手がかかりそうなので気が向いたらやってみることにする

Jetson NanoでFPVラジコンを作るシリーズ 記事一覧

「Jetson NanoでFPVラジコンを作るシリーズ」の各記事のリンク・簡単な内容のまとめです。

続きを読む

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;
}

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

続きを読む