ししかわです。
社員研修の一環で二足歩行ロボットを作って、競技会「Humanoid Autonomous Challenge(HAC)」に参加します。
M5Stackの画像認識モジュール「M5Stack UnitV2」をM5Stackのユニットとしてではなく単独で使い、ロボットを制御できるようにしよう!というマニアックな計画を進めています。
環境構築のゴールはUnitV2でカメラとの通信、ロボットとの通信ができることです。つまりカメラ画像からボールの位置やフィールドの形を認識したり、シリアル通信を使ってロボットに動作の指令を出したりできるようにします。
前回はクロスコンパイル環境を構築して、UnitV2で動作するプログラムを作れました。今回はOpenCVなど外部ライブラリの機能を使えるようにしていきます。
UnitV2のビルトイン機能のソースは公開されている
#UnitV2 の顔認識すごいな。完全な横顔も認識できる pic.twitter.com/Gj3pNhkRAL
— ししかわ/Shinya Ishikawa (@meganetaaan) July 28, 2021
UnitV2は購入してすぐに様々なカメラ機能が使えてとても便利です。たとえば次のような機能があります。
- カメラ画像のストリーミング
- 顔認識、個人の識別
- QRコード、バーコード認識
実はこれらの機能のソースコードは、GitHubリポジトリでUnitV2Frameworkという名前で公開されています。
自分で一からビルド環境を整えるのも良いですが、せっかくなので使えるものは使って時間を節約しましょう!ここからはUnitV2Frameworkのソースを出発点として機能を足していくことにします。
UnitV2Frameworkのソースを見る
CMake
前回使ったArm向けのコンパイラツールチェインがここでも使われています。ただしCMakeというビルドツールを介してビルドする点が異なります。
リポジトリ直下にCMakeスクリプトが定義されています。スクリプトの読み方の説明は省略します(公式ドキュメントを参照してください)。ここでやっていることはそんなに難しくなくて、使用するコンパイラを指定したり、ビルド済みの外部ライブラリとリンクしたり、ビルド対象を指定したりしているだけです。
外部ライブラリ
UnitV2Frameworkは次の外部ライブラリに依存しています。
なお、今後作りたいロボット制御アプリケーションは、認識処理にUnitV2Framework、ロボットの駆動にMAX−E1のライブラリを使います。全体の依存関係を図示すると次のようになります。
内部のソースコード
mainディレクトリには各ビルトイン機能に対応するサンプルコードが定義されています。各サンプルコードそれぞれにmain関数が定義されており、独立したバイナリになります。例えばcamera_stream.cppはUnitV2上でcamera_streamという名前のバイナリとして保存されています。
ビルトイン機能をブラウザから使う際は、Pythonのサーバプログラムserver.py
がこれらの機能の中から一つを選択して起動します。
srcディレクトリに内部で使うクラス群が定義されています。ソースコードの中身の説明は、次回以降必要に応じて書きますので今は眺めるだけで大丈夫です。
UnitV2FrameworkをDockerでビルドする
(2021/10/14追記)現在、UnitV2Frameworkに対して「PC向けのビルド設定を加える」などの改善を独自に加えています。これに伴いDockerコンテナの内容やビルドコマンドも若干変わっています。最新のビルド手順はリポジトリのREADMEを参照してください。
UnitV2Frameworkを、依存ライブラリを含めてビルドしてみます。ブログの読者が再現しやすくするために、必要なパッケージやライブラリはDockerイメージ内に準備していきます。私は普段VSCodeを使うので、VSCode devcontainerの機能を使います。

一式記述済みのDockerfileは以下です。このリポジトリをVSCodeで開き「Reopen in Container」を実行することで、コンテナが立ち上がります。
ここではDockerを使った環境構築と依存ライブラリのビルド方法を説明します。とりあえず動かせればよい!という人は読み飛ばして構いません。
後述のコマンドはDockerfileの記述から抜粋しますが、適宜読み替えて貰えれば非DockerのLinux環境でも動くはずです。
コンパイラツールチェインのインストール
前回紹介したとおりArmアーキテクチャ向けのコンパイラツールチェインをインストールします。apt-getコマンドを使います。Dockerイメージのサイズを小さく保つためのベストプラクティスとして--no-install-recommends
オプションをつけて余分なパッケージのインストールを抑制したり、インストールと同時にキャッシュの削除も行ったりしています。
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf \ && apt-get clean && rm -rf /var/lib/opt/lists/*
依存ライブラリの取得
依存ライブラリをまとめて取得します。
WORKDIR /external RUN git clone --depth 1 https://github.com/opencv/opencv.git \ && git clone --depth 1 https://github.com/opencv/opencv_contrib.git \ && git clone --depth 1 https://github.com/Tencent/ncnn.git \ && wget https://jaist.dl.sourceforge.net/project/zbar/zbar/0.10/zbar-0.10.tar.bz2 \ && tar -jxvf zbar-0.10.tar.bz2 \ && rm zbar-0.10.tar.bz2
それぞれ記事執筆時点での最新版を持ってきています。git cloneコマンドに--depth 1
オプションを付けることで最新のコミットのみ取得します。これもDockerイメージのサイズを抑えるのに役立ちます。
依存ライブラリのビルド
依存ライブラリを順番にビルドしていきます。ビルドコマンドはUnitV2FrameworkのREADMEの指示に従います。
まずOpenCVのビルドです。ビルドフラグCMAKE_TOOLCHAIN
でArmツールチェインを指定しています。またOpenCVは外部の追加機能が別リポジトリ(opencv_contrib)に分かれているため、ビルドオプションでリポジトリのパスを指定する必要があります。
# Build OpenCV with extra modules WORKDIR /external/opencv/platforms/linux/ RUN mkdir build \ && cd build \ && cmake -DCMAKE_TOOLCHAIN_FILE=../arm-gnueabi.toolchain.cmake -DOPENCV_EXTRA_MODULES_PATH=../../../../opencv_contrib/modules/ -DBUILD_LIST=tracking,imgcodecs,videoio,highgui,features2d,ml,xfeatures2d -DCMAKE_BUILD_TYPE=Release ../../.. \ && make \ && make install
続いてNCNNのビルドです。OpenCVと同じくツールチェインを指定する以外は特に注意することはありません。
WORKDIR /external/ncnn RUN mkdir build \ && cd build \ && cmake -DCMAKE_TOOLCHAIN_FILE=../toolchains/arm-linux-gnueabihf.toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DNCNN_VULKAN=OFF -DNCNN_BUILD_EXAMPLES=ON .. \ && make \ && make install
最後にzbarですが、元リポジトリのREADMEのコマンドそのままだとビルドに失敗します。環境変数NMを指定する、CFLAGSを空にするなどのおまじないが必要でした。
WORKDIR /external/zbar-0.10 RUN env NM=nm CFLAGS="" ./configure --prefix=$(pwd)/build --host=arm-none-linux-gnueabihf --build=x86_64-linux --enable-shared --without-gtk --without-python --without-qt --without-imagemagick --disable-video CC=arm-linux-gnueabihf-gcc CXX=arm-linux-gnueabihf-g++ \ && make \ && make install
フレームワークのビルド
ここまでで依存ライブラリの準備が完了しました。Dockerイメージをビルドして、コンテナを立ち上げてみましょう。
リポジトリをVSCodeで開き、コマンドパレット(Ctrl+Shift+P)から「Reopen in Container」と打ち込んで実行します。このときDockerイメージのビルド、コンテナ起動、ワークスペースのマウント等が自動で実行され、VSCodeがコンテナ内で再度開きます。
UnitV2FrameworkのCMakeで指定される通り、依存ライブラリの一部のファイルをlibディレクトリにコピーする必要があります。
$ cp /external/ncnn/build/install/lib/libncnn.a /workspaces/UnitV2Framework/lib/ $ cp /external/zbar-0.10/build/lib/libzbar.so /workspaces/UnitV2Framework/lib/
あとはプロジェクトのルートディレクトリでcmake
を実行します。先程ビルドしたOpenCVのパスと、ビルドするサンプルの名前をフラグで指定します。
cmake -DOPENCV_DIR=/external/opencv/platforms/linux/build -DTARGET=camera_stream . make
ビルドが成功するとbin
ディレクトリにexample
という名前のバイナリができているはずです。
ビルドしたUnitV2Frameworkを動かしてみる
エラーなくビルドできたら、次は動作確認です。カメラ映像のストリーミング(camera_stream)のサンプルを実際にUnitV2上で動かしてみましょう。ただし、元のサンプルと全く同じ動作だと、入れ替えたプログラムなのかどうかが分かりづらいため一箇所ソースを書き換えます。ここではカメラの画像をグレースケールに変換します。
UnitV2Framework/main/camera_stream.cpp
#include <opencv2/core/utility.hpp> #include <opencv2/tracking.hpp> #include <opencv2/highgui.hpp> // グレースケール変換のため追加でインクルード #include <opencv2/imgproc/types_c.h> #include "framework.h" using namespace cv; #define IMAGE_DIV 2 int main( int argc, char** argv ) { startFramework("Camera Stream", 640, 480); int low_quality_mode = false; while(1) { Mat mat_src; getMat(mat_src); /* 中略 */ // カメラの画像をグレースケールに変換する Mat gray_img; cvtColor(mat_src, gray_img, CV_BGR2GRAY); // クライアントに画像を送信する sendMat(gray_img); } return 0; }
再びビルドします。
cmake -DOPENCV_DIR=/external/opencv/platforms/linux/build -DTARGET=camera_stream . make
ビルドしたバイナリexample
をUnitV2に転送して、元のバイナリと交換してみます。
このとき元のバイナリを上書きしないように注意しましょう!先にUnitV2の/home/m5stack/payload
ディレクトリ毎バックアップを取るのが良いかもしれません。
# ホストPC上で実行 # 元のcamera_streamをcamera_stream.bakという名前に変更 $ ssh m5stack@unitv2.local 'mv /home/m5stack/payload/bin/camera_stream{,.bak}' # ビルドしたバイナリと差し替え $ scp ./bin/example m5stack@unitv2.local:/home/m5stack/payload/bin/camera_stream
なお、転送するのはサンプルコードのバイナリであるexample
だけで十分です。本来バイナリが動作するためには依存ライブラリも必要なのですが、すでにUnitV2側にインストール済みであるためそちらを代用します。
ビルトイン機能の自動起動を無効化しているので、手動でサーバを立ち上げます。
# UnitV2上で実行 unitv2% cd /home/m5stack/payload unitv2% sudo env LD_LIBRARY_PATH=/home/m5stack/payload/opencv4/lib:$LD_LIBRARY_PATH python server.py Password: /usr/lib/python3.8/subprocess.py:838: RuntimeWarning: line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used /usr/lib/python3.8/subprocess.py:844: RuntimeWarning: line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used server_core.py:3: DeprecationWarning: The import 'werkzeug.secure_filename' is deprecated and will be removed in Werkzeug 1.0. Use 'from werkzeug.utils import secure_filename' instead. from werkzeug import secure_filename rm: can't remove './uploads/temp/*': No such file or directory 2021-08-10 04:36:35,060 - [line:795] - INFO: Server PID = 1158, Core PID = 1159 /usr/lib/python3.8/subprocess.py:838: RuntimeWarning: line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used /usr/lib/python3.8/subprocess.py:844: RuntimeWarning: line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used 2021-08-10 04:36:35,072 - [line:833] - INFO: Wait for process to start
最後にホストPCのブラウザからunitv2.local
にアクセスします。白黒のカメラ画像が表示されれば成功です!
以上です。開発環境を頑張ってDocker化してみたのでぜひ試してみてください。既存のコードのバックアップを取るのをお忘れなく!
次回はフレームワークに修正を加えていき、いよいよUnitV2からロボットを動かします!
→次の記事
